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