Skip to content

Commit 22f4967

Browse files
authored
Merge pull request #4726 from payloadcms/feat/migration-transactions
feat: improve transaction support by passing req to migrations
2 parents 4873c36 + 555d027 commit 22f4967

File tree

15 files changed

+140
-118
lines changed

15 files changed

+140
-118
lines changed

docs/database/migrations.mdx

Lines changed: 22 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,8 @@ keywords: database, migrations, ddl, sql, mongodb, postgres, documentation, Cont
66
desc: Payload features first-party database migrations all done in TypeScript.
77
---
88

9-
Payload exposes a full suite of migration controls available for your use. Migration commands are accessible via the `npm run payload` command in your project directory.
9+
Payload exposes a full suite of migration controls available for your use. Migration commands are accessible via
10+
the `npm run payload` command in your project directory.
1011

1112
Ensure you have an npm script called "payload" in your `package.json` file.
1213

@@ -24,33 +25,42 @@ Ensure you have an npm script called "payload" in your `package.json` file.
2425

2526
### Migration file contents
2627

27-
Payload stores all created migrations in a folder that you can specify. By default, migrations are stored in `./src/migrations`.
28+
Payload stores all created migrations in a folder that you can specify. By default, migrations are stored
29+
in `./src/migrations`.
2830

29-
A migration file has two exports - an `up` function, which is called when a migration is executed, and a `down` function that will be called if for some reason the migration fails to complete successfully. The `up` function should contain all changes that you attempt to make within the migration, and the `down` should ideally revert any changes you make.
31+
A migration file has two exports - an `up` function, which is called when a migration is executed, and a `down` function
32+
that will be called if for some reason the migration fails to complete successfully. The `up` function should contain
33+
all changes that you attempt to make within the migration, and the `down` should ideally revert any changes you make.
3034

31-
For an added level of safety, migrations should leverage Payload [transactions](/docs/database/transactions).
35+
For an added level of safety, migrations should leverage Payload [transactions](/docs/database/transactions). Migration
36+
functions should make use of the `req` by adding it to the arguments of your payload local API calls such
37+
as `payload.create` and database adapter methods like `payload.db.create`.
3238

3339
Here is an example migration file:
3440

3541
```ts
3642
import { MigrateUpArgs, MigrateDownArgs } from '@payloadcms/your-db-adapter'
3743

38-
export async function up({ payload }: MigrateUpArgs): Promise<void> {
44+
export async function up ({ payload, req }: MigrateUpArgs): Promise<void> {
3945
// Perform changes to your database here.
4046
// You have access to `payload` as an argument, and
4147
// everything is done in TypeScript.
4248
};
4349

44-
export async function down({ payload }: MigrateDownArgs): Promise<void> {
50+
export async function down ({ payload, req }: MigrateDownArgs): Promise<void> {
4551
// Do whatever you need to revert changes if the `up` function fails
4652
};
4753
```
4854

4955
### Migrations Directory
5056

51-
Each DB adapter has an optional property `migrationDir` where you can override where you want your migrations to be stored/read. If this is not specified, Payload will check the default and possibly make a best effort to find your migrations directory by searching in common locations ie. `./src/migrations`, `./dist/migrations`, `./migrations`, etc.
57+
Each DB adapter has an optional property `migrationDir` where you can override where you want your migrations to be
58+
stored/read. If this is not specified, Payload will check the default and possibly make a best effort to find your
59+
migrations directory by searching in common locations ie. `./src/migrations`, `./dist/migrations`, `./migrations`, etc.
5260

53-
All database adapters should implement similar migration patterns, but there will be small differences based on the adapter and its specific needs. Below is a list of all migration commands that should be supported by your database adapter.
61+
All database adapters should implement similar migration patterns, but there will be small differences based on the
62+
adapter and its specific needs. Below is a list of all migration commands that should be supported by your database
63+
adapter.
5464

5565
## Commands
5666

@@ -64,15 +74,17 @@ npm run payload migrate
6474

6575
### Create
6676

67-
Create a new migration file in the migrations directory. You can optionally name the migration that will be created. By default, migrations will be named using a timestamp.
77+
Create a new migration file in the migrations directory. You can optionally name the migration that will be created. By
78+
default, migrations will be named using a timestamp.
6879

6980
```text
7081
npm run payload migrate:create optional-name-here
7182
```
7283

7384
### Status
7485

75-
The `migrate:status` command will check the status of migrations and output a table of which migrations have been run, and which migrations have not yet run.
86+
The `migrate:status` command will check the status of migrations and output a table of which migrations have been run,
87+
and which migrations have not yet run.
7688

7789
`payload migrate:status`
7890

packages/db-mongodb/src/migrateFresh.ts

Lines changed: 12 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,9 @@
11
import type { PayloadRequest } from 'payload/types'
22

33
import { readMigrationFiles } from 'payload/database'
4+
import { commitTransaction } from 'payload/dist/utilities/commitTransaction'
5+
import { initTransaction } from 'payload/dist/utilities/initTransaction'
6+
import { killTransaction } from 'payload/dist/utilities/killTransaction'
47
import prompts from 'prompts'
58

69
import type { MongooseAdapter } from '.'
@@ -14,9 +17,9 @@ export async function migrateFresh(this: MongooseAdapter): Promise<void> {
1417
const { confirm: acceptWarning } = await prompts(
1518
{
1619
name: 'confirm',
20+
type: 'confirm',
1721
initial: false,
1822
message: `WARNING: This will drop your database and run all migrations. Are you sure you want to proceed?`,
19-
type: 'confirm',
2023
},
2124
{
2225
onCancel: () => {
@@ -40,29 +43,29 @@ export async function migrateFresh(this: MongooseAdapter): Promise<void> {
4043
msg: `Found ${migrationFiles.length} migration files.`,
4144
})
4245

43-
let transactionID
46+
const req = {} as PayloadRequest
47+
4448
// Run all migrate up
4549
for (const migration of migrationFiles) {
4650
payload.logger.info({ msg: `Migrating: ${migration.name}` })
4751
try {
4852
const start = Date.now()
49-
transactionID = await this.beginTransaction()
50-
await migration.up({ payload })
53+
await initTransaction(req)
54+
await migration.up({ payload, req })
5155
await payload.create({
5256
collection: 'payload-migrations',
5357
data: {
5458
name: migration.name,
5559
batch: 1,
5660
},
57-
req: {
58-
transactionID,
59-
} as PayloadRequest,
61+
req,
6062
})
61-
await this.commitTransaction(transactionID)
63+
64+
await commitTransaction(req)
6265

6366
payload.logger.info({ msg: `Migrated: ${migration.name} (${Date.now() - start}ms)` })
6467
} catch (err: unknown) {
65-
await this.rollbackTransaction(transactionID)
68+
await killTransaction(req)
6669
payload.logger.error({
6770
err,
6871
msg: `Error running migration ${migration.name}. Rolling back.`,

packages/db-postgres/src/migrate.ts

Lines changed: 11 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,12 @@
11
/* eslint-disable no-restricted-syntax, no-await-in-loop */
22
import type { Payload } from 'payload'
33
import type { Migration } from 'payload/database'
4+
import type { PayloadRequest } from 'payload/dist/express/types'
45

56
import { readMigrationFiles } from 'payload/database'
7+
import { commitTransaction } from 'payload/dist/utilities/commitTransaction'
8+
import { initTransaction } from 'payload/dist/utilities/initTransaction'
9+
import { killTransaction } from 'payload/dist/utilities/killTransaction'
610
import prompts from 'prompts'
711

812
import type { PostgresAdapter } from './types'
@@ -42,11 +46,11 @@ export async function migrate(this: PostgresAdapter): Promise<void> {
4246
const { confirm: runMigrations } = await prompts(
4347
{
4448
name: 'confirm',
49+
type: 'confirm',
4550
initial: false,
4651
message:
4752
"It looks like you've run Payload in dev mode, meaning you've dynamically pushed changes to your database.\n\n" +
4853
"If you'd like to run migrations, data loss will occur. Would you like to proceed?",
49-
type: 'confirm',
5054
},
5155
{
5256
onCancel: () => {
@@ -79,14 +83,16 @@ async function runMigrationFile(payload: Payload, migration: Migration, batch: n
7983
const { generateDrizzleJson } = require('drizzle-kit/utils')
8084

8185
const start = Date.now()
86+
const req = {} as PayloadRequest
8287

8388
payload.logger.info({ msg: `Migrating: ${migration.name}` })
8489

8590
const pgAdapter = payload.db
8691
const drizzleJSON = generateDrizzleJson(pgAdapter.schema)
8792

8893
try {
89-
await migration.up({ payload })
94+
await initTransaction(req)
95+
await migration.up({ payload, req })
9096
payload.logger.info({ msg: `Migrated: ${migration.name} (${Date.now() - start}ms)` })
9197
await payload.create({
9298
collection: 'payload-migrations',
@@ -95,8 +101,11 @@ async function runMigrationFile(payload: Payload, migration: Migration, batch: n
95101
batch,
96102
schema: drizzleJSON,
97103
},
104+
req,
98105
})
106+
await commitTransaction(req)
99107
} catch (err: unknown) {
108+
await killTransaction(req)
100109
payload.logger.error({
101110
err,
102111
msg: parseError(err, `Error running migration ${migration.name}`),

packages/db-postgres/src/migrateDown.ts

Lines changed: 9 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,9 @@
22
import type { PayloadRequest } from 'payload/types'
33

44
import { getMigrations, readMigrationFiles } from 'payload/database'
5+
import { commitTransaction } from 'payload/dist/utilities/commitTransaction'
6+
import { initTransaction } from 'payload/dist/utilities/initTransaction'
7+
import { killTransaction } from 'payload/dist/utilities/killTransaction'
58

69
import type { PostgresAdapter } from './types'
710

@@ -32,12 +35,12 @@ export async function migrateDown(this: PostgresAdapter): Promise<void> {
3235
}
3336

3437
const start = Date.now()
35-
let transactionID
38+
const req = {} as PayloadRequest
3639

3740
try {
3841
payload.logger.info({ msg: `Migrating down: ${migrationFile.name}` })
39-
transactionID = await this.beginTransaction()
40-
await migrationFile.down({ payload })
42+
await initTransaction(req)
43+
await migrationFile.down({ payload, req })
4144
payload.logger.info({
4245
msg: `Migrated down: ${migrationFile.name} (${Date.now() - start}ms)`,
4346
})
@@ -47,15 +50,13 @@ export async function migrateDown(this: PostgresAdapter): Promise<void> {
4750
await payload.delete({
4851
id: migration.id,
4952
collection: 'payload-migrations',
50-
req: {
51-
transactionID,
52-
} as PayloadRequest,
53+
req,
5354
})
5455
}
5556

56-
await this.commitTransaction(transactionID)
57+
await commitTransaction(req)
5758
} catch (err: unknown) {
58-
await this.rollbackTransaction(transactionID)
59+
await killTransaction(req)
5960

6061
payload.logger.error({
6162
err,

packages/db-postgres/src/migrateFresh.ts

Lines changed: 12 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,9 @@ import type { PayloadRequest } from 'payload/types'
22

33
import { sql } from 'drizzle-orm'
44
import { readMigrationFiles } from 'payload/database'
5+
import { commitTransaction } from 'payload/dist/utilities/commitTransaction'
6+
import { initTransaction } from 'payload/dist/utilities/initTransaction'
7+
import { killTransaction } from 'payload/dist/utilities/killTransaction'
58
import prompts from 'prompts'
69

710
import type { PostgresAdapter } from './types'
@@ -17,9 +20,9 @@ export async function migrateFresh(this: PostgresAdapter): Promise<void> {
1720
const { confirm: acceptWarning } = await prompts(
1821
{
1922
name: 'confirm',
23+
type: 'confirm',
2024
initial: false,
2125
message: `WARNING: This will drop your database and run all migrations. Are you sure you want to proceed?`,
22-
type: 'confirm',
2326
},
2427
{
2528
onCancel: () => {
@@ -36,36 +39,35 @@ export async function migrateFresh(this: PostgresAdapter): Promise<void> {
3639
msg: `Dropping database.`,
3740
})
3841

39-
await this.drizzle.execute(sql`drop schema public cascade;\ncreate schema public;`)
42+
await this.drizzle.execute(sql`drop schema public cascade;
43+
create schema public;`)
4044

4145
const migrationFiles = await readMigrationFiles({ payload })
4246
payload.logger.debug({
4347
msg: `Found ${migrationFiles.length} migration files.`,
4448
})
4549

46-
let transactionID
50+
const req = {} as PayloadRequest
4751
// Run all migrate up
4852
for (const migration of migrationFiles) {
4953
payload.logger.info({ msg: `Migrating: ${migration.name}` })
5054
try {
5155
const start = Date.now()
52-
transactionID = await this.beginTransaction()
53-
await migration.up({ payload })
56+
await initTransaction(req)
57+
await migration.up({ payload, req })
5458
await payload.create({
5559
collection: 'payload-migrations',
5660
data: {
5761
name: migration.name,
5862
batch: 1,
5963
},
60-
req: {
61-
transactionID,
62-
} as PayloadRequest,
64+
req,
6365
})
64-
await this.commitTransaction(transactionID)
66+
await commitTransaction(req)
6567

6668
payload.logger.info({ msg: `Migrated: ${migration.name} (${Date.now() - start}ms)` })
6769
} catch (err: unknown) {
68-
await this.rollbackTransaction(transactionID)
70+
await killTransaction(req)
6971
payload.logger.error({
7072
err,
7173
msg: parseError(err, `Error running migration ${migration.name}. Rolling back`),

packages/db-postgres/src/migrateRefresh.ts

Lines changed: 14 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,9 @@
22
import type { PayloadRequest } from 'payload/types'
33

44
import { getMigrations, readMigrationFiles } from 'payload/database'
5-
import { DatabaseError } from 'pg'
5+
import { commitTransaction } from 'payload/dist/utilities/commitTransaction'
6+
import { initTransaction } from 'payload/dist/utilities/initTransaction'
7+
import { killTransaction } from 'payload/dist/utilities/killTransaction'
68

79
import type { PostgresAdapter } from './types'
810

@@ -29,7 +31,7 @@ export async function migrateRefresh(this: PostgresAdapter) {
2931
msg: `Rolling back batch ${latestBatch} consisting of ${existingMigrations.length} migration(s).`,
3032
})
3133

32-
let transactionID
34+
const req = {} as PayloadRequest
3335

3436
// Reverse order of migrations to rollback
3537
existingMigrations.reverse()
@@ -43,8 +45,8 @@ export async function migrateRefresh(this: PostgresAdapter) {
4345

4446
payload.logger.info({ msg: `Migrating down: ${migration.name}` })
4547
const start = Date.now()
46-
transactionID = await this.beginTransaction()
47-
await migrationFile.down({ payload })
48+
await initTransaction(req)
49+
await migrationFile.down({ payload, req })
4850
payload.logger.info({
4951
msg: `Migrated down: ${migration.name} (${Date.now() - start}ms)`,
5052
})
@@ -53,18 +55,17 @@ export async function migrateRefresh(this: PostgresAdapter) {
5355
if (tableExists) {
5456
await payload.delete({
5557
collection: 'payload-migrations',
56-
req: {
57-
transactionID,
58-
} as PayloadRequest,
58+
req,
5959
where: {
6060
name: {
6161
equals: migration.name,
6262
},
6363
},
6464
})
6565
}
66+
await commitTransaction(req)
6667
} catch (err: unknown) {
67-
await this.rollbackTransaction(transactionID)
68+
await killTransaction(req)
6869
payload.logger.error({
6970
err,
7071
msg: parseError(err, `Error running migration ${migration.name}. Rolling back.`),
@@ -78,23 +79,21 @@ export async function migrateRefresh(this: PostgresAdapter) {
7879
payload.logger.info({ msg: `Migrating: ${migration.name}` })
7980
try {
8081
const start = Date.now()
81-
transactionID = await this.beginTransaction()
82-
await migration.up({ payload })
82+
await initTransaction(req)
83+
await migration.up({ payload, req })
8384
await payload.create({
8485
collection: 'payload-migrations',
8586
data: {
8687
name: migration.name,
8788
executed: true,
8889
},
89-
req: {
90-
transactionID,
91-
} as PayloadRequest,
90+
req,
9291
})
93-
await this.commitTransaction(transactionID)
92+
await commitTransaction(req)
9493

9594
payload.logger.info({ msg: `Migrated: ${migration.name} (${Date.now() - start}ms)` })
9695
} catch (err: unknown) {
97-
await this.rollbackTransaction(transactionID)
96+
await killTransaction(req)
9897
payload.logger.error({
9998
err,
10099
msg: parseError(err, `Error running migration ${migration.name}. Rolling back.`),

0 commit comments

Comments
 (0)