payload icon indicating copy to clipboard operation
payload copied to clipboard

feat: expose `session` / `db` in migrations to use the active transaction with the database directly

Open r1tsuu opened this issue 2 months ago • 0 comments

This PR adds a feature which fixes another issue with migrations in Postgres and does few refactors that significantly reduce code duplication.

Previously, if you needed to use the underlying database directly in migrations with the active transaction (for example to execute raw SQL), created from payload create:migration, as req doesn't work there you had to do something like this:

// Postgres
export async function up({ payload, req }: MigrateUpArgs): Promise<void> {
  const db = payload.db.sessions?.[await req.transactionID!].db ?? payload.db
  const { rows: posts } = await db.execute(sql`SELECT * from posts`)
}

// MongoDB
export async function up({ payload, req }: MigrateUpArgs): Promise<void> {
  const session = payload.db.sessions?.[await req.transactionID!]
  const posts = await payload.db.collections.posts.collection.find({ session }).toArray()
}

Which was:

  1. Awkward to write
  2. Not documented anywhere

Now, we expose session and db to up and down functions for you:

MongoDB:

import { type MigrateUpArgs } from '@payloadcms/db-mongodb'

export async function up({ session, payload, req }: MigrateUpArgs): Promise<void> {
  const posts = await payload.db.collections.posts.collection.find({ session }).toArray()
}

Postgres:

import { type MigrateUpArgs, sql } from '@payloadcms/db-postgres'

export async function up({ db, payload, req }: MigrateUpArgs): Promise<void> {
  const { rows: posts } = await db.execute(sql`SELECT * from posts`)
}

SQLite:

import { type MigrateUpArgs, sql } from '@payloadcms/db-sqlite'

export async function up({ db, payload, req }: MigrateUpArgs): Promise<void> {
  const { rows: posts } = await db.run(sql`SELECT * from posts`)
}

This actually was a thing with Postgres migrations, we already were passing db, but:

  1. Only for up and when running payload migrate, not for example with payload migrate:fresh
  2. Not documented neither in TypeScript or docs.

By ensuring we use db, this also fixes an issue that affects all Postgres/SQLite migrations:

Currently, if we run payload migration:create with the postgres adapter we get a file like this:

import { MigrateUpArgs, MigrateDownArgs, sql } from '@payloadcms/db-postgres'

export async function up({ payload, req }: MigrateUpArgs): Promise<void> {
  await payload.db.drizzle.execute(sql`
   CREATE TABLE IF NOT EXISTS "users" (
  	"id" serial PRIMARY KEY NOT NULL,
  );

Looks good? Not exactly! payload.db.drizzle.execute() doesn't really use the current transaction which can lead to some problems. Instead, it should use the db from payload.db.sessions?.[await req.transactionID!].db because that's where we store our Drizzle instance with the transaction.

But now, if we generate payload migrate:create we get:

import { MigrateUpArgs, MigrateDownArgs, sql } from '@payloadcms/db-postgres'

export async function up({ db, payload, req }: MigrateUpArgs): Promise<void> {
  await db.execute(sql`
   CREATE TABLE IF NOT EXISTS "users" (
  	"id" serial PRIMARY KEY NOT NULL,
  );

Which is what we want, as the db is passed correctly here: https://github.com/payloadcms/payload/blob/76428373e4ad94ec29dd58ee293a68b0b67429dc/packages/drizzle/src/migrate.ts#L88-L90

export async function up({ db, payload, req }: MigrateUpArgs): Promise<void> {
  const dbWithTransaction = payload.db.sessions?.[await req.transactionID!].db
  payload.logger.info({ one: db === dbWithTransaction })
  payload.logger.info({ two: db === payload.db.drizzle })
image

Additionally, this PR refactors:

  • createMigration with Drizzle - now we have sharable buildCreateMigration in @payloadcms/drizzle to reduce copy-pasting of the same logic.
  • the v2-v3 relationships migration for Postgres is now shared between db-postgres and db-vercel-postgres, again to reduce copy-paste.

r1tsuu avatar Dec 10 '24 03:12 r1tsuu