rastack 0.0.48 → 0.0.50
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 +26 -0
- package/dist/compile/analyze.js +70 -3
- 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 +107 -13
- package/dist/define/index.js +1 -1
- package/dist/import/tabular.d.ts +8 -2
- package/dist/import/tabular.js +1 -1
- package/dist/rastack-admin.d.ts +14 -7
- package/dist/rastack-admin.js +85 -40
- 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 +209 -0
- package/hooks/entity.ts +222 -0
- package/hooks/form/entity-form.ts +354 -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 +41 -0
- package/hooks/registry.ts +39 -0
- package/package.json +1 -1
- package/provider/provider.tsx +8 -1
- package/provider/types.ts +7 -2
- package/src/compile/analyze.ts +83 -4
- 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 +148 -20
- package/src/import/tabular.ts +9 -2
- package/src/rastack-admin.ts +105 -46
- 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/transitions.spec.ts +372 -0
- package/test/typed-hooks.spec.ts +407 -0
- package/wasm/rastack_wasm.js +1 -1
- package/wasm/rastack_wasm_bg.wasm +0 -0
package/dist/define/index.d.ts
CHANGED
|
@@ -22,10 +22,19 @@
|
|
|
22
22
|
* });
|
|
23
23
|
* ```
|
|
24
24
|
*/
|
|
25
|
-
/**
|
|
26
|
-
|
|
25
|
+
/**
|
|
26
|
+
* A scalar field. The literal `__rastackScalar` type is what the compiler
|
|
27
|
+
* reads; `Nullable` phantom-carries the `null: true` option so {@link RowOf}
|
|
28
|
+
* can add `| null` to the inferred row type.
|
|
29
|
+
*/
|
|
30
|
+
export interface Scalar<T extends string, Nullable extends boolean = boolean> {
|
|
27
31
|
readonly __rastackScalar: T;
|
|
32
|
+
readonly __rastackNullable?: Nullable;
|
|
28
33
|
}
|
|
34
|
+
/** Whether an options literal declares `null: true` (as a type). */
|
|
35
|
+
type NullOf<O> = O extends {
|
|
36
|
+
null: true;
|
|
37
|
+
} ? true : false;
|
|
29
38
|
/**
|
|
30
39
|
* A foreign-key field. `App`/`Model` are carried as string-literal type
|
|
31
40
|
* arguments so the compiler can recover the relation target purely from types.
|
|
@@ -64,12 +73,12 @@ export interface DateOptions {
|
|
|
64
73
|
* runtime if a consumer wants it.
|
|
65
74
|
*/
|
|
66
75
|
export declare const s: {
|
|
67
|
-
string(_opts?:
|
|
68
|
-
int(_opts?:
|
|
69
|
-
float(_opts?:
|
|
70
|
-
bool(_opts?:
|
|
71
|
-
datetime(_opts?:
|
|
72
|
-
uuid(_opts?:
|
|
76
|
+
string<const O extends StringOptions = StringOptions>(_opts?: O): Scalar<"string", NullOf<O>>;
|
|
77
|
+
int<const O extends NumberOptions = NumberOptions>(_opts?: O): Scalar<"int", NullOf<O>>;
|
|
78
|
+
float<const O extends NumberOptions = NumberOptions>(_opts?: O): Scalar<"float", NullOf<O>>;
|
|
79
|
+
bool<const O extends BoolOptions = BoolOptions>(_opts?: O): Scalar<"bool", NullOf<O>>;
|
|
80
|
+
datetime<const O extends DateOptions = DateOptions>(_opts?: O): Scalar<"datetime", NullOf<O>>;
|
|
81
|
+
uuid<const O extends StringOptions = StringOptions>(_opts?: O): Scalar<"uuid", NullOf<O>>;
|
|
73
82
|
/**
|
|
74
83
|
* A foreign key. The target is `() => SomeResource`; the compiler resolves
|
|
75
84
|
* the relation from the *type* `Ref<App, Model>` this returns — it does not
|
|
@@ -105,6 +114,52 @@ export interface AccessOptions {
|
|
|
105
114
|
scope?: "owner" | "shared";
|
|
106
115
|
adminGroups?: string[];
|
|
107
116
|
}
|
|
117
|
+
/**
|
|
118
|
+
* One edge of a resource's state machine. `from` is the state (or states) the
|
|
119
|
+
* transition may fire from, `to` the state it enters, and `set` the effect —
|
|
120
|
+
* field patches the engine applies alongside the state change. The block is
|
|
121
|
+
* static data: it compiles into the manifest and is enforced by
|
|
122
|
+
* `rastack-api-core` on every surface (server, Lambda, in-browser WASM), so a
|
|
123
|
+
* transition behaves as a declarative, typesafe backend function with no
|
|
124
|
+
* per-resource server code.
|
|
125
|
+
*/
|
|
126
|
+
export interface TransitionEdge {
|
|
127
|
+
from: string | readonly string[];
|
|
128
|
+
to: string;
|
|
129
|
+
set?: Record<string, string | number | boolean | null>;
|
|
130
|
+
}
|
|
131
|
+
/**
|
|
132
|
+
* A resource's declarative state machine, keyed off one state field:
|
|
133
|
+
*
|
|
134
|
+
* ```ts
|
|
135
|
+
* transitions: {
|
|
136
|
+
* field: "status",
|
|
137
|
+
* states: ["scheduled", "boarding", "departed", "cancelled"],
|
|
138
|
+
* initial: "scheduled",
|
|
139
|
+
* on: {
|
|
140
|
+
* board: { from: "scheduled", to: "boarding" },
|
|
141
|
+
* depart: { from: "boarding", to: "departed" },
|
|
142
|
+
* cancel: { from: ["scheduled", "boarding"], to: "cancelled" },
|
|
143
|
+
* },
|
|
144
|
+
* }
|
|
145
|
+
* ```
|
|
146
|
+
*
|
|
147
|
+
* `states` becomes the field's allowed-value set (`enum` in OpenAPI, the
|
|
148
|
+
* `member` gate in the validation machine) and `initial` its default. A write
|
|
149
|
+
* that moves the field between states without a declared edge is rejected on
|
|
150
|
+
* every surface, and `useForm(Flight).transition("board")` exposes the edges
|
|
151
|
+
* as typed form actions.
|
|
152
|
+
*/
|
|
153
|
+
export interface TransitionsOptions {
|
|
154
|
+
/** The field carrying the machine state. */
|
|
155
|
+
field: string;
|
|
156
|
+
/** Every state the field may hold. */
|
|
157
|
+
states: readonly string[];
|
|
158
|
+
/** The state new records start in. */
|
|
159
|
+
initial?: string;
|
|
160
|
+
/** Named transitions. */
|
|
161
|
+
on: Record<string, TransitionEdge>;
|
|
162
|
+
}
|
|
108
163
|
export interface ResourceOptions {
|
|
109
164
|
/** DRF-style search fields. */
|
|
110
165
|
search?: string[];
|
|
@@ -120,19 +175,58 @@ export interface ResourceOptions {
|
|
|
120
175
|
permission?: "authenticatedOrReadOnly" | "authenticated" | "public";
|
|
121
176
|
/** Row-level security + physical tenant partitioning. */
|
|
122
177
|
access?: AccessOptions;
|
|
178
|
+
/** Declarative state machine over one field, enforced on every write surface. */
|
|
179
|
+
transitions?: TransitionsOptions;
|
|
123
180
|
}
|
|
124
181
|
/**
|
|
125
182
|
* A resource definition. `App`/`Model` are preserved as string-literal type
|
|
126
|
-
* arguments so a {@link Ref} to this resource carries a recoverable target
|
|
183
|
+
* arguments so a {@link Ref} to this resource carries a recoverable target,
|
|
184
|
+
* and the options object's literal type is preserved so downstream types can
|
|
185
|
+
* read the declared transition names (`useForm(Flight).transition("board")`
|
|
186
|
+
* only accepts names the resource actually declares).
|
|
127
187
|
*/
|
|
128
|
-
export interface ResourceType<App extends string, Model extends string, F> {
|
|
188
|
+
export interface ResourceType<App extends string, Model extends string, F, O extends ResourceOptions = ResourceOptions> {
|
|
129
189
|
readonly __rastack: "resource";
|
|
130
190
|
readonly __app: App;
|
|
131
191
|
readonly __model: Model;
|
|
132
192
|
readonly app: App;
|
|
133
193
|
readonly model: Model;
|
|
134
194
|
readonly fields: F;
|
|
135
|
-
readonly options:
|
|
195
|
+
readonly options: O;
|
|
136
196
|
}
|
|
137
|
-
/** Define a resource. Capture app/model as literal types
|
|
138
|
-
export declare function resource<App extends string, Model extends string, F extends Record<string, AnyField
|
|
197
|
+
/** Define a resource. Capture app/model (and options) as literal types. */
|
|
198
|
+
export declare function resource<App extends string, Model extends string, F extends Record<string, AnyField>, const O extends ResourceOptions = ResourceOptions>(app: App, model: Model, fields: F, options?: O): ResourceType<App, Model, F, O>;
|
|
199
|
+
/**
|
|
200
|
+
* The transition names an entity declares — from a `resource()` value's
|
|
201
|
+
* options, or from a class's `static transitions` block. Entities with no
|
|
202
|
+
* transitions resolve to `never`, so `form.transition(...)` is uncallable on
|
|
203
|
+
* them at the type level.
|
|
204
|
+
*/
|
|
205
|
+
export type TransitionNamesOf<E> = E extends {
|
|
206
|
+
options: {
|
|
207
|
+
transitions: {
|
|
208
|
+
on: infer On;
|
|
209
|
+
};
|
|
210
|
+
};
|
|
211
|
+
} ? Extract<keyof On, string> : E extends {
|
|
212
|
+
transitions: {
|
|
213
|
+
on: infer On;
|
|
214
|
+
};
|
|
215
|
+
} ? Extract<keyof On, string> : E extends string ? string : never;
|
|
216
|
+
type ScalarTsType<T extends string> = T extends "int" | "float" ? number : T extends "bool" ? boolean : string;
|
|
217
|
+
/** The TypeScript value type one field of a `resource()` definition holds. */
|
|
218
|
+
export type FieldValueOf<F> = F extends Scalar<infer T, infer N> ? N extends true ? ScalarTsType<T> | null : ScalarTsType<T> : F extends Ref<string, string> ? number | string : unknown;
|
|
219
|
+
/**
|
|
220
|
+
* The row type a `resource()` definition implies — what `useData(Flight)`
|
|
221
|
+
* infers with **no type argument**: each field's scalar kind maps to its TS
|
|
222
|
+
* type (`null: true` adds `| null`), FKs are ids, plus the server-assigned
|
|
223
|
+
* `id`. Classes carry their own row type, and interfaces resolve through the
|
|
224
|
+
* `EntityTypes` registry (`rastack/hooks/registry`), so every reference form
|
|
225
|
+
* infers.
|
|
226
|
+
*/
|
|
227
|
+
export type RowOf<R> = R extends ResourceType<string, string, infer F, ResourceOptions> ? {
|
|
228
|
+
id: number | string;
|
|
229
|
+
} & {
|
|
230
|
+
[K in keyof F]: FieldValueOf<F[K]>;
|
|
231
|
+
} : never;
|
|
232
|
+
export {};
|
package/dist/define/index.js
CHANGED
|
@@ -60,7 +60,7 @@ exports.s = {
|
|
|
60
60
|
return { __rastackRef: true };
|
|
61
61
|
},
|
|
62
62
|
};
|
|
63
|
-
/** Define a resource. Capture app/model as literal types
|
|
63
|
+
/** Define a resource. Capture app/model (and options) as literal types. */
|
|
64
64
|
function resource(app, model, fields, options = {}) {
|
|
65
65
|
return {
|
|
66
66
|
__rastack: "resource",
|
package/dist/import/tabular.d.ts
CHANGED
|
@@ -9,7 +9,7 @@
|
|
|
9
9
|
* Everything here is data-in/data-out (no filesystem), tested in
|
|
10
10
|
* `tools/test/import.spec.ts`; the parsers and the CLI are the IO shell.
|
|
11
11
|
*/
|
|
12
|
-
import { type FieldSpec, type FieldState, type RecordRun, type SpecRelation } from "../validate";
|
|
12
|
+
import { type FieldSpec, type FieldState, type RecordRun, type SpecRelation, type TransitionsModel } from "../validate";
|
|
13
13
|
/** A parsed tabular source: one header row + string cell rows. */
|
|
14
14
|
export interface TabularData {
|
|
15
15
|
headers: string[];
|
|
@@ -24,7 +24,7 @@ export interface ColumnMapping {
|
|
|
24
24
|
export interface ImportIssue {
|
|
25
25
|
row: number;
|
|
26
26
|
field: string;
|
|
27
|
-
gate: FieldState;
|
|
27
|
+
gate: FieldState | "transition";
|
|
28
28
|
constraint: string;
|
|
29
29
|
message: string;
|
|
30
30
|
}
|
|
@@ -60,6 +60,12 @@ export interface ValidateDatasetOptions {
|
|
|
60
60
|
relatedIds?: Record<string, Set<string>>;
|
|
61
61
|
/** Custom FK resolver (wins over `relatedIds`). */
|
|
62
62
|
resolveRelation?: (relation: SpecRelation, value: unknown) => boolean;
|
|
63
|
+
/**
|
|
64
|
+
* The resource's state machine (`schema.rastack.json` `transitions`).
|
|
65
|
+
* Imported rows are creates, so the record-level `transition` gate rejects
|
|
66
|
+
* rows born mid-machine (a state other than the initial one).
|
|
67
|
+
*/
|
|
68
|
+
transitions?: TransitionsModel;
|
|
63
69
|
}
|
|
64
70
|
/**
|
|
65
71
|
* Run a whole tabular source through a resource's record machine. Each row is
|
package/dist/import/tabular.js
CHANGED
|
@@ -47,7 +47,7 @@ function mapHeaders(headers, specs) {
|
|
|
47
47
|
*/
|
|
48
48
|
function validateDataset(specs, data, options = {}) {
|
|
49
49
|
const { columns, missingFields } = mapHeaders(data.headers, specs);
|
|
50
|
-
const machine = (0, validate_1.compileRecordMachine)(specs);
|
|
50
|
+
const machine = (0, validate_1.compileRecordMachine)(specs, options.transitions);
|
|
51
51
|
const ctx = (0, validate_1.createDatasetContext)(machine, options);
|
|
52
52
|
const rows = [];
|
|
53
53
|
const issues = [];
|
package/dist/rastack-admin.d.ts
CHANGED
|
@@ -1,14 +1,21 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
/**
|
|
3
|
-
* `rastack admin` — the full, Django-admin-style console, served
|
|
4
|
-
*
|
|
3
|
+
* `rastack admin` — the full, Django-admin-style console, served **in the
|
|
4
|
+
* browser over the prebuilt WebAssembly engine** (exactly like `rastack dev`).
|
|
5
5
|
*
|
|
6
6
|
* rastack admin [resourcesDir] [--warehouse <dir>] [--port <n>] [--out <dir>] [--no-open]
|
|
7
7
|
*
|
|
8
|
-
*
|
|
9
|
-
*
|
|
10
|
-
*
|
|
11
|
-
*
|
|
12
|
-
*
|
|
8
|
+
* The console is React API Stack's own React admin (`tools/admin`, built from
|
|
9
|
+
* `rastack/components` + `rastack/theme`). This command compiles the resources,
|
|
10
|
+
* then boots a dependency-free static server that hands the browser everything
|
|
11
|
+
* it needs to run the admin locally over WASM — no native server, no cargo:
|
|
12
|
+
* • the bundled React admin (`/dev/admin.js`), which defaults to `mode:"local"`,
|
|
13
|
+
* • the prebuilt WASM engine (`/wasm/…`) shipped in the package,
|
|
14
|
+
* • the compiled `schema.rastack.json` / `openapi.json`,
|
|
15
|
+
* • the committed Iceberg warehouse (`/data/warehouse/…`) to seed from.
|
|
16
|
+
*
|
|
17
|
+
* The engine is the single prebuilt `.wasm` bundle shipped in the npm package;
|
|
18
|
+
* if it is missing the command errors and points at `rastack update` — it never
|
|
19
|
+
* falls back to a from-source cargo build.
|
|
13
20
|
*/
|
|
14
21
|
export {};
|
package/dist/rastack-admin.js
CHANGED
|
@@ -1,16 +1,23 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
"use strict";
|
|
3
3
|
/**
|
|
4
|
-
* `rastack admin` — the full, Django-admin-style console, served
|
|
5
|
-
*
|
|
4
|
+
* `rastack admin` — the full, Django-admin-style console, served **in the
|
|
5
|
+
* browser over the prebuilt WebAssembly engine** (exactly like `rastack dev`).
|
|
6
6
|
*
|
|
7
7
|
* rastack admin [resourcesDir] [--warehouse <dir>] [--port <n>] [--out <dir>] [--no-open]
|
|
8
8
|
*
|
|
9
|
-
*
|
|
10
|
-
*
|
|
11
|
-
*
|
|
12
|
-
*
|
|
13
|
-
*
|
|
9
|
+
* The console is React API Stack's own React admin (`tools/admin`, built from
|
|
10
|
+
* `rastack/components` + `rastack/theme`). This command compiles the resources,
|
|
11
|
+
* then boots a dependency-free static server that hands the browser everything
|
|
12
|
+
* it needs to run the admin locally over WASM — no native server, no cargo:
|
|
13
|
+
* • the bundled React admin (`/dev/admin.js`), which defaults to `mode:"local"`,
|
|
14
|
+
* • the prebuilt WASM engine (`/wasm/…`) shipped in the package,
|
|
15
|
+
* • the compiled `schema.rastack.json` / `openapi.json`,
|
|
16
|
+
* • the committed Iceberg warehouse (`/data/warehouse/…`) to seed from.
|
|
17
|
+
*
|
|
18
|
+
* The engine is the single prebuilt `.wasm` bundle shipped in the npm package;
|
|
19
|
+
* if it is missing the command errors and points at `rastack update` — it never
|
|
20
|
+
* falls back to a from-source cargo build.
|
|
14
21
|
*/
|
|
15
22
|
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
|
|
16
23
|
if (k2 === undefined) k2 = k;
|
|
@@ -48,6 +55,7 @@ var __importStar = (this && this.__importStar) || (function () {
|
|
|
48
55
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
49
56
|
const child_process_1 = require("child_process");
|
|
50
57
|
const fs = __importStar(require("fs"));
|
|
58
|
+
const http = __importStar(require("http"));
|
|
51
59
|
const path = __importStar(require("path"));
|
|
52
60
|
const harness_1 = require("./dev/harness");
|
|
53
61
|
const HERE = __dirname; // dist/ when installed — the bundle lives beside us.
|
|
@@ -59,6 +67,42 @@ function compile(resourcesDir, outDir) {
|
|
|
59
67
|
return false;
|
|
60
68
|
return fs.existsSync(path.join(outDir, "schema.rastack.json"));
|
|
61
69
|
}
|
|
70
|
+
/** Locate the prebuilt wasm bundle dir shipped in the package (no cargo fallback). */
|
|
71
|
+
function resolveWasmDir() {
|
|
72
|
+
for (const dir of (0, harness_1.wasmBundleCandidates)(HERE)) {
|
|
73
|
+
if (fs.existsSync(path.join(dir, harness_1.WASM_ENTRY)))
|
|
74
|
+
return dir;
|
|
75
|
+
}
|
|
76
|
+
return null;
|
|
77
|
+
}
|
|
78
|
+
/** Serve one file if it exists and stays within `root`; else 404/403. */
|
|
79
|
+
function serveUnder(res, root, relPath) {
|
|
80
|
+
const safeRoot = path.resolve(root);
|
|
81
|
+
const rel = decodeURIComponent(relPath).replace(/^\/+/, "");
|
|
82
|
+
const target = path.resolve(safeRoot, rel);
|
|
83
|
+
// Path-traversal guard: the resolved target must stay inside the root.
|
|
84
|
+
if (target !== safeRoot && !target.startsWith(safeRoot + path.sep)) {
|
|
85
|
+
res.writeHead(403).end("Forbidden");
|
|
86
|
+
return;
|
|
87
|
+
}
|
|
88
|
+
fs.readFile(target, (err, data) => {
|
|
89
|
+
if (err) {
|
|
90
|
+
res.writeHead(404).end("Not found");
|
|
91
|
+
return;
|
|
92
|
+
}
|
|
93
|
+
res.writeHead(200, { "Content-Type": (0, harness_1.contentType)(target) }).end(data);
|
|
94
|
+
});
|
|
95
|
+
}
|
|
96
|
+
/** Serve an exact file (no traversal concern — a fixed internal artifact). */
|
|
97
|
+
function serveFile(res, file) {
|
|
98
|
+
fs.readFile(file, (err, data) => {
|
|
99
|
+
if (err) {
|
|
100
|
+
res.writeHead(404).end("Not found");
|
|
101
|
+
return;
|
|
102
|
+
}
|
|
103
|
+
res.writeHead(200, { "Content-Type": (0, harness_1.contentType)(file) }).end(data);
|
|
104
|
+
});
|
|
105
|
+
}
|
|
62
106
|
/** Best-effort open the default browser at `url`. */
|
|
63
107
|
function openBrowser(url) {
|
|
64
108
|
const cmd = process.platform === "darwin"
|
|
@@ -81,47 +125,48 @@ function main(argv) {
|
|
|
81
125
|
console.error(" ✗ compile failed — fix the resource graph and re-run.");
|
|
82
126
|
process.exit(1);
|
|
83
127
|
}
|
|
84
|
-
const
|
|
85
|
-
if (!
|
|
86
|
-
console.error(" ✗
|
|
128
|
+
const wasmDir = resolveWasmDir();
|
|
129
|
+
if (!wasmDir) {
|
|
130
|
+
console.error("\n ✗ No prebuilt WASM engine found in this rastack install.\n" +
|
|
131
|
+
" Update rastack to pull the prebuilt bundle:\n" +
|
|
132
|
+
" rastack update\n");
|
|
87
133
|
process.exit(1);
|
|
88
134
|
}
|
|
89
|
-
const
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
const manifest = path.join(args.outDir, "schema.rastack.json");
|
|
93
|
-
const serverArgs = [
|
|
94
|
-
"admin",
|
|
95
|
-
"--manifest",
|
|
96
|
-
manifest,
|
|
97
|
-
"--warehouse",
|
|
98
|
-
args.warehouseDir,
|
|
99
|
-
"--addr",
|
|
100
|
-
`127.0.0.1:${args.port}`,
|
|
101
|
-
// Local dev context: the admin runs unauthenticated, exactly like the old
|
|
102
|
-
// native admin. Production admins run the server with --jwks/--admin-group.
|
|
103
|
-
"--insecure-dev-auth",
|
|
104
|
-
];
|
|
105
|
-
const url = `http://127.0.0.1:${args.port}/admin/`;
|
|
106
|
-
// The server serves the React bundle from here; hand the path via env so the
|
|
107
|
-
// Rust side stays free of JS-toolchain knowledge.
|
|
108
|
-
const child = (0, child_process_1.spawn)(binary, serverArgs, {
|
|
109
|
-
stdio: "inherit",
|
|
110
|
-
env: { ...process.env, RASTACK_ADMIN_ASSETS: HERE },
|
|
111
|
-
});
|
|
112
|
-
child.on("error", () => {
|
|
113
|
-
console.error(` ✗ Could not run the Rust server (${binary}). Build it first:\n` +
|
|
114
|
-
` cd rust && cargo build --release\n`);
|
|
135
|
+
const adminBundle = path.join(HERE, "admin.js");
|
|
136
|
+
if (!fs.existsSync(adminBundle)) {
|
|
137
|
+
console.error(" ✗ admin bundle (dist/admin.js) not found — reinstall rastack.");
|
|
115
138
|
process.exit(1);
|
|
139
|
+
}
|
|
140
|
+
const server = http.createServer((req, res) => {
|
|
141
|
+
const url = (req.url || "/").split("?")[0];
|
|
142
|
+
if (req.method !== "GET") {
|
|
143
|
+
res.writeHead(405).end("Method not allowed");
|
|
144
|
+
return;
|
|
145
|
+
}
|
|
146
|
+
if (url === "/" || url === "/index.html") {
|
|
147
|
+
res
|
|
148
|
+
.writeHead(200, { "Content-Type": "text/html; charset=utf-8" })
|
|
149
|
+
.end((0, harness_1.renderShell)("React API Stack — admin"));
|
|
150
|
+
return;
|
|
151
|
+
}
|
|
152
|
+
if (url === "/dev/admin.js")
|
|
153
|
+
return serveFile(res, adminBundle);
|
|
154
|
+
if (url.startsWith("/wasm/"))
|
|
155
|
+
return serveUnder(res, wasmDir, url.slice("/wasm/".length));
|
|
156
|
+
if (url === "/schema.rastack.json" || url === "/openapi.json")
|
|
157
|
+
return serveUnder(res, args.outDir, path.basename(url));
|
|
158
|
+
if (url.startsWith("/data/warehouse/"))
|
|
159
|
+
return serveUnder(res, args.warehouseDir, url.slice("/data/warehouse/".length));
|
|
160
|
+
res.writeHead(404).end("Not found");
|
|
116
161
|
});
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
162
|
+
server.listen(args.port, () => {
|
|
163
|
+
const url = `http://localhost:${args.port}`;
|
|
164
|
+
console.log(` ✓ WASM engine: ${path.relative(process.cwd(), wasmDir)}`);
|
|
120
165
|
console.log(`\n ▸ ${url}\n`);
|
|
121
166
|
console.log(" Press Ctrl+C to stop.\n");
|
|
122
167
|
if (args.open)
|
|
123
168
|
openBrowser(url);
|
|
124
|
-
}
|
|
169
|
+
});
|
|
125
170
|
}
|
|
126
171
|
if (require.main === module) {
|
|
127
172
|
main(process.argv.slice(2));
|
package/dist/rastack-import.js
CHANGED
|
@@ -121,7 +121,10 @@ function main() {
|
|
|
121
121
|
const data = loadFile(file);
|
|
122
122
|
console.log(`\nrastack import ${path_1.default.basename(file)} → ${resourceName} ` +
|
|
123
123
|
`[${data.headers.length} column(s), ${data.rows.length} row(s)]\n`);
|
|
124
|
-
const report = (0, import_1.validateDataset)(specs, data, {
|
|
124
|
+
const report = (0, import_1.validateDataset)(specs, data, {
|
|
125
|
+
relatedIds,
|
|
126
|
+
transitions: resource.transitions,
|
|
127
|
+
});
|
|
125
128
|
printReport(report);
|
|
126
129
|
const outPath = getArg("--out");
|
|
127
130
|
if (outPath) {
|
|
@@ -36,6 +36,8 @@ function specFromManifestField(field) {
|
|
|
36
36
|
spec.maxLength = field.maxLength;
|
|
37
37
|
if (field.unique)
|
|
38
38
|
spec.unique = true;
|
|
39
|
+
if (field.options && field.options.length > 0)
|
|
40
|
+
spec.options = field.options;
|
|
39
41
|
if (field.default !== undefined)
|
|
40
42
|
spec.default = field.default;
|
|
41
43
|
if (field.type === "fk" && field.relation)
|
package/dist/validate/index.d.ts
CHANGED
package/dist/validate/index.js
CHANGED
|
@@ -16,3 +16,4 @@ var __exportStar = (this && this.__exportStar) || function(m, exports) {
|
|
|
16
16
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
17
17
|
__exportStar(require("./machine"), exports);
|
|
18
18
|
__exportStar(require("./adapters"), exports);
|
|
19
|
+
__exportStar(require("./transitions"), exports);
|
|
@@ -34,7 +34,15 @@
|
|
|
34
34
|
* single keystroke — passes through them untouched, exactly like the server,
|
|
35
35
|
* where uniqueness and FK existence are checked against the table, not the
|
|
36
36
|
* payload.
|
|
37
|
+
*
|
|
38
|
+
* Record machines add one **record-level** gate on top of the per-field
|
|
39
|
+
* chains: `transition`. A resource that declares a `transitions` block gets
|
|
40
|
+
* its state-field writes guarded against the declared edges (with the
|
|
41
|
+
* persisted row supplied via `ctx.previous`), so an illegal state jump fails
|
|
42
|
+
* a form, an import row, and a server write with the same message — see
|
|
43
|
+
* `./transitions.ts`.
|
|
37
44
|
*/
|
|
45
|
+
import type { TransitionsModel } from "../compile/model";
|
|
38
46
|
/** The `{ app, model }` a foreign-key field points at. */
|
|
39
47
|
export interface SpecRelation {
|
|
40
48
|
app: string;
|
|
@@ -107,6 +115,12 @@ export interface ValidationContext {
|
|
|
107
115
|
seen?: Map<string, Set<string>>;
|
|
108
116
|
/** Resolve whether a FK value exists on the target resource (the `resolved` gate). */
|
|
109
117
|
resolveRelation?: (relation: SpecRelation, value: unknown) => boolean;
|
|
118
|
+
/**
|
|
119
|
+
* The persisted row this record updates (the record-level `transition`
|
|
120
|
+
* guard). Absent = create semantics: the state field must hold the initial
|
|
121
|
+
* state. Only consulted by record machines compiled with a transitions block.
|
|
122
|
+
*/
|
|
123
|
+
previous?: Record<string, unknown>;
|
|
110
124
|
}
|
|
111
125
|
/** The result of running a value through a field machine. */
|
|
112
126
|
export interface FieldRun {
|
|
@@ -144,11 +158,18 @@ export declare function runFieldMachine(machine: FieldMachine, rawValue: unknown
|
|
|
144
158
|
/** A record machine is the composition of its fields' machines. */
|
|
145
159
|
export interface RecordMachine {
|
|
146
160
|
fields: FieldMachine[];
|
|
161
|
+
/**
|
|
162
|
+
* The resource's declarative state machine, when it declares one. Runs as a
|
|
163
|
+
* record-level gate after the per-field gates: a write that moves the state
|
|
164
|
+
* field without a declared edge fails at the `transition` gate.
|
|
165
|
+
*/
|
|
166
|
+
transitions?: TransitionsModel;
|
|
147
167
|
}
|
|
148
168
|
/** One field's failure inside a record run. */
|
|
149
169
|
export interface RecordError {
|
|
150
170
|
field: string;
|
|
151
|
-
gate
|
|
171
|
+
/** The gate that rejected the value — a field gate, or the record-level `transition` gate. */
|
|
172
|
+
gate: FieldState | "transition";
|
|
152
173
|
constraint: string;
|
|
153
174
|
message: string;
|
|
154
175
|
}
|
|
@@ -161,7 +182,7 @@ export interface RecordRun {
|
|
|
161
182
|
fields: Record<string, FieldRun>;
|
|
162
183
|
errors: RecordError[];
|
|
163
184
|
}
|
|
164
|
-
export declare function compileRecordMachine(specs: FieldSpec[]): RecordMachine;
|
|
185
|
+
export declare function compileRecordMachine(specs: FieldSpec[], transitions?: TransitionsModel): RecordMachine;
|
|
165
186
|
export declare function runRecordMachine(machine: RecordMachine, record: Record<string, unknown>, ctx?: ValidationContext): RecordRun;
|
|
166
187
|
/**
|
|
167
188
|
* Build the dataset-level context for a record machine: a seen-set per
|
package/dist/validate/machine.js
CHANGED
|
@@ -35,6 +35,13 @@
|
|
|
35
35
|
* single keystroke — passes through them untouched, exactly like the server,
|
|
36
36
|
* where uniqueness and FK existence are checked against the table, not the
|
|
37
37
|
* payload.
|
|
38
|
+
*
|
|
39
|
+
* Record machines add one **record-level** gate on top of the per-field
|
|
40
|
+
* chains: `transition`. A resource that declares a `transitions` block gets
|
|
41
|
+
* its state-field writes guarded against the declared edges (with the
|
|
42
|
+
* persisted row supplied via `ctx.previous`), so an illegal state jump fails
|
|
43
|
+
* a form, an import row, and a server write with the same message — see
|
|
44
|
+
* `./transitions.ts`.
|
|
38
45
|
*/
|
|
39
46
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
40
47
|
exports.GATE_ORDER = void 0;
|
|
@@ -44,6 +51,7 @@ exports.runFieldMachine = runFieldMachine;
|
|
|
44
51
|
exports.compileRecordMachine = compileRecordMachine;
|
|
45
52
|
exports.runRecordMachine = runRecordMachine;
|
|
46
53
|
exports.createDatasetContext = createDatasetContext;
|
|
54
|
+
const transitions_1 = require("./transitions");
|
|
47
55
|
/** The intermediate (gate) states, in canonical machine order. */
|
|
48
56
|
exports.GATE_ORDER = [
|
|
49
57
|
"present",
|
|
@@ -300,8 +308,11 @@ function runFieldMachine(machine, rawValue, ctx = EMPTY_CTX) {
|
|
|
300
308
|
path.push("valid");
|
|
301
309
|
return { state: "valid", path, value };
|
|
302
310
|
}
|
|
303
|
-
function compileRecordMachine(specs) {
|
|
304
|
-
|
|
311
|
+
function compileRecordMachine(specs, transitions) {
|
|
312
|
+
const machine = { fields: specs.map(compileFieldMachine) };
|
|
313
|
+
if (transitions)
|
|
314
|
+
machine.transitions = transitions;
|
|
315
|
+
return machine;
|
|
305
316
|
}
|
|
306
317
|
function runRecordMachine(machine, record, ctx = EMPTY_CTX) {
|
|
307
318
|
const fields = {};
|
|
@@ -316,6 +327,28 @@ function runRecordMachine(machine, record, ctx = EMPTY_CTX) {
|
|
|
316
327
|
errors.push({ field: name, ...run.failed });
|
|
317
328
|
}
|
|
318
329
|
}
|
|
330
|
+
// The record-level `transition` gate: with the per-field gates passed, a
|
|
331
|
+
// state-field change must follow a declared edge (against `ctx.previous`;
|
|
332
|
+
// no previous row = create semantics). A field-level failure on the state
|
|
333
|
+
// field already explains itself, so the gate only runs when that field is
|
|
334
|
+
// clean — one error per cause, like every other gate.
|
|
335
|
+
const transitions = machine.transitions;
|
|
336
|
+
if (transitions && !fields[transitions.field]?.failed) {
|
|
337
|
+
// Guard against what was actually submitted: an absent state field is not
|
|
338
|
+
// a transition, but the field machine defaults it in `values`.
|
|
339
|
+
const submitted = record[transitions.field] === undefined
|
|
340
|
+
? {}
|
|
341
|
+
: { [transitions.field]: values[transitions.field] };
|
|
342
|
+
const guard = (0, transitions_1.transitionGuard)(transitions, ctx.previous, submitted);
|
|
343
|
+
if (!guard.ok) {
|
|
344
|
+
errors.push({
|
|
345
|
+
field: transitions.field,
|
|
346
|
+
gate: "transition",
|
|
347
|
+
constraint: `transitions: ${(0, transitions_1.describeTransitions)(transitions).join("; ")}`,
|
|
348
|
+
message: guard.message,
|
|
349
|
+
});
|
|
350
|
+
}
|
|
351
|
+
}
|
|
319
352
|
return {
|
|
320
353
|
state: errors.length === 0 ? "valid" : "invalid",
|
|
321
354
|
values,
|
|
@@ -0,0 +1,86 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Declarative state transitions — the record-level layer of the constraint
|
|
3
|
+
* state machine.
|
|
4
|
+
*
|
|
5
|
+
* A resource opts in with a `transitions` block (on `resource()` options, or a
|
|
6
|
+
* `static transitions` on an entity class):
|
|
7
|
+
*
|
|
8
|
+
* ```ts
|
|
9
|
+
* transitions: {
|
|
10
|
+
* field: "status",
|
|
11
|
+
* states: ["scheduled", "boarding", "departed", "cancelled"],
|
|
12
|
+
* initial: "scheduled",
|
|
13
|
+
* on: {
|
|
14
|
+
* board: { from: "scheduled", to: "boarding" },
|
|
15
|
+
* depart: { from: "boarding", to: "departed" },
|
|
16
|
+
* cancel: { from: ["scheduled", "boarding"], to: "cancelled",
|
|
17
|
+
* set: { gate: null } },
|
|
18
|
+
* },
|
|
19
|
+
* }
|
|
20
|
+
* ```
|
|
21
|
+
*
|
|
22
|
+
* The block compiles into the manifest verbatim (with `from` normalised to a
|
|
23
|
+
* list), and this module is the *shared executable form* of it: the same pure
|
|
24
|
+
* functions guard a form submit (`useForm(...).transition("board")`), the
|
|
25
|
+
* record machine (`runRecordMachine` with a `previous` row in context), and —
|
|
26
|
+
* re-implemented 1:1 in `rastack-api-core` — every server/WASM write. A write
|
|
27
|
+
* that jumps between states without a declared edge is rejected on every
|
|
28
|
+
* surface, which is what makes a transition "a backend function that is
|
|
29
|
+
* state-machine safe": the only way to move a record between states is a
|
|
30
|
+
* named edge, and the edge's `set` patches are applied by the engine.
|
|
31
|
+
*/
|
|
32
|
+
import type { TransitionEdgeModel, TransitionsModel } from "../compile/model";
|
|
33
|
+
export type { TransitionEdgeModel, TransitionsModel } from "../compile/model";
|
|
34
|
+
/** A transition available from a given state, ready for a UI action list. */
|
|
35
|
+
export interface AvailableTransition {
|
|
36
|
+
name: string;
|
|
37
|
+
to: string;
|
|
38
|
+
set?: Record<string, unknown>;
|
|
39
|
+
}
|
|
40
|
+
/** The outcome of a transition guard or application. */
|
|
41
|
+
export type TransitionResult = {
|
|
42
|
+
ok: true;
|
|
43
|
+
patch: Record<string, unknown>;
|
|
44
|
+
} | {
|
|
45
|
+
ok: false;
|
|
46
|
+
message: string;
|
|
47
|
+
};
|
|
48
|
+
/**
|
|
49
|
+
* Normalise a raw (authored) transitions block into the canonical model shape:
|
|
50
|
+
* `from` becomes a string list, non-string states are dropped, and anything
|
|
51
|
+
* structurally broken yields `undefined` (the compiler then reports it).
|
|
52
|
+
*/
|
|
53
|
+
export declare function normalizeTransitions(raw: unknown): TransitionsModel | undefined;
|
|
54
|
+
/**
|
|
55
|
+
* Sanity-check a transitions block against its resource's fields. Returns
|
|
56
|
+
* human-readable problems (empty = valid); the compiler surfaces each as an
|
|
57
|
+
* error diagnostic, mirroring the circular-dependency check.
|
|
58
|
+
*/
|
|
59
|
+
export declare function transitionProblems(transitions: TransitionsModel, fields: Array<{
|
|
60
|
+
name: string;
|
|
61
|
+
}>): string[];
|
|
62
|
+
/** The state a record is in — its state-field value, else the initial state. */
|
|
63
|
+
export declare function currentState(transitions: TransitionsModel, record: Record<string, unknown> | undefined): string | undefined;
|
|
64
|
+
/** Every transition that may fire from `state` — the form's action list. */
|
|
65
|
+
export declare function allowedTransitions(transitions: TransitionsModel, state: unknown): AvailableTransition[];
|
|
66
|
+
/** The declared edge covering `from → to`, if any. */
|
|
67
|
+
export declare function findTransition(transitions: TransitionsModel, from: string, to: string): {
|
|
68
|
+
name: string;
|
|
69
|
+
edge: TransitionEdgeModel;
|
|
70
|
+
} | undefined;
|
|
71
|
+
/**
|
|
72
|
+
* Guard one write. `previous` is the persisted row (`undefined` = create).
|
|
73
|
+
* A create must start at the initial state; an update that changes the state
|
|
74
|
+
* field must follow a declared edge. Anything else passes untouched — the
|
|
75
|
+
* guard constrains only the state field.
|
|
76
|
+
*/
|
|
77
|
+
export declare function transitionGuard(transitions: TransitionsModel, previous: Record<string, unknown> | undefined, next: Record<string, unknown>): TransitionResult;
|
|
78
|
+
/**
|
|
79
|
+
* Fire a *named* transition from a record's current state: validates the edge
|
|
80
|
+
* is available and returns the full patch to write — the state change plus the
|
|
81
|
+
* edge's `set` effects. This is the client half of the backend function; the
|
|
82
|
+
* server re-derives the same patch from the same manifest.
|
|
83
|
+
*/
|
|
84
|
+
export declare function applyTransition(transitions: TransitionsModel, name: string, record: Record<string, unknown> | undefined): TransitionResult;
|
|
85
|
+
/** Human-readable edge list — `"board: scheduled → boarding"` — for UI hints. */
|
|
86
|
+
export declare function describeTransitions(transitions: TransitionsModel): string[];
|