210 lines
4.9 KiB
Markdown
210 lines
4.9 KiB
Markdown
---
|
|
title: Database Adapters & Transactions
|
|
description: Database adapters, storage, email, and transaction patterns
|
|
tags: [payload, database, mongodb, postgres, sqlite, transactions]
|
|
---
|
|
|
|
# Payload CMS Adapters
|
|
|
|
## Database Adapters
|
|
|
|
### MongoDB
|
|
|
|
```typescript
|
|
import { mongooseAdapter } from '@payloadcms/db-mongodb'
|
|
|
|
export default buildConfig({
|
|
db: mongooseAdapter({
|
|
url: process.env.DATABASE_URL,
|
|
}),
|
|
})
|
|
```
|
|
|
|
### Postgres
|
|
|
|
```typescript
|
|
import { postgresAdapter } from '@payloadcms/db-postgres'
|
|
|
|
export default buildConfig({
|
|
db: postgresAdapter({
|
|
pool: {
|
|
connectionString: process.env.DATABASE_URL,
|
|
},
|
|
push: false, // Don't auto-push schema changes
|
|
migrationDir: './migrations',
|
|
}),
|
|
})
|
|
```
|
|
|
|
### SQLite
|
|
|
|
```typescript
|
|
import { sqliteAdapter } from '@payloadcms/db-sqlite'
|
|
|
|
export default buildConfig({
|
|
db: sqliteAdapter({
|
|
client: {
|
|
url: 'file:./payload.db',
|
|
},
|
|
transactionOptions: {}, // Enable transactions (disabled by default)
|
|
}),
|
|
})
|
|
```
|
|
|
|
## Transactions
|
|
|
|
Payload automatically uses transactions for all-or-nothing database operations.
|
|
|
|
### Threading req Through Operations
|
|
|
|
**CRITICAL**: When performing nested operations in hooks, always pass `req` to maintain transaction context.
|
|
|
|
```typescript
|
|
// ✅ CORRECT: Thread req through nested operations
|
|
const resaveChildren: CollectionAfterChangeHook = async ({ collection, doc, req }) => {
|
|
// Find children - pass req
|
|
const children = await req.payload.find({
|
|
collection: 'children',
|
|
where: { parent: { equals: doc.id } },
|
|
req, // Maintains transaction context
|
|
})
|
|
|
|
// Update each child - pass req
|
|
for (const child of children.docs) {
|
|
await req.payload.update({
|
|
id: child.id,
|
|
collection: 'children',
|
|
data: { updatedField: 'value' },
|
|
req, // Same transaction as parent operation
|
|
})
|
|
}
|
|
}
|
|
|
|
// ❌ WRONG: Missing req breaks transaction
|
|
const brokenHook: CollectionAfterChangeHook = async ({ collection, doc, req }) => {
|
|
const children = await req.payload.find({
|
|
collection: 'children',
|
|
where: { parent: { equals: doc.id } },
|
|
// Missing req - separate transaction or no transaction
|
|
})
|
|
|
|
for (const child of children.docs) {
|
|
await req.payload.update({
|
|
id: child.id,
|
|
collection: 'children',
|
|
data: { updatedField: 'value' },
|
|
// Missing req - if parent operation fails, these updates persist
|
|
})
|
|
}
|
|
}
|
|
```
|
|
|
|
**Why This Matters:**
|
|
|
|
- **MongoDB (with replica sets)**: Creates atomic session across operations
|
|
- **PostgreSQL**: All operations use same Drizzle transaction
|
|
- **SQLite (with transactions enabled)**: Ensures rollback on errors
|
|
- **Without req**: Each operation runs independently, breaking atomicity
|
|
|
|
### Manual Transaction Control
|
|
|
|
```typescript
|
|
const transactionID = await payload.db.beginTransaction()
|
|
try {
|
|
await payload.create({
|
|
collection: 'orders',
|
|
data: orderData,
|
|
req: { transactionID },
|
|
})
|
|
await payload.update({
|
|
collection: 'inventory',
|
|
id: itemId,
|
|
data: { stock: newStock },
|
|
req: { transactionID },
|
|
})
|
|
await payload.db.commitTransaction(transactionID)
|
|
} catch (error) {
|
|
await payload.db.rollbackTransaction(transactionID)
|
|
throw error
|
|
}
|
|
```
|
|
|
|
## Storage Adapters
|
|
|
|
Available storage adapters:
|
|
|
|
- **@payloadcms/storage-s3** - AWS S3
|
|
- **@payloadcms/storage-azure** - Azure Blob Storage
|
|
- **@payloadcms/storage-gcs** - Google Cloud Storage
|
|
- **@payloadcms/storage-r2** - Cloudflare R2
|
|
- **@payloadcms/storage-vercel-blob** - Vercel Blob
|
|
- **@payloadcms/storage-uploadthing** - Uploadthing
|
|
|
|
### AWS S3
|
|
|
|
```typescript
|
|
import { s3Storage } from '@payloadcms/storage-s3'
|
|
|
|
export default buildConfig({
|
|
plugins: [
|
|
s3Storage({
|
|
collections: {
|
|
media: true,
|
|
},
|
|
bucket: process.env.S3_BUCKET,
|
|
config: {
|
|
credentials: {
|
|
accessKeyId: process.env.S3_ACCESS_KEY_ID,
|
|
secretAccessKey: process.env.S3_SECRET_ACCESS_KEY,
|
|
},
|
|
region: process.env.S3_REGION,
|
|
},
|
|
}),
|
|
],
|
|
})
|
|
```
|
|
|
|
## Email Adapters
|
|
|
|
### Nodemailer (SMTP)
|
|
|
|
```typescript
|
|
import { nodemailerAdapter } from '@payloadcms/email-nodemailer'
|
|
|
|
export default buildConfig({
|
|
email: nodemailerAdapter({
|
|
defaultFromAddress: 'noreply@example.com',
|
|
defaultFromName: 'My App',
|
|
transportOptions: {
|
|
host: process.env.SMTP_HOST,
|
|
port: 587,
|
|
auth: {
|
|
user: process.env.SMTP_USER,
|
|
pass: process.env.SMTP_PASS,
|
|
},
|
|
},
|
|
}),
|
|
})
|
|
```
|
|
|
|
### Resend
|
|
|
|
```typescript
|
|
import { resendAdapter } from '@payloadcms/email-resend'
|
|
|
|
export default buildConfig({
|
|
email: resendAdapter({
|
|
defaultFromAddress: 'noreply@example.com',
|
|
defaultFromName: 'My App',
|
|
apiKey: process.env.RESEND_API_KEY,
|
|
}),
|
|
})
|
|
```
|
|
|
|
## Important Notes
|
|
|
|
1. **MongoDB Transactions**: Require replica set configuration
|
|
2. **SQLite Transactions**: Disabled by default, enable with `transactionOptions: {}`
|
|
3. **Pass req**: Always pass `req` to nested operations in hooks for transaction safety
|
|
4. **Point Fields**: Not supported in SQLite
|