@apso/cli 0.43.4 → 0.45.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.
Files changed (65) hide show
  1. package/README.md +7 -11
  2. package/dist/commands/deploy.js +1 -1
  3. package/dist/commands/generate.js +21 -1
  4. package/dist/commands/mcp/serve.js +30 -9
  5. package/dist/commands/migrate/generate.d.ts +3 -1
  6. package/dist/commands/migrate/generate.js +42 -8
  7. package/dist/commands/migrate/plan.d.ts +12 -0
  8. package/dist/commands/migrate/plan.js +88 -0
  9. package/dist/commands/migrate/sandbox.d.ts +11 -0
  10. package/dist/commands/migrate/sandbox.js +132 -0
  11. package/dist/commands/migrate/verify.d.ts +5 -2
  12. package/dist/commands/migrate/verify.js +44 -9
  13. package/dist/commands/migrate.d.ts +16 -3
  14. package/dist/commands/migrate.js +171 -96
  15. package/dist/lib/ddl/catalog.d.ts +21 -0
  16. package/dist/lib/ddl/catalog.js +58 -0
  17. package/dist/lib/ddl/change.d.ts +19 -0
  18. package/dist/lib/ddl/change.js +393 -0
  19. package/dist/lib/ddl/index.d.ts +80 -0
  20. package/dist/lib/ddl/index.js +309 -0
  21. package/dist/lib/ddl/migration-file.d.ts +42 -0
  22. package/dist/lib/ddl/migration-file.js +222 -0
  23. package/dist/lib/generators/base.d.ts +1 -0
  24. package/dist/lib/generators/go.d.ts +2 -1
  25. package/dist/lib/generators/go.js +37 -6
  26. package/dist/lib/generators/python.d.ts +4 -2
  27. package/dist/lib/generators/python.js +31 -8
  28. package/dist/lib/generators/typescript.d.ts +2 -1
  29. package/dist/lib/generators/typescript.js +43 -6
  30. package/dist/lib/guards.d.ts +11 -1
  31. package/dist/lib/guards.js +20 -1
  32. package/dist/lib/plan/baseline.d.ts +22 -0
  33. package/dist/lib/plan/baseline.js +100 -0
  34. package/dist/lib/plan/index.d.ts +52 -0
  35. package/dist/lib/plan/index.js +272 -0
  36. package/dist/lib/plan/prompts.d.ts +18 -0
  37. package/dist/lib/plan/prompts.js +77 -0
  38. package/dist/lib/templates/go/index-module.eta +6 -0
  39. package/dist/lib/templates/go/middleware/auth.eta +299 -59
  40. package/dist/lib/templates/guards/api-key-auth.guard.eta +146 -0
  41. package/dist/lib/templates/guards/auth.guard.eta +6 -0
  42. package/dist/lib/templates/guards/guards.module.eta +11 -19
  43. package/dist/lib/templates/guards/index.eta +2 -2
  44. package/dist/lib/templates/guards/jwt-auth.guard.eta +172 -0
  45. package/dist/lib/templates/guards/scope.guard.eta +20 -2
  46. package/dist/lib/templates/python/auth.eta +217 -0
  47. package/dist/lib/templates/python/index-module.eta +11 -0
  48. package/dist/lib/templates/python/models/model-col-enum.eta +1 -1
  49. package/dist/lib/templates/python/schemas/schema.eta +22 -1
  50. package/dist/lib/templates/rest/controller-rest.eta +16 -1
  51. package/dist/lib/templates/rest/index-module-rest.eta +6 -0
  52. package/dist/lib/types/auth.d.ts +41 -7
  53. package/dist/lib/types/auth.js +61 -2
  54. package/dist/lib/types/generator.d.ts +6 -0
  55. package/dist/lib/types/index.d.ts +1 -1
  56. package/dist/lib/types/index.js +3 -1
  57. package/dist/lib/utils/crud-deps.d.ts +13 -0
  58. package/dist/lib/utils/crud-deps.js +37 -1
  59. package/dist/lib/verify/index.d.ts +27 -9
  60. package/dist/lib/verify/index.js +249 -21
  61. package/dist/lib/verify/review.d.ts +14 -0
  62. package/dist/lib/verify/review.js +98 -0
  63. package/npm-shrinkwrap.json +2 -2
  64. package/oclif.manifest.json +176 -23
  65. package/package.json +1 -1
@@ -0,0 +1,98 @@
1
+ "use strict";
2
+ /**
3
+ * Review a TypeORM-generated migration for statements that lose data or fail
4
+ * on a populated database. TypeORM changes a column type by DROP + ADD, casts
5
+ * enums without remapping removed values, and adds NOT NULL columns as if the
6
+ * table were empty. The generated file is kept; each finding names a
7
+ * statement to rewrite.
8
+ */
9
+ Object.defineProperty(exports, "__esModule", { value: true });
10
+ exports.reviewMigration = void 0;
11
+ /** The SQL of each queryRunner.query call in a method body. */
12
+ const queries = (body) => [...body.matchAll(/queryRunner\.query\(\s*`([\S\s]*?)`/g)].map((m) => m[1]);
13
+ const enumValues = (sql) => {
14
+ var _a, _b;
15
+ return new Set([...((_b = (_a = sql === null || sql === void 0 ? void 0 : sql.match(/ENUM\(([^)]*)\)/)) === null || _a === void 0 ? void 0 : _a[1]) !== null && _b !== void 0 ? _b : "").matchAll(/'([^']*)'/g)].map((v) => v[1]));
16
+ };
17
+ const typeSql = (sqls, type) => sqls.find((s) => s.startsWith(`CREATE TYPE "public"."${type}" AS ENUM`));
18
+ const list = (values) => values.map((v) => `"${v}"`).join(", ");
19
+ function reviewMigration(source) {
20
+ var _a, _b, _c, _d, _e;
21
+ const [upBody, downBody = ""] = source.split(/public\s+async\s+down\s*\(/);
22
+ const up = queries(upBody);
23
+ const down = queries(downBody);
24
+ const findings = [];
25
+ const add = (code, table, column, message) => findings.push({ code, table, column, message });
26
+ // Statements on tables this migration creates touch no existing rows.
27
+ const created = new Set(up.map((q) => { var _a; return (_a = q.match(/^CREATE TABLE "([^"]+)"/)) === null || _a === void 0 ? void 0 : _a[1]; }).filter(Boolean));
28
+ const existing = (table) => !created.has(table);
29
+ // An UPDATE or INSERT before a statement that names the column means the
30
+ // migration already moves or backfills its data.
31
+ const handled = (i, col) => up.slice(0, i).some((q) => /^(UPDATE|INSERT)\b/.test(q) && q.includes(`"${col}"`));
32
+ const dropped = new Map();
33
+ const added = new Map();
34
+ for (const [i, q] of up.entries()) {
35
+ const d = q.match(/^ALTER TABLE "([^"]+)" DROP COLUMN "([^"]+)"/);
36
+ if (d && !handled(i, d[2]))
37
+ dropped.set(d[1], [...((_a = dropped.get(d[1])) !== null && _a !== void 0 ? _a : []), d[2]]);
38
+ const a = q.match(/^ALTER TABLE "([^"]+)" ADD "([^"]+)"/);
39
+ if (a)
40
+ added.set(a[1], [...((_b = added.get(a[1])) !== null && _b !== void 0 ? _b : []), a[2]]);
41
+ }
42
+ for (const [table, cols] of dropped) {
43
+ const adds = (_c = added.get(table)) !== null && _c !== void 0 ? _c : [];
44
+ const newCols = adds.filter((a) => !cols.includes(a));
45
+ for (const col of cols) {
46
+ if (adds.includes(col))
47
+ add("TYPE_CHANGE_DROPS_DATA", table, col, `${table}.${col}: TypeORM drops and re-adds the column to change its type, deleting every value. Replace the DROP and ADD with ALTER COLUMN "${col}" TYPE ... USING a cast that converts the existing values.`);
48
+ else if (newCols.length > 0)
49
+ add("POSSIBLE_RENAME", table, col, `${table}.${col} is dropped while ${list(newCols)} is added. If that is a rename, replace the DROP and ADD with RENAME COLUMN to keep the data; otherwise every value in "${col}" is deleted.`);
50
+ else
51
+ add("DROPS_COLUMN", table, col, created.size > 0
52
+ ? `${table}.${col} is dropped with all its values while this migration creates ${list([...created])}. If the data moves there, copy it with INSERT ... SELECT before the DROP; otherwise confirm the loss.`
53
+ : `${table}.${col} is dropped with all its values. Confirm the loss, or copy the data first.`);
54
+ }
55
+ }
56
+ for (const [i, q] of up.entries()) {
57
+ let m = q.match(/^DROP TABLE "([^"]+)"/);
58
+ if (m)
59
+ add("DROPS_TABLE", m[1], undefined, `Table ${m[1]} is dropped with all its rows. Confirm the loss, or rename the table if an entity was renamed.`);
60
+ m = q.match(/^ALTER TABLE "([^"]+)" ADD "([^"]+)" [^,]*\bNOT NULL\b(.*)$/);
61
+ if (m && existing(m[1])) {
62
+ const def = (_d = m[3].match(/DEFAULT (.+)$/)) === null || _d === void 0 ? void 0 : _d[1].trim();
63
+ if (def)
64
+ add("DEFAULT_BACKFILLS_ROWS", m[1], m[2], `${m[1]}.${m[2]} is added NOT NULL DEFAULT ${def}, so every existing row gets ${def}. Confirm that is right for existing rows, or add the column nullable, backfill it, then SET NOT NULL.`);
65
+ else
66
+ add("NOT_NULL_WITHOUT_BACKFILL", m[1], m[2], `${m[1]}.${m[2]} is added NOT NULL with no default, which fails if ${m[1]} has any rows. Add it nullable, backfill it, then SET NOT NULL.`);
67
+ }
68
+ m = q.match(/^ALTER TABLE "([^"]+)" ALTER COLUMN "([^"]+)" SET NOT NULL/);
69
+ if (m && existing(m[1]) && !handled(i, m[2]))
70
+ add("SET_NOT_NULL", m[1], m[2], `${m[1]}.${m[2]} becomes NOT NULL, which fails on any row where it is null. Backfill those rows first.`);
71
+ m = q.match(/^ALTER TABLE "([^"]+)" ALTER COLUMN "([^"]+)" TYPE (?!"public"\."[^"]+_enum")/);
72
+ if (m && existing(m[1]))
73
+ add("TYPE_CAST", m[1], m[2], `${m[1]}.${m[2]} changes type in place; a value the cast cannot convert fails the migration. Check the USING clause handles every existing value.`);
74
+ m =
75
+ (_e = q.match(/^CREATE UNIQUE INDEX "[^"]+" ON "([^"]+)" \(([^)]+)\)/)) !== null && _e !== void 0 ? _e : q.match(/^ALTER TABLE "([^"]+)" ADD CONSTRAINT "[^"]+" UNIQUE \(([^)]+)\)/);
76
+ if (m && existing(m[1]))
77
+ add("UNIQUE_ON_EXISTING_ROWS", m[1], m[2].replace(/"/g, ""), `A unique constraint on ${m[1]} (${m[2].replace(/"/g, "")}) fails if existing rows hold duplicates. Check for duplicates first and decide which row wins.`);
78
+ m = q.match(/^ALTER TABLE "([^"]+)" ADD CONSTRAINT "[^"]+" FOREIGN KEY \("([^"]+)"\) REFERENCES "([^"]+)"/);
79
+ if (m && existing(m[1]) && created.has(m[3]))
80
+ add("FOREIGN_KEY_TO_NEW_TABLE", m[1], m[2], `Existing ${m[1]} rows must point at ${m[3]}, which this migration creates empty, so the foreign key fails on any row with ${m[2]} set. If ${m[3]} replaces an existing entity, replace the CREATE TABLE with ALTER TABLE "<old table>" RENAME TO "${m[3]}" and ALTER SEQUENCE "<old table>_id_seq" RENAME TO "${m[3]}_id_seq"; TypeORM leaves the old table in place.`);
81
+ }
82
+ // TypeORM swaps an enum by renaming the old type to *_old, creating the new
83
+ // one and casting through text; down() recreates *_old with the old values.
84
+ for (const q of up) {
85
+ const m = q.match(/^ALTER TYPE "public"\."([^"]+)" RENAME TO "([^"]+_old)"/);
86
+ if (!m)
87
+ continue;
88
+ const now = enumValues(typeSql(up, m[1]));
89
+ const cast = up.find((s) => s.includes(`TYPE "public"."${m[1]}" USING`));
90
+ // A USING clause that names a removed value maps it; the rest still fail.
91
+ const removed = [...enumValues(typeSql(down, m[2]))].filter((v) => !now.has(v) && !(cast === null || cast === void 0 ? void 0 : cast.includes(`'${v}'`)));
92
+ const col = cast === null || cast === void 0 ? void 0 : cast.match(/^ALTER TABLE "([^"]+)" ALTER COLUMN "([^"]+)"/);
93
+ if (removed.length > 0 && col)
94
+ add("ENUM_VALUES_REMOVED", col[1], col[2], `${col[1]}.${col[2]} drops enum value ${removed.map((v) => `'${v}'`).join(", ")}; the cast fails on any row that still holds it. Map those rows to a remaining value in the USING clause, e.g. CASE "${col[2]}"::text WHEN '${removed[0]}' THEN '...' ELSE "${col[2]}"::text END.`);
95
+ }
96
+ return findings;
97
+ }
98
+ exports.reviewMigration = reviewMigration;
@@ -1,12 +1,12 @@
1
1
  {
2
2
  "name": "@apso/cli",
3
- "version": "0.43.4",
3
+ "version": "0.45.0",
4
4
  "lockfileVersion": 3,
5
5
  "requires": true,
6
6
  "packages": {
7
7
  "": {
8
8
  "name": "@apso/cli",
9
- "version": "0.43.4",
9
+ "version": "0.45.0",
10
10
  "license": "Apache-2.0",
11
11
  "dependencies": {
12
12
  "@apso/schema-tools": "^0.2.0",
@@ -1,5 +1,5 @@
1
1
  {
2
- "version": "0.43.4",
2
+ "version": "0.45.0",
3
3
  "commands": {
4
4
  "config": {
5
5
  "id": "config",
@@ -379,7 +379,7 @@
379
379
  },
380
380
  "migrate": {
381
381
  "id": "migrate",
382
- "description": "Detect schema changes, generate migration SQL, and test against a local PGlite sandbox",
382
+ "description": "Migrate the database to this .apsorc in one step: regenerate the code, compare with the deployed schema, ask about anything ambiguous (a rename or a drop, how a field splits, what replaces a removed value), write the migration, and test it on a local copy. In a terminal it asks; for an agent it prints the open questions as JSON and exits 2, and the agent reruns it with --answer. The steps are also separate commands: apso migrate plan, generate and verify.",
383
383
  "strict": true,
384
384
  "pluginName": "@apso/cli",
385
385
  "pluginAlias": "@apso/cli",
@@ -388,27 +388,65 @@
388
388
  "hiddenAliases": [],
389
389
  "examples": [
390
390
  "$ apso migrate",
391
- "$ apso migrate --apply",
392
- "$ apso migrate --reset",
393
- "$ apso migrate --sql"
391
+ "$ apso migrate --answer field:Contact.phone=rename:mobilePhone",
392
+ "$ apso migrate --seed seed.sql --check checks.sql"
394
393
  ],
395
394
  "flags": {
396
- "apply": {
397
- "name": "apply",
395
+ "baseline": {
396
+ "name": "baseline",
397
+ "type": "option",
398
+ "description": "The deployed .apsorc, the first time only. Later runs find it from the last migration",
399
+ "multiple": false
400
+ },
401
+ "answer": {
402
+ "name": "answer",
403
+ "type": "option",
404
+ "description": "Answer a question: <question id>=<option>. Repeatable.",
405
+ "multiple": true,
406
+ "default": []
407
+ },
408
+ "seed": {
409
+ "name": "seed",
410
+ "type": "option",
411
+ "description": "SQL file of rows to load first; the test then fails if the migration loses any",
412
+ "multiple": false
413
+ },
414
+ "check": {
415
+ "name": "check",
416
+ "type": "option",
417
+ "description": "SQL file of SELECTs with a boolean ok column, run after the migration",
418
+ "multiple": false
419
+ },
420
+ "accept-loss": {
421
+ "name": "accept-loss",
422
+ "type": "option",
423
+ "description": "A table or table.column whose seeded data may be dropped on purpose (repeatable)",
424
+ "multiple": true,
425
+ "default": []
426
+ },
427
+ "name": {
428
+ "name": "name",
429
+ "type": "option",
430
+ "description": "Migration name",
431
+ "multiple": false,
432
+ "default": "SchemaUpdate"
433
+ },
434
+ "skip-generate": {
435
+ "name": "skip-generate",
398
436
  "type": "boolean",
399
- "description": "Apply the migration: update the local snapshot after a successful test",
437
+ "description": "Don't regenerate code from .apsorc first",
400
438
  "allowNo": false
401
439
  },
402
- "reset": {
403
- "name": "reset",
440
+ "skip-verify": {
441
+ "name": "skip-verify",
404
442
  "type": "boolean",
405
- "description": "Reset the sandbox (clear snapshot and PGlite data)",
443
+ "description": "Write the migration without testing it",
406
444
  "allowNo": false
407
445
  },
408
- "sql": {
409
- "name": "sql",
446
+ "json": {
447
+ "name": "json",
410
448
  "type": "boolean",
411
- "description": "Output raw SQL only (for piping to files)",
449
+ "description": "Print the result as JSON (the default when output is not a terminal)",
412
450
  "allowNo": false
413
451
  }
414
452
  },
@@ -771,7 +809,7 @@
771
809
  },
772
810
  "migrate:generate": {
773
811
  "id": "migrate:generate",
774
- "description": "Generate a TypeORM migration from the deployed schema to this project's entities, using a local PGlite database (TypeScript services)",
812
+ "description": "Write a migration from the deployed schema to this project's .apsorc. Apso renders the SQL from the answered migration plan (apso migrate plan), including data moves and a down() that reverses them. Exits 2 while plan questions are open. The migration is a TypeORM class, an Alembic revision or a goose SQL file, by the service's language. --typeorm uses TypeORM's generator on a local PGlite database instead (TypeScript services).",
775
813
  "strict": true,
776
814
  "pluginName": "@apso/cli",
777
815
  "pluginAlias": "@apso/cli",
@@ -779,14 +817,15 @@
779
817
  "aliases": [],
780
818
  "hiddenAliases": [],
781
819
  "examples": [
782
- "$ apso migrate generate --baseline deployed.apsorc"
820
+ "$ apso migrate plan --answer entity:Company=rename:Organization",
821
+ "$ apso migrate generate",
822
+ "$ apso migrate generate --typeorm"
783
823
  ],
784
824
  "flags": {
785
825
  "baseline": {
786
826
  "name": "baseline",
787
827
  "type": "option",
788
- "description": "The deployed .apsorc to migrate from",
789
- "required": true,
828
+ "description": "The deployed .apsorc to migrate from. Defaults to the schema after the newest planned migration, or the .apsorc from the last commit that changed the migrations folder",
790
829
  "multiple": false
791
830
  },
792
831
  "name": {
@@ -796,6 +835,19 @@
796
835
  "multiple": false,
797
836
  "default": "SchemaUpdate"
798
837
  },
838
+ "plan": {
839
+ "name": "plan",
840
+ "type": "option",
841
+ "description": "Migration plan with the answers",
842
+ "multiple": false,
843
+ "default": ".apso/migration-plan.json"
844
+ },
845
+ "typeorm": {
846
+ "name": "typeorm",
847
+ "type": "boolean",
848
+ "description": "Use TypeORM's migration:generate instead of Apso's plan (no data moves)",
849
+ "allowNo": false
850
+ },
799
851
  "json": {
800
852
  "name": "json",
801
853
  "type": "boolean",
@@ -805,9 +857,90 @@
805
857
  },
806
858
  "args": {}
807
859
  },
860
+ "migrate:plan": {
861
+ "id": "migrate:plan",
862
+ "description": "Compare the deployed .apsorc with this one and write a migration plan. Changes whose intent is ambiguous (rename or drop, how a field splits, which value replaces a removed enum value) become questions; answer them in the plan file or with --answer. Exits 2 while questions are open.",
863
+ "strict": true,
864
+ "pluginName": "@apso/cli",
865
+ "pluginAlias": "@apso/cli",
866
+ "pluginType": "core",
867
+ "aliases": [],
868
+ "hiddenAliases": [],
869
+ "examples": [
870
+ "$ apso migrate plan",
871
+ "$ apso migrate plan --answer field:Contact.phone=rename:mobilePhone",
872
+ "$ apso migrate plan --baseline deployed.apsorc"
873
+ ],
874
+ "flags": {
875
+ "baseline": {
876
+ "name": "baseline",
877
+ "type": "option",
878
+ "description": "The deployed .apsorc to migrate from. Defaults to the schema after the newest planned migration, or the .apsorc from the last commit that changed the migrations folder",
879
+ "multiple": false
880
+ },
881
+ "plan": {
882
+ "name": "plan",
883
+ "type": "option",
884
+ "description": "Plan file to read answers from and write",
885
+ "multiple": false,
886
+ "default": ".apso/migration-plan.json"
887
+ },
888
+ "answer": {
889
+ "name": "answer",
890
+ "type": "option",
891
+ "description": "Answer a question: <question id>=<option>. Repeatable.",
892
+ "multiple": true,
893
+ "default": []
894
+ },
895
+ "json": {
896
+ "name": "json",
897
+ "type": "boolean",
898
+ "description": "Print the open questions as JSON (the default when output is not a terminal)",
899
+ "allowNo": false
900
+ }
901
+ },
902
+ "args": {}
903
+ },
904
+ "migrate:sandbox": {
905
+ "id": "migrate:sandbox",
906
+ "description": "Test schema changes against a local PGlite sandbox that keeps its own snapshot of the last schema (the older flow; apso migrate is the guided one)",
907
+ "strict": true,
908
+ "pluginName": "@apso/cli",
909
+ "pluginAlias": "@apso/cli",
910
+ "pluginType": "core",
911
+ "aliases": [],
912
+ "hiddenAliases": [],
913
+ "examples": [
914
+ "$ apso migrate sandbox",
915
+ "$ apso migrate sandbox --apply",
916
+ "$ apso migrate sandbox --reset",
917
+ "$ apso migrate sandbox --sql"
918
+ ],
919
+ "flags": {
920
+ "apply": {
921
+ "name": "apply",
922
+ "type": "boolean",
923
+ "description": "Apply the migration: update the local snapshot after a successful test",
924
+ "allowNo": false
925
+ },
926
+ "reset": {
927
+ "name": "reset",
928
+ "type": "boolean",
929
+ "description": "Reset the sandbox (clear snapshot and PGlite data)",
930
+ "allowNo": false
931
+ },
932
+ "sql": {
933
+ "name": "sql",
934
+ "type": "boolean",
935
+ "description": "Output raw SQL only (for piping to files)",
936
+ "allowNo": false
937
+ }
938
+ },
939
+ "args": {}
940
+ },
808
941
  "migrate:verify": {
809
942
  "id": "migrate:verify",
810
- "description": "Run a migration on a local PGlite copy of the deployed schema, then check the database matches this project's entities (TypeScript services)",
943
+ "description": "Run a migration on a local PGlite copy of the deployed schema, then check the database matches this project's entities (TypeScript), or a fresh build of its .apsorc (Python and Go, which also run down and check it gives back the deployed schema)",
811
944
  "strict": true,
812
945
  "pluginName": "@apso/cli",
813
946
  "pluginAlias": "@apso/cli",
@@ -815,23 +948,43 @@
815
948
  "aliases": [],
816
949
  "hiddenAliases": [],
817
950
  "examples": [
951
+ "$ apso migrate verify",
952
+ "$ apso migrate verify --seed seed.sql --check checks.sql",
953
+ "$ apso migrate verify --seed seed.sql --check checks.sql --accept-loss contact.fullName",
818
954
  "$ apso migrate verify --baseline deployed.apsorc --migration src/migrations/1700000000000-SchemaUpdate-migration.ts"
819
955
  ],
820
956
  "flags": {
821
957
  "baseline": {
822
958
  "name": "baseline",
823
959
  "type": "option",
824
- "description": "The deployed .apsorc the migration starts from",
825
- "required": true,
960
+ "description": "The deployed .apsorc the migration starts from. Defaults to the schema saved with the migration's plan",
826
961
  "multiple": false
827
962
  },
828
963
  "migration": {
829
964
  "name": "migration",
830
965
  "type": "option",
831
- "description": "The migration file to run",
832
- "required": true,
966
+ "description": "The migration file to run. Defaults to the newest planned migration",
833
967
  "multiple": false
834
968
  },
969
+ "seed": {
970
+ "name": "seed",
971
+ "type": "option",
972
+ "description": "SQL file of rows to insert into the deployed schema before the migration runs; the check then fails if the migration loses any of them",
973
+ "multiple": false
974
+ },
975
+ "check": {
976
+ "name": "check",
977
+ "type": "option",
978
+ "description": "SQL file of SELECTs run after the migration; each row must have a boolean ok (and a label naming the check), e.g. SELECT bool_and(\"firstName\" <> '') AS ok, 'every contact has a first name' AS label FROM contact",
979
+ "multiple": false
980
+ },
981
+ "accept-loss": {
982
+ "name": "accept-loss",
983
+ "type": "option",
984
+ "description": "A table or table.column whose seeded data the migration may drop on purpose (repeatable); pair it with --check to prove where the data went",
985
+ "multiple": true,
986
+ "default": []
987
+ },
835
988
  "skip-build": {
836
989
  "name": "skip-build",
837
990
  "type": "boolean",
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@apso/cli",
3
- "version": "0.43.4",
3
+ "version": "0.45.0",
4
4
  "mcpName": "io.github.apsoai/apso",
5
5
  "description": "Apso CLI",
6
6
  "author": "Apso by Mavric - @mavric",