rastack 0.0.22 → 0.0.23
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 +2 -0
- package/cache/cache.ts +1 -1
- package/cache/revocation.ts +1 -1
- package/cache/storage.ts +1 -1
- package/cache/types.ts +1 -1
- package/define.ts +2 -0
- package/dist/compile/analyze.js +3 -3
- package/dist/compile/entities.d.ts +68 -0
- package/dist/compile/entities.js +496 -0
- package/dist/compile/index.d.ts +24 -7
- package/dist/compile/index.js +48 -16
- package/dist/compile/model.d.ts +2 -2
- package/dist/compile/model.js +1 -1
- package/dist/compile/program.d.ts +2 -1
- package/dist/compile/program.js +8 -3
- package/dist/csv-schema.js +1 -1
- package/dist/define/db.d.ts +73 -0
- package/dist/define/db.js +35 -0
- package/dist/define/index.d.ts +8 -8
- package/dist/define/index.js +10 -10
- package/dist/index.js +1 -1
- package/dist/rastack-compile.d.ts +11 -0
- package/dist/{rad-compile.js → rastack-compile.js} +22 -12
- package/dist/{rad-wasm-build.d.ts → rastack-wasm-build.d.ts} +2 -2
- package/dist/{rad-wasm-build.js → rastack-wasm-build.js} +6 -6
- package/dist/rastack.d.ts +17 -0
- package/dist/{rad.js → rastack.js} +24 -24
- package/dist/scan.d.ts +3 -3
- package/dist/scan.js +4 -4
- package/dist/seed.d.ts +2 -2
- package/dist/seed.js +9 -9
- package/hooks/real-time/pusher.ts +2 -2
- package/package.json +2 -3
- package/provider/provider.tsx +12 -12
- package/provider/types.ts +16 -16
- package/provider/warehouse.ts +1 -1
- package/provider/wasm.ts +10 -10
- package/src/compile/analyze.ts +3 -3
- package/src/compile/entities.ts +602 -0
- package/src/compile/index.ts +57 -15
- package/src/compile/model.ts +2 -2
- package/src/compile/program.ts +8 -3
- package/src/csv-schema.ts +1 -1
- package/src/define/db.ts +64 -0
- package/src/define/index.ts +16 -16
- package/src/index.ts +1 -1
- package/src/{rad-compile.ts → rastack-compile.ts} +30 -13
- package/src/{rad-wasm-build.ts → rastack-wasm-build.ts} +6 -6
- package/src/{rad.ts → rastack.ts} +24 -24
- package/src/scan.ts +4 -4
- package/src/seed.ts +9 -9
- package/sync/engine.ts +1 -1
- package/sync/types.ts +1 -1
- package/test/cache.spec.ts +1 -1
- package/test/compile.spec.ts +6 -6
- package/test/csv-schema.spec.ts +2 -2
- package/test/entities.spec.ts +381 -0
- package/types.ts +3 -0
- package/dist/rad-compile.d.ts +0 -11
- package/dist/rad.d.ts +0 -17
package/src/compile/index.ts
CHANGED
|
@@ -1,12 +1,15 @@
|
|
|
1
1
|
import * as fs from "fs";
|
|
2
2
|
import * as path from "path";
|
|
3
3
|
import { analyze } from "./analyze";
|
|
4
|
+
import { analyzeEntities, detectCycles, EntityDiagnostic } from "./entities";
|
|
4
5
|
import { buildManifest, manifestToJson } from "./manifest";
|
|
5
6
|
import { Manifest, ResourceModel } from "./model";
|
|
6
7
|
import { buildOpenApi, openApiToJson } from "./openapi";
|
|
7
8
|
import { createProgram } from "./program";
|
|
8
9
|
|
|
9
10
|
export { analyze } from "./analyze";
|
|
11
|
+
export { analyzeEntities, detectCycles } from "./entities";
|
|
12
|
+
export type { EntityAnalysis, EntityDiagnostic } from "./entities";
|
|
10
13
|
export { buildManifest } from "./manifest";
|
|
11
14
|
export { buildOpenApi } from "./openapi";
|
|
12
15
|
export * from "./model";
|
|
@@ -15,6 +18,7 @@ export interface CompileResult {
|
|
|
15
18
|
manifest: Manifest;
|
|
16
19
|
openapi: any;
|
|
17
20
|
files: string[];
|
|
21
|
+
diagnostics: EntityDiagnostic[];
|
|
18
22
|
}
|
|
19
23
|
|
|
20
24
|
/** Directories never worth walking for resource definitions. */
|
|
@@ -22,19 +26,20 @@ const IGNORE_DIRS = new Set([
|
|
|
22
26
|
"node_modules",
|
|
23
27
|
".git",
|
|
24
28
|
".expo",
|
|
25
|
-
".rad",
|
|
29
|
+
".rad", // legacy output dir
|
|
30
|
+
".rastack",
|
|
26
31
|
"dist",
|
|
27
32
|
"web-build",
|
|
28
33
|
]);
|
|
29
34
|
|
|
30
35
|
/**
|
|
31
|
-
* Every `.ts`
|
|
32
|
-
* file. Recursion is what lets resource definitions live **co-located
|
|
33
|
-
* pages that consume them** — e.g. `app/airports/_schema.
|
|
34
|
-
* `app/airports/index.tsx` — instead of in one top-level `resources/`
|
|
35
|
-
* `.tsx`
|
|
36
|
-
* `
|
|
37
|
-
* harmless.
|
|
36
|
+
* Every `.ts`/`.tsx` source file under a directory (recursively), or an
|
|
37
|
+
* explicit file. Recursion is what lets resource definitions live **co-located
|
|
38
|
+
* with the pages that consume them** — e.g. `app/airports/_schema.rastack.ts`
|
|
39
|
+
* beside `app/airports/index.tsx` — instead of in one top-level `resources/`
|
|
40
|
+
* folder. `.tsx` files carry no `resource(...)` definitions but are scanned
|
|
41
|
+
* for the typed hook calls (`useEntityList<Airport>(…)`) that root the
|
|
42
|
+
* interface-driven entity graph; co-located helpers are harmless.
|
|
38
43
|
*/
|
|
39
44
|
export function resolveResourceFiles(input: string): string[] {
|
|
40
45
|
const stat = fs.existsSync(input) ? fs.statSync(input) : undefined;
|
|
@@ -45,7 +50,7 @@ export function resolveResourceFiles(input: string): string[] {
|
|
|
45
50
|
for (const entry of fs.readdirSync(dir, { withFileTypes: true })) {
|
|
46
51
|
if (entry.isDirectory()) {
|
|
47
52
|
if (!IGNORE_DIRS.has(entry.name)) walk(path.join(dir, entry.name));
|
|
48
|
-
} else if (entry.name
|
|
53
|
+
} else if (/\.tsx?$/.test(entry.name) && !entry.name.endsWith(".d.ts")) {
|
|
49
54
|
files.push(path.resolve(dir, entry.name));
|
|
50
55
|
}
|
|
51
56
|
}
|
|
@@ -56,26 +61,63 @@ export function resolveResourceFiles(input: string): string[] {
|
|
|
56
61
|
throw new Error(`No such resources path: ${input}`);
|
|
57
62
|
}
|
|
58
63
|
|
|
59
|
-
|
|
60
|
-
|
|
64
|
+
export interface ProjectAnalysis {
|
|
65
|
+
resources: ResourceModel[];
|
|
66
|
+
diagnostics: EntityDiagnostic[];
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
/**
|
|
70
|
+
* Analyse a project into the canonical model: explicit `resource()` DSL
|
|
71
|
+
* definitions plus resources inferred from hook-rooted TypeScript interfaces.
|
|
72
|
+
* When both surfaces define the same `app.model`, the explicit DSL definition
|
|
73
|
+
* wins — hook usage of a generated `IAirport` interface resolves back onto the
|
|
74
|
+
* authored resource instead of creating a duplicate. Cycle detection runs over
|
|
75
|
+
* the merged FK graph.
|
|
76
|
+
*/
|
|
77
|
+
export function analyzeProject(input: string): ProjectAnalysis {
|
|
61
78
|
const files = resolveResourceFiles(input);
|
|
62
79
|
const program = createProgram(files);
|
|
63
|
-
|
|
80
|
+
|
|
81
|
+
const fromDsl = analyze(program, files);
|
|
82
|
+
const fromInterfaces = analyzeEntities(program, files);
|
|
83
|
+
|
|
84
|
+
const defined = new Set(fromDsl.map((r) => `${r.app}.${r.model}`));
|
|
85
|
+
const resources = [
|
|
86
|
+
...fromDsl,
|
|
87
|
+
...fromInterfaces.resources.filter((r) => !defined.has(`${r.app}.${r.model}`)),
|
|
88
|
+
];
|
|
89
|
+
resources.sort((a, b) =>
|
|
90
|
+
a.app === b.app ? a.model.localeCompare(b.model) : a.app.localeCompare(b.app),
|
|
91
|
+
);
|
|
92
|
+
|
|
93
|
+
return {
|
|
94
|
+
resources,
|
|
95
|
+
diagnostics: [...fromInterfaces.diagnostics, ...detectCycles(resources)],
|
|
96
|
+
};
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
/** Analyse resource definitions into the canonical model (no file output). */
|
|
100
|
+
export function analyzeResources(input: string): ResourceModel[] {
|
|
101
|
+
return analyzeProject(input).resources;
|
|
64
102
|
}
|
|
65
103
|
|
|
66
104
|
/** Compile resource definitions to a manifest + OpenAPI, writing both to disk. */
|
|
67
105
|
export function compile(input: string, outDir: string): CompileResult {
|
|
68
|
-
const resources =
|
|
106
|
+
const { resources, diagnostics } = analyzeProject(input);
|
|
107
|
+
const errors = diagnostics.filter((d) => d.severity === "error");
|
|
108
|
+
if (errors.length) {
|
|
109
|
+
throw new Error(errors.map((e) => e.message).join("\n"));
|
|
110
|
+
}
|
|
69
111
|
const manifest = buildManifest(resources);
|
|
70
112
|
const openapi = buildOpenApi(resources);
|
|
71
113
|
|
|
72
114
|
fs.mkdirSync(outDir, { recursive: true });
|
|
73
|
-
const manifestPath = path.join(outDir, "schema.
|
|
115
|
+
const manifestPath = path.join(outDir, "schema.rastack.json");
|
|
74
116
|
const openapiPath = path.join(outDir, "openapi.json");
|
|
75
117
|
fs.writeFileSync(manifestPath, manifestToJson(manifest), { flag: "w" });
|
|
76
118
|
fs.writeFileSync(openapiPath, openApiToJson(openapi), { flag: "w" });
|
|
77
119
|
|
|
78
|
-
return { manifest, openapi, files: [manifestPath, openapiPath] };
|
|
120
|
+
return { manifest, openapi, files: [manifestPath, openapiPath], diagnostics };
|
|
79
121
|
}
|
|
80
122
|
|
|
81
123
|
// -- introspection (replaces `manage.py rastack check|list|urls`) -----------
|
package/src/compile/model.ts
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* The canonical, backend-agnostic data model produced by the compiler. This is
|
|
3
|
-
* what `schema.
|
|
3
|
+
* what `schema.rastack.json` serializes and what both the OpenAPI emitter and the
|
|
4
4
|
* Rust API consume.
|
|
5
5
|
*/
|
|
6
6
|
|
|
@@ -52,7 +52,7 @@ export interface AdminModel {
|
|
|
52
52
|
}
|
|
53
53
|
|
|
54
54
|
/**
|
|
55
|
-
* Row-level access control, enforced by `
|
|
55
|
+
* Row-level access control, enforced by `rastack-api-core` on every surface.
|
|
56
56
|
* `ownerField` names the field stamped with the authenticated subject;
|
|
57
57
|
* `scope: "owner"` places the table under a per-identity warehouse prefix
|
|
58
58
|
* (`tenants/{sub}/…`) so object-storage IAM can fence data per user.
|
package/src/compile/program.ts
CHANGED
|
@@ -3,7 +3,8 @@ import * as ts from "typescript";
|
|
|
3
3
|
|
|
4
4
|
/**
|
|
5
5
|
* Build a `ts.Program` (and thus a `TypeChecker`) over a set of resource
|
|
6
|
-
* definition files. The
|
|
6
|
+
* definition files. The authoring subpaths (`rastack/define` for the DSL,
|
|
7
|
+
* `rastack/types` for the `DbConfig` column brands) are mapped to the
|
|
7
8
|
* in-tree/installed sources via `paths` so both the shipped package name and
|
|
8
9
|
* the local demo/tests resolve without extra linking.
|
|
9
10
|
*/
|
|
@@ -18,10 +19,14 @@ export function createProgram(fileNames: string[]): ts.Program {
|
|
|
18
19
|
skipLibCheck: true,
|
|
19
20
|
esModuleInterop: true,
|
|
20
21
|
noEmit: true,
|
|
22
|
+
// `.tsx` app files are scanned for the typed hook calls that root the
|
|
23
|
+
// interface-driven entity graph.
|
|
24
|
+
jsx: ts.JsxEmit.ReactJSX,
|
|
21
25
|
baseUrl: process.cwd(),
|
|
22
26
|
paths: {
|
|
23
|
-
"
|
|
24
|
-
"
|
|
27
|
+
"rastack/define": [defineDir],
|
|
28
|
+
"rastack/define/*": [defineDir + "/*"],
|
|
29
|
+
"rastack/types": [path.join(defineDir, "db")],
|
|
25
30
|
},
|
|
26
31
|
};
|
|
27
32
|
|
package/src/csv-schema.ts
CHANGED
|
@@ -47,7 +47,7 @@ const DEFAULT_PII_PATTERNS = [
|
|
|
47
47
|
];
|
|
48
48
|
|
|
49
49
|
function loadPiiConfig(repoRoot: string): PiiConfig {
|
|
50
|
-
const configPath = path.join(repoRoot, ".
|
|
50
|
+
const configPath = path.join(repoRoot, ".rastack", "pii-patterns.yml");
|
|
51
51
|
if (!fs.existsSync(configPath)) {
|
|
52
52
|
return { patterns: DEFAULT_PII_PATTERNS, luhn_check: true, require_policy: true };
|
|
53
53
|
}
|
package/src/define/db.ts
ADDED
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Type-level column configuration for interface-driven entities.
|
|
3
|
+
*
|
|
4
|
+
* Interface entities are plain TypeScript, so their column constraints are
|
|
5
|
+
* expressed *in the type system*: intersect a property type with one or more
|
|
6
|
+
* {@link DbConfig} brands and the compiler reads the configuration straight
|
|
7
|
+
* off the type — the same way it reads foreign keys.
|
|
8
|
+
*
|
|
9
|
+
* ```ts
|
|
10
|
+
* import { Default, Int, MaxLength, PrimaryKey, Unique, UUID } from "rastack/types";
|
|
11
|
+
*
|
|
12
|
+
* export interface Passenger {
|
|
13
|
+
* id: UUID & PrimaryKey;
|
|
14
|
+
* seat: string & MaxLength<4> & Unique;
|
|
15
|
+
* age: Int; // number maps to float; Int refines it
|
|
16
|
+
* status: string & Default<"active">;
|
|
17
|
+
* }
|
|
18
|
+
* ```
|
|
19
|
+
*
|
|
20
|
+
* Recurring configurations can extend {@link DbConfig} once and be reused:
|
|
21
|
+
*
|
|
22
|
+
* ```ts
|
|
23
|
+
* interface Registration extends DbConfig<{ maxLength: 10; unique: true }> {}
|
|
24
|
+
*
|
|
25
|
+
* export interface Aircraft {
|
|
26
|
+
* registration: string & Registration;
|
|
27
|
+
* }
|
|
28
|
+
* ```
|
|
29
|
+
*
|
|
30
|
+
* The brands are "flavors", not hard brands: the phantom `__rastackDb` property is
|
|
31
|
+
* optional and never exists at runtime, so a plain `"3F"` stays assignable to
|
|
32
|
+
* `string & MaxLength<4> & Unique` — no casts anywhere in app code.
|
|
33
|
+
*/
|
|
34
|
+
|
|
35
|
+
/**
|
|
36
|
+
* The phantom carrier every constraint type extends. `C` holds the column
|
|
37
|
+
* configuration as literal types; the compiler recovers it from the `__rastackDb`
|
|
38
|
+
* property of the intersected field type.
|
|
39
|
+
*/
|
|
40
|
+
export interface DbConfig<C = unknown> {
|
|
41
|
+
readonly __rastackDb?: C;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
/** The column value must be unique. */
|
|
45
|
+
export interface Unique extends DbConfig<{ unique: true }> {}
|
|
46
|
+
|
|
47
|
+
/** The column is the primary key. */
|
|
48
|
+
export interface PrimaryKey extends DbConfig<{ primaryKey: true }> {}
|
|
49
|
+
|
|
50
|
+
/** Maximum length for string columns: `string & MaxLength<120>`. */
|
|
51
|
+
export interface MaxLength<N extends number> extends DbConfig<{ maxLength: N }> {}
|
|
52
|
+
|
|
53
|
+
/** Server-side default value: `string & Default<"scheduled">`, `number & Default<0>`. */
|
|
54
|
+
export interface Default<V extends string | number | boolean>
|
|
55
|
+
extends DbConfig<{ default: V }> {}
|
|
56
|
+
|
|
57
|
+
/** An integer column — a bare `number` property maps to `float`. */
|
|
58
|
+
export type Int = number & DbConfig<{ int: true }>;
|
|
59
|
+
|
|
60
|
+
/** A UUID column, stored as a string. */
|
|
61
|
+
export type UUID = string & DbConfig<{ uuid: true }>;
|
|
62
|
+
|
|
63
|
+
/** A datetime column authored as a string (a `Date` property maps directly). */
|
|
64
|
+
export type DateTimeString = string & DbConfig<{ datetime: true }>;
|
package/src/define/index.ts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
/**
|
|
2
|
-
*
|
|
2
|
+
* rastack authoring DSL — the TypeScript-first source of truth.
|
|
3
3
|
*
|
|
4
4
|
* Resources are authored as plain TypeScript. The compiler
|
|
5
5
|
* (`tools/src/compile`) reads these definitions and, crucially, **infers
|
|
@@ -9,7 +9,7 @@
|
|
|
9
9
|
* `Ref`'s string-literal type arguments.
|
|
10
10
|
*
|
|
11
11
|
* ```ts
|
|
12
|
-
* import { resource, s } from "
|
|
12
|
+
* import { resource, s } from "rastack/define";
|
|
13
13
|
*
|
|
14
14
|
* export const Airport = resource("airports", "airport", {
|
|
15
15
|
* code: s.string({ maxLength: 3, unique: true }),
|
|
@@ -23,9 +23,9 @@
|
|
|
23
23
|
* ```
|
|
24
24
|
*/
|
|
25
25
|
|
|
26
|
-
/** A scalar field. The literal `
|
|
26
|
+
/** A scalar field. The literal `__rastackScalar` type is what the compiler reads. */
|
|
27
27
|
export interface Scalar<T extends string> {
|
|
28
|
-
readonly
|
|
28
|
+
readonly __rastackScalar: T;
|
|
29
29
|
}
|
|
30
30
|
|
|
31
31
|
/**
|
|
@@ -33,7 +33,7 @@ export interface Scalar<T extends string> {
|
|
|
33
33
|
* arguments so the compiler can recover the relation target purely from types.
|
|
34
34
|
*/
|
|
35
35
|
export interface Ref<App extends string, Model extends string> {
|
|
36
|
-
readonly
|
|
36
|
+
readonly __rastackRef: true;
|
|
37
37
|
readonly __app: App;
|
|
38
38
|
readonly __model: Model;
|
|
39
39
|
}
|
|
@@ -72,22 +72,22 @@ export interface DateOptions {
|
|
|
72
72
|
*/
|
|
73
73
|
export const s = {
|
|
74
74
|
string(_opts?: StringOptions): Scalar<"string"> {
|
|
75
|
-
return {
|
|
75
|
+
return { __rastackScalar: "string" } as Scalar<"string">;
|
|
76
76
|
},
|
|
77
77
|
int(_opts?: NumberOptions): Scalar<"int"> {
|
|
78
|
-
return {
|
|
78
|
+
return { __rastackScalar: "int" } as Scalar<"int">;
|
|
79
79
|
},
|
|
80
80
|
float(_opts?: NumberOptions): Scalar<"float"> {
|
|
81
|
-
return {
|
|
81
|
+
return { __rastackScalar: "float" } as Scalar<"float">;
|
|
82
82
|
},
|
|
83
83
|
bool(_opts?: BoolOptions): Scalar<"bool"> {
|
|
84
|
-
return {
|
|
84
|
+
return { __rastackScalar: "bool" } as Scalar<"bool">;
|
|
85
85
|
},
|
|
86
86
|
datetime(_opts?: DateOptions): Scalar<"datetime"> {
|
|
87
|
-
return {
|
|
87
|
+
return { __rastackScalar: "datetime" } as Scalar<"datetime">;
|
|
88
88
|
},
|
|
89
89
|
uuid(_opts?: StringOptions): Scalar<"uuid"> {
|
|
90
|
-
return {
|
|
90
|
+
return { __rastackScalar: "uuid" } as Scalar<"uuid">;
|
|
91
91
|
},
|
|
92
92
|
/**
|
|
93
93
|
* A foreign key. The target is `() => SomeResource`; the compiler resolves
|
|
@@ -97,7 +97,7 @@ export const s = {
|
|
|
97
97
|
ref<A extends string, M extends string>(
|
|
98
98
|
_thunk: () => ResourceType<A, M, any>,
|
|
99
99
|
): Ref<A, M> {
|
|
100
|
-
return {
|
|
100
|
+
return { __rastackRef: true } as Ref<A, M>;
|
|
101
101
|
},
|
|
102
102
|
};
|
|
103
103
|
|
|
@@ -113,8 +113,8 @@ export interface SyncOptions {
|
|
|
113
113
|
}
|
|
114
114
|
|
|
115
115
|
/**
|
|
116
|
-
* Row-level access control. Enforced by `
|
|
117
|
-
* identical on the server (Lambda / `
|
|
116
|
+
* Row-level access control. Enforced by `rastack-api-core`, so the rules are
|
|
117
|
+
* identical on the server (Lambda / `rastack serve`) and in the in-browser WASM
|
|
118
118
|
* engine.
|
|
119
119
|
*
|
|
120
120
|
* - `ownerField` — the field that records the owning identity (the verified
|
|
@@ -152,7 +152,7 @@ export interface ResourceOptions {
|
|
|
152
152
|
* arguments so a {@link Ref} to this resource carries a recoverable target.
|
|
153
153
|
*/
|
|
154
154
|
export interface ResourceType<App extends string, Model extends string, F> {
|
|
155
|
-
readonly
|
|
155
|
+
readonly __rastack: "resource";
|
|
156
156
|
readonly __app: App;
|
|
157
157
|
readonly __model: Model;
|
|
158
158
|
readonly app: App;
|
|
@@ -173,7 +173,7 @@ export function resource<
|
|
|
173
173
|
options: ResourceOptions = {},
|
|
174
174
|
): ResourceType<App, Model, F> {
|
|
175
175
|
return {
|
|
176
|
-
|
|
176
|
+
__rastack: "resource",
|
|
177
177
|
__app: app,
|
|
178
178
|
__model: model,
|
|
179
179
|
app,
|
package/src/index.ts
CHANGED
|
@@ -9,7 +9,7 @@ const schemaOnly = args.includes("--schema-only");
|
|
|
9
9
|
|
|
10
10
|
const currentDir = process.cwd(); // Get the current working directory
|
|
11
11
|
const kodeDir = path.join(currentDir, ".kode");
|
|
12
|
-
const configFile = path.join(kodeDir, "generate-
|
|
12
|
+
const configFile = path.join(kodeDir, "generate-rastack-sdk.json");
|
|
13
13
|
let config: any = {};
|
|
14
14
|
|
|
15
15
|
// Create the .kode directory if it doesn't exist
|
|
@@ -1,19 +1,25 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
|
|
3
3
|
/**
|
|
4
|
-
* `
|
|
4
|
+
* `rastack compile | check | list | urls` — the TypeScript-first replacement for the
|
|
5
5
|
* old Django `manage.py rastack` command.
|
|
6
6
|
*
|
|
7
|
-
*
|
|
8
|
-
*
|
|
9
|
-
*
|
|
10
|
-
*
|
|
7
|
+
* rastack compile [resourcesDir] [outDir] defs → schema.rastack.json + openapi.json
|
|
8
|
+
* rastack check [resourcesDir] validate the resource graph (exit != 0 on error)
|
|
9
|
+
* rastack list [resourcesDir] print every resource + its fields/relations
|
|
10
|
+
* rastack urls [resourcesDir] print the generated /api/{app}/v1/{model}/ routes
|
|
11
11
|
*/
|
|
12
12
|
|
|
13
|
-
import {
|
|
13
|
+
import {
|
|
14
|
+
analyzeProject,
|
|
15
|
+
analyzeResources,
|
|
16
|
+
collisions,
|
|
17
|
+
compile,
|
|
18
|
+
urlsFor,
|
|
19
|
+
} from "./compile";
|
|
14
20
|
|
|
15
21
|
const DEFAULT_RESOURCES = "resources";
|
|
16
|
-
const DEFAULT_OUT = ".
|
|
22
|
+
const DEFAULT_OUT = ".rastack";
|
|
17
23
|
|
|
18
24
|
function main(argv: string[]): void {
|
|
19
25
|
const [sub, ...rest] = argv;
|
|
@@ -23,14 +29,25 @@ function main(argv: string[]): void {
|
|
|
23
29
|
const resourcesDir = rest[0] || DEFAULT_RESOURCES;
|
|
24
30
|
const outDir = rest[1] || DEFAULT_OUT;
|
|
25
31
|
const result = compile(resourcesDir, outDir);
|
|
32
|
+
for (const d of result.diagnostics) console.warn(` ⚠ ${d.message}`);
|
|
26
33
|
console.log(
|
|
27
34
|
`✓ compiled ${result.manifest.resources.length} resource(s) → ${result.files.join(", ")}`,
|
|
28
35
|
);
|
|
29
36
|
break;
|
|
30
37
|
}
|
|
31
38
|
case "check": {
|
|
32
|
-
const resources =
|
|
33
|
-
|
|
39
|
+
const { resources, diagnostics } = analyzeProject(
|
|
40
|
+
rest[0] || DEFAULT_RESOURCES,
|
|
41
|
+
);
|
|
42
|
+
const errors = [
|
|
43
|
+
...collisions(resources),
|
|
44
|
+
...diagnostics
|
|
45
|
+
.filter((d) => d.severity === "error")
|
|
46
|
+
.map((d) => d.message),
|
|
47
|
+
];
|
|
48
|
+
for (const d of diagnostics.filter((d) => d.severity === "warning")) {
|
|
49
|
+
console.warn(` ⚠ ${d.message}`);
|
|
50
|
+
}
|
|
34
51
|
if (errors.length) {
|
|
35
52
|
for (const e of errors) console.error(` ✗ ${e}`);
|
|
36
53
|
console.error(`${errors.length} error(s) found.`);
|
|
@@ -74,10 +91,10 @@ function main(argv: string[]): void {
|
|
|
74
91
|
console.error(
|
|
75
92
|
`Unknown compile subcommand: "${sub}"\n\n` +
|
|
76
93
|
`Usage:\n` +
|
|
77
|
-
`
|
|
78
|
-
`
|
|
79
|
-
`
|
|
80
|
-
`
|
|
94
|
+
` rastack compile [resourcesDir] [outDir]\n` +
|
|
95
|
+
` rastack check [resourcesDir]\n` +
|
|
96
|
+
` rastack list [resourcesDir]\n` +
|
|
97
|
+
` rastack urls [resourcesDir]\n`,
|
|
81
98
|
);
|
|
82
99
|
process.exit(1);
|
|
83
100
|
}
|
|
@@ -1,11 +1,11 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
|
|
3
3
|
/**
|
|
4
|
-
* `
|
|
4
|
+
* `rastack wasm` — compile the Rust API to WebAssembly for in-browser local dev.
|
|
5
5
|
*
|
|
6
6
|
* Pipeline:
|
|
7
7
|
* 1. cargo build -p rad-wasm --release --target wasm32-unknown-unknown
|
|
8
|
-
* 2. wasm-bindgen --target web → stack/wasm/
|
|
8
|
+
* 2. wasm-bindgen --target web → stack/wasm/rastack_wasm.js + rastack_wasm_bg.wasm
|
|
9
9
|
* 3. wasm-opt -Oz (if available) to shrink the binary
|
|
10
10
|
*
|
|
11
11
|
* The generated bundle is what `<RAStackProvider mode="local">` loads. Prereqs:
|
|
@@ -25,7 +25,7 @@ const wasmPath = path.join(
|
|
|
25
25
|
"target",
|
|
26
26
|
"wasm32-unknown-unknown",
|
|
27
27
|
"release",
|
|
28
|
-
"
|
|
28
|
+
"rastack_wasm.wasm",
|
|
29
29
|
);
|
|
30
30
|
|
|
31
31
|
function run(cmd: string, args: string[], cwd = repoRoot): void {
|
|
@@ -77,7 +77,7 @@ run("wasm-bindgen", [
|
|
|
77
77
|
"--out-dir",
|
|
78
78
|
outDir,
|
|
79
79
|
"--out-name",
|
|
80
|
-
"
|
|
80
|
+
"rastack_wasm",
|
|
81
81
|
wasmPath,
|
|
82
82
|
]);
|
|
83
83
|
|
|
@@ -90,13 +90,13 @@ fs.writeFileSync(
|
|
|
90
90
|
|
|
91
91
|
if (has("wasm-opt")) {
|
|
92
92
|
console.log("→ wasm-opt -Oz");
|
|
93
|
-
const bg = path.join(outDir, "
|
|
93
|
+
const bg = path.join(outDir, "rastack_wasm_bg.wasm");
|
|
94
94
|
run("wasm-opt", ["-Oz", "-o", bg, bg]);
|
|
95
95
|
} else {
|
|
96
96
|
console.log("· wasm-opt not found — skipping size optimisation (optional)");
|
|
97
97
|
}
|
|
98
98
|
|
|
99
|
-
const size = fs.statSync(path.join(outDir, "
|
|
99
|
+
const size = fs.statSync(path.join(outDir, "rastack_wasm_bg.wasm")).size;
|
|
100
100
|
console.log(
|
|
101
101
|
`✓ wasm engine built: stack/wasm/ (${(size / 1024).toFixed(0)} KB)`,
|
|
102
102
|
);
|
|
@@ -1,19 +1,19 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
|
|
3
3
|
/**
|
|
4
|
-
*
|
|
4
|
+
* rastack — unified CLI for React API Stack
|
|
5
5
|
*
|
|
6
6
|
* Commands:
|
|
7
|
-
*
|
|
8
|
-
*
|
|
9
|
-
*
|
|
10
|
-
*
|
|
11
|
-
*
|
|
12
|
-
*
|
|
13
|
-
*
|
|
14
|
-
*
|
|
15
|
-
*
|
|
16
|
-
*
|
|
7
|
+
* rastack compile [resourcesDir] [outDir] TypeScript resources → schema.rastack.json + openapi.json
|
|
8
|
+
* rastack check [resourcesDir] Validate the resource graph
|
|
9
|
+
* rastack list [resourcesDir] Print resources, fields and relations
|
|
10
|
+
* rastack urls [resourcesDir] Print the generated /api/{app}/v1/{model}/ routes
|
|
11
|
+
* rastack generate [--schema-only] Generate typed hooks from OpenAPI
|
|
12
|
+
* rastack serve [--warehouse dir] Run the Rust API over an Iceberg warehouse
|
|
13
|
+
* rastack admin [--warehouse dir] Run the Rust admin (Django-admin-style DB browser)
|
|
14
|
+
* rastack wasm Compile the API to WebAssembly (in-browser local dev)
|
|
15
|
+
* rastack scan [files...] [--fail-on-pii] Scan CSVs for PII and schema info
|
|
16
|
+
* rastack seed [--env dev] [--source gs://] Load CSVs into Iceberg namespace
|
|
17
17
|
*/
|
|
18
18
|
|
|
19
19
|
import { execSync, spawnSync } from "child_process";
|
|
@@ -22,7 +22,7 @@ import path from "path";
|
|
|
22
22
|
const [command, ...rest] = process.argv.slice(2);
|
|
23
23
|
|
|
24
24
|
function run(script: string, args: string[] = rest) {
|
|
25
|
-
// `
|
|
25
|
+
// `rastack` is a local developer CLI: `script` is a fixed internal dist filename
|
|
26
26
|
// and `args` come from the developer's own shell (process.argv), not from an
|
|
27
27
|
// untrusted network request — there is no command-injection surface here.
|
|
28
28
|
// nosemgrep: javascript.lang.security.detect-child-process.detect-child-process
|
|
@@ -36,8 +36,8 @@ function run(script: string, args: string[] = rest) {
|
|
|
36
36
|
function runRust(subcommand: string) {
|
|
37
37
|
const repoRoot = path.resolve(__dirname, "..", "..");
|
|
38
38
|
const binary =
|
|
39
|
-
process.env.
|
|
40
|
-
path.join(repoRoot, "rust", "target", "release", "
|
|
39
|
+
process.env.RASTACK_SERVER_BIN ||
|
|
40
|
+
path.join(repoRoot, "rust", "target", "release", "rastack-server");
|
|
41
41
|
// Runs the locally-built Rust server binary with a fixed internal subcommand
|
|
42
42
|
// ("serve"/"admin"); `rest` is the developer's own CLI args, not untrusted
|
|
43
43
|
// input — no command-injection surface.
|
|
@@ -61,8 +61,8 @@ switch (command) {
|
|
|
61
61
|
case "check":
|
|
62
62
|
case "list":
|
|
63
63
|
case "urls":
|
|
64
|
-
//
|
|
65
|
-
run("
|
|
64
|
+
// rastack-compile dispatches on its own first argument, so pass the command.
|
|
65
|
+
run("rastack-compile.js", [command, ...rest]);
|
|
66
66
|
break;
|
|
67
67
|
case "serve":
|
|
68
68
|
runRust("serve");
|
|
@@ -72,7 +72,7 @@ switch (command) {
|
|
|
72
72
|
break;
|
|
73
73
|
case "wasm":
|
|
74
74
|
// Compile the API to WebAssembly for in-browser local dev.
|
|
75
|
-
run("
|
|
75
|
+
run("rastack-wasm-build.js", rest);
|
|
76
76
|
break;
|
|
77
77
|
case "scan":
|
|
78
78
|
run("scan.js");
|
|
@@ -89,13 +89,13 @@ switch (command) {
|
|
|
89
89
|
console.error(
|
|
90
90
|
`Unknown command: "${command}"\n\n` +
|
|
91
91
|
`Usage:\n` +
|
|
92
|
-
`
|
|
93
|
-
`
|
|
94
|
-
`
|
|
95
|
-
`
|
|
96
|
-
`
|
|
97
|
-
`
|
|
98
|
-
`
|
|
92
|
+
` rastack compile [resourcesDir] [outDir]\n` +
|
|
93
|
+
` rastack check | list | urls [resourcesDir]\n` +
|
|
94
|
+
` rastack generate [--schema-only]\n` +
|
|
95
|
+
` rastack serve | admin [--warehouse dir]\n` +
|
|
96
|
+
` rastack wasm Compile the API to WebAssembly (browser local dev)\n` +
|
|
97
|
+
` rastack scan [files...] [--fail-on-pii]\n` +
|
|
98
|
+
` rastack seed [--env dev|staging] [--source gs://bucket/ | ./myapp-dev/]\n`,
|
|
99
99
|
);
|
|
100
100
|
process.exit(1);
|
|
101
101
|
}
|
package/src/scan.ts
CHANGED
|
@@ -1,14 +1,14 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
|
|
3
3
|
/**
|
|
4
|
-
*
|
|
4
|
+
* rastack scan
|
|
5
5
|
*
|
|
6
6
|
* Scans CSV files in myapp-dev/ for PII column patterns and schema issues.
|
|
7
7
|
* Exits non-zero if violations are found and --fail-on-pii is passed.
|
|
8
8
|
*
|
|
9
9
|
* Usage:
|
|
10
|
-
*
|
|
11
|
-
*
|
|
10
|
+
* rastack scan [files...] [--fail-on-pii]
|
|
11
|
+
* rastack scan myapp-dev/passengers.csv myapp-dev/flights.csv --fail-on-pii
|
|
12
12
|
*/
|
|
13
13
|
|
|
14
14
|
import path from "path";
|
|
@@ -80,7 +80,7 @@ if (totalViolations > 0) {
|
|
|
80
80
|
);
|
|
81
81
|
if (failOnPii) {
|
|
82
82
|
console.error(
|
|
83
|
-
"Failing because --fail-on-pii is set. Remove PII data or add access policies in .
|
|
83
|
+
"Failing because --fail-on-pii is set. Remove PII data or add access policies in .rastack/policies/.",
|
|
84
84
|
);
|
|
85
85
|
process.exit(1);
|
|
86
86
|
}
|