nextjs-cms 0.9.21 → 0.9.22

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1 +1 @@
1
- {"version":3,"file":"update-sections.d.ts","sourceRoot":"","sources":["../../../src/cli/lib/update-sections.ts"],"names":[],"mappings":"AAmzEA,wBAAsB,cAAc,CAAC,SAAS,UAAQ,iBAoBrD"}
1
+ {"version":3,"file":"update-sections.d.ts","sourceRoot":"","sources":["../../../src/cli/lib/update-sections.ts"],"names":[],"mappings":"AAm3EA,wBAAsB,cAAc,CAAC,SAAS,UAAQ,iBAoBrD"}
@@ -383,6 +383,40 @@ function buildLocaleFieldConfig() {
383
383
  order: 0,
384
384
  });
385
385
  }
386
+ async function createEditorPhotosTable(localized) {
387
+ await db.execute(sql `
388
+ CREATE TABLE IF NOT EXISTS \`editor_photos\` (
389
+ \`photo\` VARCHAR(100) NOT NULL PRIMARY KEY,
390
+ \`section\` VARCHAR(100) NOT NULL,
391
+ \`item_id\` VARCHAR(50) NOT NULL,
392
+ \`field\` VARCHAR(100) NOT NULL,
393
+ \`meta\` LONGTEXT,
394
+ ${localized ? sql `\`locale\` VARCHAR(10) NOT NULL,` : sql ``}
395
+ \`linked\` BOOLEAN DEFAULT false,
396
+ \`created_at\` TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP
397
+ )
398
+ `);
399
+ }
400
+ async function ensureEditorPhotosLocaleColumn(defaultLocaleCode, knownColumns) {
401
+ let columns = knownColumns ?? (await MysqlTableChecker.getColumns('editor_photos').catch(() => []));
402
+ if (columns.includes('locale')) {
403
+ return false;
404
+ }
405
+ if (columns.length === 0) {
406
+ await createEditorPhotosTable(true);
407
+ columns = await MysqlTableChecker.getColumns('editor_photos').catch(() => []);
408
+ if (columns.includes('locale')) {
409
+ log.info(chalk.gray(` - Created 'editor_photos' table with 'locale' column.`));
410
+ return true;
411
+ }
412
+ throw new Error(`Failed to create 'editor_photos' table with a 'locale' column.`);
413
+ }
414
+ await db.execute(sql `ALTER TABLE \`editor_photos\` ADD COLUMN \`locale\` VARCHAR(10) DEFAULT NULL`);
415
+ await db.execute(sql `UPDATE \`editor_photos\` SET \`locale\` = ${defaultLocaleCode} WHERE \`locale\` IS NULL`);
416
+ await db.execute(sql `ALTER TABLE \`editor_photos\` MODIFY COLUMN \`locale\` VARCHAR(10) NOT NULL`);
417
+ log.info(chalk.gray(` - Added 'locale' column to 'editor_photos' and backfilled existing rows with '${defaultLocaleCode}'.`));
418
+ return true;
419
+ }
386
420
  async function ensureTableRegistryEntry(tableName, sectionName) {
387
421
  await db
388
422
  .insert(NextJsCmsTablesTable)
@@ -987,46 +1021,54 @@ const main = async (s) => {
987
1021
  */
988
1022
  const RESERVED_TABLE_SUFFIX = '_locales';
989
1023
  const RESERVED_COLUMN_NAME = 'locale';
990
- const reservedNameErrors = [];
1024
+ const configurationErrors = [];
1025
+ const builtSections = sections.map((_s) => {
1026
+ const section = _s.build();
1027
+ section.buildFields();
1028
+ return section;
1029
+ });
1030
+ const managedSectionTables = new Map(builtSections.map((section) => [section.db.table, section.name]));
991
1031
  /**
992
1032
  * Insert the sections into the database
993
1033
  */
994
- for (const _s of sections) {
995
- const s = _s.build();
996
- s.buildFields();
1034
+ for (const s of builtSections) {
997
1035
  localesTableAssetMetadata.set(s.localesTableName, {
998
1036
  sectionName: s.name,
999
1037
  fields: getLocalizedAssetFields(s.fieldConfigs),
1000
1038
  });
1001
1039
  if (s.db.table.endsWith(RESERVED_TABLE_SUFFIX)) {
1002
- reservedNameErrors.push(`Section '${s.name}': table name '${s.db.table}' ends with reserved suffix '${RESERVED_TABLE_SUFFIX}'.`);
1040
+ configurationErrors.push(`Section '${s.name}': table name '${s.db.table}' ends with reserved suffix '${RESERVED_TABLE_SUFFIX}'.`);
1003
1041
  }
1004
1042
  for (const field of s.fields) {
1005
1043
  if (field.name === RESERVED_COLUMN_NAME) {
1006
- reservedNameErrors.push(`Section '${s.name}', field '${field.name}': field name '${RESERVED_COLUMN_NAME}' is reserved.`);
1044
+ configurationErrors.push(`Section '${s.name}', field '${field.name}': field name '${RESERVED_COLUMN_NAME}' is reserved.`);
1007
1045
  }
1008
1046
  if (field.destinationDb) {
1009
1047
  if (field.destinationDb.table.endsWith(RESERVED_TABLE_SUFFIX)) {
1010
- reservedNameErrors.push(`Section '${s.name}', field '${field.name}': destinationDb.table '${field.destinationDb.table}' ends with reserved suffix '${RESERVED_TABLE_SUFFIX}'.`);
1048
+ configurationErrors.push(`Section '${s.name}', field '${field.name}': destinationDb.table '${field.destinationDb.table}' ends with reserved suffix '${RESERVED_TABLE_SUFFIX}'.`);
1011
1049
  }
1012
1050
  if (field.destinationDb.itemIdentifier === RESERVED_COLUMN_NAME ||
1013
1051
  field.destinationDb.selectIdentifier === RESERVED_COLUMN_NAME) {
1014
- reservedNameErrors.push(`Section '${s.name}', field '${field.name}': destinationDb identifier column name '${RESERVED_COLUMN_NAME}' is reserved.`);
1052
+ configurationErrors.push(`Section '${s.name}', field '${field.name}': destinationDb identifier column name '${RESERVED_COLUMN_NAME}' is reserved.`);
1015
1053
  }
1016
1054
  }
1017
1055
  if (isExternalSelectDbField(field)) {
1056
+ const ownerSectionName = managedSectionTables.get(field.db.table);
1057
+ if (ownerSectionName) {
1058
+ configurationErrors.push(`Section '${s.name}', field '${field.name}': db.table '${field.db.table}' belongs to managed section '${ownerSectionName}'. Use the 'section' prop instead of 'db' so update-sections does not treat the section table as an external lookup table.`);
1059
+ }
1018
1060
  if (field.db.table.endsWith(RESERVED_TABLE_SUFFIX)) {
1019
- reservedNameErrors.push(`Section '${s.name}', field '${field.name}': db.table '${field.db.table}' ends with reserved suffix '${RESERVED_TABLE_SUFFIX}'.`);
1061
+ configurationErrors.push(`Section '${s.name}', field '${field.name}': db.table '${field.db.table}' ends with reserved suffix '${RESERVED_TABLE_SUFFIX}'.`);
1020
1062
  }
1021
1063
  const reservedColumns = [field.db.identifier, field.db.label, field.db.orderBy].filter((column) => column === RESERVED_COLUMN_NAME);
1022
1064
  if (reservedColumns.length > 0) {
1023
- reservedNameErrors.push(`Section '${s.name}', field '${field.name}': db column name '${RESERVED_COLUMN_NAME}' is reserved.`);
1065
+ configurationErrors.push(`Section '${s.name}', field '${field.name}': db column name '${RESERVED_COLUMN_NAME}' is reserved.`);
1024
1066
  }
1025
1067
  }
1026
1068
  }
1027
1069
  const galleryForValidation = await s.getGallery();
1028
1070
  if (galleryForValidation?.db.tableName && galleryForValidation.db.tableName.endsWith(RESERVED_TABLE_SUFFIX)) {
1029
- reservedNameErrors.push(`Section '${s.name}': gallery table '${galleryForValidation.db.tableName}' ends with reserved suffix '${RESERVED_TABLE_SUFFIX}'.`);
1071
+ configurationErrors.push(`Section '${s.name}': gallery table '${galleryForValidation.db.tableName}' ends with reserved suffix '${RESERVED_TABLE_SUFFIX}'.`);
1030
1072
  }
1031
1073
  /**
1032
1074
  * Generate the Drizzle schema for the table
@@ -1335,13 +1377,13 @@ const main = async (s) => {
1335
1377
  }
1336
1378
  }
1337
1379
  /**
1338
- * Reject the run before any DB write if any section used a reserved name.
1380
+ * Reject the run before schema writes if any section configuration is unsafe.
1339
1381
  */
1340
- if (reservedNameErrors.length > 0) {
1341
- s.stop(`update-sections aborted: ${reservedNameErrors.length} reserved-name conflict(s) detected.\n` +
1342
- reservedNameErrors.map((e) => ` - ${e}`).join('\n') +
1343
- `\n\nThe suffix '${RESERVED_TABLE_SUFFIX}' and the column name '${RESERVED_COLUMN_NAME}' are reserved for the localization system. Please rename and re-run.`);
1344
- throw new Error('update-sections: reserved name conflicts detected');
1382
+ if (configurationErrors.length > 0) {
1383
+ s.stop(`update-sections aborted: ${configurationErrors.length} configuration conflict(s) detected.\n` +
1384
+ configurationErrors.map((e) => ` - ${e}`).join('\n') +
1385
+ `\n\nThe suffix '${RESERVED_TABLE_SUFFIX}' and the column name '${RESERVED_COLUMN_NAME}' are reserved for the localization system. Section-managed tables must be referenced with the 'section' prop. Please update cms.config.ts and re-run.`);
1386
+ throw new Error('update-sections: configuration conflicts detected');
1345
1387
  }
1346
1388
  /**
1347
1389
  * Write schema file if schema generation is enabled
@@ -1423,6 +1465,7 @@ const main = async (s) => {
1423
1465
  if (localizationCurrentlyEnabled && cmsConfig.localization) {
1424
1466
  const configLocalizationState = buildStoredEnabledLocalizationState(cmsConfig.localization);
1425
1467
  enableTargetState = configLocalizationState;
1468
+ const storedPendingDisableLocalizationState = storedEnabledLocalizationState?.transition?.type === 'disable' ? storedEnabledLocalizationState : null;
1426
1469
  if (storedEnabledLocalizationState &&
1427
1470
  storedEnabledLocalizationState.defaultLocale !== configLocalizationState.defaultLocale) {
1428
1471
  s.stop(`Aborting. Changing localization defaultLocale is not supported (stored='${storedEnabledLocalizationState.defaultLocale}', config='${configLocalizationState.defaultLocale}'). Revert cms.config.ts and re-run update-sections.`);
@@ -1451,18 +1494,23 @@ const main = async (s) => {
1451
1494
  }
1452
1495
  }
1453
1496
  const needsEditorPhotosMigration = editorPhotosExists && !editorPhotosHasLocale;
1454
- const shouldRunEnableTransition = storedPendingEnableLocalizationState || !storedEnabledLocalizationState;
1497
+ const shouldRunEnableTransition = storedPendingEnableLocalizationState ||
1498
+ !storedEnabledLocalizationState ||
1499
+ storedPendingDisableLocalizationState;
1455
1500
  if (shouldRunEnableTransition) {
1456
1501
  const affectedList = [...(needsEditorPhotosMigration ? ['editor_photos'] : []), ...affectedLocalizedTables];
1457
1502
  const affectedListText = affectedList.length > 0
1458
1503
  ? affectedList.map((n) => ` - ${n}`).join('\n')
1459
1504
  : ` - No existing tables need a locale backfill; update-sections will verify localized tables after sync.`;
1460
1505
  const isEnableRetry = storedPendingEnableLocalizationState?.transition.status === 'pending';
1506
+ const isDisableRecovery = storedPendingDisableLocalizationState?.transition?.status === 'pending';
1461
1507
  s.stop();
1462
1508
  const confirm = await select({
1463
- message: (isEnableRetry
1464
- ? `A previous localization enable attempt did not complete. update-sections will retry setup now.\n\n`
1465
- : `It looks like you enabled localization. Your existing data in these tables will be marked as defaultLocale '${configLocalizationState.defaultLocale}':\n`) +
1509
+ message: (isDisableRecovery
1510
+ ? `A previous localization disable attempt did not complete, but cms.config.ts has localization enabled again. update-sections will repair localization artifacts and clear the pending disable state after verification.\n\n`
1511
+ : isEnableRetry
1512
+ ? `A previous localization enable attempt did not complete. update-sections will retry setup now.\n\n`
1513
+ : `It looks like you enabled localization. Your existing data in these tables will be marked as defaultLocale '${configLocalizationState.defaultLocale}':\n`) +
1466
1514
  affectedListText +
1467
1515
  `\n\n${chalk.redBright('WARNING:')} Existing data in these tables MUST already be in '${configLocalizationState.defaultLocale}'. ` +
1468
1516
  `Changing defaultLocale after this point is not supported. If your data is not in '${configLocalizationState.defaultLocale}', ` +
@@ -1483,22 +1531,12 @@ const main = async (s) => {
1483
1531
  type: 'enable',
1484
1532
  status: 'pending',
1485
1533
  startedAt: storedPendingEnableLocalizationState?.transition.startedAt ?? now,
1486
- ...(isEnableRetry ? { lastAttemptAt: now } : {}),
1534
+ ...(isEnableRetry || isDisableRecovery ? { lastAttemptAt: now } : {}),
1487
1535
  });
1488
1536
  await writeStoredLocalizationState(enablePendingState);
1489
- if (needsEditorPhotosMigration) {
1490
- try {
1491
- await db.execute(sql `ALTER TABLE \`editor_photos\` ADD COLUMN \`locale\` VARCHAR(10) DEFAULT NULL`);
1492
- await db.execute(sql `UPDATE \`editor_photos\` SET \`locale\` = ${configLocalizationState.defaultLocale} WHERE \`locale\` IS NULL`);
1493
- await db.execute(sql `ALTER TABLE \`editor_photos\` MODIFY COLUMN \`locale\` VARCHAR(10) NOT NULL`);
1494
- log.info(chalk.gray(` - Added 'locale' column to 'editor_photos' and backfilled existing rows with '${configLocalizationState.defaultLocale}'.`));
1495
- }
1496
- catch (error) {
1497
- console.error(chalk.red(` - Error migrating 'editor_photos' for localization enable:`, error));
1498
- }
1499
- }
1500
1537
  s.start();
1501
1538
  }
1539
+ await ensureEditorPhotosLocaleColumn(configLocalizationState.defaultLocale, editorPhotosColumns);
1502
1540
  }
1503
1541
  else if (!localizationCurrentlyEnabled && storedEnabledLocalizationState) {
1504
1542
  /**
@@ -1534,6 +1572,7 @@ const main = async (s) => {
1534
1572
  ` 3. Drop '_locales' tables entirely\n\n` +
1535
1573
  `Affected tables:\n` +
1536
1574
  affected.map((t) => ` - ${t.name} (${t.type})`).join('\n') +
1575
+ `\n\n${chalk.redBright('WARNING: This action is irreversible and will permanently delete data and affected tables. It is recommended to make a backup of your data before proceeding.')}` +
1537
1576
  `\n\nProceed?`,
1538
1577
  options: [
1539
1578
  { value: 'yes', label: `Yes, that's correct` },
@@ -1 +1 @@
1
- {"version":3,"file":"MysqlTable.d.ts","sourceRoot":"","sources":["../../../../src/core/db/table-checker/MysqlTable.ts"],"names":[],"mappings":"AAGA,OAAO,EAAE,cAAc,EAAE,MAAM,cAAc,CAAA;AAG7C;;GAEG;AACH,qBAAa,iBAAkB,SAAQ,cAAc;WACpC,iBAAiB;WAgBjB,UAAU,CAAC,SAAS,EAAE,MAAM,GAAG,OAAO,CAAC,MAAM,EAAE,CAAC;WAkBhD,eAAe,CAAC,KAAK,EAAE,MAAM;;;kBAIR,MAAM;qBAAW,MAAM,EAAE;;;kBAC1B,MAAM;qBAAW,MAAM,EAAE;;;;kBACtB,MAAM;qBAAW,MAAM,EAAE;;;WA2DhD,yBAAyB,CAAC,KAAK,EAAE,MAAM;;mBAU7B,MAAM;kBACP,MAAM;kBACN,MAAM;iBACP,MAAM;qBACF,MAAM;mBACR,MAAM;;;CAahC"}
1
+ {"version":3,"file":"MysqlTable.d.ts","sourceRoot":"","sources":["../../../../src/core/db/table-checker/MysqlTable.ts"],"names":[],"mappings":"AAGA,OAAO,EAAE,cAAc,EAAE,MAAM,cAAc,CAAA;AAG7C;;GAEG;AACH,qBAAa,iBAAkB,SAAQ,cAAc;WACpC,iBAAiB;WAgBjB,UAAU,CAAC,SAAS,EAAE,MAAM,GAAG,OAAO,CAAC,MAAM,EAAE,CAAC;WAoBhD,eAAe,CAAC,KAAK,EAAE,MAAM;;;kBAIR,MAAM;qBAAW,MAAM,EAAE;;;kBAC1B,MAAM;qBAAW,MAAM,EAAE;;;;kBACtB,MAAM;qBAAW,MAAM,EAAE;;;WA2DhD,yBAAyB,CAAC,KAAK,EAAE,MAAM;;mBAU7B,MAAM;kBACP,MAAM;kBACN,MAAM;iBACP,MAAM;qBACF,MAAM;mBACR,MAAM;;;CAahC"}
@@ -26,7 +26,9 @@ export class MysqlTableChecker extends DbTableChecker {
26
26
  SELECT COLUMN_NAME
27
27
  FROM information_schema.COLUMNS c
28
28
  inner join information_schema.TABLES t ON t.TABLE_NAME = c.TABLE_NAME
29
- WHERE t.TABLE_NAME = ${tableName}`;
29
+ WHERE t.TABLE_SCHEMA = DATABASE()
30
+ AND c.TABLE_SCHEMA = DATABASE()
31
+ AND t.TABLE_NAME = ${tableName}`;
30
32
  const _cols = [];
31
33
  const _res = await db.execute(statement);
32
34
  // @ts-ignore
@@ -70,13 +70,13 @@ declare const optionsSchema: z.ZodObject<{
70
70
  maxDate: z.ZodOptional<z.ZodUnion<readonly [z.ZodDate, z.ZodLiteral<"now">]>>;
71
71
  defaultStartValue: z.ZodOptional<z.ZodDate>;
72
72
  defaultEndValue: z.ZodOptional<z.ZodDate>;
73
- order: z.ZodOptional<z.ZodNumber>;
74
- localized: z.ZodOptional<z.ZodBoolean>;
75
73
  label: z.ZodOptional<z.ZodUnion<readonly [z.ZodString, z.ZodRecord<z.ZodString, z.ZodString>]>>;
76
74
  required: z.ZodOptional<z.ZodBoolean>;
77
75
  defaultValue: z.ZodOptional<z.ZodAny>;
76
+ order: z.ZodOptional<z.ZodNumber>;
78
77
  conditionalRules: z.ZodOptional<z.ZodArray<z.ZodCustom<import("../types/index.js").ConditionalRule, import("../types/index.js").ConditionalRule>>>;
79
78
  adminGenerated: z.ZodOptional<z.ZodUnion<readonly [z.ZodLiteral<true>, z.ZodLiteral<false>, z.ZodLiteral<"readonly">]>>;
79
+ localized: z.ZodOptional<z.ZodBoolean>;
80
80
  }, z.core.$strict>;
81
81
  declare const dateRangeFieldConfigSchema: z.ZodObject<{
82
82
  /**
@@ -97,13 +97,13 @@ declare const dateRangeFieldConfigSchema: z.ZodObject<{
97
97
  maxDate: z.ZodOptional<z.ZodUnion<readonly [z.ZodDate, z.ZodLiteral<"now">]>>;
98
98
  defaultStartValue: z.ZodOptional<z.ZodDate>;
99
99
  defaultEndValue: z.ZodOptional<z.ZodDate>;
100
- order: z.ZodOptional<z.ZodNumber>;
101
- localized: z.ZodOptional<z.ZodBoolean>;
102
100
  label: z.ZodOptional<z.ZodUnion<readonly [z.ZodString, z.ZodRecord<z.ZodString, z.ZodString>]>>;
103
101
  required: z.ZodOptional<z.ZodBoolean>;
104
102
  defaultValue: z.ZodOptional<z.ZodAny>;
103
+ order: z.ZodOptional<z.ZodNumber>;
105
104
  conditionalRules: z.ZodOptional<z.ZodArray<z.ZodCustom<import("../types/index.js").ConditionalRule, import("../types/index.js").ConditionalRule>>>;
106
105
  adminGenerated: z.ZodOptional<z.ZodUnion<readonly [z.ZodLiteral<true>, z.ZodLiteral<false>, z.ZodLiteral<"readonly">]>>;
106
+ localized: z.ZodOptional<z.ZodBoolean>;
107
107
  }, z.core.$strict>;
108
108
  export type DateRangeFieldConfig = z.infer<typeof dateRangeFieldConfigSchema>;
109
109
  /**