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:
```ts
// 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:
```ts
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:
```ts
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:
```ts
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:
```ts
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:
```ts
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:
76428373e4/packages/drizzle/src/migrate.ts (L88-L90)
```ts
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 })
```
<img width="336" alt="image"
src="https://github.com/user-attachments/assets/f9fab5a9-44c2-44a9-95dd-8e5cf267f027">
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.
138 lines
5.3 KiB
Plaintext
138 lines
5.3 KiB
Plaintext
---
|
|
title: Transactions
|
|
label: Transactions
|
|
order: 30
|
|
keywords: database, transactions, sql, mongodb, postgres, documentation, Content Management System, cms, headless, typescript, node, react, nextjs
|
|
desc: Database transactions are fully supported within Payload.
|
|
---
|
|
|
|
Database transactions allow your application to make a series of database changes in an all-or-nothing commit. Consider an HTTP request that creates a new **Order** and has an `afterChange` hook to update the stock count of related **Items**. If an error occurs when updating an **Item** and an HTTP error is returned to the user, you would not want the new **Order** to be persisted or any other items to be changed either. This kind of interaction with the database is handled seamlessly with transactions.
|
|
|
|
By default, Payload will use transactions for all data changing operations, as long as it is supported by the configured database. Database changes are contained within all Payload operations and any errors thrown will result in all changes being rolled back without being committed. When transactions are not supported by the database, Payload will continue to operate as expected without them.
|
|
|
|
<Banner type="info">
|
|
<strong>Note:</strong>
|
|
<br />
|
|
MongoDB requires a connection to a replicaset in order to make use of transactions.
|
|
</Banner>
|
|
|
|
<Banner type="info">
|
|
<strong>Note:</strong>
|
|
<br />
|
|
Transactions in SQLite are disabled by default. You need to pass `transactionOptions: {}` to enable them.
|
|
</Banner>
|
|
|
|
The initial request made to Payload will begin a new transaction and attach it to the `req.transactionID`. If you have a `hook` that interacts with the database, you can opt in to using the same transaction by passing the `req` in the arguments. For example:
|
|
|
|
```ts
|
|
const afterChange: CollectionAfterChangeHook = async ({ req }) => {
|
|
// because req.transactionID is assigned from Payload and passed through,
|
|
// my-slug will only persist if the entire request is successful
|
|
await req.payload.create({
|
|
req,
|
|
collection: 'my-slug',
|
|
data: {
|
|
some: 'data',
|
|
},
|
|
})
|
|
}
|
|
```
|
|
|
|
## Async Hooks with Transactions
|
|
|
|
Since Payload hooks can be async and be written to not await the result, it is possible to have an incorrect success response returned on a request that is rolled back. If you have a hook where you do not `await` the result, then you should **not** pass the `req.transactionID`.
|
|
|
|
```ts
|
|
const afterChange: CollectionAfterChangeHook = async ({ req }) => {
|
|
// WARNING: an async call made with the same req, but NOT awaited,
|
|
// may fail resulting in an OK response being returned with response data that is not committed
|
|
const dangerouslyIgnoreAsync = req.payload.create({
|
|
req,
|
|
collection: 'my-slug',
|
|
data: {
|
|
some: 'other data',
|
|
},
|
|
})
|
|
|
|
// Should this call fail, it will not rollback other changes
|
|
// because the req (and its transactionID) is not passed through
|
|
const safelyIgnoredAsync = req.payload.create({
|
|
collection: 'my-slug',
|
|
data: {
|
|
some: 'other data',
|
|
},
|
|
})
|
|
}
|
|
```
|
|
|
|
## Direct Transaction Access
|
|
|
|
When writing your own scripts or custom endpoints, you may wish to have direct control over transactions. This is useful for interacting with your database outside of Payload's local API.
|
|
|
|
The following functions can be used for managing transactions:
|
|
|
|
- `payload.db.beginTransaction` - Starts a new session and returns a transaction ID for use in other Payload Local API calls.
|
|
- `payload.db.commitTransaction` - Takes the identifier for the transaction, finalizes any changes.
|
|
- `payload.db.rollbackTransaction` - Takes the identifier for the transaction, discards any changes.
|
|
|
|
Payload uses the `req` object to pass the transaction ID through to the database adapter. If you are not using the `req` object, you can make a new object to pass the transaction ID directly to database adapter methods and local API calls.
|
|
Example:
|
|
|
|
```ts
|
|
import payload from 'payload'
|
|
import config from './payload.config'
|
|
|
|
const standalonePayloadScript = async () => {
|
|
// initialize Payload
|
|
await payload.init({ config })
|
|
|
|
const transactionID = await payload.db.beginTransaction()
|
|
|
|
try {
|
|
// Make an update using the local API
|
|
await payload.update({
|
|
collection: 'posts',
|
|
data: {
|
|
some: 'data',
|
|
},
|
|
where: {
|
|
slug: { equals: 'my-slug' }
|
|
},
|
|
req: { transactionID },
|
|
})
|
|
|
|
/*
|
|
You can make additional db changes or run other functions
|
|
that need to be committed on an all or nothing basis
|
|
*/
|
|
|
|
// Commit the transaction
|
|
await payload.db.commitTransaction(transactionID)
|
|
} catch (error) {
|
|
// Rollback the transaction
|
|
await payload.db.rollbackTransaction(transactionID)
|
|
}
|
|
}
|
|
|
|
standalonePayloadScript()
|
|
```
|
|
|
|
## Disabling Transactions
|
|
|
|
If you wish to disable transactions entirely, you can do so by passing `false` as the `transactionOptions` in your database adapter configuration. All the official Payload database adapters support this option.
|
|
|
|
In addition to allowing database transactions to be disabled at the adapter level. You can prevent Payload from using a transaction in direct calls to the local API by adding `disableTransaction: true` to the args. For example:
|
|
|
|
```ts
|
|
await payload.update({
|
|
collection: 'posts',
|
|
data: {
|
|
some: 'data',
|
|
},
|
|
where: {
|
|
slug: { equals: 'my-slug' }
|
|
},
|
|
disableTransaction: true,
|
|
})
|
|
```
|