dzql 0.5.33 → 0.6.0

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 (150) hide show
  1. package/.env.sample +28 -0
  2. package/compose.yml +28 -0
  3. package/dist/client/index.ts +1 -0
  4. package/dist/client/stores/useMyProfileStore.ts +114 -0
  5. package/dist/client/stores/useOrgDashboardStore.ts +131 -0
  6. package/dist/client/stores/useVenueDetailStore.ts +117 -0
  7. package/dist/client/ws.ts +716 -0
  8. package/dist/db/migrations/000_core.sql +92 -0
  9. package/dist/db/migrations/20251229T212912022Z_schema.sql +3020 -0
  10. package/dist/db/migrations/20251229T212912022Z_subscribables.sql +371 -0
  11. package/dist/runtime/manifest.json +1562 -0
  12. package/docs/README.md +293 -36
  13. package/docs/feature-requests/applyPatch-bug-report.md +85 -0
  14. package/docs/feature-requests/connection-ready-profile.md +57 -0
  15. package/docs/feature-requests/hidden-bug-report.md +111 -0
  16. package/docs/feature-requests/hidden-fields-subscribables.md +34 -0
  17. package/docs/feature-requests/subscribable-param-key-bug.md +38 -0
  18. package/docs/feature-requests/todo.md +146 -0
  19. package/docs/for_ai.md +641 -0
  20. package/docs/project-setup.md +432 -0
  21. package/examples/blog.ts +50 -0
  22. package/examples/invalid.ts +18 -0
  23. package/examples/venues.js +485 -0
  24. package/package.json +23 -60
  25. package/src/cli/codegen/client.ts +99 -0
  26. package/src/cli/codegen/manifest.ts +95 -0
  27. package/src/cli/codegen/pinia.ts +174 -0
  28. package/src/cli/codegen/realtime.ts +58 -0
  29. package/src/cli/codegen/sql.ts +698 -0
  30. package/src/cli/codegen/subscribable_sql.ts +547 -0
  31. package/src/cli/codegen/subscribable_store.ts +184 -0
  32. package/src/cli/codegen/types.ts +142 -0
  33. package/src/cli/compiler/analyzer.ts +52 -0
  34. package/src/cli/compiler/graph_rules.ts +251 -0
  35. package/src/cli/compiler/ir.ts +233 -0
  36. package/src/cli/compiler/loader.ts +132 -0
  37. package/src/cli/compiler/permissions.ts +227 -0
  38. package/src/cli/index.ts +164 -0
  39. package/src/client/index.ts +1 -0
  40. package/src/client/ws.ts +286 -0
  41. package/src/create/.env.example +8 -0
  42. package/src/create/README.md +101 -0
  43. package/src/create/compose.yml +14 -0
  44. package/src/create/domain.ts +153 -0
  45. package/src/create/package.json +24 -0
  46. package/src/create/server.ts +18 -0
  47. package/src/create/setup.sh +11 -0
  48. package/src/create/tsconfig.json +15 -0
  49. package/src/runtime/auth.ts +39 -0
  50. package/src/runtime/db.ts +33 -0
  51. package/src/runtime/errors.ts +51 -0
  52. package/src/runtime/index.ts +98 -0
  53. package/src/runtime/js_functions.ts +63 -0
  54. package/src/runtime/manifest_loader.ts +29 -0
  55. package/src/runtime/namespace.ts +483 -0
  56. package/src/runtime/server.ts +87 -0
  57. package/src/runtime/ws.ts +197 -0
  58. package/src/shared/ir.ts +197 -0
  59. package/tests/client.test.ts +38 -0
  60. package/tests/codegen.test.ts +71 -0
  61. package/tests/compiler.test.ts +45 -0
  62. package/tests/graph_rules.test.ts +173 -0
  63. package/tests/integration/db.test.ts +174 -0
  64. package/tests/integration/e2e.test.ts +65 -0
  65. package/tests/integration/features.test.ts +922 -0
  66. package/tests/integration/full_stack.test.ts +262 -0
  67. package/tests/integration/setup.ts +45 -0
  68. package/tests/ir.test.ts +32 -0
  69. package/tests/namespace.test.ts +395 -0
  70. package/tests/permissions.test.ts +55 -0
  71. package/tests/pinia.test.ts +48 -0
  72. package/tests/realtime.test.ts +22 -0
  73. package/tests/runtime.test.ts +80 -0
  74. package/tests/subscribable_gen.test.ts +72 -0
  75. package/tests/subscribable_reactivity.test.ts +258 -0
  76. package/tests/venues_gen.test.ts +25 -0
  77. package/tsconfig.json +20 -0
  78. package/tsconfig.tsbuildinfo +1 -0
  79. package/README.md +0 -90
  80. package/bin/cli.js +0 -727
  81. package/docs/compiler/ADVANCED_FILTERS.md +0 -183
  82. package/docs/compiler/CODING_STANDARDS.md +0 -415
  83. package/docs/compiler/COMPARISON.md +0 -673
  84. package/docs/compiler/QUICKSTART.md +0 -326
  85. package/docs/compiler/README.md +0 -134
  86. package/docs/examples/README.md +0 -38
  87. package/docs/examples/blog.sql +0 -160
  88. package/docs/examples/venue-detail-simple.sql +0 -8
  89. package/docs/examples/venue-detail-subscribable.sql +0 -45
  90. package/docs/for-ai/claude-guide.md +0 -1210
  91. package/docs/getting-started/quickstart.md +0 -125
  92. package/docs/getting-started/subscriptions-quick-start.md +0 -203
  93. package/docs/getting-started/tutorial.md +0 -1104
  94. package/docs/guides/atomic-updates.md +0 -299
  95. package/docs/guides/client-stores.md +0 -730
  96. package/docs/guides/composite-primary-keys.md +0 -158
  97. package/docs/guides/custom-functions.md +0 -362
  98. package/docs/guides/drop-semantics.md +0 -554
  99. package/docs/guides/field-defaults.md +0 -240
  100. package/docs/guides/interpreter-vs-compiler.md +0 -237
  101. package/docs/guides/many-to-many.md +0 -929
  102. package/docs/guides/subscriptions.md +0 -537
  103. package/docs/reference/api.md +0 -1373
  104. package/docs/reference/client.md +0 -224
  105. package/src/client/stores/index.js +0 -8
  106. package/src/client/stores/useAppStore.js +0 -285
  107. package/src/client/stores/useWsStore.js +0 -289
  108. package/src/client/ws.js +0 -762
  109. package/src/compiler/cli/compile-example.js +0 -33
  110. package/src/compiler/cli/compile-subscribable.js +0 -43
  111. package/src/compiler/cli/debug-compile.js +0 -44
  112. package/src/compiler/cli/debug-parse.js +0 -26
  113. package/src/compiler/cli/debug-path-parser.js +0 -18
  114. package/src/compiler/cli/debug-subscribable-parser.js +0 -21
  115. package/src/compiler/cli/index.js +0 -174
  116. package/src/compiler/codegen/auth-codegen.js +0 -153
  117. package/src/compiler/codegen/drop-semantics-codegen.js +0 -553
  118. package/src/compiler/codegen/graph-rules-codegen.js +0 -450
  119. package/src/compiler/codegen/notification-codegen.js +0 -232
  120. package/src/compiler/codegen/operation-codegen.js +0 -1382
  121. package/src/compiler/codegen/permission-codegen.js +0 -318
  122. package/src/compiler/codegen/subscribable-codegen.js +0 -827
  123. package/src/compiler/compiler.js +0 -371
  124. package/src/compiler/index.js +0 -11
  125. package/src/compiler/parser/entity-parser.js +0 -440
  126. package/src/compiler/parser/path-parser.js +0 -290
  127. package/src/compiler/parser/subscribable-parser.js +0 -244
  128. package/src/database/dzql-core.sql +0 -161
  129. package/src/database/migrations/001_schema.sql +0 -60
  130. package/src/database/migrations/002_functions.sql +0 -890
  131. package/src/database/migrations/003_operations.sql +0 -1135
  132. package/src/database/migrations/004_search.sql +0 -581
  133. package/src/database/migrations/005_entities.sql +0 -730
  134. package/src/database/migrations/006_auth.sql +0 -94
  135. package/src/database/migrations/007_events.sql +0 -133
  136. package/src/database/migrations/008_hello.sql +0 -18
  137. package/src/database/migrations/008a_meta.sql +0 -172
  138. package/src/database/migrations/009_subscriptions.sql +0 -240
  139. package/src/database/migrations/010_atomic_updates.sql +0 -157
  140. package/src/database/migrations/010_fix_m2m_events.sql +0 -94
  141. package/src/index.js +0 -40
  142. package/src/server/api.js +0 -9
  143. package/src/server/db.js +0 -442
  144. package/src/server/index.js +0 -317
  145. package/src/server/logger.js +0 -259
  146. package/src/server/mcp.js +0 -594
  147. package/src/server/meta-route.js +0 -251
  148. package/src/server/namespace.js +0 -292
  149. package/src/server/subscriptions.js +0 -351
  150. package/src/server/ws.js +0 -573
@@ -0,0 +1,395 @@
1
+ /**
2
+ * Tests for TzqlNamespace - invokej integration
3
+ *
4
+ * These tests verify the namespace works correctly with the manifest
5
+ * and can execute CRUD operations against a real database.
6
+ */
7
+
8
+ import { describe, test, expect, beforeAll, afterAll, beforeEach, spyOn } from "bun:test";
9
+ import { V2TestDatabase } from "./integration/setup.js";
10
+ import { generateCoreSQL, generateEntitySQL, generateSchemaSQL } from "../src/cli/codegen/sql.js";
11
+ import { generateManifest } from "../src/cli/codegen/manifest.js";
12
+ import { generateIR } from "../src/cli/compiler/ir.js";
13
+ import { entities } from "../examples/blog.js";
14
+ import { writeFileSync, mkdirSync, rmSync } from "fs";
15
+ import { join } from "path";
16
+
17
+ const blogDomain = { entities, subscribables: {} };
18
+
19
+ describe("TzqlNamespace", () => {
20
+ let db: V2TestDatabase;
21
+ let sql: any;
22
+ let testManifestPath: string;
23
+ let originalEnv: string | undefined;
24
+ let consoleOutput: string[] = [];
25
+ let consoleErrorOutput: string[] = [];
26
+
27
+ beforeAll(async () => {
28
+ // Setup test database
29
+ db = new V2TestDatabase();
30
+ sql = await db.setup();
31
+
32
+ // Generate and apply SQL
33
+ const ir = generateIR(blogDomain);
34
+ const coreSQL = generateCoreSQL();
35
+ const postsSchema = generateSchemaSQL("posts", ir.entities.posts);
36
+ const commentsSchema = generateSchemaSQL("comments", ir.entities.comments);
37
+ const postsSQL = generateEntitySQL("posts", ir.entities.posts);
38
+ const commentsSQL = generateEntitySQL("comments", ir.entities.comments);
39
+
40
+ await db.applySQL(coreSQL);
41
+ await db.applySQL(postsSchema);
42
+ await db.applySQL(commentsSchema);
43
+ await db.applySQL(postsSQL);
44
+ await db.applySQL(commentsSQL);
45
+
46
+ // Create users table for auth functions
47
+ await db.applySQL(`
48
+ CREATE TABLE IF NOT EXISTS users (
49
+ id serial PRIMARY KEY,
50
+ email text UNIQUE NOT NULL,
51
+ password_hash text NOT NULL,
52
+ name text
53
+ )
54
+ `);
55
+
56
+ // Generate manifest and write to temp location
57
+ const manifest = generateManifest(ir);
58
+ testManifestPath = join(process.cwd(), "dist/runtime");
59
+ mkdirSync(testManifestPath, { recursive: true });
60
+ writeFileSync(
61
+ join(testManifestPath, "manifest.json"),
62
+ JSON.stringify(manifest, null, 2)
63
+ );
64
+
65
+ // Set DATABASE_URL to test database
66
+ originalEnv = process.env.DATABASE_URL;
67
+ const testDbUrl = db.baseUrl.replace(/\/[^/]*$/, `/${db.dbName}`);
68
+ process.env.DATABASE_URL = testDbUrl;
69
+ });
70
+
71
+ afterAll(async () => {
72
+ // Restore environment
73
+ if (originalEnv !== undefined) {
74
+ process.env.DATABASE_URL = originalEnv;
75
+ } else {
76
+ delete process.env.DATABASE_URL;
77
+ }
78
+
79
+ // Cleanup manifest
80
+ try {
81
+ rmSync(join(testManifestPath, "manifest.json"));
82
+ } catch {}
83
+
84
+ await db.teardown();
85
+ });
86
+
87
+ beforeEach(() => {
88
+ consoleOutput = [];
89
+ consoleErrorOutput = [];
90
+ });
91
+
92
+ // Helper to capture console output and prevent process.exit
93
+ function setupMocks() {
94
+ const logSpy = spyOn(console, "log").mockImplementation((...args) => {
95
+ consoleOutput.push(args.map(String).join(" "));
96
+ });
97
+
98
+ const errorSpy = spyOn(console, "error").mockImplementation((...args) => {
99
+ consoleErrorOutput.push(args.map(String).join(" "));
100
+ });
101
+
102
+ const exitSpy = spyOn(process, "exit").mockImplementation((() => {
103
+ // Don't actually exit - just throw to stop execution
104
+ throw new Error("EXIT");
105
+ }) as any);
106
+
107
+ return () => {
108
+ logSpy.mockRestore();
109
+ errorSpy.mockRestore();
110
+ exitSpy.mockRestore();
111
+ };
112
+ }
113
+
114
+ // Helper to parse JSON output - find the first valid JSON object
115
+ function parseOutput(): any {
116
+ for (const line of consoleOutput) {
117
+ try {
118
+ const parsed = JSON.parse(line);
119
+ if (parsed && typeof parsed === 'object' && 'success' in parsed) {
120
+ return parsed;
121
+ }
122
+ } catch {
123
+ continue;
124
+ }
125
+ }
126
+ return null;
127
+ }
128
+
129
+ function parseErrorOutput(): any {
130
+ if (consoleErrorOutput.length === 0) return null;
131
+ // Find JSON in error output
132
+ for (const line of consoleErrorOutput) {
133
+ try {
134
+ return JSON.parse(line);
135
+ } catch {
136
+ continue;
137
+ }
138
+ }
139
+ return null;
140
+ }
141
+
142
+ test("entities() lists available entities", async () => {
143
+ const { TzqlNamespace } = await import("../src/runtime/namespace.js");
144
+ const ns = new TzqlNamespace();
145
+
146
+ const restore = setupMocks();
147
+
148
+ try {
149
+ await ns.entities();
150
+ } catch (e: any) {
151
+ if (e.message !== "EXIT") throw e;
152
+ } finally {
153
+ restore();
154
+ }
155
+
156
+ const result = parseOutput();
157
+ expect(result).not.toBeNull();
158
+ expect(result.success).toBe(true);
159
+ expect(result.entities).toBeDefined();
160
+ expect(result.entities.posts).toBeDefined();
161
+ expect(result.entities.comments).toBeDefined();
162
+ });
163
+
164
+ test("functions() lists available functions", async () => {
165
+ const { TzqlNamespace } = await import("../src/runtime/namespace.js");
166
+ const ns = new TzqlNamespace();
167
+
168
+ const restore = setupMocks();
169
+
170
+ try {
171
+ await ns.functions();
172
+ } catch (e: any) {
173
+ if (e.message !== "EXIT") throw e;
174
+ } finally {
175
+ restore();
176
+ }
177
+
178
+ const result = parseOutput();
179
+ expect(result).not.toBeNull();
180
+ expect(result.success).toBe(true);
181
+ expect(result.functions).toBeDefined();
182
+ expect(result.functions.save_posts).toBeDefined();
183
+ expect(result.functions.get_posts).toBeDefined();
184
+ expect(result.functions.search_posts).toBeDefined();
185
+ expect(result.functions.delete_posts).toBeDefined();
186
+ expect(result.functions.login_user).toBeDefined();
187
+ expect(result.functions.register_user).toBeDefined();
188
+ });
189
+
190
+ test("save() creates a new entity", async () => {
191
+ const { TzqlNamespace } = await import("../src/runtime/namespace.js");
192
+ const ns = new TzqlNamespace(1); // userId = 1
193
+
194
+ const restore = setupMocks();
195
+
196
+ try {
197
+ await ns.save(
198
+ null,
199
+ "posts",
200
+ JSON.stringify({ title: "Test Post", content: "Hello", author_id: 1 })
201
+ );
202
+ } catch (e: any) {
203
+ if (e.message !== "EXIT") throw e;
204
+ } finally {
205
+ restore();
206
+ }
207
+
208
+ const result = parseOutput();
209
+ expect(result).not.toBeNull();
210
+ expect(result.success).toBe(true);
211
+ expect(result.result).toBeDefined();
212
+ expect(result.result.id).toBeDefined();
213
+ expect(result.result.title).toBe("Test Post");
214
+ });
215
+
216
+ test("get() retrieves an entity by ID", async () => {
217
+ // First create a post directly in DB
218
+ const created = await sql`
219
+ INSERT INTO posts (title, content, author_id)
220
+ VALUES ('Get Test', 'Content', 1)
221
+ RETURNING id
222
+ `;
223
+ const postId = created[0].id;
224
+
225
+ const { TzqlNamespace } = await import("../src/runtime/namespace.js");
226
+ const ns = new TzqlNamespace(1);
227
+
228
+ const restore = setupMocks();
229
+
230
+ try {
231
+ await ns.get(null, "posts", JSON.stringify({ id: postId }));
232
+ } catch (e: any) {
233
+ if (e.message !== "EXIT") throw e;
234
+ } finally {
235
+ restore();
236
+ }
237
+
238
+ const result = parseOutput();
239
+ expect(result).not.toBeNull();
240
+ expect(result.success).toBe(true);
241
+ expect(result.result).toBeDefined();
242
+ expect(result.result.id).toBe(postId);
243
+ expect(result.result.title).toBe("Get Test");
244
+ });
245
+
246
+ test("search() finds entities", async () => {
247
+ // Create some posts
248
+ await sql`
249
+ INSERT INTO posts (title, content, author_id)
250
+ VALUES ('Search Post 1', 'Content', 1), ('Search Post 2', 'More', 1)
251
+ `;
252
+
253
+ const { TzqlNamespace } = await import("../src/runtime/namespace.js");
254
+ const ns = new TzqlNamespace(1);
255
+
256
+ const restore = setupMocks();
257
+
258
+ try {
259
+ await ns.search(null, "posts", JSON.stringify({ limit: 10 }));
260
+ } catch (e: any) {
261
+ if (e.message !== "EXIT") throw e;
262
+ } finally {
263
+ restore();
264
+ }
265
+
266
+ const result = parseOutput();
267
+ expect(result).not.toBeNull();
268
+ expect(result.success).toBe(true);
269
+ expect(result.result).toBeArray();
270
+ expect(result.result.length).toBeGreaterThan(0);
271
+ });
272
+
273
+ test("delete() removes an entity", async () => {
274
+ // Create a post to delete - author_id must match userId for permission
275
+ const created = await sql`
276
+ INSERT INTO posts (title, content, author_id)
277
+ VALUES ('To Delete', 'Content', 1)
278
+ RETURNING id
279
+ `;
280
+ const postId = created[0].id;
281
+
282
+ const { TzqlNamespace } = await import("../src/runtime/namespace.js");
283
+ const ns = new TzqlNamespace(1); // userId must match author_id
284
+
285
+ const restore = setupMocks();
286
+
287
+ try {
288
+ await ns.delete(null, "posts", JSON.stringify({ id: postId }));
289
+ } catch (e: any) {
290
+ if (e.message !== "EXIT") throw e;
291
+ } finally {
292
+ restore();
293
+ }
294
+
295
+ const result = parseOutput();
296
+ const errorResult = parseErrorOutput();
297
+
298
+ if (!result && errorResult) {
299
+ console.log("Delete failed with:", errorResult);
300
+ }
301
+
302
+ expect(result).not.toBeNull();
303
+ expect(result.success).toBe(true);
304
+
305
+ // Verify it's gone
306
+ const check = await sql`SELECT * FROM posts WHERE id = ${postId}`;
307
+ expect(check.length).toBe(0);
308
+ });
309
+
310
+ test("call() executes arbitrary manifest function", async () => {
311
+ const { TzqlNamespace } = await import("../src/runtime/namespace.js");
312
+ const ns = new TzqlNamespace(1);
313
+
314
+ const restore = setupMocks();
315
+
316
+ try {
317
+ await ns.call(
318
+ null,
319
+ "save_posts",
320
+ JSON.stringify({ title: "Call Test", content: "Via call()", author_id: 1 })
321
+ );
322
+ } catch (e: any) {
323
+ if (e.message !== "EXIT") throw e;
324
+ } finally {
325
+ restore();
326
+ }
327
+
328
+ const result = parseOutput();
329
+ const errorResult = parseErrorOutput();
330
+
331
+ if (!result && errorResult) {
332
+ console.log("Call failed with:", errorResult);
333
+ }
334
+
335
+ expect(result).not.toBeNull();
336
+ expect(result.success).toBe(true);
337
+ expect(result.result.title).toBe("Call Test");
338
+ });
339
+
340
+ test("call() with unknown function returns error", async () => {
341
+ const { TzqlNamespace } = await import("../src/runtime/namespace.js");
342
+ const ns = new TzqlNamespace(1);
343
+
344
+ const restore = setupMocks();
345
+
346
+ try {
347
+ await ns.call(null, "nonexistent_function", "{}");
348
+ } catch (e: any) {
349
+ if (e.message !== "EXIT") {
350
+ throw e;
351
+ }
352
+ } finally {
353
+ restore();
354
+ }
355
+
356
+ const errorResult = parseErrorOutput();
357
+ expect(errorResult).not.toBeNull();
358
+ expect(errorResult.success).toBe(false);
359
+ expect(errorResult.error).toContain("not found in manifest");
360
+ });
361
+
362
+ test("search() without entity shows usage", async () => {
363
+ const { TzqlNamespace } = await import("../src/runtime/namespace.js");
364
+ const ns = new TzqlNamespace(1);
365
+
366
+ const restore = setupMocks();
367
+
368
+ try {
369
+ await ns.search(null, undefined, "{}");
370
+ } catch (e: any) {
371
+ if (e.message !== "EXIT") throw e;
372
+ } finally {
373
+ restore();
374
+ }
375
+
376
+ expect(consoleErrorOutput.some((msg) => msg.includes("entity name required"))).toBe(true);
377
+ });
378
+
379
+ test("save() with invalid JSON shows error", async () => {
380
+ const { TzqlNamespace } = await import("../src/runtime/namespace.js");
381
+ const ns = new TzqlNamespace(1);
382
+
383
+ const restore = setupMocks();
384
+
385
+ try {
386
+ await ns.save(null, "posts", "not valid json");
387
+ } catch (e: any) {
388
+ if (e.message !== "EXIT") throw e;
389
+ } finally {
390
+ restore();
391
+ }
392
+
393
+ expect(consoleErrorOutput.some((msg) => msg.includes("valid JSON"))).toBe(true);
394
+ });
395
+ });
@@ -0,0 +1,55 @@
1
+ import { describe, test, expect } from "bun:test";
2
+ import { compilePermission } from "../src/cli/compiler/permissions.js";
3
+
4
+ // Mock IR context for resolving relationships
5
+ const mockContext = {
6
+ entities: {
7
+ posts: {
8
+ table: 'posts',
9
+ primaryKey: ['id'],
10
+ columns: [{name: 'id', type: 'int'}, {name: 'org_id', type: 'int'}]
11
+ },
12
+ organisations: {
13
+ table: 'organisations',
14
+ primaryKey: ['id'],
15
+ columns: [{name: 'id', type: 'int'}]
16
+ },
17
+ acts_for: {
18
+ table: 'acts_for',
19
+ primaryKey: ['user_id', 'org_id'],
20
+ columns: [{name: 'user_id', type: 'int'}, {name: 'org_id', type: 'int'}, {name: 'role', type: 'text'}]
21
+ }
22
+ }
23
+ };
24
+
25
+ describe("Permission Compiler", () => {
26
+
27
+ test("should compile simple owner check", () => {
28
+ // Rule: @author_id == @user_id
29
+ // Context: posts table
30
+ const sql = compilePermission('posts', '@author_id', mockContext);
31
+
32
+ // Should generate: (p_data->>'author_id')::int = p_user_id
33
+ expect(sql).toContain("(p_data->>'author_id')::int = p_user_id");
34
+ });
35
+
36
+ test("should compile graph traversal", () => {
37
+ // Rule: @org_id->acts_for[org_id=$].user_id
38
+ // Meaning: Join via org_id from p_data, check if user_id matches
39
+
40
+ const sql = compilePermission('posts', '@org_id->acts_for[org_id=$].user_id', mockContext);
41
+
42
+ expect(sql).toContain("EXISTS");
43
+ expect(sql).toContain("FROM acts_for");
44
+ expect(sql).toContain("acts_for.org_id = (p_data->>'org_id')::int"); // Join via jsonb param
45
+ expect(sql).toContain("acts_for.user_id = p_user_id"); // Match
46
+ });
47
+
48
+ test("should compile condition", () => {
49
+ // Rule: @org_id->acts_for[org_id=$]{role='admin'}.user_id
50
+ const sql = compilePermission('posts', "@org_id->acts_for[org_id=$]{role='admin'}.user_id", mockContext);
51
+
52
+ expect(sql).toContain("acts_for.role='admin'");
53
+ });
54
+
55
+ });
@@ -0,0 +1,48 @@
1
+ import { describe, test, expect } from "bun:test";
2
+ import { generatePiniaStore } from "../src/cli/codegen/pinia.js";
3
+ import { generateManifest } from "../src/cli/codegen/manifest.js";
4
+ import { generateIR } from "../src/cli/compiler/ir.js";
5
+
6
+ // Use domain config format (with schema), not IR format
7
+ const mockDomainConfig = {
8
+ entities: {
9
+ posts: {
10
+ schema: {
11
+ id: "serial primary key",
12
+ title: "text"
13
+ },
14
+ permissions: { create: [], view: [], update: [], delete: [] }
15
+ }
16
+ },
17
+ subscribables: {}
18
+ };
19
+
20
+ const mockManifest = generateManifest(generateIR(mockDomainConfig));
21
+
22
+ describe("Pinia Store Generation", () => {
23
+ test("should generate a Pinia store with basic CRUD and table_changed", () => {
24
+ const piniaCode = generatePiniaStore(mockManifest, "posts");
25
+
26
+ // Check TypeScript imports
27
+ expect(piniaCode).toContain("import { defineStore } from 'pinia'");
28
+ expect(piniaCode).toContain("import { ref, type Ref } from 'vue'");
29
+ expect(piniaCode).toContain("import { ws } from '../../client.js';");
30
+
31
+ // Check type definitions
32
+ expect(piniaCode).toContain("export interface Posts {");
33
+ expect(piniaCode).toContain("export interface TableChangedPayload {");
34
+
35
+ // Check store definition
36
+ expect(piniaCode).toContain("export const usePostsStore = defineStore('posts-store', () => {");
37
+
38
+ // Check typed CRUD methods
39
+ expect(piniaCode).toContain("async function get(id: number): Promise<Posts | null>");
40
+ expect(piniaCode).toContain("async function save(data: Partial<Posts>): Promise<Posts>");
41
+ expect(piniaCode).toContain("async function remove(id: number): Promise<Posts>");
42
+ expect(piniaCode).toContain("async function search(query:");
43
+
44
+ // Check table_changed handler
45
+ expect(piniaCode).toContain("function table_changed(payload: TableChangedPayload): void");
46
+ expect(piniaCode).toContain("if (payload.table === 'posts')");
47
+ });
48
+ });
@@ -0,0 +1,22 @@
1
+ import { describe, test, expect } from "bun:test";
2
+ import { generateAffectedKeysFunction } from "../src/cli/codegen/realtime.js";
3
+
4
+ const mockSubscribable = {
5
+ name: "post_detail",
6
+ params: { post_id: "int" },
7
+ root: { entity: "posts", key: "post_id" },
8
+ scopeTables: ["posts", "comments"]
9
+ };
10
+
11
+ describe("Realtime Logic Generation", () => {
12
+ test("should generate affected keys function", () => {
13
+ const sql = generateAffectedKeysFunction("post_detail", mockSubscribable);
14
+
15
+ expect(sql).toContain("CREATE OR REPLACE FUNCTION dzql_v2.post_detail_affected_keys");
16
+ expect(sql).toContain("RETURNS text[]");
17
+
18
+ // Logic check: should return array of keys
19
+ expect(sql).toContain("WHEN 'posts' THEN");
20
+ expect(sql).toContain("RETURN ARRAY['post_detail:' || COALESCE(p_new->>'id', p_old->>'id')]");
21
+ });
22
+ });
@@ -0,0 +1,80 @@
1
+ import { describe, test, expect, beforeAll } from "bun:test";
2
+ import { loadManifest } from "../src/runtime/manifest_loader.js";
3
+ import { handleRequest } from "../src/runtime/server.js";
4
+ import { ErrorCode } from "../src/runtime/errors.js";
5
+
6
+ // Mock Manifest
7
+ const mockManifest = {
8
+ version: "2.0.0",
9
+ functions: {
10
+ "save_posts": {
11
+ schema: "dzql_v2",
12
+ name: "save_posts",
13
+ args: ["p_user_id", "p_data"],
14
+ returnType: "jsonb"
15
+ }
16
+ },
17
+ entities: {},
18
+ subscribables: {}
19
+ };
20
+
21
+ // Mock DB
22
+ const mockDB = {
23
+ query: async (text: string, params: any[]) => {
24
+ // Simulate Success
25
+ if (params[1].title === "Hello") {
26
+ return [{ result: { id: 1, ...params[1] } }];
27
+ }
28
+ // Simulate Permission Error
29
+ if (params[1].title === "Hacked") {
30
+ const e: any = new Error("permission_denied");
31
+ e.code = "P0001";
32
+ throw e;
33
+ }
34
+ // Simulate Unique Violation
35
+ if (params[1].title === "Duplicate") {
36
+ const e: any = new Error("duplicate key value");
37
+ e.code = "23505";
38
+ throw e;
39
+ }
40
+ return [];
41
+ }
42
+ };
43
+
44
+ describe("Runtime Security", () => {
45
+ beforeAll(() => {
46
+ loadManifest(mockManifest);
47
+ });
48
+
49
+ test("should execute allowlisted function", async () => {
50
+ const result = await handleRequest(mockDB, "save_posts", { title: "Hello" }, 1);
51
+ expect(result.id).toBe(1);
52
+ });
53
+
54
+ test("should reject unknown function (Injection Attempt)", async () => {
55
+ try {
56
+ await handleRequest(mockDB, "pg_sleep", {}, 1);
57
+ expect(true).toBe(false);
58
+ } catch (e: any) {
59
+ expect(e.message).toContain("not found in manifest");
60
+ }
61
+ });
62
+
63
+ test("should return PERMISSION_DENIED", async () => {
64
+ try {
65
+ await handleRequest(mockDB, "save_posts", { title: "Hacked" }, 1);
66
+ expect(true).toBe(false);
67
+ } catch (e: any) {
68
+ expect(e.code).toBe(ErrorCode.PERMISSION_DENIED);
69
+ }
70
+ });
71
+
72
+ test("should return CONFLICT", async () => {
73
+ try {
74
+ await handleRequest(mockDB, "save_posts", { title: "Duplicate" }, 1);
75
+ expect(true).toBe(false);
76
+ } catch (e: any) {
77
+ expect(e.code).toBe(ErrorCode.CONFLICT);
78
+ }
79
+ });
80
+ });
@@ -0,0 +1,72 @@
1
+ import { describe, test, expect } from "bun:test";
2
+ import { generateSubscribableStore } from "../src/cli/codegen/subscribable_store.js";
3
+ import { generateSubscribableSQL } from "../src/cli/codegen/subscribable_sql.js";
4
+ import { generateManifest } from "../src/cli/codegen/manifest.js";
5
+ import { generateIR } from "../src/cli/compiler/ir.js";
6
+ import { entities, subscribables } from "../examples/venues.js";
7
+
8
+ const venuesDomain = { entities, subscribables };
9
+
10
+ describe("Subscribable Store Generation", () => {
11
+ test("should generate venue_detail store with deep graph patching", () => {
12
+ const ir = generateIR(venuesDomain);
13
+ const manifest = generateManifest(ir);
14
+
15
+ // Generate the store for the 'venue_detail' subscribable
16
+ const code = generateSubscribableStore(manifest, "venue_detail");
17
+
18
+ console.log("--- GENERATED VENUE DETAIL STORE ---");
19
+ console.log(code);
20
+ console.log("------------------------------------");
21
+
22
+ expect(code).toContain("useVenueDetailStore");
23
+ // Check for TypeScript interface
24
+ expect(code).toContain("export interface VenueDetailParams");
25
+ // Check for nested table handling
26
+ expect(code).toContain("case 'sites':");
27
+ expect(code).toContain("case 'allocations':");
28
+ // Check for parent lookup logic with type annotation
29
+ expect(code).toContain("const parent = doc.sites?.find");
30
+ });
31
+
32
+ test("should generate async bind function that awaits first data", () => {
33
+ const ir = generateIR(venuesDomain);
34
+ const manifest = generateManifest(ir);
35
+
36
+ const code = generateSubscribableStore(manifest, "venue_detail");
37
+
38
+ // Check that bind is async with typed params
39
+ expect(code).toContain("async function bind(params: VenueDetailParams)");
40
+ // Check for Promise-based ready signal
41
+ expect(code).toContain("const ready = new Promise<void>");
42
+ expect(code).toContain("resolveReady()");
43
+ // Check that bind awaits ready
44
+ expect(code).toContain("await ready");
45
+ // Check that plain object with empty data is stored (preserves reactivity on merge)
46
+ expect(code).toContain("{ data: {}, loading: true, ready }");
47
+ // Check initial data is merged via Object.assign to preserve reactivity
48
+ expect(code).toContain("Object.assign(documents.value[key].data, eventData");
49
+ // Check existing subscription handling waits for ready
50
+ expect(code).toContain("await existing.ready");
51
+ // Check unbind function exists with typed params
52
+ expect(code).toContain("function unbind(params: VenueDetailParams)");
53
+ });
54
+
55
+ test("should generate flat SQL structure for nested includes", () => {
56
+ const ir = generateIR(venuesDomain);
57
+ const sub = ir.subscribables.venue_detail;
58
+
59
+ const sql = generateSubscribableSQL("venue_detail", sub, ir.entities);
60
+
61
+ console.log("--- GENERATED SQL ---");
62
+ console.log(sql);
63
+ console.log("---------------------");
64
+
65
+ // Check that nested includes use jsonb concatenation (||) for flat structure
66
+ // This merges entity fields with nested arrays into one flat object
67
+ // Uses to_jsonb() (not row_to_json) for type compatibility with || operator
68
+ expect(sql).toContain("to_jsonb(rel.*) || jsonb_build_object(");
69
+ // The allocations are nested inside the flat sites object
70
+ expect(sql).toContain("'allocations', COALESCE((");
71
+ });
72
+ });