nextjs-cms-kit 0.5.1

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 (45) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +232 -0
  3. package/dist/commands/db-config.d.ts +3 -0
  4. package/dist/commands/db-config.d.ts.map +1 -0
  5. package/dist/commands/db-config.js +20 -0
  6. package/dist/commands/fix-master-admin.d.ts +3 -0
  7. package/dist/commands/fix-master-admin.d.ts.map +1 -0
  8. package/dist/commands/fix-master-admin.js +19 -0
  9. package/dist/commands/set-master-admin.d.ts +3 -0
  10. package/dist/commands/set-master-admin.d.ts.map +1 -0
  11. package/dist/commands/set-master-admin.js +29 -0
  12. package/dist/commands/setup.d.ts +3 -0
  13. package/dist/commands/setup.d.ts.map +1 -0
  14. package/dist/commands/setup.js +61 -0
  15. package/dist/commands/update-sections.d.ts +3 -0
  16. package/dist/commands/update-sections.d.ts.map +1 -0
  17. package/dist/commands/update-sections.js +33 -0
  18. package/dist/index.d.ts +5 -0
  19. package/dist/index.d.ts.map +1 -0
  20. package/dist/index.js +38 -0
  21. package/dist/lib/actions.d.ts +7 -0
  22. package/dist/lib/actions.d.ts.map +1 -0
  23. package/dist/lib/actions.js +62 -0
  24. package/dist/lib/add-table-keys.d.ts +32 -0
  25. package/dist/lib/add-table-keys.d.ts.map +1 -0
  26. package/dist/lib/add-table-keys.js +168 -0
  27. package/dist/lib/db-config-setup.d.ts +4 -0
  28. package/dist/lib/db-config-setup.d.ts.map +1 -0
  29. package/dist/lib/db-config-setup.js +216 -0
  30. package/dist/lib/fix-master-admin.d.ts +2 -0
  31. package/dist/lib/fix-master-admin.d.ts.map +1 -0
  32. package/dist/lib/fix-master-admin.js +7 -0
  33. package/dist/lib/reload-env.d.ts +16 -0
  34. package/dist/lib/reload-env.d.ts.map +1 -0
  35. package/dist/lib/reload-env.js +42 -0
  36. package/dist/lib/schema-generator.d.ts +10 -0
  37. package/dist/lib/schema-generator.d.ts.map +1 -0
  38. package/dist/lib/schema-generator.js +168 -0
  39. package/dist/lib/set-master-admin.d.ts +2 -0
  40. package/dist/lib/set-master-admin.d.ts.map +1 -0
  41. package/dist/lib/set-master-admin.js +53 -0
  42. package/dist/lib/update-sections.d.ts +2 -0
  43. package/dist/lib/update-sections.d.ts.map +1 -0
  44. package/dist/lib/update-sections.js +898 -0
  45. package/package.json +55 -0
@@ -0,0 +1,898 @@
1
+ // import '@/envConfig'
2
+ import { db } from 'nextjs-cms/db/client';
3
+ import { LZTablesTable } from 'nextjs-cms/db/schema';
4
+ import { eq, sql } from 'drizzle-orm';
5
+ import fs from 'fs';
6
+ import { SectionFactory } from 'nextjs-cms/core/factories';
7
+ import { CheckboxField, ColorField, DateField, MapField, NumberField, PasswordField, RichTextField, SelectField, SelectMultipleField, TagsField, TextAreaField, TextField, textAreaField, textField, } from 'nextjs-cms/core/fields';
8
+ import { is } from 'nextjs-cms/core/helpers';
9
+ import { FileField } from 'nextjs-cms/core/fields';
10
+ import chalk from 'chalk';
11
+ import { intro, select, spinner, log } from '@clack/prompts';
12
+ import { MysqlTableChecker } from 'nextjs-cms/core/db';
13
+ import { generateDrizzleSchema } from './schema-generator';
14
+ import { addTableKeys } from './add-table-keys';
15
+ function generateFieldSQL(input) {
16
+ let fieldSQL = `\`${input.name}\` `;
17
+ /**
18
+ * Check the field type and generate the SQL accordingly
19
+ */
20
+ switch (true) {
21
+ case is(input, TextField):
22
+ fieldSQL += `VARCHAR(${input.maxLength ?? 255}) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci`;
23
+ break;
24
+ case is(input, TextAreaField) || is(input, RichTextField):
25
+ fieldSQL += 'LONGTEXT CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci';
26
+ break;
27
+ case is(input, SelectMultipleField):
28
+ fieldSQL += 'VARCHAR(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci';
29
+ break;
30
+ case is(input, SelectField):
31
+ if (input.optionsType === 'static' && input.options) {
32
+ fieldSQL += `ENUM(${input.options.map((option) => `'${option.value}'`).join(', ')}) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci`;
33
+ }
34
+ else {
35
+ fieldSQL += 'VARCHAR(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci';
36
+ }
37
+ break;
38
+ case is(input, CheckboxField):
39
+ fieldSQL += 'BOOLEAN';
40
+ break;
41
+ case is(input, ColorField):
42
+ fieldSQL += 'CHAR(7)'; // #RRGGBB
43
+ break;
44
+ case is(input, DateField):
45
+ switch (input.format) {
46
+ case 'date':
47
+ fieldSQL += 'DATE';
48
+ break;
49
+ case 'datetime':
50
+ fieldSQL += 'DATETIME';
51
+ break;
52
+ case 'timestamp':
53
+ fieldSQL += 'TIMESTAMP';
54
+ break;
55
+ default:
56
+ fieldSQL += 'DATETIME';
57
+ break;
58
+ }
59
+ break;
60
+ case is(input, NumberField): {
61
+ // Default format: 'int' if not specified
62
+ const format = input.format;
63
+ // FLOAT / DOUBLE
64
+ if (format === 'float' || format === 'double') {
65
+ fieldSQL += format === 'float' ? 'FLOAT' : 'DOUBLE';
66
+ if (input.minValue !== undefined && input.minValue >= 0) {
67
+ fieldSQL += ' UNSIGNED';
68
+ }
69
+ break;
70
+ }
71
+ // ---------- INTEGER TYPES ----------
72
+ const unsigned = input.minValue !== undefined && input.minValue >= 0;
73
+ // Decide if we have any bounds we can use to shrink the integer type
74
+ const hasBounds = input.minValue !== undefined || input.maxValue !== undefined || input.maxLength !== undefined;
75
+ // From maxLength, approximate numeric bounds if explicit min/max not provided
76
+ const pow10 = (d) => Math.pow(10, d);
77
+ const approxFromDigits = (digits) => {
78
+ if (digits === undefined)
79
+ return {};
80
+ const max = pow10(digits) - 1;
81
+ // if unsigned, min will be clamped to 0 below
82
+ const min = -max;
83
+ return { min, max };
84
+ };
85
+ const approx = approxFromDigits(input.maxLength);
86
+ let neededMin = input.minValue ?? approx.min;
87
+ const neededMax = input.maxValue ?? approx.max;
88
+ if (unsigned)
89
+ neededMin = neededMin === undefined ? 0 : Math.max(0, neededMin);
90
+ const ranges = unsigned
91
+ ? [
92
+ { type: 'TINYINT', min: 0, max: 255 },
93
+ { type: 'SMALLINT', min: 0, max: 65_535 },
94
+ { type: 'MEDIUMINT', min: 0, max: 16_777_215 },
95
+ { type: 'INT', min: 0, max: 4_294_967_295 },
96
+ // BIGINT max clipped to safe JS number for comparison purposes
97
+ { type: 'BIGINT', min: 0, max: Number.MAX_SAFE_INTEGER },
98
+ ]
99
+ : [
100
+ { type: 'TINYINT', min: -128, max: 127 },
101
+ { type: 'SMALLINT', min: -32_768, max: 32_767 },
102
+ { type: 'MEDIUMINT', min: -8_388_608, max: 8_388_607 },
103
+ { type: 'INT', min: -2_147_483_648, max: 2_147_483_647 },
104
+ { type: 'BIGINT', min: Number.MIN_SAFE_INTEGER, max: Number.MAX_SAFE_INTEGER },
105
+ ];
106
+ const fits = (r) => {
107
+ const okMin = neededMin === undefined || neededMin >= r.min;
108
+ const okMax = neededMax === undefined || neededMax <= r.max;
109
+ return okMin && okMax;
110
+ };
111
+ // Choose type:
112
+ // - If no bounds at all, default to INT (common + avoids accidental TINYINT)
113
+ // - If auto-increment and still no bounds, keep INT
114
+ // - Otherwise, pick the smallest that fits
115
+ let chosen = 'INT';
116
+ if (hasBounds) {
117
+ chosen = ranges.find(fits)?.type ?? 'INT';
118
+ // If auto-increment, don't go smaller than INT by default (optional safety)
119
+ if (input.hasAutoIncrement &&
120
+ (chosen === 'TINYINT' || chosen === 'SMALLINT' || chosen === 'MEDIUMINT')) {
121
+ chosen = 'INT';
122
+ }
123
+ }
124
+ fieldSQL += chosen;
125
+ if (unsigned)
126
+ fieldSQL += ' UNSIGNED';
127
+ // Display width:
128
+ // - If user specified maxLength, honor it for any integer type
129
+ // - Else, if chosen is INT, default to (11) as requested
130
+ const width = input.maxLength ?? (chosen === 'INT' ? 11 : undefined);
131
+ if (width !== undefined) {
132
+ fieldSQL += `(${width})`;
133
+ }
134
+ // AUTO_INCREMENT only for integer types
135
+ if (input.hasAutoIncrement) {
136
+ fieldSQL += ' AUTO_INCREMENT';
137
+ }
138
+ break;
139
+ }
140
+ case is(input, MapField):
141
+ fieldSQL += 'VARCHAR(200) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci';
142
+ break;
143
+ case is(input, FileField):
144
+ fieldSQL += 'VARCHAR(50) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci';
145
+ break;
146
+ case is(input, PasswordField):
147
+ fieldSQL += 'VARCHAR(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci';
148
+ break;
149
+ case is(input, TagsField):
150
+ fieldSQL += 'VARCHAR(50) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci';
151
+ break;
152
+ default:
153
+ fieldSQL += 'VARCHAR(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci';
154
+ break;
155
+ }
156
+ if (input.required && !input.isConditional()) {
157
+ fieldSQL += ' NOT NULL';
158
+ }
159
+ else if (!input.defaultValue) {
160
+ fieldSQL += ' DEFAULT NULL';
161
+ }
162
+ if (input.defaultValue !== null && input.defaultValue !== undefined) {
163
+ fieldSQL += ` DEFAULT '${input.defaultValue}'`;
164
+ }
165
+ return fieldSQL;
166
+ }
167
+ async function createTable(table, options) {
168
+ /**
169
+ * Generate the CREATE TABLE SQL
170
+ */
171
+ let createTableSQL = `CREATE TABLE \`${table.name}\` (`;
172
+ /**
173
+ * Loop through the fields and generate the SQL for each field
174
+ */
175
+ for (const input of table.fields) {
176
+ if (input.destinationDb)
177
+ continue;
178
+ let fieldSQL = generateFieldSQL(input);
179
+ /**
180
+ * Check if the field is a primary key
181
+ */
182
+ if (input.name === table.identifier?.name) {
183
+ fieldSQL += ' UNIQUE';
184
+ }
185
+ createTableSQL += `${fieldSQL}, `;
186
+ }
187
+ /**
188
+ * Now, let's add the `created_at`, `updated_at`, `created_by`, and `updated_by` fields
189
+ */
190
+ if (options) {
191
+ if (options.createdAt !== false) {
192
+ createTableSQL += '`created_at` TIMESTAMP DEFAULT CURRENT_TIMESTAMP, ';
193
+ }
194
+ if (options.updatedAt !== false) {
195
+ createTableSQL += '`updated_at` TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, ';
196
+ }
197
+ if (options.createdBy !== false) {
198
+ createTableSQL += '`created_by` VARCHAR(50) NOT NULL, '; // Should reference the admin ID?
199
+ }
200
+ if (options.updatedBy !== false) {
201
+ createTableSQL += '`updated_by` VARCHAR(50), '; // Should reference the admin ID?
202
+ }
203
+ }
204
+ /**
205
+ * Add the primary key
206
+ */
207
+ // eslint-disable-next-line @typescript-eslint/no-unnecessary-condition
208
+ if (!(table.primaryKey && table.primaryKey.length > 0)) {
209
+ /**
210
+ * Log a warning if there's no primary key
211
+ */
212
+ console.warn(chalk.yellow(` - Warning: Table \`${table.name}\` has no primary key!`));
213
+ }
214
+ else {
215
+ // addTableKeys function will handle the primary key
216
+ /*createTableSQL += 'PRIMARY KEY ('
217
+ for (const key of table.primaryKey) {
218
+ createTableSQL += `\`${key.name}\`, `
219
+ }
220
+ createTableSQL = createTableSQL.slice(0, -2) + '), '*/
221
+ }
222
+ /**
223
+ * Remove the last comma and space, and then close the CREATE TABLE statement
224
+ */
225
+ createTableSQL = createTableSQL.slice(0, -2) + ') ENGINE=InnoDB;';
226
+ /**
227
+ * Try to create the table
228
+ */
229
+ try {
230
+ /**
231
+ * Execute the CREATE TABLE SQL
232
+ */
233
+ await db.execute(sql `${sql.raw(createTableSQL)}`);
234
+ /**
235
+ * Insert the table name into the `__lz_tables` table
236
+ */
237
+ await db
238
+ .insert(LZTablesTable)
239
+ .values({
240
+ tableName: table.name,
241
+ sectionName: table.sectionName,
242
+ })
243
+ .catch((error) => {
244
+ console.error('Error inserting into __lz_tables table:', error);
245
+ });
246
+ }
247
+ catch (error) {
248
+ console.log(chalk.red(` - Error creating table \`${table.name}\`:`, error));
249
+ }
250
+ /**
251
+ * Get the existing keys for the table
252
+ */
253
+ const existingKeys = await MysqlTableChecker.getExistingKeys(table.name);
254
+ /**
255
+ * Add the keys to the table
256
+ */
257
+ const keyErrors = await addTableKeys(table, existingKeys);
258
+ console.log(chalk.italic.hex(`${keyErrors > 0 ? `#FFA500` : `#fafafa`}`)(` - Table \`${table.name}\` created successfully ${keyErrors > 0 ? `with ${keyErrors} error(s).` : ''}`));
259
+ }
260
+ async function renameTable(oldName, newName) {
261
+ /**
262
+ * Execute ALTER TABLE statement to rename the table
263
+ */
264
+ try {
265
+ await db.execute(sql `ALTER TABLE \`${sql.raw(oldName)}\` RENAME \`${sql.raw(newName)}\``);
266
+ /**
267
+ * Update the table name in the `__lz_tables` table
268
+ */
269
+ await db
270
+ .update(LZTablesTable)
271
+ .set({
272
+ tableName: newName,
273
+ })
274
+ .where(eq(LZTablesTable.tableName, oldName))
275
+ .catch((error) => {
276
+ console.error('Error updating __lz_tables table:', error);
277
+ });
278
+ console.log(`Table \`${oldName}\` renamed to \`${newName}\` successfully.`);
279
+ }
280
+ catch (error) {
281
+ console.error(`Error renaming table \`${oldName}\` to \`${newName}\`:`, error);
282
+ }
283
+ }
284
+ async function updateTable(table, s) {
285
+ console.log();
286
+ console.log(chalk.blueBright(`Updating table '${table.name}' for section '${table.sectionName}'`));
287
+ s.start();
288
+ /**
289
+ * Get the existing keys for the table
290
+ */
291
+ const existingKeys = await MysqlTableChecker.getExistingKeys(table.name);
292
+ /**
293
+ * Get the existing table structure (columns)
294
+ */
295
+ const existingFieldsData = await MysqlTableChecker.getExistingTableStructure(table.name);
296
+ let sqlErrors = 0;
297
+ const alterTableSQLs = [];
298
+ /**
299
+ * Filter out the fields that already exist in the table
300
+ */
301
+ const existingFields = existingFieldsData ? Object.keys(existingFieldsData) : [];
302
+ const fieldsToAdd = table.fields.filter((field) => !existingFields.some((existingField) => existingField === field.name));
303
+ /**
304
+ * Let's find out fields that need to be updated.
305
+ * If a field exists in both the desired fields and the existing fields, we should mark it for update.
306
+ * TODO: Actually, we should check if the field needs to be updated, and only update if necessary. (INFORMATION_SCHEMA will help)
307
+ */
308
+ const fieldsToUpdate = table.fields.filter((field) => existingFields.some((existingField) => existingField === field.name));
309
+ /**
310
+ * Let's find out fields to remove as well.
311
+ * If a field exists in the table but not in the desired fields,
312
+ * or if it has a destinationDb, TODO: Add the values to the destination table? (CRITICAL! Loss of data if not done)
313
+ * we should mark it for removal.
314
+ */
315
+ let fieldsToRemove = existingFields.filter((existingField) => !table.fields.some((field) => field.name === existingField) ||
316
+ table.fields.some((field) => field.destinationDb && field.name === existingField));
317
+ /**
318
+ * Check if there are fields to update
319
+ */
320
+ if (fieldsToUpdate.length > 0) {
321
+ /**
322
+ * Loop through the tables to update
323
+ */
324
+ for (const field of fieldsToUpdate) {
325
+ // TODO: We should also check if the field type has changed,
326
+ // and if so, notify the user that the field type will be updated,
327
+ // and data may be lost! (e.g. changing a field from VARCHAR to INT)
328
+ if (field.destinationDb)
329
+ continue;
330
+ let fieldSQL = generateFieldSQL(field);
331
+ /**
332
+ * Check if the field is a primary key
333
+ */
334
+ if (field.name === table.identifier?.name) {
335
+ fieldSQL += ' FIRST';
336
+ }
337
+ /**
338
+ * Add the SQL to modify the field
339
+ */
340
+ alterTableSQLs.push({
341
+ field: field.name,
342
+ table: table.name,
343
+ action: 'modify',
344
+ sql: `MODIFY COLUMN ${fieldSQL}`,
345
+ });
346
+ }
347
+ }
348
+ /**
349
+ * Check if there are fields to add
350
+ */
351
+ if (fieldsToAdd.length > 0) {
352
+ /**
353
+ * Loop through the fields to add
354
+ */
355
+ for (const field of fieldsToAdd) {
356
+ if (field.destinationDb)
357
+ continue;
358
+ let fieldSQL = generateFieldSQL(field);
359
+ /**
360
+ * Check if the field is an identifier
361
+ */
362
+ if (field.name === table.identifier?.name) {
363
+ fieldSQL += ' UNIQUE FIRST';
364
+ }
365
+ /**
366
+ * Check if there are fields to remove
367
+ * If there are, ask the user if they want to add the new field or rename a to-be-removed field
368
+ */
369
+ if (fieldsToRemove.length > 0) {
370
+ s.stop();
371
+ const opType = await select({
372
+ message: `Is field '${field.name}' of table '${table.name}' a new field?`,
373
+ options: [
374
+ { value: 'new', label: 'Create new field' },
375
+ { value: 'rename', label: 'Rename existing field' },
376
+ ],
377
+ });
378
+ s.start();
379
+ switch (opType) {
380
+ case 'new':
381
+ /**
382
+ * Field doesn't exist.
383
+ * Add the field to the table
384
+ */
385
+ alterTableSQLs.push({
386
+ field: field.name,
387
+ table: table.name,
388
+ action: 'add',
389
+ sql: `ADD COLUMN ${fieldSQL}`,
390
+ });
391
+ break;
392
+ case 'rename': {
393
+ s.stop();
394
+ const oldField = await select({
395
+ message: `Select the field to rename to '${field.name}'`,
396
+ options: fieldsToRemove.map((field) => {
397
+ return { value: field, label: field };
398
+ }),
399
+ });
400
+ s.start();
401
+ if (oldField && typeof oldField === 'string') {
402
+ console.log(`Renaming field '${oldField}' to '${field.name}'`);
403
+ /**
404
+ * Rename the field
405
+ */
406
+ alterTableSQLs.push({
407
+ field: field.name,
408
+ table: table.name,
409
+ action: 'rename',
410
+ sql: `RENAME COLUMN \`${oldField}\` TO ${field.name}`,
411
+ });
412
+ /**
413
+ * Remove the field from the fieldsToRemove array
414
+ */
415
+ fieldsToRemove = fieldsToRemove.filter((field) => {
416
+ return field !== oldField;
417
+ });
418
+ }
419
+ break;
420
+ }
421
+ }
422
+ }
423
+ else {
424
+ /**
425
+ * Field doesn't exist.
426
+ * Add the field to the table
427
+ */
428
+ alterTableSQLs.push({
429
+ field: field.name,
430
+ table: table.name,
431
+ action: 'add',
432
+ sql: `ADD COLUMN ${fieldSQL}`,
433
+ });
434
+ }
435
+ }
436
+ }
437
+ /**
438
+ * Finally, check if there are fields to remove
439
+ */
440
+ if (fieldsToRemove.length > 0) {
441
+ /**
442
+ * Loop through the fields to remove
443
+ */
444
+ s.stop('Found ' + fieldsToRemove.length + ' field(s) to remove:');
445
+ for (const field of fieldsToRemove) {
446
+ const shouldRemove = await select({
447
+ message: `You are about to remove field '${field}' of table '${table.name}'. Remove it?`,
448
+ options: [
449
+ { value: 'no', label: 'No' },
450
+ { value: 'yes', label: 'Yes' },
451
+ ],
452
+ initialValue: 'no',
453
+ });
454
+ if (shouldRemove === 'yes') {
455
+ log.info(chalk.red(`Removing field '${field}'`));
456
+ alterTableSQLs.push({
457
+ field: field,
458
+ table: table.name,
459
+ action: 'remove',
460
+ sql: `DROP COLUMN \`${field}\``,
461
+ });
462
+ }
463
+ else {
464
+ log.info(chalk.yellow(`- Field ${chalk.underline.italic(field)} not removed.`));
465
+ }
466
+ s.start();
467
+ }
468
+ }
469
+ /**
470
+ * Execute ALTER TABLE statements
471
+ */
472
+ for (const alterSQL of alterTableSQLs) {
473
+ try {
474
+ await db.execute(sql `ALTER TABLE \`${sql.raw(table.name)}\` ${sql.raw(alterSQL.sql)}`);
475
+ switch (alterSQL.action) {
476
+ case 'add':
477
+ console.log(chalk.gray(` - Field ${chalk.underline.italic(alterSQL.field)} Added to \`${table.name}\`.`));
478
+ break;
479
+ case 'modify':
480
+ // Activate this when we only update the fields that need to be updated
481
+ // console.log(chalk.gray(` - ${alterSQL.field}: Modified in \`${table.name}\`.`))
482
+ break;
483
+ case 'remove':
484
+ console.log(chalk.gray(` - Field ${chalk.underline.italic(alterSQL.field)} Removed from \`${table.name}\`.`));
485
+ break;
486
+ }
487
+ }
488
+ catch (error) {
489
+ sqlErrors++;
490
+ console.error(chalk.red(` - ${alterSQL.field}: Error modifying \`${table.name}\`.\`${alterSQL.field}\`:`, error));
491
+ }
492
+ }
493
+ /**
494
+ * Add the keys to the table
495
+ */
496
+ const keyErrors = await addTableKeys(table, existingKeys);
497
+ sqlErrors += keyErrors;
498
+ s.stop(chalk.italic.hex(`${sqlErrors > 0 ? `#FFA500` : `#fafafa`}`)(`- Table \`${table.name}\` modified successfully ${sqlErrors > 0 ? `with ${sqlErrors} error(s).` : ''}`));
499
+ }
500
+ const main = async (s) => {
501
+ /**
502
+ * Remove the existing schema file
503
+ */
504
+ console.log(chalk.white(`Removing existing schema file...`));
505
+ s.start();
506
+ const schemaFilePath = './dynamic-schemas/schema.ts';
507
+ try {
508
+ if (fs.existsSync(schemaFilePath)) {
509
+ fs.unlinkSync(schemaFilePath);
510
+ }
511
+ }
512
+ catch (error) {
513
+ console.error('Error removing schema file:', error);
514
+ }
515
+ s.stop();
516
+ console.log(chalk.white(`Generating Drizzle schema...`));
517
+ s.start();
518
+ const drizzleTableSchemas = [];
519
+ const drizzleColumnTypes = ['mysqlTable'];
520
+ let sections = [];
521
+ const desiredTables = [];
522
+ let existingTables = [];
523
+ sections = await SectionFactory.getSections();
524
+ console.log(`Found ${sections.length} section(s) to insert: `);
525
+ console.log(chalk.gray(sections.map((s) => s.name).join(', ')));
526
+ /**
527
+ * Let's see if the table `__lz_tables` exists in the database.
528
+ * If it doesn't, we'll create it.
529
+ * It has two fields: `name` and `created_at`.
530
+ */
531
+ await db.execute(sql ` CREATE TABLE IF NOT EXISTS __lz_tables ( name VARCHAR(100) NOT NULL, created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ) `);
532
+ /**
533
+ * Get the existing tables from the database
534
+ */
535
+ existingTables = await db
536
+ .select({
537
+ name: LZTablesTable.tableName,
538
+ })
539
+ .from(LZTablesTable);
540
+ /**
541
+ * Insert the sections into the database
542
+ */
543
+ for (const _s of sections) {
544
+ const s = _s.build();
545
+ s.buildFields();
546
+ /**
547
+ * Generate the Drizzle schema for the table
548
+ */
549
+ const drizzleSchema = generateDrizzleSchema({
550
+ name: s.db.table,
551
+ fields: s.fieldConfigs,
552
+ identifier: s.db.identifier,
553
+ });
554
+ drizzleTableSchemas.push(drizzleSchema.schema);
555
+ drizzleSchema.columnTypes.forEach((type) => {
556
+ if (!drizzleColumnTypes.includes(type)) {
557
+ drizzleColumnTypes.push(type);
558
+ }
559
+ });
560
+ desiredTables.push({
561
+ name: s.db.table,
562
+ fields: s.fields,
563
+ sectionName: s.name,
564
+ sectionType: s.type,
565
+ identifier: s.db.identifier,
566
+ primaryKey: s.db.primaryKey,
567
+ fulltext: s.db.fulltext,
568
+ unique: s.db.unique,
569
+ index: s.db.index,
570
+ });
571
+ /**
572
+ * Check for `destinationDb`
573
+ */
574
+ for (const field of s.fields) {
575
+ /**
576
+ * TODO: We should also check for input.db in select fields, and add the table to the desiredTables array!
577
+ */
578
+ if (field.destinationDb) {
579
+ console.log('Destination DB found for input:', field.name, 'with table:', field.destinationDb.table);
580
+ /**
581
+ * TODO: We should get the type of the identifier and the Select fields to match the types
582
+ */
583
+ const referenceIdFieldConfig = textField({
584
+ name: field.destinationDb.itemIdentifier,
585
+ label: 'Reference Id',
586
+ required: true,
587
+ order: 0,
588
+ });
589
+ const selectIdFieldConfig = textField({
590
+ name: field.destinationDb.selectIdentifier,
591
+ label: 'Select Id',
592
+ required: true,
593
+ order: 0,
594
+ });
595
+ desiredTables.push({
596
+ name: field.destinationDb.table,
597
+ fields: [referenceIdFieldConfig.build(), selectIdFieldConfig.build()],
598
+ sectionName: s.name,
599
+ sectionType: 'destinationDb',
600
+ identifier: undefined,
601
+ /**
602
+ * We can use both the referenceIdField and the selectIdField as a composite primary key
603
+ * since they are unique together for each row
604
+ */
605
+ primaryKey: [referenceIdFieldConfig, selectIdFieldConfig],
606
+ });
607
+ }
608
+ }
609
+ /**
610
+ * Check for gallery
611
+ */
612
+ if (s.gallery?.db.tableName) {
613
+ console.log('Gallery found for section:', s.name, 'with table:', s.gallery.db.tableName);
614
+ const photoField = textField({
615
+ name: s.gallery.db.photoNameField || 'photo',
616
+ label: 'Photo Name',
617
+ required: true,
618
+ order: 0,
619
+ });
620
+ desiredTables.push({
621
+ name: s.gallery.db.tableName,
622
+ fields: [
623
+ textField({
624
+ name: s.gallery.db.referenceIdentifierField || 'reference_id',
625
+ label: 'Reference Id',
626
+ required: true,
627
+ order: 0,
628
+ }).build(),
629
+ photoField.build(),
630
+ textAreaField({
631
+ name: s.gallery.db.metaField || 'meta',
632
+ label: 'Photo Information',
633
+ required: false,
634
+ order: 0,
635
+ }).build(),
636
+ ],
637
+ sectionName: s.name,
638
+ sectionType: 'gallery',
639
+ /**
640
+ * We can use the photoField as the identifier for the gallery table
641
+ * since it's unique, we can also use it as the primary key
642
+ */
643
+ identifier: photoField,
644
+ primaryKey: [photoField],
645
+ });
646
+ }
647
+ }
648
+ /**
649
+ * Make sure the dynamic-schemas directory exists
650
+ */
651
+ if (!fs.existsSync('./dynamic-schemas')) {
652
+ fs.mkdirSync('./dynamic-schemas');
653
+ }
654
+ /**
655
+ * Append the Drizzle column types to the schema file
656
+ */
657
+ fs.appendFileSync(schemaFilePath, 'import {' + drizzleColumnTypes.join(',') + "} from 'drizzle-orm/mysql-core'\n\n");
658
+ /**
659
+ * Append the Drizzle table schemas to the schema file
660
+ */
661
+ fs.appendFileSync(schemaFilePath, drizzleTableSchemas.join('\n'));
662
+ s.stop();
663
+ console.log(chalk.white('Finding tables to create, update or remove...'));
664
+ s.start();
665
+ /**
666
+ * Filter out the tables that already exist in the database
667
+ */
668
+ const tablesToCreate = desiredTables.filter((table) => !existingTables.some((existingTable) => existingTable.name === table.name));
669
+ /**
670
+ * Let's find out tables that need to be updated.
671
+ * If a table exists in both the desired tables and the existing tables, we should mark it for update.
672
+ */
673
+ const tablesToUpdate = desiredTables.filter((table) => existingTables.some((existingTable) => existingTable.name === table.name));
674
+ /**
675
+ * Let's find out tables to remove as well.
676
+ * If a table exists in the database but not in the desired tables, we should mark it for removal.
677
+ */
678
+ let tablesToRemove = existingTables.filter((existingTable) => !desiredTables.some((table) => table.name === existingTable.name));
679
+ s.stop();
680
+ console.log('Desired tables:');
681
+ console.log(chalk.gray(desiredTables.map((table) => table.name).join(', ')));
682
+ console.log('Existing tables:');
683
+ console.log(chalk.gray(existingTables.map((table) => table.name).join(', ')));
684
+ console.log('Tables to update:');
685
+ console.log(chalk.gray(tablesToUpdate.map((table) => table.name).join(', ')));
686
+ console.log('Tables to create:');
687
+ console.log(chalk.gray(tablesToCreate.map((table) => table.name).join(', ')));
688
+ console.log('Tables to remove:');
689
+ console.log(chalk.gray(tablesToRemove.map((table) => table.name).join(', ')));
690
+ console.log(`\n`);
691
+ intro(chalk.inverse(' update-sections '));
692
+ /**
693
+ * Check if there are tables to update
694
+ */
695
+ if (tablesToUpdate.length > 0) {
696
+ /**
697
+ * Loop through the tables to update
698
+ */
699
+ for (const table of tablesToUpdate) {
700
+ await updateTable(table, s);
701
+ }
702
+ }
703
+ /**
704
+ * Check if there are tables to create
705
+ */
706
+ if (tablesToCreate.length > 0) {
707
+ console.log(chalk.bold.bgGreenBright(`There are ${tablesToCreate.length} table(s) to create:`));
708
+ /**
709
+ * Loop through the tables to create
710
+ */
711
+ for (const table of tablesToCreate) {
712
+ /**
713
+ * Check if there are tables to remove
714
+ * If there are, ask the user if they want to create the new table or rename a removed table
715
+ */
716
+ if (tablesToRemove.length > 0) {
717
+ s.stop();
718
+ const opType = await select({
719
+ message: `Is table '${table.name}' for section '${table.sectionName}' a new table?`,
720
+ options: [
721
+ { value: 'new', label: 'Create new table' },
722
+ { value: 'rename', label: 'Rename existing table' },
723
+ ],
724
+ });
725
+ s.start();
726
+ switch (opType) {
727
+ case 'new': {
728
+ console.log(chalk.blueBright(`Creating table '${table.name}' for section '${table.sectionName}'`));
729
+ let options = {
730
+ createdAt: true,
731
+ updatedAt: true,
732
+ createdBy: true,
733
+ updatedBy: true,
734
+ };
735
+ if (table.sectionType === 'simple') {
736
+ options = {
737
+ createdAt: false,
738
+ updatedAt: true,
739
+ createdBy: false,
740
+ updatedBy: true,
741
+ };
742
+ }
743
+ if (table.sectionType === 'gallery') {
744
+ options = {
745
+ updatedBy: false,
746
+ updatedAt: false,
747
+ createdBy: true,
748
+ createdAt: true,
749
+ };
750
+ }
751
+ if (table.sectionType === 'destinationDb, selectDb') {
752
+ options = {
753
+ createdAt: false,
754
+ updatedAt: false,
755
+ createdBy: false,
756
+ updatedBy: false,
757
+ };
758
+ }
759
+ await createTable(table, options);
760
+ break;
761
+ }
762
+ case 'rename': {
763
+ s.stop();
764
+ const tableToRename = await select({
765
+ message: `Select the table to rename to '${table.name}'`,
766
+ options: tablesToRemove.map((table) => {
767
+ return { value: table.name, label: table.name };
768
+ }),
769
+ });
770
+ s.start();
771
+ if (tableToRename && typeof tableToRename === 'string') {
772
+ console.log(`Renaming table '${tableToRename}' to '${table.name}'`);
773
+ await renameTable(tableToRename, table.name);
774
+ await updateTable(table, s);
775
+ /**
776
+ * Remove the table from the tablesToRemove array
777
+ */
778
+ tablesToRemove = tablesToRemove.filter((table) => {
779
+ return table.name !== tableToRename;
780
+ });
781
+ }
782
+ break;
783
+ }
784
+ }
785
+ }
786
+ else {
787
+ /*if (
788
+ await confirm({
789
+ message: `You are about to create a new table '${table.name}' for section '${table.sectionName}'. Proceed?`,
790
+ })
791
+ ) {*/
792
+ console.log(chalk.blueBright(`Creating table '${table.name}' for section '${table.sectionName}'`));
793
+ let options = {
794
+ createdAt: true,
795
+ updatedAt: true,
796
+ createdBy: true,
797
+ updatedBy: true,
798
+ };
799
+ if (table.sectionType === 'simple') {
800
+ options = {
801
+ createdAt: false,
802
+ updatedAt: true,
803
+ createdBy: false,
804
+ updatedBy: true,
805
+ };
806
+ }
807
+ if (table.sectionType === 'gallery') {
808
+ options = {
809
+ updatedBy: false,
810
+ updatedAt: false,
811
+ createdBy: true,
812
+ createdAt: true,
813
+ };
814
+ }
815
+ if (table.sectionType === 'destinationDb') {
816
+ options = {
817
+ createdAt: false,
818
+ updatedAt: false,
819
+ createdBy: false,
820
+ updatedBy: false,
821
+ };
822
+ }
823
+ await createTable(table, options);
824
+ /*} else {
825
+ console.log('Aborting...')
826
+ return null
827
+ }*/
828
+ }
829
+ }
830
+ }
831
+ /**
832
+ * Finally, check if there are tables to remove
833
+ */
834
+ if (tablesToRemove.length > 0) {
835
+ console.log(chalk.red(`There are ${tablesToRemove.length} table(s) to remove:`));
836
+ console.log(chalk.red(` - Warning: Removing a table will result in a permanent loss of its data!`));
837
+ /**
838
+ * Loop through the tables to remove
839
+ */
840
+ for (const table of tablesToRemove) {
841
+ s.stop();
842
+ const opType = await select({
843
+ message: `You are about to remove table '${table.name}'. Proceed?`,
844
+ options: [
845
+ { value: 'yes', label: 'Yes, drop it' },
846
+ { value: 'no', label: 'No, keep it' },
847
+ { value: 'later', label: 'Ask me later' },
848
+ ],
849
+ initialValue: 'later',
850
+ });
851
+ s.start();
852
+ switch (opType) {
853
+ case 'yes':
854
+ case 'no':
855
+ if (opType === 'yes') {
856
+ console.log(chalk.gray(` - Dropping table '${table.name}'`));
857
+ await db.execute(sql `DROP TABLE \`${sql.raw(table.name)}\``);
858
+ }
859
+ else {
860
+ console.log(chalk.gray(` - Keeping table '${table.name}'`));
861
+ }
862
+ /**
863
+ * Remove the table from the `__lz_tables` table
864
+ */
865
+ await db
866
+ .delete(LZTablesTable)
867
+ .where(eq(LZTablesTable.tableName, table.name))
868
+ .catch((error) => {
869
+ console.error('Error deleting from __lz_tables table:', error);
870
+ });
871
+ break;
872
+ case 'later':
873
+ console.log(chalk.gray(` - You will be asked next time for dropping table '${table.name}'`));
874
+ break;
875
+ }
876
+ }
877
+ }
878
+ };
879
+ export async function updateSections(useDevEnv = false) {
880
+ const s = spinner();
881
+ try {
882
+ // s.start()
883
+ // Set environment file if dev mode
884
+ if (useDevEnv) {
885
+ process.env.NODE_ENV = 'development';
886
+ }
887
+ // Run the main update logic
888
+ await main(s);
889
+ s.stop('Sections updated successfully');
890
+ }
891
+ catch (error) {
892
+ s.stop('Failed to update sections');
893
+ throw error;
894
+ }
895
+ }
896
+ // TODO: Two things to do:
897
+ // 1. Merge the createTable and updateTable functions into one function to also add the constraints while creating tables
898
+ // 2. Add the foreign key constraints logic