@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,309 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.schemaDdl = exports.createTableSql = exports.addConstraintSql = exports.createIndexSql = exports.createTypeSql = exports.schemaModel = exports.columnSpec = exports.enumTypeName = exports.tableName = exports.literal = exports.q = exports.constraintName = void 0;
4
+ /**
5
+ * Apso's own DDL writer: the CREATE statements for a whole .apsorc schema.
6
+ *
7
+ * Column rules mirror the TypeScript entity templates (templates/entities),
8
+ * and names follow TypeORM's DefaultNamingStrategy (a prefix plus a sha1 of
9
+ * "table_columns"). A database built from this SQL therefore matches the one
10
+ * TypeORM builds for the generated entities, and services already deployed
11
+ * see no drift. test/lib/ddl/project-parity.test.ts checks that against a
12
+ * real scaffolded service.
13
+ */
14
+ const crypto_1 = require("crypto");
15
+ const field_1 = require("../utils/field");
16
+ const casing_1 = require("../utils/casing");
17
+ const relationships_1 = require("../utils/relationships");
18
+ // TypeORM's own casing, used for join table and join column names.
19
+ // eslint-disable-next-line @typescript-eslint/no-var-requires
20
+ const typeormCase = require("typeorm/util/StringUtils");
21
+ const sha1 = (s) => (0, crypto_1.createHash)("sha1").update(s).digest("hex");
22
+ const key = (table, columns) => `${table.replace(".", "_")}_${[...columns].sort().join("_")}`;
23
+ exports.constraintName = {
24
+ primaryKey: (t, cols) => "PK_" + sha1(key(t, cols)).slice(0, 27),
25
+ unique: (t, cols) => "UQ_" + sha1(key(t, cols)).slice(0, 27),
26
+ relation: (t, cols) => "REL_" + sha1(key(t, cols)).slice(0, 26),
27
+ foreignKey: (t, cols) => "FK_" + sha1(key(t, cols)).slice(0, 27),
28
+ index: (t, cols) => "IDX_" + sha1(key(t, cols)).slice(0, 26),
29
+ };
30
+ const q = (name) => `"${name.replace(/"/g, '""')}"`;
31
+ exports.q = q;
32
+ const literal = (v) => `'${v.replace(/'/g, "''")}'`;
33
+ exports.literal = literal;
34
+ const tableName = (e) => e.table || (0, casing_1.snakeCase)(e.name);
35
+ exports.tableName = tableName;
36
+ const enumTypeName = (table, column) => `${table}_${column}_enum`;
37
+ exports.enumTypeName = enumTypeName;
38
+ const hasDefault = (f) => f.default !== null && f.default !== undefined;
39
+ // TypeORM quotes every default it is given except booleans.
40
+ const quoted = (f) => hasDefault(f) ? (0, exports.literal)(String(f.default)) : undefined;
41
+ /** One column as its entity template declares it. */
42
+ function columnSpec(table, f) {
43
+ const textLike = (type) => [
44
+ type,
45
+ quoted(f),
46
+ Boolean(f.unique),
47
+ ];
48
+ const { length: size } = f;
49
+ let type;
50
+ let def;
51
+ let unique = false;
52
+ switch (f.type) {
53
+ case "text":
54
+ case "string":
55
+ [type, def, unique] = textLike("text");
56
+ break;
57
+ case "varchar":
58
+ [type, def, unique] = textLike(`character varying(${size || 255})`);
59
+ break;
60
+ case "char":
61
+ [type, def, unique] = textLike(`character(${size || 1})`);
62
+ break;
63
+ case "enum":
64
+ type = `"public".${(0, exports.q)((0, exports.enumTypeName)(table, f.name))}`;
65
+ def = quoted(f);
66
+ break;
67
+ case "integer":
68
+ case "bigint":
69
+ case "smallint":
70
+ type = f.type;
71
+ def = quoted(f);
72
+ break;
73
+ case "decimal":
74
+ case "numeric":
75
+ type = `numeric(${f.precision || 10},${f.scale || 2})`;
76
+ def = quoted(f);
77
+ break;
78
+ case "float":
79
+ type = "numeric";
80
+ def = quoted(f);
81
+ break;
82
+ case "double":
83
+ type = "double precision";
84
+ def = quoted(f);
85
+ break;
86
+ case "boolean":
87
+ type = "boolean";
88
+ def = String(String(f.default).toLowerCase() === "true");
89
+ break;
90
+ case "uuid":
91
+ type = "uuid";
92
+ def = hasDefault(f) ? String(f.default) : undefined;
93
+ break;
94
+ case "date":
95
+ type = "date";
96
+ break;
97
+ case "timestamp":
98
+ type = "TIMESTAMP";
99
+ break;
100
+ case "timestamptz":
101
+ type = "TIMESTAMP WITH TIME ZONE";
102
+ break;
103
+ case "json":
104
+ case "jsonb":
105
+ type = "jsonb";
106
+ break;
107
+ case "json-plain":
108
+ type = "json";
109
+ break;
110
+ default:
111
+ // ponytail: spatial, range and other special templates; add each with a parity case.
112
+ throw new Error(`The DDL writer does not support field type "${f.type}" yet (${table}.${f.name}).`);
113
+ }
114
+ let sql = `${(0, exports.q)(f.name)} ${type}`;
115
+ if (!f.nullable)
116
+ sql += " NOT NULL";
117
+ if (def !== undefined)
118
+ sql += ` DEFAULT ${def}`;
119
+ return {
120
+ sql,
121
+ type,
122
+ notNull: !f.nullable,
123
+ default: def,
124
+ unique,
125
+ index: Boolean(f.index),
126
+ };
127
+ }
128
+ exports.columnSpec = columnSpec;
129
+ const cols = (c) => c.map((x) => (0, exports.q)(x)).join(", ");
130
+ const column = (name, type, notNull, def, serial) => ({
131
+ name,
132
+ sql: `${(0, exports.q)(name)} ${serial ? "SERIAL" : type}${notNull ? " NOT NULL" : ""}${def && !serial ? ` DEFAULT ${def}` : ""}`,
133
+ type,
134
+ notNull,
135
+ default: def,
136
+ serial,
137
+ });
138
+ const pkColumn = (e) => { var _a, _b, _c; return (_c = (_b = ((_a = e.fields) !== null && _a !== void 0 ? _a : []).find((f) => f.primary)) === null || _b === void 0 ? void 0 : _b.name) !== null && _c !== void 0 ? _c : "id"; };
139
+ const fkType = (target) => (target === null || target === void 0 ? void 0 : target.primaryKeyType) === "uuid" ? "uuid" : "integer";
140
+ function schemaModel(input) {
141
+ var _a, _b, _c, _d, _e;
142
+ const { entities, relationshipMap } = input;
143
+ const byName = new Map(entities.map((e) => [e.name, e]));
144
+ const model = {
145
+ extensions: [],
146
+ enums: [],
147
+ tables: [],
148
+ constraints: [],
149
+ indexes: [],
150
+ };
151
+ const entityTables = [];
152
+ const joinTables = [];
153
+ const inline = [];
154
+ const foreignKeys = [];
155
+ const index = (table, columns, unique = false) => model.indexes.push({
156
+ key: `index:${table}(${columns.join(",")})`,
157
+ name: exports.constraintName.index(table, columns),
158
+ table,
159
+ columns,
160
+ unique,
161
+ });
162
+ const unique = (table, columns, name, list = inline) => list.push({
163
+ key: `unique:${table}(${columns.join(",")})`,
164
+ name,
165
+ table,
166
+ kind: "unique",
167
+ columns,
168
+ body: `UNIQUE (${cols(columns)})`,
169
+ });
170
+ const primaryKey = (table, columns, list = inline) => list.push({
171
+ key: `pk:${table}`,
172
+ name: exports.constraintName.primaryKey(table, columns),
173
+ table,
174
+ kind: "pk",
175
+ columns,
176
+ body: `PRIMARY KEY (${cols(columns)})`,
177
+ });
178
+ const foreignKey = (table, col, target, targetCol, actions) => foreignKeys.push({
179
+ key: `fk:${table}(${col})->${target}`,
180
+ name: exports.constraintName.foreignKey(table, [col]),
181
+ table,
182
+ kind: "fk",
183
+ columns: [col],
184
+ references: target,
185
+ body: `FOREIGN KEY (${(0, exports.q)(col)}) REFERENCES ${(0, exports.q)(target)}(${(0, exports.q)(targetCol)}) ${actions}`,
186
+ });
187
+ for (const e of entities) {
188
+ const table = (0, exports.tableName)(e);
189
+ const t = { name: table, columns: [] };
190
+ const customPk = ((_a = e.fields) !== null && _a !== void 0 ? _a : []).some((f) => f.primary);
191
+ if (!customPk) {
192
+ const pkType = e.primaryKeyType || "serial";
193
+ if (pkType === "uuid") {
194
+ model.extensions.push(`CREATE EXTENSION IF NOT EXISTS "uuid-ossp"`);
195
+ t.columns.push(column("id", "uuid", true, "uuid_generate_v4()"));
196
+ }
197
+ else if (pkType === "text")
198
+ t.columns.push(column("id", "text", true));
199
+ else
200
+ t.columns.push(column("id", "integer", true, `nextval('${table}_id_seq'::regclass)`, true));
201
+ }
202
+ if (e.created_at)
203
+ t.columns.push(column("created_at", "TIMESTAMP", true, "now()"));
204
+ if (e.updated_at)
205
+ t.columns.push(column("updated_at", "TIMESTAMP", true, "now()"));
206
+ for (const f of (0, field_1.getFieldForTemplate)((_b = e.fields) !== null && _b !== void 0 ? _b : [], e.name)) {
207
+ if (!customPk && f.name === "id")
208
+ continue;
209
+ if (e.created_at && f.name === "created_at")
210
+ continue;
211
+ if (e.updated_at && f.name === "updated_at")
212
+ continue;
213
+ if (f.type === "enum" && f.values)
214
+ model.enums.push({
215
+ name: (0, exports.enumTypeName)(table, f.name),
216
+ values: [...f.values],
217
+ });
218
+ const spec = columnSpec(table, f);
219
+ t.columns.push(column(f.name, spec.type, spec.notNull, spec.default));
220
+ if (spec.unique)
221
+ unique(table, [f.name], exports.constraintName.unique(table, [f.name]));
222
+ if (spec.index)
223
+ index(table, [f.name]);
224
+ }
225
+ for (const i of (_c = e.indexes) !== null && _c !== void 0 ? _c : [])
226
+ index(table, i.fields, i.unique);
227
+ for (const u of (_d = e.uniques) !== null && _d !== void 0 ? _d : [])
228
+ unique(table, u.fields, u.name || exports.constraintName.unique(table, u.fields));
229
+ for (const r of (0, relationships_1.getRelationshipForTemplate)(e.name, relationshipMap[e.name] || [], entities)) {
230
+ const target = byName.get(r.name);
231
+ if (!target)
232
+ continue;
233
+ const targetTable = (0, exports.tableName)(target);
234
+ // The template gives OneToOne onDelete CASCADE and a nullable join column.
235
+ const oneToOne = r.type === "OneToOne";
236
+ const onDelete = r.cascadeDelete || oneToOne ? "CASCADE" : "NO ACTION";
237
+ if (r.type === "ManyToOne" || (oneToOne && r.joinTable)) {
238
+ const col = r.type === "ManyToOne"
239
+ ? r.camelCasedId
240
+ : typeormCase.camelCase(`${r.relationshipName}_id`);
241
+ t.columns.push(column(col, fkType(target), !(r.nullable || oneToOne)));
242
+ if (oneToOne)
243
+ unique(table, [col], exports.constraintName.relation(table, [col]));
244
+ if (r.type === "ManyToOne" && r.index)
245
+ index(table, [col]);
246
+ foreignKey(table, col, targetTable, pkColumn(target), `ON DELETE ${onDelete} ON UPDATE NO ACTION`);
247
+ }
248
+ if (r.type === "ManyToMany" && r.joinTable) {
249
+ const join = r.joinTableName ||
250
+ typeormCase.snakeCase(`${table}_${r.pluralizedRelationshipName}_${targetTable}`);
251
+ const ownCol = r.joinColumnName || typeormCase.camelCase(`${table}_${pkColumn(e)}`);
252
+ const otherCol = r.inverseJoinColumnName ||
253
+ typeormCase.camelCase(`${targetTable}_${pkColumn(target)}`);
254
+ joinTables.push({
255
+ name: join,
256
+ columns: [
257
+ column(ownCol, fkType(e), true),
258
+ column(otherCol, fkType(target), true),
259
+ ],
260
+ });
261
+ primaryKey(join, [ownCol, otherCol]);
262
+ index(join, [ownCol]);
263
+ index(join, [otherCol]);
264
+ // TypeORM cascades the inverse key only when no inverse relation exists.
265
+ const inverse = r.biDirectional ? "NO ACTION" : "CASCADE";
266
+ foreignKey(join, ownCol, table, pkColumn(e), "ON DELETE CASCADE ON UPDATE CASCADE");
267
+ foreignKey(join, otherCol, targetTable, pkColumn(target), `ON DELETE ${inverse} ON UPDATE ${inverse}`);
268
+ }
269
+ }
270
+ primaryKey(table, customPk
271
+ ? ((_e = e.fields) !== null && _e !== void 0 ? _e : []).filter((f) => f.primary).map((f) => f.name)
272
+ : ["id"]);
273
+ entityTables.unshift(t);
274
+ }
275
+ model.extensions = [...new Set(model.extensions)];
276
+ model.tables = [...entityTables, ...joinTables];
277
+ model.constraints = [...inline, ...foreignKeys];
278
+ return model;
279
+ }
280
+ exports.schemaModel = schemaModel;
281
+ const createTypeSql = (e) => `CREATE TYPE "public".${(0, exports.q)(e.name)} AS ENUM(${e.values
282
+ .map((v) => (0, exports.literal)(v))
283
+ .join(", ")})`;
284
+ exports.createTypeSql = createTypeSql;
285
+ const createIndexSql = (i) => `CREATE ${i.unique ? "UNIQUE " : ""}INDEX ${(0, exports.q)(i.name)} ON ${(0, exports.q)(i.table)} (${cols(i.columns)})`;
286
+ exports.createIndexSql = createIndexSql;
287
+ const addConstraintSql = (c) => `ALTER TABLE ${(0, exports.q)(c.table)} ADD CONSTRAINT ${(0, exports.q)(c.name)} ${c.body}`;
288
+ exports.addConstraintSql = addConstraintSql;
289
+ const createTableSql = (t, constraints) => `CREATE TABLE ${(0, exports.q)(t.name)} (${[
290
+ ...t.columns.map((c) => c.sql),
291
+ ...constraints
292
+ .filter((c) => c.table === t.name && c.kind !== "fk")
293
+ .map((c) => `CONSTRAINT ${(0, exports.q)(c.name)} ${c.body}`),
294
+ ].join(", ")})`;
295
+ exports.createTableSql = createTableSql;
296
+ /** CREATE statements for every enum type, table, index and foreign key. */
297
+ function schemaDdl(input) {
298
+ const m = schemaModel(input);
299
+ return [
300
+ ...m.extensions,
301
+ ...m.enums.map((e) => (0, exports.createTypeSql)(e)),
302
+ ...m.tables.map((t) => (0, exports.createTableSql)(t, m.constraints)),
303
+ ...m.indexes.map((i) => (0, exports.createIndexSql)(i)),
304
+ ...m.constraints
305
+ .filter((c) => c.kind === "fk")
306
+ .map((c) => (0, exports.addConstraintSql)(c)),
307
+ ];
308
+ }
309
+ exports.schemaDdl = schemaDdl;
@@ -0,0 +1,42 @@
1
+ import { ApsorcType } from "../apsorc-parser";
2
+ import { Question } from "../plan";
3
+ import { Finding } from "../verify/review";
4
+ import { DownDdl } from "./change";
5
+ export declare function typeormMigration(className: string, up: string[], down: DownDdl): string;
6
+ /**
7
+ * A Python string literal for any SQL. A JSON string is one (same escapes),
8
+ * where """...""" breaks on SQL that ends in a quoted identifier (#148).
9
+ */
10
+ export declare const pyString: (s: string) => string;
11
+ export declare function alembicMigration(revision: string, downRevision: string | undefined, up: string[], down: DownDdl): string;
12
+ /**
13
+ * Statements for a goose SQL file. goose splits on lines ending in ";", so a
14
+ * statement with a ";" of its own is fenced with StatementBegin/End.
15
+ */
16
+ export declare const gooseStatements: (statements: string[]) => string;
17
+ export declare function gooseMigration(up: string[], down: DownDdl): string;
18
+ /** The newest Alembic revision: the one no other revision names as its down_revision. */
19
+ export declare function alembicHead(dir: string): string | undefined;
20
+ /**
21
+ * The up and down SQL of an Alembic or goose migration, for running it on a
22
+ * database without the framework. down is undefined when it refuses to run.
23
+ */
24
+ export declare function migrationSql(file: string, source: string): {
25
+ up: string;
26
+ down?: string;
27
+ };
28
+ export interface PlannedMigration {
29
+ ok: boolean;
30
+ needed: boolean;
31
+ /** Questions still open in the plan; nothing is written while any are. */
32
+ open?: Question[];
33
+ file?: string;
34
+ planFile?: string;
35
+ findings?: Finding[];
36
+ steps: {
37
+ name: string;
38
+ ok: boolean;
39
+ output?: string;
40
+ }[];
41
+ }
42
+ export declare function writePlannedMigration(projectDir: string, before: ApsorcType, name: string, planPath: string, now?: number): PlannedMigration;
@@ -0,0 +1,222 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.writePlannedMigration = exports.migrationSql = exports.alembicHead = exports.gooseMigration = exports.gooseStatements = exports.alembicMigration = exports.pyString = exports.typeormMigration = void 0;
4
+ const tslib_1 = require("tslib");
5
+ /**
6
+ * Write a migration from an answered plan: up is Apso's change DDL, down the
7
+ * reverse, and the plan is saved beside the migration so the answers that
8
+ * produced it stay with it. The SQL is the same for every language; only the
9
+ * wrapper differs: a TypeORM class, an Alembic revision, or a goose SQL file.
10
+ */
11
+ const fs = tslib_1.__importStar(require("fs"));
12
+ const path = tslib_1.__importStar(require("path"));
13
+ const plan_1 = require("../plan");
14
+ const baseline_1 = require("../plan/baseline");
15
+ const casing_1 = require("../utils/casing");
16
+ const review_1 = require("../verify/review");
17
+ const change_1 = require("./change");
18
+ const _1 = require(".");
19
+ const template = (sql) => "`" +
20
+ sql.replace(/\\/g, "\\\\").replace(/`/g, "\\`").replace(/\${/g, "\\${") +
21
+ "`";
22
+ const queries = (statements) => statements
23
+ .map((s) => ` await queryRunner.query(${template(s)});`)
24
+ .join("\n");
25
+ function typeormMigration(className, up, down) {
26
+ const downBody = "down" in down
27
+ ? queries(down.down)
28
+ : ` throw new Error(${JSON.stringify(irreversible(down))});`;
29
+ return `import { MigrationInterface, QueryRunner } from "typeorm";
30
+
31
+ // Generated by apso migrate generate from the answered migration plan saved beside this file.
32
+ export class ${className} implements MigrationInterface {
33
+ name = '${className}'
34
+
35
+ public async up(queryRunner: QueryRunner): Promise<void> {
36
+ ${queries(up)}
37
+ }
38
+
39
+ public async down(queryRunner: QueryRunner): Promise<void> {
40
+ ${downBody}
41
+ }
42
+
43
+ }
44
+ `;
45
+ }
46
+ exports.typeormMigration = typeormMigration;
47
+ const irreversible = (down) => `This migration can't be reversed automatically: ${down.irreversible.join("; ")}`;
48
+ /**
49
+ * A Python string literal for any SQL. A JSON string is one (same escapes),
50
+ * where """...""" breaks on SQL that ends in a quoted identifier (#148).
51
+ */
52
+ const pyString = (s) => JSON.stringify(s);
53
+ exports.pyString = pyString;
54
+ const executes = (statements) => statements.map((s) => ` op.execute(${(0, exports.pyString)(s)})`).join("\n");
55
+ function alembicMigration(revision, downRevision, up, down) {
56
+ const downBody = "down" in down
57
+ ? executes(down.down) || " pass"
58
+ : ` raise NotImplementedError(${(0, exports.pyString)(irreversible(down))})`;
59
+ return `"""Generated by apso migrate generate from the answered migration plan saved beside this file."""
60
+ from alembic import op
61
+
62
+ revision = ${(0, exports.pyString)(revision)}
63
+ down_revision = ${downRevision === undefined ? "None" : (0, exports.pyString)(downRevision)}
64
+ branch_labels = None
65
+ depends_on = None
66
+
67
+
68
+ def upgrade() -> None:
69
+ ${executes(up)}
70
+
71
+
72
+ def downgrade() -> None:
73
+ ${downBody}
74
+ `;
75
+ }
76
+ exports.alembicMigration = alembicMigration;
77
+ /**
78
+ * Statements for a goose SQL file. goose splits on lines ending in ";", so a
79
+ * statement with a ";" of its own is fenced with StatementBegin/End.
80
+ */
81
+ const gooseStatements = (statements) => statements
82
+ .map((s) => s.includes(";")
83
+ ? `-- +goose StatementBegin\n${s};\n-- +goose StatementEnd`
84
+ : `${s};`)
85
+ .join("\n");
86
+ exports.gooseStatements = gooseStatements;
87
+ function gooseMigration(up, down) {
88
+ // goose has no way to refuse a down, so the SQL refuses it instead.
89
+ const downBody = "down" in down
90
+ ? (0, exports.gooseStatements)(down.down)
91
+ : (0, exports.gooseStatements)([
92
+ `DO $$ BEGIN RAISE EXCEPTION ${(0, _1.literal)(irreversible(down).replace(/%/g, "%%"))}; END $$`,
93
+ ]);
94
+ return `-- Generated by apso migrate generate from the answered migration plan saved beside this file.
95
+ -- +goose Up
96
+ ${(0, exports.gooseStatements)(up)}
97
+
98
+ -- +goose Down
99
+ ${downBody}
100
+ `;
101
+ }
102
+ exports.gooseMigration = gooseMigration;
103
+ /** The newest Alembic revision: the one no other revision names as its down_revision. */
104
+ function alembicHead(dir) {
105
+ var _a;
106
+ if (!fs.existsSync(dir))
107
+ return undefined;
108
+ const revisions = [];
109
+ const downs = new Set();
110
+ for (const f of fs
111
+ .readdirSync(dir)
112
+ .filter((f) => f.endsWith(".py"))
113
+ .sort()) {
114
+ const source = fs.readFileSync(path.join(dir, f)).toString();
115
+ const rev = source.match(/^revision\b[^\n=]*=\s*["']([^"']+)["']/m);
116
+ if (rev)
117
+ revisions.push(rev[1]);
118
+ const down = source.match(/^down_revision\b[^\n=]*=(.*)$/m);
119
+ for (const d of (_a = down === null || down === void 0 ? void 0 : down[1].matchAll(/["']([^"']+)["']/g)) !== null && _a !== void 0 ? _a : [])
120
+ downs.add(d[1]);
121
+ }
122
+ return revisions.filter((r) => !downs.has(r)).pop();
123
+ }
124
+ exports.alembicHead = alembicHead;
125
+ const calls = (body) => [...body.matchAll(/^\s*op\.execute\((".*")\)\s*$/gm)]
126
+ .map((m) => JSON.parse(m[1]))
127
+ .join(";\n");
128
+ /**
129
+ * The up and down SQL of an Alembic or goose migration, for running it on a
130
+ * database without the framework. down is undefined when it refuses to run.
131
+ */
132
+ function migrationSql(file, source) {
133
+ if (file.endsWith(".sql")) {
134
+ const [up, down = ""] = source
135
+ .split(/^-- \+goose Down\b.*$/m)
136
+ .map((s) => s.replace(/^[\S\s]*?^-- \+goose Up\b.*$/m, ""));
137
+ return { up, down: /RAISE EXCEPTION/.test(down) ? undefined : down };
138
+ }
139
+ const [upBody, downBody = ""] = source.split(/^def downgrade\b/m);
140
+ return {
141
+ up: calls(upBody),
142
+ down: /raise NotImplementedError/.test(downBody) ? undefined : calls(downBody),
143
+ };
144
+ }
145
+ exports.migrationSql = migrationSql;
146
+ function writePlannedMigration(projectDir, before, name, planPath, now = Date.now()) {
147
+ const after = JSON.parse(fs.readFileSync(path.join(projectDir, ".apsorc")).toString());
148
+ const previous = fs.existsSync(planPath)
149
+ ? JSON.parse(fs.readFileSync(planPath).toString())
150
+ : undefined;
151
+ const plan = (0, plan_1.planMigration)(before, after, previous);
152
+ fs.mkdirSync(path.dirname(planPath), { recursive: true });
153
+ fs.writeFileSync(planPath, JSON.stringify({ ...plan, before }, null, 2) + "\n");
154
+ const open = (0, plan_1.openQuestions)(plan);
155
+ if (open.length > 0)
156
+ return {
157
+ ok: false,
158
+ needed: true,
159
+ open,
160
+ steps: [
161
+ {
162
+ name: "plan",
163
+ ok: false,
164
+ output: `${open.length} question(s) in ${path.relative(projectDir, planPath)} are open. Answer them with apso migrate plan --answer, then generate again.`,
165
+ },
166
+ ],
167
+ };
168
+ let up;
169
+ let down;
170
+ try {
171
+ up = (0, change_1.changeDdl)(before, after, plan).up;
172
+ down = (0, change_1.downDdl)(before, after, plan);
173
+ }
174
+ catch (error) {
175
+ const output = error instanceof Error ? error.message : String(error);
176
+ return {
177
+ ok: false,
178
+ needed: true,
179
+ steps: [
180
+ {
181
+ name: "render",
182
+ ok: false,
183
+ output: `${output}\nTo use TypeORM's generator instead, rerun with --typeorm.`,
184
+ },
185
+ ],
186
+ };
187
+ }
188
+ if (up.length === 0)
189
+ return { ok: true, needed: false, steps: [{ name: "render", ok: true }] };
190
+ const className = `${(0, casing_1.pascalCase)(name)}Migration${now}`;
191
+ const dir = (0, baseline_1.migrationsDir)(projectDir, after);
192
+ fs.mkdirSync(dir, { recursive: true });
193
+ // goose orders by the number before "_", and its own files use
194
+ // YYYYMMDDHHMMSS, so Python and Go use that too.
195
+ const stamp = new Date(now).toISOString().replace(/\D/g, "").slice(0, 14);
196
+ const base = after.language === "python" || after.language === "go"
197
+ ? `${stamp}_${(0, casing_1.snakeCase)(name)}`
198
+ : `${now}-${name}-migration`;
199
+ const file = base + (0, baseline_1.migrationExtension)(after);
200
+ const source = after.language === "python"
201
+ ? alembicMigration(stamp, alembicHead(dir), up, down)
202
+ : after.language === "go"
203
+ ? gooseMigration(up, down)
204
+ : typeormMigration(className, up, down);
205
+ fs.writeFileSync(path.join(dir, file), source);
206
+ fs.writeFileSync(path.join(dir, `${base}.plan.json`),
207
+ // The schemas let later runs find the baseline without being told.
208
+ JSON.stringify({ ...plan, before, after }, null, 2) + "\n");
209
+ // The answers now live beside the migration; the next change starts fresh.
210
+ fs.rmSync(planPath, { force: true });
211
+ const rel = (f) => path.relative(projectDir, path.join(dir, f));
212
+ return {
213
+ ok: true,
214
+ needed: true,
215
+ file: rel(file),
216
+ planFile: rel(`${base}.plan.json`),
217
+ // The review reads TypeORM's wrapper; the SQL is the same in all three.
218
+ findings: (0, review_1.reviewMigration)(typeormMigration(className, up, down)),
219
+ steps: [{ name: "render", ok: true }],
220
+ };
221
+ }
222
+ exports.writePlannedMigration = writePlannedMigration;
@@ -55,6 +55,7 @@ export declare abstract class BaseGenerator implements LanguageGenerator {
55
55
  abstract generateIndexModule(entities: Entity[], apiType: string, opts?: {
56
56
  emitEvents?: boolean;
57
57
  http?: boolean;
58
+ auth?: AuthConfig;
58
59
  }): Promise<GeneratedFile[]>;
59
60
  /**
60
61
  * Generate guard files (auth, scope, etc.)
@@ -35,6 +35,7 @@ export declare class GoGenerator extends BaseGenerator {
35
35
  generateIndexModule(entities: Entity[], _apiType: string, opts?: {
36
36
  emitEvents?: boolean;
37
37
  http?: boolean;
38
+ auth?: AuthConfig;
38
39
  }): Promise<GeneratedFile[]>;
39
40
  /**
40
41
  * Emits a schema-derived manifest of the entities that opted in to
@@ -54,6 +55,6 @@ export declare class GoGenerator extends BaseGenerator {
54
55
  generateDomainEvents(entities: Entity[], _apiType?: string, opts?: {
55
56
  emitEvents?: boolean;
56
57
  }): Promise<GeneratedFile[]>;
57
- generateGuards(_entities: Entity[], auth?: AuthConfig): Promise<GeneratedFile[]>;
58
+ generateGuards(entities: Entity[], auth?: AuthConfig): Promise<GeneratedFile[]>;
58
59
  generateQueryUtils(_entities: Entity[], _apiType: string): Promise<GeneratedFile[]>;
59
60
  }
@@ -5,10 +5,13 @@ const tslib_1 = require("tslib");
5
5
  const path = tslib_1.__importStar(require("path"));
6
6
  const Eta = tslib_1.__importStar(require("eta"));
7
7
  const base_1 = require("./base");
8
+ const types_1 = require("../types");
9
+ const guards_1 = require("../guards");
8
10
  const field_1 = require("../utils/field");
9
11
  const relationships_1 = require("../utils/relationships");
10
12
  const casing_1 = require("../utils/casing");
11
13
  const events_1 = require("../events");
14
+ const migration_file_1 = require("../ddl/migration-file");
12
15
  const pluralize_1 = tslib_1.__importDefault(require("pluralize"));
13
16
  /**
14
17
  * Go/Gin generator that produces GORM models, DTOs, Gin handlers,
@@ -127,6 +130,8 @@ class GoGenerator extends base_1.BaseGenerator {
127
130
  goType: this.mapFieldToGoType(field),
128
131
  pascalName: (0, casing_1.pascalCase)(field.name),
129
132
  snakeName: (0, casing_1.snakeCase)(field.name),
133
+ // The enum type the enums package declares (EntityFieldEnum).
134
+ enumType: field.type === "enum" ? field.dataType : undefined,
130
135
  }));
131
136
  }
132
137
  async generateEntity(options) {
@@ -293,8 +298,8 @@ class GoGenerator extends base_1.BaseGenerator {
293
298
  .slice(0, 14);
294
299
  const name = migrationName || "auto";
295
300
  const filename = `${timestamp}_${name}.sql`;
296
- const upStatements = upSql.map((sql) => `${sql};`).join("\n");
297
- const downStatements = (downSql || []).map((sql) => `${sql};`).join("\n");
301
+ const upStatements = (0, migration_file_1.gooseStatements)(upSql);
302
+ const downStatements = (0, migration_file_1.gooseStatements)(downSql || []);
298
303
  const content = `-- +goose Up
299
304
  ${upStatements}
300
305
 
@@ -356,6 +361,7 @@ ${downStatements}
356
361
  });
357
362
  const content = await this.renderTemplate("./index-module", {
358
363
  entities: entitiesData,
364
+ hasAuth: Boolean(opts === null || opts === void 0 ? void 0 : opts.auth),
359
365
  });
360
366
  // Generate model registry for AutoMigrate support
361
367
  const modelRefs = entities
@@ -414,10 +420,35 @@ ${modelRefs}
414
420
  },
415
421
  ];
416
422
  }
417
- async generateGuards(_entities, auth) {
418
- const content = await this.renderTemplate("./middleware/auth", {
419
- auth,
420
- });
423
+ async generateGuards(entities, auth) {
424
+ if (!auth)
425
+ return [];
426
+ if ((0, guards_1.hasScopedEntities)(entities))
427
+ console.warn("[apso] scopeBy is enforced in TypeScript services only; this Go service authenticates requests but does not filter rows by tenant");
428
+ let data;
429
+ if ((0, types_1.isJwtAuth)(auth)) {
430
+ data = { jwt: (0, types_1.resolveJwtAuth)(auth) };
431
+ }
432
+ else if ((0, types_1.isApiKeyAuth)(auth)) {
433
+ const apiKey = (0, guards_1.apiKeyTemplateData)((0, types_1.resolveApiKeyAuth)(auth), entities);
434
+ const entity = entities.find((e) => e.name === apiKey.apiKeyEntity);
435
+ // gorm's default naming: snake_case of the Go field name.
436
+ const column = (name) => (0, casing_1.snakeCase)(name);
437
+ data = {
438
+ apiKey: {
439
+ ...apiKey,
440
+ goModel: (0, casing_1.pascalCase)(entity.name),
441
+ keyColumn: column(apiKey.keyField),
442
+ expiresColumn: column("expiresAt"),
443
+ workspaceColumn: column(apiKey.workspaceField),
444
+ roleColumn: apiKey.roleField ? column(apiKey.roleField) : undefined,
445
+ },
446
+ };
447
+ }
448
+ else {
449
+ throw new Error(`auth.provider "${auth.provider}" is generated for TypeScript services only. For Go, use "jwt" (Better Auth issues JWTs with its JWT plugin) or "api-key".`);
450
+ }
451
+ const content = await this.renderTemplate("./middleware/auth", data);
421
452
  return [
422
453
  {
423
454
  path: "middleware/auth.go",