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.
Files changed (140) hide show
  1. package/CHANGELOG.md +43013 -0
  2. package/dist/.prettierrc +7 -0
  3. package/dist/compile/analyze.d.ts +12 -0
  4. package/dist/compile/analyze.js +233 -0
  5. package/dist/compile/index.d.ts +19 -0
  6. package/dist/compile/index.js +107 -0
  7. package/dist/compile/manifest.d.ts +4 -0
  8. package/dist/compile/manifest.js +12 -0
  9. package/dist/compile/model.d.ts +54 -0
  10. package/dist/compile/model.js +7 -0
  11. package/dist/compile/openapi.d.ts +11 -0
  12. package/dist/compile/openapi.js +233 -0
  13. package/dist/compile/program.d.ts +10 -0
  14. package/dist/compile/program.js +61 -0
  15. package/dist/csv-schema.d.ts +28 -0
  16. package/dist/csv-schema.js +162 -0
  17. package/dist/define/index.d.ts +117 -0
  18. package/dist/define/index.js +74 -0
  19. package/dist/entity-generation/delete-method.d.ts +1 -0
  20. package/dist/entity-generation/delete-method.js +31 -0
  21. package/dist/entity-generation/form.d.ts +13 -0
  22. package/dist/entity-generation/form.js +136 -0
  23. package/dist/entity-generation/get-method.d.ts +1 -0
  24. package/dist/entity-generation/get-method.js +67 -0
  25. package/dist/entity-generation/imports.d.ts +5 -0
  26. package/dist/entity-generation/imports.js +91 -0
  27. package/dist/entity-generation/sync-method.d.ts +30 -0
  28. package/dist/entity-generation/sync-method.js +158 -0
  29. package/dist/entity-generation/update-method.d.ts +1 -0
  30. package/dist/entity-generation/update-method.js +29 -0
  31. package/dist/index.d.ts +2 -0
  32. package/dist/index.js +53 -0
  33. package/dist/rad-compile.d.ts +11 -0
  34. package/dist/rad-compile.js +82 -0
  35. package/dist/rad-wasm-build.d.ts +14 -0
  36. package/dist/rad-wasm-build.js +74 -0
  37. package/dist/rad.d.ts +17 -0
  38. package/dist/rad.js +88 -0
  39. package/dist/scan.d.ts +12 -0
  40. package/dist/scan.js +79 -0
  41. package/dist/schema/camel-to-pastel.d.ts +1 -0
  42. package/dist/schema/camel-to-pastel.js +7 -0
  43. package/dist/schema/capitalise-first-letter.d.ts +1 -0
  44. package/dist/schema/capitalise-first-letter.js +7 -0
  45. package/dist/schema/extract-response.d.ts +2 -0
  46. package/dist/schema/extract-response.js +30 -0
  47. package/dist/schema/fetch-schema.d.ts +7 -0
  48. package/dist/schema/fetch-schema.js +23 -0
  49. package/dist/schema/remove-non-model-paths.d.ts +2 -0
  50. package/dist/schema/remove-non-model-paths.js +15 -0
  51. package/dist/schema/to-camel-case.d.ts +1 -0
  52. package/dist/schema/to-camel-case.js +11 -0
  53. package/dist/schema/to-pastel-case.d.ts +1 -0
  54. package/dist/schema/to-pastel-case.js +10 -0
  55. package/dist/schema-convert.d.ts +1 -0
  56. package/dist/schema-convert.js +63 -0
  57. package/dist/schema-entities.d.ts +14 -0
  58. package/dist/schema-entities.js +293 -0
  59. package/dist/schema-fetch.d.ts +1 -0
  60. package/dist/schema-fetch.js +81 -0
  61. package/dist/schema-full.d.ts +1 -0
  62. package/dist/schema-full.js +45 -0
  63. package/dist/schema-index.d.ts +1 -0
  64. package/dist/schema-index.js +34 -0
  65. package/dist/schema-params.d.ts +1 -0
  66. package/dist/schema-params.js +100 -0
  67. package/dist/seed.d.ts +17 -0
  68. package/dist/seed.js +252 -0
  69. package/hooks/form/form.ts +207 -0
  70. package/hooks/form/index.ts +8 -0
  71. package/hooks/form/interfaces.ts +217 -0
  72. package/hooks/form/structure.ts +39 -0
  73. package/hooks/form/validate-schema.ts +49 -0
  74. package/hooks/index.ts +3 -0
  75. package/hooks/query/api.ts +42 -0
  76. package/hooks/query/delete.ts +45 -0
  77. package/hooks/query/fetch.ts +48 -0
  78. package/hooks/query/index.ts +21 -0
  79. package/hooks/query/interfaces.ts +111 -0
  80. package/hooks/query/list.ts +286 -0
  81. package/hooks/query/update.ts +88 -0
  82. package/hooks/query/url.ts +31 -0
  83. package/hooks/real-time/index.ts +1 -0
  84. package/hooks/real-time/pusher.ts +43 -0
  85. package/jest.config.cjs +6 -0
  86. package/package.json +57 -0
  87. package/provider/index.ts +13 -0
  88. package/provider/provider.tsx +187 -0
  89. package/provider/types.ts +114 -0
  90. package/provider/warehouse.ts +167 -0
  91. package/provider/wasm.ts +124 -0
  92. package/runtime.ts +3 -0
  93. package/src/.prettierrc +7 -0
  94. package/src/compile/analyze.ts +224 -0
  95. package/src/compile/index.ts +86 -0
  96. package/src/compile/manifest.ts +10 -0
  97. package/src/compile/model.ts +69 -0
  98. package/src/compile/openapi.ts +266 -0
  99. package/src/compile/program.ts +40 -0
  100. package/src/csv-schema.ts +187 -0
  101. package/src/define/index.ts +162 -0
  102. package/src/entity-generation/delete-method.ts +37 -0
  103. package/src/entity-generation/form.ts +169 -0
  104. package/src/entity-generation/get-method.ts +80 -0
  105. package/src/entity-generation/imports.ts +110 -0
  106. package/src/entity-generation/sync-method.ts +222 -0
  107. package/src/entity-generation/update-method.ts +38 -0
  108. package/src/index.ts +77 -0
  109. package/src/rad-compile.ts +88 -0
  110. package/src/rad-wasm-build.ts +102 -0
  111. package/src/rad.ts +101 -0
  112. package/src/scan.ts +89 -0
  113. package/src/schema/camel-to-pastel.ts +3 -0
  114. package/src/schema/capitalise-first-letter.ts +3 -0
  115. package/src/schema/extract-response.ts +38 -0
  116. package/src/schema/fetch-schema.ts +12 -0
  117. package/src/schema/remove-non-model-paths.ts +13 -0
  118. package/src/schema/to-camel-case.ts +7 -0
  119. package/src/schema/to-pastel-case.ts +6 -0
  120. package/src/schema-convert.ts +61 -0
  121. package/src/schema-entities.ts +401 -0
  122. package/src/schema-fetch.ts +82 -0
  123. package/src/schema-full.ts +38 -0
  124. package/src/schema-index.ts +29 -0
  125. package/src/schema-params.ts +116 -0
  126. package/src/seed.ts +297 -0
  127. package/sync/engine.ts +392 -0
  128. package/sync/hooks.ts +450 -0
  129. package/sync/index.ts +8 -0
  130. package/sync/persistence.ts +237 -0
  131. package/sync/provider.tsx +91 -0
  132. package/sync/registry.ts +36 -0
  133. package/sync/store.ts +126 -0
  134. package/sync/transactions.ts +300 -0
  135. package/sync/types.ts +94 -0
  136. package/test/compile.spec.ts +192 -0
  137. package/test/csv-schema.spec.ts +143 -0
  138. package/test/schema-entities.spec.ts +688 -0
  139. package/tsconfig.json +24 -0
  140. package/types.ts +19 -0
@@ -0,0 +1,192 @@
1
+ import { existsSync, mkdirSync, readFileSync, rmSync, writeFileSync } from "fs";
2
+ import { join } from "path";
3
+ import {
4
+ analyzeResources,
5
+ buildOpenApi,
6
+ collisions,
7
+ compile,
8
+ ResourceModel,
9
+ } from "../src/compile";
10
+ import { generateFiles, generateFolders } from "../src/schema-entities";
11
+ import { removeNonModelPaths } from "../src/schema/remove-non-model-paths";
12
+
13
+ /**
14
+ * The compiler's fixtures are real `.ts` files that import the DSL by its
15
+ * package name (`rad-tools/define`); `createProgram` maps that to the in-tree
16
+ * source, so the type checker resolves the `Ref`/`Resource` brands exactly as it
17
+ * would in a consumer project.
18
+ */
19
+ const workRoot = join(__dirname, "compile-fixtures");
20
+ const resourcesDir = join(workRoot, "resources");
21
+ const outDir = join(workRoot, "out");
22
+
23
+ const AIRPORTS_TS = `
24
+ import { resource, s } from "rad-tools/define";
25
+
26
+ export const Airport = resource("airports", "airport", {
27
+ code: s.string({ maxLength: 3, unique: true }),
28
+ name: s.string({ maxLength: 120 }),
29
+ }, { search: ["code", "name"], admin: { listDisplay: ["code", "name"] } });
30
+
31
+ export const Terminal = resource("airports", "terminal", {
32
+ airport: s.ref(() => Airport),
33
+ label: s.string({ maxLength: 20 }),
34
+ });
35
+
36
+ export const Flight = resource("airports", "flight", {
37
+ number: s.string({ maxLength: 8 }),
38
+ status: s.string({ maxLength: 20, default: "scheduled" }),
39
+ }, { sync: { mode: "local", scope: "owner_id={user.id}", confirmed: ["update"] } });
40
+
41
+ export const Gate = resource("airports", "gate", {
42
+ number: s.string({ maxLength: 5 }),
43
+ }, { sync: { mode: "local", load: "lazy" } });
44
+
45
+ export const Booking = resource("airports", "booking", {
46
+ reference: s.string({ maxLength: 12 }),
47
+ }, { sync: { mode: "local", clientIds: true } });
48
+ `;
49
+
50
+ function findResource(
51
+ resources: ResourceModel[],
52
+ app: string,
53
+ model: string,
54
+ ): ResourceModel {
55
+ const found = resources.find((r) => r.app === app && r.model === model);
56
+ if (!found) throw new Error(`missing ${app}.${model}`);
57
+ return found;
58
+ }
59
+
60
+ describe("rad compile — resource analysis & FK inference", () => {
61
+ beforeAll(() => {
62
+ mkdirSync(resourcesDir, { recursive: true });
63
+ writeFileSync(join(resourcesDir, "airports.ts"), AIRPORTS_TS);
64
+ });
65
+
66
+ afterAll(() => {
67
+ rmSync(workRoot, { recursive: true, force: true });
68
+ });
69
+
70
+ it("infers a foreign key from the field type", () => {
71
+ const resources = analyzeResources(resourcesDir);
72
+ const terminal = findResource(resources, "airports", "terminal");
73
+
74
+ const airportField = terminal.fields.find((f) => f.name === "airport");
75
+ expect(airportField?.type).toBe("fk");
76
+ expect(airportField?.relation).toEqual({ app: "airports", model: "airport" });
77
+
78
+ expect(terminal.relations).toEqual([
79
+ { field: "airport", target: "airports.airport", kind: "fk" },
80
+ ]);
81
+ });
82
+
83
+ it("maps scalar fields and their constraints", () => {
84
+ const resources = analyzeResources(resourcesDir);
85
+ const airport = findResource(resources, "airports", "airport");
86
+
87
+ const code = airport.fields.find((f) => f.name === "code");
88
+ expect(code).toMatchObject({ type: "string", maxLength: 3, unique: true });
89
+
90
+ const name = airport.fields.find((f) => f.name === "name");
91
+ expect(name).toMatchObject({ type: "string", maxLength: 120 });
92
+
93
+ expect(airport.search).toEqual(["code", "name"]);
94
+ expect(airport.admin).toEqual({ listDisplay: ["code", "name"] });
95
+ });
96
+
97
+ it("captures sync config (local / lazy / client-ids / confirmed)", () => {
98
+ const resources = analyzeResources(resourcesDir);
99
+
100
+ expect(findResource(resources, "airports", "flight").sync).toMatchObject({
101
+ mode: "local",
102
+ scope: "owner_id={user.id}",
103
+ confirmed: ["update"],
104
+ });
105
+ expect(findResource(resources, "airports", "gate").sync).toMatchObject({
106
+ mode: "local",
107
+ load: "lazy",
108
+ });
109
+ expect(findResource(resources, "airports", "booking").sync).toMatchObject({
110
+ mode: "local",
111
+ clientIds: true,
112
+ });
113
+ });
114
+
115
+ it("reports no collisions for a valid graph", () => {
116
+ const resources = analyzeResources(resourcesDir);
117
+ expect(collisions(resources)).toEqual([]);
118
+ });
119
+ });
120
+
121
+ describe("rad compile — OpenAPI emission", () => {
122
+ beforeAll(() => {
123
+ mkdirSync(resourcesDir, { recursive: true });
124
+ writeFileSync(join(resourcesDir, "airports.ts"), AIRPORTS_TS);
125
+ });
126
+
127
+ afterAll(() => {
128
+ rmSync(workRoot, { recursive: true, force: true });
129
+ });
130
+
131
+ it("writes schema.rad.json and openapi.json", () => {
132
+ const result = compile(resourcesDir, outDir);
133
+ expect(existsSync(join(outDir, "schema.rad.json"))).toBe(true);
134
+ expect(existsSync(join(outDir, "openapi.json"))).toBe(true);
135
+ expect(result.manifest.resources.length).toBe(5);
136
+ });
137
+
138
+ it("emits the /api/{app}/v1/{model}/ contract with FK + sync extensions", () => {
139
+ const resources = analyzeResources(resourcesDir);
140
+ const doc = buildOpenApi(resources);
141
+
142
+ // URL-segment contract the codegen keys off.
143
+ expect(doc.paths["/api/airports/v1/terminal/"]).toBeDefined();
144
+ expect(doc.paths["/api/airports/v1/terminal/{id}/"]).toBeDefined();
145
+
146
+ // FK field carries the new relation extension.
147
+ const terminalSchema = doc.components.schemas.Terminal;
148
+ expect(terminalSchema.properties.airport["x-rastack-relation"]).toEqual({
149
+ app: "airports",
150
+ model: "airport",
151
+ });
152
+
153
+ // Local-mode sync extensions, confirmed on update.
154
+ const flightList = doc.paths["/api/airports/v1/flight/"];
155
+ expect(flightList.get["x-rastack-sync"]).toBe("local");
156
+ expect(doc.paths["/api/airports/v1/flight/{id}/"].put["x-rastack-sync"]).toBe(
157
+ "confirmed",
158
+ );
159
+ expect(doc.paths["/api/airports/v1/gate/"].get["x-rastack-sync-load"]).toBe(
160
+ "lazy",
161
+ );
162
+ expect(
163
+ doc.paths["/api/airports/v1/booking/"].post["x-rastack-sync-client-ids"],
164
+ ).toBe(true);
165
+ });
166
+
167
+ it("round-trips through the existing hooks codegen", async () => {
168
+ const resources = analyzeResources(resourcesDir);
169
+ const doc = buildOpenApi(resources);
170
+ const paths = removeNonModelPaths(doc.paths);
171
+
172
+ const entitiesPath = join(workRoot, "entities");
173
+ mkdirSync(entitiesPath, { recursive: true });
174
+ await generateFolders(entitiesPath, paths);
175
+ await generateFiles(entitiesPath, paths);
176
+
177
+ // Non-sync entity → plain async hook.
178
+ const airportApi = readFileSync(
179
+ join(entitiesPath, "airports", "airport", "api.ts"),
180
+ "utf-8",
181
+ );
182
+ expect(airportApi).toContain("export function useListAirport(");
183
+
184
+ // Sync entity → store-backed hooks + a defineEntity descriptor.
185
+ const flightApi = readFileSync(
186
+ join(entitiesPath, "airports", "flight", "api.ts"),
187
+ "utf-8",
188
+ );
189
+ expect(flightApi).toContain("defineEntity<IFlight>({");
190
+ expect(flightApi).toContain("from 'rastack/sync'");
191
+ });
192
+ });
@@ -0,0 +1,143 @@
1
+ import path from "path";
2
+ import os from "os";
3
+ import fs from "fs";
4
+ import { parseCsv, inferSchema, discoverCsvFiles } from "../src/csv-schema";
5
+
6
+ const REPO_ROOT = path.join(__dirname, "..");
7
+
8
+ function tmpCsv(content: string): string {
9
+ const dir = fs.mkdtempSync(path.join(os.tmpdir(), "rad-test-"));
10
+ const file = path.join(dir, "test.csv");
11
+ fs.writeFileSync(file, content);
12
+ return file;
13
+ }
14
+
15
+ describe("parseCsv", () => {
16
+ it("parses headers and rows", () => {
17
+ const { headers, rows } = parseCsv("id,name,age\n1,Alice,30\n2,Bob,25");
18
+ expect(headers).toEqual(["id", "name", "age"]);
19
+ expect(rows).toHaveLength(2);
20
+ expect(rows[0]).toEqual(["1", "Alice", "30"]);
21
+ });
22
+
23
+ it("handles quoted fields with commas", () => {
24
+ const { headers, rows } = parseCsv(`name,bio\nAlice,"Engineer, Pilot"\nBob,Writer`);
25
+ expect(rows[0][1]).toBe("Engineer, Pilot");
26
+ });
27
+
28
+ it("returns empty for blank input", () => {
29
+ const { headers, rows } = parseCsv("");
30
+ expect(headers).toHaveLength(0);
31
+ expect(rows).toHaveLength(0);
32
+ });
33
+ });
34
+
35
+ describe("inferSchema — type inference", () => {
36
+ it("infers integer columns", () => {
37
+ const csv = tmpCsv("id,score\n1,100\n2,200\n3,300");
38
+ const { schema } = inferSchema(csv, REPO_ROOT);
39
+ const id = schema.columns.find((c) => c.name === "id")!;
40
+ const score = schema.columns.find((c) => c.name === "score")!;
41
+ expect(id.type).toBe("integer");
42
+ expect(score.type).toBe("integer");
43
+ });
44
+
45
+ it("infers float columns", () => {
46
+ const csv = tmpCsv("price\n1.99\n3.50\n0.01");
47
+ const { schema } = inferSchema(csv, REPO_ROOT);
48
+ expect(schema.columns[0].type).toBe("float");
49
+ });
50
+
51
+ it("infers boolean columns", () => {
52
+ const csv = tmpCsv("active\ntrue\nfalse\ntrue");
53
+ const { schema } = inferSchema(csv, REPO_ROOT);
54
+ expect(schema.columns[0].type).toBe("boolean");
55
+ });
56
+
57
+ it("infers date columns", () => {
58
+ const csv = tmpCsv("dob\n1990-01-01\n1985-06-15");
59
+ const { schema } = inferSchema(csv, REPO_ROOT);
60
+ // dob matches PII but type should still be date
61
+ expect(schema.columns[0].type).toBe("date");
62
+ });
63
+
64
+ it("infers datetime columns", () => {
65
+ const csv = tmpCsv("created_at\n2024-01-01T10:00:00Z\n2024-02-01T12:30:00Z");
66
+ const { schema } = inferSchema(csv, REPO_ROOT);
67
+ expect(schema.columns[0].type).toBe("datetime");
68
+ });
69
+
70
+ it("marks id column as primaryKey", () => {
71
+ const csv = tmpCsv("id,name\n1,Alice");
72
+ const { schema } = inferSchema(csv, REPO_ROOT);
73
+ const id = schema.columns.find((c) => c.name === "id")!;
74
+ expect(id.primaryKey).toBe(true);
75
+ });
76
+
77
+ it("detects nullable columns", () => {
78
+ const csv = tmpCsv("id,note\n1,hello\n2,\n3,world");
79
+ const { schema } = inferSchema(csv, REPO_ROOT);
80
+ const note = schema.columns.find((c) => c.name === "note")!;
81
+ expect(note.nullable).toBe(true);
82
+ });
83
+ });
84
+
85
+ describe("inferSchema — PII detection", () => {
86
+ it("flags passport_no as PII", () => {
87
+ const csv = tmpCsv("id,passport_no\n1,AB123456\n2,CD789012");
88
+ const { schema, piiViolations } = inferSchema(csv, REPO_ROOT);
89
+ const col = schema.columns.find((c) => c.name === "passport_no")!;
90
+ expect(col.pii).toBe(true);
91
+ expect(piiViolations).toHaveLength(1);
92
+ expect(piiViolations[0].column).toBe("passport_no");
93
+ });
94
+
95
+ it("flags email as PII", () => {
96
+ const csv = tmpCsv("id,email\n1,alice@example.com");
97
+ const { piiViolations } = inferSchema(csv, REPO_ROOT);
98
+ expect(piiViolations.some((v) => v.column === "email")).toBe(true);
99
+ });
100
+
101
+ it("flags dob as PII", () => {
102
+ const csv = tmpCsv("id,dob\n1,1990-01-01");
103
+ const { piiViolations } = inferSchema(csv, REPO_ROOT);
104
+ expect(piiViolations.some((v) => v.column === "dob")).toBe(true);
105
+ });
106
+
107
+ it("does not flag non-PII columns", () => {
108
+ const csv = tmpCsv("id,flight_code,origin,destination\n1,SA101,JNB,LHR");
109
+ const { piiViolations } = inferSchema(csv, REPO_ROOT);
110
+ expect(piiViolations).toHaveLength(0);
111
+ });
112
+
113
+ it("generates a stable fingerprint", () => {
114
+ const csv = tmpCsv("id,name\n1,Alice\n2,Bob");
115
+ const { schema: a } = inferSchema(csv, REPO_ROOT);
116
+ const { schema: b } = inferSchema(csv, REPO_ROOT);
117
+ expect(a.fingerprint).toBe(b.fingerprint);
118
+ });
119
+
120
+ it("generates different fingerprints for different schemas", () => {
121
+ const csvA = tmpCsv("id,name\n1,Alice");
122
+ const csvB = tmpCsv("id,name,email\n1,Alice,a@b.com");
123
+ const { schema: a } = inferSchema(csvA, REPO_ROOT);
124
+ const { schema: b } = inferSchema(csvB, REPO_ROOT);
125
+ expect(a.fingerprint).not.toBe(b.fingerprint);
126
+ });
127
+ });
128
+
129
+ describe("discoverCsvFiles", () => {
130
+ it("finds all csv files in a directory", () => {
131
+ const dir = fs.mkdtempSync(path.join(os.tmpdir(), "rad-test-"));
132
+ fs.writeFileSync(path.join(dir, "a.csv"), "id\n1");
133
+ fs.writeFileSync(path.join(dir, "b.csv"), "id\n2");
134
+ fs.writeFileSync(path.join(dir, "readme.txt"), "not a csv");
135
+ const files = discoverCsvFiles(dir);
136
+ expect(files).toHaveLength(2);
137
+ expect(files.every((f) => f.endsWith(".csv"))).toBe(true);
138
+ });
139
+
140
+ it("returns empty for missing directory", () => {
141
+ expect(discoverCsvFiles("/nonexistent/path")).toHaveLength(0);
142
+ });
143
+ });