rastack 0.0.43 → 0.0.45

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (41) hide show
  1. package/CHANGELOG.md +14 -0
  2. package/admin/AdminApp.tsx +461 -0
  3. package/admin/adapters.ts +346 -0
  4. package/admin/httpEngine.ts +56 -0
  5. package/admin/index.tsx +140 -0
  6. package/admin/ui/FilterBar.tsx +231 -0
  7. package/admin/ui/Popover.tsx +54 -0
  8. package/admin/ui/Sidebar.tsx +86 -0
  9. package/admin/ui/icons.tsx +146 -0
  10. package/admin/ui/styles.ts +398 -0
  11. package/admin/useAdmin.ts +434 -0
  12. package/dist/admin.js +459 -0
  13. package/dist/dev/harness.d.ts +70 -0
  14. package/dist/dev/harness.js +189 -0
  15. package/dist/rastack-admin.d.ts +14 -0
  16. package/dist/rastack-admin.js +128 -0
  17. package/dist/rastack-dev.d.ts +18 -0
  18. package/dist/rastack-dev.js +197 -0
  19. package/dist/rastack-wasm-build.d.ts +9 -4
  20. package/dist/rastack-wasm-build.js +44 -33
  21. package/dist/rastack.d.ts +2 -3
  22. package/dist/rastack.js +13 -32
  23. package/dist/wasm/package.json +4 -0
  24. package/dist/wasm/rastack_wasm.d.ts +100 -0
  25. package/dist/wasm/rastack_wasm.js +745 -0
  26. package/dist/wasm/rastack_wasm_bg.wasm +0 -0
  27. package/dist/wasm/rastack_wasm_bg.wasm.d.ts +28 -0
  28. package/package.json +6 -2
  29. package/scripts/build-admin.js +48 -0
  30. package/scripts/copy-wasm.js +35 -0
  31. package/src/dev/harness.ts +174 -0
  32. package/src/rastack-admin.ts +112 -0
  33. package/src/rastack-dev.ts +197 -0
  34. package/src/rastack-wasm-build.ts +52 -41
  35. package/src/rastack.ts +14 -37
  36. package/test/dev.spec.ts +373 -0
  37. package/wasm/package.json +4 -0
  38. package/wasm/rastack_wasm.d.ts +100 -0
  39. package/wasm/rastack_wasm.js +745 -0
  40. package/wasm/rastack_wasm_bg.wasm +0 -0
  41. package/wasm/rastack_wasm_bg.wasm.d.ts +28 -0
@@ -1,14 +1,17 @@
1
1
  #!/usr/bin/env node
2
2
 
3
3
  /**
4
- * `rastack wasm` — compile the Rust API to WebAssembly for in-browser local dev.
4
+ * Compile the Rust API to WebAssembly for in-browser local dev — the engine
5
+ * `rastack dev` serves and `<RAStackProvider mode="local">` loads.
5
6
  *
6
7
  * Pipeline:
7
8
  * 1. cargo build -p rastack-wasm --release --target wasm32-unknown-unknown
8
- * 2. wasm-bindgen --target web → stack/wasm/rastack_wasm.js + rastack_wasm_bg.wasm
9
+ * 2. wasm-bindgen --target web → tools/wasm/rastack_wasm.js + rastack_wasm_bg.wasm
9
10
  * 3. wasm-opt -Oz (if available) to shrink the binary
10
11
  *
11
- * The generated bundle is what `<RAStackProvider mode="local">` loads. Prereqs:
12
+ * This is invoked by the release CI (to bundle a prebuilt engine into the npm
13
+ * package) and by `rastack dev` as a from-source fallback when no prebuilt
14
+ * bundle is present but the toolchain is. Prereqs:
12
15
  * rustup target add wasm32-unknown-unknown
13
16
  * cargo install wasm-bindgen-cli (version must match the wasm-bindgen crate)
14
17
  */
@@ -56,47 +59,55 @@ function has(cmd: string): boolean {
56
59
  return spawnSync(cmd, ["--version"], { stdio: "ignore" }).status === 0;
57
60
  }
58
61
 
59
- console.log("→ cargo build -p rastack-wasm --release (wasm32-unknown-unknown)");
60
- run(
61
- "cargo",
62
- [
63
- "build",
64
- "-p",
65
- "rastack-wasm",
66
- "--release",
62
+ /** Run the full compile → wasm-bindgen → optimise pipeline. Throws via `run`'s
63
+ * `process.exit` on a missing tool or a non-zero step. */
64
+ export function buildWasm(): void {
65
+ console.log(
66
+ "→ cargo build -p rastack-wasm --release (wasm32-unknown-unknown)",
67
+ );
68
+ run(
69
+ "cargo",
70
+ [
71
+ "build",
72
+ "-p",
73
+ "rastack-wasm",
74
+ "--release",
75
+ "--target",
76
+ "wasm32-unknown-unknown",
77
+ ],
78
+ rustDir,
79
+ );
80
+
81
+ console.log("→ wasm-bindgen → tools/wasm/");
82
+ run("wasm-bindgen", [
67
83
  "--target",
68
- "wasm32-unknown-unknown",
69
- ],
70
- rustDir,
71
- );
84
+ "web",
85
+ "--out-dir",
86
+ outDir,
87
+ "--out-name",
88
+ "rastack_wasm",
89
+ wasmPath,
90
+ ]);
72
91
 
73
- console.log("→ wasm-bindgen → stack/wasm/");
74
- run("wasm-bindgen", [
75
- "--target",
76
- "web",
77
- "--out-dir",
78
- outDir,
79
- "--out-name",
80
- "rastack_wasm",
81
- wasmPath,
82
- ]);
92
+ // Mark the emitted bundle as ESM so Node and bundlers treat it correctly
93
+ // without forcing the parent rastack package to be a module.
94
+ fs.writeFileSync(
95
+ path.join(outDir, "package.json"),
96
+ JSON.stringify({ type: "module", sideEffects: false }, null, 2) + "\n",
97
+ );
83
98
 
84
- // Mark the emitted bundle as ESM so Node and bundlers treat it correctly
85
- // without forcing the parent rastack package to be a module.
86
- fs.writeFileSync(
87
- path.join(outDir, "package.json"),
88
- JSON.stringify({ type: "module", sideEffects: false }, null, 2) + "\n",
89
- );
99
+ if (has("wasm-opt")) {
100
+ console.log("→ wasm-opt -Oz");
101
+ const bg = path.join(outDir, "rastack_wasm_bg.wasm");
102
+ run("wasm-opt", ["-Oz", "-o", bg, bg]);
103
+ } else {
104
+ console.log("· wasm-opt not found — skipping size optimisation (optional)");
105
+ }
90
106
 
91
- if (has("wasm-opt")) {
92
- console.log("→ wasm-opt -Oz");
93
- const bg = path.join(outDir, "rastack_wasm_bg.wasm");
94
- run("wasm-opt", ["-Oz", "-o", bg, bg]);
95
- } else {
96
- console.log("· wasm-opt not found — skipping size optimisation (optional)");
107
+ const size = fs.statSync(path.join(outDir, "rastack_wasm_bg.wasm")).size;
108
+ console.log(`✓ wasm engine built: tools/wasm/ (${(size / 1024).toFixed(0)} KB)`);
97
109
  }
98
110
 
99
- const size = fs.statSync(path.join(outDir, "rastack_wasm_bg.wasm")).size;
100
- console.log(
101
- `✓ wasm engine built: stack/wasm/ (${(size / 1024).toFixed(0)} KB)`,
102
- );
111
+ if (require.main === module) {
112
+ buildWasm();
113
+ }
package/src/rastack.ts CHANGED
@@ -11,9 +11,8 @@
11
11
  * rastack generate [--schema-only] Generate typed hooks from OpenAPI
12
12
  * rastack tokens [input] [outDir] Compile design tokens → CSS vars + typed theme
13
13
  * rastack design [input] [--port n] Live design-system studio (showcase + edit)
14
- * rastack serve [--warehouse dir] Run the Rust API over an Iceberg warehouse
15
- * rastack admin [--warehouse dir] Run the Rust admin (Django-admin-style DB browser)
16
- * rastack wasm Compile the API to WebAssembly (in-browser local dev)
14
+ * rastack dev [--port n] [--warehouse d] Local dev: run the app + admin in-browser over WASM
15
+ * rastack admin [--warehouse dir] [--port n] Full admin console — React UI (rastack/components) over the native server
17
16
  * rastack scan [files...] [--fail-on-pii] Scan CSVs for PII and schema info
18
17
  * rastack seed [--env dev] [--source gs://] Load CSVs into Iceberg namespace
19
18
  * rastack bootstrap [--owner o] [--repo r] Stand the tier-0 AWS account stack up (once per account+region)
@@ -23,7 +22,7 @@
23
22
  * rastack update [<version>] [--check] Update the CLI in place (self-update)
24
23
  */
25
24
 
26
- import { execSync, spawnSync } from "child_process";
25
+ import { execSync } from "child_process";
27
26
  import { readFileSync } from "fs";
28
27
  import path from "path";
29
28
 
@@ -58,30 +57,6 @@ function run(script: string, args: string[] = rest) {
58
57
  }
59
58
  }
60
59
 
61
- /** Invoke the compiled Rust server binary (built by `cargo build` in rust/). */
62
- function runRust(subcommand: string) {
63
- const repoRoot = path.resolve(__dirname, "..", "..");
64
- const binary =
65
- process.env.RASTACK_SERVER_BIN ||
66
- path.join(repoRoot, "rust", "target", "release", "rastack-server");
67
- // Runs the locally-built Rust server binary with a fixed internal subcommand
68
- // ("serve"/"admin"); `rest` is the developer's own CLI args, not untrusted
69
- // input — no command-injection surface.
70
- // nosemgrep: javascript.lang.security.detect-child-process.detect-child-process
71
- const result = spawnSync(binary, [subcommand, ...rest], {
72
- stdio: "inherit",
73
- env: process.env,
74
- });
75
- if (result.error) {
76
- console.error(
77
- `Could not run the Rust server (${binary}). Build it first:\n` +
78
- ` cd rust && cargo build --release\n`,
79
- );
80
- process.exit(1);
81
- }
82
- process.exit(result.status ?? 0);
83
- }
84
-
85
60
  switch (command) {
86
61
  case "--version":
87
62
  case "-v":
@@ -95,15 +70,17 @@ switch (command) {
95
70
  // rastack-compile dispatches on its own first argument, so pass the command.
96
71
  run("rastack-compile.js", [command, ...rest]);
97
72
  break;
98
- case "serve":
99
- runRust("serve");
73
+ case "dev":
74
+ // Local development: serve the app + admin in the browser over the WASM
75
+ // engine (no native server, no cargo). Replaces `serve` + `wasm`.
76
+ run("rastack-dev.js", rest);
100
77
  break;
101
78
  case "admin":
102
- runRust("admin");
103
- break;
104
- case "wasm":
105
- // Compile the API to WebAssembly for in-browser local dev.
106
- run("rastack-wasm-build.js", rest);
79
+ // Full Django-admin-style console: the native Rust server serves React API
80
+ // Stack's own React admin (built from `rastack/components` + `rastack/theme`)
81
+ // over the real Iceberg warehouse. `rastack-admin.js` compiles, wires the
82
+ // bundle in, and boots the server.
83
+ run("rastack-admin.js", rest);
107
84
  break;
108
85
  case "tokens":
109
86
  // Design tokens → CSS custom properties + typed theme.
@@ -156,8 +133,8 @@ switch (command) {
156
133
  ` rastack generate [--schema-only]\n` +
157
134
  ` rastack tokens [input] [outDir] [--prefix p] Compile design tokens → CSS vars + typed theme\n` +
158
135
  ` rastack design [input] [--port n] Live design-system studio (showcase + edit)\n` +
159
- ` rastack serve | admin [--warehouse dir]\n` +
160
- ` rastack wasm Compile the API to WebAssembly (browser local dev)\n` +
136
+ ` rastack dev [--port n] [--warehouse dir] [--no-open] Run the app + admin in-browser over WASM (local dev)\n` +
137
+ ` rastack admin [--warehouse dir] [--port n] [--no-open] Full admin console (React UI over the native server; needs a built rastack-server)\n` +
161
138
  ` rastack scan [files...] [--fail-on-pii]\n` +
162
139
  ` rastack seed [--env dev|staging] [--source gs://bucket/ | ./myapp-dev/]\n` +
163
140
  ` rastack bootstrap [--owner o] [--repo r] [--region r] [--prefix p] [--reuse-oidc] [--print] Stand the AWS account stack up\n` +
@@ -0,0 +1,373 @@
1
+ import {
2
+ ADMIN_DEFAULTS,
3
+ DEV_DEFAULTS,
4
+ WASM_ENTRY,
5
+ contentType,
6
+ parseAdminArgs,
7
+ parseDevArgs,
8
+ renderShell,
9
+ wasmBundleCandidates,
10
+ } from "../src/dev/harness";
11
+ import {
12
+ boolFilterOptions,
13
+ buildWriteBody,
14
+ changelistHeaders,
15
+ coerceFormValue,
16
+ distinctValueOptions,
17
+ errorDetail,
18
+ fieldControlType,
19
+ filterableFields,
20
+ findResource,
21
+ fkFilterOptions,
22
+ formFields,
23
+ initialFormValues,
24
+ listPath,
25
+ detailPath,
26
+ optionLabel,
27
+ optionsFromRows,
28
+ pageCount,
29
+ parseManifest,
30
+ readListEnvelope,
31
+ withFilters,
32
+ PAGE_SIZE,
33
+ type AdminResource,
34
+ } from "../admin/adapters";
35
+
36
+ // -- harness core -----------------------------------------------------------
37
+
38
+ describe("rastack dev — arg parsing", () => {
39
+ it("defaults resources/out/warehouse/port and opens the browser", () => {
40
+ expect(parseDevArgs([])).toEqual(DEV_DEFAULTS);
41
+ expect(DEV_DEFAULTS.port).toBe(4321);
42
+ expect(DEV_DEFAULTS.warehouseDir).toBe("data/warehouse");
43
+ });
44
+
45
+ it("reads --port, --warehouse, --out, --no-open and a positional dir", () => {
46
+ const args = parseDevArgs([
47
+ "app/resources",
48
+ "--port",
49
+ "5000",
50
+ "--warehouse",
51
+ "data/wh",
52
+ "--out",
53
+ "build",
54
+ "--no-open",
55
+ ]);
56
+ expect(args).toEqual({
57
+ resourcesDir: "app/resources",
58
+ outDir: "build",
59
+ warehouseDir: "data/wh",
60
+ port: 5000,
61
+ open: false,
62
+ });
63
+ });
64
+
65
+ it("ignores a non-numeric --port (keeps the default)", () => {
66
+ expect(parseDevArgs(["--port", "abc"]).port).toBe(4321);
67
+ });
68
+ });
69
+
70
+ describe("rastack admin — arg parsing", () => {
71
+ it("defaults to the server's port 8000 and the same dirs as dev", () => {
72
+ expect(parseAdminArgs([])).toEqual(ADMIN_DEFAULTS);
73
+ expect(ADMIN_DEFAULTS.port).toBe(8000);
74
+ expect(ADMIN_DEFAULTS.warehouseDir).toBe("data/warehouse");
75
+ });
76
+
77
+ it("reads --port, --warehouse, --out, --no-open and a positional dir", () => {
78
+ expect(
79
+ parseAdminArgs(["res", "--port", "9100", "--warehouse", "wh", "--no-open"]),
80
+ ).toEqual({
81
+ resourcesDir: "res",
82
+ outDir: ".rastack",
83
+ warehouseDir: "wh",
84
+ port: 9100,
85
+ open: false,
86
+ });
87
+ });
88
+ });
89
+
90
+ describe("rastack dev — content types", () => {
91
+ it("maps wasm to application/wasm (required for streaming instantiation)", () => {
92
+ expect(contentType("/wasm/rastack_wasm_bg.wasm")).toBe("application/wasm");
93
+ });
94
+ it("maps the common asset types", () => {
95
+ expect(contentType("/dev/admin.js")).toBe("text/javascript; charset=utf-8");
96
+ expect(contentType("/schema.rastack.json")).toBe(
97
+ "application/json; charset=utf-8",
98
+ );
99
+ expect(contentType("a/version-hint.text")).toBe("text/plain; charset=utf-8");
100
+ expect(contentType("data/v3.parquet")).toBe("application/octet-stream");
101
+ });
102
+ it("falls back to octet-stream for unknown extensions", () => {
103
+ expect(contentType("mystery.bin")).toBe("application/octet-stream");
104
+ });
105
+ });
106
+
107
+ describe("rastack dev — wasm bundle resolution", () => {
108
+ it("includes the installed dist/wasm and repo tools/wasm locations", () => {
109
+ const dirs = wasmBundleCandidates("/opt/rastack/dist", "/repo");
110
+ expect(dirs).toContain("/opt/rastack/dist/wasm");
111
+ expect(dirs).toContain("/repo/tools/wasm");
112
+ // The entry filename the resolver probes for.
113
+ expect(WASM_ENTRY).toBe("rastack_wasm.js");
114
+ });
115
+ });
116
+
117
+ describe("rastack dev — HTML shell", () => {
118
+ it("mounts #root and loads the bundled admin", () => {
119
+ const html = renderShell();
120
+ expect(html).toContain('<div id="root">');
121
+ expect(html).toContain('<script src="/dev/admin.js">');
122
+ expect(html.startsWith("<!doctype html>")).toBe(true);
123
+ });
124
+ });
125
+
126
+ // -- admin adapters ---------------------------------------------------------
127
+
128
+ /** The in-repo airline fixtures, in manifest JSON shape. */
129
+ const MANIFEST = {
130
+ version: 1,
131
+ resources: [
132
+ {
133
+ app: "airports",
134
+ model: "airport",
135
+ fields: [
136
+ { name: "code", type: "string", maxLength: 3, unique: true },
137
+ { name: "name", type: "string", maxLength: 120 },
138
+ ],
139
+ search: ["code", "name"],
140
+ admin: { listDisplay: ["code", "name"] },
141
+ },
142
+ {
143
+ app: "airports",
144
+ model: "terminal",
145
+ fields: [
146
+ {
147
+ name: "airport",
148
+ type: "fk",
149
+ relation: { app: "airports", model: "airport" },
150
+ },
151
+ { name: "label", type: "string", maxLength: 20 },
152
+ { name: "active", type: "bool", null: true },
153
+ ],
154
+ },
155
+ {
156
+ app: "airports",
157
+ model: "secret",
158
+ fields: [{ name: "value", type: "string" }],
159
+ admin: false,
160
+ },
161
+ ],
162
+ };
163
+
164
+ describe("admin adapters — manifest parsing", () => {
165
+ const resources = parseManifest(MANIFEST);
166
+
167
+ it("parses resources, fields, search, listDisplay and admin visibility", () => {
168
+ const airport = findResource(resources, "airports", "airport")!;
169
+ expect(airport.key).toBe("airports.airport");
170
+ expect(airport.label).toBe("Airport");
171
+ expect(airport.search).toEqual(["code", "name"]);
172
+ expect(airport.listDisplay).toEqual(["code", "name"]);
173
+ expect(airport.adminEnabled).toBe(true);
174
+
175
+ const secret = findResource(resources, "airports", "secret")!;
176
+ expect(secret.adminEnabled).toBe(false);
177
+ });
178
+
179
+ it("accepts a JSON string as well as an object", () => {
180
+ expect(parseManifest(JSON.stringify(MANIFEST)).length).toBe(3);
181
+ });
182
+
183
+ it("carries FK relation + nullability on fields", () => {
184
+ const terminal = findResource(resources, "airports", "terminal")!;
185
+ const fk = terminal.fields.find((f) => f.name === "airport")!;
186
+ expect(fk.type).toBe("fk");
187
+ expect(fk.relation).toEqual({ app: "airports", model: "airport" });
188
+ const active = terminal.fields.find((f) => f.name === "active")!;
189
+ expect(active.nullable).toBe(true);
190
+ });
191
+ });
192
+
193
+ describe("admin adapters — changelist", () => {
194
+ const resources = parseManifest(MANIFEST);
195
+ const airport = findResource(resources, "airports", "airport")!;
196
+ const terminal = findResource(resources, "airports", "terminal")!;
197
+
198
+ it("prepends an id column, then listDisplay (else all fields)", () => {
199
+ expect(changelistHeaders(airport).map((h) => h.key)).toEqual([
200
+ "id",
201
+ "code",
202
+ "name",
203
+ ]);
204
+ // terminal has no listDisplay → all fields after id
205
+ expect(changelistHeaders(terminal).map((h) => h.key)).toEqual([
206
+ "id",
207
+ "airport",
208
+ "label",
209
+ "active",
210
+ ]);
211
+ });
212
+
213
+ it("uses the admin's fixed page size and correct page count", () => {
214
+ expect(PAGE_SIZE).toBe(25);
215
+ expect(pageCount(0, 25)).toBe(1);
216
+ expect(pageCount(25, 25)).toBe(1);
217
+ expect(pageCount(26, 25)).toBe(2);
218
+ });
219
+
220
+ it("reads the DRF list envelope defensively", () => {
221
+ expect(readListEnvelope({ count: 2, results: [{ id: 1 }] })).toEqual({
222
+ count: 2,
223
+ results: [{ id: 1 }],
224
+ });
225
+ expect(readListEnvelope(undefined)).toEqual({ count: 0, results: [] });
226
+ });
227
+
228
+ it("builds the REST paths the WASM engine answers", () => {
229
+ expect(listPath("airports", "airport")).toBe("/api/airports/v1/airport/");
230
+ expect(detailPath("airports", "airport", 7)).toBe(
231
+ "/api/airports/v1/airport/7/",
232
+ );
233
+ });
234
+ });
235
+
236
+ describe("admin adapters — forms", () => {
237
+ const resources = parseManifest(MANIFEST);
238
+ const airport = findResource(resources, "airports", "airport")!;
239
+ const terminal = findResource(resources, "airports", "terminal")!;
240
+
241
+ it("chooses controls from the field datatype (FK → select)", () => {
242
+ const fk = terminal.fields.find((f) => f.name === "airport")!;
243
+ const label = terminal.fields.find((f) => f.name === "label")!;
244
+ const active = terminal.fields.find((f) => f.name === "active")!;
245
+ expect(fieldControlType(fk)).toBe("select");
246
+ expect(fieldControlType(label)).toBe("text");
247
+ expect(fieldControlType(active)).toBe("boolean");
248
+ });
249
+
250
+ it("seeds initial values (blank strings, false for booleans)", () => {
251
+ expect(initialFormValues(terminal)).toEqual({
252
+ airport: "",
253
+ label: "",
254
+ active: false,
255
+ });
256
+ });
257
+
258
+ it("coerces values like the Rust admin (numbers/bools parsed, empties omitted)", () => {
259
+ const fk = terminal.fields.find((f) => f.name === "airport")!;
260
+ const active = terminal.fields.find((f) => f.name === "active")!;
261
+ const label = terminal.fields.find((f) => f.name === "label")!;
262
+ expect(coerceFormValue(fk, "12")).toBe(12);
263
+ expect(coerceFormValue(active, "true")).toBe(true);
264
+ expect(coerceFormValue(active, false)).toBe(false);
265
+ expect(coerceFormValue(label, "")).toBeUndefined(); // omitted
266
+ });
267
+
268
+ it("builds a write body, omitting empty non-bool fields", () => {
269
+ const body = buildWriteBody(terminal, {
270
+ airport: "3",
271
+ label: "",
272
+ active: true,
273
+ });
274
+ expect(body).toEqual({ airport: 3, active: true });
275
+ expect("label" in body).toBe(false);
276
+ });
277
+
278
+ it("labels FK options by the first string field, else #id", () => {
279
+ expect(optionLabel({ id: 5, code: "JFK", name: "Kennedy" })).toBe(
280
+ "JFK (#5)",
281
+ );
282
+ expect(optionLabel({ id: 9 })).toBe("#9");
283
+ expect(optionsFromRows([{ id: 1, code: "LAX" }])).toEqual([
284
+ { label: "LAX (#1)", value: 1 },
285
+ ]);
286
+ });
287
+
288
+ it("form fields exclude the primary key", () => {
289
+ const withPk: AdminResource = {
290
+ ...airport,
291
+ fields: [
292
+ { name: "id", type: "int", primaryKey: true, nullable: false },
293
+ ...airport.fields,
294
+ ],
295
+ };
296
+ expect(formFields(withPk).map((f) => f.name)).toEqual(["code", "name"]);
297
+ });
298
+
299
+ it("extracts a {detail} error message", () => {
300
+ expect(errorDetail({ detail: "nope" })).toBe("nope");
301
+ expect(errorDetail("raw string")).toBe("raw string");
302
+ expect(errorDetail({})).toBe("Request failed");
303
+ });
304
+ });
305
+
306
+ describe("admin adapters — Linear-style filters", () => {
307
+ const resources = parseManifest(MANIFEST);
308
+ const airport = findResource(resources, "airports", "airport")!;
309
+ const terminal = findResource(resources, "airports", "terminal")!;
310
+
311
+ it("offers FK, boolean and non-unique string fields as facets", () => {
312
+ // `airport`: `code` is unique (an identifier, not a facet), `name` is a
313
+ // plain string → a single `choice` facet.
314
+ expect(filterableFields(airport)).toEqual([
315
+ { name: "name", label: "Name", kind: "choice" },
316
+ ]);
317
+ // `terminal`: FK + string + bool all filterable.
318
+ expect(filterableFields(terminal)).toEqual([
319
+ {
320
+ name: "airport",
321
+ label: "Airport",
322
+ kind: "fk",
323
+ relation: { app: "airports", model: "airport" },
324
+ },
325
+ { name: "label", label: "Label", kind: "choice" },
326
+ { name: "active", label: "Active", kind: "bool" },
327
+ ]);
328
+ });
329
+
330
+ it("booleans are True/False", () => {
331
+ expect(boolFilterOptions()).toEqual([
332
+ { value: "true", label: "True" },
333
+ { value: "false", label: "False" },
334
+ ]);
335
+ });
336
+
337
+ it("distinct values become sorted options, dropping empties", () => {
338
+ const rows = [
339
+ { label: "B" },
340
+ { label: "A" },
341
+ { label: "B" },
342
+ { label: "" },
343
+ { label: null },
344
+ ];
345
+ expect(distinctValueOptions(rows, "label")).toEqual([
346
+ { value: "A", label: "A" },
347
+ { value: "B", label: "B" },
348
+ ]);
349
+ });
350
+
351
+ it("drops a high-cardinality field (no useful facet)", () => {
352
+ const rows = Array.from({ length: 5 }, (_, i) => ({ v: `x${i}` }));
353
+ expect(distinctValueOptions(rows, "v", 3)).toEqual([]);
354
+ });
355
+
356
+ it("FK options reuse the option-label heuristic against the target rows", () => {
357
+ expect(
358
+ fkFilterOptions([
359
+ { id: 1, code: "JFK" },
360
+ { id: 2, code: "LHR" },
361
+ ]),
362
+ ).toEqual([
363
+ { value: "1", label: "JFK (#1)" },
364
+ { value: "2", label: "LHR (#2)" },
365
+ ]);
366
+ });
367
+
368
+ it("merges filters under the reserved keys (page/search always win)", () => {
369
+ expect(
370
+ withFilters({ page: "2", search: "x" }, { status: "active", page: "9" }),
371
+ ).toEqual({ status: "active", page: "2", search: "x" });
372
+ });
373
+ });
@@ -0,0 +1,4 @@
1
+ {
2
+ "type": "module",
3
+ "sideEffects": false
4
+ }
@@ -0,0 +1,100 @@
1
+ /* tslint:disable */
2
+ /* eslint-disable */
3
+
4
+ /**
5
+ * The in-browser API engine. Cheap to clone (everything behind `Arc`), which
6
+ * is what lets each async call own its state without borrowing across `await`.
7
+ */
8
+ export class RastackApi {
9
+ free(): void;
10
+ [Symbol.dispose](): void;
11
+ /**
12
+ * Snapshot the entire warehouse to a blob for persistence (`data/` folder,
13
+ * IndexedDB, or an upload to S3).
14
+ */
15
+ exportWarehouse(): Promise<any>;
16
+ /**
17
+ * Handle one request. `query` is a `{ [k]: string }` object, `body` any
18
+ * JSON value (or `undefined`). Resolves to `{ status, body }`.
19
+ */
20
+ handle(method: string, path: string, query: any, body: any): Promise<any>;
21
+ /**
22
+ * Seed the warehouse from a blob previously produced by
23
+ * [`export_warehouse`](Self::export_warehouse) (the `data/` folder or an
24
+ * IndexedDB/OPFS snapshot).
25
+ */
26
+ importWarehouse(blob: Uint8Array): Promise<any>;
27
+ /**
28
+ * Build an engine from the compiled manifest (`schema.rastack.json`) and an
29
+ * optional OpenAPI document (`openapi.json`, served at `/api/schema/`).
30
+ */
31
+ constructor(manifest_json: string, openapi_json?: string | null);
32
+ /**
33
+ * Seed one Iceberg file (metadata or data file) at its warehouse key. The
34
+ * provider walks the committed `data/warehouse` Iceberg tree over HTTP and
35
+ * calls this for each file, so the browser reads the *actual* Iceberg files
36
+ * — no bundled blob.
37
+ */
38
+ seedFile(key: string, bytes: Uint8Array): Promise<any>;
39
+ /**
40
+ * Run subsequent requests as a signed-in identity (the Cognito `sub` and
41
+ * `cognito:groups` claims). Owner fields are stamped from — and row
42
+ * filters and tenant scoping keyed by — this subject, matching the server
43
+ * byte-for-byte. Pass no subject to fall back to the trusted local
44
+ * context.
45
+ */
46
+ setAuth(subject: string | null | undefined, groups: any): void;
47
+ }
48
+
49
+ export type InitInput = RequestInfo | URL | Response | BufferSource | WebAssembly.Module;
50
+
51
+ export interface InitOutput {
52
+ readonly memory: WebAssembly.Memory;
53
+ readonly __wbg_rastackapi_free: (a: number, b: number) => void;
54
+ readonly rastackapi_exportWarehouse: (a: number) => any;
55
+ readonly rastackapi_handle: (a: number, b: number, c: number, d: number, e: number, f: any, g: any) => any;
56
+ readonly rastackapi_importWarehouse: (a: number, b: number, c: number) => any;
57
+ readonly rastackapi_new: (a: number, b: number, c: number, d: number) => [number, number, number];
58
+ readonly rastackapi_seedFile: (a: number, b: number, c: number, d: number, e: number) => any;
59
+ readonly rastackapi_setAuth: (a: number, b: number, c: number, d: any) => [number, number];
60
+ readonly rust_zstd_wasm_shim_calloc: (a: number, b: number) => number;
61
+ readonly rust_zstd_wasm_shim_free: (a: number) => void;
62
+ readonly rust_zstd_wasm_shim_malloc: (a: number) => number;
63
+ readonly rust_zstd_wasm_shim_memcmp: (a: number, b: number, c: number) => number;
64
+ readonly rust_zstd_wasm_shim_memcpy: (a: number, b: number, c: number) => number;
65
+ readonly rust_zstd_wasm_shim_memmove: (a: number, b: number, c: number) => number;
66
+ readonly rust_zstd_wasm_shim_memset: (a: number, b: number, c: number) => number;
67
+ readonly rust_zstd_wasm_shim_qsort: (a: number, b: number, c: number, d: number) => void;
68
+ readonly wasm_bindgen_1a7aa9db5392c486___convert__closures_____invoke___wasm_bindgen_1a7aa9db5392c486___JsValue__core_7d5f0a2ba6a62c33___result__Result_____wasm_bindgen_1a7aa9db5392c486___JsError___true_: (a: number, b: number, c: any) => [number, number];
69
+ readonly wasm_bindgen_1a7aa9db5392c486___convert__closures_____invoke___js_sys_191ff9fb2bd47800___Function_fn_wasm_bindgen_1a7aa9db5392c486___JsValue_____wasm_bindgen_1a7aa9db5392c486___sys__Undefined___js_sys_191ff9fb2bd47800___Function_fn_wasm_bindgen_1a7aa9db5392c486___JsValue_____wasm_bindgen_1a7aa9db5392c486___sys__Undefined_______true_: (a: number, b: number, c: any, d: any) => void;
70
+ readonly __wbindgen_malloc: (a: number, b: number) => number;
71
+ readonly __wbindgen_realloc: (a: number, b: number, c: number, d: number) => number;
72
+ readonly __wbindgen_exn_store: (a: number) => void;
73
+ readonly __externref_table_alloc: () => number;
74
+ readonly __wbindgen_externrefs: WebAssembly.Table;
75
+ readonly __wbindgen_destroy_closure: (a: number, b: number) => void;
76
+ readonly __externref_table_dealloc: (a: number) => void;
77
+ readonly __wbindgen_start: () => void;
78
+ }
79
+
80
+ export type SyncInitInput = BufferSource | WebAssembly.Module;
81
+
82
+ /**
83
+ * Instantiates the given `module`, which can either be bytes or
84
+ * a precompiled `WebAssembly.Module`.
85
+ *
86
+ * @param {{ module: SyncInitInput }} module - Passing `SyncInitInput` directly is deprecated.
87
+ *
88
+ * @returns {InitOutput}
89
+ */
90
+ export function initSync(module: { module: SyncInitInput } | SyncInitInput): InitOutput;
91
+
92
+ /**
93
+ * If `module_or_path` is {RequestInfo} or {URL}, makes a request and
94
+ * for everything else, calls `WebAssembly.instantiate` directly.
95
+ *
96
+ * @param {{ module_or_path: InitInput | Promise<InitInput> }} module_or_path - Passing `InitInput` directly is deprecated.
97
+ *
98
+ * @returns {Promise<InitOutput>}
99
+ */
100
+ export default function __wbg_init (module_or_path?: { module_or_path: InitInput | Promise<InitInput> } | InitInput | Promise<InitInput>): Promise<InitOutput>;