dzql 0.6.3 → 0.6.5

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 (41) hide show
  1. package/README.md +33 -0
  2. package/docs/for_ai.md +14 -18
  3. package/docs/project-setup.md +15 -14
  4. package/package.json +28 -6
  5. package/src/cli/codegen/client.ts +5 -6
  6. package/src/cli/codegen/subscribable_store.ts +5 -5
  7. package/src/runtime/ws.ts +16 -15
  8. package/.env.sample +0 -28
  9. package/compose.yml +0 -28
  10. package/dist/client/index.ts +0 -1
  11. package/dist/client/stores/useMyProfileStore.ts +0 -114
  12. package/dist/client/stores/useOrgDashboardStore.ts +0 -131
  13. package/dist/client/stores/useVenueDetailStore.ts +0 -117
  14. package/dist/client/ws.ts +0 -716
  15. package/dist/db/migrations/000_core.sql +0 -92
  16. package/dist/db/migrations/20260101T235039268Z_schema.sql +0 -3020
  17. package/dist/db/migrations/20260101T235039268Z_subscribables.sql +0 -371
  18. package/dist/runtime/manifest.json +0 -1562
  19. package/examples/blog.ts +0 -50
  20. package/examples/invalid.ts +0 -18
  21. package/examples/venues.js +0 -485
  22. package/tests/client.test.ts +0 -38
  23. package/tests/codegen.test.ts +0 -71
  24. package/tests/compiler.test.ts +0 -45
  25. package/tests/graph_rules.test.ts +0 -173
  26. package/tests/integration/db.test.ts +0 -174
  27. package/tests/integration/e2e.test.ts +0 -65
  28. package/tests/integration/features.test.ts +0 -922
  29. package/tests/integration/full_stack.test.ts +0 -262
  30. package/tests/integration/setup.ts +0 -45
  31. package/tests/ir.test.ts +0 -32
  32. package/tests/namespace.test.ts +0 -395
  33. package/tests/permissions.test.ts +0 -55
  34. package/tests/pinia.test.ts +0 -48
  35. package/tests/realtime.test.ts +0 -22
  36. package/tests/runtime.test.ts +0 -80
  37. package/tests/subscribable_gen.test.ts +0 -72
  38. package/tests/subscribable_reactivity.test.ts +0 -258
  39. package/tests/venues_gen.test.ts +0 -25
  40. package/tsconfig.json +0 -20
  41. package/tsconfig.tsbuildinfo +0 -1
@@ -1,262 +0,0 @@
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
- });
@@ -1,45 +0,0 @@
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
- }
package/tests/ir.test.ts DELETED
@@ -1,32 +0,0 @@
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
- });