dzql 0.5.33 → 0.6.1

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 (142) 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 +309 -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 +653 -0
  20. package/docs/project-setup.md +456 -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 +166 -0
  39. package/src/client/index.ts +1 -0
  40. package/src/client/ws.ts +286 -0
  41. package/src/runtime/auth.ts +39 -0
  42. package/src/runtime/db.ts +33 -0
  43. package/src/runtime/errors.ts +51 -0
  44. package/src/runtime/index.ts +98 -0
  45. package/src/runtime/js_functions.ts +63 -0
  46. package/src/runtime/manifest_loader.ts +29 -0
  47. package/src/runtime/namespace.ts +483 -0
  48. package/src/runtime/server.ts +87 -0
  49. package/src/runtime/ws.ts +197 -0
  50. package/src/shared/ir.ts +197 -0
  51. package/tests/client.test.ts +38 -0
  52. package/tests/codegen.test.ts +71 -0
  53. package/tests/compiler.test.ts +45 -0
  54. package/tests/graph_rules.test.ts +173 -0
  55. package/tests/integration/db.test.ts +174 -0
  56. package/tests/integration/e2e.test.ts +65 -0
  57. package/tests/integration/features.test.ts +922 -0
  58. package/tests/integration/full_stack.test.ts +262 -0
  59. package/tests/integration/setup.ts +45 -0
  60. package/tests/ir.test.ts +32 -0
  61. package/tests/namespace.test.ts +395 -0
  62. package/tests/permissions.test.ts +55 -0
  63. package/tests/pinia.test.ts +48 -0
  64. package/tests/realtime.test.ts +22 -0
  65. package/tests/runtime.test.ts +80 -0
  66. package/tests/subscribable_gen.test.ts +72 -0
  67. package/tests/subscribable_reactivity.test.ts +258 -0
  68. package/tests/venues_gen.test.ts +25 -0
  69. package/tsconfig.json +20 -0
  70. package/tsconfig.tsbuildinfo +1 -0
  71. package/README.md +0 -90
  72. package/bin/cli.js +0 -727
  73. package/docs/compiler/ADVANCED_FILTERS.md +0 -183
  74. package/docs/compiler/CODING_STANDARDS.md +0 -415
  75. package/docs/compiler/COMPARISON.md +0 -673
  76. package/docs/compiler/QUICKSTART.md +0 -326
  77. package/docs/compiler/README.md +0 -134
  78. package/docs/examples/README.md +0 -38
  79. package/docs/examples/blog.sql +0 -160
  80. package/docs/examples/venue-detail-simple.sql +0 -8
  81. package/docs/examples/venue-detail-subscribable.sql +0 -45
  82. package/docs/for-ai/claude-guide.md +0 -1210
  83. package/docs/getting-started/quickstart.md +0 -125
  84. package/docs/getting-started/subscriptions-quick-start.md +0 -203
  85. package/docs/getting-started/tutorial.md +0 -1104
  86. package/docs/guides/atomic-updates.md +0 -299
  87. package/docs/guides/client-stores.md +0 -730
  88. package/docs/guides/composite-primary-keys.md +0 -158
  89. package/docs/guides/custom-functions.md +0 -362
  90. package/docs/guides/drop-semantics.md +0 -554
  91. package/docs/guides/field-defaults.md +0 -240
  92. package/docs/guides/interpreter-vs-compiler.md +0 -237
  93. package/docs/guides/many-to-many.md +0 -929
  94. package/docs/guides/subscriptions.md +0 -537
  95. package/docs/reference/api.md +0 -1373
  96. package/docs/reference/client.md +0 -224
  97. package/src/client/stores/index.js +0 -8
  98. package/src/client/stores/useAppStore.js +0 -285
  99. package/src/client/stores/useWsStore.js +0 -289
  100. package/src/client/ws.js +0 -762
  101. package/src/compiler/cli/compile-example.js +0 -33
  102. package/src/compiler/cli/compile-subscribable.js +0 -43
  103. package/src/compiler/cli/debug-compile.js +0 -44
  104. package/src/compiler/cli/debug-parse.js +0 -26
  105. package/src/compiler/cli/debug-path-parser.js +0 -18
  106. package/src/compiler/cli/debug-subscribable-parser.js +0 -21
  107. package/src/compiler/cli/index.js +0 -174
  108. package/src/compiler/codegen/auth-codegen.js +0 -153
  109. package/src/compiler/codegen/drop-semantics-codegen.js +0 -553
  110. package/src/compiler/codegen/graph-rules-codegen.js +0 -450
  111. package/src/compiler/codegen/notification-codegen.js +0 -232
  112. package/src/compiler/codegen/operation-codegen.js +0 -1382
  113. package/src/compiler/codegen/permission-codegen.js +0 -318
  114. package/src/compiler/codegen/subscribable-codegen.js +0 -827
  115. package/src/compiler/compiler.js +0 -371
  116. package/src/compiler/index.js +0 -11
  117. package/src/compiler/parser/entity-parser.js +0 -440
  118. package/src/compiler/parser/path-parser.js +0 -290
  119. package/src/compiler/parser/subscribable-parser.js +0 -244
  120. package/src/database/dzql-core.sql +0 -161
  121. package/src/database/migrations/001_schema.sql +0 -60
  122. package/src/database/migrations/002_functions.sql +0 -890
  123. package/src/database/migrations/003_operations.sql +0 -1135
  124. package/src/database/migrations/004_search.sql +0 -581
  125. package/src/database/migrations/005_entities.sql +0 -730
  126. package/src/database/migrations/006_auth.sql +0 -94
  127. package/src/database/migrations/007_events.sql +0 -133
  128. package/src/database/migrations/008_hello.sql +0 -18
  129. package/src/database/migrations/008a_meta.sql +0 -172
  130. package/src/database/migrations/009_subscriptions.sql +0 -240
  131. package/src/database/migrations/010_atomic_updates.sql +0 -157
  132. package/src/database/migrations/010_fix_m2m_events.sql +0 -94
  133. package/src/index.js +0 -40
  134. package/src/server/api.js +0 -9
  135. package/src/server/db.js +0 -442
  136. package/src/server/index.js +0 -317
  137. package/src/server/logger.js +0 -259
  138. package/src/server/mcp.js +0 -594
  139. package/src/server/meta-route.js +0 -251
  140. package/src/server/namespace.js +0 -292
  141. package/src/server/subscriptions.js +0 -351
  142. 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
+ });