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
package/src/compile/index.ts
CHANGED
|
@@ -1,17 +1,27 @@
|
|
|
1
1
|
import * as fs from "fs";
|
|
2
2
|
import * as path from "path";
|
|
3
|
-
import { analyze } from "./analyze";
|
|
4
|
-
import {
|
|
3
|
+
import { analyze, analyzeDsl } from "./analyze";
|
|
4
|
+
import {
|
|
5
|
+
analyzeEntities,
|
|
6
|
+
detectCycles,
|
|
7
|
+
EntityDiagnostic,
|
|
8
|
+
EntityTypeExport,
|
|
9
|
+
} from "./entities";
|
|
5
10
|
import { buildManifest, manifestToJson } from "./manifest";
|
|
6
11
|
import { Manifest, ResourceModel } from "./model";
|
|
7
12
|
import { buildOpenApi, openApiToJson } from "./openapi";
|
|
8
13
|
import { createProgram } from "./program";
|
|
14
|
+
import { transitionProblems } from "../validate/transitions";
|
|
9
15
|
|
|
10
|
-
export { analyze } from "./analyze";
|
|
16
|
+
export { analyze, analyzeDsl } from "./analyze";
|
|
11
17
|
export { analyzeEntities, detectCycles } from "./entities";
|
|
12
|
-
export type {
|
|
18
|
+
export type {
|
|
19
|
+
EntityAnalysis,
|
|
20
|
+
EntityDiagnostic,
|
|
21
|
+
EntityTypeExport,
|
|
22
|
+
} from "./entities";
|
|
13
23
|
export { buildManifest } from "./manifest";
|
|
14
|
-
export { buildOpenApi } from "./openapi";
|
|
24
|
+
export { buildOpenApi, writeSchema } from "./openapi";
|
|
15
25
|
export * from "./model";
|
|
16
26
|
|
|
17
27
|
export interface CompileResult {
|
|
@@ -64,6 +74,8 @@ export function resolveResourceFiles(input: string): string[] {
|
|
|
64
74
|
export interface ProjectAnalysis {
|
|
65
75
|
resources: ResourceModel[];
|
|
66
76
|
diagnostics: EntityDiagnostic[];
|
|
77
|
+
/** Where each entity's TS type lives — the `rastack-env.d.ts` registry input. */
|
|
78
|
+
types: EntityTypeExport[];
|
|
67
79
|
}
|
|
68
80
|
|
|
69
81
|
/**
|
|
@@ -78,24 +90,58 @@ export function analyzeProject(input: string): ProjectAnalysis {
|
|
|
78
90
|
const files = resolveResourceFiles(input);
|
|
79
91
|
const program = createProgram(files);
|
|
80
92
|
|
|
81
|
-
const fromDsl =
|
|
93
|
+
const fromDsl = analyzeDsl(program, files);
|
|
82
94
|
const fromInterfaces = analyzeEntities(program, files);
|
|
83
95
|
|
|
84
|
-
const defined = new Set(fromDsl.map((r) => `${r.app}.${r.model}`));
|
|
96
|
+
const defined = new Set(fromDsl.resources.map((r) => `${r.app}.${r.model}`));
|
|
85
97
|
const resources = [
|
|
86
|
-
...fromDsl,
|
|
98
|
+
...fromDsl.resources,
|
|
87
99
|
...fromInterfaces.resources.filter((r) => !defined.has(`${r.app}.${r.model}`)),
|
|
88
100
|
];
|
|
89
101
|
resources.sort((a, b) =>
|
|
90
102
|
a.app === b.app ? a.model.localeCompare(b.model) : a.app.localeCompare(b.app),
|
|
91
103
|
);
|
|
92
104
|
|
|
105
|
+
// Registry entries follow the same merge rule as the resources: the DSL
|
|
106
|
+
// definition wins an `app.model` collision.
|
|
107
|
+
const types = [
|
|
108
|
+
...fromDsl.types,
|
|
109
|
+
...fromInterfaces.types.filter((t) => !defined.has(t.key)),
|
|
110
|
+
].sort((a, b) => a.key.localeCompare(b.key));
|
|
111
|
+
|
|
93
112
|
return {
|
|
94
113
|
resources,
|
|
95
|
-
|
|
114
|
+
types,
|
|
115
|
+
diagnostics: [
|
|
116
|
+
...fromInterfaces.diagnostics,
|
|
117
|
+
...detectCycles(resources),
|
|
118
|
+
...detectTransitionProblems(resources),
|
|
119
|
+
],
|
|
96
120
|
};
|
|
97
121
|
}
|
|
98
122
|
|
|
123
|
+
/**
|
|
124
|
+
* Validate every resource's `transitions` block against its own fields and
|
|
125
|
+
* state set — an edge to an undeclared state, an unknown state field, or a
|
|
126
|
+
* `set` patch targeting a non-field are authoring errors, caught at compile
|
|
127
|
+
* time like a circular FK.
|
|
128
|
+
*/
|
|
129
|
+
export function detectTransitionProblems(
|
|
130
|
+
resources: ResourceModel[],
|
|
131
|
+
): EntityDiagnostic[] {
|
|
132
|
+
const diagnostics: EntityDiagnostic[] = [];
|
|
133
|
+
for (const r of resources) {
|
|
134
|
+
if (!r.transitions) continue;
|
|
135
|
+
for (const problem of transitionProblems(r.transitions, r.fields)) {
|
|
136
|
+
diagnostics.push({
|
|
137
|
+
severity: "error",
|
|
138
|
+
message: `${r.app}.${r.model}: ${problem}`,
|
|
139
|
+
});
|
|
140
|
+
}
|
|
141
|
+
}
|
|
142
|
+
return diagnostics;
|
|
143
|
+
}
|
|
144
|
+
|
|
99
145
|
/** Analyse resource definitions into the canonical model (no file output). */
|
|
100
146
|
export function analyzeResources(input: string): ResourceModel[] {
|
|
101
147
|
return analyzeProject(input).resources;
|
|
@@ -103,7 +149,7 @@ export function analyzeResources(input: string): ResourceModel[] {
|
|
|
103
149
|
|
|
104
150
|
/** Compile resource definitions to a manifest + OpenAPI, writing both to disk. */
|
|
105
151
|
export function compile(input: string, outDir: string): CompileResult {
|
|
106
|
-
const { resources, diagnostics } = analyzeProject(input);
|
|
152
|
+
const { resources, diagnostics, types } = analyzeProject(input);
|
|
107
153
|
const errors = diagnostics.filter((d) => d.severity === "error");
|
|
108
154
|
if (errors.length) {
|
|
109
155
|
throw new Error(errors.map((e) => e.message).join("\n"));
|
|
@@ -116,8 +162,59 @@ export function compile(input: string, outDir: string): CompileResult {
|
|
|
116
162
|
const openapiPath = path.join(outDir, "openapi.json");
|
|
117
163
|
fs.writeFileSync(manifestPath, manifestToJson(manifest), { flag: "w" });
|
|
118
164
|
fs.writeFileSync(openapiPath, openApiToJson(openapi), { flag: "w" });
|
|
165
|
+
const files = [manifestPath, openapiPath];
|
|
119
166
|
|
|
120
|
-
|
|
167
|
+
const registryDts = buildEntityRegistryDts(types, outDir);
|
|
168
|
+
if (registryDts) {
|
|
169
|
+
const registryPath = path.join(outDir, "rastack-env.d.ts");
|
|
170
|
+
fs.writeFileSync(registryPath, registryDts, { flag: "w" });
|
|
171
|
+
files.push(registryPath);
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
return { manifest, openapi, files, diagnostics };
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
/**
|
|
178
|
+
* The `rastack-env.d.ts` the compiler emits beside the manifest: a types-only
|
|
179
|
+
* declaration-merge of `rastack/hooks/registry`'s `EntityTypes`, mapping every
|
|
180
|
+
* exported entity's `"app.model"` key to *your own* type — classes and
|
|
181
|
+
* interfaces directly, `resource()` consts through `RowOf<typeof X>`. With
|
|
182
|
+
* this file in the program, `useData("airports.terminal")` infers its rows
|
|
183
|
+
* with no type argument. Unexported declarations can't be imported and are
|
|
184
|
+
* skipped. Returns `undefined` when there is nothing to register.
|
|
185
|
+
*/
|
|
186
|
+
export function buildEntityRegistryDts(
|
|
187
|
+
types: EntityTypeExport[],
|
|
188
|
+
outDir: string,
|
|
189
|
+
): string | undefined {
|
|
190
|
+
const entries = types.filter((t) => t.exported);
|
|
191
|
+
if (!entries.length) return undefined;
|
|
192
|
+
|
|
193
|
+
const lines = entries.map((t) => {
|
|
194
|
+
let rel = path
|
|
195
|
+
.relative(path.resolve(outDir), t.fileName)
|
|
196
|
+
.replace(/\\/g, "/")
|
|
197
|
+
.replace(/\.(tsx|ts)$/, "");
|
|
198
|
+
if (!rel.startsWith(".")) rel = `./${rel}`;
|
|
199
|
+
const type =
|
|
200
|
+
t.kind === "resourceConst"
|
|
201
|
+
? `import("rastack/define").RowOf<typeof import("${rel}").${t.typeName}>`
|
|
202
|
+
: `import("${rel}").${t.typeName}`;
|
|
203
|
+
return ` "${t.key}": ${type};`;
|
|
204
|
+
});
|
|
205
|
+
|
|
206
|
+
return (
|
|
207
|
+
"// Generated by `rastack compile` — do not edit.\n" +
|
|
208
|
+
'// Maps manifest keys to your own entity types so `useData("app.model")` /\n' +
|
|
209
|
+
'// `useForm("app.model")` infer their rows with no type argument.\n' +
|
|
210
|
+
"// Types only: nothing to import, no runtime code.\n" +
|
|
211
|
+
'declare module "rastack/hooks/registry" {\n' +
|
|
212
|
+
" interface EntityTypes {\n" +
|
|
213
|
+
`${lines.join("\n")}\n` +
|
|
214
|
+
" }\n" +
|
|
215
|
+
"}\n" +
|
|
216
|
+
"export {};\n"
|
|
217
|
+
);
|
|
121
218
|
}
|
|
122
219
|
|
|
123
220
|
// -- introspection (replaces `manage.py rastack check|list|urls`) -----------
|
package/src/compile/model.ts
CHANGED
|
@@ -27,6 +27,12 @@ export interface FieldModel {
|
|
|
27
27
|
primaryKey?: boolean;
|
|
28
28
|
null?: boolean;
|
|
29
29
|
default?: unknown;
|
|
30
|
+
/**
|
|
31
|
+
* The closed set of allowed values (the `member` gate / a `select` control).
|
|
32
|
+
* Stamped by the compiler — today from a `transitions` block, whose `states`
|
|
33
|
+
* become the state field's value set.
|
|
34
|
+
*/
|
|
35
|
+
options?: Array<string | number>;
|
|
30
36
|
/** Present iff `type === "fk"` — the resource this field points at. */
|
|
31
37
|
relation?: RelationTarget;
|
|
32
38
|
}
|
|
@@ -63,6 +69,39 @@ export interface AccessModel {
|
|
|
63
69
|
adminGroups?: string[];
|
|
64
70
|
}
|
|
65
71
|
|
|
72
|
+
/**
|
|
73
|
+
* One edge of a resource's state machine: a named transition from one (or
|
|
74
|
+
* several) states to exactly one target state. `set` is the transition's
|
|
75
|
+
* *effect* — field patches the engine applies when the edge fires — which is
|
|
76
|
+
* what makes a transition a declarative backend function: declared once in
|
|
77
|
+
* TypeScript, compiled here, executed by `rastack-api-core` on every surface.
|
|
78
|
+
*/
|
|
79
|
+
export interface TransitionEdgeModel {
|
|
80
|
+
/** Source states this transition may fire from (normalised to a list). */
|
|
81
|
+
from: string[];
|
|
82
|
+
/** The state entered when the transition fires. */
|
|
83
|
+
to: string;
|
|
84
|
+
/** Field patches applied by the engine alongside the state change. */
|
|
85
|
+
set?: Record<string, unknown>;
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
/**
|
|
89
|
+
* A resource's declarative state machine, keyed off one state field. Compiled
|
|
90
|
+
* into the manifest and enforced on every write surface: the TS record machine
|
|
91
|
+
* (forms, `rastack import`) and `rastack-api-core` (server, Lambda, WASM) all
|
|
92
|
+
* reject a write that jumps between states without a declared edge.
|
|
93
|
+
*/
|
|
94
|
+
export interface TransitionsModel {
|
|
95
|
+
/** The field that carries the machine state. */
|
|
96
|
+
field: string;
|
|
97
|
+
/** Every state the field may hold (becomes the field's `options`). */
|
|
98
|
+
states: string[];
|
|
99
|
+
/** The state new records start in (becomes the field's default). */
|
|
100
|
+
initial?: string;
|
|
101
|
+
/** Named transitions — `on.board = { from: ["scheduled"], to: "boarding" }`. */
|
|
102
|
+
on: Record<string, TransitionEdgeModel>;
|
|
103
|
+
}
|
|
104
|
+
|
|
66
105
|
export interface ResourceModel {
|
|
67
106
|
app: string;
|
|
68
107
|
model: string;
|
|
@@ -74,6 +113,7 @@ export interface ResourceModel {
|
|
|
74
113
|
ordering?: string[];
|
|
75
114
|
permission?: string;
|
|
76
115
|
access?: AccessModel;
|
|
116
|
+
transitions?: TransitionsModel;
|
|
77
117
|
}
|
|
78
118
|
|
|
79
119
|
export interface Manifest {
|
package/src/compile/openapi.ts
CHANGED
|
@@ -116,7 +116,15 @@ function readSchema(resource: ResourceModel): any {
|
|
|
116
116
|
return { type: "object", properties, required };
|
|
117
117
|
}
|
|
118
118
|
|
|
119
|
-
|
|
119
|
+
/**
|
|
120
|
+
* The write JSON Schema for a resource — what the API accepts on
|
|
121
|
+
* create/update. Exported because the generic `useForm(Entity)` hook derives
|
|
122
|
+
* the *same* schema at runtime straight from the manifest, so a form
|
|
123
|
+
* validates against exactly the contract the emitted OpenAPI documents.
|
|
124
|
+
* Resources with a state machine carry it as `x-rastack-transitions`, the
|
|
125
|
+
* write-side twin of `x-rastack-relation`.
|
|
126
|
+
*/
|
|
127
|
+
export function writeSchema(resource: ResourceModel, patched: boolean): any {
|
|
120
128
|
const properties: Record<string, any> = {};
|
|
121
129
|
const required: string[] = [];
|
|
122
130
|
for (const field of resource.fields) {
|
|
@@ -128,6 +136,9 @@ function writeSchema(resource: ResourceModel, patched: boolean): any {
|
|
|
128
136
|
}
|
|
129
137
|
const schema: any = { type: "object", properties };
|
|
130
138
|
if (required.length) schema.required = required;
|
|
139
|
+
if (resource.transitions) {
|
|
140
|
+
schema["x-rastack-transitions"] = resource.transitions;
|
|
141
|
+
}
|
|
131
142
|
return schema;
|
|
132
143
|
}
|
|
133
144
|
|
|
@@ -156,6 +167,7 @@ function fieldProperty(field: FieldModel): any {
|
|
|
156
167
|
}
|
|
157
168
|
const base = scalarProperty(field.type);
|
|
158
169
|
if (field.maxLength !== undefined) base.maxLength = field.maxLength;
|
|
170
|
+
if (field.options && field.options.length > 0) base.enum = [...field.options];
|
|
159
171
|
if (field.null) base.nullable = true;
|
|
160
172
|
return base;
|
|
161
173
|
}
|
package/src/define/index.ts
CHANGED
|
@@ -23,19 +23,49 @@
|
|
|
23
23
|
* ```
|
|
24
24
|
*/
|
|
25
25
|
|
|
26
|
-
/**
|
|
27
|
-
|
|
26
|
+
/**
|
|
27
|
+
* Every constraint a scalar builder may be handed, merged — the shape the
|
|
28
|
+
* runtime value carries so a manifest can be assembled without the compiler.
|
|
29
|
+
*/
|
|
30
|
+
export interface ScalarFieldOptions {
|
|
31
|
+
maxLength?: number;
|
|
32
|
+
unique?: boolean;
|
|
33
|
+
primaryKey?: boolean;
|
|
34
|
+
default?: unknown;
|
|
35
|
+
null?: boolean;
|
|
36
|
+
autoNow?: boolean;
|
|
37
|
+
autoNowAdd?: boolean;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
/**
|
|
41
|
+
* A scalar field. The literal `__rastackScalar` type is what the compiler
|
|
42
|
+
* reads; `Nullable` phantom-carries the `null: true` option so {@link RowOf}
|
|
43
|
+
* can add `| null` to the inferred row type. The authored options ride along
|
|
44
|
+
* on the runtime value too, so `manifestFromResources()` can build the same
|
|
45
|
+
* manifest `rastack compile` emits — with no compile step at all.
|
|
46
|
+
*/
|
|
47
|
+
export interface Scalar<T extends string, Nullable extends boolean = boolean> {
|
|
28
48
|
readonly __rastackScalar: T;
|
|
49
|
+
readonly __rastackNullable?: Nullable;
|
|
50
|
+
/** The constraint options as authored — carried at runtime. */
|
|
51
|
+
readonly options?: ScalarFieldOptions;
|
|
29
52
|
}
|
|
30
53
|
|
|
54
|
+
/** Whether an options literal declares `null: true` (as a type). */
|
|
55
|
+
type NullOf<O> = O extends { null: true } ? true : false;
|
|
56
|
+
|
|
31
57
|
/**
|
|
32
58
|
* A foreign-key field. `App`/`Model` are carried as string-literal type
|
|
33
|
-
* arguments so the compiler can recover the relation target purely from
|
|
59
|
+
* arguments so the compiler can recover the relation target purely from
|
|
60
|
+
* types; the target thunk is *also* kept on the runtime value so
|
|
61
|
+
* `manifestFromResources()` can recover the relation with no compiler.
|
|
34
62
|
*/
|
|
35
63
|
export interface Ref<App extends string, Model extends string> {
|
|
36
64
|
readonly __rastackRef: true;
|
|
37
65
|
readonly __app: App;
|
|
38
66
|
readonly __model: Model;
|
|
67
|
+
/** The authored `() => Resource` thunk — carried at runtime. */
|
|
68
|
+
readonly target?: () => ResourceType<App, Model, any>;
|
|
39
69
|
}
|
|
40
70
|
|
|
41
71
|
export interface StringOptions {
|
|
@@ -65,39 +95,66 @@ export interface DateOptions {
|
|
|
65
95
|
}
|
|
66
96
|
|
|
67
97
|
/**
|
|
68
|
-
* Field builders. Each returns a
|
|
69
|
-
*
|
|
70
|
-
*
|
|
71
|
-
*
|
|
98
|
+
* Field builders. Each returns a value whose *type* encodes the field kind
|
|
99
|
+
* (what the compiler reads — it analyses the source, it does not execute it)
|
|
100
|
+
* and whose runtime shape keeps the authored options/target, so the same
|
|
101
|
+
* definition doubles as its own manifest via `manifestFromResources()`.
|
|
72
102
|
*/
|
|
73
103
|
export const s = {
|
|
74
|
-
string
|
|
75
|
-
|
|
104
|
+
string<const O extends StringOptions = StringOptions>(
|
|
105
|
+
opts?: O,
|
|
106
|
+
): Scalar<"string", NullOf<O>> {
|
|
107
|
+
return { __rastackScalar: "string", options: opts } as Scalar<
|
|
108
|
+
"string",
|
|
109
|
+
NullOf<O>
|
|
110
|
+
>;
|
|
76
111
|
},
|
|
77
|
-
int
|
|
78
|
-
|
|
112
|
+
int<const O extends NumberOptions = NumberOptions>(
|
|
113
|
+
opts?: O,
|
|
114
|
+
): Scalar<"int", NullOf<O>> {
|
|
115
|
+
return { __rastackScalar: "int", options: opts } as Scalar<"int", NullOf<O>>;
|
|
79
116
|
},
|
|
80
|
-
float
|
|
81
|
-
|
|
117
|
+
float<const O extends NumberOptions = NumberOptions>(
|
|
118
|
+
opts?: O,
|
|
119
|
+
): Scalar<"float", NullOf<O>> {
|
|
120
|
+
return { __rastackScalar: "float", options: opts } as Scalar<
|
|
121
|
+
"float",
|
|
122
|
+
NullOf<O>
|
|
123
|
+
>;
|
|
82
124
|
},
|
|
83
|
-
bool
|
|
84
|
-
|
|
125
|
+
bool<const O extends BoolOptions = BoolOptions>(
|
|
126
|
+
opts?: O,
|
|
127
|
+
): Scalar<"bool", NullOf<O>> {
|
|
128
|
+
return { __rastackScalar: "bool", options: opts } as Scalar<
|
|
129
|
+
"bool",
|
|
130
|
+
NullOf<O>
|
|
131
|
+
>;
|
|
85
132
|
},
|
|
86
|
-
datetime
|
|
87
|
-
|
|
133
|
+
datetime<const O extends DateOptions = DateOptions>(
|
|
134
|
+
opts?: O,
|
|
135
|
+
): Scalar<"datetime", NullOf<O>> {
|
|
136
|
+
return { __rastackScalar: "datetime", options: opts } as Scalar<
|
|
137
|
+
"datetime",
|
|
138
|
+
NullOf<O>
|
|
139
|
+
>;
|
|
88
140
|
},
|
|
89
|
-
uuid
|
|
90
|
-
|
|
141
|
+
uuid<const O extends StringOptions = StringOptions>(
|
|
142
|
+
opts?: O,
|
|
143
|
+
): Scalar<"uuid", NullOf<O>> {
|
|
144
|
+
return { __rastackScalar: "uuid", options: opts } as Scalar<
|
|
145
|
+
"uuid",
|
|
146
|
+
NullOf<O>
|
|
147
|
+
>;
|
|
91
148
|
},
|
|
92
149
|
/**
|
|
93
150
|
* A foreign key. The target is `() => SomeResource`; the compiler resolves
|
|
94
|
-
* the relation from the *type* `Ref<App, Model>` this returns
|
|
95
|
-
*
|
|
151
|
+
* the relation from the *type* `Ref<App, Model>` this returns, and the
|
|
152
|
+
* runtime keeps the thunk so `manifestFromResources()` can resolve it too.
|
|
96
153
|
*/
|
|
97
154
|
ref<A extends string, M extends string>(
|
|
98
|
-
|
|
155
|
+
thunk: () => ResourceType<A, M, any>,
|
|
99
156
|
): Ref<A, M> {
|
|
100
|
-
return { __rastackRef: true } as Ref<A, M>;
|
|
157
|
+
return { __rastackRef: true, target: thunk } as Ref<A, M>;
|
|
101
158
|
},
|
|
102
159
|
};
|
|
103
160
|
|
|
@@ -132,6 +189,54 @@ export interface AccessOptions {
|
|
|
132
189
|
adminGroups?: string[];
|
|
133
190
|
}
|
|
134
191
|
|
|
192
|
+
/**
|
|
193
|
+
* One edge of a resource's state machine. `from` is the state (or states) the
|
|
194
|
+
* transition may fire from, `to` the state it enters, and `set` the effect —
|
|
195
|
+
* field patches the engine applies alongside the state change. The block is
|
|
196
|
+
* static data: it compiles into the manifest and is enforced by
|
|
197
|
+
* `rastack-api-core` on every surface (server, Lambda, in-browser WASM), so a
|
|
198
|
+
* transition behaves as a declarative, typesafe backend function with no
|
|
199
|
+
* per-resource server code.
|
|
200
|
+
*/
|
|
201
|
+
export interface TransitionEdge {
|
|
202
|
+
from: string | readonly string[];
|
|
203
|
+
to: string;
|
|
204
|
+
set?: Record<string, string | number | boolean | null>;
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
/**
|
|
208
|
+
* A resource's declarative state machine, keyed off one state field:
|
|
209
|
+
*
|
|
210
|
+
* ```ts
|
|
211
|
+
* transitions: {
|
|
212
|
+
* field: "status",
|
|
213
|
+
* states: ["scheduled", "boarding", "departed", "cancelled"],
|
|
214
|
+
* initial: "scheduled",
|
|
215
|
+
* on: {
|
|
216
|
+
* board: { from: "scheduled", to: "boarding" },
|
|
217
|
+
* depart: { from: "boarding", to: "departed" },
|
|
218
|
+
* cancel: { from: ["scheduled", "boarding"], to: "cancelled" },
|
|
219
|
+
* },
|
|
220
|
+
* }
|
|
221
|
+
* ```
|
|
222
|
+
*
|
|
223
|
+
* `states` becomes the field's allowed-value set (`enum` in OpenAPI, the
|
|
224
|
+
* `member` gate in the validation machine) and `initial` its default. A write
|
|
225
|
+
* that moves the field between states without a declared edge is rejected on
|
|
226
|
+
* every surface, and `useForm(Flight).transition("board")` exposes the edges
|
|
227
|
+
* as typed form actions.
|
|
228
|
+
*/
|
|
229
|
+
export interface TransitionsOptions {
|
|
230
|
+
/** The field carrying the machine state. */
|
|
231
|
+
field: string;
|
|
232
|
+
/** Every state the field may hold. */
|
|
233
|
+
states: readonly string[];
|
|
234
|
+
/** The state new records start in. */
|
|
235
|
+
initial?: string;
|
|
236
|
+
/** Named transitions. */
|
|
237
|
+
on: Record<string, TransitionEdge>;
|
|
238
|
+
}
|
|
239
|
+
|
|
135
240
|
export interface ResourceOptions {
|
|
136
241
|
/** DRF-style search fields. */
|
|
137
242
|
search?: string[];
|
|
@@ -145,33 +250,44 @@ export interface ResourceOptions {
|
|
|
145
250
|
permission?: "authenticatedOrReadOnly" | "authenticated" | "public";
|
|
146
251
|
/** Row-level security + physical tenant partitioning. */
|
|
147
252
|
access?: AccessOptions;
|
|
253
|
+
/** Declarative state machine over one field, enforced on every write surface. */
|
|
254
|
+
transitions?: TransitionsOptions;
|
|
148
255
|
}
|
|
149
256
|
|
|
150
257
|
/**
|
|
151
258
|
* A resource definition. `App`/`Model` are preserved as string-literal type
|
|
152
|
-
* arguments so a {@link Ref} to this resource carries a recoverable target
|
|
259
|
+
* arguments so a {@link Ref} to this resource carries a recoverable target,
|
|
260
|
+
* and the options object's literal type is preserved so downstream types can
|
|
261
|
+
* read the declared transition names (`useForm(Flight).transition("board")`
|
|
262
|
+
* only accepts names the resource actually declares).
|
|
153
263
|
*/
|
|
154
|
-
export interface ResourceType<
|
|
264
|
+
export interface ResourceType<
|
|
265
|
+
App extends string,
|
|
266
|
+
Model extends string,
|
|
267
|
+
F,
|
|
268
|
+
O extends ResourceOptions = ResourceOptions,
|
|
269
|
+
> {
|
|
155
270
|
readonly __rastack: "resource";
|
|
156
271
|
readonly __app: App;
|
|
157
272
|
readonly __model: Model;
|
|
158
273
|
readonly app: App;
|
|
159
274
|
readonly model: Model;
|
|
160
275
|
readonly fields: F;
|
|
161
|
-
readonly options:
|
|
276
|
+
readonly options: O;
|
|
162
277
|
}
|
|
163
278
|
|
|
164
|
-
/** Define a resource. Capture app/model as literal types
|
|
279
|
+
/** Define a resource. Capture app/model (and options) as literal types. */
|
|
165
280
|
export function resource<
|
|
166
281
|
App extends string,
|
|
167
282
|
Model extends string,
|
|
168
283
|
F extends Record<string, AnyField>,
|
|
284
|
+
const O extends ResourceOptions = ResourceOptions,
|
|
169
285
|
>(
|
|
170
286
|
app: App,
|
|
171
287
|
model: Model,
|
|
172
288
|
fields: F,
|
|
173
|
-
options:
|
|
174
|
-
): ResourceType<App, Model, F> {
|
|
289
|
+
options: O = {} as O,
|
|
290
|
+
): ResourceType<App, Model, F, O> {
|
|
175
291
|
return {
|
|
176
292
|
__rastack: "resource",
|
|
177
293
|
__app: app,
|
|
@@ -182,3 +298,91 @@ export function resource<
|
|
|
182
298
|
options,
|
|
183
299
|
};
|
|
184
300
|
}
|
|
301
|
+
|
|
302
|
+
/**
|
|
303
|
+
* The transition names an entity declares — from a `resource()` value's
|
|
304
|
+
* options, or from a class's `static transitions` block. Entities with no
|
|
305
|
+
* transitions resolve to `never`, so `form.transition(...)` is uncallable on
|
|
306
|
+
* them at the type level.
|
|
307
|
+
*/
|
|
308
|
+
export type TransitionNamesOf<E> = E extends {
|
|
309
|
+
options: { transitions: { on: infer On } };
|
|
310
|
+
}
|
|
311
|
+
? Extract<keyof On, string>
|
|
312
|
+
: E extends { transitions: { on: infer On } }
|
|
313
|
+
? Extract<keyof On, string>
|
|
314
|
+
: E extends string
|
|
315
|
+
? string // an `"app.model"` ref carries no type-level transition info
|
|
316
|
+
: never;
|
|
317
|
+
|
|
318
|
+
// ---------------------------------------------------------------------------
|
|
319
|
+
// Row inference — `useData(Flight)` knows its rows from the definition itself
|
|
320
|
+
// ---------------------------------------------------------------------------
|
|
321
|
+
|
|
322
|
+
type ScalarTsType<T extends string> = T extends "int" | "float"
|
|
323
|
+
? number
|
|
324
|
+
: T extends "bool"
|
|
325
|
+
? boolean
|
|
326
|
+
: string; // string / datetime / uuid all travel as strings
|
|
327
|
+
|
|
328
|
+
/** The TypeScript value type one field of a `resource()` definition holds. */
|
|
329
|
+
export type FieldValueOf<F> =
|
|
330
|
+
F extends Scalar<infer T, infer N>
|
|
331
|
+
? N extends true
|
|
332
|
+
? ScalarTsType<T> | null
|
|
333
|
+
: ScalarTsType<T>
|
|
334
|
+
: F extends Ref<string, string>
|
|
335
|
+
? number | string // FK values are opaque ids
|
|
336
|
+
: unknown;
|
|
337
|
+
|
|
338
|
+
/**
|
|
339
|
+
* The row type a `resource()` definition implies — what `useData(Flight)`
|
|
340
|
+
* infers with **no type argument**: each field's scalar kind maps to its TS
|
|
341
|
+
* type (`null: true` adds `| null`), FKs are ids, plus the server-assigned
|
|
342
|
+
* `id`. Classes carry their own row type, and interfaces resolve through the
|
|
343
|
+
* `EntityTypes` registry (`rastack/hooks/registry`), so every reference form
|
|
344
|
+
* infers.
|
|
345
|
+
*/
|
|
346
|
+
export type RowOf<R> =
|
|
347
|
+
R extends ResourceType<string, string, infer F, ResourceOptions>
|
|
348
|
+
? { id: number | string } & { [K in keyof F]: FieldValueOf<F[K]> }
|
|
349
|
+
: never;
|
|
350
|
+
|
|
351
|
+
/** The `"app.model"` registry key a `resource()` definition implies. */
|
|
352
|
+
export type EntityKeyOf<R> =
|
|
353
|
+
R extends ResourceType<infer A, infer M, any, ResourceOptions>
|
|
354
|
+
? `${A}.${M}`
|
|
355
|
+
: never;
|
|
356
|
+
|
|
357
|
+
/**
|
|
358
|
+
* The `EntityTypes` registry entries a module of `resource()` definitions
|
|
359
|
+
* implies — the *hand-written* alternative to the `rastack-env.d.ts` that
|
|
360
|
+
* `rastack compile` emits. Point it at your resources module once and the
|
|
361
|
+
* registry tracks it forever, with no compile step in the loop:
|
|
362
|
+
*
|
|
363
|
+
* ```ts
|
|
364
|
+
* // rastack-env.d.ts — written once by you, never regenerated
|
|
365
|
+
* import type { EntityTypesOf } from "rastack/define";
|
|
366
|
+
* declare module "rastack/hooks/registry" {
|
|
367
|
+
* interface EntityTypes
|
|
368
|
+
* extends EntityTypesOf<typeof import("./resources/airports")> {}
|
|
369
|
+
* }
|
|
370
|
+
* ```
|
|
371
|
+
*
|
|
372
|
+
* Non-resource exports are dropped, so `typeof import("…")` over a whole
|
|
373
|
+
* module is safe. (Class entities carry their own row type — they never need
|
|
374
|
+
* the registry; interfaces still register via the compile-emitted file.)
|
|
375
|
+
*/
|
|
376
|
+
export type EntityTypesOf<M> = {
|
|
377
|
+
[K in keyof M as EntityKeyOf<M[K]>]: RowOf<M[K]>;
|
|
378
|
+
};
|
|
379
|
+
|
|
380
|
+
// The runtime manifest builder — a resource() definition is its own manifest.
|
|
381
|
+
export {
|
|
382
|
+
isResourceValue,
|
|
383
|
+
manifestFromResources,
|
|
384
|
+
resourceModel,
|
|
385
|
+
toManifest,
|
|
386
|
+
} from "./manifest";
|
|
387
|
+
export type { AnyResourceValue, ManifestInput } from "./manifest";
|
|
388
|
+
export type { Manifest, ResourceModel } from "../compile/model";
|