payload
Version:
Node, React, Headless CMS and Application Framework built on Next.js
277 lines (276 loc) • 11 kB
JavaScript
import { calculateVersionLocaleStatuses, toSnakeCase } from '../shared.js';
import { migrateMainCollectionStatus } from './migrateMainCollection.js';
import { migrateMainGlobalStatus } from './migrateMainGlobal.js';
export async function up(args) {
const { collectionSlug, db, globalSlug, payload, req, sql } = args;
if (!collectionSlug && !globalSlug) {
throw new Error('Either collectionSlug or globalSlug must be provided');
}
if (collectionSlug && globalSlug) {
throw new Error('Cannot provide both collectionSlug and globalSlug');
}
const entitySlug = collectionSlug || globalSlug;
// Convert camelCase slugs to snake_case and add version prefix/suffix
const versionsTable = collectionSlug ? `_${toSnakeCase(collectionSlug)}_v` : `_${toSnakeCase(globalSlug)}_v`;
const localesTable = `${versionsTable}_locales`;
if (!payload.config.localization) {
throw new Error('Localization is not enabled in payload config');
}
// Check if versions are enabled on this collection/global
let entityConfig;
if (collectionSlug) {
const collection = payload.config.collections.find((c)=>c.slug === collectionSlug);
if (collection) {
entityConfig = collection;
}
} else if (globalSlug) {
const global = payload.config.globals.find((g)=>g.slug === globalSlug);
if (global) {
entityConfig = global;
}
}
if (!entityConfig) {
throw new Error(`${collectionSlug ? 'Collection' : 'Global'} not found: ${collectionSlug || globalSlug}`);
}
payload.logger.info({
msg: `Starting _status localization migration for ${collectionSlug ? 'collection' : 'global'}: ${entitySlug}`
});
// Get filtered locales if filterAvailableLocales is defined
let locales = payload.config.localization.localeCodes;
if (typeof payload.config.localization.filterAvailableLocales === 'function') {
const filteredLocaleObjects = await payload.config.localization.filterAvailableLocales({
locales: payload.config.localization.locales,
req
});
locales = filteredLocaleObjects.map((locale)=>locale.code);
}
payload.logger.info({
msg: `Locales: ${locales.join(', ')}`
});
// Check if versions are enabled in config (skip if not)
if (!entityConfig.versions) {
payload.logger.info({
msg: `Skipping migration for ${collectionSlug ? 'collection' : 'global'}: ${entitySlug} - versions not enabled`
});
return;
}
// Validate that version__status column exists before proceeding
const columnCheckResult = await db.execute({
drizzle: db.drizzle,
sql: sql`
SELECT EXISTS (
SELECT FROM information_schema.columns
WHERE table_schema = 'public'
AND table_name = ${versionsTable}
AND column_name = 'version__status'
) as exists
`
});
if (!columnCheckResult.rows[0]?.exists) {
throw new Error(`Migration aborted: version__status column not found in ${versionsTable} table. ` + `This migration should only run on schemas that have NOT yet been migrated to per-locale status. ` + `If you've already run this migration, no action is needed.`);
}
// 1. Check if the locales table exists
const localesTableCheckResult = await db.execute({
drizzle: db.drizzle,
sql: sql`
SELECT EXISTS (
SELECT FROM information_schema.tables
WHERE table_schema = 'public'
AND table_name = ${localesTable}
) as exists
`
});
const localesTableExists = localesTableCheckResult.rows[0]?.exists;
if (!localesTableExists) {
// SCENARIO 1: Create the locales table (first localized field in versions)
payload.logger.info({
msg: `Creating new locales table: ${localesTable}`
});
await db.execute({
drizzle: db.drizzle,
sql: sql`
CREATE TABLE ${sql.identifier(localesTable)} (
id SERIAL PRIMARY KEY,
_locale VARCHAR NOT NULL,
_parent_id INTEGER NOT NULL,
version__status VARCHAR,
UNIQUE(_locale, _parent_id),
FOREIGN KEY (_parent_id) REFERENCES ${sql.identifier(versionsTable)}(id) ON DELETE CASCADE
)
`
});
// Create one row per locale per version record
// Simple approach: copy the same status to all locales
for (const locale of locales){
const inserted = await db.execute({
drizzle: db.drizzle,
sql: sql`
INSERT INTO ${sql.identifier(localesTable)} (_locale, _parent_id, version__status)
SELECT ${locale}, id, version__status
FROM ${sql.identifier(versionsTable)}
RETURNING id
`
});
payload.logger.info({
msg: `Inserted ${inserted.length} rows for locale: ${locale}`
});
}
} else {
// SCENARIO 2: Add version__status column to existing locales table
payload.logger.info({
msg: `Adding version__status column to existing table: ${localesTable}`
});
await db.execute({
drizzle: db.drizzle,
sql: sql`
ALTER TABLE ${sql.identifier(localesTable)} ADD COLUMN version__status VARCHAR
`
});
// INTELLIGENT DATA MIGRATION using historical publishedLocale data
payload.logger.info({
msg: 'Processing version history to determine status per locale...'
});
// First, get the list of locales that actually exist in the locales table
// This is important because the config may have more locales defined than what's in the OLD schema
const existingLocalesResult = await db.execute({
drizzle: db.drizzle,
sql: sql`
SELECT DISTINCT _locale
FROM ${sql.identifier(localesTable)}
ORDER BY _locale
`
});
const existingLocales = existingLocalesResult.rows.map((row)=>row._locale);
payload.logger.info({
msg: `Found existing locales in table: ${existingLocales.join(', ')}`
});
// Get all version records grouped by parent document, ordered chronologically
const versionsResult = await db.execute({
drizzle: db.drizzle,
sql: sql`
SELECT id, parent_id as parent, version__status as _status, published_locale, snapshot, created_at
FROM ${sql.identifier(versionsTable)}
ORDER BY parent_id, created_at ASC
`
});
// Use shared function to calculate version locale statuses
// Only process locales that actually exist in the locales table
const versionLocaleStatus = calculateVersionLocaleStatuses(versionsResult.rows, existingLocales, payload);
// Now update the locales table with the calculated status for each version
payload.logger.info({
msg: 'Updating locales table with calculated statuses...'
});
let updateCount = 0;
for (const [versionId, localeMap] of versionLocaleStatus.entries()){
for (const [locale, status] of localeMap.entries()){
await db.execute({
drizzle: db.drizzle,
sql: sql`
UPDATE ${sql.identifier(localesTable)}
SET version__status = ${status}
WHERE _parent_id = ${versionId}
AND _locale = ${locale}
`
});
updateCount++;
}
}
payload.logger.info({
msg: `Updated ${updateCount} locale rows with status`
});
}
// 3. Drop the old version__status column from main versions table
await db.execute({
drizzle: db.drizzle,
sql: sql`
ALTER TABLE ${sql.identifier(versionsTable)} DROP COLUMN version__status
`
});
// 4. Create and populate _status column in main collection/global locales table
// With localizeStatus enabled, _status is a localized field stored in the collection's locales table
// We need to create this column and populate it based on the latest version status per locale
const mainTable = collectionSlug ? toSnakeCase(collectionSlug) : toSnakeCase(globalSlug);
const mainLocalesTable = `${mainTable}_locales`;
const localesTableCheck = await db.execute({
drizzle: db.drizzle,
sql: sql`
SELECT EXISTS (
SELECT FROM information_schema.tables
WHERE table_schema = 'public'
AND table_name = ${mainLocalesTable}
) as exists
`
});
if (localesTableCheck.rows[0]?.exists) {
// Check if _status column already exists in the locales table
const statusColumnCheck = await db.execute({
drizzle: db.drizzle,
sql: sql`
SELECT EXISTS (
SELECT FROM information_schema.columns
WHERE table_schema = 'public'
AND table_name = ${mainLocalesTable}
AND column_name = '_status'
) as exists
`
});
if (!statusColumnCheck.rows[0]?.exists) {
// Add _status column to locales table
await db.execute({
drizzle: db.drizzle,
sql: sql`
ALTER TABLE ${sql.identifier(mainLocalesTable)}
ADD COLUMN _status VARCHAR DEFAULT 'draft'
`
});
}
// Now populate the _status values from the latest version
if (collectionSlug) {
await migrateMainCollectionStatus({
collectionSlug,
db,
locales,
payload,
sql,
versionsTable
});
} else if (globalSlug) {
await migrateMainGlobalStatus({
db,
globalSlug,
locales,
payload,
sql,
versionsTable
});
}
} else {
payload.logger.info({
msg: `No locales table found: ${mainLocalesTable} (collection/global not localized)`
});
}
// 5. Drop _status from main table if it exists (it will be in locales table now)
const mainTableStatusCheck = await db.execute({
drizzle: db.drizzle,
sql: sql`
SELECT EXISTS (
SELECT FROM information_schema.columns
WHERE table_schema = 'public'
AND table_name = ${mainTable}
AND column_name = '_status'
) as exists
`
});
if (mainTableStatusCheck.rows[0]?.exists) {
await db.execute({
drizzle: db.drizzle,
sql: sql`
ALTER TABLE ${sql.identifier(mainTable)} DROP COLUMN _status
`
});
}
payload.logger.info({
msg: 'Migration completed successfully'
});
}
//# sourceMappingURL=up.js.map