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,166 @@
1
+ #!/usr/bin/env bun
2
+ import { loadDomain } from "./compiler/loader.js";
3
+ import { analyzeDomain } from "./compiler/analyzer.js";
4
+ import { generateIR } from "./compiler/ir.js";
5
+ import { generateCoreSQL, generateEntitySQL, generateSchemaSQL } from "./codegen/sql.js";
6
+ import { generateSubscribableSQL } from "./codegen/subscribable_sql.js";
7
+ import { generateManifest } from "./codegen/manifest.js";
8
+ import { generateSubscribableStore } from "./codegen/subscribable_store.js";
9
+ import { generateClientSDK } from "./codegen/client.js";
10
+ import { writeFileSync, mkdirSync, copyFileSync, rmSync } from "fs";
11
+ import { resolve, dirname } from "path";
12
+
13
+ const args = process.argv.slice(2);
14
+ let command = args[0];
15
+ let input = args[1];
16
+ let outputDir = "dist"; // Default output directory
17
+
18
+ // If first arg looks like a file (ends with .ts or .js), treat it as compile target
19
+ if (command && (command.endsWith('.ts') || command.endsWith('.js'))) {
20
+ input = command;
21
+ command = 'compile';
22
+ }
23
+
24
+ // Parse optional output directory flag
25
+ const outputFlagIndex = args.indexOf('-o');
26
+ const longOutputFlagIndex = args.indexOf('--output');
27
+
28
+ if (outputFlagIndex > -1 && args[outputFlagIndex + 1]) {
29
+ outputDir = args[outputFlagIndex + 1];
30
+ } else if (longOutputFlagIndex > -1 && args[longOutputFlagIndex + 1]) {
31
+ outputDir = args[longOutputFlagIndex + 1];
32
+ }
33
+
34
+ async function main() {
35
+ console.log("DZQL Compiler v0.6.0");
36
+
37
+ if (command === "compile") {
38
+ if (!input) {
39
+ console.error("Usage: dzql <file> or dzql compile <file>");
40
+ process.exit(1);
41
+ }
42
+
43
+ try {
44
+ // Phase 1: Load & Analyze
45
+ const fullInputPath = resolve(process.cwd(), input);
46
+
47
+ // Clean Output Directory
48
+ const absOutputDir = resolve(process.cwd(), outputDir);
49
+ console.log(`[Compiler] Cleaning ${absOutputDir}...`);
50
+ try {
51
+ rmSync(absOutputDir, { recursive: true, force: true });
52
+ } catch (e) { /* ignore */ }
53
+
54
+ const domain = await loadDomain(fullInputPath);
55
+ console.log("[Compiler] Domain loaded.");
56
+
57
+ const errors = analyzeDomain(domain);
58
+ if (errors.length > 0) {
59
+ console.error("[Compiler] Validation Failed:");
60
+ errors.forEach(err => console.error(` - ${err}`));
61
+ process.exit(1);
62
+ }
63
+
64
+ // Phase 2: Generate IR
65
+ const ir = generateIR(domain);
66
+ console.log(`[Compiler] IR Generated. Subscribables: ${Object.keys(ir.subscribables).join(', ')}`);
67
+
68
+ // Phase 3: Generate SQL
69
+ const coreSQL = generateCoreSQL();
70
+ const entitySQL: string[] = [];
71
+ for (const [name, entityIR] of Object.entries(ir.entities)) {
72
+ entitySQL.push(generateSchemaSQL(name, entityIR));
73
+ // Skip CRUD generation for unmanaged entities (e.g., junction tables)
74
+ if (entityIR.managed !== false) {
75
+ entitySQL.push(generateEntitySQL(name, entityIR));
76
+ } else {
77
+ console.log(`[Compiler] Skipping CRUD for unmanaged entity: ${name}`);
78
+ }
79
+ }
80
+
81
+ // Generate subscribable SQL functions
82
+ const subscribableSQL: string[] = [];
83
+ for (const [name, subIR] of Object.entries(ir.subscribables)) {
84
+ console.log(`[Compiler] Generating SQL for subscribable: ${name}`);
85
+ subscribableSQL.push(generateSubscribableSQL(name, subIR as any, ir.entities));
86
+ }
87
+
88
+ // Collect custom functions SQL
89
+ const customFunctionSQL: string[] = [];
90
+ for (const fn of ir.customFunctions) {
91
+ console.log(`[Compiler] Adding custom function: ${fn.name}`);
92
+ customFunctionSQL.push(fn.sql);
93
+ }
94
+
95
+ // Phase 4: Generate Manifest
96
+ const manifest = generateManifest(ir);
97
+
98
+ // --- OUTPUT GENERATION ---
99
+
100
+ // 1. Database
101
+ const dbDir = resolve(outputDir, "db/migrations");
102
+ mkdirSync(dbDir, { recursive: true });
103
+
104
+ writeFileSync(resolve(dbDir, `000_core.sql`), coreSQL);
105
+ const timestamp = new Date().toISOString().replace(/[:.-]/g, '');
106
+
107
+ // Combine entity SQL with custom functions
108
+ const schemaContent = customFunctionSQL.length > 0
109
+ ? entitySQL.join('\n') + '\n\n-- Custom Functions\n' + customFunctionSQL.join('\n\n')
110
+ : entitySQL.join('\n');
111
+ writeFileSync(resolve(dbDir, `${timestamp}_schema.sql`), schemaContent);
112
+
113
+ if (customFunctionSQL.length > 0) {
114
+ console.log(`[Generated] ${customFunctionSQL.length} Custom Functions`);
115
+ }
116
+
117
+ // Write subscribable SQL
118
+ if (subscribableSQL.length > 0) {
119
+ writeFileSync(resolve(dbDir, `${timestamp}_subscribables.sql`), subscribableSQL.join('\n\n'));
120
+ console.log(`[Generated] ${subscribableSQL.length} Subscribable SQL functions`);
121
+ }
122
+
123
+ console.log(`[Generated] DB Migrations in ${dbDir}`);
124
+
125
+ // 2. Runtime
126
+ const runtimeDir = resolve(outputDir, "runtime");
127
+ mkdirSync(runtimeDir, { recursive: true });
128
+ writeFileSync(resolve(runtimeDir, `manifest.json`), JSON.stringify(manifest, null, 2));
129
+ console.log(`[Generated] Runtime Manifest in ${runtimeDir}`);
130
+
131
+ // 3. Client SDK (TypeScript)
132
+ const clientDir = resolve(outputDir, "client");
133
+ mkdirSync(clientDir, { recursive: true });
134
+
135
+ // Generate Core SDK as TypeScript
136
+ const clientCode = generateClientSDK(manifest);
137
+ writeFileSync(resolve(clientDir, `ws.ts`), clientCode);
138
+
139
+ // Generate Index
140
+ writeFileSync(resolve(clientDir, `index.ts`), `export * from './ws.js';`);
141
+
142
+ console.log(`[Generated] Client SDK in ${clientDir}`);
143
+
144
+ // 4. Stores (TypeScript)
145
+ const storeDir = resolve(clientDir, "stores");
146
+ mkdirSync(storeDir, { recursive: true });
147
+
148
+ for (const subName of Object.keys(ir.subscribables)) {
149
+ const storeCode = generateSubscribableStore(manifest, subName);
150
+ const fileName = `use${subName.replace(/(^|_)([a-z])/g, (g) => g.at(-1)!.toUpperCase())}Store.ts`;
151
+ writeFileSync(resolve(storeDir, fileName), storeCode);
152
+ }
153
+ console.log(`[Generated] ${Object.keys(ir.subscribables).length} Pinia Stores in ${storeDir}`);
154
+
155
+ console.log("[Compiler] Build Complete.");
156
+
157
+ } catch (e) {
158
+ console.error(e);
159
+ process.exit(1);
160
+ }
161
+ } else {
162
+ console.log("Unknown command. Try 'compile'.");
163
+ }
164
+ }
165
+
166
+ main();
@@ -0,0 +1 @@
1
+ export * from './ws.js';
@@ -0,0 +1,286 @@
1
+ // Core WebSocket Manager for TZQL Client
2
+ // Handles connection, auth, reconnects, and message dispatching.
3
+ // This is a pure transport layer - it does not manage or cache data.
4
+
5
+ export interface WebSocketOptions {
6
+ url?: string;
7
+ maxReconnectAttempts?: number;
8
+ tokenName?: string;
9
+ }
10
+
11
+ // Get default token name from environment (build-time injection)
12
+ function getDefaultTokenName(): string {
13
+ // Vite: import.meta.env.VITE_TZQL_TOKEN_NAME
14
+ // @ts-ignore
15
+ if (typeof import.meta !== 'undefined' && import.meta.env?.VITE_TZQL_TOKEN_NAME) {
16
+ // @ts-ignore
17
+ return import.meta.env.VITE_TZQL_TOKEN_NAME;
18
+ }
19
+ // Node/bundlers: process.env.TZQL_TOKEN_NAME
20
+ if (typeof process !== 'undefined' && process.env?.TZQL_TOKEN_NAME) {
21
+ return process.env.TZQL_TOKEN_NAME;
22
+ }
23
+ return 'tzql_token';
24
+ }
25
+
26
+ export class WebSocketManager {
27
+ protected ws: WebSocket | null = null;
28
+ protected messageId = 0;
29
+ protected pendingRequests = new Map<number, { resolve: (val: any) => void; reject: (err: any) => void }>();
30
+ protected methodHandlers = new Map<string, Set<(params: any) => void>>();
31
+ protected subscriptionCallbacks = new Map<string, (event: any) => void>();
32
+ protected readyCallbacks = new Set<(user: any) => void>();
33
+ protected reconnectAttempts = 0;
34
+ protected maxReconnectAttempts = 5;
35
+ protected tokenName = 'tzql_token';
36
+ protected isShuttingDown = false;
37
+
38
+ // Connection state
39
+ public user: any = null;
40
+ public ready: boolean = false;
41
+
42
+ // To be populated by generated code
43
+ public api: any = {};
44
+
45
+ constructor(options: WebSocketOptions = {}) {
46
+ this.maxReconnectAttempts = options.maxReconnectAttempts ?? 5;
47
+ this.tokenName = options.tokenName ?? getDefaultTokenName();
48
+ }
49
+
50
+ async login(credentials: any) {
51
+ try {
52
+ const result = await this.call('login_user', credentials);
53
+ if (result && result.token) {
54
+ if (typeof localStorage !== 'undefined') {
55
+ localStorage.setItem(this.tokenName, result.token);
56
+ }
57
+ await this.authenticate(result.token);
58
+ }
59
+ return result;
60
+ } catch (e) {
61
+ throw e;
62
+ }
63
+ }
64
+
65
+ async authenticate(token: string) {
66
+ return this.call('auth', { token });
67
+ }
68
+
69
+ async register(credentials: any, options: any = {}) {
70
+ try {
71
+ const params = { ...credentials, options };
72
+ const result = await this.call('register_user', params);
73
+ if (result && result.token) {
74
+ if (typeof localStorage !== 'undefined') {
75
+ localStorage.setItem(this.tokenName, result.token);
76
+ }
77
+ }
78
+ return result;
79
+ } catch (e) {
80
+ throw e;
81
+ }
82
+ }
83
+
84
+ async logout() {
85
+ if (typeof localStorage !== 'undefined') {
86
+ localStorage.removeItem(this.tokenName);
87
+ }
88
+ this.user = null;
89
+ this.ready = false;
90
+ try { await this.call('logout'); } catch(e) {}
91
+ this.ws?.close();
92
+ }
93
+
94
+ connect(url: string | null = null, timeout = 5000): Promise<void> {
95
+ return new Promise((resolve, reject) => {
96
+ this.ready = false;
97
+ this.user = null;
98
+
99
+ let wsUrl = url;
100
+ if (!wsUrl) {
101
+ if (typeof window !== "undefined") {
102
+ const protocol = window.location.protocol === "https:" ? "wss:" : "ws:";
103
+ wsUrl = protocol + "//" + window.location.host + "/ws";
104
+ } else {
105
+ wsUrl = "ws://localhost:3000/ws";
106
+ }
107
+ }
108
+
109
+ if (typeof localStorage !== 'undefined') {
110
+ const token = localStorage.getItem(this.tokenName);
111
+ if (token) {
112
+ if (wsUrl.includes('?')) wsUrl += '&token=' + encodeURIComponent(token);
113
+ else wsUrl += '?token=' + encodeURIComponent(token);
114
+ }
115
+ }
116
+
117
+ const connectionTimeout = setTimeout(() => {
118
+ if (this.ws) this.ws.close();
119
+ reject(new Error('WebSocket connection timed out after ' + timeout + 'ms'));
120
+ }, timeout);
121
+
122
+ this.ws = new WebSocket(wsUrl);
123
+
124
+ this.ws.onopen = () => {
125
+ clearTimeout(connectionTimeout);
126
+ console.log('[TZQL] Connected to ' + wsUrl);
127
+ this.reconnectAttempts = 0;
128
+ resolve();
129
+ };
130
+
131
+ this.ws.onmessage = (event) => {
132
+ try {
133
+ const message = JSON.parse(event.data);
134
+ this.handleMessage(message);
135
+ } catch (error) {
136
+ console.error("[TZQL] Failed to parse message:", error);
137
+ }
138
+ };
139
+
140
+ this.ws.onclose = () => {
141
+ console.log("[TZQL] Disconnected");
142
+ if (!this.isShuttingDown) {
143
+ this.attemptReconnect();
144
+ }
145
+ };
146
+
147
+ this.ws.onerror = (error) => {
148
+ clearTimeout(connectionTimeout);
149
+ console.error("[TZQL] Connection error:", error);
150
+ reject(error);
151
+ };
152
+ });
153
+ }
154
+
155
+ attemptReconnect() {
156
+ if (this.reconnectAttempts < this.maxReconnectAttempts) {
157
+ this.reconnectAttempts++;
158
+ const delay = 1000 * this.reconnectAttempts;
159
+ setTimeout(() => {
160
+ console.log('[TZQL] Reconnecting (' + this.reconnectAttempts + ')...');
161
+ this.connect();
162
+ }, delay);
163
+ }
164
+ }
165
+
166
+ handleMessage(message: any) {
167
+ // Handle connection:ready message
168
+ if (message.method === "connection:ready") {
169
+ this.user = message.params?.user || null;
170
+ this.ready = true;
171
+ this.readyCallbacks.forEach((cb) => cb(this.user));
172
+ return;
173
+ }
174
+
175
+ // Handle RPC responses (messages with id)
176
+ if (message.id && this.pendingRequests.has(message.id)) {
177
+ const resolver = this.pendingRequests.get(message.id);
178
+ if (resolver) {
179
+ this.pendingRequests.delete(message.id);
180
+ if (message.error) {
181
+ const err: any = new Error(message.error.message || 'Unknown error');
182
+ err.code = message.error.code;
183
+ resolver.reject(err);
184
+ } else {
185
+ resolver.resolve(message.result);
186
+ }
187
+ }
188
+ return;
189
+ }
190
+
191
+ // Handle subscription events - dispatch to registered subscription callbacks
192
+ if (message.method === "subscription:event") {
193
+ const event = message.params?.event;
194
+ if (event) {
195
+ // Dispatch to all subscription handlers - they filter by table/scope
196
+ for (const [subId, callback] of this.subscriptionCallbacks) {
197
+ callback(event);
198
+ }
199
+ }
200
+ return;
201
+ }
202
+
203
+ // Handle other server-initiated messages (broadcasts) - route to registered handlers
204
+ if (message.method) {
205
+ const handlers = this.methodHandlers.get(message.method);
206
+ if (handlers) {
207
+ handlers.forEach((cb) => cb(message.params));
208
+ }
209
+ }
210
+ }
211
+
212
+ call(method: string, params: any = {}) {
213
+ return new Promise((resolve, reject) => {
214
+ if (!this.ws || this.ws.readyState !== WebSocket.OPEN) {
215
+ reject(new Error("WebSocket not connected"));
216
+ return;
217
+ }
218
+ const id = ++this.messageId;
219
+ this.pendingRequests.set(id, { resolve, reject });
220
+ this.ws.send(JSON.stringify({ jsonrpc: "2.0", method, params, id }));
221
+ });
222
+ }
223
+
224
+ /**
225
+ * Register a callback for a server-initiated method
226
+ * @param method - The method name to listen for
227
+ * @param callback - Called with params when server sends this method
228
+ * @returns Unsubscribe function
229
+ */
230
+ on(method: string, callback: (params: any) => void) {
231
+ if (!this.methodHandlers.has(method)) {
232
+ this.methodHandlers.set(method, new Set());
233
+ }
234
+ this.methodHandlers.get(method)!.add(callback);
235
+ return () => {
236
+ const handlers = this.methodHandlers.get(method);
237
+ if (handlers) {
238
+ handlers.delete(callback);
239
+ if (handlers.size === 0) {
240
+ this.methodHandlers.delete(method);
241
+ }
242
+ }
243
+ };
244
+ }
245
+
246
+ /**
247
+ * Register a callback to be called when connection is ready
248
+ * @param callback - Called with user profile (or null if not authenticated)
249
+ * @returns Unsubscribe function
250
+ */
251
+ onReady(callback: (user: any) => void) {
252
+ if (this.ready) {
253
+ callback(this.user);
254
+ }
255
+ this.readyCallbacks.add(callback);
256
+ return () => this.readyCallbacks.delete(callback);
257
+ }
258
+
259
+ /**
260
+ * Subscribe to a subscribable document
261
+ * @param method - The subscribe method name (e.g., "subscribe_venue_detail")
262
+ * @param params - Subscription parameters
263
+ * @param callback - Called with initial data and on updates
264
+ * @returns Promise that resolves to an unsubscribe function
265
+ */
266
+ async subscribe(method: string, params: any, callback: (data: any) => void): Promise<() => void> {
267
+ // Call server to get initial snapshot and subscription_id
268
+ const result = await this.call(method, params) as {
269
+ subscription_id: string;
270
+ data: any;
271
+ };
272
+
273
+ // Register callback for subscription events
274
+ this.subscriptionCallbacks.set(result.subscription_id, callback);
275
+
276
+ // Call callback with initial data
277
+ callback(result.data);
278
+
279
+ // Return unsubscribe function
280
+ return () => {
281
+ this.subscriptionCallbacks.delete(result.subscription_id);
282
+ // Notify server
283
+ this.call(`unsubscribe_${method.replace('subscribe_', '')}`, { subscription_id: result.subscription_id }).catch(() => {});
284
+ };
285
+ }
286
+ }
@@ -0,0 +1,39 @@
1
+ import { jwtVerify, SignJWT } from "jose";
2
+
3
+ const JWT_SECRET = process.env.JWT_SECRET || "default_dev_secret";
4
+ const SECRET_KEY = new TextEncoder().encode(JWT_SECRET);
5
+
6
+ export interface UserSession {
7
+ userId: number;
8
+ role?: string;
9
+ [key: string]: any;
10
+ }
11
+
12
+ export async function verifyToken(token: string): Promise<UserSession> {
13
+ try {
14
+ const { payload } = await jwtVerify(token, SECRET_KEY);
15
+
16
+ // Normalize user_id vs sub
17
+ const uid = payload.user_id || payload.sub;
18
+
19
+ if (!uid) {
20
+ throw new Error("Invalid token: missing user_id");
21
+ }
22
+
23
+ return {
24
+ userId: Number(uid),
25
+ role: payload.role as string,
26
+ ...payload
27
+ };
28
+ } catch (err: any) {
29
+ throw new Error(`Authentication failed: ${err.message}`);
30
+ }
31
+ }
32
+
33
+ export async function signToken(payload: any): Promise<string> {
34
+ return await new SignJWT(payload)
35
+ .setProtectedHeader({ alg: 'HS256' })
36
+ .setIssuedAt()
37
+ .setExpirationTime('7d')
38
+ .sign(SECRET_KEY);
39
+ }
@@ -0,0 +1,33 @@
1
+ import postgres from "postgres";
2
+
3
+ export class Database {
4
+ sql: postgres.Sql;
5
+
6
+ constructor(connectionString: string, options: any = {}) {
7
+ this.sql = postgres(connectionString, {
8
+ max: options.max || 10,
9
+ idle_timeout: options.idleTimeout || 20,
10
+ connect_timeout: options.connectTimeout || 10,
11
+ onnotice: () => {}, // Suppress notices
12
+ ...options
13
+ });
14
+ }
15
+
16
+ async query(text: string, params: any[] = []) {
17
+ return this.sql.unsafe(text, params);
18
+ }
19
+
20
+ async listen(channel: string, callback: (payload: string) => void) {
21
+ console.log(`[DB] Setting up LISTEN on channel: ${channel}`);
22
+ const result = await this.sql.listen(channel, (payload) => {
23
+ console.log(`[DB] LISTEN callback triggered with payload:`, payload);
24
+ callback(payload);
25
+ });
26
+ console.log(`[DB] LISTEN setup complete, result:`, result);
27
+ return result;
28
+ }
29
+
30
+ async close() {
31
+ await this.sql.end();
32
+ }
33
+ }
@@ -0,0 +1,51 @@
1
+ export enum ErrorCode {
2
+ PERMISSION_DENIED = "PERMISSION_DENIED",
3
+ NOT_FOUND = "NOT_FOUND",
4
+ VALIDATION_ERROR = "VALIDATION_ERROR",
5
+ CONFLICT = "CONFLICT",
6
+ RATE_LIMITED = "RATE_LIMITED",
7
+ INTERNAL_ERROR = "INTERNAL_ERROR"
8
+ }
9
+
10
+ export class AppError extends Error {
11
+ code: ErrorCode;
12
+ constructor(code: ErrorCode, message?: string) {
13
+ super(message || code);
14
+ this.code = code;
15
+ }
16
+ }
17
+
18
+ export function mapDatabaseError(err: any): AppError {
19
+ // If it's already an AppError (e.g. thrown explicitly from PL/pgSQL RAISE), pass it through
20
+ // Postgres RAISE EXCEPTION '...' usually comes as a generic error with a message
21
+
22
+ const code = err.code; // SQLSTATE
23
+ const message = err.message || "";
24
+
25
+ // 1. Explicit RAISE EXCEPTION from our PL/pgSQL functions
26
+ if (message.includes("permission_denied")) {
27
+ return new AppError(ErrorCode.PERMISSION_DENIED);
28
+ }
29
+ if (message.includes("not_found")) {
30
+ return new AppError(ErrorCode.NOT_FOUND);
31
+ }
32
+ if (message.includes("validation_error")) {
33
+ return new AppError(ErrorCode.VALIDATION_ERROR, message);
34
+ }
35
+
36
+ // 2. Standard SQLSTATE Mapping
37
+ switch (code) {
38
+ case "23505": // Unique violation
39
+ return new AppError(ErrorCode.CONFLICT, "Unique constraint violation");
40
+ case "23503": // Foreign key violation
41
+ return new AppError(ErrorCode.VALIDATION_ERROR, "Invalid reference");
42
+ case "23502": // Not null violation
43
+ return new AppError(ErrorCode.VALIDATION_ERROR, "Missing required field");
44
+ case "42P01": // Undefined table (shouldn't happen in prod if manifest is correct)
45
+ return new AppError(ErrorCode.INTERNAL_ERROR, "Table not found");
46
+ default:
47
+ // Log the real error internally, return generic to client
48
+ // console.error(err); // Done in server.ts
49
+ return new AppError(ErrorCode.INTERNAL_ERROR);
50
+ }
51
+ }
@@ -0,0 +1,98 @@
1
+ import { serve } from "bun";
2
+ import { config } from "dotenv";
3
+ import { Database } from "./db.js";
4
+ import { WebSocketServer } from "./ws.js";
5
+ import { loadManifest } from "./manifest_loader.js";
6
+ import { readFileSync } from "fs";
7
+ import { resolve } from "path";
8
+
9
+ // Re-export JS function registration API for custom functions
10
+ export { registerJsFunction, type JsFunctionHandler, type JsFunctionContext } from "./js_functions.js";
11
+
12
+ // Load .env file if present
13
+ config();
14
+
15
+ // Configuration
16
+ const PORT = process.env.PORT || 3000;
17
+ const DB_URL = process.env.DATABASE_URL || "postgres://dzql_test:dzql_test@localhost:5433/dzql_test";
18
+ const MANIFEST_PATH = process.env.MANIFEST_PATH || "./dist/runtime/manifest.json";
19
+
20
+ // 1. Initialize DB
21
+ const db = new Database(DB_URL);
22
+
23
+ // 2. Load Manifest
24
+ try {
25
+ const manifestPath = resolve(process.cwd(), MANIFEST_PATH);
26
+ const manifestContent = readFileSync(manifestPath, "utf-8");
27
+ const manifest = JSON.parse(manifestContent);
28
+ loadManifest(manifest);
29
+ console.log(`[Runtime] Loaded Manifest v${manifest.version}`);
30
+ } catch (e) {
31
+ console.warn(`[Runtime] Warning: Could not load manifest from ${MANIFEST_PATH}. Ensure you have compiled the project.`);
32
+ }
33
+
34
+ // 3. Initialize WebSocket Server
35
+ const wsServer = new WebSocketServer(db);
36
+
37
+ // 4. Start Commit Listener (Realtime)
38
+ async function startListener() {
39
+ console.log("[Runtime] Setting up LISTEN on dzql_v2 channel...");
40
+ await db.listen("dzql_v2", async (payload) => {
41
+ console.log(`[Runtime] RAW NOTIFY received:`, payload);
42
+ try {
43
+ const { commit_id } = JSON.parse(payload);
44
+ console.log(`[Runtime] Received Commit: ${commit_id}`);
45
+
46
+ // Fetch events
47
+ const events = await db.query(`
48
+ SELECT * FROM dzql_v2.events
49
+ WHERE commit_id = $1
50
+ ORDER BY id ASC
51
+ `, [commit_id]);
52
+
53
+ for (const event of events) {
54
+ // Broadcast
55
+ const message = JSON.stringify({
56
+ jsonrpc: "2.0",
57
+ method: "subscription:event",
58
+ params: {
59
+ event: {
60
+ table: event.table_name,
61
+ op: event.op,
62
+ pk: event.pk,
63
+ data: event.data,
64
+ // old_data is filtered out
65
+ user_id: event.user_id
66
+ }
67
+ }
68
+ });
69
+ wsServer.broadcast(message);
70
+ }
71
+ } catch (e) {
72
+ console.error("[Runtime] Listener Error:", e);
73
+ }
74
+ });
75
+ console.log("[Runtime] Listening for DB Events...");
76
+ }
77
+
78
+ // Wait for listener to be ready before starting server
79
+ await startListener();
80
+
81
+ // 5. Start Web Server
82
+ const server = serve({
83
+ port: PORT,
84
+ async fetch(req, server) {
85
+ const url = new URL(req.url);
86
+
87
+ // Extract token from query params for WebSocket connections
88
+ const token = url.searchParams.get("token");
89
+
90
+ if (server.upgrade(req, { data: { token } })) {
91
+ return;
92
+ }
93
+ return new Response("TZQL Runtime Active", { status: 200 });
94
+ },
95
+ websocket: wsServer.handlers
96
+ });
97
+
98
+ console.log(`[Runtime] Server listening on port ${PORT}`);