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.
- package/CHANGELOG.md +4 -0
- package/components/auto-form/AutoForm.tsx +13 -0
- package/components/auto-form/use-auto-form.ts +28 -0
- package/components/types.ts +8 -0
- package/dist/admin.js +13 -13
- package/dist/compile/analyze.d.ts +20 -0
- package/dist/compile/analyze.js +60 -49
- package/dist/compile/entities.d.ts +19 -0
- package/dist/compile/entities.js +87 -13
- package/dist/compile/index.d.ts +23 -4
- package/dist/compile/index.js +86 -7
- package/dist/compile/model.d.ts +38 -0
- package/dist/compile/openapi.d.ts +9 -0
- package/dist/compile/openapi.js +14 -0
- package/dist/define/index.d.ts +162 -21
- package/dist/define/index.js +28 -22
- package/dist/define/manifest.d.ts +64 -0
- package/dist/define/manifest.js +250 -0
- package/dist/import/tabular.d.ts +8 -2
- package/dist/import/tabular.js +1 -1
- package/dist/plugin/core.d.ts +108 -0
- package/dist/plugin/core.js +198 -0
- package/dist/plugin/index.d.ts +112 -0
- package/dist/plugin/index.js +203 -0
- package/dist/rastack-import.js +4 -1
- package/dist/validate/adapters.js +2 -0
- package/dist/validate/index.d.ts +1 -0
- package/dist/validate/index.js +1 -0
- package/dist/validate/machine.d.ts +23 -2
- package/dist/validate/machine.js +35 -2
- package/dist/validate/transitions.d.ts +86 -0
- package/dist/validate/transitions.js +199 -0
- package/dist/wasm/rastack_wasm.js +1 -1
- package/dist/wasm/rastack_wasm_bg.wasm +0 -0
- package/hooks/data.ts +221 -0
- package/hooks/entity.ts +228 -0
- package/hooks/form/entity-form.ts +358 -0
- package/hooks/form/form.ts +8 -1
- package/hooks/form/index.ts +7 -1
- package/hooks/index.ts +4 -0
- package/hooks/manifest.ts +77 -0
- package/hooks/registry.ts +56 -0
- package/package.json +1 -1
- package/plugin.ts +8 -0
- package/provider/provider.tsx +26 -5
- package/provider/types.ts +15 -3
- package/src/compile/analyze.ts +74 -45
- package/src/compile/entities.ts +111 -11
- package/src/compile/index.ts +108 -11
- package/src/compile/model.ts +40 -0
- package/src/compile/openapi.ts +13 -1
- package/src/define/index.ts +233 -29
- package/src/define/manifest.ts +278 -0
- package/src/import/tabular.ts +9 -2
- package/src/plugin/core.ts +236 -0
- package/src/plugin/index.ts +243 -0
- package/src/rastack-import.ts +4 -1
- package/src/validate/adapters.ts +1 -0
- package/src/validate/index.ts +1 -0
- package/src/validate/machine.ts +55 -3
- package/src/validate/transitions.ts +232 -0
- package/test/components.spec.ts +22 -0
- package/test/plugin.spec.ts +315 -0
- package/test/runtime-manifest.spec.ts +309 -0
- package/test/transitions.spec.ts +372 -0
- package/test/typed-hooks.spec.ts +412 -0
- package/wasm/rastack_wasm.js +1 -1
- 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;
|
package/dist/compile/analyze.js
CHANGED
|
@@ -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
|
-
|
|
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
|
-
|
|
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
|
/**
|
package/dist/compile/entities.js
CHANGED
|
@@ -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)
|
|
71
|
-
|
|
72
|
-
const
|
|
73
|
-
|
|
74
|
-
|
|
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
|
-
|
|
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
|
-
/**
|
|
163
|
-
function
|
|
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
|
|
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) {
|
package/dist/compile/index.d.ts
CHANGED
|
@@ -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[];
|
package/dist/compile/index.js
CHANGED
|
@@ -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.
|
|
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
|
-
|
|
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
|
-
|
|
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. */
|
package/dist/compile/model.d.ts
CHANGED
|
@@ -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;
|