rastack 0.0.16
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 +43013 -0
- package/dist/.prettierrc +7 -0
- package/dist/compile/analyze.d.ts +12 -0
- package/dist/compile/analyze.js +233 -0
- package/dist/compile/index.d.ts +19 -0
- package/dist/compile/index.js +107 -0
- package/dist/compile/manifest.d.ts +4 -0
- package/dist/compile/manifest.js +12 -0
- package/dist/compile/model.d.ts +54 -0
- package/dist/compile/model.js +7 -0
- package/dist/compile/openapi.d.ts +11 -0
- package/dist/compile/openapi.js +233 -0
- package/dist/compile/program.d.ts +10 -0
- package/dist/compile/program.js +61 -0
- package/dist/csv-schema.d.ts +28 -0
- package/dist/csv-schema.js +162 -0
- package/dist/define/index.d.ts +117 -0
- package/dist/define/index.js +74 -0
- package/dist/entity-generation/delete-method.d.ts +1 -0
- package/dist/entity-generation/delete-method.js +31 -0
- package/dist/entity-generation/form.d.ts +13 -0
- package/dist/entity-generation/form.js +136 -0
- package/dist/entity-generation/get-method.d.ts +1 -0
- package/dist/entity-generation/get-method.js +67 -0
- package/dist/entity-generation/imports.d.ts +5 -0
- package/dist/entity-generation/imports.js +91 -0
- package/dist/entity-generation/sync-method.d.ts +30 -0
- package/dist/entity-generation/sync-method.js +158 -0
- package/dist/entity-generation/update-method.d.ts +1 -0
- package/dist/entity-generation/update-method.js +29 -0
- package/dist/index.d.ts +2 -0
- package/dist/index.js +53 -0
- package/dist/rad-compile.d.ts +11 -0
- package/dist/rad-compile.js +82 -0
- package/dist/rad-wasm-build.d.ts +14 -0
- package/dist/rad-wasm-build.js +74 -0
- package/dist/rad.d.ts +17 -0
- package/dist/rad.js +88 -0
- package/dist/scan.d.ts +12 -0
- package/dist/scan.js +79 -0
- package/dist/schema/camel-to-pastel.d.ts +1 -0
- package/dist/schema/camel-to-pastel.js +7 -0
- package/dist/schema/capitalise-first-letter.d.ts +1 -0
- package/dist/schema/capitalise-first-letter.js +7 -0
- package/dist/schema/extract-response.d.ts +2 -0
- package/dist/schema/extract-response.js +30 -0
- package/dist/schema/fetch-schema.d.ts +7 -0
- package/dist/schema/fetch-schema.js +23 -0
- package/dist/schema/remove-non-model-paths.d.ts +2 -0
- package/dist/schema/remove-non-model-paths.js +15 -0
- package/dist/schema/to-camel-case.d.ts +1 -0
- package/dist/schema/to-camel-case.js +11 -0
- package/dist/schema/to-pastel-case.d.ts +1 -0
- package/dist/schema/to-pastel-case.js +10 -0
- package/dist/schema-convert.d.ts +1 -0
- package/dist/schema-convert.js +63 -0
- package/dist/schema-entities.d.ts +14 -0
- package/dist/schema-entities.js +293 -0
- package/dist/schema-fetch.d.ts +1 -0
- package/dist/schema-fetch.js +81 -0
- package/dist/schema-full.d.ts +1 -0
- package/dist/schema-full.js +45 -0
- package/dist/schema-index.d.ts +1 -0
- package/dist/schema-index.js +34 -0
- package/dist/schema-params.d.ts +1 -0
- package/dist/schema-params.js +100 -0
- package/dist/seed.d.ts +17 -0
- package/dist/seed.js +252 -0
- package/hooks/form/form.ts +207 -0
- package/hooks/form/index.ts +8 -0
- package/hooks/form/interfaces.ts +217 -0
- package/hooks/form/structure.ts +39 -0
- package/hooks/form/validate-schema.ts +49 -0
- package/hooks/index.ts +3 -0
- package/hooks/query/api.ts +42 -0
- package/hooks/query/delete.ts +45 -0
- package/hooks/query/fetch.ts +48 -0
- package/hooks/query/index.ts +21 -0
- package/hooks/query/interfaces.ts +111 -0
- package/hooks/query/list.ts +286 -0
- package/hooks/query/update.ts +88 -0
- package/hooks/query/url.ts +31 -0
- package/hooks/real-time/index.ts +1 -0
- package/hooks/real-time/pusher.ts +43 -0
- package/jest.config.cjs +6 -0
- package/package.json +57 -0
- package/provider/index.ts +13 -0
- package/provider/provider.tsx +187 -0
- package/provider/types.ts +114 -0
- package/provider/warehouse.ts +167 -0
- package/provider/wasm.ts +124 -0
- package/runtime.ts +3 -0
- package/src/.prettierrc +7 -0
- package/src/compile/analyze.ts +224 -0
- package/src/compile/index.ts +86 -0
- package/src/compile/manifest.ts +10 -0
- package/src/compile/model.ts +69 -0
- package/src/compile/openapi.ts +266 -0
- package/src/compile/program.ts +40 -0
- package/src/csv-schema.ts +187 -0
- package/src/define/index.ts +162 -0
- package/src/entity-generation/delete-method.ts +37 -0
- package/src/entity-generation/form.ts +169 -0
- package/src/entity-generation/get-method.ts +80 -0
- package/src/entity-generation/imports.ts +110 -0
- package/src/entity-generation/sync-method.ts +222 -0
- package/src/entity-generation/update-method.ts +38 -0
- package/src/index.ts +77 -0
- package/src/rad-compile.ts +88 -0
- package/src/rad-wasm-build.ts +102 -0
- package/src/rad.ts +101 -0
- package/src/scan.ts +89 -0
- package/src/schema/camel-to-pastel.ts +3 -0
- package/src/schema/capitalise-first-letter.ts +3 -0
- package/src/schema/extract-response.ts +38 -0
- package/src/schema/fetch-schema.ts +12 -0
- package/src/schema/remove-non-model-paths.ts +13 -0
- package/src/schema/to-camel-case.ts +7 -0
- package/src/schema/to-pastel-case.ts +6 -0
- package/src/schema-convert.ts +61 -0
- package/src/schema-entities.ts +401 -0
- package/src/schema-fetch.ts +82 -0
- package/src/schema-full.ts +38 -0
- package/src/schema-index.ts +29 -0
- package/src/schema-params.ts +116 -0
- package/src/seed.ts +297 -0
- package/sync/engine.ts +392 -0
- package/sync/hooks.ts +450 -0
- package/sync/index.ts +8 -0
- package/sync/persistence.ts +237 -0
- package/sync/provider.tsx +91 -0
- package/sync/registry.ts +36 -0
- package/sync/store.ts +126 -0
- package/sync/transactions.ts +300 -0
- package/sync/types.ts +94 -0
- package/test/compile.spec.ts +192 -0
- package/test/csv-schema.spec.ts +143 -0
- package/test/schema-entities.spec.ts +688 -0
- package/tsconfig.json +24 -0
- package/types.ts +19 -0
|
@@ -0,0 +1,88 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* `rad compile | check | list | urls` — the TypeScript-first replacement for the
|
|
5
|
+
* old Django `manage.py rastack` command.
|
|
6
|
+
*
|
|
7
|
+
* rad compile [resourcesDir] [outDir] defs → schema.rad.json + openapi.json
|
|
8
|
+
* rad check [resourcesDir] validate the resource graph (exit != 0 on error)
|
|
9
|
+
* rad list [resourcesDir] print every resource + its fields/relations
|
|
10
|
+
* rad urls [resourcesDir] print the generated /api/{app}/v1/{model}/ routes
|
|
11
|
+
*/
|
|
12
|
+
|
|
13
|
+
import { analyzeResources, collisions, compile, urlsFor } from "./compile";
|
|
14
|
+
|
|
15
|
+
const DEFAULT_RESOURCES = "resources";
|
|
16
|
+
const DEFAULT_OUT = ".rad";
|
|
17
|
+
|
|
18
|
+
function main(argv: string[]): void {
|
|
19
|
+
const [sub, ...rest] = argv;
|
|
20
|
+
|
|
21
|
+
switch (sub) {
|
|
22
|
+
case "compile": {
|
|
23
|
+
const resourcesDir = rest[0] || DEFAULT_RESOURCES;
|
|
24
|
+
const outDir = rest[1] || DEFAULT_OUT;
|
|
25
|
+
const result = compile(resourcesDir, outDir);
|
|
26
|
+
console.log(
|
|
27
|
+
`✓ compiled ${result.manifest.resources.length} resource(s) → ${result.files.join(", ")}`,
|
|
28
|
+
);
|
|
29
|
+
break;
|
|
30
|
+
}
|
|
31
|
+
case "check": {
|
|
32
|
+
const resources = analyzeResources(rest[0] || DEFAULT_RESOURCES);
|
|
33
|
+
const errors = collisions(resources);
|
|
34
|
+
if (errors.length) {
|
|
35
|
+
for (const e of errors) console.error(` ✗ ${e}`);
|
|
36
|
+
console.error(`${errors.length} error(s) found.`);
|
|
37
|
+
process.exit(1);
|
|
38
|
+
}
|
|
39
|
+
console.log(`✓ ${resources.length} resource(s): no collisions.`);
|
|
40
|
+
break;
|
|
41
|
+
}
|
|
42
|
+
case "list": {
|
|
43
|
+
const resources = analyzeResources(rest[0] || DEFAULT_RESOURCES);
|
|
44
|
+
if (!resources.length) {
|
|
45
|
+
console.log("No resources found.");
|
|
46
|
+
break;
|
|
47
|
+
}
|
|
48
|
+
for (const r of resources) {
|
|
49
|
+
console.log(`${r.app}.${r.model}`);
|
|
50
|
+
for (const f of r.fields) {
|
|
51
|
+
const rel = f.relation ? ` → ${f.relation.app}.${f.relation.model}` : "";
|
|
52
|
+
console.log(` ${f.name}: ${f.type}${rel}`);
|
|
53
|
+
}
|
|
54
|
+
if (r.sync) {
|
|
55
|
+
const flags = [
|
|
56
|
+
r.sync.mode,
|
|
57
|
+
r.sync.load === "lazy" ? "lazy" : "",
|
|
58
|
+
r.sync.clientIds ? "client-ids" : "",
|
|
59
|
+
r.sync.confirmed ? `confirmed: ${r.sync.confirmed.join(",")}` : "",
|
|
60
|
+
]
|
|
61
|
+
.filter(Boolean)
|
|
62
|
+
.join(", ");
|
|
63
|
+
console.log(` sync: ${flags}`);
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
break;
|
|
67
|
+
}
|
|
68
|
+
case "urls": {
|
|
69
|
+
const resources = analyzeResources(rest[0] || DEFAULT_RESOURCES);
|
|
70
|
+
for (const url of urlsFor(resources)) console.log(url);
|
|
71
|
+
break;
|
|
72
|
+
}
|
|
73
|
+
default:
|
|
74
|
+
console.error(
|
|
75
|
+
`Unknown compile subcommand: "${sub}"\n\n` +
|
|
76
|
+
`Usage:\n` +
|
|
77
|
+
` rad compile [resourcesDir] [outDir]\n` +
|
|
78
|
+
` rad check [resourcesDir]\n` +
|
|
79
|
+
` rad list [resourcesDir]\n` +
|
|
80
|
+
` rad urls [resourcesDir]\n`,
|
|
81
|
+
);
|
|
82
|
+
process.exit(1);
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
if (require.main === module) {
|
|
87
|
+
main(process.argv.slice(2));
|
|
88
|
+
}
|
|
@@ -0,0 +1,102 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* `rad wasm` — compile the Rust API to WebAssembly for in-browser local dev.
|
|
5
|
+
*
|
|
6
|
+
* Pipeline:
|
|
7
|
+
* 1. cargo build -p rad-wasm --release --target wasm32-unknown-unknown
|
|
8
|
+
* 2. wasm-bindgen --target web → stack/wasm/rad_wasm.js + rad_wasm_bg.wasm
|
|
9
|
+
* 3. wasm-opt -Oz (if available) to shrink the binary
|
|
10
|
+
*
|
|
11
|
+
* The generated bundle is what `<RAStackProvider mode="local">` loads. Prereqs:
|
|
12
|
+
* rustup target add wasm32-unknown-unknown
|
|
13
|
+
* cargo install wasm-bindgen-cli (version must match the wasm-bindgen crate)
|
|
14
|
+
*/
|
|
15
|
+
|
|
16
|
+
import { spawnSync } from "child_process";
|
|
17
|
+
import fs from "fs";
|
|
18
|
+
import path from "path";
|
|
19
|
+
|
|
20
|
+
const repoRoot = path.resolve(__dirname, "..", "..");
|
|
21
|
+
const rustDir = path.join(repoRoot, "rust");
|
|
22
|
+
const outDir = path.join(repoRoot, "tools", "wasm");
|
|
23
|
+
const wasmPath = path.join(
|
|
24
|
+
rustDir,
|
|
25
|
+
"target",
|
|
26
|
+
"wasm32-unknown-unknown",
|
|
27
|
+
"release",
|
|
28
|
+
"rad_wasm.wasm",
|
|
29
|
+
);
|
|
30
|
+
|
|
31
|
+
function run(cmd: string, args: string[], cwd = repoRoot): void {
|
|
32
|
+
// Local build tool: `cmd`/`args` are the fixed toolchain invocations below
|
|
33
|
+
// (cargo, wasm-bindgen, wasm-opt), not untrusted input — no injection surface.
|
|
34
|
+
// nosemgrep: javascript.lang.security.detect-child-process.detect-child-process
|
|
35
|
+
const result = spawnSync(cmd, args, {
|
|
36
|
+
stdio: "inherit",
|
|
37
|
+
cwd,
|
|
38
|
+
env: process.env,
|
|
39
|
+
});
|
|
40
|
+
if (result.error) {
|
|
41
|
+
console.error(`\nCould not run \`${cmd}\`. Is it installed and on PATH?`);
|
|
42
|
+
if (cmd === "wasm-bindgen") {
|
|
43
|
+
console.error(" cargo install wasm-bindgen-cli");
|
|
44
|
+
}
|
|
45
|
+
process.exit(1);
|
|
46
|
+
}
|
|
47
|
+
if (result.status !== 0) {
|
|
48
|
+
process.exit(result.status ?? 1);
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
function has(cmd: string): boolean {
|
|
53
|
+
// Probes a fixed toolchain name (e.g. "wasm-opt") for availability; not
|
|
54
|
+
// untrusted input.
|
|
55
|
+
// nosemgrep: javascript.lang.security.detect-child-process.detect-child-process
|
|
56
|
+
return spawnSync(cmd, ["--version"], { stdio: "ignore" }).status === 0;
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
console.log("→ cargo build -p rad-wasm --release (wasm32-unknown-unknown)");
|
|
60
|
+
run(
|
|
61
|
+
"cargo",
|
|
62
|
+
[
|
|
63
|
+
"build",
|
|
64
|
+
"-p",
|
|
65
|
+
"rad-wasm",
|
|
66
|
+
"--release",
|
|
67
|
+
"--target",
|
|
68
|
+
"wasm32-unknown-unknown",
|
|
69
|
+
],
|
|
70
|
+
rustDir,
|
|
71
|
+
);
|
|
72
|
+
|
|
73
|
+
console.log("→ wasm-bindgen → stack/wasm/");
|
|
74
|
+
run("wasm-bindgen", [
|
|
75
|
+
"--target",
|
|
76
|
+
"web",
|
|
77
|
+
"--out-dir",
|
|
78
|
+
outDir,
|
|
79
|
+
"--out-name",
|
|
80
|
+
"rad_wasm",
|
|
81
|
+
wasmPath,
|
|
82
|
+
]);
|
|
83
|
+
|
|
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
|
+
);
|
|
90
|
+
|
|
91
|
+
if (has("wasm-opt")) {
|
|
92
|
+
console.log("→ wasm-opt -Oz");
|
|
93
|
+
const bg = path.join(outDir, "rad_wasm_bg.wasm");
|
|
94
|
+
run("wasm-opt", ["-Oz", "-o", bg, bg]);
|
|
95
|
+
} else {
|
|
96
|
+
console.log("· wasm-opt not found — skipping size optimisation (optional)");
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
const size = fs.statSync(path.join(outDir, "rad_wasm_bg.wasm")).size;
|
|
100
|
+
console.log(
|
|
101
|
+
`✓ wasm engine built: stack/wasm/ (${(size / 1024).toFixed(0)} KB)`,
|
|
102
|
+
);
|
package/src/rad.ts
ADDED
|
@@ -0,0 +1,101 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* rad — unified CLI for React API Stack
|
|
5
|
+
*
|
|
6
|
+
* Commands:
|
|
7
|
+
* rad compile [resourcesDir] [outDir] TypeScript resources → schema.rad.json + openapi.json
|
|
8
|
+
* rad check [resourcesDir] Validate the resource graph
|
|
9
|
+
* rad list [resourcesDir] Print resources, fields and relations
|
|
10
|
+
* rad urls [resourcesDir] Print the generated /api/{app}/v1/{model}/ routes
|
|
11
|
+
* rad generate [--schema-only] Generate typed hooks from OpenAPI
|
|
12
|
+
* rad serve [--warehouse dir] Run the Rust API over an Iceberg warehouse
|
|
13
|
+
* rad admin [--warehouse dir] Run the Rust admin (Django-admin-style DB browser)
|
|
14
|
+
* rad wasm Compile the API to WebAssembly (in-browser local dev)
|
|
15
|
+
* rad scan [files...] [--fail-on-pii] Scan CSVs for PII and schema info
|
|
16
|
+
* rad seed [--env dev] [--source gs://] Load CSVs into Iceberg namespace
|
|
17
|
+
*/
|
|
18
|
+
|
|
19
|
+
import { execSync, spawnSync } from "child_process";
|
|
20
|
+
import path from "path";
|
|
21
|
+
|
|
22
|
+
const [command, ...rest] = process.argv.slice(2);
|
|
23
|
+
|
|
24
|
+
function run(script: string, args: string[] = rest) {
|
|
25
|
+
// `rad` is a local developer CLI: `script` is a fixed internal dist filename
|
|
26
|
+
// and `args` come from the developer's own shell (process.argv), not from an
|
|
27
|
+
// untrusted network request — there is no command-injection surface here.
|
|
28
|
+
// nosemgrep: javascript.lang.security.detect-child-process.detect-child-process
|
|
29
|
+
execSync(`node ${path.join(__dirname, script)} ${args.join(" ")}`, {
|
|
30
|
+
stdio: "inherit",
|
|
31
|
+
env: process.env,
|
|
32
|
+
});
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
/** Invoke the compiled Rust server binary (built by `cargo build` in rust/). */
|
|
36
|
+
function runRust(subcommand: string) {
|
|
37
|
+
const repoRoot = path.resolve(__dirname, "..", "..");
|
|
38
|
+
const binary =
|
|
39
|
+
process.env.RAD_SERVER_BIN ||
|
|
40
|
+
path.join(repoRoot, "rust", "target", "release", "rad-server");
|
|
41
|
+
// Runs the locally-built Rust server binary with a fixed internal subcommand
|
|
42
|
+
// ("serve"/"admin"); `rest` is the developer's own CLI args, not untrusted
|
|
43
|
+
// input — no command-injection surface.
|
|
44
|
+
// nosemgrep: javascript.lang.security.detect-child-process.detect-child-process
|
|
45
|
+
const result = spawnSync(binary, [subcommand, ...rest], {
|
|
46
|
+
stdio: "inherit",
|
|
47
|
+
env: process.env,
|
|
48
|
+
});
|
|
49
|
+
if (result.error) {
|
|
50
|
+
console.error(
|
|
51
|
+
`Could not run the Rust server (${binary}). Build it first:\n` +
|
|
52
|
+
` cd rust && cargo build --release\n`,
|
|
53
|
+
);
|
|
54
|
+
process.exit(1);
|
|
55
|
+
}
|
|
56
|
+
process.exit(result.status ?? 0);
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
switch (command) {
|
|
60
|
+
case "compile":
|
|
61
|
+
case "check":
|
|
62
|
+
case "list":
|
|
63
|
+
case "urls":
|
|
64
|
+
// rad-compile dispatches on its own first argument, so pass the command.
|
|
65
|
+
run("rad-compile.js", [command, ...rest]);
|
|
66
|
+
break;
|
|
67
|
+
case "serve":
|
|
68
|
+
runRust("serve");
|
|
69
|
+
break;
|
|
70
|
+
case "admin":
|
|
71
|
+
runRust("admin");
|
|
72
|
+
break;
|
|
73
|
+
case "wasm":
|
|
74
|
+
// Compile the API to WebAssembly for in-browser local dev.
|
|
75
|
+
run("rad-wasm-build.js", rest);
|
|
76
|
+
break;
|
|
77
|
+
case "scan":
|
|
78
|
+
run("scan.js");
|
|
79
|
+
break;
|
|
80
|
+
case "seed":
|
|
81
|
+
run("seed.js");
|
|
82
|
+
break;
|
|
83
|
+
case "generate":
|
|
84
|
+
case "gen":
|
|
85
|
+
// Proxy to existing rastack codegen pipeline
|
|
86
|
+
run("index.js");
|
|
87
|
+
break;
|
|
88
|
+
default:
|
|
89
|
+
console.error(
|
|
90
|
+
`Unknown command: "${command}"\n\n` +
|
|
91
|
+
`Usage:\n` +
|
|
92
|
+
` rad compile [resourcesDir] [outDir]\n` +
|
|
93
|
+
` rad check | list | urls [resourcesDir]\n` +
|
|
94
|
+
` rad generate [--schema-only]\n` +
|
|
95
|
+
` rad serve | admin [--warehouse dir]\n` +
|
|
96
|
+
` rad wasm Compile the API to WebAssembly (browser local dev)\n` +
|
|
97
|
+
` rad scan [files...] [--fail-on-pii]\n` +
|
|
98
|
+
` rad seed [--env dev|staging] [--source gs://bucket/ | ./myapp-dev/]\n`,
|
|
99
|
+
);
|
|
100
|
+
process.exit(1);
|
|
101
|
+
}
|
package/src/scan.ts
ADDED
|
@@ -0,0 +1,89 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* rad scan
|
|
5
|
+
*
|
|
6
|
+
* Scans CSV files in myapp-dev/ for PII column patterns and schema issues.
|
|
7
|
+
* Exits non-zero if violations are found and --fail-on-pii is passed.
|
|
8
|
+
*
|
|
9
|
+
* Usage:
|
|
10
|
+
* rad scan [files...] [--fail-on-pii]
|
|
11
|
+
* rad scan myapp-dev/passengers.csv myapp-dev/flights.csv --fail-on-pii
|
|
12
|
+
*/
|
|
13
|
+
|
|
14
|
+
import path from "path";
|
|
15
|
+
import { discoverCsvFiles, inferSchema } from "./csv-schema";
|
|
16
|
+
|
|
17
|
+
const args = process.argv.slice(2);
|
|
18
|
+
const failOnPii = args.includes("--fail-on-pii");
|
|
19
|
+
const fileArgs = args.filter((a) => !a.startsWith("--") && a.endsWith(".csv"));
|
|
20
|
+
|
|
21
|
+
const repoRoot = process.cwd();
|
|
22
|
+
const devDir = path.join(repoRoot, "myapp-dev");
|
|
23
|
+
|
|
24
|
+
const files = fileArgs.length > 0 ? fileArgs : discoverCsvFiles(devDir);
|
|
25
|
+
|
|
26
|
+
if (files.length === 0) {
|
|
27
|
+
console.log("No CSV files found to scan.");
|
|
28
|
+
process.exit(0);
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
let totalViolations = 0;
|
|
32
|
+
let hasErrors = false;
|
|
33
|
+
|
|
34
|
+
for (const csvPath of files) {
|
|
35
|
+
const rel = path.relative(repoRoot, csvPath);
|
|
36
|
+
|
|
37
|
+
let result;
|
|
38
|
+
try {
|
|
39
|
+
result = inferSchema(csvPath, repoRoot);
|
|
40
|
+
} catch (err) {
|
|
41
|
+
console.error(`ERROR ${rel}: ${(err as Error).message}`);
|
|
42
|
+
hasErrors = true;
|
|
43
|
+
continue;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
const { schema, piiViolations } = result;
|
|
47
|
+
|
|
48
|
+
const colSummary = schema.columns
|
|
49
|
+
.map((c) => {
|
|
50
|
+
const tags: string[] = [c.type];
|
|
51
|
+
if (c.primaryKey) tags.push("PK");
|
|
52
|
+
if (c.nullable) tags.push("nullable");
|
|
53
|
+
if (c.pii) tags.push("PII");
|
|
54
|
+
return ` ${c.name.padEnd(24)} ${tags.join(", ")}`;
|
|
55
|
+
})
|
|
56
|
+
.join("\n");
|
|
57
|
+
|
|
58
|
+
console.log(`\n${rel} [${schema.rowCount} rows, fingerprint: ${schema.fingerprint}]`);
|
|
59
|
+
console.log(colSummary);
|
|
60
|
+
|
|
61
|
+
if (piiViolations.length > 0) {
|
|
62
|
+
totalViolations += piiViolations.length;
|
|
63
|
+
console.log(`\n ⚠ PII violations (${piiViolations.length}):`);
|
|
64
|
+
for (const v of piiViolations) {
|
|
65
|
+
console.log(` ${v.column}: ${v.reason}`);
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
console.log("");
|
|
71
|
+
|
|
72
|
+
if (hasErrors) {
|
|
73
|
+
console.error("Scan completed with errors.");
|
|
74
|
+
process.exit(1);
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
if (totalViolations > 0) {
|
|
78
|
+
console.warn(
|
|
79
|
+
`Scan found ${totalViolations} PII violation(s) across ${files.length} file(s).`,
|
|
80
|
+
);
|
|
81
|
+
if (failOnPii) {
|
|
82
|
+
console.error(
|
|
83
|
+
"Failing because --fail-on-pii is set. Remove PII data or add access policies in .rad/policies/.",
|
|
84
|
+
);
|
|
85
|
+
process.exit(1);
|
|
86
|
+
}
|
|
87
|
+
} else {
|
|
88
|
+
console.log(`Scan passed. ${files.length} file(s) checked, no PII violations.`);
|
|
89
|
+
}
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
import {
|
|
2
|
+
MediaTypeObject,
|
|
3
|
+
OperationObject,
|
|
4
|
+
ReferenceObject,
|
|
5
|
+
} from "@ts-stack/openapi-spec";
|
|
6
|
+
|
|
7
|
+
export function extractResponseBodySchema(
|
|
8
|
+
methodInformation: OperationObject,
|
|
9
|
+
): [string | undefined, boolean] {
|
|
10
|
+
let responseBodySchema: string | undefined;
|
|
11
|
+
let isList = false;
|
|
12
|
+
|
|
13
|
+
if (!methodInformation.responses) return [undefined, false];
|
|
14
|
+
|
|
15
|
+
Object.keys(methodInformation.responses).forEach((responseKey) => {
|
|
16
|
+
// @ts-ignore
|
|
17
|
+
const responseData = methodInformation.responses[responseKey];
|
|
18
|
+
if (responseData?.content) {
|
|
19
|
+
const mediaType = responseData.content[
|
|
20
|
+
"application/json"
|
|
21
|
+
] as MediaTypeObject;
|
|
22
|
+
if (mediaType?.schema) {
|
|
23
|
+
const refSchema = (mediaType.schema as ReferenceObject).$ref;
|
|
24
|
+
if (refSchema) {
|
|
25
|
+
responseBodySchema = refSchema.replace("#/components/schemas/", "");
|
|
26
|
+
if (responseBodySchema.startsWith("Paginated")) {
|
|
27
|
+
isList = true;
|
|
28
|
+
responseBodySchema = responseBodySchema
|
|
29
|
+
.replace("Paginated", "")
|
|
30
|
+
.slice(0, -4);
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
});
|
|
36
|
+
|
|
37
|
+
return [responseBodySchema, isList];
|
|
38
|
+
}
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
import { ComponentsObject, PathsObject } from "@ts-stack/openapi-spec";
|
|
2
|
+
import axios from "axios";
|
|
3
|
+
|
|
4
|
+
interface OpenAPISchema {
|
|
5
|
+
paths: PathsObject;
|
|
6
|
+
components: ComponentsObject;
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
export async function fetchSchema(url: string): Promise<OpenAPISchema> {
|
|
10
|
+
const response = await axios.get<OpenAPISchema>(url);
|
|
11
|
+
return response.data;
|
|
12
|
+
}
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
import { PathsObject } from "@ts-stack/openapi-spec";
|
|
2
|
+
|
|
3
|
+
export function removeNonModelPaths(paths: PathsObject): PathsObject {
|
|
4
|
+
const filteredPaths: PathsObject = {};
|
|
5
|
+
Object.keys(paths).forEach((key) => {
|
|
6
|
+
const appName = key.split("/")[2];
|
|
7
|
+
const modelName = key.split("/")[4];
|
|
8
|
+
if (appName && modelName) {
|
|
9
|
+
filteredPaths[key] = paths[key];
|
|
10
|
+
}
|
|
11
|
+
});
|
|
12
|
+
return filteredPaths;
|
|
13
|
+
}
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
import { readdirSync, writeFileSync } from "fs";
|
|
2
|
+
import { join } from "path";
|
|
3
|
+
import { compileFromFile } from "json-schema-to-typescript";
|
|
4
|
+
|
|
5
|
+
function removeUnknownIndexSignature(code: string): string {
|
|
6
|
+
return code
|
|
7
|
+
.split("\n")
|
|
8
|
+
.filter((line) => !line.includes("[k: string]: unknown;"))
|
|
9
|
+
.join("\n");
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
function removeDuplicates(code: string): string {
|
|
13
|
+
const lines = code.split("\n");
|
|
14
|
+
const uniqueExports = new Set();
|
|
15
|
+
let filteredCode = "";
|
|
16
|
+
|
|
17
|
+
lines.forEach((line) => {
|
|
18
|
+
if (line.includes("export type ") && !uniqueExports.has(line)) {
|
|
19
|
+
uniqueExports.add(line);
|
|
20
|
+
filteredCode += line + "\n";
|
|
21
|
+
} else if (!line.includes("export type ")) {
|
|
22
|
+
filteredCode += line + "\n";
|
|
23
|
+
}
|
|
24
|
+
});
|
|
25
|
+
|
|
26
|
+
return filteredCode;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
async function main() {
|
|
30
|
+
console.log("Converting Schema");
|
|
31
|
+
const args: string[] = process.argv.slice(2);
|
|
32
|
+
const basePath = args[0];
|
|
33
|
+
const schemaFolderPath = join(basePath, "api-schema");
|
|
34
|
+
|
|
35
|
+
let interfaceFileData = "";
|
|
36
|
+
|
|
37
|
+
const schemaFiles = readdirSync(schemaFolderPath);
|
|
38
|
+
|
|
39
|
+
let compilePromises = schemaFiles.map((schemaFile) => {
|
|
40
|
+
const schemaFilePath = join(schemaFolderPath, schemaFile);
|
|
41
|
+
|
|
42
|
+
return compileFromFile(schemaFilePath, {
|
|
43
|
+
bannerComment: "",
|
|
44
|
+
style: { useTabs: false, tabWidth: 2 },
|
|
45
|
+
ignoreMinAndMaxItems: true,
|
|
46
|
+
strictIndexSignatures: false,
|
|
47
|
+
}).then((result) => {
|
|
48
|
+
const removedSignature = removeUnknownIndexSignature(result);
|
|
49
|
+
return removedSignature + "\n";
|
|
50
|
+
});
|
|
51
|
+
});
|
|
52
|
+
|
|
53
|
+
let results = await Promise.all(compilePromises);
|
|
54
|
+
interfaceFileData = results.join("\n");
|
|
55
|
+
interfaceFileData = removeDuplicates(interfaceFileData);
|
|
56
|
+
|
|
57
|
+
const outputFile = join(basePath, "interface.ts");
|
|
58
|
+
writeFileSync(outputFile, interfaceFileData);
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
main().catch((error) => console.error(error));
|