dzql 0.5.32 → 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,262 @@
1
+ import { describe, test, expect, beforeAll, afterAll } from "bun:test";
2
+ import { spawn, spawnSync } from "bun";
3
+ import { V2TestDatabase } from "./setup.js";
4
+ import { existsSync, readFileSync, writeFileSync, mkdirSync } from "fs";
5
+ import { resolve, dirname } from "path";
6
+
7
+ // Tests may run from repo root or packages/tzql directory
8
+ // Use import.meta to get the correct path relative to this file
9
+ const TEST_DIR = dirname(new URL(import.meta.url).pathname);
10
+ const PACKAGE_ROOT = resolve(TEST_DIR, "../..");
11
+ const DIST_ROOT = resolve(PACKAGE_ROOT, "dist");
12
+
13
+ // Compile the venues example before tests run
14
+ function compileVenuesExample() {
15
+ const examplePath = resolve(PACKAGE_ROOT, "examples/venues.js");
16
+ const compilerPath = resolve(PACKAGE_ROOT, "src/cli/index.ts");
17
+
18
+ console.log("[Test] Compiling venues example...");
19
+ const result = spawnSync({
20
+ cmd: ["bun", "run", compilerPath, "compile", examplePath, "-o", DIST_ROOT],
21
+ cwd: PACKAGE_ROOT,
22
+ env: process.env,
23
+ stdout: "pipe",
24
+ stderr: "pipe"
25
+ });
26
+
27
+ if (result.exitCode !== 0) {
28
+ console.error("[Test] Compile failed:", new TextDecoder().decode(result.stderr));
29
+ return false;
30
+ }
31
+ console.log("[Test] Compile output:", new TextDecoder().decode(result.stdout));
32
+ return true;
33
+ }
34
+
35
+ // Pre-process the generated client to fix the import path for testing
36
+ function patchGeneratedClient() {
37
+ const clientPath = resolve(DIST_ROOT, "client/ws.ts");
38
+ if (!existsSync(clientPath)) return;
39
+
40
+ const content = readFileSync(clientPath, "utf8");
41
+ if (content.includes("from 'tzql/client'")) {
42
+ const tzqlClientPath = resolve(PACKAGE_ROOT, "src/client/index.ts");
43
+ const patched = content.replace(
44
+ "from 'tzql/client'",
45
+ `from '${tzqlClientPath}'`
46
+ );
47
+ writeFileSync(clientPath, patched);
48
+ }
49
+ }
50
+
51
+ // Compile and check if dist exists
52
+ const COMPILE_SUCCESS = compileVenuesExample();
53
+ const DIST_EXISTS = COMPILE_SUCCESS && existsSync(resolve(DIST_ROOT, "runtime/manifest.json"));
54
+
55
+ if (DIST_EXISTS) {
56
+ patchGeneratedClient();
57
+ }
58
+
59
+ describe.skipIf(!DIST_EXISTS)("Full Stack V2 Integration (Runtime + Client + Pinia)", () => {
60
+ let db: V2TestDatabase;
61
+ let sql: any;
62
+ let serverProcess: any;
63
+ let useVenueDetailStore: any;
64
+ let ws: any;
65
+
66
+ beforeAll(async () => {
67
+ // Dynamic imports for optional dependencies
68
+ const { createPinia, setActivePinia } = await import("pinia");
69
+ const clientPath = resolve(DIST_ROOT, "client/ws.ts");
70
+ ws = (await import(clientPath)).ws;
71
+
72
+ // 1. Setup DB
73
+ db = new V2TestDatabase();
74
+ sql = await db.setup();
75
+
76
+ // Apply Schema (Core + Venues)
77
+ const fs = require('fs');
78
+ const path = require('path');
79
+ const distPath = resolve(DIST_ROOT, 'db/migrations');
80
+ const coreSql = fs.readFileSync(path.join(distPath, '000_core.sql'), 'utf8');
81
+ const schemaFiles = fs.readdirSync(distPath)
82
+ .filter((f: string) => f.includes('_schema.sql'))
83
+ .sort().reverse();
84
+ const subscribableFiles = fs.readdirSync(distPath)
85
+ .filter((f: string) => f.includes('_subscribables.sql'))
86
+ .sort().reverse();
87
+ console.log(`[Test] Loading schema file: ${schemaFiles[0]}`);
88
+ const entitySql = fs.readFileSync(path.join(distPath, schemaFiles[0]), 'utf8');
89
+
90
+ await db.applySQL(coreSql);
91
+ await db.applySQL(entitySql);
92
+
93
+ // Load subscribable SQL functions
94
+ if (subscribableFiles.length > 0) {
95
+ console.log(`[Test] Loading subscribables file: ${subscribableFiles[0]}`);
96
+ const subscribableSql = fs.readFileSync(path.join(distPath, subscribableFiles[0]), 'utf8');
97
+ await db.applySQL(subscribableSql);
98
+ }
99
+
100
+ // 2. Start Runtime Server
101
+ const runtimePath = new URL("../../src/runtime/index.ts", import.meta.url).pathname;
102
+ const manifestPath = resolve(DIST_ROOT, 'runtime/manifest.json');
103
+
104
+ const testDbUrl = db.baseUrl.replace(/\/[^/]*$/, `/${db.dbName}`);
105
+ console.log("[Test] Server DATABASE_URL:", testDbUrl);
106
+ console.log("[Test] MANIFEST_PATH:", manifestPath);
107
+ serverProcess = spawn({
108
+ cmd: ["bun", "run", runtimePath],
109
+ env: {
110
+ ...process.env,
111
+ PORT: "3001", // Test port
112
+ DATABASE_URL: testDbUrl,
113
+ MANIFEST_PATH: manifestPath,
114
+ JWT_SECRET: "test-secret"
115
+ },
116
+ stdout: "pipe",
117
+ stderr: "pipe"
118
+ });
119
+
120
+ // Pipe stderr to console for debugging
121
+ const stderrReader = serverProcess.stderr.getReader();
122
+ (async () => {
123
+ while (true) {
124
+ const { done, value } = await stderrReader.read();
125
+ if (done) break;
126
+ console.error("[Server ERR]", new TextDecoder().decode(value));
127
+ }
128
+ })();
129
+
130
+ // Pipe stdout to console (keep reading in background)
131
+ let serverReady = false;
132
+ const serverReadyPromise = new Promise<void>((resolve) => {
133
+ const stdoutReader = serverProcess.stdout.getReader();
134
+ (async () => {
135
+ while (true) {
136
+ const { done, value } = await stdoutReader.read();
137
+ if (done) break;
138
+ const text = new TextDecoder().decode(value);
139
+ console.log("[Server]", text.trim());
140
+ if (text.includes("Server listening") && !serverReady) {
141
+ serverReady = true;
142
+ resolve();
143
+ }
144
+ }
145
+ })();
146
+ });
147
+
148
+ // Wait for server to be ready
149
+ await serverReadyPromise;
150
+
151
+ // 3. Setup Pinia
152
+ setActivePinia(createPinia());
153
+
154
+ // Import generated store
155
+ const storePath = resolve(DIST_ROOT, "client/stores/useVenueDetailStore.ts");
156
+ const mod = await import(storePath);
157
+ useVenueDetailStore = mod.useVenueDetailStore;
158
+ });
159
+
160
+ afterAll(async () => {
161
+ if (serverProcess) serverProcess.kill();
162
+ await db.teardown();
163
+ });
164
+
165
+ test("should receive connection:ready on connect (anonymous)", async () => {
166
+ // Connect Client without token
167
+ await ws.connect("ws://localhost:3001/ws");
168
+
169
+ // Wait briefly for connection:ready message
170
+ await new Promise(r => setTimeout(r, 100));
171
+
172
+ // Should be ready but no user
173
+ expect(ws.ready).toBe(true);
174
+ expect(ws.user).toBe(null);
175
+ });
176
+
177
+ test("should register and login", async () => {
178
+ // Register
179
+ const reg = await ws.register({ email: "tester@example.com", password: "password123" });
180
+ expect(reg.user_id).toBeDefined();
181
+
182
+ // Login
183
+ const login = await ws.login({ email: "tester@example.com", password: "password123" });
184
+ expect(login.token).toBeDefined();
185
+ });
186
+
187
+ test("should receive connection:ready with user profile on reconnect with token", async () => {
188
+ // Login to get a token
189
+ const login = await ws.login({ email: "tester@example.com", password: "password123" });
190
+ const token = login.token;
191
+ expect(token).toBeDefined();
192
+
193
+ // Close current connection
194
+ ws.ws?.close();
195
+ await new Promise(r => setTimeout(r, 100));
196
+
197
+ // Reconnect with token in URL (simulating what browser does with localStorage)
198
+ await ws.connect(`ws://localhost:3001/ws?token=${encodeURIComponent(token)}`);
199
+
200
+ // Wait for connection:ready message
201
+ await new Promise(r => setTimeout(r, 200));
202
+
203
+ // Should be ready with user profile
204
+ expect(ws.ready).toBe(true);
205
+ expect(ws.user).not.toBe(null);
206
+ expect(ws.user.email).toBe("tester@example.com");
207
+ // Password hash should be stripped
208
+ expect(ws.user.password_hash).toBeUndefined();
209
+ });
210
+
211
+ test("should sync data via Pinia store", async () => {
212
+ const store = useVenueDetailStore();
213
+
214
+ // 1. Create Data (via SDK)
215
+ // We need an Org first because Venue requires it
216
+ const org = await ws.api.save_organisations({ name: "Test Org" });
217
+
218
+ // VERIFY GRAPH RULE: Check acts_for
219
+ const memberships = await sql`SELECT * FROM acts_for WHERE org_id = ${org.id}`;
220
+ console.log("[Test] Memberships:", memberships);
221
+ if (memberships.length === 0) {
222
+ console.error("[Test] Graph Rule Failed: No acts_for created!");
223
+ } else {
224
+ console.log("[Test] Active:", memberships[0].active);
225
+ }
226
+
227
+ // Create Venue
228
+ const venue = await ws.api.save_venues({ name: "Live Sync Venue", org_id: org.id, address: "123 Web St" });
229
+
230
+ // 2. Bind Store (async - waits for first data)
231
+ const doc = await store.bind({ venue_id: venue.id });
232
+
233
+ // Vue reactivity auto-unwraps refs when accessed from a reactive object
234
+ // So doc.loading is already the value (boolean), not the ref
235
+ expect(doc.loading).toBe(false);
236
+ // Verify subscription received data with correct structure
237
+ expect(doc.data?.venues?.name).toBe("Live Sync Venue");
238
+ expect(doc.data?.venues?.org_id).toBe(org.id);
239
+ expect(doc.data?.org?.name).toBe("Test Org");
240
+ expect(doc.data?.sites).toEqual([]);
241
+
242
+ // 3. Update Data (via SDK) -> Should trigger Realtime Update
243
+ // Note: org_id needed for permission check on update
244
+ const updated = await ws.api.save_venues({ id: venue.id, org_id: org.id, name: "Updated via WebSocket" });
245
+ expect(updated.name).toBe("Updated via WebSocket");
246
+
247
+ // Check if events were written to the database
248
+ const events = await sql`SELECT * FROM dzql_v2.events ORDER BY id DESC LIMIT 5`;
249
+ console.log("[Test] Events in DB:", events.length, events.map((e: any) => ({ table: e.table_name, op: e.op, data_name: e.data?.name })));
250
+
251
+ // Manually trigger a NOTIFY to test if listener works
252
+ console.log("[Test] Manually triggering pg_notify...");
253
+ await sql`SELECT pg_notify('dzql_v2', '{"commit_id": 999}')`;
254
+
255
+ // Wait for event propagation
256
+ await new Promise(r => setTimeout(r, 500));
257
+
258
+ // 4. Verify Store Update via Realtime
259
+ console.log("[Test] After realtime wait - doc.data?.venues?.name:", doc.data?.venues?.name);
260
+ expect(doc.data?.venues?.name).toBe("Updated via WebSocket");
261
+ });
262
+ });
@@ -0,0 +1,45 @@
1
+ import postgres from "postgres";
2
+
3
+ export class V2TestDatabase {
4
+ adminSql: any;
5
+ testSql: any;
6
+ dbName: string;
7
+ baseUrl: string;
8
+
9
+ constructor() {
10
+ this.dbName = `tzql_test_${process.pid}`;
11
+ this.baseUrl = process.env.DATABASE_URL || "postgres://dzql_test:dzql_test@localhost:5433/dzql_test";
12
+ }
13
+
14
+ async setup() {
15
+ // 1. Connect as admin to create DB
16
+ const adminUrl = this.baseUrl; // Assumes baseUrl points to accessible DB
17
+ this.adminSql = postgres(adminUrl, { max: 1, onnotice: () => {} });
18
+
19
+ try {
20
+ await this.adminSql.unsafe(`DROP DATABASE IF EXISTS ${this.dbName}`);
21
+ await this.adminSql.unsafe(`CREATE DATABASE ${this.dbName}`);
22
+ } catch (e) {
23
+ // Ignore if DB creation fails (might exist)
24
+ }
25
+
26
+ // 2. Connect to the fresh Test DB
27
+ const testUrl = this.baseUrl.replace(/\/[^/]*$/, `/${this.dbName}`);
28
+ this.testSql = postgres(testUrl, { onnotice: () => {} });
29
+
30
+ return this.testSql;
31
+ }
32
+
33
+ async applySQL(sqlContent: string) {
34
+ if (!this.testSql) throw new Error("DB not connected");
35
+ await this.testSql.unsafe(sqlContent);
36
+ }
37
+
38
+ async teardown() {
39
+ if (this.testSql) await this.testSql.end();
40
+ if (this.adminSql) {
41
+ await this.adminSql.unsafe(`DROP DATABASE IF EXISTS ${this.dbName}`);
42
+ await this.adminSql.end();
43
+ }
44
+ }
45
+ }
@@ -0,0 +1,32 @@
1
+ import { describe, test, expect } from "bun:test";
2
+ import { generateIR } from "../src/cli/compiler/ir.js";
3
+
4
+ const rawDomain = {
5
+ entities: {
6
+ users: {
7
+ schema: {
8
+ id: "serial PRIMARY KEY",
9
+ name: "text NOT NULL"
10
+ },
11
+ permissions: {
12
+ view: ["public"]
13
+ }
14
+ }
15
+ },
16
+ subscribables: {}
17
+ };
18
+
19
+ describe("IR Generator", () => {
20
+ test("should normalize entities", () => {
21
+ const ir = generateIR(rawDomain);
22
+
23
+ expect(ir.entities.users).toBeDefined();
24
+ expect(ir.entities.users.primaryKey).toEqual(["id"]);
25
+ expect(ir.entities.users.columns).toHaveLength(2);
26
+ expect(ir.entities.users.columns[0].name).toBe("id");
27
+
28
+ // Check permission normalization
29
+ expect(ir.entities.users.permissions.view).toEqual(["public"]);
30
+ expect(ir.entities.users.permissions.create).toEqual([]); // Defaulted
31
+ });
32
+ });