@metaobjectsdev/cli 1.0.5 → 1.0.7
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +1 -0
- package/dist/src/commands/migrate.d.ts +1 -1
- package/dist/src/commands/migrate.d.ts.map +1 -1
- package/dist/src/commands/migrate.js +72 -17
- package/dist/src/commands/migrate.js.map +1 -1
- package/dist/src/commands/verify.js +1 -1
- package/dist/src/commands/verify.js.map +1 -1
- package/dist/src/index.d.ts.map +1 -1
- package/dist/src/index.js +2 -0
- package/dist/src/index.js.map +1 -1
- package/dist/src/lib/args.d.ts +11 -0
- package/dist/src/lib/args.d.ts.map +1 -1
- package/dist/src/lib/args.js +35 -0
- package/dist/src/lib/args.js.map +1 -1
- package/dist/src/lib/config.d.ts +3 -0
- package/dist/src/lib/config.d.ts.map +1 -1
- package/dist/src/lib/config.js +1 -0
- package/dist/src/lib/config.js.map +1 -1
- package/dist/src/lib/output.d.ts +7 -0
- package/dist/src/lib/output.d.ts.map +1 -1
- package/dist/src/lib/output.js +6 -1
- package/dist/src/lib/output.js.map +1 -1
- package/package.json +10 -10
- package/src/commands/migrate.ts +76 -18
- package/src/commands/verify.ts +1 -1
- package/src/index.ts +2 -0
- package/src/lib/args.ts +43 -0
- package/src/lib/config.ts +4 -0
- package/src/lib/output.ts +13 -1
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@metaobjectsdev/cli",
|
|
3
|
-
"version": "1.0.
|
|
3
|
+
"version": "1.0.7",
|
|
4
4
|
"description": "CLI for MetaObjects: scaffold, codegen, migrate, and drift-detection commands.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "./dist/src/index.js",
|
|
@@ -51,15 +51,15 @@
|
|
|
51
51
|
"@biomejs/js-api": "^0.7.0",
|
|
52
52
|
"@biomejs/wasm-nodejs": "^1.9.4",
|
|
53
53
|
"@libsql/kysely-libsql": "^0.4.0",
|
|
54
|
-
"@metaobjectsdev/codegen-ts": "1.0.
|
|
55
|
-
"@metaobjectsdev/codegen-ts-react": "1.0.
|
|
56
|
-
"@metaobjectsdev/codegen-ts-tanstack": "1.0.
|
|
57
|
-
"@metaobjectsdev/docs-site": "1.0.
|
|
58
|
-
"@metaobjectsdev/metadata": "1.0.
|
|
59
|
-
"@metaobjectsdev/migrate-ts": "1.0.
|
|
60
|
-
"@metaobjectsdev/render": "1.0.
|
|
61
|
-
"@metaobjectsdev/runtime-ts": "1.0.
|
|
62
|
-
"@metaobjectsdev/sdk": "1.0.
|
|
54
|
+
"@metaobjectsdev/codegen-ts": "1.0.7",
|
|
55
|
+
"@metaobjectsdev/codegen-ts-react": "1.0.7",
|
|
56
|
+
"@metaobjectsdev/codegen-ts-tanstack": "1.0.7",
|
|
57
|
+
"@metaobjectsdev/docs-site": "1.0.7",
|
|
58
|
+
"@metaobjectsdev/metadata": "1.0.7",
|
|
59
|
+
"@metaobjectsdev/migrate-ts": "1.0.7",
|
|
60
|
+
"@metaobjectsdev/render": "1.0.7",
|
|
61
|
+
"@metaobjectsdev/runtime-ts": "1.0.7",
|
|
62
|
+
"@metaobjectsdev/sdk": "1.0.7",
|
|
63
63
|
"@toon-format/toon": "^2.3.0",
|
|
64
64
|
"jiti": "^2.4.0"
|
|
65
65
|
},
|
package/src/commands/migrate.ts
CHANGED
|
@@ -17,6 +17,9 @@ import { collectionLoadOptions } from "../lib/collection-load-options.js";
|
|
|
17
17
|
import { exclusionNotes, importedOption, migrateScopeMismatch } from "../lib/migrate-scope.js";
|
|
18
18
|
import {
|
|
19
19
|
allowOptionFor,
|
|
20
|
+
isBlockedChangesError,
|
|
21
|
+
isPrimaryKeyChangeError,
|
|
22
|
+
isDeclaredRenameError,
|
|
20
23
|
buildExpectedSchemaWithProvenance,
|
|
21
24
|
scopeExpectedSchema,
|
|
22
25
|
scopedDiffInputs,
|
|
@@ -32,8 +35,7 @@ import {
|
|
|
32
35
|
readSnapshot,
|
|
33
36
|
writeSnapshot,
|
|
34
37
|
qualifiedDbName,
|
|
35
|
-
BlockedChangesError,
|
|
36
|
-
PrimaryKeyChangeError,
|
|
38
|
+
type BlockedChangesError,
|
|
37
39
|
renderD1,
|
|
38
40
|
writeMigrationD1,
|
|
39
41
|
writeMigrationFlyway,
|
|
@@ -46,6 +48,7 @@ import {
|
|
|
46
48
|
type AmbiguousChange,
|
|
47
49
|
type AmbiguousResolution,
|
|
48
50
|
type Change,
|
|
51
|
+
type DataHazard,
|
|
49
52
|
type D1Binding,
|
|
50
53
|
type EmitResult,
|
|
51
54
|
type D1Runner,
|
|
@@ -112,6 +115,15 @@ MIGRATE FLAGS:
|
|
|
112
115
|
never existed.
|
|
113
116
|
--on-ambiguous abort|rename|drop-add
|
|
114
117
|
How to handle ambiguous renames (default: abort)
|
|
118
|
+
--rename-table [schema.]old=new
|
|
119
|
+
Declare a table rename (repeatable). Resolves the drop+create
|
|
120
|
+
as ALTER TABLE ... RENAME TO whatever the rename heuristic
|
|
121
|
+
makes of the names; refused if no such pair is pending.
|
|
122
|
+
--rename-column [schema.]table.old=new
|
|
123
|
+
Declare a column rename (repeatable); keeps the column's data.
|
|
124
|
+
'table' is the name in the metadata (the NEW name when the
|
|
125
|
+
table is renamed in the same run). The column's type,
|
|
126
|
+
nullability and default must not change in the same run.
|
|
115
127
|
--from-db Introspect live DB instead of using the committed snapshot
|
|
116
128
|
--apply Run pending migration files against the DB after writing
|
|
117
129
|
--rollback <target> Roll back applied migrations newer than <target>
|
|
@@ -269,6 +281,47 @@ function mapOnAmbiguous(v: "abort" | "rename" | "drop-add"): AmbiguousResolution
|
|
|
269
281
|
return v === "drop-add" ? "drop+add" : v;
|
|
270
282
|
}
|
|
271
283
|
|
|
284
|
+
/**
|
|
285
|
+
* The report-and-exit for an engine refusal whose message the engine already made
|
|
286
|
+
* actionable — the sibling of `refuseScopeMismatch` above, which is the same decision
|
|
287
|
+
* for scope. The engine's guards (`isPrimaryKeyChangeError` et al.) narrow at the catch
|
|
288
|
+
* site; only the hint varies by refusal, so each hint is written once.
|
|
289
|
+
*/
|
|
290
|
+
function refuseEngineError(err: Error, hint: string, fmt: OutputFormat): number {
|
|
291
|
+
log.error(`migrate: ${err.message}`);
|
|
292
|
+
emitStructuredError(`migrate: ${err.message}`, hint, fmt);
|
|
293
|
+
return 1;
|
|
294
|
+
}
|
|
295
|
+
|
|
296
|
+
const PK_CHANGE_HINT =
|
|
297
|
+
"align the primary key manually, or reconcile the metadata identity to match the live table";
|
|
298
|
+
const DECLARED_RENAME_HINT =
|
|
299
|
+
"fix or remove the --rename-table / --rename-column flag; a table's name is its name in the metadata (the new name when it is renamed too)";
|
|
300
|
+
|
|
301
|
+
/**
|
|
302
|
+
* Warn about changes that apply to an empty table and fail on a populated one. Warn, not
|
|
303
|
+
* refuse: the diff cannot see rows, and a new required column on an empty table is fine.
|
|
304
|
+
* The Postgres migration file carries the same warning, with the preparation step, above
|
|
305
|
+
* the statement.
|
|
306
|
+
*/
|
|
307
|
+
function warnDataHazards(hazards: readonly DataHazard[]): string[] {
|
|
308
|
+
const warnings = hazards.map((h) => {
|
|
309
|
+
const t = h.schema !== undefined ? `${h.schema}.${h.table}` : h.table;
|
|
310
|
+
switch (h.kind) {
|
|
311
|
+
case "add-required-column":
|
|
312
|
+
return `${t}.${h.column} is added NOT NULL with no default — fails if ${t} has rows. `
|
|
313
|
+
+ `Give the field a @default, or add it optional, backfill, then make it required.`;
|
|
314
|
+
case "set-not-null":
|
|
315
|
+
return `${t}.${h.column} becomes NOT NULL — fails if any row holds NULL. Backfill it first.`;
|
|
316
|
+
case "add-check":
|
|
317
|
+
return `CHECK ${h.check} is added to ${t} — fails if any existing row violates `
|
|
318
|
+
+ `(${h.expression}). Fix those rows first.`;
|
|
319
|
+
}
|
|
320
|
+
});
|
|
321
|
+
for (const w of warnings) log.warn(`migrate: ${w}`);
|
|
322
|
+
return warnings;
|
|
323
|
+
}
|
|
324
|
+
|
|
272
325
|
/**
|
|
273
326
|
* The command that actually creates tables in a fresh/empty database. This is the
|
|
274
327
|
* correct first step for a brand-new project — it introspects the (empty) DB, diffs
|
|
@@ -657,6 +710,7 @@ export async function migrateCommand(
|
|
|
657
710
|
let applyFailed = false;
|
|
658
711
|
let blocked: BlockedEntry[] = [];
|
|
659
712
|
let ambiguous: AmbiguousEntry[] = [];
|
|
713
|
+
let hazardWarnings: string[] = [];
|
|
660
714
|
let changeCounts: Record<string, number> = {};
|
|
661
715
|
|
|
662
716
|
try {
|
|
@@ -712,6 +766,7 @@ export async function migrateCommand(
|
|
|
712
766
|
actual,
|
|
713
767
|
dialect: kysely.dialect,
|
|
714
768
|
allow: tokensToAllowOptions(config.allow),
|
|
769
|
+
renames: config.renames,
|
|
715
770
|
// #258 — adopting a live DB whose PRIMARY KEY differs from the metadata identity
|
|
716
771
|
// has no expressible migration; refuse loudly instead of emitting SQL that drops
|
|
717
772
|
// the constraint and breaks referencing FKs at apply.
|
|
@@ -723,11 +778,14 @@ export async function migrateCommand(
|
|
|
723
778
|
});
|
|
724
779
|
} catch (err) {
|
|
725
780
|
// #258 — a primary-key move has no expressible migration; refuse loudly.
|
|
726
|
-
if (err
|
|
727
|
-
|
|
728
|
-
emitStructuredError(`migrate: ${err.message}`, "align the primary key manually, or reconcile the metadata identity to match the live table", fmt);
|
|
781
|
+
if (isPrimaryKeyChangeError(err)) {
|
|
782
|
+
const rc = refuseEngineError(err, PK_CHANGE_HINT, fmt);
|
|
729
783
|
await kysely.close();
|
|
730
|
-
return
|
|
784
|
+
return rc;
|
|
785
|
+
}
|
|
786
|
+
if (isDeclaredRenameError(err)) {
|
|
787
|
+
await kysely.close();
|
|
788
|
+
return refuseEngineError(err, DECLARED_RENAME_HINT, fmt);
|
|
731
789
|
}
|
|
732
790
|
// diff() throws when onAmbiguous returns "abort" — surface as exit 1
|
|
733
791
|
// with the collected ambiguity list.
|
|
@@ -756,6 +814,7 @@ export async function migrateCommand(
|
|
|
756
814
|
}
|
|
757
815
|
|
|
758
816
|
changeCounts = summarizeChanges(diffResult.changes);
|
|
817
|
+
hazardWarnings = warnDataHazards(diffResult.hazards);
|
|
759
818
|
|
|
760
819
|
// #313 — refuse to AUTHOR a drop for an object the committed snapshot never
|
|
761
820
|
// contained. This path diffs metadata against introspection and never reads the
|
|
@@ -813,7 +872,7 @@ export async function migrateCommand(
|
|
|
813
872
|
...(actual.meta !== undefined ? { actualMeta: actual.meta } : {}),
|
|
814
873
|
});
|
|
815
874
|
} catch (err) {
|
|
816
|
-
if (err
|
|
875
|
+
if (isBlockedChangesError(err)) {
|
|
817
876
|
blocked = blockedToEntries(err);
|
|
818
877
|
exitCode = 1;
|
|
819
878
|
} else {
|
|
@@ -944,6 +1003,7 @@ export async function migrateCommand(
|
|
|
944
1003
|
format: config.format,
|
|
945
1004
|
applied: appliedNames,
|
|
946
1005
|
applyFailed,
|
|
1006
|
+
warnings: hazardWarnings,
|
|
947
1007
|
};
|
|
948
1008
|
const output =
|
|
949
1009
|
fmt === "toon" ? formatMigrateResultToon(migrateResult)
|
|
@@ -1267,6 +1327,7 @@ export async function runOfflineGenerate(
|
|
|
1267
1327
|
// the default `meta migrate` — performing no import exclusion at all.
|
|
1268
1328
|
...importedOption(collection),
|
|
1269
1329
|
allow: tokensToAllowOptions(config.allow),
|
|
1330
|
+
renames: config.renames,
|
|
1270
1331
|
onAmbiguous: async (a) => {
|
|
1271
1332
|
collectedAmbiguous.push(a);
|
|
1272
1333
|
return onAmbiguousResolution;
|
|
@@ -1274,11 +1335,8 @@ export async function runOfflineGenerate(
|
|
|
1274
1335
|
});
|
|
1275
1336
|
} catch (err) {
|
|
1276
1337
|
// #258 — a primary-key move has no expressible migration; refuse loudly.
|
|
1277
|
-
if (err
|
|
1278
|
-
|
|
1279
|
-
emitStructuredError(`migrate: ${err.message}`, "align the primary key manually, or reconcile the metadata identity to match the live table", fmt);
|
|
1280
|
-
return 1;
|
|
1281
|
-
}
|
|
1338
|
+
if (isPrimaryKeyChangeError(err)) return refuseEngineError(err, PK_CHANGE_HINT, fmt);
|
|
1339
|
+
if (isDeclaredRenameError(err)) return refuseEngineError(err, DECLARED_RENAME_HINT, fmt);
|
|
1282
1340
|
if ((err as Error).message.includes("aborted by onAmbiguous")) {
|
|
1283
1341
|
log.error(`migrate: ambiguous rename/drop detected; re-run with --on-ambiguous rename|drop-add`);
|
|
1284
1342
|
return 1;
|
|
@@ -1304,6 +1362,7 @@ export async function runOfflineGenerate(
|
|
|
1304
1362
|
log.error(`migrate: --slug <name> required when there are changes (e.g., --slug add-user-shipping)`);
|
|
1305
1363
|
return 2;
|
|
1306
1364
|
}
|
|
1365
|
+
warnDataHazards(diffResult.hazards);
|
|
1307
1366
|
|
|
1308
1367
|
const emitResult = emit(diffResult.changes, {
|
|
1309
1368
|
dialect: config.dialect,
|
|
@@ -1523,6 +1582,7 @@ async function runD1Migrate(
|
|
|
1523
1582
|
// @constraintName models churning and enum @values changes silent on D1.
|
|
1524
1583
|
dialect: "d1",
|
|
1525
1584
|
allow: tokensToAllowOptions(config.allow),
|
|
1585
|
+
renames: config.renames,
|
|
1526
1586
|
// #258 — adopting a live D1 DB whose PRIMARY KEY differs from the metadata identity
|
|
1527
1587
|
// has no expressible migration; refuse loudly instead of emitting SQL that drops
|
|
1528
1588
|
// the constraint and breaks referencing FKs at apply (same failure as the online path).
|
|
@@ -1534,11 +1594,8 @@ async function runD1Migrate(
|
|
|
1534
1594
|
});
|
|
1535
1595
|
} catch (err) {
|
|
1536
1596
|
// #258 — a primary-key move has no expressible migration; refuse loudly.
|
|
1537
|
-
if (err
|
|
1538
|
-
|
|
1539
|
-
emitStructuredError(`migrate: ${err.message}`, "align the primary key manually, or reconcile the metadata identity to match the live table", fmt);
|
|
1540
|
-
return 1;
|
|
1541
|
-
}
|
|
1597
|
+
if (isPrimaryKeyChangeError(err)) return refuseEngineError(err, PK_CHANGE_HINT, fmt);
|
|
1598
|
+
if (isDeclaredRenameError(err)) return refuseEngineError(err, DECLARED_RENAME_HINT, fmt);
|
|
1542
1599
|
if ((err as Error).message.includes("aborted by onAmbiguous")) {
|
|
1543
1600
|
const entries = ambiguousToEntries(collectedAmbiguous);
|
|
1544
1601
|
for (const e of entries) {
|
|
@@ -1551,6 +1608,7 @@ async function runD1Migrate(
|
|
|
1551
1608
|
}
|
|
1552
1609
|
|
|
1553
1610
|
const changeCounts = summarizeChanges(diffResult.changes);
|
|
1611
|
+
warnDataHazards(diffResult.hazards);
|
|
1554
1612
|
|
|
1555
1613
|
// Views are emitted by the one schema-diff path: renderD1 = renderSqlite (which
|
|
1556
1614
|
// renders view DDL) + the D1 safety pass (applied inside renderD1, stripping the
|
|
@@ -1572,7 +1630,7 @@ async function runD1Migrate(
|
|
|
1572
1630
|
try {
|
|
1573
1631
|
emitResult = renderD1(diffResult.changes, expected, actual.meta, actual);
|
|
1574
1632
|
} catch (err) {
|
|
1575
|
-
if (err
|
|
1633
|
+
if (isBlockedChangesError(err)) {
|
|
1576
1634
|
const entries = blockedToEntries(err);
|
|
1577
1635
|
for (const e of entries) {
|
|
1578
1636
|
log.error(`migrate: blocked '${e.kind}' on ${e.description} (allow with --allow ${e.allowFlag})`);
|
package/src/commands/verify.ts
CHANGED
|
@@ -122,7 +122,7 @@ const ERR_UNKNOWN_ATTR = "ERR_UNKNOWN_ATTR";
|
|
|
122
122
|
*/
|
|
123
123
|
const EMPTY_MIGRATE_FLAGS = {
|
|
124
124
|
db: undefined, dialect: undefined, format: undefined, outDir: undefined, slug: undefined,
|
|
125
|
-
allow: [], onAmbiguous: undefined, dryRun: false, d1Binding: undefined, remote: false,
|
|
125
|
+
allow: [], onAmbiguous: undefined, renames: [], dryRun: false, d1Binding: undefined, remote: false,
|
|
126
126
|
apply: false, rollback: undefined, yes: false, fromDb: false, baseline: false,
|
|
127
127
|
applyPending: false,
|
|
128
128
|
} as const satisfies MigrateFlags;
|
package/src/index.ts
CHANGED
|
@@ -143,6 +143,8 @@ MIGRATE FLAGS:
|
|
|
143
143
|
drop-check,drop-view,drop-view-cascade,
|
|
144
144
|
adopt-view,nullable-to-not-null,drop-identity-default
|
|
145
145
|
--on-ambiguous abort|rename|drop-add Default abort
|
|
146
|
+
--rename-table [schema.]old=new Declare a table rename (repeatable)
|
|
147
|
+
--rename-column [schema.]table.old=new Declare a column rename; keeps its data (repeatable)
|
|
146
148
|
--d1 <binding> D1 binding name from wrangler.toml (only with --dialect d1)
|
|
147
149
|
--remote Target remote D1 instead of local (only with --dialect d1)
|
|
148
150
|
--apply Run 'wrangler d1 migrations apply' after writing files
|
package/src/lib/args.ts
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { parseArgs } from "node:util";
|
|
2
|
+
import type { DeclaredRename } from "@metaobjectsdev/migrate-ts";
|
|
2
3
|
import { parseAdvisoryLimit } from "./advisory.js";
|
|
3
4
|
|
|
4
5
|
// ---------------------------------------------------------------------------
|
|
@@ -510,6 +511,8 @@ export interface MigrateFlags {
|
|
|
510
511
|
slug: string | undefined;
|
|
511
512
|
allow: AllowToken[];
|
|
512
513
|
onAmbiguous: OnAmbiguous | undefined;
|
|
514
|
+
/** `--rename-table` / `--rename-column`: renames the author declares (see `DeclaredRename`). */
|
|
515
|
+
renames: DeclaredRename[];
|
|
513
516
|
dryRun: boolean;
|
|
514
517
|
// D1-specific:
|
|
515
518
|
d1Binding: string | undefined;
|
|
@@ -539,6 +542,8 @@ export const MIGRATE_OPTIONS = {
|
|
|
539
542
|
"slug": { type: "string" },
|
|
540
543
|
"allow": { type: "string", multiple: true },
|
|
541
544
|
"on-ambiguous": { type: "string" },
|
|
545
|
+
"rename-table": { type: "string", multiple: true },
|
|
546
|
+
"rename-column": { type: "string", multiple: true },
|
|
542
547
|
"dry-run": { type: "boolean", default: false },
|
|
543
548
|
"from-db": { type: "boolean", default: false },
|
|
544
549
|
"d1": { type: "string" },
|
|
@@ -593,6 +598,10 @@ export function parseMigrateArgs(argv: string[]): MigrateFlags {
|
|
|
593
598
|
slug: values.slug as string | undefined,
|
|
594
599
|
allow: allowTokens as AllowToken[],
|
|
595
600
|
onAmbiguous: onAmb as OnAmbiguous | undefined,
|
|
601
|
+
renames: [
|
|
602
|
+
...((values["rename-table"] as string[] | undefined) ?? []).map(parseTableRename),
|
|
603
|
+
...((values["rename-column"] as string[] | undefined) ?? []).map(parseColumnRename),
|
|
604
|
+
],
|
|
596
605
|
dryRun: !!values["dry-run"],
|
|
597
606
|
d1Binding: values.d1 as string | undefined,
|
|
598
607
|
remote: !!values.remote,
|
|
@@ -605,6 +614,40 @@ export function parseMigrateArgs(argv: string[]): MigrateFlags {
|
|
|
605
614
|
};
|
|
606
615
|
}
|
|
607
616
|
|
|
617
|
+
/** `--rename-table [schema.]old=new` — the new name is bare: a rename never moves schemas. */
|
|
618
|
+
function parseTableRename(v: string): DeclaredRename {
|
|
619
|
+
const { path, to } = splitRename(v, "--rename-table", "[schema.]old=new", 1, 2);
|
|
620
|
+
const from = path[path.length - 1]!;
|
|
621
|
+
return path.length === 2 ? { kind: "table", schema: path[0]!, from, to } : { kind: "table", from, to };
|
|
622
|
+
}
|
|
623
|
+
|
|
624
|
+
/** `--rename-column [schema.]table.old=new` — `table` is the table's name in the metadata. */
|
|
625
|
+
function parseColumnRename(v: string): DeclaredRename {
|
|
626
|
+
const { path, to } = splitRename(v, "--rename-column", "[schema.]table.old=new", 2, 3);
|
|
627
|
+
const [from, table] = [path[path.length - 1]!, path[path.length - 2]!];
|
|
628
|
+
return path.length === 3
|
|
629
|
+
? { kind: "column", schema: path[0]!, table, from, to }
|
|
630
|
+
: { kind: "column", table, from, to };
|
|
631
|
+
}
|
|
632
|
+
|
|
633
|
+
/**
|
|
634
|
+
* Split `<path>=<new>` into the dotted path and the new name, refusing anything the
|
|
635
|
+
* flag's grammar cannot hold — including a path of [minPath, maxPath] segments, so the
|
|
636
|
+
* `invalid <flag>` message has the one construction site its callers also use.
|
|
637
|
+
*/
|
|
638
|
+
function splitRename(
|
|
639
|
+
v: string, flag: string, shape: string, minPath: number, maxPath: number,
|
|
640
|
+
): { path: string[]; to: string } {
|
|
641
|
+
const eq = v.indexOf("=");
|
|
642
|
+
const path = eq < 0 ? [] : v.slice(0, eq).split(".");
|
|
643
|
+
const to = eq < 0 ? "" : v.slice(eq + 1);
|
|
644
|
+
if (to === "" || to.includes(".") || to.includes("=") || path.some((p) => p === "")
|
|
645
|
+
|| path.length < minPath || path.length > maxPath) {
|
|
646
|
+
throw new Error(`invalid ${flag} '${v}'; expected ${shape}`);
|
|
647
|
+
}
|
|
648
|
+
return { path, to };
|
|
649
|
+
}
|
|
650
|
+
|
|
608
651
|
// ---------------------------------------------------------------------------
|
|
609
652
|
// eject flags — FR-040 §4.2(a)
|
|
610
653
|
// ---------------------------------------------------------------------------
|
package/src/lib/config.ts
CHANGED
|
@@ -3,6 +3,7 @@ import { join } from "node:path";
|
|
|
3
3
|
import { ConfigSchema, type Config, DEFAULT_METAOBJECTS_DIR } from "@metaobjectsdev/sdk";
|
|
4
4
|
import type { GenFlags, MigrateFlags, MigrateFormat } from "./args.js";
|
|
5
5
|
import type { Dialect } from "./kysely.js";
|
|
6
|
+
import type { DeclaredRename } from "@metaobjectsdev/migrate-ts";
|
|
6
7
|
|
|
7
8
|
// ---------------------------------------------------------------------------
|
|
8
9
|
// Built-in defaults
|
|
@@ -42,6 +43,8 @@ export interface ResolvedMigrateConfig {
|
|
|
42
43
|
/** Output-format adapter (#192): "default" homegrown layout, or "flyway" V__/U__. */
|
|
43
44
|
format: MigrateFormat;
|
|
44
45
|
onAmbiguous: "abort" | "rename" | "drop-add";
|
|
46
|
+
/** Declared renames — CLI-only, never config: a rename is a one-time migration event. */
|
|
47
|
+
renames: DeclaredRename[];
|
|
45
48
|
allow: string[];
|
|
46
49
|
slug: string | undefined;
|
|
47
50
|
dryRun: boolean;
|
|
@@ -121,6 +124,7 @@ export async function resolveMigrateConfig(
|
|
|
121
124
|
dialect: flags.dialect ?? cfgBlock.dialect ?? MIGRATE_DEFAULTS.dialect,
|
|
122
125
|
format: flags.format ?? cfgBlock.format ?? MIGRATE_DEFAULTS.format,
|
|
123
126
|
onAmbiguous: flags.onAmbiguous ?? cfgBlock.onAmbiguous ?? MIGRATE_DEFAULTS.onAmbiguous,
|
|
127
|
+
renames: flags.renames,
|
|
124
128
|
allow: flags.allow.length > 0
|
|
125
129
|
? flags.allow
|
|
126
130
|
: (cfgBlock.allow ?? MIGRATE_DEFAULTS.allow),
|
package/src/lib/output.ts
CHANGED
|
@@ -181,6 +181,12 @@ export interface MigrateResultShape {
|
|
|
181
181
|
* that cannot work.
|
|
182
182
|
*/
|
|
183
183
|
format?: "default" | "flyway";
|
|
184
|
+
/**
|
|
185
|
+
* Data-hazard warnings: changes that apply to an empty table and fail on a populated one.
|
|
186
|
+
* Text output already carries them on stderr; JSON/toon carry them here so a programmatic
|
|
187
|
+
* caller sees the risk before applying.
|
|
188
|
+
*/
|
|
189
|
+
warnings?: string[];
|
|
184
190
|
}
|
|
185
191
|
|
|
186
192
|
export function formatMigrateResult(result: MigrateResultShape, _opts: FormatOptions): string {
|
|
@@ -298,6 +304,7 @@ export function migrateResultToData(result: MigrateResultShape): {
|
|
|
298
304
|
written: string[];
|
|
299
305
|
summary: string;
|
|
300
306
|
help: string[];
|
|
307
|
+
warnings?: string[];
|
|
301
308
|
} {
|
|
302
309
|
const changeEntries = Object.entries(result.changeCounts).filter(([, v]) => v > 0);
|
|
303
310
|
const changes = changeEntries.map(([kind, count]) => ({ kind, count }));
|
|
@@ -352,7 +359,12 @@ export function migrateResultToData(result: MigrateResultShape): {
|
|
|
352
359
|
help = ["re-run with --slug <name> to write the migration"];
|
|
353
360
|
}
|
|
354
361
|
|
|
355
|
-
|
|
362
|
+
const warnings = result.warnings ?? [];
|
|
363
|
+
return {
|
|
364
|
+
changes, written: result.writtenPaths, summary, help,
|
|
365
|
+
// Only when present, so a run with no hazard keeps its existing shape.
|
|
366
|
+
...(warnings.length > 0 ? { warnings } : {}),
|
|
367
|
+
};
|
|
356
368
|
}
|
|
357
369
|
|
|
358
370
|
export function formatMigrateResultToon(result: MigrateResultShape): string {
|