@momentumcms/migrations 0.3.0 → 0.4.0

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.
@@ -0,0 +1,1686 @@
1
+ // libs/migrations/src/cli/generate.ts
2
+ import { writeFileSync as writeFileSync2, mkdirSync as mkdirSync2 } from "node:fs";
3
+ import { join as join2, resolve } from "node:path";
4
+
5
+ // libs/core/src/lib/collections/define-collection.ts
6
+ function defineCollection(config) {
7
+ const collection = {
8
+ timestamps: true,
9
+ // Enable timestamps by default
10
+ ...config
11
+ };
12
+ if (!collection.slug) {
13
+ throw new Error("Collection must have a slug");
14
+ }
15
+ if (!collection.fields || collection.fields.length === 0) {
16
+ throw new Error(`Collection "${collection.slug}" must have at least one field`);
17
+ }
18
+ if (!/^[a-z][a-z0-9-]*$/.test(collection.slug)) {
19
+ throw new Error(
20
+ `Collection slug "${collection.slug}" must be kebab-case (lowercase letters, numbers, and hyphens, starting with a letter)`
21
+ );
22
+ }
23
+ return collection;
24
+ }
25
+ function getSoftDeleteField(config) {
26
+ if (!config.softDelete)
27
+ return null;
28
+ if (config.softDelete === true)
29
+ return "deletedAt";
30
+ const sdConfig = config.softDelete;
31
+ return sdConfig.field ?? "deletedAt";
32
+ }
33
+
34
+ // libs/core/src/lib/fields/field.types.ts
35
+ function isNamedTab(tab) {
36
+ return typeof tab.name === "string" && tab.name.length > 0;
37
+ }
38
+ function flattenDataFields(fields) {
39
+ const result = [];
40
+ for (const field of fields) {
41
+ if (field.type === "tabs") {
42
+ for (const tab of field.tabs) {
43
+ if (isNamedTab(tab)) {
44
+ const syntheticGroup = {
45
+ name: tab.name,
46
+ type: "group",
47
+ label: tab.label,
48
+ description: tab.description,
49
+ fields: tab.fields
50
+ };
51
+ result.push(syntheticGroup);
52
+ } else {
53
+ result.push(...flattenDataFields(tab.fields));
54
+ }
55
+ }
56
+ } else if (field.type === "collapsible" || field.type === "row") {
57
+ result.push(...flattenDataFields(field.fields));
58
+ } else {
59
+ result.push(field);
60
+ }
61
+ }
62
+ return result;
63
+ }
64
+
65
+ // libs/core/src/lib/fields/field-builders.ts
66
+ function text(name, options = {}) {
67
+ return {
68
+ name,
69
+ type: "text",
70
+ ...options
71
+ };
72
+ }
73
+ function number(name, options = {}) {
74
+ return {
75
+ name,
76
+ type: "number",
77
+ ...options
78
+ };
79
+ }
80
+ function json(name, options = {}) {
81
+ return {
82
+ name,
83
+ type: "json",
84
+ ...options
85
+ };
86
+ }
87
+
88
+ // libs/core/src/lib/collections/media.collection.ts
89
+ var MediaCollection = defineCollection({
90
+ slug: "media",
91
+ labels: {
92
+ singular: "Media",
93
+ plural: "Media"
94
+ },
95
+ upload: {
96
+ mimeTypes: ["image/*", "application/pdf", "video/*", "audio/*"]
97
+ },
98
+ admin: {
99
+ useAsTitle: "filename",
100
+ defaultColumns: ["filename", "mimeType", "filesize", "createdAt"]
101
+ },
102
+ fields: [
103
+ text("filename", {
104
+ required: true,
105
+ label: "Filename",
106
+ description: "Original filename of the uploaded file"
107
+ }),
108
+ text("mimeType", {
109
+ required: true,
110
+ label: "MIME Type",
111
+ description: "File MIME type (e.g., image/jpeg, application/pdf)"
112
+ }),
113
+ number("filesize", {
114
+ label: "File Size",
115
+ description: "File size in bytes"
116
+ }),
117
+ text("path", {
118
+ label: "Storage Path",
119
+ description: "Path/key where the file is stored",
120
+ admin: {
121
+ hidden: true
122
+ }
123
+ }),
124
+ text("url", {
125
+ label: "URL",
126
+ description: "Public URL to access the file"
127
+ }),
128
+ text("alt", {
129
+ label: "Alt Text",
130
+ description: "Alternative text for accessibility"
131
+ }),
132
+ number("width", {
133
+ label: "Width",
134
+ description: "Image width in pixels (for images only)"
135
+ }),
136
+ number("height", {
137
+ label: "Height",
138
+ description: "Image height in pixels (for images only)"
139
+ }),
140
+ json("focalPoint", {
141
+ label: "Focal Point",
142
+ description: "Focal point coordinates for image cropping",
143
+ admin: {
144
+ hidden: true
145
+ }
146
+ })
147
+ ],
148
+ access: {
149
+ // Media is readable by anyone by default
150
+ read: () => true,
151
+ // Only authenticated users can create/update/delete
152
+ create: ({ req }) => !!req?.user,
153
+ update: ({ req }) => !!req?.user,
154
+ delete: ({ req }) => !!req?.user
155
+ }
156
+ });
157
+
158
+ // libs/core/src/lib/migrations.ts
159
+ function resolveMigrationMode(mode) {
160
+ if (mode === "push" || mode === "migrate")
161
+ return mode;
162
+ const env = process.env["NODE_ENV"];
163
+ if (env === "production")
164
+ return "migrate";
165
+ return "push";
166
+ }
167
+ function resolveMigrationConfig(config) {
168
+ if (!config)
169
+ return void 0;
170
+ const mode = resolveMigrationMode(config.mode);
171
+ return {
172
+ ...config,
173
+ directory: config.directory ?? "./migrations",
174
+ mode,
175
+ cloneTest: config.cloneTest ?? mode === "migrate",
176
+ dangerDetection: config.dangerDetection ?? true,
177
+ autoApply: config.autoApply ?? mode === "push"
178
+ };
179
+ }
180
+
181
+ // libs/migrations/src/lib/schema/schema-snapshot.ts
182
+ import { createHash } from "node:crypto";
183
+ var INTERNAL_TABLES = /* @__PURE__ */ new Set(["_momentum_migrations", "_momentum_seeds", "_globals"]);
184
+ function computeSchemaChecksum(tables) {
185
+ const normalized = tables.map((t) => ({
186
+ name: t.name,
187
+ columns: [...t.columns].sort((a, b) => a.name.localeCompare(b.name)),
188
+ foreignKeys: [...t.foreignKeys].sort(
189
+ (a, b) => a.constraintName.localeCompare(b.constraintName)
190
+ ),
191
+ indexes: [...t.indexes].sort((a, b) => a.name.localeCompare(b.name))
192
+ })).sort((a, b) => a.name.localeCompare(b.name));
193
+ const json2 = JSON.stringify(normalized);
194
+ return createHash("sha256").update(json2).digest("hex");
195
+ }
196
+ function createSchemaSnapshot(dialect, tables) {
197
+ return {
198
+ dialect,
199
+ tables,
200
+ capturedAt: (/* @__PURE__ */ new Date()).toISOString(),
201
+ checksum: computeSchemaChecksum(tables)
202
+ };
203
+ }
204
+ function serializeSnapshot(snapshot) {
205
+ return JSON.stringify(snapshot, null, " ");
206
+ }
207
+ function deserializeSnapshot(json2) {
208
+ const parsed = JSON.parse(json2);
209
+ if (!isSchemaSnapshot(parsed)) {
210
+ throw new Error("Invalid schema snapshot JSON");
211
+ }
212
+ return parsed;
213
+ }
214
+ function isSchemaSnapshot(value) {
215
+ if (typeof value !== "object" || value === null)
216
+ return false;
217
+ const obj = value;
218
+ return (obj["dialect"] === "postgresql" || obj["dialect"] === "sqlite") && Array.isArray(obj["tables"]) && typeof obj["capturedAt"] === "string" && typeof obj["checksum"] === "string";
219
+ }
220
+
221
+ // libs/migrations/src/lib/schema/column-type-map.ts
222
+ function fieldToPostgresType(field) {
223
+ switch (field.type) {
224
+ case "text":
225
+ case "textarea":
226
+ case "richText":
227
+ case "password":
228
+ case "radio":
229
+ case "point":
230
+ return "TEXT";
231
+ case "email":
232
+ case "slug":
233
+ case "select":
234
+ return "VARCHAR(255)";
235
+ case "number":
236
+ return "NUMERIC";
237
+ case "checkbox":
238
+ return "BOOLEAN";
239
+ case "date":
240
+ return "TIMESTAMPTZ";
241
+ case "relationship":
242
+ case "upload":
243
+ return "VARCHAR(36)";
244
+ case "array":
245
+ case "group":
246
+ case "blocks":
247
+ case "json":
248
+ return "JSONB";
249
+ case "tabs":
250
+ case "collapsible":
251
+ case "row":
252
+ return "TEXT";
253
+ default:
254
+ return "TEXT";
255
+ }
256
+ }
257
+ function fieldToSqliteType(field) {
258
+ switch (field.type) {
259
+ case "text":
260
+ case "textarea":
261
+ case "richText":
262
+ case "email":
263
+ case "slug":
264
+ case "select":
265
+ case "password":
266
+ case "radio":
267
+ case "point":
268
+ return "TEXT";
269
+ case "number":
270
+ return "REAL";
271
+ case "checkbox":
272
+ return "INTEGER";
273
+ case "date":
274
+ case "relationship":
275
+ case "upload":
276
+ return "TEXT";
277
+ case "array":
278
+ case "group":
279
+ case "blocks":
280
+ case "json":
281
+ return "TEXT";
282
+ default:
283
+ return "TEXT";
284
+ }
285
+ }
286
+ function fieldToColumnType(field, dialect) {
287
+ if (dialect === "postgresql")
288
+ return fieldToPostgresType(field);
289
+ return fieldToSqliteType(field);
290
+ }
291
+ function normalizeColumnType(rawType, dialect) {
292
+ const upper = rawType.toUpperCase().trim();
293
+ if (dialect === "postgresql") {
294
+ return normalizePgType(upper);
295
+ }
296
+ return normalizeSqliteType(upper);
297
+ }
298
+ function normalizePgType(type) {
299
+ const charVaryingMatch = type.match(/^CHARACTER VARYING\((\d+)\)$/);
300
+ if (charVaryingMatch)
301
+ return `VARCHAR(${charVaryingMatch[1]})`;
302
+ if (type === "CHARACTER VARYING")
303
+ return "VARCHAR(255)";
304
+ if (type === "TIMESTAMP WITH TIME ZONE")
305
+ return "TIMESTAMPTZ";
306
+ if (type === "TIMESTAMP WITHOUT TIME ZONE")
307
+ return "TIMESTAMP";
308
+ if (type === "BOOLEAN")
309
+ return "BOOLEAN";
310
+ if (type === "NUMERIC")
311
+ return "NUMERIC";
312
+ if (type === "TEXT")
313
+ return "TEXT";
314
+ if (type === "JSONB")
315
+ return "JSONB";
316
+ if (type === "JSON")
317
+ return "JSON";
318
+ if (type === "INTEGER")
319
+ return "INTEGER";
320
+ if (type === "BIGINT")
321
+ return "BIGINT";
322
+ if (type === "REAL")
323
+ return "REAL";
324
+ if (type === "DOUBLE PRECISION")
325
+ return "DOUBLE PRECISION";
326
+ return type;
327
+ }
328
+ function normalizeSqliteType(type) {
329
+ if (type === "INT" || type === "INTEGER")
330
+ return "INTEGER";
331
+ if (type === "REAL" || type === "FLOAT" || type === "DOUBLE")
332
+ return "REAL";
333
+ return type;
334
+ }
335
+ function areTypesCompatible(typeA, typeB, dialect) {
336
+ const normA = normalizeColumnType(typeA, dialect);
337
+ const normB = normalizeColumnType(typeB, dialect);
338
+ return normA === normB;
339
+ }
340
+
341
+ // libs/migrations/src/lib/schema/collections-to-schema.ts
342
+ function mapOnDelete(onDelete, required) {
343
+ const effective = required && (!onDelete || onDelete === "set-null") ? "restrict" : onDelete;
344
+ switch (effective) {
345
+ case "restrict":
346
+ return "RESTRICT";
347
+ case "cascade":
348
+ return "CASCADE";
349
+ default:
350
+ return "SET NULL";
351
+ }
352
+ }
353
+ function getTableName(collection) {
354
+ return collection.dbName ?? collection.slug;
355
+ }
356
+ function hasVersionDrafts(collection) {
357
+ const versions = collection.versions;
358
+ if (!versions)
359
+ return false;
360
+ if (typeof versions === "boolean")
361
+ return false;
362
+ return !!versions.drafts;
363
+ }
364
+ function isCollectionConfig(value) {
365
+ return typeof value === "object" && value !== null && "slug" in value && "fields" in value;
366
+ }
367
+ function resolveCollectionRef(ref) {
368
+ try {
369
+ const resolved = ref();
370
+ if (isCollectionConfig(resolved)) {
371
+ return resolved;
372
+ }
373
+ return null;
374
+ } catch {
375
+ return null;
376
+ }
377
+ }
378
+ function buildAutoColumns(collection, dialect) {
379
+ const columns = [];
380
+ columns.push({
381
+ name: "id",
382
+ type: dialect === "postgresql" ? "VARCHAR(36)" : "TEXT",
383
+ nullable: false,
384
+ defaultValue: null,
385
+ isPrimaryKey: true
386
+ });
387
+ const timestamps = collection.timestamps;
388
+ const addCreatedAt = timestamps !== false && (timestamps === true || timestamps === void 0 || timestamps.createdAt !== false);
389
+ const addUpdatedAt = timestamps !== false && (timestamps === true || timestamps === void 0 || timestamps.updatedAt !== false);
390
+ if (addCreatedAt) {
391
+ columns.push({
392
+ name: "createdAt",
393
+ type: dialect === "postgresql" ? "TIMESTAMPTZ" : "TEXT",
394
+ nullable: false,
395
+ defaultValue: null,
396
+ isPrimaryKey: false
397
+ });
398
+ }
399
+ if (addUpdatedAt) {
400
+ columns.push({
401
+ name: "updatedAt",
402
+ type: dialect === "postgresql" ? "TIMESTAMPTZ" : "TEXT",
403
+ nullable: false,
404
+ defaultValue: null,
405
+ isPrimaryKey: false
406
+ });
407
+ }
408
+ if (hasVersionDrafts(collection)) {
409
+ columns.push({
410
+ name: "_status",
411
+ type: dialect === "postgresql" ? "VARCHAR(20)" : "TEXT",
412
+ nullable: false,
413
+ defaultValue: "'draft'",
414
+ isPrimaryKey: false
415
+ });
416
+ }
417
+ const softDeleteCol = getSoftDeleteField(collection);
418
+ if (softDeleteCol) {
419
+ columns.push({
420
+ name: softDeleteCol,
421
+ type: dialect === "postgresql" ? "TIMESTAMPTZ" : "TEXT",
422
+ nullable: true,
423
+ defaultValue: null,
424
+ isPrimaryKey: false
425
+ });
426
+ }
427
+ return columns;
428
+ }
429
+ function fieldToColumn(field, dialect) {
430
+ return {
431
+ name: field.name,
432
+ type: fieldToColumnType(field, dialect),
433
+ nullable: !field.required,
434
+ defaultValue: null,
435
+ isPrimaryKey: false
436
+ };
437
+ }
438
+ function buildForeignKeys(tableName, fields) {
439
+ const foreignKeys = [];
440
+ for (const field of fields) {
441
+ if (field.type !== "relationship")
442
+ continue;
443
+ if (field.hasMany)
444
+ continue;
445
+ if (field.relationTo && field.relationTo.length > 0)
446
+ continue;
447
+ const target = resolveCollectionRef(field.collection);
448
+ if (!target)
449
+ continue;
450
+ const targetTable = getTableName(target);
451
+ const onDelete = mapOnDelete(field.onDelete, !!field.required);
452
+ foreignKeys.push({
453
+ constraintName: `fk_${tableName}_${field.name}`,
454
+ column: field.name,
455
+ referencedTable: targetTable,
456
+ referencedColumn: "id",
457
+ onDelete
458
+ });
459
+ }
460
+ return foreignKeys;
461
+ }
462
+ function buildIndexes(tableName, collection) {
463
+ const indexes = [];
464
+ const sdField = getSoftDeleteField(collection);
465
+ if (sdField) {
466
+ indexes.push({
467
+ name: `idx_${tableName}_${sdField}`,
468
+ columns: [sdField],
469
+ unique: false
470
+ });
471
+ }
472
+ if (collection.indexes) {
473
+ for (const idx of collection.indexes) {
474
+ indexes.push({
475
+ name: idx.name ?? `idx_${tableName}_${idx.columns.join("_")}`,
476
+ columns: [...idx.columns],
477
+ unique: !!idx.unique
478
+ });
479
+ }
480
+ }
481
+ return indexes;
482
+ }
483
+ function buildVersionTable(collection, dialect) {
484
+ if (!collection.versions)
485
+ return null;
486
+ const baseTable = getTableName(collection);
487
+ const tableName = `${baseTable}_versions`;
488
+ const columns = [
489
+ {
490
+ name: "id",
491
+ type: dialect === "postgresql" ? "VARCHAR(36)" : "TEXT",
492
+ nullable: false,
493
+ defaultValue: null,
494
+ isPrimaryKey: true
495
+ },
496
+ {
497
+ name: "parent",
498
+ type: dialect === "postgresql" ? "VARCHAR(36)" : "TEXT",
499
+ nullable: false,
500
+ defaultValue: null,
501
+ isPrimaryKey: false
502
+ },
503
+ {
504
+ name: "version",
505
+ type: "TEXT",
506
+ nullable: false,
507
+ defaultValue: null,
508
+ isPrimaryKey: false
509
+ },
510
+ {
511
+ name: "_status",
512
+ type: dialect === "postgresql" ? "VARCHAR(20)" : "TEXT",
513
+ nullable: false,
514
+ defaultValue: "'draft'",
515
+ isPrimaryKey: false
516
+ },
517
+ {
518
+ name: "autosave",
519
+ type: dialect === "postgresql" ? "BOOLEAN" : "INTEGER",
520
+ nullable: false,
521
+ defaultValue: dialect === "postgresql" ? "false" : "0",
522
+ isPrimaryKey: false
523
+ },
524
+ {
525
+ name: "publishedAt",
526
+ type: dialect === "postgresql" ? "TIMESTAMPTZ" : "TEXT",
527
+ nullable: true,
528
+ defaultValue: null,
529
+ isPrimaryKey: false
530
+ },
531
+ {
532
+ name: "createdAt",
533
+ type: dialect === "postgresql" ? "TIMESTAMPTZ" : "TEXT",
534
+ nullable: false,
535
+ defaultValue: null,
536
+ isPrimaryKey: false
537
+ },
538
+ {
539
+ name: "updatedAt",
540
+ type: dialect === "postgresql" ? "TIMESTAMPTZ" : "TEXT",
541
+ nullable: false,
542
+ defaultValue: null,
543
+ isPrimaryKey: false
544
+ }
545
+ ];
546
+ const foreignKeys = [
547
+ {
548
+ constraintName: `fk_${tableName}_parent`,
549
+ column: "parent",
550
+ referencedTable: baseTable,
551
+ referencedColumn: "id",
552
+ onDelete: "CASCADE"
553
+ }
554
+ ];
555
+ const indexes = [
556
+ { name: `idx_${tableName}_parent`, columns: ["parent"], unique: false },
557
+ { name: `idx_${tableName}_status`, columns: ["_status"], unique: false },
558
+ { name: `idx_${tableName}_createdAt`, columns: ["createdAt"], unique: false }
559
+ ];
560
+ return { name: tableName, columns, foreignKeys, indexes };
561
+ }
562
+ function collectionToTableSnapshot(collection, dialect) {
563
+ const tableName = getTableName(collection);
564
+ const dataFields = flattenDataFields(collection.fields);
565
+ const columns = [
566
+ ...buildAutoColumns(collection, dialect),
567
+ ...dataFields.map((f) => fieldToColumn(f, dialect))
568
+ ];
569
+ const foreignKeys = buildForeignKeys(tableName, dataFields);
570
+ const indexes = buildIndexes(tableName, collection);
571
+ return { name: tableName, columns, foreignKeys, indexes };
572
+ }
573
+ function collectionsToSchema(collections, dialect) {
574
+ const tables = [];
575
+ for (const collection of collections) {
576
+ tables.push(collectionToTableSnapshot(collection, dialect));
577
+ const versionTable = buildVersionTable(collection, dialect);
578
+ if (versionTable) {
579
+ tables.push(versionTable);
580
+ }
581
+ }
582
+ return createSchemaSnapshot(dialect, tables);
583
+ }
584
+
585
+ // libs/migrations/src/lib/schema/schema-diff.ts
586
+ var DEFAULT_DIFF_OPTIONS = {
587
+ detectRenames: true,
588
+ renameSimilarityThreshold: 0.6
589
+ };
590
+ function diffSchemas(desired, actual, dialect, options) {
591
+ const opts = { ...DEFAULT_DIFF_OPTIONS, ...options };
592
+ const operations = [];
593
+ const summary = [];
594
+ const desiredMap = /* @__PURE__ */ new Map();
595
+ const actualMap = /* @__PURE__ */ new Map();
596
+ for (const t of desired.tables)
597
+ desiredMap.set(t.name, t);
598
+ for (const t of actual.tables)
599
+ actualMap.set(t.name, t);
600
+ for (const [name, desiredTable] of desiredMap) {
601
+ if (!actualMap.has(name)) {
602
+ operations.push({
603
+ type: "createTable",
604
+ table: name,
605
+ columns: desiredTable.columns.map((c) => ({
606
+ name: c.name,
607
+ type: c.type,
608
+ nullable: c.nullable,
609
+ defaultValue: c.defaultValue ?? void 0,
610
+ primaryKey: c.isPrimaryKey || void 0
611
+ }))
612
+ });
613
+ summary.push(`Create table "${name}"`);
614
+ for (const fk of desiredTable.foreignKeys) {
615
+ operations.push({
616
+ type: "addForeignKey",
617
+ table: name,
618
+ constraintName: fk.constraintName,
619
+ column: fk.column,
620
+ referencedTable: fk.referencedTable,
621
+ referencedColumn: fk.referencedColumn,
622
+ onDelete: fk.onDelete
623
+ });
624
+ }
625
+ for (const idx of desiredTable.indexes) {
626
+ operations.push({
627
+ type: "createIndex",
628
+ table: name,
629
+ indexName: idx.name,
630
+ columns: idx.columns,
631
+ unique: idx.unique
632
+ });
633
+ }
634
+ }
635
+ }
636
+ for (const [name] of actualMap) {
637
+ if (!desiredMap.has(name)) {
638
+ operations.push({ type: "dropTable", table: name });
639
+ summary.push(`Drop table "${name}"`);
640
+ }
641
+ }
642
+ for (const [name, desiredTable] of desiredMap) {
643
+ const actualTable = actualMap.get(name);
644
+ if (!actualTable)
645
+ continue;
646
+ const tableOps = diffTable(desiredTable, actualTable, dialect, opts);
647
+ operations.push(...tableOps.operations);
648
+ summary.push(...tableOps.summary);
649
+ }
650
+ return {
651
+ hasChanges: operations.length > 0,
652
+ operations,
653
+ summary
654
+ };
655
+ }
656
+ function diffTable(desired, actual, dialect, opts) {
657
+ const operations = [];
658
+ const summary = [];
659
+ const tableName = desired.name;
660
+ const colOps = diffColumns(tableName, desired.columns, actual.columns, dialect, opts);
661
+ operations.push(...colOps.operations);
662
+ summary.push(...colOps.summary);
663
+ const fkOps = diffForeignKeys(tableName, desired.foreignKeys, actual.foreignKeys);
664
+ operations.push(...fkOps.operations);
665
+ summary.push(...fkOps.summary);
666
+ const idxOps = diffIndexes(tableName, desired.indexes, actual.indexes);
667
+ operations.push(...idxOps.operations);
668
+ summary.push(...idxOps.summary);
669
+ return { operations, summary };
670
+ }
671
+ function diffColumns(tableName, desiredColumns, actualColumns, dialect, opts) {
672
+ const operations = [];
673
+ const summary = [];
674
+ const desiredMap = /* @__PURE__ */ new Map();
675
+ const actualMap = /* @__PURE__ */ new Map();
676
+ for (const c of desiredColumns)
677
+ desiredMap.set(c.name, c);
678
+ for (const c of actualColumns)
679
+ actualMap.set(c.name, c);
680
+ const renamedFrom = /* @__PURE__ */ new Set();
681
+ const renamedTo = /* @__PURE__ */ new Set();
682
+ if (opts.detectRenames) {
683
+ const missingInActual = [...desiredMap.keys()].filter((k) => !actualMap.has(k));
684
+ const extraInActual = [...actualMap.keys()].filter((k) => !desiredMap.has(k));
685
+ for (const newName of missingInActual) {
686
+ const desiredCol = desiredMap.get(newName);
687
+ for (const oldName of extraInActual) {
688
+ if (renamedFrom.has(oldName))
689
+ continue;
690
+ const actualCol = actualMap.get(oldName);
691
+ if (areTypesCompatible(desiredCol.type, actualCol.type, dialect)) {
692
+ operations.push({
693
+ type: "renameColumn",
694
+ table: tableName,
695
+ from: oldName,
696
+ to: newName
697
+ });
698
+ summary.push(
699
+ `Rename column "${tableName}"."${oldName}" \u2192 "${newName}"`
700
+ );
701
+ renamedFrom.add(oldName);
702
+ renamedTo.add(newName);
703
+ break;
704
+ }
705
+ }
706
+ }
707
+ }
708
+ for (const [name, desiredCol] of desiredMap) {
709
+ if (actualMap.has(name) || renamedTo.has(name))
710
+ continue;
711
+ operations.push({
712
+ type: "addColumn",
713
+ table: tableName,
714
+ column: name,
715
+ columnType: desiredCol.type,
716
+ nullable: desiredCol.nullable,
717
+ defaultValue: desiredCol.defaultValue ?? void 0
718
+ });
719
+ summary.push(`Add column "${tableName}"."${name}" (${desiredCol.type})`);
720
+ }
721
+ for (const [name, actualCol] of actualMap) {
722
+ if (desiredMap.has(name) || renamedFrom.has(name))
723
+ continue;
724
+ operations.push({
725
+ type: "dropColumn",
726
+ table: tableName,
727
+ column: name,
728
+ previousType: actualCol.type,
729
+ previousNullable: actualCol.nullable
730
+ });
731
+ summary.push(`Drop column "${tableName}"."${name}"`);
732
+ }
733
+ for (const [name, desiredCol] of desiredMap) {
734
+ const actualCol = actualMap.get(name);
735
+ if (!actualCol)
736
+ continue;
737
+ if (!areTypesCompatible(desiredCol.type, actualCol.type, dialect)) {
738
+ operations.push({
739
+ type: "alterColumnType",
740
+ table: tableName,
741
+ column: name,
742
+ fromType: normalizeColumnType(actualCol.type, dialect),
743
+ toType: normalizeColumnType(desiredCol.type, dialect)
744
+ });
745
+ summary.push(
746
+ `Change type "${tableName}"."${name}": ${actualCol.type} \u2192 ${desiredCol.type}`
747
+ );
748
+ }
749
+ if (desiredCol.nullable !== actualCol.nullable) {
750
+ operations.push({
751
+ type: "alterColumnNullable",
752
+ table: tableName,
753
+ column: name,
754
+ nullable: desiredCol.nullable
755
+ });
756
+ summary.push(
757
+ `Change nullable "${tableName}"."${name}": ${actualCol.nullable} \u2192 ${desiredCol.nullable}`
758
+ );
759
+ }
760
+ if (normalizeDefault(desiredCol.defaultValue) !== normalizeDefault(actualCol.defaultValue)) {
761
+ operations.push({
762
+ type: "alterColumnDefault",
763
+ table: tableName,
764
+ column: name,
765
+ defaultValue: desiredCol.defaultValue,
766
+ previousDefault: actualCol.defaultValue
767
+ });
768
+ summary.push(
769
+ `Change default "${tableName}"."${name}": ${actualCol.defaultValue ?? "NULL"} \u2192 ${desiredCol.defaultValue ?? "NULL"}`
770
+ );
771
+ }
772
+ }
773
+ return { operations, summary };
774
+ }
775
+ function normalizeDefault(value) {
776
+ if (value === null || value === void 0 || value === "")
777
+ return null;
778
+ return value;
779
+ }
780
+ function diffForeignKeys(tableName, desiredFks, actualFks) {
781
+ const operations = [];
782
+ const summary = [];
783
+ const desiredMap = /* @__PURE__ */ new Map();
784
+ const actualMap = /* @__PURE__ */ new Map();
785
+ for (const fk of desiredFks)
786
+ desiredMap.set(fk.constraintName, fk);
787
+ for (const fk of actualFks)
788
+ actualMap.set(fk.constraintName, fk);
789
+ for (const [name, fk] of desiredMap) {
790
+ if (!actualMap.has(name)) {
791
+ operations.push({
792
+ type: "addForeignKey",
793
+ table: tableName,
794
+ constraintName: fk.constraintName,
795
+ column: fk.column,
796
+ referencedTable: fk.referencedTable,
797
+ referencedColumn: fk.referencedColumn,
798
+ onDelete: fk.onDelete
799
+ });
800
+ summary.push(`Add foreign key "${name}" on "${tableName}"`);
801
+ } else {
802
+ const actualFk = actualMap.get(name);
803
+ if (fk.column !== actualFk.column || fk.referencedTable !== actualFk.referencedTable || fk.referencedColumn !== actualFk.referencedColumn || fk.onDelete !== actualFk.onDelete) {
804
+ operations.push({
805
+ type: "dropForeignKey",
806
+ table: tableName,
807
+ constraintName: name
808
+ });
809
+ operations.push({
810
+ type: "addForeignKey",
811
+ table: tableName,
812
+ constraintName: fk.constraintName,
813
+ column: fk.column,
814
+ referencedTable: fk.referencedTable,
815
+ referencedColumn: fk.referencedColumn,
816
+ onDelete: fk.onDelete
817
+ });
818
+ summary.push(`Modify foreign key "${name}" on "${tableName}"`);
819
+ }
820
+ }
821
+ }
822
+ for (const [name] of actualMap) {
823
+ if (!desiredMap.has(name)) {
824
+ operations.push({
825
+ type: "dropForeignKey",
826
+ table: tableName,
827
+ constraintName: name
828
+ });
829
+ summary.push(`Drop foreign key "${name}" on "${tableName}"`);
830
+ }
831
+ }
832
+ return { operations, summary };
833
+ }
834
+ function diffIndexes(tableName, desiredIdxs, actualIdxs) {
835
+ const operations = [];
836
+ const summary = [];
837
+ const desiredMap = /* @__PURE__ */ new Map();
838
+ const actualMap = /* @__PURE__ */ new Map();
839
+ for (const idx of desiredIdxs)
840
+ desiredMap.set(idx.name, idx);
841
+ for (const idx of actualIdxs)
842
+ actualMap.set(idx.name, idx);
843
+ for (const [name, idx] of desiredMap) {
844
+ if (!actualMap.has(name)) {
845
+ operations.push({
846
+ type: "createIndex",
847
+ table: tableName,
848
+ indexName: idx.name,
849
+ columns: idx.columns,
850
+ unique: idx.unique
851
+ });
852
+ summary.push(`Create index "${name}" on "${tableName}"`);
853
+ } else {
854
+ const actualIdx = actualMap.get(name);
855
+ if (idx.unique !== actualIdx.unique || JSON.stringify(idx.columns) !== JSON.stringify(actualIdx.columns)) {
856
+ operations.push({
857
+ type: "dropIndex",
858
+ table: tableName,
859
+ indexName: name
860
+ });
861
+ operations.push({
862
+ type: "createIndex",
863
+ table: tableName,
864
+ indexName: idx.name,
865
+ columns: idx.columns,
866
+ unique: idx.unique
867
+ });
868
+ summary.push(`Modify index "${name}" on "${tableName}"`);
869
+ }
870
+ }
871
+ }
872
+ for (const [name] of actualMap) {
873
+ if (!desiredMap.has(name)) {
874
+ operations.push({
875
+ type: "dropIndex",
876
+ table: tableName,
877
+ indexName: name
878
+ });
879
+ summary.push(`Drop index "${name}" on "${tableName}"`);
880
+ }
881
+ }
882
+ return { operations, summary };
883
+ }
884
+
885
+ // libs/migrations/src/lib/danger/danger-detector.ts
886
+ function detectDangers(operations, dialect) {
887
+ const warnings = [];
888
+ for (let i = 0; i < operations.length; i++) {
889
+ const op = operations[i];
890
+ warnings.push(...checkOperation(op, i, operations, dialect));
891
+ }
892
+ const severityOrder = { error: 0, warning: 1, info: 2 };
893
+ warnings.sort((a, b) => severityOrder[a.severity] - severityOrder[b.severity]);
894
+ return {
895
+ warnings,
896
+ hasErrors: warnings.some((w) => w.severity === "error"),
897
+ hasWarnings: warnings.some((w) => w.severity === "warning")
898
+ };
899
+ }
900
+ function checkOperation(op, index, _allOps, dialect) {
901
+ const warnings = [];
902
+ switch (op.type) {
903
+ case "dropTable":
904
+ warnings.push({
905
+ severity: "error",
906
+ operation: op,
907
+ operationIndex: index,
908
+ message: `Dropping table "${op.table}" will permanently delete all data.`,
909
+ suggestion: 'Consider renaming the table with a deprecation prefix (e.g., "_deprecated_") and scheduling deletion after verifying no data is needed.'
910
+ });
911
+ break;
912
+ case "dropColumn":
913
+ warnings.push({
914
+ severity: "warning",
915
+ operation: op,
916
+ operationIndex: index,
917
+ message: `Dropping column "${op.table}"."${op.column}" will permanently delete all values in this column.`,
918
+ suggestion: "Before dropping, verify the column data is either migrated elsewhere or truly unneeded. Consider a backup or data export first."
919
+ });
920
+ break;
921
+ case "alterColumnType":
922
+ warnings.push(...checkTypeChange(op, index, dialect));
923
+ break;
924
+ case "alterColumnNullable":
925
+ if (!op.nullable) {
926
+ warnings.push({
927
+ severity: "warning",
928
+ operation: op,
929
+ operationIndex: index,
930
+ message: `Setting "${op.table}"."${op.column}" to NOT NULL may fail if existing rows contain NULL values.`,
931
+ suggestion: "First backfill NULL values with a default (e.g., UPDATE table SET column = 'default' WHERE column IS NULL), then add the NOT NULL constraint."
932
+ });
933
+ }
934
+ break;
935
+ case "addColumn":
936
+ if (!op.nullable && !op.defaultValue) {
937
+ warnings.push({
938
+ severity: "error",
939
+ operation: op,
940
+ operationIndex: index,
941
+ message: `Adding NOT NULL column "${op.table}"."${op.column}" without a default value will fail if the table has existing rows.`,
942
+ suggestion: "Either add a DEFAULT value, make the column nullable first and backfill, or add the column as nullable, backfill, then alter to NOT NULL."
943
+ });
944
+ }
945
+ break;
946
+ case "renameColumn":
947
+ warnings.push({
948
+ severity: "warning",
949
+ operation: op,
950
+ operationIndex: index,
951
+ message: `Renaming "${op.table}"."${op.from}" to "${op.to}" may break application code that references the old name.`,
952
+ suggestion: "Deploy application code changes to use the new column name before or alongside the migration. Consider a phased approach: add new column, migrate data, update code, then drop old column."
953
+ });
954
+ break;
955
+ case "renameTable":
956
+ warnings.push({
957
+ severity: "warning",
958
+ operation: op,
959
+ operationIndex: index,
960
+ message: `Renaming table "${op.from}" to "${op.to}" may break application code and queries.`,
961
+ suggestion: "Update application code to use the new table name before or alongside the migration."
962
+ });
963
+ break;
964
+ case "addForeignKey":
965
+ if (dialect === "postgresql") {
966
+ warnings.push({
967
+ severity: "info",
968
+ operation: op,
969
+ operationIndex: index,
970
+ message: `Adding foreign key "${op.constraintName}" acquires an ACCESS EXCLUSIVE lock on the referenced table.`,
971
+ suggestion: "On large tables, consider adding the FK constraint with NOT VALID first, then validating separately: ALTER TABLE ... ADD CONSTRAINT ... NOT VALID; ALTER TABLE ... VALIDATE CONSTRAINT ..."
972
+ });
973
+ }
974
+ break;
975
+ case "createIndex":
976
+ if (dialect === "postgresql" && !isCreateIndexConcurrent(op)) {
977
+ warnings.push({
978
+ severity: "info",
979
+ operation: op,
980
+ operationIndex: index,
981
+ message: `Creating index "${op.indexName}" will lock "${op.table}" for writes during index creation.`,
982
+ suggestion: "For large tables, consider CREATE INDEX CONCURRENTLY to avoid blocking writes (requires running outside a transaction)."
983
+ });
984
+ }
985
+ break;
986
+ }
987
+ return warnings;
988
+ }
989
+ function checkTypeChange(op, index, dialect) {
990
+ const warnings = [];
991
+ if (dialect === "sqlite") {
992
+ warnings.push({
993
+ severity: "error",
994
+ operation: op,
995
+ operationIndex: index,
996
+ message: `SQLite does not support ALTER COLUMN TYPE. Changing "${op.table}"."${op.column}" from ${op.fromType} to ${op.toType} requires a table rebuild.`,
997
+ suggestion: "Create a new table with the desired schema, copy data, drop old table, and rename new table. Use a raw SQL migration for this."
998
+ });
999
+ return warnings;
1000
+ }
1001
+ if (isLossyTypeChange(op.fromType, op.toType)) {
1002
+ warnings.push({
1003
+ severity: "warning",
1004
+ operation: op,
1005
+ operationIndex: index,
1006
+ message: `Changing "${op.table}"."${op.column}" from ${op.fromType} to ${op.toType} may cause data loss or cast errors.`,
1007
+ suggestion: "Test the type conversion on a clone database first. Consider adding a USING clause with explicit cast logic."
1008
+ });
1009
+ }
1010
+ if (isTableRewriteType(op.fromType, op.toType)) {
1011
+ warnings.push({
1012
+ severity: "info",
1013
+ operation: op,
1014
+ operationIndex: index,
1015
+ message: `Changing "${op.table}"."${op.column}" from ${op.fromType} to ${op.toType} may require a table rewrite on large tables.`,
1016
+ suggestion: "On large tables, this can take significant time and lock the table. Consider running during low-traffic periods or using a phased approach."
1017
+ });
1018
+ }
1019
+ return warnings;
1020
+ }
1021
+ function isLossyTypeChange(from, to) {
1022
+ const fromUpper = from.toUpperCase();
1023
+ const toUpper = to.toUpperCase();
1024
+ if (isTextType(fromUpper) && isNumericType(toUpper))
1025
+ return true;
1026
+ if (fromUpper === "NUMERIC" && (toUpper === "INTEGER" || toUpper === "SMALLINT"))
1027
+ return true;
1028
+ if (fromUpper === "BIGINT" && (toUpper === "INTEGER" || toUpper === "SMALLINT"))
1029
+ return true;
1030
+ if (fromUpper === "DOUBLE PRECISION" && toUpper === "REAL")
1031
+ return true;
1032
+ if ((fromUpper === "JSONB" || fromUpper === "JSON") && !fromUpper.includes("JSON"))
1033
+ return true;
1034
+ if (fromUpper.includes("TIMESTAMP") && toUpper === "DATE")
1035
+ return true;
1036
+ const fromLength = extractLength(fromUpper);
1037
+ const toLength = extractLength(toUpper);
1038
+ if (fromLength && toLength && toLength < fromLength)
1039
+ return true;
1040
+ return false;
1041
+ }
1042
+ function isTableRewriteType(from, to) {
1043
+ const fromUpper = from.toUpperCase();
1044
+ const toUpper = to.toUpperCase();
1045
+ if (fromUpper.startsWith("VARCHAR") && toUpper === "TEXT")
1046
+ return false;
1047
+ if (fromUpper === "TEXT" && toUpper.startsWith("VARCHAR"))
1048
+ return true;
1049
+ if (isNumericType(fromUpper) !== isNumericType(toUpper))
1050
+ return true;
1051
+ return false;
1052
+ }
1053
+ function isTextType(type) {
1054
+ return type === "TEXT" || type.startsWith("VARCHAR") || type.startsWith("CHAR");
1055
+ }
1056
+ function isNumericType(type) {
1057
+ return ["INTEGER", "BIGINT", "SMALLINT", "NUMERIC", "REAL", "DOUBLE PRECISION", "FLOAT"].includes(
1058
+ type
1059
+ );
1060
+ }
1061
+ function extractLength(type) {
1062
+ const match = type.match(/\((\d+)\)/);
1063
+ return match ? parseInt(match[1], 10) : null;
1064
+ }
1065
+ function isCreateIndexConcurrent(_op) {
1066
+ return false;
1067
+ }
1068
+
1069
+ // libs/migrations/src/lib/generator/sql-generator.ts
1070
+ function operationToSql(op, dialect) {
1071
+ switch (op.type) {
1072
+ case "createTable":
1073
+ return generateCreateTable(op, dialect);
1074
+ case "dropTable":
1075
+ return `DROP TABLE IF EXISTS "${op.table}"`;
1076
+ case "renameTable":
1077
+ return `ALTER TABLE "${op.from}" RENAME TO "${op.to}"`;
1078
+ case "addColumn":
1079
+ return generateAddColumn(op, dialect);
1080
+ case "dropColumn":
1081
+ return generateDropColumn(op, dialect);
1082
+ case "alterColumnType":
1083
+ return generateAlterColumnType(op, dialect);
1084
+ case "alterColumnNullable":
1085
+ return generateAlterColumnNullable(op, dialect);
1086
+ case "alterColumnDefault":
1087
+ return generateAlterColumnDefault(op, dialect);
1088
+ case "renameColumn":
1089
+ return `ALTER TABLE "${op.table}" RENAME COLUMN "${op.from}" TO "${op.to}"`;
1090
+ case "addForeignKey":
1091
+ return generateAddForeignKey(op, dialect);
1092
+ case "dropForeignKey":
1093
+ return generateDropForeignKey(op, dialect);
1094
+ case "createIndex":
1095
+ return generateCreateIndex(op);
1096
+ case "dropIndex":
1097
+ return `DROP INDEX IF EXISTS "${op.indexName}"`;
1098
+ case "rawSql":
1099
+ return op.upSql;
1100
+ }
1101
+ }
1102
+ function operationToReverseSql(op, dialect) {
1103
+ switch (op.type) {
1104
+ case "createTable":
1105
+ return `DROP TABLE IF EXISTS "${op.table}"`;
1106
+ case "dropTable":
1107
+ return null;
1108
+ case "renameTable":
1109
+ return `ALTER TABLE "${op.to}" RENAME TO "${op.from}"`;
1110
+ case "addColumn":
1111
+ return `ALTER TABLE "${op.table}" DROP COLUMN "${op.column}"`;
1112
+ case "dropColumn":
1113
+ if (op.previousType) {
1114
+ const nullable = op.previousNullable !== false ? "" : " NOT NULL";
1115
+ return `ALTER TABLE "${op.table}" ADD COLUMN "${op.column}" ${op.previousType}${nullable}`;
1116
+ }
1117
+ return null;
1118
+ case "alterColumnType":
1119
+ return generateAlterColumnType(
1120
+ { ...op, fromType: op.toType, toType: op.fromType },
1121
+ dialect
1122
+ );
1123
+ case "alterColumnNullable":
1124
+ return generateAlterColumnNullable(
1125
+ { ...op, nullable: !op.nullable },
1126
+ dialect
1127
+ );
1128
+ case "alterColumnDefault":
1129
+ return generateAlterColumnDefault(
1130
+ {
1131
+ ...op,
1132
+ defaultValue: op.previousDefault,
1133
+ previousDefault: op.defaultValue
1134
+ },
1135
+ dialect
1136
+ );
1137
+ case "renameColumn":
1138
+ return `ALTER TABLE "${op.table}" RENAME COLUMN "${op.to}" TO "${op.from}"`;
1139
+ case "addForeignKey":
1140
+ return generateDropForeignKey(
1141
+ { type: "dropForeignKey", table: op.table, constraintName: op.constraintName },
1142
+ dialect
1143
+ );
1144
+ case "dropForeignKey":
1145
+ return null;
1146
+ case "createIndex":
1147
+ return `DROP INDEX IF EXISTS "${op.indexName}"`;
1148
+ case "dropIndex":
1149
+ return null;
1150
+ case "rawSql":
1151
+ return op.downSql;
1152
+ }
1153
+ }
1154
+ function operationsToUpSql(operations, dialect) {
1155
+ return operations.map((op) => operationToSql(op, dialect));
1156
+ }
1157
+ function operationsToDownSql(operations, dialect) {
1158
+ return [...operations].reverse().map((op) => operationToReverseSql(op, dialect)).filter((sql) => sql !== null);
1159
+ }
1160
+ function generateCreateTable(op, _dialect) {
1161
+ const columnDefs = op.columns.map((c) => {
1162
+ let def = `"${c.name}" ${c.type}`;
1163
+ if (c.primaryKey)
1164
+ def += " PRIMARY KEY";
1165
+ if (!c.nullable)
1166
+ def += " NOT NULL";
1167
+ if (c.defaultValue)
1168
+ def += ` DEFAULT ${c.defaultValue}`;
1169
+ return def;
1170
+ });
1171
+ return `CREATE TABLE "${op.table}" (
1172
+ ${columnDefs.join(",\n ")}
1173
+ )`;
1174
+ }
1175
+ function generateAddColumn(op, _dialect) {
1176
+ let sql = `ALTER TABLE "${op.table}" ADD COLUMN "${op.column}" ${op.columnType}`;
1177
+ if (!op.nullable)
1178
+ sql += " NOT NULL";
1179
+ if (op.defaultValue)
1180
+ sql += ` DEFAULT ${op.defaultValue}`;
1181
+ return sql;
1182
+ }
1183
+ function generateDropColumn(op, _dialect) {
1184
+ return `ALTER TABLE "${op.table}" DROP COLUMN "${op.column}"`;
1185
+ }
1186
+ function generateAlterColumnType(op, dialect) {
1187
+ if (dialect === "sqlite") {
1188
+ return `-- SQLite: Cannot alter column type for "${op.table}"."${op.column}" (${op.fromType} \u2192 ${op.toType}). Requires table rebuild.`;
1189
+ }
1190
+ const using = op.castExpression ? ` USING ${op.castExpression}` : ` USING "${op.column}"::${op.toType}`;
1191
+ return `ALTER TABLE "${op.table}" ALTER COLUMN "${op.column}" TYPE ${op.toType}${using}`;
1192
+ }
1193
+ function generateAlterColumnNullable(op, dialect) {
1194
+ if (dialect === "sqlite") {
1195
+ return `-- SQLite: Cannot alter nullable for "${op.table}"."${op.column}". Requires table rebuild.`;
1196
+ }
1197
+ if (op.nullable) {
1198
+ return `ALTER TABLE "${op.table}" ALTER COLUMN "${op.column}" DROP NOT NULL`;
1199
+ }
1200
+ return `ALTER TABLE "${op.table}" ALTER COLUMN "${op.column}" SET NOT NULL`;
1201
+ }
1202
+ function generateAlterColumnDefault(op, dialect) {
1203
+ if (dialect === "sqlite") {
1204
+ return `-- SQLite: Cannot alter default for "${op.table}"."${op.column}". Requires table rebuild.`;
1205
+ }
1206
+ if (op.defaultValue === null) {
1207
+ return `ALTER TABLE "${op.table}" ALTER COLUMN "${op.column}" DROP DEFAULT`;
1208
+ }
1209
+ return `ALTER TABLE "${op.table}" ALTER COLUMN "${op.column}" SET DEFAULT ${op.defaultValue}`;
1210
+ }
1211
+ function generateAddForeignKey(op, dialect) {
1212
+ if (dialect === "sqlite") {
1213
+ return `-- SQLite: Cannot add FK "${op.constraintName}" after table creation. Requires table rebuild.`;
1214
+ }
1215
+ return `ALTER TABLE "${op.table}" ADD CONSTRAINT "${op.constraintName}" FOREIGN KEY ("${op.column}") REFERENCES "${op.referencedTable}"("${op.referencedColumn}") ON DELETE ${op.onDelete}`;
1216
+ }
1217
+ function generateDropForeignKey(op, dialect) {
1218
+ if (dialect === "sqlite") {
1219
+ return `-- SQLite: Cannot drop FK "${op.constraintName}" after table creation. Requires table rebuild.`;
1220
+ }
1221
+ return `ALTER TABLE "${op.table}" DROP CONSTRAINT "${op.constraintName}"`;
1222
+ }
1223
+ function generateCreateIndex(op) {
1224
+ const unique = op.unique ? "UNIQUE " : "";
1225
+ const cols = op.columns.map((c) => `"${c}"`).join(", ");
1226
+ return `CREATE ${unique}INDEX IF NOT EXISTS "${op.indexName}" ON "${op.table}" (${cols})`;
1227
+ }
1228
+
1229
+ // libs/migrations/src/lib/generator/migration-file-generator.ts
1230
+ function generateMigrationName(name, timestamp) {
1231
+ const d = timestamp ?? /* @__PURE__ */ new Date();
1232
+ const pad = (n) => String(n).padStart(2, "0");
1233
+ const prefix = `${d.getFullYear()}${pad(d.getMonth() + 1)}${pad(d.getDate())}${pad(d.getHours())}${pad(d.getMinutes())}${pad(d.getSeconds())}`;
1234
+ return `${prefix}_${name}`;
1235
+ }
1236
+ function generateMigrationFileContent(diff, options) {
1237
+ const { name, description, dialect } = options;
1238
+ const desc = description ?? (diff.summary.join("; ") || "Auto-generated migration");
1239
+ const upStatements = operationsToUpSql(diff.operations, dialect);
1240
+ const downStatements = operationsToDownSql(diff.operations, dialect);
1241
+ const operationsMeta = serializeOperationsMeta(diff.operations);
1242
+ const lines = [];
1243
+ lines.push("import type { MigrationFile, MigrationContext } from '@momentumcms/migrations';");
1244
+ lines.push("");
1245
+ lines.push("export const meta: MigrationFile['meta'] = {");
1246
+ lines.push(` name: ${JSON.stringify(name)},`);
1247
+ lines.push(` description: ${JSON.stringify(desc)},`);
1248
+ lines.push(` operations: ${operationsMeta},`);
1249
+ lines.push("};");
1250
+ lines.push("");
1251
+ lines.push("export async function up(ctx: MigrationContext): Promise<void> {");
1252
+ for (const sql of upStatements) {
1253
+ if (sql.startsWith("--")) {
1254
+ lines.push(` // ${sql.slice(3)}`);
1255
+ } else {
1256
+ lines.push(` await ctx.sql(${JSON.stringify(sql)});`);
1257
+ }
1258
+ }
1259
+ if (upStatements.length === 0) {
1260
+ lines.push(" // No operations");
1261
+ }
1262
+ lines.push("}");
1263
+ lines.push("");
1264
+ lines.push("export async function down(ctx: MigrationContext): Promise<void> {");
1265
+ for (const sql of downStatements) {
1266
+ if (sql.startsWith("--")) {
1267
+ lines.push(` // ${sql.slice(3)}`);
1268
+ } else {
1269
+ lines.push(` await ctx.sql(${JSON.stringify(sql)});`);
1270
+ }
1271
+ }
1272
+ if (downStatements.length === 0) {
1273
+ lines.push(" // Cannot reverse all operations");
1274
+ }
1275
+ lines.push("}");
1276
+ lines.push("");
1277
+ return lines.join("\n");
1278
+ }
1279
+ function serializeOperationsMeta(operations) {
1280
+ const simplified = operations.map((op) => {
1281
+ switch (op.type) {
1282
+ case "createTable":
1283
+ return { type: op.type, table: op.table };
1284
+ case "dropTable":
1285
+ return { type: op.type, table: op.table };
1286
+ case "renameTable":
1287
+ return { type: op.type, from: op.from, to: op.to };
1288
+ case "addColumn":
1289
+ return {
1290
+ type: op.type,
1291
+ table: op.table,
1292
+ column: op.column,
1293
+ nullable: op.nullable,
1294
+ defaultValue: op.defaultValue ?? null
1295
+ };
1296
+ case "dropColumn":
1297
+ return { type: op.type, table: op.table, column: op.column };
1298
+ case "alterColumnType":
1299
+ return {
1300
+ type: op.type,
1301
+ table: op.table,
1302
+ column: op.column,
1303
+ fromType: op.fromType,
1304
+ toType: op.toType
1305
+ };
1306
+ case "alterColumnNullable":
1307
+ return { type: op.type, table: op.table, column: op.column, nullable: op.nullable };
1308
+ case "alterColumnDefault":
1309
+ return { type: op.type, table: op.table, column: op.column };
1310
+ case "renameColumn":
1311
+ return { type: op.type, table: op.table, from: op.from, to: op.to };
1312
+ case "addForeignKey":
1313
+ return { type: op.type, table: op.table, constraintName: op.constraintName };
1314
+ case "dropForeignKey":
1315
+ return { type: op.type, table: op.table, constraintName: op.constraintName };
1316
+ case "createIndex":
1317
+ return { type: op.type, table: op.table, indexName: op.indexName };
1318
+ case "dropIndex":
1319
+ return { type: op.type, table: op.table, indexName: op.indexName };
1320
+ case "rawSql":
1321
+ return { type: op.type, description: op.description };
1322
+ }
1323
+ });
1324
+ return JSON.stringify(simplified, null, " ");
1325
+ }
1326
+
1327
+ // libs/migrations/src/lib/loader/snapshot-manager.ts
1328
+ import { readFileSync, writeFileSync, existsSync, mkdirSync } from "node:fs";
1329
+ import { join } from "node:path";
1330
+ var SNAPSHOT_FILENAME = ".snapshot.json";
1331
+ function readSnapshot(directory) {
1332
+ const filePath = join(directory, SNAPSHOT_FILENAME);
1333
+ if (!existsSync(filePath))
1334
+ return null;
1335
+ const json2 = readFileSync(filePath, "utf-8");
1336
+ return deserializeSnapshot(json2);
1337
+ }
1338
+ function writeSnapshot(directory, snapshot) {
1339
+ mkdirSync(directory, { recursive: true });
1340
+ const filePath = join(directory, SNAPSHOT_FILENAME);
1341
+ writeFileSync(filePath, serializeSnapshot(snapshot), "utf-8");
1342
+ }
1343
+
1344
+ // libs/migrations/src/cli/shared.ts
1345
+ import { pathToFileURL } from "node:url";
1346
+
1347
+ // libs/migrations/src/lib/schema/introspect-postgres.ts
1348
+ async function introspectPostgres(queryFn, schema = "public") {
1349
+ const [columnRows, fkRows, indexRows, pkRows] = await Promise.all([
1350
+ queryFn(
1351
+ `SELECT table_name, column_name, data_type, character_maximum_length, is_nullable, column_default
1352
+ FROM information_schema.columns
1353
+ WHERE table_schema = $1
1354
+ ORDER BY table_name, ordinal_position`,
1355
+ [schema]
1356
+ ),
1357
+ queryFn(
1358
+ `SELECT
1359
+ tc.table_name,
1360
+ tc.constraint_name,
1361
+ kcu.column_name,
1362
+ ccu.table_name AS foreign_table_name,
1363
+ ccu.column_name AS foreign_column_name,
1364
+ rc.delete_rule
1365
+ FROM information_schema.table_constraints tc
1366
+ JOIN information_schema.key_column_usage kcu
1367
+ ON tc.constraint_name = kcu.constraint_name
1368
+ AND tc.table_schema = kcu.table_schema
1369
+ JOIN information_schema.constraint_column_usage ccu
1370
+ ON ccu.constraint_name = tc.constraint_name
1371
+ AND ccu.table_schema = tc.table_schema
1372
+ JOIN information_schema.referential_constraints rc
1373
+ ON rc.constraint_name = tc.constraint_name
1374
+ AND rc.constraint_schema = tc.constraint_schema
1375
+ WHERE tc.constraint_type = 'FOREIGN KEY'
1376
+ AND tc.table_schema = $1
1377
+ ORDER BY tc.table_name, tc.constraint_name`,
1378
+ [schema]
1379
+ ),
1380
+ queryFn(
1381
+ `SELECT tablename, indexname, indexdef
1382
+ FROM pg_indexes
1383
+ WHERE schemaname = $1
1384
+ ORDER BY tablename, indexname`,
1385
+ [schema]
1386
+ ),
1387
+ queryFn(
1388
+ `SELECT tc.table_name, kcu.column_name
1389
+ FROM information_schema.table_constraints tc
1390
+ JOIN information_schema.key_column_usage kcu
1391
+ ON tc.constraint_name = kcu.constraint_name
1392
+ AND tc.table_schema = kcu.table_schema
1393
+ WHERE tc.constraint_type = 'PRIMARY KEY'
1394
+ AND tc.table_schema = $1`,
1395
+ [schema]
1396
+ )
1397
+ ]);
1398
+ const pkLookup = /* @__PURE__ */ new Map();
1399
+ for (const row2 of pkRows) {
1400
+ const tableName = row2.table_name;
1401
+ if (!pkLookup.has(tableName)) {
1402
+ pkLookup.set(tableName, /* @__PURE__ */ new Set());
1403
+ }
1404
+ pkLookup.get(tableName).add(row2.column_name);
1405
+ }
1406
+ const tableColumnsMap = /* @__PURE__ */ new Map();
1407
+ for (const row2 of columnRows) {
1408
+ const tableName = row2.table_name;
1409
+ if (INTERNAL_TABLES.has(tableName))
1410
+ continue;
1411
+ if (!tableColumnsMap.has(tableName)) {
1412
+ tableColumnsMap.set(tableName, []);
1413
+ }
1414
+ const rawType = buildPgColumnType(row2);
1415
+ const pkSet = pkLookup.get(tableName);
1416
+ tableColumnsMap.get(tableName).push({
1417
+ name: row2.column_name,
1418
+ type: normalizeColumnType(rawType, "postgresql"),
1419
+ nullable: row2.is_nullable === "YES",
1420
+ defaultValue: row2.column_default,
1421
+ isPrimaryKey: pkSet?.has(row2.column_name) ?? false
1422
+ });
1423
+ }
1424
+ const tableFkMap = /* @__PURE__ */ new Map();
1425
+ for (const row2 of fkRows) {
1426
+ const tableName = row2.table_name;
1427
+ if (INTERNAL_TABLES.has(tableName))
1428
+ continue;
1429
+ if (!tableFkMap.has(tableName)) {
1430
+ tableFkMap.set(tableName, []);
1431
+ }
1432
+ tableFkMap.get(tableName).push({
1433
+ constraintName: row2.constraint_name,
1434
+ column: row2.column_name,
1435
+ referencedTable: row2.foreign_table_name,
1436
+ referencedColumn: row2.foreign_column_name,
1437
+ onDelete: row2.delete_rule
1438
+ });
1439
+ }
1440
+ const fkConstraintNames = new Set(fkRows.map((r) => r.constraint_name));
1441
+ const tableIndexMap = /* @__PURE__ */ new Map();
1442
+ for (const row2 of indexRows) {
1443
+ const tableName = row2.tablename;
1444
+ if (INTERNAL_TABLES.has(tableName))
1445
+ continue;
1446
+ if (row2.indexdef.includes("PRIMARY KEY"))
1447
+ continue;
1448
+ if (fkConstraintNames.has(row2.indexname))
1449
+ continue;
1450
+ if (row2.indexname.endsWith("_pkey"))
1451
+ continue;
1452
+ if (!tableIndexMap.has(tableName)) {
1453
+ tableIndexMap.set(tableName, []);
1454
+ }
1455
+ const columns = extractIndexColumns(row2.indexdef);
1456
+ const unique = row2.indexdef.toUpperCase().includes("UNIQUE");
1457
+ tableIndexMap.get(tableName).push({
1458
+ name: row2.indexname,
1459
+ columns,
1460
+ unique
1461
+ });
1462
+ }
1463
+ const tables = [];
1464
+ for (const [tableName, columns] of tableColumnsMap) {
1465
+ tables.push({
1466
+ name: tableName,
1467
+ columns,
1468
+ foreignKeys: tableFkMap.get(tableName) ?? [],
1469
+ indexes: tableIndexMap.get(tableName) ?? []
1470
+ });
1471
+ }
1472
+ return createSchemaSnapshot("postgresql", tables);
1473
+ }
1474
+ function buildPgColumnType(row2) {
1475
+ const dataType = row2.data_type.toUpperCase();
1476
+ if (dataType === "CHARACTER VARYING") {
1477
+ const len = row2.character_maximum_length ?? 255;
1478
+ return `VARCHAR(${len})`;
1479
+ }
1480
+ if (dataType === "CHARACTER") {
1481
+ const len = row2.character_maximum_length ?? 1;
1482
+ return `CHAR(${len})`;
1483
+ }
1484
+ return dataType;
1485
+ }
1486
+ function extractIndexColumns(indexDef) {
1487
+ const match = indexDef.match(/\(([^)]+)\)\s*$/);
1488
+ if (!match)
1489
+ return [];
1490
+ return match[1].split(",").map((col) => col.trim().replace(/^"/, "").replace(/"$/, "")).filter((col) => col.length > 0);
1491
+ }
1492
+
1493
+ // libs/migrations/src/lib/schema/introspect-sqlite.ts
1494
+ async function introspectSqlite(queryFn) {
1495
+ const masterRows = await queryFn(
1496
+ `SELECT name, type, sql FROM sqlite_master WHERE type = 'table' AND name NOT LIKE 'sqlite_%' ORDER BY name`
1497
+ );
1498
+ const tables = [];
1499
+ for (const masterRow of masterRows) {
1500
+ const tableName = masterRow.name;
1501
+ if (INTERNAL_TABLES.has(tableName))
1502
+ continue;
1503
+ const columnRows = await queryFn(
1504
+ `PRAGMA table_info("${tableName}")`
1505
+ );
1506
+ const columns = columnRows.map((row2) => ({
1507
+ name: row2.name,
1508
+ type: normalizeColumnType(row2.type || "TEXT", "sqlite"),
1509
+ nullable: row2.notnull === 0,
1510
+ defaultValue: row2.dflt_value,
1511
+ isPrimaryKey: row2.pk > 0
1512
+ }));
1513
+ const fkRows = await queryFn(
1514
+ `PRAGMA foreign_key_list("${tableName}")`
1515
+ );
1516
+ const foreignKeys = fkRows.map((row2) => ({
1517
+ constraintName: `fk_${tableName}_${row2.from}`,
1518
+ column: row2.from,
1519
+ referencedTable: row2.table,
1520
+ referencedColumn: row2.to,
1521
+ onDelete: row2.on_delete
1522
+ }));
1523
+ const indexListRows = await queryFn(
1524
+ `PRAGMA index_list("${tableName}")`
1525
+ );
1526
+ const indexes = [];
1527
+ for (const idxRow of indexListRows) {
1528
+ if (idxRow.origin === "pk")
1529
+ continue;
1530
+ const indexInfoRows = await queryFn(
1531
+ `PRAGMA index_info("${idxRow.name}")`
1532
+ );
1533
+ const indexColumns = indexInfoRows.sort((a, b) => a.seqno - b.seqno).map((r) => r.name).filter((n) => n.length > 0);
1534
+ if (indexColumns.length > 0) {
1535
+ indexes.push({
1536
+ name: idxRow.name,
1537
+ columns: indexColumns,
1538
+ unique: idxRow.unique === 1
1539
+ });
1540
+ }
1541
+ }
1542
+ tables.push({
1543
+ name: tableName,
1544
+ columns,
1545
+ foreignKeys,
1546
+ indexes
1547
+ });
1548
+ }
1549
+ return createSchemaSnapshot("sqlite", tables);
1550
+ }
1551
+
1552
+ // libs/migrations/src/cli/shared.ts
1553
+ function isResolvedConfig(value) {
1554
+ return typeof value === "object" && value !== null && "collections" in value && "db" in value;
1555
+ }
1556
+ async function loadMomentumConfig(configPath) {
1557
+ const configUrl = pathToFileURL(configPath).href;
1558
+ const mod = await import(configUrl);
1559
+ const raw = mod["default"] ?? mod;
1560
+ if (!isResolvedConfig(raw)) {
1561
+ throw new Error(`Config at ${configPath} is not a valid ResolvedMomentumConfig`);
1562
+ }
1563
+ if (!raw.db?.adapter) {
1564
+ throw new Error(`Config at ${configPath} is missing db.adapter`);
1565
+ }
1566
+ if (!raw.collections || raw.collections.length === 0) {
1567
+ throw new Error(`Config at ${configPath} has no collections`);
1568
+ }
1569
+ return raw;
1570
+ }
1571
+ function resolveDialect(adapter) {
1572
+ if (!adapter.dialect) {
1573
+ throw new Error(
1574
+ "DatabaseAdapter.dialect is not set. Ensure your adapter factory (postgresAdapter/sqliteAdapter) sets the dialect property."
1575
+ );
1576
+ }
1577
+ return adapter.dialect;
1578
+ }
1579
+ function buildIntrospector(adapter, dialect) {
1580
+ if (!adapter.queryRaw) {
1581
+ throw new Error("DatabaseAdapter must implement queryRaw for introspection");
1582
+ }
1583
+ const queryRaw = adapter.queryRaw.bind(adapter);
1584
+ if (dialect === "postgresql") {
1585
+ const queryFn2 = async (sql, params) => queryRaw(sql, params);
1586
+ return () => introspectPostgres(queryFn2);
1587
+ }
1588
+ const queryFn = async (sql, params) => queryRaw(sql, params);
1589
+ return () => introspectSqlite(queryFn);
1590
+ }
1591
+ function parseMigrationArgs(args) {
1592
+ const configPath = args.find((a) => !a.startsWith("--"));
1593
+ if (!configPath) {
1594
+ throw new Error("Usage: npx tsx <command>.ts <configPath> [options]");
1595
+ }
1596
+ let name;
1597
+ const nameIdx = args.indexOf("--name");
1598
+ if (nameIdx !== -1 && args[nameIdx + 1]) {
1599
+ name = args[nameIdx + 1];
1600
+ }
1601
+ return {
1602
+ configPath,
1603
+ name,
1604
+ dryRun: args.includes("--dry-run"),
1605
+ testOnly: args.includes("--test-only"),
1606
+ skipCloneTest: args.includes("--skip-clone-test")
1607
+ };
1608
+ }
1609
+
1610
+ // libs/migrations/src/cli/generate.ts
1611
+ async function main() {
1612
+ const args = parseMigrationArgs(process.argv.slice(2));
1613
+ const config = await loadMomentumConfig(resolve(args.configPath));
1614
+ const adapter = config.db.adapter;
1615
+ const dialect = resolveDialect(adapter);
1616
+ const migrationConfig = resolveMigrationConfig(config.migrations ?? {});
1617
+ if (!migrationConfig) {
1618
+ console.warn("No migration config found. Add migrations to your momentum.config.ts.");
1619
+ process.exit(1);
1620
+ }
1621
+ const directory = resolve(migrationConfig.directory);
1622
+ const desired = collectionsToSchema(config.collections, dialect);
1623
+ let previous = readSnapshot(directory);
1624
+ if (!previous) {
1625
+ try {
1626
+ const introspect = buildIntrospector(adapter, dialect);
1627
+ previous = await introspect();
1628
+ } catch {
1629
+ previous = createSchemaSnapshot(dialect, []);
1630
+ }
1631
+ }
1632
+ const diff = diffSchemas(desired, previous, dialect);
1633
+ if (diff.operations.length === 0) {
1634
+ console.warn("Schema up to date. No migration needed.");
1635
+ if (!args.dryRun) {
1636
+ writeSnapshot(directory, desired);
1637
+ }
1638
+ return;
1639
+ }
1640
+ if (migrationConfig.dangerDetection) {
1641
+ const dangers = detectDangers(diff.operations, dialect);
1642
+ if (dangers.warnings.length > 0) {
1643
+ console.warn("\n--- Danger Detection ---");
1644
+ for (const w of dangers.warnings) {
1645
+ console.warn(` [${w.severity}] ${w.message}`);
1646
+ if (w.suggestion)
1647
+ console.warn(` Suggestion: ${w.suggestion}`);
1648
+ }
1649
+ if (dangers.hasErrors) {
1650
+ console.error("\nBlocked: migration contains error-severity dangers.");
1651
+ console.error(
1652
+ "Review the operations and adjust your collections, or disable danger detection."
1653
+ );
1654
+ process.exit(1);
1655
+ }
1656
+ console.warn("");
1657
+ }
1658
+ }
1659
+ const migrationName = args.name ?? "migration";
1660
+ const timestampedName = generateMigrationName(migrationName);
1661
+ const fileContent = generateMigrationFileContent(diff, {
1662
+ name: timestampedName,
1663
+ dialect
1664
+ });
1665
+ if (args.dryRun) {
1666
+ console.warn("\n--- Dry Run (migration file content) ---\n");
1667
+ console.warn(fileContent);
1668
+ console.warn(`
1669
+ Would write: ${join2(directory, `${timestampedName}.ts`)}`);
1670
+ console.warn(`Operations: ${diff.operations.length}`);
1671
+ console.warn(`Summary: ${diff.summary.join("; ")}`);
1672
+ return;
1673
+ }
1674
+ mkdirSync2(directory, { recursive: true });
1675
+ const filePath = join2(directory, `${timestampedName}.ts`);
1676
+ writeFileSync2(filePath, fileContent, "utf-8");
1677
+ writeSnapshot(directory, desired);
1678
+ console.warn(`
1679
+ Generated migration: ${filePath}`);
1680
+ console.warn(`Operations: ${diff.operations.length}`);
1681
+ console.warn(`Summary: ${diff.summary.join("; ")}`);
1682
+ }
1683
+ main().catch((err) => {
1684
+ console.error("Migration generate failed:", err);
1685
+ process.exit(1);
1686
+ });