Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
57 changes: 52 additions & 5 deletions packages/db-mongodb/src/createMigration.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,16 @@ import type { CreateMigration, MigrationTemplateArgs } from 'payload'

import fs from 'fs'
import path from 'path'
import { getPredefinedMigration, writeMigrationIndex } from 'payload'
import {
bootstrapConfigState,
diffConfig,
generateDataMigrationCode,
getPredefinedMigration,
readConfigState,
resolvePrompts,
serializeConfig,
writeMigrationIndex,
} from 'payload'
import { fileURLToPath } from 'url'

const migrationTemplate = ({ downSQL, imports, upSQL }: MigrationTemplateArgs): string => `import {
Expand Down Expand Up @@ -39,7 +48,47 @@ export const createMigration: CreateMigration = async function createMigration({
payload,
})

const migrationFileContent = migrationTemplate(predefinedMigration)
// Config-diff: compute data migrations to append
const prevSnapshot = await readConfigState(dir)
const nextSnapshot = serializeConfig(payload.config)
let dataUpCode = ''
let dataDownCode = ''

if (prevSnapshot !== null) {
const changes = diffConfig(prevSnapshot, nextSnapshot)
if (changes.length > 0) {
const { shouldAbort } = await resolvePrompts(changes)
if (shouldAbort) {
process.exit(1)
}
const localization = payload.config.localization || null
const { downCode, upCode } = generateDataMigrationCode(changes, {
defaultLocale: localization?.defaultLocale,
})
dataUpCode = upCode
dataDownCode = downCode
}
} else {
await bootstrapConfigState(payload, dir)
}

const hasContent = predefinedMigration.upSQL || predefinedMigration.downSQL || dataUpCode

if (skipEmpty && !hasContent) {
writeMigrationIndex({ migrationsDir: payload.db.migrationDir })
return
}

const mergedUpSQL =
[predefinedMigration.upSQL, dataUpCode].filter(Boolean).join('\n') || undefined
const mergedDownSQL =
[predefinedMigration.downSQL, dataDownCode].filter(Boolean).join('\n') || undefined

const migrationFileContent = migrationTemplate({
...predefinedMigration,
downSQL: mergedDownSQL,
upSQL: mergedUpSQL,
})

const [yyymmdd, hhmmss] = new Date().toISOString().split('T')

Expand All @@ -52,9 +101,7 @@ export const createMigration: CreateMigration = async function createMigration({
const fileName = migrationName ? `${timestamp}_${formattedName}.ts` : `${timestamp}_migration.ts`
const filePath = `${dir}/${fileName}`

if (!skipEmpty) {
fs.writeFileSync(filePath, migrationFileContent)
}
fs.writeFileSync(filePath, migrationFileContent)

writeMigrationIndex({ migrationsDir: payload.db.migrationDir })

Expand Down
6 changes: 6 additions & 0 deletions packages/db-mongodb/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -51,7 +51,10 @@ import { findGlobalVersions } from './findGlobalVersions.js'
import { findOne } from './findOne.js'
import { findVersions } from './findVersions.js'
import { init } from './init.js'
import { migrateFieldDelocalized } from './migrateFieldDelocalized.js'
import { migrateFieldLocalized } from './migrateFieldLocalized.js'
import { migrateFresh } from './migrateFresh.js'
import { migrateVersionsEnabled } from './migrateVersionsEnabled.js'
import { queryDrafts } from './queryDrafts.js'
import { beginTransaction } from './transactions/beginTransaction.js'
import { commitTransaction } from './transactions/commitTransaction.js'
Expand Down Expand Up @@ -329,7 +332,10 @@ export function mongooseAdapter({
findOne,
findVersions,
init,
migrateFieldDelocalized,
migrateFieldLocalized,
migrateFresh,
migrateVersionsEnabled,
migrationDir,
packageName: '@payloadcms/db-mongodb',
payload,
Expand Down
67 changes: 67 additions & 0 deletions packages/db-mongodb/src/migrateFieldDelocalized.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,67 @@
import type { PayloadRequest } from 'payload'

import type { MongooseAdapter } from './index.js'

import { getCollection } from './utilities/getEntity.js'

function getValueAtPath(doc: Record<string, unknown>, path: string): unknown {
return path.split('.').reduce((obj: any, key) => obj?.[key], doc)
}

const BATCH_SIZE = 1000

export async function migrateFieldDelocalized(
this: MongooseAdapter,
args: {
defaultLocale: string
entity: 'collection' | 'global'
fieldPath: string
req: PayloadRequest
slug: string
},
): Promise<void> {
const { slug, defaultLocale, entity, fieldPath } = args
const { payload } = this

payload.logger.warn(
`[config-migration] Delocalizing field "${fieldPath}" on ${entity} "${slug}" — keeping only "${defaultLocale}" value`,
)

if (entity === 'collection') {
const { Model } = getCollection({ adapter: this, collectionSlug: slug })
const nativeCollection = Model.collection

let page = 1
let hasMore = true

while (hasMore) {
const docs = await nativeCollection
.find({})
.skip((page - 1) * BATCH_SIZE)
.limit(BATCH_SIZE)
.toArray()

for (const doc of docs) {
const currentValue = getValueAtPath(doc as any, fieldPath)
if (
currentValue !== null &&
currentValue !== undefined &&
typeof currentValue === 'object' &&
!Array.isArray(currentValue)
) {
const defaultValue = (currentValue as Record<string, unknown>)[defaultLocale] ?? null
const update: Record<string, unknown> = {}
update[fieldPath] = defaultValue
await nativeCollection.updateOne({ _id: doc._id }, { $set: update })
}
}

hasMore = docs.length === BATCH_SIZE
page++
}
}

payload.logger.info(
`[config-migration] Done delocalizing field "${fieldPath}" on ${entity} "${slug}"`,
)
}
68 changes: 68 additions & 0 deletions packages/db-mongodb/src/migrateFieldLocalized.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,68 @@
import type { PayloadRequest } from 'payload'

import type { MongooseAdapter } from './index.js'

import { getCollection } from './utilities/getEntity.js'

function getValueAtPath(doc: Record<string, unknown>, path: string): unknown {
return path.split('.').reduce((obj: any, key) => obj?.[key], doc)
}

const BATCH_SIZE = 1000

export async function migrateFieldLocalized(
this: MongooseAdapter,
args: {
defaultLocale: string
entity: 'collection' | 'global'
fieldPath: string
req: PayloadRequest
slug: string
},
): Promise<void> {
const { slug, defaultLocale, entity, fieldPath } = args
const { payload } = this

payload.logger.info(
`[config-migration] Localizing field "${fieldPath}" on ${entity} "${slug}" → locale "${defaultLocale}"`,
)

if (entity === 'collection') {
const { Model } = getCollection({ adapter: this, collectionSlug: slug })
const nativeCollection = Model.collection

let page = 1
let hasMore = true

while (hasMore) {
const docs = await nativeCollection
.find({})
.skip((page - 1) * BATCH_SIZE)
.limit(BATCH_SIZE)
.toArray()

for (const doc of docs) {
const currentValue = getValueAtPath(doc as any, fieldPath)
// Skip if already in localized shape
if (
currentValue !== null &&
currentValue !== undefined &&
typeof currentValue === 'object' &&
!Array.isArray(currentValue)
) {
continue
}
const update: Record<string, unknown> = {}
update[fieldPath] = { [defaultLocale]: currentValue }
await nativeCollection.updateOne({ _id: doc._id }, { $set: update })
}

hasMore = docs.length === BATCH_SIZE
page++
}
}

payload.logger.info(
`[config-migration] Done localizing field "${fieldPath}" on ${entity} "${slug}"`,
)
}
55 changes: 55 additions & 0 deletions packages/db-mongodb/src/migrateVersionsEnabled.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,55 @@
import type { PayloadRequest } from 'payload'

import { batchTransform } from 'payload/migrations'

import type { MongooseAdapter } from './index.js'

const BATCH_SIZE = 1000

export async function migrateVersionsEnabled(
this: MongooseAdapter,
args: {
entity: 'collection' | 'global'
initialStatus: 'draft' | 'published'
req: PayloadRequest
slug: string
},
): Promise<void> {
const { slug, entity, initialStatus, req } = args
const { payload } = this

payload.logger.info(
`[config-migration] MongoDB: creating version entries for ${entity} "${slug}" with _status: ${initialStatus}`,
)

if (entity === 'collection') {
await batchTransform({
batchSize: BATCH_SIZE,
fetcher: ({ limit, page }: { limit: number; page: number }) =>
payload.db.find({ collection: slug, limit, page, pagination: true, req }),
transform: async (doc: any) => {
await payload.db.createVersion({
autosave: false,
collectionSlug: slug as any,
createdAt: doc.createdAt ?? new Date().toISOString(),
parent: doc.id,
req,
updatedAt: doc.updatedAt ?? new Date().toISOString(),
versionData: { ...doc, _status: initialStatus },
})
},
})
} else {
const globalDoc = await payload.db.findGlobal({ slug, req })
await payload.db.createGlobalVersion({
autosave: false,
createdAt: globalDoc.createdAt ?? new Date().toISOString(),
globalSlug: slug as any,
req,
updatedAt: globalDoc.updatedAt ?? new Date().toISOString(),
versionData: { ...globalDoc, _status: initialStatus },
})
}

payload.logger.info(`[config-migration] MongoDB: done creating version entries for "${slug}"`)
}
2 changes: 2 additions & 0 deletions packages/db-postgres/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,7 @@ import {
migrateRefresh,
migrateReset,
migrateStatus,
migrateVersionsEnabled,
operatorMap,
queryDrafts,
rollbackTransaction,
Expand Down Expand Up @@ -204,6 +205,7 @@ export function postgresAdapter(args: Args): DatabaseAdapterObj<PostgresAdapter>
migrateRefresh,
migrateReset,
migrateStatus,
migrateVersionsEnabled,
migrationDir,
packageName: '@payloadcms/db-postgres',
payload,
Expand Down
2 changes: 2 additions & 0 deletions packages/db-sqlite/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,7 @@ import {
migrateRefresh,
migrateReset,
migrateStatus,
migrateVersionsEnabled,
operatorMap,
queryDrafts,
rollbackTransaction,
Expand Down Expand Up @@ -212,6 +213,7 @@ export function sqliteAdapter(args: Args): DatabaseAdapterObj<SQLiteAdapter> {
migrateRefresh,
migrateReset,
migrateStatus,
migrateVersionsEnabled,
migrationDir,
packageName: '@payloadcms/db-sqlite',
payload,
Expand Down
1 change: 1 addition & 0 deletions packages/drizzle/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@ export { migrateFresh } from './migrateFresh.js'
export { migrateRefresh } from './migrateRefresh.js'
export { migrateReset } from './migrateReset.js'
export { migrateStatus } from './migrateStatus.js'
export { migrateVersionsEnabled } from './migrateVersionsEnabled.js'
export { buildQuery } from './queries/buildQuery.js'
export { operatorMap } from './queries/operatorMap.js'
export type { Operators } from './queries/operatorMap.js'
Expand Down
5 changes: 5 additions & 0 deletions packages/drizzle/src/migrate.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,8 @@ import {
initTransaction,
killTransaction,
readMigrationFiles,
serializeConfig,
writeConfigState,
} from 'payload'
import prompts from 'prompts'

Expand Down Expand Up @@ -85,6 +87,9 @@ export const migrate: DrizzleAdapter['migrate'] = async function migrate(

await runMigrationFile(payload, migration, newBatch)
}

// Update config snapshot after all migrations succeed
await writeConfigState(payload.db.migrationDir, serializeConfig(payload.config))
}

async function runMigrationFile(payload: Payload, migration: Migration, batch: number) {
Expand Down
Loading
Loading