nextjs-cms 0.9.31 → 0.9.33

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.
Files changed (49) hide show
  1. package/dist/api/actions/pages.d.ts +3 -3
  2. package/dist/api/trpc/root.d.ts +3 -3
  3. package/dist/api/trpc/routers/navigation.d.ts +3 -3
  4. package/dist/api/trpc/server.d.ts +9 -9
  5. package/dist/cli/lib/update-sections.d.ts.map +1 -1
  6. package/dist/cli/lib/update-sections.js +217 -59
  7. package/dist/cli/utils/schema-generator.d.ts.map +1 -1
  8. package/dist/cli/utils/schema-generator.js +4 -1
  9. package/dist/core/config/config-loader.d.ts +2 -2
  10. package/dist/core/fields/date-range.d.ts +4 -4
  11. package/dist/core/fields/document.d.ts.map +1 -1
  12. package/dist/core/fields/document.js +13 -1
  13. package/dist/core/fields/photo.d.ts +6 -6
  14. package/dist/core/fields/photo.d.ts.map +1 -1
  15. package/dist/core/fields/photo.js +30 -9
  16. package/dist/core/fields/richText.d.ts +8 -8
  17. package/dist/core/fields/select.d.ts +13 -0
  18. package/dist/core/fields/select.d.ts.map +1 -1
  19. package/dist/core/fields/select.js +1 -0
  20. package/dist/core/fields/selectMultiple.d.ts +9 -0
  21. package/dist/core/fields/selectMultiple.d.ts.map +1 -1
  22. package/dist/core/fields/selectMultiple.js +1 -0
  23. package/dist/core/fields/video.d.ts +1 -0
  24. package/dist/core/fields/video.d.ts.map +1 -1
  25. package/dist/core/fields/video.js +18 -2
  26. package/dist/core/sections/category.d.ts +6 -6
  27. package/dist/core/sections/hasItems.d.ts +12 -12
  28. package/dist/core/sections/section.d.ts +4 -4
  29. package/dist/core/sections/simple.d.ts +6 -6
  30. package/dist/db/schema.d.ts +17 -0
  31. package/dist/db/schema.d.ts.map +1 -1
  32. package/dist/db/schema.js +1 -0
  33. package/dist/translations/base/en.d.ts +6 -0
  34. package/dist/translations/base/en.d.ts.map +1 -1
  35. package/dist/translations/base/en.js +6 -0
  36. package/dist/translations/client.d.ts +76 -4
  37. package/dist/translations/client.d.ts.map +1 -1
  38. package/dist/translations/server.d.ts +76 -4
  39. package/dist/translations/server.d.ts.map +1 -1
  40. package/dist/validators/document.d.ts.map +1 -1
  41. package/dist/validators/document.js +2 -1
  42. package/dist/validators/file-extension.d.ts +3 -0
  43. package/dist/validators/file-extension.d.ts.map +1 -0
  44. package/dist/validators/file-extension.js +9 -0
  45. package/dist/validators/photo.d.ts.map +1 -1
  46. package/dist/validators/photo.js +2 -1
  47. package/dist/validators/video.d.ts.map +1 -1
  48. package/dist/validators/video.js +2 -1
  49. package/package.json +3 -3
@@ -1 +1 @@
1
- {"version":3,"file":"update-sections.d.ts","sourceRoot":"","sources":["../../../src/cli/lib/update-sections.ts"],"names":[],"mappings":"AAw3EA,wBAAsB,cAAc,CAAC,SAAS,UAAQ,iBAoBrD"}
1
+ {"version":3,"file":"update-sections.d.ts","sourceRoot":"","sources":["../../../src/cli/lib/update-sections.ts"],"names":[],"mappings":"AAklFA,wBAAsB,cAAc,CAAC,SAAS,UAAQ,iBAoBrD"}
@@ -1,6 +1,6 @@
1
1
  import fs from 'fs';
2
2
  import path from 'path';
3
- import { intro, log, select, spinner } from '@clack/prompts';
3
+ import { intro, log, note, select, spinner } from '@clack/prompts';
4
4
  import chalk from 'chalk';
5
5
  import { eq, sql } from 'drizzle-orm';
6
6
  import { getCMSConfig } from '../../core/config/index.js';
@@ -180,7 +180,7 @@ function generateFieldSQL(input) {
180
180
  fieldSQL += 'VARCHAR(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci';
181
181
  break;
182
182
  case is(input, TagsField):
183
- fieldSQL += 'VARCHAR(50) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci';
183
+ fieldSQL += 'LONGTEXT CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci';
184
184
  break;
185
185
  default:
186
186
  fieldSQL += 'VARCHAR(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci';
@@ -192,8 +192,8 @@ function generateFieldSQL(input) {
192
192
  else if (!input.defaultValue) {
193
193
  fieldSQL += ' DEFAULT NULL';
194
194
  }
195
- if (input.defaultValue !== null && input.defaultValue !== undefined && input.required) {
196
- // Only add sql default when the field is required,
195
+ if (input.defaultValue !== null && input.defaultValue !== undefined) {
196
+ // TODO: Should we only add sql default when the field is required?
197
197
  // to allow null values in non-required fields
198
198
  // with defaultValue prop set.
199
199
  // Example: Admin cleared the defaultValue of
@@ -379,6 +379,125 @@ function buildSelectDbFieldConfig(name, label, type = 'text', required = true) {
379
379
  order: 0,
380
380
  });
381
381
  }
382
+ function emptyTableRegistryConfig() {
383
+ return { unmanagedColumns: [] };
384
+ }
385
+ function parseTableRegistryConfig(value, tableName) {
386
+ if (!value)
387
+ return emptyTableRegistryConfig();
388
+ try {
389
+ const parsed = JSON.parse(value);
390
+ const unmanagedColumns = Array.isArray(parsed.unmanagedColumns)
391
+ ? parsed.unmanagedColumns.filter((column) => typeof column === 'string' && column.trim().length > 0)
392
+ : [];
393
+ return { unmanagedColumns: [...new Set(unmanagedColumns)] };
394
+ }
395
+ catch (error) {
396
+ console.warn(chalk.yellow(` - Warning: Invalid config for table '${tableName}'. Ignoring table config.`));
397
+ return emptyTableRegistryConfig();
398
+ }
399
+ }
400
+ function serializeTableRegistryConfig(config) {
401
+ return JSON.stringify({
402
+ unmanagedColumns: [...new Set(config.unmanagedColumns)].sort(),
403
+ });
404
+ }
405
+ async function updateTableRegistryConfig(tableName, config) {
406
+ await db
407
+ .update(NextJsCmsTablesTable)
408
+ .set({ config: serializeTableRegistryConfig(config) })
409
+ .where(eq(NextJsCmsTablesTable.tableName, tableName));
410
+ }
411
+ function columnTypeForSelectDbSummary(type) {
412
+ return type === 'number' ? 'INT' : 'VARCHAR(255)';
413
+ }
414
+ function getFieldSummaryType(field) {
415
+ switch (true) {
416
+ case is(field, NumberField):
417
+ return 'INT';
418
+ case is(field, TextField):
419
+ return `VARCHAR(${field.maxLength ?? 255})`;
420
+ case is(field, TextAreaField) || is(field, RichTextField):
421
+ return 'LONGTEXT';
422
+ case is(field, DateField):
423
+ return field.format === 'datetime' ? 'DATETIME' : 'DATE';
424
+ case is(field, CheckboxField):
425
+ return 'BOOLEAN';
426
+ default:
427
+ return 'configured';
428
+ }
429
+ }
430
+ function normalizeColumnTypeForSummary(type) {
431
+ const normalized = type.toUpperCase();
432
+ if (normalized === 'INT(11)')
433
+ return 'INT';
434
+ return normalized;
435
+ }
436
+ function formatExistingTableColumns(existingStructure) {
437
+ const columns = Object.values(existingStructure).map((column) => {
438
+ return ` - ${column.Field}: ${normalizeColumnTypeForSummary(column.Type)}`;
439
+ });
440
+ return columns.length > 0 ? columns : [' - none'];
441
+ }
442
+ function buildDesiredTableSummary(table) {
443
+ if (table.sectionType === 'selectDb' && table.selectDb) {
444
+ const identifierSummary = table.selectDb.identifierTypeExplicit
445
+ ? ` - Identifier: ${table.selectDb.identifier}, identifierType: ${table.selectDb.identifierType} -> ${columnTypeForSelectDbSummary(table.selectDb.identifierType)}`
446
+ : ` - Identifier: ${table.selectDb.identifier}`;
447
+ const labelSummary = table.selectDb.labelTypeExplicit
448
+ ? ` - Label: ${table.selectDb.label}, labelType: ${table.selectDb.labelType} -> ${columnTypeForSelectDbSummary(table.selectDb.labelType)}`
449
+ : ` - Label: ${table.selectDb.label}`;
450
+ return [identifierSummary, labelSummary];
451
+ }
452
+ return table.fields.flatMap((field) => {
453
+ return getFieldColumnName(field).map((column) => ` - ${column}: ${getFieldSummaryType(field)}`);
454
+ });
455
+ }
456
+ function buildExistingTableDifferences(table, existingStructure) {
457
+ const configuredColumns = table.fields.flatMap(getFieldColumnName);
458
+ const existingColumns = Object.keys(existingStructure);
459
+ const missingColumns = configuredColumns.filter((column) => !existingColumns.includes(column));
460
+ const extraColumns = existingColumns.filter((column) => !configuredColumns.includes(column));
461
+ const typeMismatches = table.fields.flatMap((field) => {
462
+ return getFieldColumnName(field).flatMap((column) => {
463
+ const existingColumn = existingStructure[column];
464
+ if (!existingColumn)
465
+ return [];
466
+ const expectedType = normalizeColumnTypeForSummary(getFieldSummaryType(field));
467
+ const actualType = normalizeColumnTypeForSummary(existingColumn.Type);
468
+ if (expectedType === 'CONFIGURED' || expectedType === actualType)
469
+ return [];
470
+ return ` - ${column}: config ${expectedType}, table ${actualType}`;
471
+ });
472
+ });
473
+ const differences = [];
474
+ differences.push(` - Missing configured columns: ${missingColumns.length > 0 ? missingColumns.join(', ') : 'none'}`);
475
+ differences.push(` - Extra existing columns: ${extraColumns.length > 0 ? extraColumns.join(', ') : 'none'}`);
476
+ if (typeMismatches.length > 0) {
477
+ differences.push(' - Type mismatches:');
478
+ differences.push(...typeMismatches);
479
+ }
480
+ else {
481
+ differences.push(' - Type mismatches: none');
482
+ }
483
+ return differences;
484
+ }
485
+ function buildExistingTablePromptMessage(table, existingStructure) {
486
+ return [
487
+ `Table '${table.name}' already exists in your database, here's the table and config summary:`,
488
+ '',
489
+ 'Your config:',
490
+ ...buildDesiredTableSummary(table),
491
+ '',
492
+ 'Existing table:',
493
+ ...formatExistingTableColumns(existingStructure),
494
+ '',
495
+ 'Differences:',
496
+ ...buildExistingTableDifferences(table, existingStructure),
497
+ '',
498
+ 'How do you want to handle this table?',
499
+ ].join('\n');
500
+ }
382
501
  function buildLocaleFieldConfig() {
383
502
  return textField({
384
503
  name: 'locale',
@@ -440,6 +559,12 @@ async function ensureTableRegistryEntry(tableName, sectionName) {
440
559
  console.error('Error inserting into __nextjs_cms_tables table:', error);
441
560
  });
442
561
  }
562
+ async function ensureTableRegistryConfigColumn() {
563
+ const columns = await MysqlTableChecker.getColumns('__nextjs_cms_tables');
564
+ if (columns.includes('config'))
565
+ return;
566
+ await db.execute(sql `ALTER TABLE __nextjs_cms_tables ADD COLUMN config LONGTEXT NULL`);
567
+ }
443
568
  async function createTable(table, options) {
444
569
  /**
445
570
  * Generate the CREATE TABLE SQL
@@ -669,6 +794,14 @@ async function updateTable(table, s, ctx) {
669
794
  * Filter out the fields that already exist in the table
670
795
  */
671
796
  const existingFields = existingFieldsData ? Object.keys(existingFieldsData) : [];
797
+ const configuredFields = table.fields.flatMap(getFieldColumnName);
798
+ const tableConfig = parseTableRegistryConfig(table.config, table.name);
799
+ const prunedUnmanagedColumns = tableConfig.unmanagedColumns.filter((column) => existingFields.includes(column) && !configuredFields.includes(column));
800
+ if (prunedUnmanagedColumns.length !== tableConfig.unmanagedColumns.length) {
801
+ tableConfig.unmanagedColumns = prunedUnmanagedColumns;
802
+ await updateTableRegistryConfig(table.name, tableConfig);
803
+ }
804
+ const unmanagedColumns = new Set(tableConfig.unmanagedColumns);
672
805
  const fieldsToAdd = table.fields.filter((field) => !getFieldColumnName(field).some((col) => existingFields.includes(col)));
673
806
  /**
674
807
  * Let's find out fields that need to be updated.
@@ -682,8 +815,9 @@ async function updateTable(table, s, ctx) {
682
815
  * or if it has a destinationDb, TODO: Add the values to the destination table? (CRITICAL! Loss of data if not done)
683
816
  * we should mark it for removal.
684
817
  */
685
- let fieldsToRemove = existingFields.filter((existingField) => !table.fields.some((field) => getFieldColumnName(field).includes(existingField)) ||
686
- table.fields.some((field) => field.destinationDb && getFieldColumnName(field).includes(existingField)));
818
+ let fieldsToRemove = existingFields.filter((existingField) => !unmanagedColumns.has(existingField) &&
819
+ (!table.fields.some((field) => getFieldColumnName(field).includes(existingField)) ||
820
+ table.fields.some((field) => field.destinationDb && getFieldColumnName(field).includes(existingField))));
687
821
  /**
688
822
  * Check if there are fields to update
689
823
  */
@@ -907,15 +1041,18 @@ async function updateTable(table, s, ctx) {
907
1041
  s.stop('Found ' + fieldsToRemove.length + ' field(s) to remove:');
908
1042
  for (const field of fieldsToRemove) {
909
1043
  const shouldRemove = await select({
910
- message: `You are about to remove field '${field}' of table '${table.name}'. Remove it?`,
1044
+ message: `Field '${field}' exists in table '${table.name}' but is not in your config.\n\nWhat do you want to do?`,
911
1045
  options: [
912
- { value: 'no', label: 'No' },
913
- { value: 'yes', label: 'Yes' },
1046
+ { value: 'yes', label: 'Drop field' },
1047
+ { value: 'no', label: 'Keep field' },
1048
+ { value: 'ask_later', label: 'Ask me later' },
914
1049
  ],
915
1050
  initialValue: 'no',
916
1051
  });
917
1052
  if (shouldRemove === 'yes') {
918
1053
  log.info(chalk.red(`Removing field '${field}'`));
1054
+ tableConfig.unmanagedColumns = tableConfig.unmanagedColumns.filter((column) => column !== field);
1055
+ await updateTableRegistryConfig(table.name, tableConfig);
919
1056
  alterTableSQLs.push({
920
1057
  field: field,
921
1058
  table: table.name,
@@ -923,8 +1060,13 @@ async function updateTable(table, s, ctx) {
923
1060
  sql: `DROP COLUMN \`${field}\``,
924
1061
  });
925
1062
  }
1063
+ else if (shouldRemove === 'no') {
1064
+ tableConfig.unmanagedColumns = [...new Set([...tableConfig.unmanagedColumns, field])];
1065
+ await updateTableRegistryConfig(table.name, tableConfig);
1066
+ log.info(chalk.yellow(`- Field ${chalk.underline.italic(field)} kept as an unmanaged column.`));
1067
+ }
926
1068
  else {
927
- log.info(chalk.yellow(`- Field ${chalk.underline.italic(field)} not removed.`));
1069
+ log.info(chalk.yellow(`- Field ${chalk.underline.italic(field)} not removed. You will be asked again later.`));
928
1070
  }
929
1071
  }
930
1072
  s.start();
@@ -961,7 +1103,7 @@ async function updateTable(table, s, ctx) {
961
1103
  sqlErrors += keyErrors;
962
1104
  s.stop(chalk.italic.hex(`${sqlErrors > 0 ? `#FFA500` : `#fafafa`}`)(`- Table \`${table.name}\` modified successfully ${sqlErrors > 0 ? `with ${sqlErrors} error(s).` : ''}`));
963
1105
  }
964
- const main = async (s) => {
1106
+ const main = async (spinnerObject) => {
965
1107
  const cmsConfig = await getCMSConfig();
966
1108
  const schemaGenerationEnabled = cmsConfig.schemaGeneration.drizzle.enabled;
967
1109
  const schemaOutDir = cmsConfig.schemaGeneration.drizzle.outDir;
@@ -973,7 +1115,7 @@ const main = async (s) => {
973
1115
  */
974
1116
  if (schemaGenerationEnabled) {
975
1117
  console.log(chalk.white(`Generating Drizzle schema...`));
976
- s.start();
1118
+ spinnerObject.start();
977
1119
  }
978
1120
  else {
979
1121
  console.log(chalk.yellow(`Schema generation is disabled. Skipping Drizzle schema generation.`));
@@ -987,8 +1129,7 @@ const main = async (s) => {
987
1129
  const externalSelectDbTables = new Set();
988
1130
  const localesTableAssetMetadata = new Map();
989
1131
  sections = await SectionFactory.getSectionsSilently();
990
- console.log(`Found ${sections.length} section(s) to insert: `);
991
- console.log(chalk.gray(sections.map((s) => s.name).join(', ')));
1132
+ note(sections.length === 0 ? chalk.dim('(none)') : sections.map((s) => chalk.gray(`• ${s.name}`)).join('\n'), `Sections to insert (${sections.length})`);
992
1133
  /**
993
1134
  * Let's see if the table `__nextjs_cms_tables` exists in the database.
994
1135
  * If it doesn't, we'll create it using the same schema as `NextJsCmsTablesTable`.
@@ -997,9 +1138,11 @@ const main = async (s) => {
997
1138
  CREATE TABLE IF NOT EXISTS __nextjs_cms_tables (
998
1139
  name VARCHAR(100) NOT NULL PRIMARY KEY,
999
1140
  section VARCHAR(200),
1141
+ config LONGTEXT,
1000
1142
  created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP
1001
1143
  )
1002
1144
  `);
1145
+ await ensureTableRegistryConfigColumn();
1003
1146
  /**
1004
1147
  * Persistent key/value store for CMS-level state that must survive config removal.
1005
1148
  */
@@ -1017,6 +1160,7 @@ const main = async (s) => {
1017
1160
  existingTables = await db
1018
1161
  .select({
1019
1162
  name: NextJsCmsTablesTable.tableName,
1163
+ config: NextJsCmsTablesTable.config,
1020
1164
  })
1021
1165
  .from(NextJsCmsTablesTable);
1022
1166
  /**
@@ -1113,9 +1257,11 @@ const main = async (s) => {
1113
1257
  for (const field of s.fields) {
1114
1258
  if (isExternalSelectDbField(field) && !externalSelectDbTables.has(field.db.table)) {
1115
1259
  externalSelectDbTables.add(field.db.table);
1116
- const identifierFieldConfig = buildSelectDbFieldConfig(field.db.identifier, 'Option Id', field.db.identifierType ?? 'text');
1117
- const labelFieldConfig = buildSelectDbFieldConfig(field.db.label, 'Option Label');
1118
- const selectDbFieldConfigs = [identifierFieldConfig, labelFieldConfig];
1260
+ const identifierType = field.db.identifierType ?? 'text';
1261
+ const labelType = field.db.labelType ?? 'text';
1262
+ const identifierFieldConfig = buildSelectDbFieldConfig(field.db.identifier, 'Option Id', identifierType);
1263
+ const typedLabelFieldConfig = buildSelectDbFieldConfig(field.db.label, 'Option Label', labelType);
1264
+ const selectDbFieldConfigs = [identifierFieldConfig, typedLabelFieldConfig];
1119
1265
  if (field.db.orderBy &&
1120
1266
  field.db.orderBy !== field.db.identifier &&
1121
1267
  field.db.orderBy !== field.db.label) {
@@ -1128,6 +1274,14 @@ const main = async (s) => {
1128
1274
  sectionType: 'selectDb',
1129
1275
  identifier: identifierFieldConfig,
1130
1276
  primaryKey: [identifierFieldConfig],
1277
+ selectDb: {
1278
+ identifier: field.db.identifier,
1279
+ identifierType,
1280
+ identifierTypeExplicit: field.db.identifierType !== undefined,
1281
+ label: field.db.label,
1282
+ labelType,
1283
+ labelTypeExplicit: field.db.labelType !== undefined,
1284
+ },
1131
1285
  });
1132
1286
  const selectDbSchema = generateDrizzleSchema({
1133
1287
  name: field.db.table,
@@ -1138,7 +1292,7 @@ const main = async (s) => {
1138
1292
  selectDbSchema.drizzleImports.forEach((type) => drizzleImports.add(type));
1139
1293
  }
1140
1294
  if (field.destinationDb) {
1141
- console.log('Destination DB found for input:', field.name, 'with table:', field.destinationDb.table);
1295
+ log.info(`Destination DB found for input: ${chalk.cyan(field.name)} with table: ${chalk.cyan(field.destinationDb.table)}`);
1142
1296
  const parentIdentifierType = s.db.identifier.type;
1143
1297
  const referenceIdFieldConfig = parentIdentifierType === 'number'
1144
1298
  ? numberField({
@@ -1385,7 +1539,7 @@ const main = async (s) => {
1385
1539
  * Reject the run before schema writes if any section configuration is unsafe.
1386
1540
  */
1387
1541
  if (configurationErrors.length > 0) {
1388
- s.stop(`update-sections aborted: ${configurationErrors.length} configuration conflict(s) detected.\n` +
1542
+ spinnerObject.stop(`update-sections aborted: ${configurationErrors.length} configuration conflict(s) detected.\n` +
1389
1543
  configurationErrors.map((e) => ` - ${e}`).join('\n') +
1390
1544
  `\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.`);
1391
1545
  throw new Error('update-sections: configuration conflicts detected');
@@ -1408,10 +1562,10 @@ const main = async (s) => {
1408
1562
  "} from 'drizzle-orm/mysql-core'\n\n" +
1409
1563
  drizzleTableSchemas.join('\n');
1410
1564
  fs.writeFileSync(schemaFilePath, schemaFileContent, 'utf8');
1411
- s.stop();
1565
+ spinnerObject.stop();
1412
1566
  }
1413
1567
  console.log(chalk.white('Finding tables to create, update or remove...'));
1414
- s.start();
1568
+ spinnerObject.start();
1415
1569
  /**
1416
1570
  * Filter out the tables that already exist in the database
1417
1571
  */
@@ -1420,23 +1574,24 @@ const main = async (s) => {
1420
1574
  * Let's find out tables that need to be updated.
1421
1575
  * If a table exists in both the desired tables and the existing tables, we should mark it for update.
1422
1576
  */
1423
- const tablesToUpdate = desiredTables.filter((table) => existingTables.some((existingTable) => existingTable.name === table.name));
1577
+ const tablesToUpdate = desiredTables
1578
+ .filter((table) => existingTables.some((existingTable) => existingTable.name === table.name))
1579
+ .map((table) => {
1580
+ const existingTable = existingTables.find((existingTable) => existingTable.name === table.name);
1581
+ return { ...table, config: existingTable?.config };
1582
+ });
1424
1583
  /**
1425
1584
  * Let's find out tables to remove as well.
1426
1585
  * If a table exists in the database but not in the desired tables, we should mark it for removal.
1427
1586
  */
1428
1587
  let tablesToRemove = existingTables.filter((existingTable) => !desiredTables.some((table) => table.name === existingTable.name));
1429
- s.stop();
1430
- console.log('Desired tables:');
1431
- console.log(chalk.gray(desiredTables.map((table) => table.name).join(', ')));
1432
- console.log('Existing tables:');
1433
- console.log(chalk.gray(existingTables.map((table) => table.name).join(', ')));
1434
- console.log('Tables to update:');
1435
- console.log(chalk.gray(tablesToUpdate.map((table) => table.name).join(', ')));
1436
- console.log('Tables to create:');
1437
- console.log(chalk.gray(tablesToCreate.map((table) => table.name).join(', ')));
1438
- console.log('Tables to remove:');
1439
- console.log(chalk.gray(tablesToRemove.map((table) => table.name).join(', ')));
1588
+ spinnerObject.stop();
1589
+ const formatTableList = (tables, color) => tables.length === 0 ? chalk.dim('(none)') : tables.map((table) => color(`• ${table.name}`)).join('\n');
1590
+ note(formatTableList(desiredTables, chalk.gray), `Desired tables (${desiredTables.length})`);
1591
+ note(formatTableList(existingTables, chalk.gray), `Existing tables (${existingTables.length})`);
1592
+ note(formatTableList(tablesToUpdate, chalk.yellow), `Tables to update (${tablesToUpdate.length})`);
1593
+ note(formatTableList(tablesToCreate, chalk.green), `Tables to create (${tablesToCreate.length})`);
1594
+ note(formatTableList(tablesToRemove, chalk.red), `Tables to remove (${tablesToRemove.length})`);
1440
1595
  console.log(`\n`);
1441
1596
  intro(chalk.inverse(' update-sections '));
1442
1597
  const localizationCurrentlyEnabled = cmsConfig.localization?.enabled === true;
@@ -1473,12 +1628,12 @@ const main = async (s) => {
1473
1628
  const storedPendingDisableLocalizationState = storedEnabledLocalizationState?.transition?.type === 'disable' ? storedEnabledLocalizationState : null;
1474
1629
  if (storedEnabledLocalizationState &&
1475
1630
  storedEnabledLocalizationState.defaultLocale !== configLocalizationState.defaultLocale) {
1476
- s.stop(`Aborting. Changing localization defaultLocale is not supported (stored='${storedEnabledLocalizationState.defaultLocale}', config='${configLocalizationState.defaultLocale}'). Revert cms.config.ts and re-run update-sections.`);
1631
+ spinnerObject.stop(`Aborting. Changing localization defaultLocale is not supported (stored='${storedEnabledLocalizationState.defaultLocale}', config='${configLocalizationState.defaultLocale}'). Revert cms.config.ts and re-run update-sections.`);
1477
1632
  return;
1478
1633
  }
1479
1634
  if (storedPendingEnableLocalizationState &&
1480
1635
  storedPendingEnableLocalizationState.defaultLocale !== configLocalizationState.defaultLocale) {
1481
- s.stop(`Aborting. Changing localization defaultLocale while enable is pending is not supported (stored='${storedPendingEnableLocalizationState.defaultLocale}', config='${configLocalizationState.defaultLocale}'). Revert cms.config.ts and re-run update-sections.`);
1636
+ spinnerObject.stop(`Aborting. Changing localization defaultLocale while enable is pending is not supported (stored='${storedPendingEnableLocalizationState.defaultLocale}', config='${configLocalizationState.defaultLocale}'). Revert cms.config.ts and re-run update-sections.`);
1482
1637
  return;
1483
1638
  }
1484
1639
  /**
@@ -1509,7 +1664,7 @@ const main = async (s) => {
1509
1664
  : ` - No existing tables need a locale backfill; update-sections will verify localized tables after sync.`;
1510
1665
  const isEnableRetry = storedPendingEnableLocalizationState?.transition.status === 'pending';
1511
1666
  const isDisableRecovery = storedPendingDisableLocalizationState?.transition?.status === 'pending';
1512
- s.stop();
1667
+ spinnerObject.stop();
1513
1668
  const confirm = await select({
1514
1669
  message: (isDisableRecovery
1515
1670
  ? `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`
@@ -1527,7 +1682,7 @@ const main = async (s) => {
1527
1682
  initialValue: 'no',
1528
1683
  });
1529
1684
  if (confirm !== 'yes') {
1530
- s.stop('Aborted. Reconfigure cms.config.ts and re-run update-sections.');
1685
+ spinnerObject.stop('Aborted. Reconfigure cms.config.ts and re-run update-sections.');
1531
1686
  return;
1532
1687
  }
1533
1688
  localizationTransition = 'enable';
@@ -1539,7 +1694,7 @@ const main = async (s) => {
1539
1694
  ...(isEnableRetry || isDisableRecovery ? { lastAttemptAt: now } : {}),
1540
1695
  });
1541
1696
  await writeStoredLocalizationState(enablePendingState);
1542
- s.start();
1697
+ spinnerObject.start();
1543
1698
  }
1544
1699
  await ensureEditorPhotosLocaleColumn(configLocalizationState.defaultLocale, editorPhotosColumns);
1545
1700
  }
@@ -1564,7 +1719,7 @@ const main = async (s) => {
1564
1719
  affected.push({ name: existing.name, type: 'localized_table' });
1565
1720
  }
1566
1721
  }
1567
- s.stop();
1722
+ spinnerObject.stop();
1568
1723
  const isDisableRetry = storedEnabledLocalizationState.transition?.status === 'pending';
1569
1724
  const confirm = await select({
1570
1725
  message: (isDisableRetry
@@ -1586,7 +1741,7 @@ const main = async (s) => {
1586
1741
  initialValue: 'no',
1587
1742
  });
1588
1743
  if (confirm !== 'yes') {
1589
- s.stop('Aborted. Reconfigure cms.config.ts and re-run update-sections.');
1744
+ spinnerObject.stop('Aborted. Reconfigure cms.config.ts and re-run update-sections.');
1590
1745
  return;
1591
1746
  }
1592
1747
  localizationTransition = 'disable';
@@ -1626,7 +1781,7 @@ const main = async (s) => {
1626
1781
  else {
1627
1782
  log.info(chalk.gray(` - 'editor_photos' already delocalized; resuming junction/_locales cleanup.`));
1628
1783
  }
1629
- s.start();
1784
+ spinnerObject.start();
1630
1785
  }
1631
1786
  const defaultLocaleCode = localizationCurrentlyEnabled
1632
1787
  ? (cmsConfig.localization?.defaultLocale ?? storedEnabledLocalizationState?.defaultLocale ?? 'en')
@@ -1640,7 +1795,7 @@ const main = async (s) => {
1640
1795
  * Loop through the tables to update
1641
1796
  */
1642
1797
  for (const table of tablesToUpdate) {
1643
- await updateTable(table, s, ctx);
1798
+ await updateTable(table, spinnerObject, ctx);
1644
1799
  }
1645
1800
  }
1646
1801
  /**
@@ -1652,23 +1807,26 @@ const main = async (s) => {
1652
1807
  * Loop through the tables to create
1653
1808
  */
1654
1809
  for (const table of tablesToCreate) {
1655
- const tableExistsInDatabase = (await MysqlTableChecker.getExistingTableStructure(table.name)) !== null;
1656
- if (tableExistsInDatabase) {
1657
- s.stop();
1658
- const overwriteExistingTable = await select({
1659
- message: `Table '${table.name}' already exists in your database, overwrite its fields if any?`,
1810
+ const existingTableStructure = await MysqlTableChecker.getExistingTableStructure(table.name);
1811
+ if (existingTableStructure) {
1812
+ spinnerObject.stop();
1813
+ const handleExistingTable = await select({
1814
+ message: buildExistingTablePromptMessage(table, existingTableStructure),
1660
1815
  options: [
1661
- { value: 'yes', label: 'Yes, overwrite fields' },
1662
- { value: 'no', label: 'No, skip for now' },
1816
+ { value: 'configure', label: 'Configure table (Recommended)' },
1817
+ {
1818
+ value: 'ask_later',
1819
+ label: 'Ask later and skip for now (Not recommended and may produce runtime errors!)',
1820
+ },
1663
1821
  ],
1664
- initialValue: 'yes',
1822
+ initialValue: 'configure',
1665
1823
  });
1666
- if (overwriteExistingTable === 'yes') {
1824
+ if (handleExistingTable === 'configure') {
1667
1825
  await ensureTableRegistryEntry(table.name, table.sectionName);
1668
- await updateTable(table, s, ctx);
1826
+ await updateTable(table, spinnerObject, ctx);
1669
1827
  }
1670
1828
  else {
1671
- console.log(chalk.yellow(`Skipping existing table '${table.name}'.`));
1829
+ console.log(chalk.yellow(`Skipping existing table '${table.name}'. You will be asked again later.`));
1672
1830
  }
1673
1831
  continue;
1674
1832
  }
@@ -1677,7 +1835,7 @@ const main = async (s) => {
1677
1835
  * If there are, ask the user if they want to create the new table or rename a removed table
1678
1836
  */
1679
1837
  if (tablesToRemove.length > 0) {
1680
- s.stop();
1838
+ spinnerObject.stop();
1681
1839
  const opType = await select({
1682
1840
  message: `Is table '${table.name}' for section '${table.sectionName}' a new table?`,
1683
1841
  options: [
@@ -1685,7 +1843,7 @@ const main = async (s) => {
1685
1843
  { value: 'rename', label: 'Rename existing table' },
1686
1844
  ],
1687
1845
  });
1688
- s.start();
1846
+ spinnerObject.start();
1689
1847
  switch (opType) {
1690
1848
  case 'new': {
1691
1849
  console.log(chalk.blueBright(`Creating table '${table.name}' for section '${table.sectionName}'`));
@@ -1694,7 +1852,7 @@ const main = async (s) => {
1694
1852
  break;
1695
1853
  }
1696
1854
  case 'rename': {
1697
- s.stop();
1855
+ spinnerObject.stop();
1698
1856
  const tableToRename = await select({
1699
1857
  message: `Select the table to rename to '${table.name}'`,
1700
1858
  options: tablesToRemove.map((table) => {
@@ -1704,7 +1862,7 @@ const main = async (s) => {
1704
1862
  if (tableToRename && typeof tableToRename === 'string') {
1705
1863
  console.log(`Renaming table '${tableToRename}' to '${table.name}'`);
1706
1864
  await renameTable(tableToRename, table.name);
1707
- await updateTable(table, s, ctx);
1865
+ await updateTable(table, spinnerObject, ctx);
1708
1866
  /**
1709
1867
  * Remove the table from the tablesToRemove array
1710
1868
  */
@@ -1750,7 +1908,7 @@ const main = async (s) => {
1750
1908
  opType = autoDropLocales;
1751
1909
  }
1752
1910
  else {
1753
- s.stop();
1911
+ spinnerObject.stop();
1754
1912
  opType = await select({
1755
1913
  message: `You are about to remove table '${table.name}'. Proceed?`,
1756
1914
  options: [
@@ -1760,7 +1918,7 @@ const main = async (s) => {
1760
1918
  ],
1761
1919
  initialValue: 'later',
1762
1920
  });
1763
- s.start();
1921
+ spinnerObject.start();
1764
1922
  }
1765
1923
  switch (opType) {
1766
1924
  case 'yes':
@@ -1 +1 @@
1
- {"version":3,"file":"schema-generator.d.ts","sourceRoot":"","sources":["../../../src/cli/utils/schema-generator.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,4BAA4B,CAAA;AAoB7D,MAAM,WAAW,YAAY;IACzB,gBAAgB,EAAE,CAAC,GAAG,EAAE,MAAM,KAAK,MAAM,CAAA;IACzC,iBAAiB,EAAE,CAAC,GAAG,EAAE,MAAM,KAAK,MAAM,CAAA;CAC7C;AAED,MAAM,WAAW,YAAY;IACzB,MAAM,EAAE,WAAW,GAAG,WAAW,GAAG,YAAY,CAAA;IAChD,OAAO,EAAE,WAAW,GAAG,WAAW,GAAG,YAAY,CAAA;CACpD;AAED;;;GAGG;AACH,wBAAgB,mBAAmB,CAAC,MAAM,EAAE,YAAY,GAAG,YAAY,CA8BtE;AAED,wBAAgB,qBAAqB,CACjC,KAAK,EAAE;IACH,IAAI,EAAE,MAAM,CAAA;IACZ,MAAM,EAAE,WAAW,EAAE,CAAA;IACrB,UAAU,CAAC,EAAE,WAAW,CAAA;IACxB,mBAAmB,CAAC,EAAE;QAAE,IAAI,EAAE,MAAM,CAAA;KAAE,EAAE,CAAA;IACxC,KAAK,CAAC,EAAE;QAAE,OAAO,EAAE;YAAE,IAAI,EAAE,MAAM,CAAA;SAAE,EAAE,CAAC;QAAC,IAAI,CAAC,EAAE,MAAM,CAAA;KAAE,EAAE,CAAA;IACxD,MAAM,CAAC,EAAE;QAAE,OAAO,EAAE;YAAE,IAAI,EAAE,MAAM,CAAA;SAAE,EAAE,CAAC;QAAC,IAAI,CAAC,EAAE,MAAM,CAAA;KAAE,EAAE,CAAA;IACzD,QAAQ,CAAC,EAAE;QAAE,OAAO,EAAE;YAAE,IAAI,EAAE,MAAM,CAAA;SAAE,EAAE,CAAC;QAAC,IAAI,CAAC,EAAE,MAAM,CAAA;KAAE,EAAE,CAAA;CAC9D,EACD,YAAY,EAAE,YAAY,GAC3B;IACC,MAAM,EAAE,MAAM,CAAA;IACd,cAAc,EAAE,MAAM,EAAE,CAAA;CAC3B,CA4MA;AAED;;;;;;;;;;GAUG;AACH,wBAAgB,0BAA0B,CAAC,MAAM,EAAE;IAC/C,eAAe,EAAE,MAAM,CAAA;IACvB,gBAAgB,EAAE,WAAW,CAAA;IAC7B,eAAe,EAAE,WAAW,EAAE,CAAA;IAC9B,YAAY,EAAE,YAAY,CAAA;CAC7B,GAAG;IACA,MAAM,EAAE,MAAM,CAAA;IACd,cAAc,EAAE,MAAM,EAAE,CAAA;CAC3B,CA+CA"}
1
+ {"version":3,"file":"schema-generator.d.ts","sourceRoot":"","sources":["../../../src/cli/utils/schema-generator.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,4BAA4B,CAAA;AAoB7D,MAAM,WAAW,YAAY;IACzB,gBAAgB,EAAE,CAAC,GAAG,EAAE,MAAM,KAAK,MAAM,CAAA;IACzC,iBAAiB,EAAE,CAAC,GAAG,EAAE,MAAM,KAAK,MAAM,CAAA;CAC7C;AAED,MAAM,WAAW,YAAY;IACzB,MAAM,EAAE,WAAW,GAAG,WAAW,GAAG,YAAY,CAAA;IAChD,OAAO,EAAE,WAAW,GAAG,WAAW,GAAG,YAAY,CAAA;CACpD;AAED;;;GAGG;AACH,wBAAgB,mBAAmB,CAAC,MAAM,EAAE,YAAY,GAAG,YAAY,CA8BtE;AAED,wBAAgB,qBAAqB,CACjC,KAAK,EAAE;IACH,IAAI,EAAE,MAAM,CAAA;IACZ,MAAM,EAAE,WAAW,EAAE,CAAA;IACrB,UAAU,CAAC,EAAE,WAAW,CAAA;IACxB,mBAAmB,CAAC,EAAE;QAAE,IAAI,EAAE,MAAM,CAAA;KAAE,EAAE,CAAA;IACxC,KAAK,CAAC,EAAE;QAAE,OAAO,EAAE;YAAE,IAAI,EAAE,MAAM,CAAA;SAAE,EAAE,CAAC;QAAC,IAAI,CAAC,EAAE,MAAM,CAAA;KAAE,EAAE,CAAA;IACxD,MAAM,CAAC,EAAE;QAAE,OAAO,EAAE;YAAE,IAAI,EAAE,MAAM,CAAA;SAAE,EAAE,CAAC;QAAC,IAAI,CAAC,EAAE,MAAM,CAAA;KAAE,EAAE,CAAA;IACzD,QAAQ,CAAC,EAAE;QAAE,OAAO,EAAE;YAAE,IAAI,EAAE,MAAM,CAAA;SAAE,EAAE,CAAC;QAAC,IAAI,CAAC,EAAE,MAAM,CAAA;KAAE,EAAE,CAAA;CAC9D,EACD,YAAY,EAAE,YAAY,GAC3B;IACC,MAAM,EAAE,MAAM,CAAA;IACd,cAAc,EAAE,MAAM,EAAE,CAAA;CAC3B,CA+MA;AAED;;;;;;;;;;GAUG;AACH,wBAAgB,0BAA0B,CAAC,MAAM,EAAE;IAC/C,eAAe,EAAE,MAAM,CAAA;IACvB,gBAAgB,EAAE,WAAW,CAAA;IAC7B,eAAe,EAAE,WAAW,EAAE,CAAA;IAC9B,YAAY,EAAE,YAAY,CAAA;CAC7B,GAAG;IACA,MAAM,EAAE,MAAM,CAAA;IACd,cAAc,EAAE,MAAM,EAAE,CAAA;CAC3B,CA+CA"}
@@ -152,13 +152,16 @@ export function generateDrizzleSchema(table, caseStyleFns) {
152
152
  }
153
153
  break;
154
154
  case 'map':
155
- case 'tags':
156
155
  case 'photo':
157
156
  case 'video':
158
157
  case 'document':
159
158
  columnType = `varchar('${input.name}', { length: 255 })`; // TEXT type
160
159
  drizzleColumnType = 'varchar';
161
160
  break;
161
+ case 'tags':
162
+ columnType = `longtext('${input.name}')`;
163
+ drizzleColumnType = 'longtext';
164
+ break;
162
165
  default:
163
166
  // TODO: Should I throw an error instead?
164
167
  columnType = `varchar('${input.name}', { length: 255 })`;