nextjs-cms 0.9.19 → 0.9.21
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.
- package/dist/api/index.d.ts +8 -48
- package/dist/api/index.d.ts.map +1 -1
- package/dist/api/lib/serverActions.d.ts +9 -49
- package/dist/api/lib/serverActions.d.ts.map +1 -1
- package/dist/api/lib/serverActions.js +85 -37
- package/dist/api/root.d.ts +16 -96
- package/dist/api/root.d.ts.map +1 -1
- package/dist/api/routers/gallery.d.ts +1 -0
- package/dist/api/routers/gallery.d.ts.map +1 -1
- package/dist/api/routers/gallery.js +36 -8
- package/dist/api/routers/hasItemsSection.d.ts +3 -30
- package/dist/api/routers/hasItemsSection.d.ts.map +1 -1
- package/dist/api/routers/navigation.d.ts +3 -3
- package/dist/api/routers/simpleSection.d.ts +1 -15
- package/dist/api/routers/simpleSection.d.ts.map +1 -1
- package/dist/cli/lib/update-sections.d.ts.map +1 -1
- package/dist/cli/lib/update-sections.js +606 -198
- package/dist/core/factories/FieldFactory.d.ts +1 -1
- package/dist/core/factories/FieldFactory.d.ts.map +1 -1
- package/dist/core/factories/FieldFactory.js +11 -9
- package/dist/core/fields/date-range.d.ts +4 -4
- package/dist/core/fields/date.d.ts.map +1 -1
- package/dist/core/fields/date.js +10 -6
- package/dist/core/fields/select.d.ts +23 -11
- package/dist/core/fields/select.d.ts.map +1 -1
- package/dist/core/fields/select.js +9 -5
- package/dist/core/fields/selectMultiple.d.ts +12 -4
- package/dist/core/fields/selectMultiple.d.ts.map +1 -1
- package/dist/core/fields/selectMultiple.js +10 -6
- package/dist/core/sections/category.d.ts +8 -6
- package/dist/core/sections/category.d.ts.map +1 -1
- package/dist/core/sections/hasItems.d.ts +8 -6
- package/dist/core/sections/hasItems.d.ts.map +1 -1
- package/dist/core/sections/section.d.ts +46 -25
- package/dist/core/sections/section.d.ts.map +1 -1
- package/dist/core/sections/section.js +18 -3
- package/dist/core/sections/simple.d.ts +6 -4
- package/dist/core/sections/simple.d.ts.map +1 -1
- package/dist/core/submit/ItemEditSubmit.d.ts.map +1 -1
- package/dist/core/submit/ItemEditSubmit.js +7 -1
- package/dist/core/submit/LocaleSubmit.d.ts +3 -3
- package/dist/core/submit/LocaleSubmit.d.ts.map +1 -1
- package/dist/core/submit/LocaleSubmit.js +9 -6
- package/dist/core/submit/submit.d.ts +4 -4
- package/dist/core/submit/submit.d.ts.map +1 -1
- package/dist/core/submit/submit.js +13 -9
- package/dist/core/types/index.d.ts +2 -2
- package/dist/core/types/index.d.ts.map +1 -1
- package/dist/db/cms-system-tables.d.ts.map +1 -1
- package/dist/db/cms-system-tables.js +2 -1
- package/dist/db/schema.d.ts +61 -0
- package/dist/db/schema.d.ts.map +1 -1
- package/dist/db/schema.js +8 -0
- package/package.json +3 -3
|
@@ -1,18 +1,17 @@
|
|
|
1
|
-
import { db } from '../../db/client.js';
|
|
2
|
-
import { NextJsCmsTablesTable } from '../../db/schema.js';
|
|
3
|
-
import { eq, sql } from 'drizzle-orm';
|
|
4
1
|
import fs from 'fs';
|
|
5
2
|
import path from 'path';
|
|
6
|
-
import {
|
|
7
|
-
import { getCMSConfig } from '../../core/config/index.js';
|
|
8
|
-
import { CheckboxField, ColorField, DateField, DateRangeField, MapField, NumberField, PasswordField, RichTextField, SelectField, SelectMultipleField, TagsField, TextAreaField, TextField, textAreaField, textField, numberField, } from '../../core/fields/index.js';
|
|
9
|
-
import { is } from '../../core/helpers/index.js';
|
|
10
|
-
import { FileField } from '../../core/fields/index.js';
|
|
3
|
+
import { intro, log, select, spinner } from '@clack/prompts';
|
|
11
4
|
import chalk from 'chalk';
|
|
12
|
-
import {
|
|
5
|
+
import { eq, sql } from 'drizzle-orm';
|
|
6
|
+
import { getCMSConfig } from '../../core/config/index.js';
|
|
13
7
|
import { MysqlTableChecker } from '../../core/db/index.js';
|
|
14
|
-
import {
|
|
8
|
+
import { SectionFactory } from '../../core/factories/index.js';
|
|
9
|
+
import { CheckboxField, ColorField, DateField, DateRangeField, FileField, MapField, NumberField, numberField, PasswordField, RichTextField, SelectField, SelectMultipleField, TagsField, TextAreaField, textAreaField, TextField, textField, } from '../../core/fields/index.js';
|
|
10
|
+
import { is } from '../../core/helpers/index.js';
|
|
11
|
+
import { db } from '../../db/client.js';
|
|
12
|
+
import { NextJsCmsTablesTable } from '../../db/schema.js';
|
|
15
13
|
import { addTableKeys } from '../utils/add-table-keys.js';
|
|
14
|
+
import { generateDrizzleSchema, generateLocalesTableSchema, resolveCaseStyleFns } from '../utils/schema-generator.js';
|
|
16
15
|
/**
|
|
17
16
|
* Returns all DB column names a field owns.
|
|
18
17
|
* Most fields own a single column (field.name), but rarely some fields own two,
|
|
@@ -215,6 +214,7 @@ function resolveCreateTableOptions(sectionType) {
|
|
|
215
214
|
createdAt: true,
|
|
216
215
|
};
|
|
217
216
|
case 'destinationDb':
|
|
217
|
+
case 'selectDb':
|
|
218
218
|
case 'locales':
|
|
219
219
|
return {
|
|
220
220
|
createdAt: true,
|
|
@@ -231,17 +231,157 @@ function resolveCreateTableOptions(sectionType) {
|
|
|
231
231
|
};
|
|
232
232
|
}
|
|
233
233
|
}
|
|
234
|
-
const
|
|
235
|
-
|
|
236
|
-
|
|
234
|
+
const STORED_LOCALIZATION_KEY = 'localization';
|
|
235
|
+
const STORED_LOCALIZATION_VERSION = 1;
|
|
236
|
+
function isStoredLocaleConfig(value) {
|
|
237
|
+
return (typeof value === 'object' &&
|
|
238
|
+
value !== null &&
|
|
239
|
+
'code' in value &&
|
|
240
|
+
'label' in value &&
|
|
241
|
+
typeof value.code === 'string' &&
|
|
242
|
+
typeof value.label === 'string' &&
|
|
243
|
+
(!('rtl' in value) || typeof value.rtl === 'boolean' || value.rtl === undefined));
|
|
244
|
+
}
|
|
245
|
+
function isStoredLocalizationTransition(value) {
|
|
246
|
+
return (typeof value === 'object' &&
|
|
247
|
+
value !== null &&
|
|
248
|
+
'type' in value &&
|
|
249
|
+
(value.type === 'enable' || value.type === 'disable') &&
|
|
250
|
+
'status' in value &&
|
|
251
|
+
value.status === 'pending' &&
|
|
252
|
+
'startedAt' in value &&
|
|
253
|
+
typeof value.startedAt === 'string' &&
|
|
254
|
+
(!('lastAttemptAt' in value) || typeof value.lastAttemptAt === 'string' || value.lastAttemptAt === undefined));
|
|
255
|
+
}
|
|
256
|
+
function isStoredLocalizationState(value) {
|
|
257
|
+
if (typeof value !== 'object' ||
|
|
258
|
+
value === null ||
|
|
259
|
+
!('version' in value) ||
|
|
260
|
+
value.version !== STORED_LOCALIZATION_VERSION ||
|
|
261
|
+
!('enabled' in value) ||
|
|
262
|
+
typeof value.enabled !== 'boolean') {
|
|
263
|
+
return false;
|
|
264
|
+
}
|
|
265
|
+
if (value.enabled === true) {
|
|
266
|
+
return ('defaultLocale' in value &&
|
|
267
|
+
typeof value.defaultLocale === 'string' &&
|
|
268
|
+
'locales' in value &&
|
|
269
|
+
Array.isArray(value.locales) &&
|
|
270
|
+
value.locales.every(isStoredLocaleConfig) &&
|
|
271
|
+
(!('transition' in value) ||
|
|
272
|
+
value.transition === undefined ||
|
|
273
|
+
(isStoredLocalizationTransition(value.transition) && value.transition.type === 'disable')));
|
|
274
|
+
}
|
|
275
|
+
if ('transition' in value && value.transition !== undefined) {
|
|
276
|
+
return (isStoredLocalizationTransition(value.transition) &&
|
|
277
|
+
value.transition.type === 'enable' &&
|
|
278
|
+
'defaultLocale' in value &&
|
|
279
|
+
typeof value.defaultLocale === 'string' &&
|
|
280
|
+
'locales' in value &&
|
|
281
|
+
Array.isArray(value.locales) &&
|
|
282
|
+
value.locales.every(isStoredLocaleConfig));
|
|
283
|
+
}
|
|
284
|
+
return (!('defaultLocale' in value) && !('locales' in value) && !('previous' in value) && !('lastDisabledAt' in value));
|
|
285
|
+
}
|
|
286
|
+
function isStoredPendingEnableLocalizationState(state) {
|
|
287
|
+
return state !== null && state !== undefined && !state.enabled && state.transition?.type === 'enable';
|
|
288
|
+
}
|
|
289
|
+
function isStoredDisabledLocalizationState(state) {
|
|
290
|
+
return state !== null && state !== undefined && !state.enabled && state.transition === undefined;
|
|
291
|
+
}
|
|
292
|
+
function parseStoredLocalizationState(value) {
|
|
293
|
+
if (!value) {
|
|
294
|
+
return null;
|
|
295
|
+
}
|
|
296
|
+
let parsed;
|
|
297
|
+
try {
|
|
298
|
+
parsed = JSON.parse(value);
|
|
299
|
+
}
|
|
300
|
+
catch {
|
|
301
|
+
throw new Error(`Invalid stored localization state in '__nextjs_cms_config'.`);
|
|
302
|
+
}
|
|
303
|
+
if (!isStoredLocalizationState(parsed)) {
|
|
304
|
+
throw new Error(`Invalid stored localization state in '__nextjs_cms_config'.`);
|
|
305
|
+
}
|
|
306
|
+
return parsed;
|
|
307
|
+
}
|
|
308
|
+
async function readStoredLocalizationState() {
|
|
309
|
+
const [rows] = await db.execute(sql `SELECT \`value\` FROM \`__nextjs_cms_config\` WHERE \`key\` = ${STORED_LOCALIZATION_KEY} LIMIT 1`);
|
|
237
310
|
const row = rows?.[0];
|
|
238
|
-
return row?.value ?? null;
|
|
311
|
+
return parseStoredLocalizationState(row?.value ?? null);
|
|
312
|
+
}
|
|
313
|
+
async function writeStoredLocalizationState(state) {
|
|
314
|
+
const value = JSON.stringify(state);
|
|
315
|
+
await db.execute(sql `INSERT INTO \`__nextjs_cms_config\` (\`key\`, \`value\`) VALUES (${STORED_LOCALIZATION_KEY}, ${value}) ON DUPLICATE KEY UPDATE \`value\` = ${value}`);
|
|
239
316
|
}
|
|
240
|
-
|
|
241
|
-
|
|
317
|
+
function buildStoredEnabledLocalizationState(localization) {
|
|
318
|
+
return {
|
|
319
|
+
version: STORED_LOCALIZATION_VERSION,
|
|
320
|
+
enabled: true,
|
|
321
|
+
defaultLocale: localization.defaultLocale,
|
|
322
|
+
locales: localization.locales.map((locale) => ({
|
|
323
|
+
code: locale.code,
|
|
324
|
+
label: locale.label,
|
|
325
|
+
rtl: locale.rtl,
|
|
326
|
+
})),
|
|
327
|
+
};
|
|
328
|
+
}
|
|
329
|
+
function buildStoredPendingEnableLocalizationState(targetState, transition) {
|
|
330
|
+
return {
|
|
331
|
+
version: STORED_LOCALIZATION_VERSION,
|
|
332
|
+
enabled: false,
|
|
333
|
+
defaultLocale: targetState.defaultLocale,
|
|
334
|
+
locales: targetState.locales,
|
|
335
|
+
transition,
|
|
336
|
+
};
|
|
337
|
+
}
|
|
338
|
+
async function getRegisteredLocalizationArtifacts(existingTables) {
|
|
339
|
+
const localeColumnTables = [];
|
|
340
|
+
const localesTables = [];
|
|
341
|
+
for (const existing of existingTables) {
|
|
342
|
+
const cols = await MysqlTableChecker.getColumns(existing.name).catch(() => []);
|
|
343
|
+
if (cols.includes('locale')) {
|
|
344
|
+
localeColumnTables.push(existing.name);
|
|
345
|
+
}
|
|
346
|
+
if (existing.name.endsWith('_locales')) {
|
|
347
|
+
const stillExists = (await MysqlTableChecker.getExistingTableStructure(existing.name)) !== null;
|
|
348
|
+
if (stillExists) {
|
|
349
|
+
localesTables.push(existing.name);
|
|
350
|
+
}
|
|
351
|
+
}
|
|
352
|
+
}
|
|
353
|
+
return { localeColumnTables, localesTables };
|
|
242
354
|
}
|
|
243
|
-
|
|
244
|
-
|
|
355
|
+
function isExternalSelectDbField(field) {
|
|
356
|
+
return ((is(field, SelectField) || is(field, SelectMultipleField)) &&
|
|
357
|
+
field.optionsType === 'db' &&
|
|
358
|
+
field.db !== null &&
|
|
359
|
+
field.db !== undefined);
|
|
360
|
+
}
|
|
361
|
+
function buildSelectDbFieldConfig(name, label, type = 'text', required = true) {
|
|
362
|
+
if (type === 'number') {
|
|
363
|
+
return numberField({
|
|
364
|
+
name,
|
|
365
|
+
label,
|
|
366
|
+
required,
|
|
367
|
+
order: 0,
|
|
368
|
+
});
|
|
369
|
+
}
|
|
370
|
+
return textField({
|
|
371
|
+
name,
|
|
372
|
+
label,
|
|
373
|
+
required,
|
|
374
|
+
order: 0,
|
|
375
|
+
});
|
|
376
|
+
}
|
|
377
|
+
function buildLocaleFieldConfig() {
|
|
378
|
+
return textField({
|
|
379
|
+
name: 'locale',
|
|
380
|
+
label: 'Locale',
|
|
381
|
+
required: true,
|
|
382
|
+
maxLength: 10,
|
|
383
|
+
order: 0,
|
|
384
|
+
});
|
|
245
385
|
}
|
|
246
386
|
async function ensureTableRegistryEntry(tableName, sectionName) {
|
|
247
387
|
await db
|
|
@@ -370,6 +510,108 @@ async function renameTable(oldName, newName) {
|
|
|
370
510
|
console.error(`Error renaming table \`${oldName}\` to \`${newName}\`:`, error);
|
|
371
511
|
}
|
|
372
512
|
}
|
|
513
|
+
const LOCALIZED_ASSET_DIRECTORIES = {
|
|
514
|
+
photo: ['.photos', '.thumbs'],
|
|
515
|
+
document: ['.documents'],
|
|
516
|
+
video: ['.videos'],
|
|
517
|
+
};
|
|
518
|
+
function getLocalizedAssetFields(fieldConfigs) {
|
|
519
|
+
return fieldConfigs
|
|
520
|
+
.filter((field) => {
|
|
521
|
+
return ('localized' in field &&
|
|
522
|
+
field.localized === true &&
|
|
523
|
+
(field.type === 'photo' || field.type === 'document' || field.type === 'video'));
|
|
524
|
+
})
|
|
525
|
+
.map((field) => ({
|
|
526
|
+
name: field.name,
|
|
527
|
+
type: field.type,
|
|
528
|
+
}));
|
|
529
|
+
}
|
|
530
|
+
async function unlinkIfExists(filePath) {
|
|
531
|
+
try {
|
|
532
|
+
await fs.promises.unlink(filePath);
|
|
533
|
+
return true;
|
|
534
|
+
}
|
|
535
|
+
catch (error) {
|
|
536
|
+
if (error && typeof error === 'object' && 'code' in error && error.code === 'ENOENT')
|
|
537
|
+
return false;
|
|
538
|
+
throw error;
|
|
539
|
+
}
|
|
540
|
+
}
|
|
541
|
+
async function deleteLocalizedAssetFilesFromLocalesTable(tableName, metadata, uploadsFolder) {
|
|
542
|
+
const result = { deletedFiles: 0, failedFiles: [] };
|
|
543
|
+
if (!metadata || metadata.fields.length === 0)
|
|
544
|
+
return result;
|
|
545
|
+
const columns = metadata.fields.map((field) => `\`${field.name}\``).join(', ');
|
|
546
|
+
const [rows] = await db.execute(sql `SELECT ${sql.raw(columns)} FROM ${sql.raw(`\`${tableName}\``)}`);
|
|
547
|
+
for (const row of rows ?? []) {
|
|
548
|
+
for (const field of metadata.fields) {
|
|
549
|
+
const fileName = row[field.name];
|
|
550
|
+
if (typeof fileName !== 'string' || fileName.trim() === '')
|
|
551
|
+
continue;
|
|
552
|
+
for (const directory of LOCALIZED_ASSET_DIRECTORIES[field.type]) {
|
|
553
|
+
const filePath = path.join(uploadsFolder, directory, metadata.sectionName, fileName);
|
|
554
|
+
try {
|
|
555
|
+
const unlinked = await unlinkIfExists(filePath);
|
|
556
|
+
if (unlinked)
|
|
557
|
+
result.deletedFiles++;
|
|
558
|
+
}
|
|
559
|
+
catch (error) {
|
|
560
|
+
// Non-ENOENT failure (e.g. EPERM, EACCES, EBUSY). Record and keep going so
|
|
561
|
+
// remaining files still get a chance to be cleaned before the table drops.
|
|
562
|
+
// TODO: report failed unlinks to the system_issues subsystem once it lands,
|
|
563
|
+
// so admins can retry/resolve them from the UI instead of digging through logs.
|
|
564
|
+
result.failedFiles.push({ filePath, error });
|
|
565
|
+
}
|
|
566
|
+
}
|
|
567
|
+
}
|
|
568
|
+
}
|
|
569
|
+
return result;
|
|
570
|
+
}
|
|
571
|
+
async function deleteGalleryLocaleFiles(tableName, photoFieldName, sectionName, defaultLocaleCode, uploadsFolder) {
|
|
572
|
+
const result = { deletedFiles: 0, failedFiles: [] };
|
|
573
|
+
const [rows] = await db.execute(sql `SELECT \`${sql.raw(photoFieldName)}\` AS photo FROM ${sql.raw(`\`${tableName}\``)} WHERE \`locale\` != ${defaultLocaleCode}`);
|
|
574
|
+
for (const row of rows ?? []) {
|
|
575
|
+
if (typeof row.photo !== 'string' || row.photo.trim() === '')
|
|
576
|
+
continue;
|
|
577
|
+
for (const directory of ['.photos', '.thumbs']) {
|
|
578
|
+
const filePath = path.join(uploadsFolder, directory, sectionName, row.photo);
|
|
579
|
+
try {
|
|
580
|
+
const unlinked = await unlinkIfExists(filePath);
|
|
581
|
+
if (unlinked)
|
|
582
|
+
result.deletedFiles++;
|
|
583
|
+
}
|
|
584
|
+
catch (error) {
|
|
585
|
+
// Non-ENOENT failure (e.g. EPERM, EACCES, EBUSY). Record and keep going so
|
|
586
|
+
// remaining files still get a chance to be cleaned before the rows are deleted.
|
|
587
|
+
// TODO: report failed unlinks to the system_issues subsystem once it lands,
|
|
588
|
+
// so admins can retry/resolve them from the UI instead of digging through logs.
|
|
589
|
+
result.failedFiles.push({ filePath, error });
|
|
590
|
+
}
|
|
591
|
+
}
|
|
592
|
+
}
|
|
593
|
+
return result;
|
|
594
|
+
}
|
|
595
|
+
async function deleteEditorPhotosLocaleFiles(defaultLocaleCode, uploadsFolder) {
|
|
596
|
+
const result = { deletedFiles: 0, failedFiles: [] };
|
|
597
|
+
const [nondefaultRows] = await db.execute(sql `SELECT \`photo\`, \`section\` FROM \`editor_photos\` WHERE \`locale\` != ${defaultLocaleCode}`);
|
|
598
|
+
for (const row of nondefaultRows ?? []) {
|
|
599
|
+
const filePath = path.join(uploadsFolder, '.photos', row.section, row.photo);
|
|
600
|
+
try {
|
|
601
|
+
const unlinked = await unlinkIfExists(filePath);
|
|
602
|
+
if (unlinked)
|
|
603
|
+
result.deletedFiles++;
|
|
604
|
+
}
|
|
605
|
+
catch (error) {
|
|
606
|
+
// Non-ENOENT failure (e.g. EPERM, EACCES, EBUSY). Record and keep going so
|
|
607
|
+
// remaining files still get a chance to be cleaned before the rows are deleted.
|
|
608
|
+
// TODO: report failed unlinks to the system_issues subsystem once it lands,
|
|
609
|
+
// so admins can retry/resolve them from the UI instead of digging through logs.
|
|
610
|
+
result.failedFiles.push({ filePath, error });
|
|
611
|
+
}
|
|
612
|
+
}
|
|
613
|
+
return result;
|
|
614
|
+
}
|
|
373
615
|
async function updateTable(table, s, ctx) {
|
|
374
616
|
console.log();
|
|
375
617
|
console.log(chalk.blueBright(`Updating table '${table.name}' for section '${table.sectionName}'`));
|
|
@@ -428,14 +670,14 @@ async function updateTable(table, s, ctx) {
|
|
|
428
670
|
}
|
|
429
671
|
}
|
|
430
672
|
/**
|
|
431
|
-
* Special handling: if this is a
|
|
673
|
+
* Special handling: if this is a localization-owned table and 'locale' is being added
|
|
432
674
|
* (either because localization was newly enabled OR a field was flipped to localized:true),
|
|
433
675
|
* run the 3-step migration inline so rows are backfilled with defaultLocale and the
|
|
434
676
|
* column ends up NOT NULL (required for the primary key to include it).
|
|
435
677
|
*/
|
|
436
678
|
const defaultLocaleCode = ctx?.defaultLocaleCode ?? 'en';
|
|
437
679
|
const localeAddIndex = fieldsToAdd.findIndex((f) => f.name === 'locale');
|
|
438
|
-
if (table.sectionType === 'destinationDb' && localeAddIndex !== -1) {
|
|
680
|
+
if ((table.sectionType === 'destinationDb' || table.sectionType === 'gallery') && localeAddIndex !== -1) {
|
|
439
681
|
const [localeField] = fieldsToAdd.splice(localeAddIndex, 1);
|
|
440
682
|
try {
|
|
441
683
|
await db.execute(sql `ALTER TABLE \`${sql.raw(table.name)}\` ADD COLUMN \`locale\` VARCHAR(10) DEFAULT NULL`);
|
|
@@ -540,18 +782,21 @@ async function updateTable(table, s, ctx) {
|
|
|
540
782
|
}
|
|
541
783
|
}
|
|
542
784
|
/**
|
|
543
|
-
* Special handling: if this is a
|
|
544
|
-
*
|
|
785
|
+
* Special handling: if this is a localization-owned table and 'locale' is being removed.
|
|
786
|
+
* For destinationDb tables, this means a select/tags field was delocalized.
|
|
787
|
+
* For gallery tables, this means gallery.localized was disabled or global localization was disabled.
|
|
545
788
|
* Handle this with a detailed prompt and proper data cleanup.
|
|
546
789
|
*/
|
|
547
|
-
if (table.sectionType === 'destinationDb'
|
|
790
|
+
if ((table.sectionType === 'destinationDb' || table.sectionType === 'gallery') &&
|
|
791
|
+
fieldsToRemove.includes('locale')) {
|
|
548
792
|
fieldsToRemove = fieldsToRemove.filter((f) => f !== 'locale');
|
|
549
|
-
// When localization was globally disabled, we already asked upfront
|
|
793
|
+
// When localization was globally disabled, we already asked upfront - auto-confirm here.
|
|
550
794
|
let shouldDelocalize = ctx?.localizationTransition === 'disable' ? 'yes' : 'no';
|
|
551
795
|
if (ctx?.localizationTransition !== 'disable') {
|
|
552
796
|
s.stop();
|
|
797
|
+
const tableDescription = table.sectionType === 'gallery' ? 'gallery table' : 'junction table';
|
|
553
798
|
shouldDelocalize = await select({
|
|
554
|
-
message: `The 'locale' column in
|
|
799
|
+
message: `The 'locale' column in ${tableDescription} '${table.name}' is no longer needed. This will:\n` +
|
|
555
800
|
` 1. Delete all non-default-locale rows (locale != '${defaultLocaleCode}')\n` +
|
|
556
801
|
` 2. Drop the 'locale' column from the table\n` +
|
|
557
802
|
` 3. Update the primary key to exclude 'locale'\n` +
|
|
@@ -566,6 +811,26 @@ async function updateTable(table, s, ctx) {
|
|
|
566
811
|
}
|
|
567
812
|
if (shouldDelocalize === 'yes') {
|
|
568
813
|
try {
|
|
814
|
+
if (table.sectionType === 'gallery') {
|
|
815
|
+
const photoFieldName = table.gallery?.photoNameField ?? 'photo';
|
|
816
|
+
try {
|
|
817
|
+
const { deletedFiles, failedFiles } = await deleteGalleryLocaleFiles(table.name, photoFieldName, table.sectionName, defaultLocaleCode, ctx?.uploadsFolder ?? '');
|
|
818
|
+
if (deletedFiles > 0) {
|
|
819
|
+
console.log(chalk.gray(` - Deleted ${deletedFiles} gallery file(s) for non-default locales from '${table.sectionName}'`));
|
|
820
|
+
}
|
|
821
|
+
if (failedFiles.length > 0) {
|
|
822
|
+
console.error(chalk.red(` - Failed to delete ${failedFiles.length} gallery file(s) from '${table.sectionName}'. These files were left on disk:`));
|
|
823
|
+
for (const { filePath, error } of failedFiles) {
|
|
824
|
+
console.error(chalk.red(` ${filePath}: ${String(error)}`));
|
|
825
|
+
}
|
|
826
|
+
}
|
|
827
|
+
}
|
|
828
|
+
catch (error) {
|
|
829
|
+
// Reached only when the SELECT itself fails (DB-level error).
|
|
830
|
+
// Per-file unlink failures are collected inside and reported above.
|
|
831
|
+
console.error(chalk.red(` - Error reading '${table.name}' to clean up gallery locale files:`, error));
|
|
832
|
+
}
|
|
833
|
+
}
|
|
569
834
|
// Delete all non-default-locale rows
|
|
570
835
|
await db.execute(sql `DELETE FROM ${sql.raw(`\`${table.name}\``)} WHERE \`locale\` != ${defaultLocaleCode}`);
|
|
571
836
|
log.info(chalk.gray(` - Deleted non-default-locale rows from '${table.name}'`));
|
|
@@ -573,6 +838,14 @@ async function updateTable(table, s, ctx) {
|
|
|
573
838
|
catch (error) {
|
|
574
839
|
console.error(chalk.red(` - Error deleting locale rows from '${table.name}':`, error));
|
|
575
840
|
}
|
|
841
|
+
if (existingKeys?.primaryKeys.includes('locale')) {
|
|
842
|
+
alterTableSQLs.push({
|
|
843
|
+
field: 'primary key',
|
|
844
|
+
table: table.name,
|
|
845
|
+
action: 'remove',
|
|
846
|
+
sql: `DROP PRIMARY KEY`,
|
|
847
|
+
});
|
|
848
|
+
}
|
|
576
849
|
alterTableSQLs.push({
|
|
577
850
|
field: 'locale',
|
|
578
851
|
table: table.name,
|
|
@@ -644,7 +917,8 @@ async function updateTable(table, s, ctx) {
|
|
|
644
917
|
/**
|
|
645
918
|
* Add the keys to the table
|
|
646
919
|
*/
|
|
647
|
-
const
|
|
920
|
+
const updatedKeys = await MysqlTableChecker.getExistingKeys(table.name);
|
|
921
|
+
const keyErrors = await addTableKeys(table, updatedKeys);
|
|
648
922
|
sqlErrors += keyErrors;
|
|
649
923
|
s.stop(chalk.italic.hex(`${sqlErrors > 0 ? `#FFA500` : `#fafafa`}`)(`- Table \`${table.name}\` modified successfully ${sqlErrors > 0 ? `with ${sqlErrors} error(s).` : ''}`));
|
|
650
924
|
}
|
|
@@ -671,6 +945,8 @@ const main = async (s) => {
|
|
|
671
945
|
let sections = [];
|
|
672
946
|
const desiredTables = [];
|
|
673
947
|
let existingTables = [];
|
|
948
|
+
const externalSelectDbTables = new Set();
|
|
949
|
+
const localesTableAssetMetadata = new Map();
|
|
674
950
|
sections = await SectionFactory.getSectionsSilently();
|
|
675
951
|
console.log(`Found ${sections.length} section(s) to insert: `);
|
|
676
952
|
console.log(chalk.gray(sections.map((s) => s.name).join(', ')));
|
|
@@ -678,25 +954,24 @@ const main = async (s) => {
|
|
|
678
954
|
* Let's see if the table `__nextjs_cms_tables` exists in the database.
|
|
679
955
|
* If it doesn't, we'll create it using the same schema as `NextJsCmsTablesTable`.
|
|
680
956
|
*/
|
|
681
|
-
await db.execute(sql `
|
|
682
|
-
CREATE TABLE IF NOT EXISTS __nextjs_cms_tables (
|
|
683
|
-
name VARCHAR(100) NOT NULL PRIMARY KEY,
|
|
684
|
-
section VARCHAR(200),
|
|
685
|
-
created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP
|
|
686
|
-
)
|
|
957
|
+
await db.execute(sql `
|
|
958
|
+
CREATE TABLE IF NOT EXISTS __nextjs_cms_tables (
|
|
959
|
+
name VARCHAR(100) NOT NULL PRIMARY KEY,
|
|
960
|
+
section VARCHAR(200),
|
|
961
|
+
created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP
|
|
962
|
+
)
|
|
687
963
|
`);
|
|
688
964
|
/**
|
|
689
|
-
* Persistent key/value store for CMS-level state that must survive config removal
|
|
690
|
-
* most importantly the original `localization.defaultLocale`, which we need to clean
|
|
691
|
-
* up locale-scoped rows correctly when localization is later disabled.
|
|
965
|
+
* Persistent key/value store for CMS-level state that must survive config removal.
|
|
692
966
|
*/
|
|
693
|
-
await db.execute(sql `
|
|
694
|
-
CREATE TABLE IF NOT EXISTS __nextjs_cms_config (
|
|
695
|
-
\`key\` VARCHAR(100) NOT NULL PRIMARY KEY,
|
|
696
|
-
\`value\`
|
|
697
|
-
updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP
|
|
698
|
-
)
|
|
967
|
+
await db.execute(sql `
|
|
968
|
+
CREATE TABLE IF NOT EXISTS __nextjs_cms_config (
|
|
969
|
+
\`key\` VARCHAR(100) NOT NULL PRIMARY KEY,
|
|
970
|
+
\`value\` LONGTEXT,
|
|
971
|
+
updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP
|
|
972
|
+
)
|
|
699
973
|
`);
|
|
974
|
+
await db.execute(sql `ALTER TABLE \`__nextjs_cms_config\` MODIFY COLUMN \`value\` LONGTEXT`);
|
|
700
975
|
/**
|
|
701
976
|
* Get the existing tables from the database
|
|
702
977
|
*/
|
|
@@ -719,6 +994,10 @@ const main = async (s) => {
|
|
|
719
994
|
for (const _s of sections) {
|
|
720
995
|
const s = _s.build();
|
|
721
996
|
s.buildFields();
|
|
997
|
+
localesTableAssetMetadata.set(s.localesTableName, {
|
|
998
|
+
sectionName: s.name,
|
|
999
|
+
fields: getLocalizedAssetFields(s.fieldConfigs),
|
|
1000
|
+
});
|
|
722
1001
|
if (s.db.table.endsWith(RESERVED_TABLE_SUFFIX)) {
|
|
723
1002
|
reservedNameErrors.push(`Section '${s.name}': table name '${s.db.table}' ends with reserved suffix '${RESERVED_TABLE_SUFFIX}'.`);
|
|
724
1003
|
}
|
|
@@ -735,6 +1014,15 @@ const main = async (s) => {
|
|
|
735
1014
|
reservedNameErrors.push(`Section '${s.name}', field '${field.name}': destinationDb identifier column name '${RESERVED_COLUMN_NAME}' is reserved.`);
|
|
736
1015
|
}
|
|
737
1016
|
}
|
|
1017
|
+
if (isExternalSelectDbField(field)) {
|
|
1018
|
+
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}'.`);
|
|
1020
|
+
}
|
|
1021
|
+
const reservedColumns = [field.db.identifier, field.db.label, field.db.orderBy].filter((column) => column === RESERVED_COLUMN_NAME);
|
|
1022
|
+
if (reservedColumns.length > 0) {
|
|
1023
|
+
reservedNameErrors.push(`Section '${s.name}', field '${field.name}': db column name '${RESERVED_COLUMN_NAME}' is reserved.`);
|
|
1024
|
+
}
|
|
1025
|
+
}
|
|
738
1026
|
}
|
|
739
1027
|
const galleryForValidation = await s.getGallery();
|
|
740
1028
|
if (galleryForValidation?.db.tableName && galleryForValidation.db.tableName.endsWith(RESERVED_TABLE_SUFFIX)) {
|
|
@@ -776,9 +1064,32 @@ const main = async (s) => {
|
|
|
776
1064
|
* Check for `destinationDb`
|
|
777
1065
|
*/
|
|
778
1066
|
for (const field of s.fields) {
|
|
779
|
-
|
|
780
|
-
|
|
781
|
-
|
|
1067
|
+
if (isExternalSelectDbField(field) && !externalSelectDbTables.has(field.db.table)) {
|
|
1068
|
+
externalSelectDbTables.add(field.db.table);
|
|
1069
|
+
const identifierFieldConfig = buildSelectDbFieldConfig(field.db.identifier, 'Option Id', field.db.identifierType ?? 'text');
|
|
1070
|
+
const labelFieldConfig = buildSelectDbFieldConfig(field.db.label, 'Option Label');
|
|
1071
|
+
const selectDbFieldConfigs = [identifierFieldConfig, labelFieldConfig];
|
|
1072
|
+
if (field.db.orderBy &&
|
|
1073
|
+
field.db.orderBy !== field.db.identifier &&
|
|
1074
|
+
field.db.orderBy !== field.db.label) {
|
|
1075
|
+
selectDbFieldConfigs.push(buildSelectDbFieldConfig(field.db.orderBy, 'Option Order', 'text', false));
|
|
1076
|
+
}
|
|
1077
|
+
desiredTables.push({
|
|
1078
|
+
name: field.db.table,
|
|
1079
|
+
fields: selectDbFieldConfigs.map((fieldConfig) => fieldConfig.build()),
|
|
1080
|
+
sectionName: s.name,
|
|
1081
|
+
sectionType: 'selectDb',
|
|
1082
|
+
identifier: identifierFieldConfig,
|
|
1083
|
+
primaryKey: [identifierFieldConfig],
|
|
1084
|
+
});
|
|
1085
|
+
const selectDbSchema = generateDrizzleSchema({
|
|
1086
|
+
name: field.db.table,
|
|
1087
|
+
fields: selectDbFieldConfigs,
|
|
1088
|
+
identifier: identifierFieldConfig,
|
|
1089
|
+
}, caseStyleFns);
|
|
1090
|
+
drizzleTableSchemas.push(selectDbSchema.schema);
|
|
1091
|
+
selectDbSchema.drizzleImports.forEach((type) => drizzleImports.add(type));
|
|
1092
|
+
}
|
|
782
1093
|
if (field.destinationDb) {
|
|
783
1094
|
console.log('Destination DB found for input:', field.name, 'with table:', field.destinationDb.table);
|
|
784
1095
|
const parentIdentifierType = s.db.identifier.type;
|
|
@@ -869,6 +1180,7 @@ const main = async (s) => {
|
|
|
869
1180
|
const gallery = await s.getGallery();
|
|
870
1181
|
if (gallery?.db.tableName) {
|
|
871
1182
|
console.log('Gallery found for section:', s.name, 'with table:', gallery.db.tableName);
|
|
1183
|
+
const isLocalizedGallery = gallery.localized === true && cmsConfig.localization?.enabled === true;
|
|
872
1184
|
const photoField = textField({
|
|
873
1185
|
name: gallery.db.photoNameField || 'photo',
|
|
874
1186
|
label: 'Photo Name',
|
|
@@ -888,58 +1200,85 @@ const main = async (s) => {
|
|
|
888
1200
|
required: true,
|
|
889
1201
|
order: 0,
|
|
890
1202
|
});
|
|
1203
|
+
const localeFieldConfig = isLocalizedGallery ? buildLocaleFieldConfig() : undefined;
|
|
1204
|
+
const galleryFields = [
|
|
1205
|
+
galleryRefField.build(),
|
|
1206
|
+
photoField.build(),
|
|
1207
|
+
textAreaField({
|
|
1208
|
+
name: gallery.db.metaField || 'meta',
|
|
1209
|
+
label: 'Photo Information',
|
|
1210
|
+
required: false,
|
|
1211
|
+
order: 0,
|
|
1212
|
+
}).build(),
|
|
1213
|
+
...(localeFieldConfig ? [localeFieldConfig.build()] : []),
|
|
1214
|
+
];
|
|
1215
|
+
const galleryPrimaryKey = localeFieldConfig ? [photoField, localeFieldConfig] : [photoField];
|
|
891
1216
|
desiredTables.push({
|
|
892
1217
|
name: gallery.db.tableName,
|
|
893
|
-
fields:
|
|
894
|
-
galleryRefField.build(),
|
|
895
|
-
photoField.build(),
|
|
896
|
-
textAreaField({
|
|
897
|
-
name: gallery.db.metaField || 'meta',
|
|
898
|
-
label: 'Photo Information',
|
|
899
|
-
required: false,
|
|
900
|
-
order: 0,
|
|
901
|
-
}).build(),
|
|
902
|
-
],
|
|
1218
|
+
fields: galleryFields,
|
|
903
1219
|
sectionName: s.name,
|
|
904
1220
|
sectionType: 'gallery',
|
|
905
1221
|
/**
|
|
906
|
-
*
|
|
907
|
-
*
|
|
1222
|
+
* Non-localized galleries keep the photo field as the identifier.
|
|
1223
|
+
* Localized galleries use a composite primary key: photo + locale.
|
|
908
1224
|
*/
|
|
909
|
-
identifier: photoField,
|
|
910
|
-
primaryKey:
|
|
1225
|
+
identifier: isLocalizedGallery ? undefined : photoField,
|
|
1226
|
+
primaryKey: galleryPrimaryKey,
|
|
1227
|
+
gallery: {
|
|
1228
|
+
referenceIdentifierField: gallery.db.referenceIdentifierField || 'reference_id',
|
|
1229
|
+
photoNameField: gallery.db.photoNameField || 'photo',
|
|
1230
|
+
metaField: gallery.db.metaField || 'meta',
|
|
1231
|
+
localized: isLocalizedGallery,
|
|
1232
|
+
},
|
|
911
1233
|
});
|
|
912
|
-
const
|
|
913
|
-
|
|
914
|
-
|
|
915
|
-
|
|
916
|
-
|
|
917
|
-
|
|
918
|
-
|
|
919
|
-
},
|
|
920
|
-
{
|
|
921
|
-
name: gallery.db.photoNameField || 'photo',
|
|
922
|
-
type: 'text',
|
|
923
|
-
required: true,
|
|
924
|
-
},
|
|
925
|
-
{
|
|
926
|
-
name: gallery.db.metaField || 'meta',
|
|
927
|
-
type: 'textarea',
|
|
928
|
-
required: false,
|
|
929
|
-
},
|
|
930
|
-
],
|
|
931
|
-
identifier: {
|
|
1234
|
+
const gallerySchemaFields = [
|
|
1235
|
+
{
|
|
1236
|
+
name: gallery.db.referenceIdentifierField || 'reference_id',
|
|
1237
|
+
type: s.db.identifier.type,
|
|
1238
|
+
required: true,
|
|
1239
|
+
},
|
|
1240
|
+
{
|
|
932
1241
|
name: gallery.db.photoNameField || 'photo',
|
|
933
1242
|
type: 'text',
|
|
1243
|
+
required: true,
|
|
1244
|
+
},
|
|
1245
|
+
{
|
|
1246
|
+
name: gallery.db.metaField || 'meta',
|
|
1247
|
+
type: 'textarea',
|
|
1248
|
+
required: false,
|
|
934
1249
|
},
|
|
1250
|
+
...(isLocalizedGallery
|
|
1251
|
+
? [
|
|
1252
|
+
{
|
|
1253
|
+
name: 'locale',
|
|
1254
|
+
type: 'text',
|
|
1255
|
+
maxLength: 10,
|
|
1256
|
+
required: true,
|
|
1257
|
+
},
|
|
1258
|
+
]
|
|
1259
|
+
: []),
|
|
1260
|
+
];
|
|
1261
|
+
const gallerySchema = generateDrizzleSchema({
|
|
1262
|
+
name: gallery.db.tableName,
|
|
1263
|
+
fields: gallerySchemaFields,
|
|
1264
|
+
...(isLocalizedGallery
|
|
1265
|
+
? {
|
|
1266
|
+
compositePrimaryKey: [{ name: gallery.db.photoNameField || 'photo' }, { name: 'locale' }],
|
|
1267
|
+
}
|
|
1268
|
+
: {
|
|
1269
|
+
identifier: {
|
|
1270
|
+
name: gallery.db.photoNameField || 'photo',
|
|
1271
|
+
type: 'text',
|
|
1272
|
+
},
|
|
1273
|
+
}),
|
|
935
1274
|
}, caseStyleFns);
|
|
936
1275
|
drizzleTableSchemas.push(gallerySchema.schema);
|
|
937
1276
|
gallerySchema.drizzleImports.forEach((type) => drizzleImports.add(type));
|
|
938
1277
|
}
|
|
939
1278
|
/**
|
|
940
|
-
* Check for localized
|
|
1279
|
+
* Check for localized content — generate a _locales table if localization is enabled
|
|
941
1280
|
*/
|
|
942
|
-
if (cmsConfig.localization?.enabled && s.
|
|
1281
|
+
if (cmsConfig.localization?.enabled && s.hasLocalizedContent) {
|
|
943
1282
|
const localesTableName = s.localesTableName;
|
|
944
1283
|
const parentIdFieldConfig = s.db.identifier.type === 'number'
|
|
945
1284
|
? numberField({
|
|
@@ -1053,120 +1392,119 @@ const main = async (s) => {
|
|
|
1053
1392
|
console.log(chalk.gray(tablesToRemove.map((table) => table.name).join(', ')));
|
|
1054
1393
|
console.log(`\n`);
|
|
1055
1394
|
intro(chalk.inverse(' update-sections '));
|
|
1056
|
-
/**
|
|
1057
|
-
* Detect localization transition by inspecting 'editor_photos' — the presence of its 'locale'
|
|
1058
|
-
* column reflects whether localization was enabled on the previous run. Pair this with the
|
|
1059
|
-
* stored `localization.defaultLocale` from `__nextjs_cms_config` so destructive cleanup uses
|
|
1060
|
-
* the correct locale (the one actually used to tag existing rows), not a guess from the
|
|
1061
|
-
* now-missing config.
|
|
1062
|
-
*/
|
|
1063
1395
|
const localizationCurrentlyEnabled = cmsConfig.localization?.enabled === true;
|
|
1064
|
-
const configDefaultLocale = cmsConfig.localization?.defaultLocale;
|
|
1065
1396
|
const editorPhotosColumns = await MysqlTableChecker.getColumns('editor_photos').catch(() => []);
|
|
1066
1397
|
const editorPhotosExists = editorPhotosColumns.length > 0;
|
|
1067
1398
|
const editorPhotosHasLocale = editorPhotosColumns.includes('locale');
|
|
1068
|
-
const
|
|
1069
|
-
|
|
1070
|
-
|
|
1071
|
-
|
|
1072
|
-
|
|
1073
|
-
|
|
1074
|
-
|
|
1075
|
-
|
|
1076
|
-
|
|
1077
|
-
|
|
1078
|
-
|
|
1079
|
-
|
|
1080
|
-
|
|
1399
|
+
const storedLocalizationState = await readStoredLocalizationState();
|
|
1400
|
+
const storedEnabledLocalizationState = storedLocalizationState?.enabled ? storedLocalizationState : null;
|
|
1401
|
+
const storedPendingEnableLocalizationState = isStoredPendingEnableLocalizationState(storedLocalizationState)
|
|
1402
|
+
? storedLocalizationState
|
|
1403
|
+
: null;
|
|
1404
|
+
const storedDisabledLocalizationState = isStoredDisabledLocalizationState(storedLocalizationState)
|
|
1405
|
+
? storedLocalizationState
|
|
1406
|
+
: null;
|
|
1407
|
+
const registeredLocalizationArtifacts = !localizationCurrentlyEnabled || storedDisabledLocalizationState
|
|
1408
|
+
? await getRegisteredLocalizationArtifacts(existingTables)
|
|
1409
|
+
: { localeColumnTables: [], localesTables: [] };
|
|
1410
|
+
const hasRegisteredLocalizationArtifacts = registeredLocalizationArtifacts.localeColumnTables.length > 0 ||
|
|
1411
|
+
registeredLocalizationArtifacts.localesTables.length > 0;
|
|
1412
|
+
const hasLocalizationArtifacts = editorPhotosHasLocale || hasRegisteredLocalizationArtifacts;
|
|
1413
|
+
if (storedDisabledLocalizationState && hasLocalizationArtifacts) {
|
|
1414
|
+
throw new Error(`Stored localization state says localization is disabled, but localization artifacts still exist in the database. Re-enable localization in cms.config.ts or fix the schema before re-running update-sections.`);
|
|
1415
|
+
}
|
|
1416
|
+
if (!localizationCurrentlyEnabled && !storedLocalizationState && hasLocalizationArtifacts) {
|
|
1417
|
+
throw new Error(`Cannot disable localization safely: no stored localization state found in '__nextjs_cms_config'. Re-enable localization in cms.config.ts and re-run update-sections before disabling it again.`);
|
|
1081
1418
|
}
|
|
1082
1419
|
let localizationTransition = null;
|
|
1083
|
-
|
|
1084
|
-
|
|
1085
|
-
|
|
1086
|
-
|
|
1087
|
-
|
|
1088
|
-
|
|
1089
|
-
|
|
1420
|
+
let disableCleanupState = null;
|
|
1421
|
+
let enableTargetState = null;
|
|
1422
|
+
let enablePendingState = null;
|
|
1423
|
+
if (localizationCurrentlyEnabled && cmsConfig.localization) {
|
|
1424
|
+
const configLocalizationState = buildStoredEnabledLocalizationState(cmsConfig.localization);
|
|
1425
|
+
enableTargetState = configLocalizationState;
|
|
1426
|
+
if (storedEnabledLocalizationState &&
|
|
1427
|
+
storedEnabledLocalizationState.defaultLocale !== configLocalizationState.defaultLocale) {
|
|
1428
|
+
s.stop(`Aborting. Changing localization defaultLocale is not supported (stored='${storedEnabledLocalizationState.defaultLocale}', config='${configLocalizationState.defaultLocale}'). Revert cms.config.ts and re-run update-sections.`);
|
|
1090
1429
|
return;
|
|
1091
1430
|
}
|
|
1092
|
-
if (
|
|
1093
|
-
|
|
1094
|
-
|
|
1095
|
-
|
|
1431
|
+
if (storedPendingEnableLocalizationState &&
|
|
1432
|
+
storedPendingEnableLocalizationState.defaultLocale !== configLocalizationState.defaultLocale) {
|
|
1433
|
+
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.`);
|
|
1434
|
+
return;
|
|
1096
1435
|
}
|
|
1097
|
-
}
|
|
1098
|
-
// Resolve the locale used for cleanup SQL. For newly-disabled, stored takes precedence;
|
|
1099
|
-
// for newly-enabled, config is the source of truth.
|
|
1100
|
-
const defaultLocaleCode = localizationCurrentlyEnabled
|
|
1101
|
-
? (configDefaultLocale ?? storedDefaultLocale ?? 'en')
|
|
1102
|
-
: (storedDefaultLocale ?? configDefaultLocale ?? 'en');
|
|
1103
|
-
if (editorPhotosExists && localizationCurrentlyEnabled && !editorPhotosHasLocale) {
|
|
1104
1436
|
/**
|
|
1105
1437
|
* Localization is newly enabled: editor_photos lacks the locale column,
|
|
1106
1438
|
* but the config now has localization.enabled = true. We must backfill
|
|
1107
|
-
* editor_photos and any existing
|
|
1439
|
+
* editor_photos and any existing tables that carry localized data
|
|
1108
1440
|
* with the configured defaultLocale.
|
|
1109
1441
|
*/
|
|
1110
|
-
const
|
|
1442
|
+
const affectedLocalizedTables = [];
|
|
1111
1443
|
for (const t of desiredTables) {
|
|
1112
|
-
if (t.sectionType !== 'destinationDb')
|
|
1444
|
+
if (t.sectionType !== 'destinationDb' && t.sectionType !== 'gallery')
|
|
1113
1445
|
continue;
|
|
1114
1446
|
if (!t.fields.some((f) => f.name === 'locale'))
|
|
1115
1447
|
continue;
|
|
1116
1448
|
const cols = await MysqlTableChecker.getColumns(t.name).catch(() => []);
|
|
1117
1449
|
if (cols.length > 0 && !cols.includes('locale')) {
|
|
1118
|
-
|
|
1450
|
+
affectedLocalizedTables.push(t.name);
|
|
1119
1451
|
}
|
|
1120
1452
|
}
|
|
1121
|
-
const
|
|
1122
|
-
|
|
1123
|
-
|
|
1124
|
-
|
|
1125
|
-
|
|
1126
|
-
|
|
1127
|
-
`
|
|
1128
|
-
|
|
1129
|
-
|
|
1130
|
-
|
|
1131
|
-
|
|
1132
|
-
|
|
1133
|
-
|
|
1134
|
-
|
|
1135
|
-
|
|
1136
|
-
|
|
1137
|
-
|
|
1138
|
-
|
|
1139
|
-
|
|
1140
|
-
|
|
1141
|
-
|
|
1142
|
-
|
|
1143
|
-
|
|
1144
|
-
|
|
1145
|
-
|
|
1146
|
-
|
|
1147
|
-
|
|
1148
|
-
|
|
1149
|
-
|
|
1150
|
-
|
|
1453
|
+
const needsEditorPhotosMigration = editorPhotosExists && !editorPhotosHasLocale;
|
|
1454
|
+
const shouldRunEnableTransition = storedPendingEnableLocalizationState || !storedEnabledLocalizationState;
|
|
1455
|
+
if (shouldRunEnableTransition) {
|
|
1456
|
+
const affectedList = [...(needsEditorPhotosMigration ? ['editor_photos'] : []), ...affectedLocalizedTables];
|
|
1457
|
+
const affectedListText = affectedList.length > 0
|
|
1458
|
+
? affectedList.map((n) => ` - ${n}`).join('\n')
|
|
1459
|
+
: ` - No existing tables need a locale backfill; update-sections will verify localized tables after sync.`;
|
|
1460
|
+
const isEnableRetry = storedPendingEnableLocalizationState?.transition.status === 'pending';
|
|
1461
|
+
s.stop();
|
|
1462
|
+
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`) +
|
|
1466
|
+
affectedListText +
|
|
1467
|
+
`\n\n${chalk.redBright('WARNING:')} Existing data in these tables MUST already be in '${configLocalizationState.defaultLocale}'. ` +
|
|
1468
|
+
`Changing defaultLocale after this point is not supported. If your data is not in '${configLocalizationState.defaultLocale}', ` +
|
|
1469
|
+
`stop now and fix your cms.config.ts before re-running update-sections.\n\nProceed?`,
|
|
1470
|
+
options: [
|
|
1471
|
+
{ value: 'yes', label: `Yes, that's correct` },
|
|
1472
|
+
{ value: 'no', label: 'No, stop - let me reconfigure localization in cms.config.ts' },
|
|
1473
|
+
],
|
|
1474
|
+
initialValue: 'no',
|
|
1475
|
+
});
|
|
1476
|
+
if (confirm !== 'yes') {
|
|
1477
|
+
s.stop('Aborted. Reconfigure cms.config.ts and re-run update-sections.');
|
|
1478
|
+
return;
|
|
1479
|
+
}
|
|
1480
|
+
localizationTransition = 'enable';
|
|
1481
|
+
const now = new Date().toISOString();
|
|
1482
|
+
enablePendingState = buildStoredPendingEnableLocalizationState(configLocalizationState, {
|
|
1483
|
+
type: 'enable',
|
|
1484
|
+
status: 'pending',
|
|
1485
|
+
startedAt: storedPendingEnableLocalizationState?.transition.startedAt ?? now,
|
|
1486
|
+
...(isEnableRetry ? { lastAttemptAt: now } : {}),
|
|
1487
|
+
});
|
|
1488
|
+
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
|
+
s.start();
|
|
1151
1501
|
}
|
|
1152
|
-
s.start();
|
|
1153
1502
|
}
|
|
1154
|
-
else if (
|
|
1155
|
-
!localizationCurrentlyEnabled &&
|
|
1156
|
-
(editorPhotosHasLocale || hasResidualLocaleColumn)) {
|
|
1503
|
+
else if (!localizationCurrentlyEnabled && storedEnabledLocalizationState) {
|
|
1157
1504
|
/**
|
|
1158
1505
|
* Localization is newly disabled (or a previous disable run was interrupted):
|
|
1159
|
-
*
|
|
1160
|
-
* but the config no longer has localization.enabled = true. We purge non-default rows
|
|
1161
|
-
* (and their files), drop the locale column from editor_photos and junction tables,
|
|
1162
|
-
* and drop _locales tables. The deletion filter uses the STORED defaultLocale (not
|
|
1163
|
-
* config, which may be missing), so rows tagged with the original base locale survive
|
|
1164
|
-
* regardless of current config.
|
|
1506
|
+
* the stored localization state is still enabled, but cms.config.ts no longer is.
|
|
1165
1507
|
*/
|
|
1166
|
-
if (!storedDefaultLocale) {
|
|
1167
|
-
s.stop(`Aborting. Cannot disable localization safely: no stored defaultLocale found in '__nextjs_cms_config'. This indicates a corrupted state.`);
|
|
1168
|
-
return;
|
|
1169
|
-
}
|
|
1170
1508
|
const affected = [
|
|
1171
1509
|
{ name: 'editor_photos', type: 'editor_photos' },
|
|
1172
1510
|
];
|
|
@@ -1180,23 +1518,26 @@ const main = async (s) => {
|
|
|
1180
1518
|
affected.push({ name: existing.name, type: 'locales' });
|
|
1181
1519
|
}
|
|
1182
1520
|
else {
|
|
1183
|
-
affected.push({ name: existing.name, type: '
|
|
1521
|
+
affected.push({ name: existing.name, type: 'localized_table' });
|
|
1184
1522
|
}
|
|
1185
1523
|
}
|
|
1186
1524
|
s.stop();
|
|
1525
|
+
const isDisableRetry = storedEnabledLocalizationState.transition?.status === 'pending';
|
|
1187
1526
|
const confirm = await select({
|
|
1188
|
-
message:
|
|
1189
|
-
`
|
|
1527
|
+
message: (isDisableRetry
|
|
1528
|
+
? `A previous localization disable attempt did not complete. update-sections will retry cleanup now.\n\n`
|
|
1529
|
+
: `It looks like you disabled localization. The 'locale' column in the tables below is no longer needed and will be dropped.\n\n`) +
|
|
1530
|
+
`Base locale (from '__nextjs_cms_config'): '${storedEnabledLocalizationState.defaultLocale}'. Rows tagged with this locale will be kept; all others will be deleted.\n\n` +
|
|
1190
1531
|
`This will:\n` +
|
|
1191
|
-
` 1. Delete all non-base-locale rows (locale != '${
|
|
1192
|
-
` 2. Drop the 'locale' column from editor_photos and
|
|
1532
|
+
` 1. Delete all non-base-locale rows (locale != '${storedEnabledLocalizationState.defaultLocale}') from editor_photos and localized tables (their files on disk will also be removed where applicable)\n` +
|
|
1533
|
+
` 2. Drop the 'locale' column from editor_photos and localized tables\n` +
|
|
1193
1534
|
` 3. Drop '_locales' tables entirely\n\n` +
|
|
1194
1535
|
`Affected tables:\n` +
|
|
1195
1536
|
affected.map((t) => ` - ${t.name} (${t.type})`).join('\n') +
|
|
1196
1537
|
`\n\nProceed?`,
|
|
1197
1538
|
options: [
|
|
1198
1539
|
{ value: 'yes', label: `Yes, that's correct` },
|
|
1199
|
-
{ value: 'no', label: 'No, stop
|
|
1540
|
+
{ value: 'no', label: 'No, stop - let me reconfigure localization in cms.config.ts' },
|
|
1200
1541
|
],
|
|
1201
1542
|
initialValue: 'no',
|
|
1202
1543
|
});
|
|
@@ -1205,22 +1546,34 @@ const main = async (s) => {
|
|
|
1205
1546
|
return;
|
|
1206
1547
|
}
|
|
1207
1548
|
localizationTransition = 'disable';
|
|
1549
|
+
const now = new Date().toISOString();
|
|
1550
|
+
disableCleanupState = {
|
|
1551
|
+
...storedEnabledLocalizationState,
|
|
1552
|
+
transition: {
|
|
1553
|
+
type: 'disable',
|
|
1554
|
+
status: 'pending',
|
|
1555
|
+
startedAt: storedEnabledLocalizationState.transition?.startedAt ?? now,
|
|
1556
|
+
...(isDisableRetry ? { lastAttemptAt: now } : {}),
|
|
1557
|
+
},
|
|
1558
|
+
};
|
|
1559
|
+
await writeStoredLocalizationState(disableCleanupState);
|
|
1208
1560
|
if (editorPhotosHasLocale) {
|
|
1209
1561
|
try {
|
|
1210
1562
|
const uploadsFolder = cmsConfig.media.upload.path;
|
|
1211
|
-
const
|
|
1212
|
-
|
|
1213
|
-
|
|
1214
|
-
|
|
1215
|
-
|
|
1216
|
-
|
|
1217
|
-
|
|
1218
|
-
|
|
1563
|
+
const { deletedFiles, failedFiles } = await deleteEditorPhotosLocaleFiles(storedEnabledLocalizationState.defaultLocale, uploadsFolder);
|
|
1564
|
+
if (deletedFiles > 0) {
|
|
1565
|
+
console.log(chalk.gray(` - Deleted ${deletedFiles} editor photo file(s) for non-default locales`));
|
|
1566
|
+
}
|
|
1567
|
+
if (failedFiles.length > 0) {
|
|
1568
|
+
// TODO: Add the failed-to-delete files to system_issues table (system_issues system not yet implemented)
|
|
1569
|
+
console.error(chalk.red(` - Failed to delete ${failedFiles.length} editor photo file(s). These files were left on disk:`));
|
|
1570
|
+
for (const { filePath, error } of failedFiles) {
|
|
1571
|
+
console.error(chalk.red(` ${filePath}: ${String(error)}`));
|
|
1219
1572
|
}
|
|
1220
1573
|
}
|
|
1221
|
-
await db.execute(sql `DELETE FROM \`editor_photos\` WHERE \`locale\` != ${
|
|
1574
|
+
await db.execute(sql `DELETE FROM \`editor_photos\` WHERE \`locale\` != ${storedEnabledLocalizationState.defaultLocale}`);
|
|
1222
1575
|
await db.execute(sql `ALTER TABLE \`editor_photos\` DROP COLUMN \`locale\``);
|
|
1223
|
-
log.info(chalk.gray(` - Removed
|
|
1576
|
+
log.info(chalk.gray(` - Removed non-base-locale row(s) and dropped 'locale' column from 'editor_photos'.`));
|
|
1224
1577
|
}
|
|
1225
1578
|
catch (error) {
|
|
1226
1579
|
console.error(chalk.red(` - Error migrating 'editor_photos' for localization disable:`, error));
|
|
@@ -1231,7 +1584,10 @@ const main = async (s) => {
|
|
|
1231
1584
|
}
|
|
1232
1585
|
s.start();
|
|
1233
1586
|
}
|
|
1234
|
-
const
|
|
1587
|
+
const defaultLocaleCode = localizationCurrentlyEnabled
|
|
1588
|
+
? (cmsConfig.localization?.defaultLocale ?? storedEnabledLocalizationState?.defaultLocale ?? 'en')
|
|
1589
|
+
: (storedEnabledLocalizationState?.defaultLocale ?? cmsConfig.localization?.defaultLocale ?? 'en');
|
|
1590
|
+
const ctx = { localizationTransition, defaultLocaleCode, uploadsFolder: cmsConfig.media.upload.path };
|
|
1235
1591
|
/**
|
|
1236
1592
|
* Check if there are tables to update
|
|
1237
1593
|
*/
|
|
@@ -1342,7 +1698,7 @@ const main = async (s) => {
|
|
|
1342
1698
|
* Loop through the tables to remove
|
|
1343
1699
|
*/
|
|
1344
1700
|
for (const table of tablesToRemove) {
|
|
1345
|
-
// When localization was globally disabled, auto-confirm dropping _locales tables
|
|
1701
|
+
// When localization was globally disabled, auto-confirm dropping _locales tables -
|
|
1346
1702
|
// the upfront prompt already covered them.
|
|
1347
1703
|
const autoDropLocales = localizationTransition === 'disable' && table.name.endsWith('_locales') ? 'yes' : null;
|
|
1348
1704
|
let opType;
|
|
@@ -1366,6 +1722,26 @@ const main = async (s) => {
|
|
|
1366
1722
|
case 'yes':
|
|
1367
1723
|
case 'no':
|
|
1368
1724
|
if (opType === 'yes') {
|
|
1725
|
+
if (table.name.endsWith('_locales')) {
|
|
1726
|
+
try {
|
|
1727
|
+
const { deletedFiles, failedFiles } = await deleteLocalizedAssetFilesFromLocalesTable(table.name, localesTableAssetMetadata.get(table.name), cmsConfig.media.upload.path);
|
|
1728
|
+
if (deletedFiles > 0) {
|
|
1729
|
+
console.log(chalk.gray(` - Deleted ${deletedFiles} localized asset file(s) referenced by '${table.name}'`));
|
|
1730
|
+
}
|
|
1731
|
+
if (failedFiles.length > 0) {
|
|
1732
|
+
// TODO: Add the failed-to-delete files to system_issues table (system_issues system not yet implemented)
|
|
1733
|
+
console.error(chalk.red(` - Failed to delete ${failedFiles.length} localized asset file(s) referenced by '${table.name}'. These files were left on disk:`));
|
|
1734
|
+
for (const { filePath, error } of failedFiles) {
|
|
1735
|
+
console.error(chalk.red(` ${filePath}: ${String(error)}`));
|
|
1736
|
+
}
|
|
1737
|
+
}
|
|
1738
|
+
}
|
|
1739
|
+
catch (error) {
|
|
1740
|
+
// Reached only when the SELECT itself fails (DB-level error).
|
|
1741
|
+
// Per-file unlink failures are collected inside and reported above.
|
|
1742
|
+
console.error(chalk.red(` - Error reading '${table.name}' to clean up localized asset files:`, error));
|
|
1743
|
+
}
|
|
1744
|
+
}
|
|
1369
1745
|
console.log(chalk.gray(` - Dropping table '${table.name}'`));
|
|
1370
1746
|
await db.execute(sql `DROP TABLE \`${sql.raw(table.name)}\``);
|
|
1371
1747
|
}
|
|
@@ -1388,19 +1764,45 @@ const main = async (s) => {
|
|
|
1388
1764
|
}
|
|
1389
1765
|
}
|
|
1390
1766
|
}
|
|
1391
|
-
|
|
1392
|
-
|
|
1393
|
-
|
|
1394
|
-
|
|
1395
|
-
|
|
1396
|
-
|
|
1397
|
-
|
|
1398
|
-
|
|
1767
|
+
if (localizationTransition === 'enable') {
|
|
1768
|
+
if (!enableTargetState || !enablePendingState) {
|
|
1769
|
+
throw new Error(`Localization enable setup state was not initialized.`);
|
|
1770
|
+
}
|
|
1771
|
+
const missingArtifacts = [];
|
|
1772
|
+
if (editorPhotosExists) {
|
|
1773
|
+
const postEditorPhotosCols = await MysqlTableChecker.getColumns('editor_photos').catch(() => []);
|
|
1774
|
+
if (!postEditorPhotosCols.includes('locale')) {
|
|
1775
|
+
missingArtifacts.push(`editor_photos.locale`);
|
|
1776
|
+
}
|
|
1777
|
+
}
|
|
1778
|
+
for (const table of desiredTables) {
|
|
1779
|
+
if ((table.sectionType === 'destinationDb' || table.sectionType === 'gallery') &&
|
|
1780
|
+
table.fields.some((field) => field.name === 'locale')) {
|
|
1781
|
+
const cols = await MysqlTableChecker.getColumns(table.name).catch(() => []);
|
|
1782
|
+
if (!cols.includes('locale')) {
|
|
1783
|
+
missingArtifacts.push(`${table.name}.locale`);
|
|
1784
|
+
}
|
|
1785
|
+
}
|
|
1786
|
+
if (table.sectionType === 'locales') {
|
|
1787
|
+
const stillExists = (await MysqlTableChecker.getExistingTableStructure(table.name)) !== null;
|
|
1788
|
+
if (!stillExists) {
|
|
1789
|
+
missingArtifacts.push(table.name);
|
|
1790
|
+
}
|
|
1791
|
+
}
|
|
1792
|
+
}
|
|
1793
|
+
if (missingArtifacts.length > 0) {
|
|
1794
|
+
throw new Error(`Localization enable setup did not complete. Missing artifacts: ${missingArtifacts.join(', ')}. The pending state was kept in '__nextjs_cms_config' and update-sections will retry on the next run.`);
|
|
1795
|
+
}
|
|
1796
|
+
await writeStoredLocalizationState(enableTargetState);
|
|
1797
|
+
}
|
|
1399
1798
|
if (localizationTransition === 'disable') {
|
|
1799
|
+
if (!disableCleanupState) {
|
|
1800
|
+
throw new Error(`Localization disable cleanup state was not initialized.`);
|
|
1801
|
+
}
|
|
1400
1802
|
const postEditorPhotosCols = await MysqlTableChecker.getColumns('editor_photos').catch(() => []);
|
|
1401
1803
|
let fullyClean = !postEditorPhotosCols.includes('locale');
|
|
1402
1804
|
if (!fullyClean) {
|
|
1403
|
-
log.warn(chalk.yellow(` - 'editor_photos' still has a 'locale' column after cleanup. Keeping
|
|
1805
|
+
log.warn(chalk.yellow(` - 'editor_photos' still has a 'locale' column after cleanup. Keeping localization disable pending so the next run can resume.`));
|
|
1404
1806
|
}
|
|
1405
1807
|
const postRegistered = fullyClean
|
|
1406
1808
|
? await db.select({ name: NextJsCmsTablesTable.tableName }).from(NextJsCmsTablesTable)
|
|
@@ -1410,26 +1812,32 @@ const main = async (s) => {
|
|
|
1410
1812
|
const cols = await MysqlTableChecker.getColumns(t.name).catch(() => []);
|
|
1411
1813
|
if (cols.includes('locale')) {
|
|
1412
1814
|
fullyClean = false;
|
|
1413
|
-
log.warn(chalk.yellow(` - Table '${t.name}' still has a 'locale' column after cleanup. Keeping
|
|
1815
|
+
log.warn(chalk.yellow(` - Table '${t.name}' still has a 'locale' column after cleanup. Keeping localization disable pending so the next run can resume.`));
|
|
1414
1816
|
break;
|
|
1415
1817
|
}
|
|
1416
1818
|
}
|
|
1417
1819
|
}
|
|
1418
1820
|
if (fullyClean) {
|
|
1419
|
-
// Check registered tables whose name ends with '_locales'
|
|
1821
|
+
// Check registered tables whose name ends with '_locales' - scoped to
|
|
1420
1822
|
// __nextjs_cms_tables so unrelated tables sharing the DB are never flagged.
|
|
1421
1823
|
const registeredLocalesTables = postRegistered.filter((t) => t.name.endsWith('_locales'));
|
|
1422
1824
|
for (const t of registeredLocalesTables) {
|
|
1423
1825
|
const stillExists = (await MysqlTableChecker.getExistingTableStructure(t.name)) !== null;
|
|
1424
1826
|
if (stillExists) {
|
|
1425
1827
|
fullyClean = false;
|
|
1426
|
-
log.warn(chalk.yellow(` - Registered '_locales' table '${t.name}' still exists after cleanup. Keeping
|
|
1828
|
+
log.warn(chalk.yellow(` - Registered '_locales' table '${t.name}' still exists after cleanup. Keeping localization disable pending so the next run can resume.`));
|
|
1427
1829
|
break;
|
|
1428
1830
|
}
|
|
1429
1831
|
}
|
|
1430
1832
|
}
|
|
1431
1833
|
if (fullyClean) {
|
|
1432
|
-
await
|
|
1834
|
+
await writeStoredLocalizationState({
|
|
1835
|
+
version: STORED_LOCALIZATION_VERSION,
|
|
1836
|
+
enabled: false,
|
|
1837
|
+
});
|
|
1838
|
+
}
|
|
1839
|
+
else {
|
|
1840
|
+
throw new Error(`Localization disable cleanup did not complete. The pending state was kept in '__nextjs_cms_config' and update-sections will retry on the next run.`);
|
|
1433
1841
|
}
|
|
1434
1842
|
}
|
|
1435
1843
|
};
|