@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,393 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.downDdl = exports.changeDdl = void 0;
4
+ /**
5
+ * Render an answered migration plan as the SQL that moves a database from
6
+ * the deployed .apsorc to the new one.
7
+ *
8
+ * Renames come first: the answered renames are applied to the deployed
9
+ * schema and its model rebuilt. Both models are built the same way, so their
10
+ * objects pair up by position and every name that differs becomes a RENAME
11
+ * (tables, columns, sequences, constraints, indexes, enum types). The renamed
12
+ * model is then diffed against the new one by identity, and the plan's
13
+ * answers supply the data steps. Statements run in a fixed order so data is
14
+ * moved before anything it lives in is dropped.
15
+ */
16
+ const apsorc_parser_1 = require("../apsorc-parser");
17
+ const plan_1 = require("../plan");
18
+ const _1 = require(".");
19
+ const answered = (plan, kind) => plan.questions.filter((x) => x.kind === kind && x.answer);
20
+ /** "field:Contact.phone" -> ["Contact", "phone"]; "enum:Contact.status:archived" -> [..., "archived"]. */
21
+ const subject = (id) => {
22
+ const [entity, rest = ""] = id.slice(id.indexOf(":") + 1).split(".");
23
+ const [field, value] = rest.split(":");
24
+ return { entity, field, value };
25
+ };
26
+ /** The deployed .apsorc with every answered rename applied. */
27
+ function applyRenames(before, after, plan) {
28
+ var _a, _b, _c;
29
+ const copy = JSON.parse(JSON.stringify(before));
30
+ for (const x of answered(plan, "removedField")) {
31
+ if (!x.answer.startsWith("rename:"))
32
+ continue;
33
+ const { entity, field } = subject(x.id);
34
+ const f = (_b = (_a = copy.entities
35
+ .find((e) => e.name === entity)) === null || _a === void 0 ? void 0 : _a.fields) === null || _b === void 0 ? void 0 : _b.find((g) => g.name === field);
36
+ if (f)
37
+ f.name = x.answer.slice("rename:".length);
38
+ }
39
+ for (const x of answered(plan, "removedEntity")) {
40
+ if (!x.answer.startsWith("rename:"))
41
+ continue;
42
+ const from = subject(x.id).entity;
43
+ const to = x.answer.slice("rename:".length);
44
+ const e = copy.entities.find((y) => y.name === from);
45
+ if (!e)
46
+ continue;
47
+ e.name = to;
48
+ const target = after.entities.find((y) => y.name === to);
49
+ if (target === null || target === void 0 ? void 0 : target.table)
50
+ e.table = target.table;
51
+ else
52
+ delete e.table;
53
+ for (const r of (_c = copy.relationships) !== null && _c !== void 0 ? _c : []) {
54
+ if (r.from === from)
55
+ r.from = to;
56
+ if (r.to === from)
57
+ r.to = to;
58
+ }
59
+ }
60
+ return copy;
61
+ }
62
+ function renames(was, now) {
63
+ const same = (k) => was[k].length === now[k].length;
64
+ if (!same("tables") ||
65
+ !same("constraints") ||
66
+ !same("indexes") ||
67
+ !same("enums"))
68
+ throw new Error("Renaming changed the schema's shape; this rename can't be rendered.");
69
+ const out = [];
70
+ was.tables.forEach((t, i) => {
71
+ const n = now.tables[i];
72
+ if (t.name !== n.name)
73
+ out.push(`ALTER TABLE ${(0, _1.q)(t.name)} RENAME TO ${(0, _1.q)(n.name)}`);
74
+ t.columns.forEach((c, j) => {
75
+ const nc = n.columns[j];
76
+ if (c.name !== nc.name)
77
+ out.push(`ALTER TABLE ${(0, _1.q)(n.name)} RENAME COLUMN ${(0, _1.q)(c.name)} TO ${(0, _1.q)(nc.name)}`);
78
+ if (c.serial && t.name !== n.name)
79
+ out.push(`ALTER SEQUENCE ${(0, _1.q)(`${t.name}_${c.name}_seq`)} RENAME TO ${(0, _1.q)(`${n.name}_${nc.name}_seq`)}`);
80
+ });
81
+ });
82
+ was.constraints.forEach((c, i) => {
83
+ const n = now.constraints[i];
84
+ if (c.name !== n.name)
85
+ out.push(`ALTER TABLE ${(0, _1.q)(n.table)} RENAME CONSTRAINT ${(0, _1.q)(c.name)} TO ${(0, _1.q)(n.name)}`);
86
+ });
87
+ was.indexes.forEach((x, i) => {
88
+ if (x.name !== now.indexes[i].name)
89
+ out.push(`ALTER INDEX "public".${(0, _1.q)(x.name)} RENAME TO ${(0, _1.q)(now.indexes[i].name)}`);
90
+ });
91
+ was.enums.forEach((e, i) => {
92
+ if (e.name !== now.enums[i].name)
93
+ out.push(`ALTER TYPE "public".${(0, _1.q)(e.name)} RENAME TO ${(0, _1.q)(now.enums[i].name)}`);
94
+ });
95
+ return out;
96
+ }
97
+ const nullableSql = (c) => `${(0, _1.q)(c.name)} ${c.type}${c.default === undefined ? "" : ` DEFAULT ${c.default}`}`;
98
+ /** Missing from the other model, or present under another name or definition. */
99
+ const changed = (x, y) => !y || x.name !== y.name || x.body !== y.body || x.unique !== y.unique;
100
+ /** The enum type name in a column type like "public"."contact_status_enum". */
101
+ const typeOf = (t) => { var _a; return ((_a = t.match(/^"public"\."(.+)"$/)) !== null && _a !== void 0 ? _a : [])[1]; };
102
+ function changeDdl(before, after, plan) {
103
+ const renamed = applyRenames(before, after, plan);
104
+ const was = (0, _1.schemaModel)((0, apsorc_parser_1.parseApsorcV2)(before));
105
+ const R = (0, _1.schemaModel)((0, apsorc_parser_1.parseApsorcV2)(renamed));
106
+ const A = (0, _1.schemaModel)((0, apsorc_parser_1.parseApsorcV2)(after));
107
+ const table = (entity, schema = after) => {
108
+ const e = schema.entities.find((x) => x.name === entity);
109
+ if (!e)
110
+ throw new Error(`No entity ${entity}`);
111
+ return (0, _1.tableName)(e);
112
+ };
113
+ const up = renames(was, R);
114
+ const later = [];
115
+ const rTables = new Map(R.tables.map((t) => [t.name, t]));
116
+ const aTables = new Map(A.tables.map((t) => [t.name, t]));
117
+ const rCons = new Map(R.constraints.map((c) => [c.key, c]));
118
+ const aCons = new Map(A.constraints.map((c) => [c.key, c]));
119
+ const rIdx = new Map(R.indexes.map((i) => [i.key, i]));
120
+ const aIdx = new Map(A.indexes.map((i) => [i.key, i]));
121
+ // Drop keys and indexes that go away or change. Foreign keys first.
122
+ for (const c of [...R.constraints].sort((a, b) => (a.kind === "fk" ? -1 : 0) - (b.kind === "fk" ? -1 : 0)))
123
+ if (changed(c, aCons.get(c.key)) &&
124
+ (c.kind === "fk" || aTables.has(c.table)))
125
+ up.push(`ALTER TABLE ${(0, _1.q)(c.table)} DROP CONSTRAINT ${(0, _1.q)(c.name)}`);
126
+ for (const i of R.indexes)
127
+ if (changed(i, aIdx.get(i.key)) && aTables.has(i.table))
128
+ up.push(`DROP INDEX "public".${(0, _1.q)(i.name)}`);
129
+ // Enum types: create new ones; swap changed ones through <name>_old.
130
+ const rEnums = new Map(R.enums.map((e) => [e.name, e]));
131
+ const swapped = new Set();
132
+ for (const e of A.enums) {
133
+ const old = rEnums.get(e.name);
134
+ if (!old)
135
+ up.push((0, _1.createTypeSql)(e));
136
+ else if (old.values.join(",") !== e.values.join(",")) {
137
+ up.push(`ALTER TYPE "public".${(0, _1.q)(e.name)} RENAME TO ${(0, _1.q)(`${e.name}_old`)}`, (0, _1.createTypeSql)(e));
138
+ swapped.add(e.name);
139
+ later.push(`DROP TYPE "public".${(0, _1.q)(`${e.name}_old`)}`);
140
+ }
141
+ }
142
+ // New tables, with their own primary and unique keys inline.
143
+ const createdInline = new Set();
144
+ for (const t of A.tables)
145
+ if (!rTables.has(t.name)) {
146
+ up.push((0, _1.createTableSql)(t, A.constraints));
147
+ for (const c of A.constraints)
148
+ if (c.table === t.name && c.kind !== "fk")
149
+ createdInline.add(c.key);
150
+ }
151
+ // Columns on tables that stay.
152
+ const setNotNull = [];
153
+ const enumMap = new Map();
154
+ for (const x of answered(plan, "enumValue")) {
155
+ const { entity, field, value } = subject(x.id);
156
+ const key = `${table(entity)}.${field}`;
157
+ if (!enumMap.has(key))
158
+ enumMap.set(key, new Map());
159
+ enumMap.get(key).set(value, x.answer);
160
+ }
161
+ const casts = new Map(answered(plan, "cast").map((x) => {
162
+ const { entity, field } = subject(x.id);
163
+ return [`${table(entity)}.${field}`, x];
164
+ }));
165
+ for (const t of A.tables) {
166
+ const old = rTables.get(t.name);
167
+ if (!old)
168
+ continue;
169
+ for (const c of t.columns) {
170
+ const o = old.columns.find((x) => x.name === c.name);
171
+ const at = `ALTER TABLE ${(0, _1.q)(t.name)} ALTER COLUMN ${(0, _1.q)(c.name)}`;
172
+ if (!o) {
173
+ const needsData = c.notNull && c.default === undefined && !c.serial;
174
+ up.push(`ALTER TABLE ${(0, _1.q)(t.name)} ADD ${needsData ? nullableSql(c) : c.sql}`);
175
+ if (needsData)
176
+ setNotNull.push(`${at} SET NOT NULL`);
177
+ continue;
178
+ }
179
+ const enumName = typeOf(c.type);
180
+ const retype = o.type !== c.type || (enumName && swapped.has(enumName));
181
+ if (retype) {
182
+ let using = `${(0, _1.q)(c.name)}::${c.type}`;
183
+ const map = enumMap.get(`${t.name}.${c.name}`);
184
+ const cast = casts.get(`${t.name}.${c.name}`);
185
+ if (enumName && (map === null || map === void 0 ? void 0 : map.size))
186
+ using = `(CASE ${(0, _1.q)(c.name)}::text ${[...map]
187
+ .map(([v, to]) => `WHEN ${(0, _1.literal)(v)} THEN ${(0, _1.literal)(to)}`)
188
+ .join(" ")} ELSE ${(0, _1.q)(c.name)}::text END)::${c.type}`;
189
+ else if (enumName)
190
+ using = `${(0, _1.q)(c.name)}::text::${c.type}`;
191
+ else if ((cast === null || cast === void 0 ? void 0 : cast.answer) === "sql")
192
+ using = `(${cast.sql})`;
193
+ if (o.default !== undefined)
194
+ up.push(`${at} DROP DEFAULT`);
195
+ up.push(`${at} TYPE ${c.type} USING ${using}`);
196
+ if (c.default !== undefined)
197
+ setNotNull.push(`${at} SET DEFAULT ${c.default}`);
198
+ }
199
+ else if (o.default !== c.default) {
200
+ setNotNull.push(c.default === undefined
201
+ ? `${at} DROP DEFAULT`
202
+ : `${at} SET DEFAULT ${c.default}`);
203
+ }
204
+ if (!o.notNull && c.notNull)
205
+ setNotNull.push(`${at} SET NOT NULL`);
206
+ if (o.notNull && !c.notNull)
207
+ up.push(`${at} DROP NOT NULL`);
208
+ }
209
+ }
210
+ // Data: split, move, copied links, backfills.
211
+ for (const x of answered(plan, "removedField")) {
212
+ const { entity, field } = subject(x.id);
213
+ const t = table(entity);
214
+ if (x.answer.startsWith("split:") && typeof x.sql === "object") {
215
+ const sets = x
216
+ .answer.slice("split:".length)
217
+ .split(",")
218
+ .map((f) => `${(0, _1.q)(f)} = (${x.sql[f]})`);
219
+ up.push(`UPDATE ${(0, _1.q)(t)} SET ${sets.join(", ")}`);
220
+ }
221
+ if (x.answer.startsWith("move:")) {
222
+ const [toEntity, toField] = x.answer.slice("move:".length).split(".");
223
+ const target = table(toEntity);
224
+ const fk = A.constraints.find((c) => c.kind === "fk" && c.table === target && c.references === t);
225
+ if (!fk)
226
+ throw new Error(`${toEntity} has no foreign key to ${entity}, so ${field} can't move there.`);
227
+ up.push(`INSERT INTO ${(0, _1.q)(target)} (${(0, _1.q)(toField)}, ${(0, _1.q)(fk.columns[0])}) SELECT ${(0, _1.q)(field)}, "id" FROM ${(0, _1.q)(t)} WHERE ${(0, _1.q)(field)} IS NOT NULL`);
228
+ }
229
+ }
230
+ for (const x of answered(plan, "relation")) {
231
+ if (x.answer !== "copy-links")
232
+ continue;
233
+ const { entity, field: target } = subject(x.id);
234
+ const from = table(entity);
235
+ const to = table(target);
236
+ const fk = R.constraints.find((c) => c.kind === "fk" && c.table === from && c.references === to);
237
+ const joinFks = A.constraints.filter((c) => c.kind === "fk" && !rTables.has(c.table));
238
+ const join = joinFks.find((c) => c.references === from &&
239
+ joinFks.some((d) => d.table === c.table && d.references === to));
240
+ const other = join &&
241
+ joinFks.find((d) => d.table === join.table && d.references === to);
242
+ if (!fk || !join || !other)
243
+ throw new Error(`Can't find where ${entity}'s links to ${target} go.`);
244
+ up.push(`INSERT INTO ${(0, _1.q)(join.table)} (${(0, _1.q)(join.columns[0])}, ${(0, _1.q)(other.columns[0])}) SELECT "id", ${(0, _1.q)(fk.columns[0])} FROM ${(0, _1.q)(from)} WHERE ${(0, _1.q)(fk.columns[0])} IS NOT NULL`);
245
+ }
246
+ for (const x of answered(plan, "backfill")) {
247
+ const { entity, field } = subject(x.id);
248
+ const value = x.answer === "sql" ? `(${x.sql})` : (0, _1.literal)(x.answer);
249
+ up.push(`UPDATE ${(0, _1.q)(table(entity))} SET ${(0, _1.q)(field)} = ${value} WHERE ${(0, _1.q)(field)} IS NULL`);
250
+ }
251
+ up.push(...setNotNull);
252
+ // Drop what is gone: columns, tables, enum types.
253
+ for (const t of R.tables) {
254
+ const now = aTables.get(t.name);
255
+ if (!now)
256
+ continue;
257
+ for (const c of t.columns)
258
+ if (!now.columns.some((x) => x.name === c.name))
259
+ up.push(`ALTER TABLE ${(0, _1.q)(t.name)} DROP COLUMN ${(0, _1.q)(c.name)}`);
260
+ }
261
+ for (const t of R.tables)
262
+ if (!aTables.has(t.name))
263
+ up.push(`DROP TABLE ${(0, _1.q)(t.name)}`);
264
+ up.push(...later);
265
+ for (const e of R.enums)
266
+ if (!A.enums.some((x) => x.name === e.name))
267
+ up.push(`DROP TYPE "public".${(0, _1.q)(e.name)}`);
268
+ // New keys and indexes, foreign keys last.
269
+ const add = (c) => changed(c, rCons.get(c.key)) && !createdInline.has(c.key);
270
+ for (const c of A.constraints)
271
+ if (c.kind !== "fk" && add(c))
272
+ up.push((0, _1.addConstraintSql)(c));
273
+ for (const i of A.indexes)
274
+ if (changed(i, rIdx.get(i.key)))
275
+ up.push((0, _1.createIndexSql)(i));
276
+ for (const c of A.constraints)
277
+ if (c.kind === "fk" && add(c))
278
+ up.push((0, _1.addConstraintSql)(c));
279
+ return { up };
280
+ }
281
+ exports.changeDdl = changeDdl;
282
+ /**
283
+ * The reverse migration: the plan from the new schema back to the deployed
284
+ * one, answered from the forward answers. Renames invert, added fields drop,
285
+ * removed enum values map back, a split rejoins, and data that moved (to a
286
+ * new table, or into many-to-many links) is copied back. Anything left
287
+ * unanswered is reported instead of guessed.
288
+ */
289
+ function downDdl(before, after, plan) {
290
+ const fwd = new Map(plan.questions.filter((x) => x.answer).map((x) => [x.id, x]));
291
+ const entityRenamedFrom = new Map(answered(plan, "removedEntity")
292
+ .filter((x) => x.answer.startsWith("rename:"))
293
+ .map((x) => [x.answer.slice("rename:".length), subject(x.id).entity]));
294
+ const forwardEntity = (name) => { var _a, _b; return (_b = (_a = [...entityRenamedFrom].find(([, old]) => old === name)) === null || _a === void 0 ? void 0 : _a[0]) !== null && _b !== void 0 ? _b : name; };
295
+ const nowTable = (entity) => (0, _1.tableName)(after.entities.find((e) => e.name === entity));
296
+ const A = (0, _1.schemaModel)((0, apsorc_parser_1.parseApsorcV2)(after));
297
+ const B = (0, _1.schemaModel)((0, apsorc_parser_1.parseApsorcV2)(before));
298
+ const answer = (x) => {
299
+ var _a;
300
+ const { entity, field, value } = subject(x.id);
301
+ if (x.kind === "removedEntity") {
302
+ const old = entityRenamedFrom.get(entity);
303
+ return { answer: old ? `rename:${old}` : "drop" };
304
+ }
305
+ if (x.kind === "removedField") {
306
+ const renamedFrom = [...fwd.values()].find((f) => f.kind === "removedField" &&
307
+ f.answer === `rename:${field}` &&
308
+ forwardEntity(subject(f.id).entity) === entity);
309
+ return {
310
+ answer: renamedFrom
311
+ ? `rename:${subject(renamedFrom.id).field}`
312
+ : "drop",
313
+ };
314
+ }
315
+ if (x.kind === "enumValue") {
316
+ const from = [...fwd.values()].find((f) => f.kind === "enumValue" &&
317
+ f.answer === value &&
318
+ subject(f.id).field === field);
319
+ return from && { answer: subject(from.id).value };
320
+ }
321
+ if (x.kind === "cast")
322
+ return { answer: "cast" };
323
+ if (x.kind === "relation")
324
+ return { answer: "drop-links" };
325
+ if (x.kind === "backfill") {
326
+ const split = fwd.get(`field:${forwardEntity(entity)}.${field}`);
327
+ if ((_a = split === null || split === void 0 ? void 0 : split.answer) === null || _a === void 0 ? void 0 : _a.startsWith("split:")) {
328
+ const parts = split.answer
329
+ .slice("split:".length)
330
+ .split(",")
331
+ .map((p) => (0, _1.q)(p));
332
+ return { answer: "sql", sql: `concat_ws(' ', ${parts.join(", ")})` };
333
+ }
334
+ }
335
+ return undefined;
336
+ };
337
+ let rev = (0, plan_1.planMigration)(after, before);
338
+ for (let round = 0; round < 3; round++) {
339
+ for (const x of rev.questions)
340
+ if (!x.answer)
341
+ Object.assign(x, answer(x));
342
+ rev = (0, plan_1.planMigration)(after, before, rev);
343
+ }
344
+ // Data the forward migration moved out of a column comes back from where it went.
345
+ const extra = [];
346
+ for (const x of answered(plan, "removedField")) {
347
+ if (!x.answer.startsWith("move:"))
348
+ continue;
349
+ const { entity, field } = subject(x.id);
350
+ const [toEntity, toField] = x.answer.slice("move:".length).split(".");
351
+ const from = nowTable(entity);
352
+ const to = nowTable(toEntity);
353
+ const fk = A.constraints.find((c) => c.kind === "fk" && c.table === to && c.references === from);
354
+ extra.push({
355
+ id: `backfill:${entity}.${field}`,
356
+ kind: "backfill",
357
+ subject: `restore ${field} from ${toEntity}`,
358
+ options: ["sql"],
359
+ answer: "sql",
360
+ sql: `SELECT ${(0, _1.q)(toField)} FROM ${(0, _1.q)(to)} x WHERE x.${(0, _1.q)(fk.columns[0])} = ${(0, _1.q)(from)}."id" ORDER BY x."id" LIMIT 1`,
361
+ });
362
+ }
363
+ for (const x of answered(plan, "relation")) {
364
+ if (x.answer !== "copy-links")
365
+ continue;
366
+ const { entity, field: target } = subject(x.id);
367
+ const from = nowTable(entity);
368
+ const to = nowTable(target);
369
+ const fk = B.constraints.find((c) => c.kind === "fk" && c.table === from && c.references === to);
370
+ const joinFks = A.constraints.filter((c) => c.kind === "fk" && !B.tables.some((t) => t.name === c.table));
371
+ const own = joinFks.find((c) => c.references === from &&
372
+ joinFks.some((d) => d.table === c.table && d.references === to));
373
+ const other = joinFks.find((d) => d.table === own.table && d.references === to);
374
+ extra.push({
375
+ id: `backfill:${entity}.${fk.columns[0]}`,
376
+ kind: "backfill",
377
+ subject: `restore ${fk.columns[0]} from ${own.table}`,
378
+ options: ["sql"],
379
+ answer: "sql",
380
+ sql: `SELECT j.${(0, _1.q)(other.columns[0])} FROM ${(0, _1.q)(own.table)} j WHERE j.${(0, _1.q)(own.columns[0])} = ${(0, _1.q)(from)}."id" LIMIT 1`,
381
+ });
382
+ }
383
+ const open = (0, plan_1.openQuestions)(rev);
384
+ if (open.length > 0)
385
+ return { irreversible: open.map((x) => `${x.id}: ${x.subject}`) };
386
+ return {
387
+ down: changeDdl(after, before, {
388
+ ...rev,
389
+ questions: [...rev.questions, ...extra],
390
+ }).up,
391
+ };
392
+ }
393
+ exports.downDdl = downDdl;
@@ -0,0 +1,80 @@
1
+ import { Entity } from "../types/entity";
2
+ import { ComputedField } from "../types/field";
3
+ import { RelationshipMap } from "../types/relationship";
4
+ export declare const constraintName: {
5
+ primaryKey: (t: string, cols: string[]) => string;
6
+ unique: (t: string, cols: string[]) => string;
7
+ relation: (t: string, cols: string[]) => string;
8
+ foreignKey: (t: string, cols: string[]) => string;
9
+ index: (t: string, cols: string[]) => string;
10
+ };
11
+ export declare const q: (name: string) => string;
12
+ export declare const literal: (v: string) => string;
13
+ export declare const tableName: (e: Entity) => string;
14
+ export declare const enumTypeName: (table: string, column: string) => string;
15
+ export interface ColumnSpec {
16
+ /** Column SQL without constraints: name, type, NOT NULL, DEFAULT. */
17
+ sql: string;
18
+ type: string;
19
+ notNull: boolean;
20
+ default?: string;
21
+ unique: boolean;
22
+ index: boolean;
23
+ }
24
+ /** One column as its entity template declares it. */
25
+ export declare function columnSpec(table: string, f: ComputedField): ColumnSpec;
26
+ export interface SchemaInput {
27
+ entities: Entity[];
28
+ relationshipMap: RelationshipMap;
29
+ }
30
+ export interface ModelColumn {
31
+ name: string;
32
+ /** The column as CREATE TABLE writes it. */
33
+ sql: string;
34
+ type: string;
35
+ notNull: boolean;
36
+ default?: string;
37
+ /** A SERIAL id, whose default is the table's id sequence. */
38
+ serial?: boolean;
39
+ }
40
+ export interface ModelTable {
41
+ name: string;
42
+ columns: ModelColumn[];
43
+ }
44
+ export interface ModelConstraint {
45
+ /** Identity independent of the name: kind, table, columns (and target). */
46
+ key: string;
47
+ name: string;
48
+ table: string;
49
+ kind: "pk" | "unique" | "fk";
50
+ columns: string[];
51
+ /** Everything after CONSTRAINT "name". */
52
+ body: string;
53
+ references?: string;
54
+ }
55
+ export interface ModelIndex {
56
+ key: string;
57
+ name: string;
58
+ table: string;
59
+ columns: string[];
60
+ unique: boolean;
61
+ }
62
+ export interface ModelEnum {
63
+ name: string;
64
+ values: string[];
65
+ }
66
+ /** Every object a schema creates, in creation order. */
67
+ export interface Model {
68
+ extensions: string[];
69
+ enums: ModelEnum[];
70
+ tables: ModelTable[];
71
+ constraints: ModelConstraint[];
72
+ indexes: ModelIndex[];
73
+ }
74
+ export declare function schemaModel(input: SchemaInput): Model;
75
+ export declare const createTypeSql: (e: ModelEnum) => string;
76
+ export declare const createIndexSql: (i: ModelIndex) => string;
77
+ export declare const addConstraintSql: (c: ModelConstraint) => string;
78
+ export declare const createTableSql: (t: ModelTable, constraints: ModelConstraint[]) => string;
79
+ /** CREATE statements for every enum type, table, index and foreign key. */
80
+ export declare function schemaDdl(input: SchemaInput): string[];