rastack 0.0.49 → 0.0.51

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 (68) hide show
  1. package/CHANGELOG.md +4 -0
  2. package/components/auto-form/AutoForm.tsx +13 -0
  3. package/components/auto-form/use-auto-form.ts +28 -0
  4. package/components/types.ts +8 -0
  5. package/dist/admin.js +13 -13
  6. package/dist/compile/analyze.d.ts +20 -0
  7. package/dist/compile/analyze.js +60 -49
  8. package/dist/compile/entities.d.ts +19 -0
  9. package/dist/compile/entities.js +87 -13
  10. package/dist/compile/index.d.ts +23 -4
  11. package/dist/compile/index.js +86 -7
  12. package/dist/compile/model.d.ts +38 -0
  13. package/dist/compile/openapi.d.ts +9 -0
  14. package/dist/compile/openapi.js +14 -0
  15. package/dist/define/index.d.ts +162 -21
  16. package/dist/define/index.js +28 -22
  17. package/dist/define/manifest.d.ts +64 -0
  18. package/dist/define/manifest.js +250 -0
  19. package/dist/import/tabular.d.ts +8 -2
  20. package/dist/import/tabular.js +1 -1
  21. package/dist/plugin/core.d.ts +108 -0
  22. package/dist/plugin/core.js +198 -0
  23. package/dist/plugin/index.d.ts +112 -0
  24. package/dist/plugin/index.js +203 -0
  25. package/dist/rastack-import.js +4 -1
  26. package/dist/validate/adapters.js +2 -0
  27. package/dist/validate/index.d.ts +1 -0
  28. package/dist/validate/index.js +1 -0
  29. package/dist/validate/machine.d.ts +23 -2
  30. package/dist/validate/machine.js +35 -2
  31. package/dist/validate/transitions.d.ts +86 -0
  32. package/dist/validate/transitions.js +199 -0
  33. package/dist/wasm/rastack_wasm.js +1 -1
  34. package/dist/wasm/rastack_wasm_bg.wasm +0 -0
  35. package/hooks/data.ts +221 -0
  36. package/hooks/entity.ts +228 -0
  37. package/hooks/form/entity-form.ts +358 -0
  38. package/hooks/form/form.ts +8 -1
  39. package/hooks/form/index.ts +7 -1
  40. package/hooks/index.ts +4 -0
  41. package/hooks/manifest.ts +77 -0
  42. package/hooks/registry.ts +56 -0
  43. package/package.json +1 -1
  44. package/plugin.ts +8 -0
  45. package/provider/provider.tsx +26 -5
  46. package/provider/types.ts +15 -3
  47. package/src/compile/analyze.ts +74 -45
  48. package/src/compile/entities.ts +111 -11
  49. package/src/compile/index.ts +108 -11
  50. package/src/compile/model.ts +40 -0
  51. package/src/compile/openapi.ts +13 -1
  52. package/src/define/index.ts +233 -29
  53. package/src/define/manifest.ts +278 -0
  54. package/src/import/tabular.ts +9 -2
  55. package/src/plugin/core.ts +236 -0
  56. package/src/plugin/index.ts +243 -0
  57. package/src/rastack-import.ts +4 -1
  58. package/src/validate/adapters.ts +1 -0
  59. package/src/validate/index.ts +1 -0
  60. package/src/validate/machine.ts +55 -3
  61. package/src/validate/transitions.ts +232 -0
  62. package/test/components.spec.ts +22 -0
  63. package/test/plugin.spec.ts +315 -0
  64. package/test/runtime-manifest.spec.ts +309 -0
  65. package/test/transitions.spec.ts +372 -0
  66. package/test/typed-hooks.spec.ts +412 -0
  67. package/wasm/rastack_wasm.js +1 -1
  68. package/wasm/rastack_wasm_bg.wasm +0 -0
@@ -1,5 +1,7 @@
1
1
  import * as ts from "typescript";
2
2
  import { ResourceModel } from "./model";
3
+ import type { EntityTypeExport } from "./entities";
4
+ export { applyTransitions } from "../define/manifest";
3
5
  /**
4
6
  * Statically analyse resource definitions and produce the canonical model.
5
7
  *
@@ -10,3 +12,21 @@ import { ResourceModel } from "./model";
10
12
  * the value — the relation is read straight out of the type system.
11
13
  */
12
14
  export declare function analyze(program: ts.Program, fileNames: string[]): ResourceModel[];
15
+ /**
16
+ * Analyse the DSL surface, also collecting where each `resource()` const
17
+ * lives — `export const Flight = resource(…)` registers in the emitted
18
+ * `rastack-env.d.ts` as `RowOf<typeof Flight>`, so string references
19
+ * (`useData("airports.flight")`) infer rows for DSL resources too.
20
+ */
21
+ export declare function analyzeDsl(program: ts.Program, fileNames: string[]): {
22
+ resources: ResourceModel[];
23
+ types: EntityTypeExport[];
24
+ };
25
+ /**
26
+ * Evaluate a *static literal* AST node (string/number/bool/null/array/object)
27
+ * into a JS value. Anything non-literal (a function, an identifier) yields
28
+ * `undefined` — the options and constraint blocks are always literals.
29
+ * Exported for the class-entity analyser, which reads `static transitions`
30
+ * blocks with the same evaluator.
31
+ */
32
+ export declare function literalToValue(node: ts.Expression | undefined): any;
@@ -33,9 +33,20 @@ var __importStar = (this && this.__importStar) || (function () {
33
33
  };
34
34
  })();
35
35
  Object.defineProperty(exports, "__esModule", { value: true });
36
+ exports.applyTransitions = void 0;
36
37
  exports.analyze = analyze;
38
+ exports.analyzeDsl = analyzeDsl;
39
+ exports.literalToValue = literalToValue;
40
+ const path = __importStar(require("path"));
37
41
  const ts = __importStar(require("typescript"));
38
42
  const program_1 = require("./program");
43
+ const manifest_1 = require("../define/manifest");
44
+ // The option normalisers live beside the runtime manifest builder
45
+ // (`define/manifest.ts`) and are shared with it, so a manifest built from
46
+ // runtime `resource()` values and one compiled from the same source can
47
+ // never disagree. Re-exported here for existing importers.
48
+ var manifest_2 = require("../define/manifest");
49
+ Object.defineProperty(exports, "applyTransitions", { enumerable: true, get: function () { return manifest_2.applyTransitions; } });
39
50
  /**
40
51
  * Statically analyse resource definitions and produce the canonical model.
41
52
  *
@@ -46,21 +57,54 @@ const program_1 = require("./program");
46
57
  * the value — the relation is read straight out of the type system.
47
58
  */
48
59
  function analyze(program, fileNames) {
60
+ return analyzeDsl(program, fileNames).resources;
61
+ }
62
+ /**
63
+ * Analyse the DSL surface, also collecting where each `resource()` const
64
+ * lives — `export const Flight = resource(…)` registers in the emitted
65
+ * `rastack-env.d.ts` as `RowOf<typeof Flight>`, so string references
66
+ * (`useData("airports.flight")`) infer rows for DSL resources too.
67
+ */
68
+ function analyzeDsl(program, fileNames) {
49
69
  const checker = program.getTypeChecker();
50
70
  const resources = [];
71
+ const types = [];
51
72
  for (const sourceFile of (0, program_1.resourceSourceFiles)(program, fileNames)) {
52
73
  ts.forEachChild(sourceFile, function walk(node) {
53
74
  if (isResourceCall(node)) {
54
75
  const model = analyzeResourceCall(node, checker);
55
- if (model)
76
+ if (model) {
56
77
  resources.push(model);
78
+ const constExport = resourceConstExport(node);
79
+ if (constExport) {
80
+ types.push({ key: `${model.app}.${model.model}`, ...constExport });
81
+ }
82
+ }
57
83
  }
58
84
  ts.forEachChild(node, walk);
59
85
  });
60
86
  }
61
87
  // Deterministic order: app, then model.
62
88
  resources.sort((a, b) => a.app === b.app ? a.model.localeCompare(b.model) : a.app.localeCompare(b.app));
63
- return resources;
89
+ types.sort((a, b) => a.key.localeCompare(b.key));
90
+ return { resources, types };
91
+ }
92
+ /** The `const X = resource(…)` binding a resource call is assigned to. */
93
+ function resourceConstExport(call) {
94
+ const decl = call.parent;
95
+ if (!ts.isVariableDeclaration(decl) || !ts.isIdentifier(decl.name)) {
96
+ return undefined;
97
+ }
98
+ const statement = decl.parent?.parent;
99
+ const exported = !!statement &&
100
+ ts.isVariableStatement(statement) &&
101
+ !!ts.getModifiers(statement)?.some((m) => m.kind === ts.SyntaxKind.ExportKeyword);
102
+ return {
103
+ typeName: decl.name.text,
104
+ fileName: path.resolve(call.getSourceFile().fileName),
105
+ exported,
106
+ kind: "resourceConst",
107
+ };
64
108
  }
65
109
  function isResourceCall(node) {
66
110
  return (ts.isCallExpression(node) &&
@@ -96,18 +140,20 @@ function analyzeResourceCall(call, checker) {
96
140
  }
97
141
  }
98
142
  const options = literalToValue(call.arguments[3]) ?? {};
99
- return {
143
+ const resource = {
100
144
  app,
101
145
  model,
102
146
  fields,
103
147
  relations,
104
- sync: normaliseSync(options.sync),
105
- admin: normaliseAdmin(options.admin),
148
+ sync: (0, manifest_1.normaliseSync)(options.sync),
149
+ admin: (0, manifest_1.normaliseAdmin)(options.admin),
106
150
  search: options.search,
107
151
  ordering: options.ordering,
108
152
  permission: options.permission,
109
- access: normaliseAccess(options.access),
153
+ access: (0, manifest_1.normaliseAccess)(options.access),
110
154
  };
155
+ (0, manifest_1.applyTransitions)(resource, options.transitions);
156
+ return resource;
111
157
  }
112
158
  /**
113
159
  * Resolve a single field from its initializer. FK detection is type-driven:
@@ -176,10 +222,18 @@ function propertyName(name) {
176
222
  * Evaluate a *static literal* AST node (string/number/bool/null/array/object)
177
223
  * into a JS value. Anything non-literal (a function, an identifier) yields
178
224
  * `undefined` — the options and constraint blocks are always literals.
225
+ * Exported for the class-entity analyser, which reads `static transitions`
226
+ * blocks with the same evaluator.
179
227
  */
180
228
  function literalToValue(node) {
181
229
  if (!node)
182
230
  return undefined;
231
+ // Look through `as const` / `satisfies` / parens — the value is unchanged.
232
+ if (ts.isAsExpression(node) ||
233
+ ts.isSatisfiesExpression(node) ||
234
+ ts.isParenthesizedExpression(node)) {
235
+ return literalToValue(node.expression);
236
+ }
183
237
  if (ts.isStringLiteralLike(node))
184
238
  return node.text;
185
239
  if (ts.isNumericLiteral(node))
@@ -211,46 +265,3 @@ function literalToValue(node) {
211
265
  }
212
266
  return undefined;
213
267
  }
214
- function normaliseSync(sync) {
215
- if (!sync || typeof sync !== "object")
216
- return undefined;
217
- const mode = sync.mode === "local" ? "local" : "server";
218
- if (mode === "server" && !sync.confirmed) {
219
- // A bare server resource carries no sync semantics.
220
- return undefined;
221
- }
222
- const model = { mode };
223
- if (typeof sync.scope === "string")
224
- model.scope = sync.scope;
225
- if (Array.isArray(sync.confirmed))
226
- model.confirmed = sync.confirmed;
227
- if (sync.load === "lazy")
228
- model.load = "lazy";
229
- if (sync.clientIds === true)
230
- model.clientIds = true;
231
- return model;
232
- }
233
- function normaliseAccess(access) {
234
- if (!access || typeof access !== "object")
235
- return undefined;
236
- const model = {};
237
- if (typeof access.ownerField === "string")
238
- model.ownerField = access.ownerField;
239
- if (access.scope === "owner" || access.scope === "shared") {
240
- model.scope = access.scope;
241
- }
242
- if (Array.isArray(access.adminGroups)) {
243
- model.adminGroups = access.adminGroups.filter((g) => typeof g === "string");
244
- }
245
- return Object.keys(model).length ? model : undefined;
246
- }
247
- function normaliseAdmin(admin) {
248
- if (admin === false)
249
- return false;
250
- if (!admin || typeof admin !== "object")
251
- return undefined;
252
- const model = {};
253
- if (Array.isArray(admin.listDisplay))
254
- model.listDisplay = admin.listDisplay;
255
- return model;
256
- }
@@ -4,9 +4,28 @@ export interface EntityDiagnostic {
4
4
  severity: "error" | "warning";
5
5
  message: string;
6
6
  }
7
+ /**
8
+ * Where one entity's TypeScript type lives — what `rastack compile` needs to
9
+ * emit the `rastack-env.d.ts` registry (`"app.model"` → `import("…").Name`)
10
+ * that lets `useData("app.model")` infer its rows with no type argument.
11
+ */
12
+ export interface EntityTypeExport {
13
+ /** `"app.model"`. */
14
+ key: string;
15
+ /** The declared class/interface name (or DSL const name). */
16
+ typeName: string;
17
+ /** Absolute path of the declaring source file. */
18
+ fileName: string;
19
+ /** Whether the declaration is exported (unexported types can't register). */
20
+ exported: boolean;
21
+ /** DSL consts register as `RowOf<typeof X>` rather than the type itself. */
22
+ kind: "type" | "resourceConst";
23
+ }
7
24
  export interface EntityAnalysis {
8
25
  resources: ResourceModel[];
9
26
  diagnostics: EntityDiagnostic[];
27
+ /** Registry entries for the class/interface entities. */
28
+ types: EntityTypeExport[];
10
29
  }
11
30
  export declare function analyzeEntities(program: ts.Program, fileNames: string[]): EntityAnalysis;
12
31
  /**
@@ -38,6 +38,7 @@ exports.detectCycles = detectCycles;
38
38
  const path = __importStar(require("path"));
39
39
  const ts = __importStar(require("typescript"));
40
40
  const program_1 = require("./program");
41
+ const analyze_1 = require("./analyze");
41
42
  function analyzeEntities(program, fileNames) {
42
43
  const checker = program.getTypeChecker();
43
44
  const diagnostics = [];
@@ -65,13 +66,39 @@ function analyzeEntities(program, fileNames) {
65
66
  roots.push(decl);
66
67
  }
67
68
  };
69
+ // Names of every candidate, for string-argument rooting.
70
+ const candidateNaming = new Map();
71
+ const namingOf = (decl) => {
72
+ let naming = candidateNaming.get(decl);
73
+ if (!naming) {
74
+ naming = entityNaming(decl);
75
+ candidateNaming.set(decl, naming);
76
+ }
77
+ return naming;
78
+ };
68
79
  for (const sf of sourceFiles) {
69
80
  ts.forEachChild(sf, function walk(node) {
70
- if (ts.isCallExpression(node) && isHookCall(node)) {
71
- for (const typeArg of node.typeArguments ?? []) {
72
- const decl = entityBehindTypeNode(typeArg, checker, scanned);
73
- if (decl && candidates.has(decl))
74
- addRoot(decl);
81
+ if (ts.isCallExpression(node)) {
82
+ if (isHookCall(node)) {
83
+ for (const typeArg of node.typeArguments ?? []) {
84
+ const decl = entityBehindTypeNode(typeArg, checker, scanned);
85
+ if (decl && candidates.has(decl))
86
+ addRoot(decl);
87
+ }
88
+ }
89
+ // `useData("airports.terminal")` — the manifest-key string names the
90
+ // entity, so it roots the declaration exactly like a type argument
91
+ // would: the compiler resolves the dotted key against the same naming
92
+ // it compiles. (Only the dotted form roots — a bare string argument to
93
+ // some `use*` hook is far too often not an entity key.)
94
+ const key = hookEntityKeyArg(node);
95
+ if (key) {
96
+ for (const decl of candidates) {
97
+ const naming = namingOf(decl);
98
+ if (naming.app === key.app && naming.model === key.model) {
99
+ addRoot(decl);
100
+ }
101
+ }
75
102
  }
76
103
  }
77
104
  ts.forEachChild(node, walk);
@@ -112,9 +139,17 @@ function analyzeEntities(program, fileNames) {
112
139
  // 3. Build the canonical models.
113
140
  const namingByDecl = new Map(included.map((decl) => [decl, entityNaming(decl)]));
114
141
  const resources = [];
142
+ const types = [];
115
143
  for (const decl of included) {
116
144
  const naming = namingByDecl.get(decl);
117
145
  const key = `${naming.app}.${naming.model}`;
146
+ types.push({
147
+ key,
148
+ typeName: decl.name.text,
149
+ fileName: path.resolve(decl.getSourceFile().fileName),
150
+ exported: !!(ts.getCombinedModifierFlags(decl) & ts.ModifierFlags.Export),
151
+ kind: "type",
152
+ });
118
153
  const fields = [];
119
154
  const relations = [];
120
155
  for (const prop of analyzed(decl)) {
@@ -146,30 +181,69 @@ function analyzeEntities(program, fileNames) {
146
181
  fields.push(prop.field);
147
182
  }
148
183
  }
149
- resources.push({
184
+ const resource = {
150
185
  app: naming.app,
151
186
  model: naming.model,
152
187
  fields,
153
188
  relations,
154
189
  search: naming.search,
155
190
  permission: naming.permission,
156
- });
191
+ };
192
+ // A class may declare its state machine inline: `static transitions =
193
+ // {...}`. Statics are never columns, so the block rides alongside the
194
+ // data properties — the class-authored twin of the DSL option.
195
+ (0, analyze_1.applyTransitions)(resource, staticTransitions(decl));
196
+ resources.push(resource);
157
197
  }
158
198
  resources.sort((a, b) => a.app === b.app ? a.model.localeCompare(b.model) : a.app.localeCompare(b.app));
159
- return { resources, diagnostics };
199
+ return { resources, diagnostics, types };
200
+ }
201
+ /**
202
+ * The literal value of a class's `static transitions = {...}` block, when
203
+ * declared. Only static object literals qualify — the block is data, exactly
204
+ * like the DSL option, and is evaluated with the same literal evaluator.
205
+ */
206
+ function staticTransitions(decl) {
207
+ if (!ts.isClassDeclaration(decl))
208
+ return undefined;
209
+ for (const member of decl.members) {
210
+ if (ts.isPropertyDeclaration(member) &&
211
+ ts.getCombinedModifierFlags(member) & ts.ModifierFlags.Static &&
212
+ (ts.isIdentifier(member.name) || ts.isStringLiteralLike(member.name)) &&
213
+ member.name.text === "transitions" &&
214
+ member.initializer) {
215
+ return (0, analyze_1.literalToValue)(member.initializer);
216
+ }
217
+ }
218
+ return undefined;
160
219
  }
161
220
  // -- hooks -------------------------------------------------------------------
162
- /** A React-style hook call: `useX(...)` or `obj.useX(...)` with type args. */
163
- function isHookCall(call) {
164
- if (!call.typeArguments?.length)
165
- return false;
221
+ /** The `useX` name of a call's callee, if it is hook-shaped. */
222
+ function hookCalleeName(call) {
166
223
  const callee = call.expression;
167
224
  const name = ts.isIdentifier(callee)
168
225
  ? callee.text
169
226
  : ts.isPropertyAccessExpression(callee)
170
227
  ? callee.name.text
171
228
  : undefined;
172
- return !!name && /^use[A-Z0-9_]/.test(name);
229
+ return name && /^use[A-Z0-9_]/.test(name) ? name : undefined;
230
+ }
231
+ /** A React-style hook call: `useX(...)` or `obj.useX(...)` with type args. */
232
+ function isHookCall(call) {
233
+ if (!call.typeArguments?.length)
234
+ return false;
235
+ return hookCalleeName(call) !== undefined;
236
+ }
237
+ const ENTITY_KEY_RE = /^([A-Za-z_$][\w$]*)\.([A-Za-z_$][\w$]*)$/;
238
+ /** The `{ app, model }` of a hook call's first-argument entity-key string. */
239
+ function hookEntityKeyArg(call) {
240
+ if (!hookCalleeName(call))
241
+ return undefined;
242
+ const first = call.arguments[0];
243
+ if (!first || !ts.isStringLiteralLike(first))
244
+ return undefined;
245
+ const match = ENTITY_KEY_RE.exec(first.text);
246
+ return match ? { app: match[1], model: match[2] } : undefined;
173
247
  }
174
248
  /** Resolve a type node to a class/interface declared in a scanned file. */
175
249
  function entityBehindTypeNode(node, checker, scanned) {
@@ -1,10 +1,10 @@
1
- import { EntityDiagnostic } from "./entities";
1
+ import { EntityDiagnostic, EntityTypeExport } from "./entities";
2
2
  import { Manifest, ResourceModel } from "./model";
3
- export { analyze } from "./analyze";
3
+ export { analyze, analyzeDsl } from "./analyze";
4
4
  export { analyzeEntities, detectCycles } from "./entities";
5
- export type { EntityAnalysis, EntityDiagnostic } from "./entities";
5
+ export type { EntityAnalysis, EntityDiagnostic, EntityTypeExport, } from "./entities";
6
6
  export { buildManifest } from "./manifest";
7
- export { buildOpenApi } from "./openapi";
7
+ export { buildOpenApi, writeSchema } from "./openapi";
8
8
  export * from "./model";
9
9
  export interface CompileResult {
10
10
  manifest: Manifest;
@@ -25,6 +25,8 @@ export declare function resolveResourceFiles(input: string): string[];
25
25
  export interface ProjectAnalysis {
26
26
  resources: ResourceModel[];
27
27
  diagnostics: EntityDiagnostic[];
28
+ /** Where each entity's TS type lives — the `rastack-env.d.ts` registry input. */
29
+ types: EntityTypeExport[];
28
30
  }
29
31
  /**
30
32
  * Analyse a project into the canonical model: explicit `resource()` DSL
@@ -35,10 +37,27 @@ export interface ProjectAnalysis {
35
37
  * the merged FK graph.
36
38
  */
37
39
  export declare function analyzeProject(input: string): ProjectAnalysis;
40
+ /**
41
+ * Validate every resource's `transitions` block against its own fields and
42
+ * state set — an edge to an undeclared state, an unknown state field, or a
43
+ * `set` patch targeting a non-field are authoring errors, caught at compile
44
+ * time like a circular FK.
45
+ */
46
+ export declare function detectTransitionProblems(resources: ResourceModel[]): EntityDiagnostic[];
38
47
  /** Analyse resource definitions into the canonical model (no file output). */
39
48
  export declare function analyzeResources(input: string): ResourceModel[];
40
49
  /** Compile resource definitions to a manifest + OpenAPI, writing both to disk. */
41
50
  export declare function compile(input: string, outDir: string): CompileResult;
51
+ /**
52
+ * The `rastack-env.d.ts` the compiler emits beside the manifest: a types-only
53
+ * declaration-merge of `rastack/hooks/registry`'s `EntityTypes`, mapping every
54
+ * exported entity's `"app.model"` key to *your own* type — classes and
55
+ * interfaces directly, `resource()` consts through `RowOf<typeof X>`. With
56
+ * this file in the program, `useData("airports.terminal")` infers its rows
57
+ * with no type argument. Unexported declarations can't be imported and are
58
+ * skipped. Returns `undefined` when there is nothing to register.
59
+ */
60
+ export declare function buildEntityRegistryDts(types: EntityTypeExport[], outDir: string): string | undefined;
42
61
  /** Detect duplicate `app.model` definitions and dangling FK targets. */
43
62
  export declare function collisions(resources: ResourceModel[]): string[];
44
63
  export declare function urlsFor(resources: ResourceModel[]): string[];
@@ -36,11 +36,13 @@ var __exportStar = (this && this.__exportStar) || function(m, exports) {
36
36
  for (var p in m) if (p !== "default" && !Object.prototype.hasOwnProperty.call(exports, p)) __createBinding(exports, m, p);
37
37
  };
38
38
  Object.defineProperty(exports, "__esModule", { value: true });
39
- exports.buildOpenApi = exports.buildManifest = exports.detectCycles = exports.analyzeEntities = exports.analyze = void 0;
39
+ exports.writeSchema = exports.buildOpenApi = exports.buildManifest = exports.detectCycles = exports.analyzeEntities = exports.analyzeDsl = exports.analyze = void 0;
40
40
  exports.resolveResourceFiles = resolveResourceFiles;
41
41
  exports.analyzeProject = analyzeProject;
42
+ exports.detectTransitionProblems = detectTransitionProblems;
42
43
  exports.analyzeResources = analyzeResources;
43
44
  exports.compile = compile;
45
+ exports.buildEntityRegistryDts = buildEntityRegistryDts;
44
46
  exports.collisions = collisions;
45
47
  exports.urlsFor = urlsFor;
46
48
  const fs = __importStar(require("fs"));
@@ -50,8 +52,10 @@ const entities_1 = require("./entities");
50
52
  const manifest_1 = require("./manifest");
51
53
  const openapi_1 = require("./openapi");
52
54
  const program_1 = require("./program");
55
+ const transitions_1 = require("../validate/transitions");
53
56
  var analyze_2 = require("./analyze");
54
57
  Object.defineProperty(exports, "analyze", { enumerable: true, get: function () { return analyze_2.analyze; } });
58
+ Object.defineProperty(exports, "analyzeDsl", { enumerable: true, get: function () { return analyze_2.analyzeDsl; } });
55
59
  var entities_2 = require("./entities");
56
60
  Object.defineProperty(exports, "analyzeEntities", { enumerable: true, get: function () { return entities_2.analyzeEntities; } });
57
61
  Object.defineProperty(exports, "detectCycles", { enumerable: true, get: function () { return entities_2.detectCycles; } });
@@ -59,6 +63,7 @@ var manifest_2 = require("./manifest");
59
63
  Object.defineProperty(exports, "buildManifest", { enumerable: true, get: function () { return manifest_2.buildManifest; } });
60
64
  var openapi_2 = require("./openapi");
61
65
  Object.defineProperty(exports, "buildOpenApi", { enumerable: true, get: function () { return openapi_2.buildOpenApi; } });
66
+ Object.defineProperty(exports, "writeSchema", { enumerable: true, get: function () { return openapi_2.writeSchema; } });
62
67
  __exportStar(require("./model"), exports);
63
68
  /** Directories never worth walking for resource definitions. */
64
69
  const IGNORE_DIRS = new Set([
@@ -112,26 +117,57 @@ function resolveResourceFiles(input) {
112
117
  function analyzeProject(input) {
113
118
  const files = resolveResourceFiles(input);
114
119
  const program = (0, program_1.createProgram)(files);
115
- const fromDsl = (0, analyze_1.analyze)(program, files);
120
+ const fromDsl = (0, analyze_1.analyzeDsl)(program, files);
116
121
  const fromInterfaces = (0, entities_1.analyzeEntities)(program, files);
117
- const defined = new Set(fromDsl.map((r) => `${r.app}.${r.model}`));
122
+ const defined = new Set(fromDsl.resources.map((r) => `${r.app}.${r.model}`));
118
123
  const resources = [
119
- ...fromDsl,
124
+ ...fromDsl.resources,
120
125
  ...fromInterfaces.resources.filter((r) => !defined.has(`${r.app}.${r.model}`)),
121
126
  ];
122
127
  resources.sort((a, b) => a.app === b.app ? a.model.localeCompare(b.model) : a.app.localeCompare(b.app));
128
+ // Registry entries follow the same merge rule as the resources: the DSL
129
+ // definition wins an `app.model` collision.
130
+ const types = [
131
+ ...fromDsl.types,
132
+ ...fromInterfaces.types.filter((t) => !defined.has(t.key)),
133
+ ].sort((a, b) => a.key.localeCompare(b.key));
123
134
  return {
124
135
  resources,
125
- diagnostics: [...fromInterfaces.diagnostics, ...(0, entities_1.detectCycles)(resources)],
136
+ types,
137
+ diagnostics: [
138
+ ...fromInterfaces.diagnostics,
139
+ ...(0, entities_1.detectCycles)(resources),
140
+ ...detectTransitionProblems(resources),
141
+ ],
126
142
  };
127
143
  }
144
+ /**
145
+ * Validate every resource's `transitions` block against its own fields and
146
+ * state set — an edge to an undeclared state, an unknown state field, or a
147
+ * `set` patch targeting a non-field are authoring errors, caught at compile
148
+ * time like a circular FK.
149
+ */
150
+ function detectTransitionProblems(resources) {
151
+ const diagnostics = [];
152
+ for (const r of resources) {
153
+ if (!r.transitions)
154
+ continue;
155
+ for (const problem of (0, transitions_1.transitionProblems)(r.transitions, r.fields)) {
156
+ diagnostics.push({
157
+ severity: "error",
158
+ message: `${r.app}.${r.model}: ${problem}`,
159
+ });
160
+ }
161
+ }
162
+ return diagnostics;
163
+ }
128
164
  /** Analyse resource definitions into the canonical model (no file output). */
129
165
  function analyzeResources(input) {
130
166
  return analyzeProject(input).resources;
131
167
  }
132
168
  /** Compile resource definitions to a manifest + OpenAPI, writing both to disk. */
133
169
  function compile(input, outDir) {
134
- const { resources, diagnostics } = analyzeProject(input);
170
+ const { resources, diagnostics, types } = analyzeProject(input);
135
171
  const errors = diagnostics.filter((d) => d.severity === "error");
136
172
  if (errors.length) {
137
173
  throw new Error(errors.map((e) => e.message).join("\n"));
@@ -143,7 +179,50 @@ function compile(input, outDir) {
143
179
  const openapiPath = path.join(outDir, "openapi.json");
144
180
  fs.writeFileSync(manifestPath, (0, manifest_1.manifestToJson)(manifest), { flag: "w" });
145
181
  fs.writeFileSync(openapiPath, (0, openapi_1.openApiToJson)(openapi), { flag: "w" });
146
- return { manifest, openapi, files: [manifestPath, openapiPath], diagnostics };
182
+ const files = [manifestPath, openapiPath];
183
+ const registryDts = buildEntityRegistryDts(types, outDir);
184
+ if (registryDts) {
185
+ const registryPath = path.join(outDir, "rastack-env.d.ts");
186
+ fs.writeFileSync(registryPath, registryDts, { flag: "w" });
187
+ files.push(registryPath);
188
+ }
189
+ return { manifest, openapi, files, diagnostics };
190
+ }
191
+ /**
192
+ * The `rastack-env.d.ts` the compiler emits beside the manifest: a types-only
193
+ * declaration-merge of `rastack/hooks/registry`'s `EntityTypes`, mapping every
194
+ * exported entity's `"app.model"` key to *your own* type — classes and
195
+ * interfaces directly, `resource()` consts through `RowOf<typeof X>`. With
196
+ * this file in the program, `useData("airports.terminal")` infers its rows
197
+ * with no type argument. Unexported declarations can't be imported and are
198
+ * skipped. Returns `undefined` when there is nothing to register.
199
+ */
200
+ function buildEntityRegistryDts(types, outDir) {
201
+ const entries = types.filter((t) => t.exported);
202
+ if (!entries.length)
203
+ return undefined;
204
+ const lines = entries.map((t) => {
205
+ let rel = path
206
+ .relative(path.resolve(outDir), t.fileName)
207
+ .replace(/\\/g, "/")
208
+ .replace(/\.(tsx|ts)$/, "");
209
+ if (!rel.startsWith("."))
210
+ rel = `./${rel}`;
211
+ const type = t.kind === "resourceConst"
212
+ ? `import("rastack/define").RowOf<typeof import("${rel}").${t.typeName}>`
213
+ : `import("${rel}").${t.typeName}`;
214
+ return ` "${t.key}": ${type};`;
215
+ });
216
+ return ("// Generated by `rastack compile` — do not edit.\n" +
217
+ '// Maps manifest keys to your own entity types so `useData("app.model")` /\n' +
218
+ '// `useForm("app.model")` infer their rows with no type argument.\n' +
219
+ "// Types only: nothing to import, no runtime code.\n" +
220
+ 'declare module "rastack/hooks/registry" {\n' +
221
+ " interface EntityTypes {\n" +
222
+ `${lines.join("\n")}\n` +
223
+ " }\n" +
224
+ "}\n" +
225
+ "export {};\n");
147
226
  }
148
227
  // -- introspection (replaces `manage.py rastack check|list|urls`) -----------
149
228
  /** Detect duplicate `app.model` definitions and dangling FK targets. */
@@ -17,6 +17,12 @@ export interface FieldModel {
17
17
  primaryKey?: boolean;
18
18
  null?: boolean;
19
19
  default?: unknown;
20
+ /**
21
+ * The closed set of allowed values (the `member` gate / a `select` control).
22
+ * Stamped by the compiler — today from a `transitions` block, whose `states`
23
+ * become the state field's value set.
24
+ */
25
+ options?: Array<string | number>;
20
26
  /** Present iff `type === "fk"` — the resource this field points at. */
21
27
  relation?: RelationTarget;
22
28
  }
@@ -48,6 +54,37 @@ export interface AccessModel {
48
54
  scope?: "owner" | "shared";
49
55
  adminGroups?: string[];
50
56
  }
57
+ /**
58
+ * One edge of a resource's state machine: a named transition from one (or
59
+ * several) states to exactly one target state. `set` is the transition's
60
+ * *effect* — field patches the engine applies when the edge fires — which is
61
+ * what makes a transition a declarative backend function: declared once in
62
+ * TypeScript, compiled here, executed by `rastack-api-core` on every surface.
63
+ */
64
+ export interface TransitionEdgeModel {
65
+ /** Source states this transition may fire from (normalised to a list). */
66
+ from: string[];
67
+ /** The state entered when the transition fires. */
68
+ to: string;
69
+ /** Field patches applied by the engine alongside the state change. */
70
+ set?: Record<string, unknown>;
71
+ }
72
+ /**
73
+ * A resource's declarative state machine, keyed off one state field. Compiled
74
+ * into the manifest and enforced on every write surface: the TS record machine
75
+ * (forms, `rastack import`) and `rastack-api-core` (server, Lambda, WASM) all
76
+ * reject a write that jumps between states without a declared edge.
77
+ */
78
+ export interface TransitionsModel {
79
+ /** The field that carries the machine state. */
80
+ field: string;
81
+ /** Every state the field may hold (becomes the field's `options`). */
82
+ states: string[];
83
+ /** The state new records start in (becomes the field's default). */
84
+ initial?: string;
85
+ /** Named transitions — `on.board = { from: ["scheduled"], to: "boarding" }`. */
86
+ on: Record<string, TransitionEdgeModel>;
87
+ }
51
88
  export interface ResourceModel {
52
89
  app: string;
53
90
  model: string;
@@ -59,6 +96,7 @@ export interface ResourceModel {
59
96
  ordering?: string[];
60
97
  permission?: string;
61
98
  access?: AccessModel;
99
+ transitions?: TransitionsModel;
62
100
  }
63
101
  export interface Manifest {
64
102
  version: 1;
@@ -10,3 +10,12 @@ import { ResourceModel } from "./model";
10
10
  */
11
11
  export declare function buildOpenApi(resources: ResourceModel[]): any;
12
12
  export declare function openApiToJson(doc: any): string;
13
+ /**
14
+ * The write JSON Schema for a resource — what the API accepts on
15
+ * create/update. Exported because the generic `useForm(Entity)` hook derives
16
+ * the *same* schema at runtime straight from the manifest, so a form
17
+ * validates against exactly the contract the emitted OpenAPI documents.
18
+ * Resources with a state machine carry it as `x-rastack-transitions`, the
19
+ * write-side twin of `x-rastack-relation`.
20
+ */
21
+ export declare function writeSchema(resource: ResourceModel, patched: boolean): any;