@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
@@ -16,7 +16,9 @@ export declare class PythonGenerator extends BaseGenerator {
16
16
  generateModule(_options: EntityGenerationOptions): Promise<GeneratedFile[]>;
17
17
  generateMigration(options: MigrationGenerationOptions): Promise<GeneratedFile[]>;
18
18
  generateEnums(entities: Entity[], _apiType: string): Promise<GeneratedFile[]>;
19
- generateIndexModule(entities: Entity[], _apiType: string): Promise<GeneratedFile[]>;
19
+ generateIndexModule(entities: Entity[], _apiType: string, opts?: {
20
+ auth?: AuthConfig;
21
+ }): Promise<GeneratedFile[]>;
20
22
  /**
21
23
  * Emits a schema-derived manifest of the entities that opted in to
22
24
  * domain-event emission (`.apsorc` `emitEvents`), mirroring the TypeScript
@@ -35,6 +37,6 @@ export declare class PythonGenerator extends BaseGenerator {
35
37
  generateDomainEvents(entities: Entity[], _apiType?: string, opts?: {
36
38
  emitEvents?: boolean;
37
39
  }): Promise<GeneratedFile[]>;
38
- generateGuards(_entities: Entity[], _auth?: AuthConfig): Promise<GeneratedFile[]>;
40
+ generateGuards(entities: Entity[], auth?: AuthConfig): Promise<GeneratedFile[]>;
39
41
  generateQueryUtils(_entities: Entity[], _apiType: string): Promise<GeneratedFile[]>;
40
42
  }
@@ -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");
8
9
  const field_1 = require("../utils/field");
9
10
  const relationships_1 = require("../utils/relationships");
10
11
  const casing_1 = require("../utils/casing");
11
12
  const events_1 = require("../events");
13
+ const guards_1 = require("../guards");
14
+ const migration_file_1 = require("../ddl/migration-file");
12
15
  const pluralize_1 = tslib_1.__importDefault(require("pluralize"));
13
16
  /**
14
17
  * Python/FastAPI generator that produces SQLAlchemy models, Pydantic schemas,
@@ -225,10 +228,10 @@ class PythonGenerator extends base_1.BaseGenerator {
225
228
  const name = migrationName || "auto";
226
229
  const filename = `${timestamp}_${name}.py`;
227
230
  const upStatements = upSql
228
- .map((sql) => ` op.execute("""${sql}""")`)
231
+ .map((sql) => ` op.execute(${(0, migration_file_1.pyString)(sql)})`)
229
232
  .join("\n");
230
233
  const downStatements = (downSql || [])
231
- .map((sql) => ` op.execute("""${sql}""")`)
234
+ .map((sql) => ` op.execute(${(0, migration_file_1.pyString)(sql)})`)
232
235
  .join("\n");
233
236
  const content = `"""${name}"""
234
237
  from alembic import op
@@ -278,8 +281,11 @@ ${downStatements || " pass"}
278
281
  },
279
282
  ];
280
283
  }
281
- async generateIndexModule(entities, _apiType) {
282
- const content = await this.renderTemplate("./index-module", { entities });
284
+ async generateIndexModule(entities, _apiType, opts) {
285
+ const content = await this.renderTemplate("./index-module", {
286
+ entities,
287
+ hasAuth: Boolean(opts === null || opts === void 0 ? void 0 : opts.auth),
288
+ });
283
289
  return [
284
290
  {
285
291
  path: "__init__.py",
@@ -319,10 +325,27 @@ ${downStatements || " pass"}
319
325
  },
320
326
  ];
321
327
  }
322
- async generateGuards(_entities, _auth) {
323
- // Python auth/guards would be implemented differently (e.g., FastAPI dependencies)
324
- // This is a placeholder for future implementation
325
- return [];
328
+ async generateGuards(entities, auth) {
329
+ if (!auth)
330
+ return [];
331
+ if ((0, guards_1.hasScopedEntities)(entities))
332
+ console.warn("[apso] scopeBy is enforced in TypeScript services only; this Python service authenticates requests but does not filter rows by tenant");
333
+ let data;
334
+ if ((0, types_1.isJwtAuth)(auth)) {
335
+ data = { jwt: (0, types_1.resolveJwtAuth)(auth) };
336
+ }
337
+ else if ((0, types_1.isApiKeyAuth)(auth)) {
338
+ const apiKey = (0, guards_1.apiKeyTemplateData)((0, types_1.resolveApiKeyAuth)(auth), entities);
339
+ data = {
340
+ apiKey: { ...apiKey, pyModule: apiKey.apiKeyEntity.toLowerCase() },
341
+ };
342
+ }
343
+ else {
344
+ throw new Error(`auth.provider "${auth.provider}" is generated for TypeScript services only. For Python, use "jwt" (Better Auth issues JWTs with its JWT plugin) or "api-key".`);
345
+ }
346
+ return [
347
+ { path: "auth.py", content: await this.renderTemplate("./auth", data) },
348
+ ];
326
349
  }
327
350
  async generateQueryUtils(_entities, _apiType) {
328
351
  const queryContent = await this.renderTemplate("./utils/query", {});
@@ -16,8 +16,9 @@ export declare class TypeScriptGenerator extends BaseGenerator {
16
16
  generateModule(options: EntityGenerationOptions): Promise<GeneratedFile[]>;
17
17
  generateMigration(_options: MigrationGenerationOptions): Promise<GeneratedFile[]>;
18
18
  generateEnums(entities: Entity[], apiType: string): Promise<GeneratedFile[]>;
19
- generateIndexModule(entities: Entity[], apiType: string, _opts?: {
19
+ generateIndexModule(entities: Entity[], apiType: string, opts?: {
20
20
  emitEvents?: boolean;
21
+ auth?: AuthConfig;
21
22
  }): Promise<GeneratedFile[]>;
22
23
  generateGuards(entities: Entity[], auth?: AuthConfig): Promise<GeneratedFile[]>;
23
24
  /**
@@ -109,7 +109,8 @@ class TypeScriptGenerator extends base_1.BaseGenerator {
109
109
  ];
110
110
  }
111
111
  async generateController(options) {
112
- const { entity, relationships, relationshipMap } = options;
112
+ var _a, _b;
113
+ const { entity, relationships, relationshipMap, auth } = options;
113
114
  const { name: entityName } = entity;
114
115
  const relationshipsForTemplate = (0, relationships_1.getRelationshipForTemplate)(entityName, relationships);
115
116
  // Handle nested relationships for joins
@@ -135,6 +136,12 @@ class TypeScriptGenerator extends base_1.BaseGenerator {
135
136
  primaryKeyType,
136
137
  associations: [...new Set(relationshipsForTemplate)],
137
138
  nestedJoinsFile,
139
+ // Direct scopeBy fields @CrudAuth adds to every query once a caller is
140
+ // authenticated. Nested scopes (task.workspaceId) are checked per row by
141
+ // ScopeGuard only.
142
+ crudAuth: auth
143
+ ? ((_b = (_a = (0, guards_1.getScopedEntities)([entity])[0]) === null || _a === void 0 ? void 0 : _a.scopes) !== null && _b !== void 0 ? _b : []).filter((s) => s.direct)
144
+ : [],
138
145
  };
139
146
  const controllerContent = await this.renderTemplate("./rest/controller-rest", data);
140
147
  const specContent = await this.renderTemplate("./rest/controller-rest-spec", data);
@@ -307,8 +314,13 @@ class TypeScriptGenerator extends base_1.BaseGenerator {
307
314
  },
308
315
  ];
309
316
  }
310
- async generateIndexModule(entities, apiType, _opts) {
311
- const content = await this.renderTemplate(`./${apiType}/index-module-${apiType}`, { entities });
317
+ async generateIndexModule(entities, apiType, opts) {
318
+ // generateGuards emits GuardsModule whenever auth or scopeBy is configured;
319
+ // listing it here is what turns its global guards on.
320
+ const content = await this.renderTemplate(`./${apiType}/index-module-${apiType}`, {
321
+ entities,
322
+ hasGuards: Boolean(opts === null || opts === void 0 ? void 0 : opts.auth) || (0, guards_1.hasScopedEntities)(entities),
323
+ });
312
324
  return [
313
325
  {
314
326
  path: "index.ts",
@@ -319,20 +331,45 @@ class TypeScriptGenerator extends base_1.BaseGenerator {
319
331
  async generateGuards(entities, auth) {
320
332
  const hasScopes = (0, guards_1.hasScopedEntities)(entities);
321
333
  const normalizedAuth = this.normalizeAuthConfig(auth);
322
- if (!hasScopes && !normalizedAuth) {
334
+ const jwt = auth && (0, auth_1.isJwtAuth)(auth) ? (0, auth_1.resolveJwtAuth)(auth) : undefined;
335
+ const apiKey = auth && (0, auth_1.isApiKeyAuth)(auth)
336
+ ? (0, guards_1.apiKeyTemplateData)((0, auth_1.resolveApiKeyAuth)(auth), entities)
337
+ : undefined;
338
+ const hasAuth = Boolean(normalizedAuth || jwt || apiKey);
339
+ if (!hasScopes && !hasAuth) {
323
340
  return [];
324
341
  }
325
342
  const scopedEntities = (0, guards_1.getScopedEntities)(entities);
326
343
  const files = [];
344
+ const authEntities = normalizedAuth
345
+ ? [
346
+ normalizedAuth.sessionEntity,
347
+ normalizedAuth.userEntity,
348
+ ...(normalizedAuth.accountUserEntity
349
+ ? [normalizedAuth.accountUserEntity]
350
+ : []),
351
+ ]
352
+ : apiKey
353
+ ? [apiKey.apiKeyEntity]
354
+ : [];
327
355
  const templateData = {
328
356
  scopedEntities,
329
357
  authConfig: normalizedAuth,
358
+ hasAuth,
359
+ authProvider: auth === null || auth === void 0 ? void 0 : auth.provider,
360
+ authEntities,
361
+ jwt,
362
+ apiKey,
330
363
  generatedAt: new Date().toISOString(),
331
364
  generatedBy: "Apso CLI",
332
365
  };
333
366
  // Generate auth.guard.ts if auth is configured
334
- if (normalizedAuth) {
335
- const authGuardContent = await this.renderTemplate("./guards/auth.guard.eta", templateData);
367
+ if (hasAuth) {
368
+ const authGuardContent = await this.renderTemplate(jwt
369
+ ? "./guards/jwt-auth.guard.eta"
370
+ : apiKey
371
+ ? "./guards/api-key-auth.guard.eta"
372
+ : "./guards/auth.guard.eta", templateData);
336
373
  files.push({
337
374
  path: "guards/auth.guard.ts",
338
375
  content: authGuardContent,
@@ -1,4 +1,4 @@
1
- import { Entity } from "./types";
1
+ import { Entity, ResolvedApiKeyAuth } from "./types";
2
2
  /**
3
3
  * Represents a scope field configuration for template rendering
4
4
  */
@@ -28,3 +28,13 @@ export declare function getScopedEntities(entities: Entity[]): ScopedEntityConfi
28
28
  * Checks if any entities have scopeBy configured.
29
29
  */
30
30
  export declare function hasScopedEntities(entities: Entity[]): boolean;
31
+ /** API key settings plus what the key entity offers, as the guard templates use them. */
32
+ export interface ApiKeyTemplateData extends ResolvedApiKeyAuth {
33
+ hasStatus: boolean;
34
+ hasExpiresAt: boolean;
35
+ }
36
+ /**
37
+ * Check the API key entity exists and has the hash field, and note whether it
38
+ * has `status` and `expiresAt`, which the guard then enforces.
39
+ */
40
+ export declare function apiKeyTemplateData(apiKey: ResolvedApiKeyAuth, entities: Entity[]): ApiKeyTemplateData;
@@ -1,6 +1,6 @@
1
1
  "use strict";
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
- exports.hasScopedEntities = exports.getScopedEntities = void 0;
3
+ exports.apiKeyTemplateData = exports.hasScopedEntities = exports.getScopedEntities = void 0;
4
4
  const tslib_1 = require("tslib");
5
5
  const pluralize_1 = tslib_1.__importDefault(require("pluralize"));
6
6
  /**
@@ -62,3 +62,22 @@ function hasScopedEntities(entities) {
62
62
  return entities.some((entity) => entity.scopeBy);
63
63
  }
64
64
  exports.hasScopedEntities = hasScopedEntities;
65
+ /**
66
+ * Check the API key entity exists and has the hash field, and note whether it
67
+ * has `status` and `expiresAt`, which the guard then enforces.
68
+ */
69
+ function apiKeyTemplateData(apiKey, entities) {
70
+ var _a;
71
+ const entity = entities.find((e) => e.name === apiKey.apiKeyEntity);
72
+ if (!entity)
73
+ throw new Error(`auth.apiKeyEntity "${apiKey.apiKeyEntity}" is not an entity in .apsorc; add it with a "${apiKey.keyField}" text field`);
74
+ const fields = new Set(((_a = entity.fields) !== null && _a !== void 0 ? _a : []).map((f) => f.name));
75
+ if (!fields.has(apiKey.keyField))
76
+ throw new Error(`Entity ${apiKey.apiKeyEntity} needs a "${apiKey.keyField}" text field holding each key's hex SHA-256 (auth.keyField)`);
77
+ return {
78
+ ...apiKey,
79
+ hasStatus: fields.has("status"),
80
+ hasExpiresAt: fields.has("expiresAt"),
81
+ };
82
+ }
83
+ exports.apiKeyTemplateData = apiKeyTemplateData;
@@ -0,0 +1,22 @@
1
+ import { ApsorcType } from "../apsorc-parser";
2
+ export interface SavedPlan {
3
+ before: ApsorcType;
4
+ after: ApsorcType;
5
+ }
6
+ export interface Baseline {
7
+ schema: ApsorcType;
8
+ /** Where the schema came from, for messages. */
9
+ source: string;
10
+ }
11
+ /** Where each language's migrations live, and the extension of one. */
12
+ export declare const migrationsDir: (projectDir: string, apsorc?: ApsorcType) => string;
13
+ export declare const migrationExtension: (apsorc?: ApsorcType) => ".ts" | ".py" | ".sql";
14
+ /** The newest migration with a saved plan, by its timestamp prefix. */
15
+ export declare function latestPlannedMigration(projectDir: string, apsorc?: ApsorcType): {
16
+ file: string;
17
+ planFile: string;
18
+ saved: SavedPlan;
19
+ } | undefined;
20
+ export declare function resolveBaseline(projectDir: string, apsorc: ApsorcType, explicit?: string, planPath?: string): Baseline;
21
+ /** A baseline as a file, for steps that scaffold it into a project copy. */
22
+ export declare function baselineFile(baseline: Baseline): string;
@@ -0,0 +1,100 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.baselineFile = exports.resolveBaseline = exports.latestPlannedMigration = exports.migrationExtension = exports.migrationsDir = void 0;
4
+ const tslib_1 = require("tslib");
5
+ /**
6
+ * Find the deployed schema without being told. Every migration
7
+ * `apso migrate generate` writes is committed with its plan, and the plan
8
+ * records the schema before and after it, so the newest one says what the
9
+ * database looks like once the migrations run. A service whose migrations
10
+ * predate this falls back to the .apsorc from the last commit that touched
11
+ * its migrations.
12
+ */
13
+ const child_process_1 = require("child_process");
14
+ const fs = tslib_1.__importStar(require("fs"));
15
+ const os = tslib_1.__importStar(require("os"));
16
+ const path = tslib_1.__importStar(require("path"));
17
+ /** Where each language's migrations live, and the extension of one. */
18
+ const migrationsDir = (projectDir, apsorc) => (apsorc === null || apsorc === void 0 ? void 0 : apsorc.language) === "python"
19
+ ? path.join(projectDir, "alembic", "versions")
20
+ : (apsorc === null || apsorc === void 0 ? void 0 : apsorc.language) === "go"
21
+ ? path.join(projectDir, "migrations")
22
+ : path.join(projectDir, (apsorc === null || apsorc === void 0 ? void 0 : apsorc.rootFolder) || "src", "migrations");
23
+ exports.migrationsDir = migrationsDir;
24
+ const migrationExtension = (apsorc) => (apsorc === null || apsorc === void 0 ? void 0 : apsorc.language) === "python"
25
+ ? ".py"
26
+ : (apsorc === null || apsorc === void 0 ? void 0 : apsorc.language) === "go"
27
+ ? ".sql"
28
+ : ".ts";
29
+ exports.migrationExtension = migrationExtension;
30
+ /** The newest migration with a saved plan, by its timestamp prefix. */
31
+ function latestPlannedMigration(projectDir, apsorc) {
32
+ const dir = (0, exports.migrationsDir)(projectDir, apsorc);
33
+ if (!fs.existsSync(dir))
34
+ return undefined;
35
+ const plans = fs
36
+ .readdirSync(dir)
37
+ .filter((f) => /^\d+[_-].+\.plan\.json$/.test(f))
38
+ .sort((a, b) => Number(b.split(/[_-]/)[0]) - Number(a.split(/[_-]/)[0]));
39
+ for (const planFile of plans) {
40
+ const saved = JSON.parse(fs.readFileSync(path.join(dir, planFile)).toString());
41
+ if (!saved.before || !saved.after)
42
+ continue;
43
+ const file = planFile.replace(/\.plan\.json$/, (0, exports.migrationExtension)(apsorc));
44
+ return {
45
+ file: path.relative(projectDir, path.join(dir, file)),
46
+ planFile: path.relative(projectDir, path.join(dir, planFile)),
47
+ saved,
48
+ };
49
+ }
50
+ return undefined;
51
+ }
52
+ exports.latestPlannedMigration = latestPlannedMigration;
53
+ const git = (cwd, args) => (0, child_process_1.execFileSync)("git", args, {
54
+ cwd,
55
+ encoding: "utf8",
56
+ stdio: ["ignore", "pipe", "ignore"],
57
+ });
58
+ function resolveBaseline(projectDir, apsorc, explicit, planPath) {
59
+ if (explicit)
60
+ return {
61
+ schema: JSON.parse(fs.readFileSync(path.resolve(explicit)).toString()),
62
+ source: explicit,
63
+ };
64
+ // A plan in progress remembers the baseline it was started from.
65
+ if (planPath && fs.existsSync(planPath)) {
66
+ const working = JSON.parse(fs.readFileSync(planPath).toString());
67
+ if (working.before)
68
+ return {
69
+ schema: working.before,
70
+ source: path.relative(projectDir, planPath),
71
+ };
72
+ }
73
+ const latest = latestPlannedMigration(projectDir, apsorc);
74
+ if (latest)
75
+ return { schema: latest.saved.after, source: latest.planFile };
76
+ try {
77
+ const dir = path.relative(projectDir, (0, exports.migrationsDir)(projectDir, apsorc));
78
+ const sha = git(projectDir, ["log", "-1", "--format=%H", "--", dir]).trim();
79
+ if (sha) {
80
+ // "<sha>:./.apsorc" is relative to cwd, so symlinked paths don't matter.
81
+ const schema = JSON.parse(git(projectDir, ["show", `${sha}:./.apsorc`]));
82
+ return {
83
+ schema,
84
+ source: `.apsorc at ${sha.slice(0, 7)}, the last commit that changed ${dir}`,
85
+ };
86
+ }
87
+ }
88
+ catch {
89
+ // No git, no history for the migrations, or no .apsorc in that commit.
90
+ }
91
+ throw new Error("Can't tell which schema is deployed: no migration from apso migrate generate yet, and no git history for the migrations folder. Pass the deployed .apsorc once with --baseline; later runs find it on their own.");
92
+ }
93
+ exports.resolveBaseline = resolveBaseline;
94
+ /** A baseline as a file, for steps that scaffold it into a project copy. */
95
+ function baselineFile(baseline) {
96
+ const file = path.join(fs.mkdtempSync(path.join(os.tmpdir(), "apso-baseline-")), ".apsorc");
97
+ fs.writeFileSync(file, JSON.stringify(baseline.schema, null, 2));
98
+ return file;
99
+ }
100
+ exports.baselineFile = baselineFile;
@@ -0,0 +1,52 @@
1
+ interface PlanField {
2
+ name: string;
3
+ type: string;
4
+ values?: string[];
5
+ nullable?: boolean;
6
+ default?: string | null;
7
+ }
8
+ interface PlanEntity {
9
+ name: string;
10
+ fields?: PlanField[];
11
+ }
12
+ interface PlanRelationship {
13
+ from: string;
14
+ to: string;
15
+ type: string;
16
+ }
17
+ export interface PlanSchema {
18
+ entities: PlanEntity[];
19
+ relationships?: PlanRelationship[];
20
+ }
21
+ export type QuestionKind = "removedField" | "removedEntity" | "enumValue" | "backfill" | "cast" | "relation";
22
+ export interface Question {
23
+ id: string;
24
+ kind: QuestionKind;
25
+ /** What changed, in words, for the agent and the person reading the plan. */
26
+ subject: string;
27
+ /** Allowed answers. "sql" and "split:..." also need `sql`. */
28
+ options: string[];
29
+ /** Set only when exactly one reading fits; still needs an answer. */
30
+ suggested?: string;
31
+ answer?: string;
32
+ /** A SQL expression, or one per target field for a split. */
33
+ sql?: string | Record<string, string>;
34
+ }
35
+ export interface Plan {
36
+ version: 1;
37
+ from: string;
38
+ to: string;
39
+ questions: Question[];
40
+ }
41
+ export declare const hashSchema: (s: PlanSchema) => string;
42
+ /** Every answer this question will accept is valid; anything else stays open. */
43
+ export declare function isAnswered(q: Question): boolean;
44
+ export declare const openQuestions: (plan: Plan) => Question[];
45
+ export declare function planMigration(before: PlanSchema, after: PlanSchema, previous?: Plan): Plan;
46
+ /**
47
+ * Plan with --answer values (<question id>=<option>) applied on top of the
48
+ * plan in progress. Throws a message for the user when an answer is malformed,
49
+ * names no question, or is not one of its options.
50
+ */
51
+ export declare function planWithAnswers(before: PlanSchema, after: PlanSchema, previous: Plan | undefined, answers: string[]): Plan;
52
+ export {};