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.
- package/.env.sample +28 -0
- package/compose.yml +28 -0
- package/dist/client/index.ts +1 -0
- package/dist/client/stores/useMyProfileStore.ts +114 -0
- package/dist/client/stores/useOrgDashboardStore.ts +131 -0
- package/dist/client/stores/useVenueDetailStore.ts +117 -0
- package/dist/client/ws.ts +716 -0
- package/dist/db/migrations/000_core.sql +92 -0
- package/dist/db/migrations/20251229T212912022Z_schema.sql +3020 -0
- package/dist/db/migrations/20251229T212912022Z_subscribables.sql +371 -0
- package/dist/runtime/manifest.json +1562 -0
- package/docs/README.md +293 -36
- package/docs/feature-requests/applyPatch-bug-report.md +85 -0
- package/docs/feature-requests/connection-ready-profile.md +57 -0
- package/docs/feature-requests/hidden-bug-report.md +111 -0
- package/docs/feature-requests/hidden-fields-subscribables.md +34 -0
- package/docs/feature-requests/subscribable-param-key-bug.md +38 -0
- package/docs/feature-requests/todo.md +146 -0
- package/docs/for_ai.md +641 -0
- package/docs/project-setup.md +432 -0
- package/examples/blog.ts +50 -0
- package/examples/invalid.ts +18 -0
- package/examples/venues.js +485 -0
- package/package.json +23 -60
- package/src/cli/codegen/client.ts +99 -0
- package/src/cli/codegen/manifest.ts +95 -0
- package/src/cli/codegen/pinia.ts +174 -0
- package/src/cli/codegen/realtime.ts +58 -0
- package/src/cli/codegen/sql.ts +698 -0
- package/src/cli/codegen/subscribable_sql.ts +547 -0
- package/src/cli/codegen/subscribable_store.ts +184 -0
- package/src/cli/codegen/types.ts +142 -0
- package/src/cli/compiler/analyzer.ts +52 -0
- package/src/cli/compiler/graph_rules.ts +251 -0
- package/src/cli/compiler/ir.ts +233 -0
- package/src/cli/compiler/loader.ts +132 -0
- package/src/cli/compiler/permissions.ts +227 -0
- package/src/cli/index.ts +164 -0
- package/src/client/index.ts +1 -0
- package/src/client/ws.ts +286 -0
- package/src/create/.env.example +8 -0
- package/src/create/README.md +101 -0
- package/src/create/compose.yml +14 -0
- package/src/create/domain.ts +153 -0
- package/src/create/package.json +24 -0
- package/src/create/server.ts +18 -0
- package/src/create/setup.sh +11 -0
- package/src/create/tsconfig.json +15 -0
- package/src/runtime/auth.ts +39 -0
- package/src/runtime/db.ts +33 -0
- package/src/runtime/errors.ts +51 -0
- package/src/runtime/index.ts +98 -0
- package/src/runtime/js_functions.ts +63 -0
- package/src/runtime/manifest_loader.ts +29 -0
- package/src/runtime/namespace.ts +483 -0
- package/src/runtime/server.ts +87 -0
- package/src/runtime/ws.ts +197 -0
- package/src/shared/ir.ts +197 -0
- package/tests/client.test.ts +38 -0
- package/tests/codegen.test.ts +71 -0
- package/tests/compiler.test.ts +45 -0
- package/tests/graph_rules.test.ts +173 -0
- package/tests/integration/db.test.ts +174 -0
- package/tests/integration/e2e.test.ts +65 -0
- package/tests/integration/features.test.ts +922 -0
- package/tests/integration/full_stack.test.ts +262 -0
- package/tests/integration/setup.ts +45 -0
- package/tests/ir.test.ts +32 -0
- package/tests/namespace.test.ts +395 -0
- package/tests/permissions.test.ts +55 -0
- package/tests/pinia.test.ts +48 -0
- package/tests/realtime.test.ts +22 -0
- package/tests/runtime.test.ts +80 -0
- package/tests/subscribable_gen.test.ts +72 -0
- package/tests/subscribable_reactivity.test.ts +258 -0
- package/tests/venues_gen.test.ts +25 -0
- package/tsconfig.json +20 -0
- package/tsconfig.tsbuildinfo +1 -0
- package/README.md +0 -90
- package/bin/cli.js +0 -727
- package/docs/compiler/ADVANCED_FILTERS.md +0 -183
- package/docs/compiler/CODING_STANDARDS.md +0 -415
- package/docs/compiler/COMPARISON.md +0 -673
- package/docs/compiler/QUICKSTART.md +0 -326
- package/docs/compiler/README.md +0 -134
- package/docs/examples/README.md +0 -38
- package/docs/examples/blog.sql +0 -160
- package/docs/examples/venue-detail-simple.sql +0 -8
- package/docs/examples/venue-detail-subscribable.sql +0 -45
- package/docs/for-ai/claude-guide.md +0 -1210
- package/docs/getting-started/quickstart.md +0 -125
- package/docs/getting-started/subscriptions-quick-start.md +0 -203
- package/docs/getting-started/tutorial.md +0 -1104
- package/docs/guides/atomic-updates.md +0 -299
- package/docs/guides/client-stores.md +0 -730
- package/docs/guides/composite-primary-keys.md +0 -158
- package/docs/guides/custom-functions.md +0 -362
- package/docs/guides/drop-semantics.md +0 -554
- package/docs/guides/field-defaults.md +0 -240
- package/docs/guides/interpreter-vs-compiler.md +0 -237
- package/docs/guides/many-to-many.md +0 -929
- package/docs/guides/subscriptions.md +0 -537
- package/docs/reference/api.md +0 -1373
- package/docs/reference/client.md +0 -224
- package/src/client/stores/index.js +0 -8
- package/src/client/stores/useAppStore.js +0 -285
- package/src/client/stores/useWsStore.js +0 -289
- package/src/client/ws.js +0 -762
- package/src/compiler/cli/compile-example.js +0 -33
- package/src/compiler/cli/compile-subscribable.js +0 -43
- package/src/compiler/cli/debug-compile.js +0 -44
- package/src/compiler/cli/debug-parse.js +0 -26
- package/src/compiler/cli/debug-path-parser.js +0 -18
- package/src/compiler/cli/debug-subscribable-parser.js +0 -21
- package/src/compiler/cli/index.js +0 -174
- package/src/compiler/codegen/auth-codegen.js +0 -153
- package/src/compiler/codegen/drop-semantics-codegen.js +0 -553
- package/src/compiler/codegen/graph-rules-codegen.js +0 -450
- package/src/compiler/codegen/notification-codegen.js +0 -232
- package/src/compiler/codegen/operation-codegen.js +0 -1382
- package/src/compiler/codegen/permission-codegen.js +0 -318
- package/src/compiler/codegen/subscribable-codegen.js +0 -827
- package/src/compiler/compiler.js +0 -371
- package/src/compiler/index.js +0 -11
- package/src/compiler/parser/entity-parser.js +0 -440
- package/src/compiler/parser/path-parser.js +0 -290
- package/src/compiler/parser/subscribable-parser.js +0 -244
- package/src/database/dzql-core.sql +0 -161
- package/src/database/migrations/001_schema.sql +0 -60
- package/src/database/migrations/002_functions.sql +0 -890
- package/src/database/migrations/003_operations.sql +0 -1135
- package/src/database/migrations/004_search.sql +0 -581
- package/src/database/migrations/005_entities.sql +0 -730
- package/src/database/migrations/006_auth.sql +0 -94
- package/src/database/migrations/007_events.sql +0 -133
- package/src/database/migrations/008_hello.sql +0 -18
- package/src/database/migrations/008a_meta.sql +0 -172
- package/src/database/migrations/009_subscriptions.sql +0 -240
- package/src/database/migrations/010_atomic_updates.sql +0 -157
- package/src/database/migrations/010_fix_m2m_events.sql +0 -94
- package/src/index.js +0 -40
- package/src/server/api.js +0 -9
- package/src/server/db.js +0 -442
- package/src/server/index.js +0 -317
- package/src/server/logger.js +0 -259
- package/src/server/mcp.js +0 -594
- package/src/server/meta-route.js +0 -251
- package/src/server/namespace.js +0 -292
- package/src/server/subscriptions.js +0 -351
- package/src/server/ws.js +0 -573
package/src/cli/index.ts
ADDED
|
@@ -0,0 +1,164 @@
|
|
|
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
|
+
const command = args[0];
|
|
15
|
+
const input = args[1];
|
|
16
|
+
let outputDir = "dist"; // Default output directory
|
|
17
|
+
|
|
18
|
+
// Parse optional output directory flag
|
|
19
|
+
const outputFlagIndex = args.indexOf('-o');
|
|
20
|
+
const longOutputFlagIndex = args.indexOf('--output');
|
|
21
|
+
|
|
22
|
+
if (outputFlagIndex > -1 && args[outputFlagIndex + 1]) {
|
|
23
|
+
outputDir = args[outputFlagIndex + 1];
|
|
24
|
+
} else if (longOutputFlagIndex > -1 && args[longOutputFlagIndex + 1]) {
|
|
25
|
+
outputDir = args[longOutputFlagIndex + 1];
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
async function main() {
|
|
29
|
+
console.log("TZQL Compiler v0.0.1");
|
|
30
|
+
|
|
31
|
+
if (command === "compile") {
|
|
32
|
+
if (!input) {
|
|
33
|
+
console.error("Usage: tzql compile <file>");
|
|
34
|
+
process.exit(1);
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
try {
|
|
38
|
+
// Phase 1: Load & Analyze
|
|
39
|
+
const fullInputPath = resolve(process.cwd(), input);
|
|
40
|
+
|
|
41
|
+
// Clean Output Directory
|
|
42
|
+
const absOutputDir = resolve(process.cwd(), outputDir);
|
|
43
|
+
console.log(`[Compiler] Cleaning ${absOutputDir}...`);
|
|
44
|
+
try {
|
|
45
|
+
rmSync(absOutputDir, { recursive: true, force: true });
|
|
46
|
+
} catch (e) { /* ignore */ }
|
|
47
|
+
|
|
48
|
+
const domain = await loadDomain(fullInputPath);
|
|
49
|
+
console.log("[Compiler] Domain loaded.");
|
|
50
|
+
|
|
51
|
+
const errors = analyzeDomain(domain);
|
|
52
|
+
if (errors.length > 0) {
|
|
53
|
+
console.error("[Compiler] Validation Failed:");
|
|
54
|
+
errors.forEach(err => console.error(` - ${err}`));
|
|
55
|
+
process.exit(1);
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
// Phase 2: Generate IR
|
|
59
|
+
const ir = generateIR(domain);
|
|
60
|
+
console.log(`[Compiler] IR Generated. Subscribables: ${Object.keys(ir.subscribables).join(', ')}`);
|
|
61
|
+
|
|
62
|
+
// Phase 3: Generate SQL
|
|
63
|
+
const coreSQL = generateCoreSQL();
|
|
64
|
+
const entitySQL: string[] = [];
|
|
65
|
+
for (const [name, entityIR] of Object.entries(ir.entities)) {
|
|
66
|
+
entitySQL.push(generateSchemaSQL(name, entityIR));
|
|
67
|
+
// Skip CRUD generation for unmanaged entities (e.g., junction tables)
|
|
68
|
+
if (entityIR.managed !== false) {
|
|
69
|
+
entitySQL.push(generateEntitySQL(name, entityIR));
|
|
70
|
+
} else {
|
|
71
|
+
console.log(`[Compiler] Skipping CRUD for unmanaged entity: ${name}`);
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
// Generate subscribable SQL functions
|
|
76
|
+
const subscribableSQL: string[] = [];
|
|
77
|
+
for (const [name, subIR] of Object.entries(ir.subscribables)) {
|
|
78
|
+
console.log(`[Compiler] Generating SQL for subscribable: ${name}`);
|
|
79
|
+
subscribableSQL.push(generateSubscribableSQL(name, subIR as any, ir.entities));
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
// Collect custom functions SQL
|
|
83
|
+
const customFunctionSQL: string[] = [];
|
|
84
|
+
for (const fn of ir.customFunctions) {
|
|
85
|
+
console.log(`[Compiler] Adding custom function: ${fn.name}`);
|
|
86
|
+
customFunctionSQL.push(fn.sql);
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
// Phase 4: Generate Manifest
|
|
90
|
+
const manifest = generateManifest(ir);
|
|
91
|
+
console.log(`[Compiler] Manifest functions: ${Object.keys(manifest.functions).length}`);
|
|
92
|
+
if (!manifest.functions['subscribe_venue_detail']) {
|
|
93
|
+
console.error("[Compiler] ERROR: subscribe_venue_detail missing from manifest functions!");
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
// --- OUTPUT GENERATION ---
|
|
97
|
+
|
|
98
|
+
// 1. Database
|
|
99
|
+
const dbDir = resolve(outputDir, "db/migrations");
|
|
100
|
+
mkdirSync(dbDir, { recursive: true });
|
|
101
|
+
|
|
102
|
+
writeFileSync(resolve(dbDir, `000_core.sql`), coreSQL);
|
|
103
|
+
const timestamp = new Date().toISOString().replace(/[:.-]/g, '');
|
|
104
|
+
|
|
105
|
+
// Combine entity SQL with custom functions
|
|
106
|
+
const schemaContent = customFunctionSQL.length > 0
|
|
107
|
+
? entitySQL.join('\n') + '\n\n-- Custom Functions\n' + customFunctionSQL.join('\n\n')
|
|
108
|
+
: entitySQL.join('\n');
|
|
109
|
+
writeFileSync(resolve(dbDir, `${timestamp}_schema.sql`), schemaContent);
|
|
110
|
+
|
|
111
|
+
if (customFunctionSQL.length > 0) {
|
|
112
|
+
console.log(`[Generated] ${customFunctionSQL.length} Custom Functions`);
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
// Write subscribable SQL
|
|
116
|
+
if (subscribableSQL.length > 0) {
|
|
117
|
+
writeFileSync(resolve(dbDir, `${timestamp}_subscribables.sql`), subscribableSQL.join('\n\n'));
|
|
118
|
+
console.log(`[Generated] ${subscribableSQL.length} Subscribable SQL functions`);
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
console.log(`[Generated] DB Migrations in ${dbDir}`);
|
|
122
|
+
|
|
123
|
+
// 2. Runtime
|
|
124
|
+
const runtimeDir = resolve(outputDir, "runtime");
|
|
125
|
+
mkdirSync(runtimeDir, { recursive: true });
|
|
126
|
+
writeFileSync(resolve(runtimeDir, `manifest.json`), JSON.stringify(manifest, null, 2));
|
|
127
|
+
console.log(`[Generated] Runtime Manifest in ${runtimeDir}`);
|
|
128
|
+
|
|
129
|
+
// 3. Client SDK (TypeScript)
|
|
130
|
+
const clientDir = resolve(outputDir, "client");
|
|
131
|
+
mkdirSync(clientDir, { recursive: true });
|
|
132
|
+
|
|
133
|
+
// Generate Core SDK as TypeScript
|
|
134
|
+
const clientCode = generateClientSDK(manifest);
|
|
135
|
+
writeFileSync(resolve(clientDir, `ws.ts`), clientCode);
|
|
136
|
+
|
|
137
|
+
// Generate Index
|
|
138
|
+
writeFileSync(resolve(clientDir, `index.ts`), `export * from './ws.js';`);
|
|
139
|
+
|
|
140
|
+
console.log(`[Generated] Client SDK in ${clientDir}`);
|
|
141
|
+
|
|
142
|
+
// 4. Stores (TypeScript)
|
|
143
|
+
const storeDir = resolve(clientDir, "stores");
|
|
144
|
+
mkdirSync(storeDir, { recursive: true });
|
|
145
|
+
|
|
146
|
+
for (const subName of Object.keys(ir.subscribables)) {
|
|
147
|
+
const storeCode = generateSubscribableStore(manifest, subName);
|
|
148
|
+
const fileName = `use${subName.replace(/(^|_)([a-z])/g, (g) => g.at(-1)!.toUpperCase())}Store.ts`;
|
|
149
|
+
writeFileSync(resolve(storeDir, fileName), storeCode);
|
|
150
|
+
}
|
|
151
|
+
console.log(`[Generated] ${Object.keys(ir.subscribables).length} Pinia Stores in ${storeDir}`);
|
|
152
|
+
|
|
153
|
+
console.log("[Compiler] Build Complete.");
|
|
154
|
+
|
|
155
|
+
} catch (e) {
|
|
156
|
+
console.error(e);
|
|
157
|
+
process.exit(1);
|
|
158
|
+
}
|
|
159
|
+
} else {
|
|
160
|
+
console.log("Unknown command. Try 'compile'.");
|
|
161
|
+
}
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
main();
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export * from './ws.js';
|
package/src/client/ws.ts
ADDED
|
@@ -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,101 @@
|
|
|
1
|
+
# {{name}}
|
|
2
|
+
|
|
3
|
+
A real-time database application powered by [DZQL](https://github.com/blueshed/dzql).
|
|
4
|
+
|
|
5
|
+
## Quick Start
|
|
6
|
+
|
|
7
|
+
```bash
|
|
8
|
+
# Start PostgreSQL
|
|
9
|
+
bun run db:up
|
|
10
|
+
|
|
11
|
+
# Copy environment file
|
|
12
|
+
cp .env.example .env
|
|
13
|
+
|
|
14
|
+
# Compile domain and generate SQL
|
|
15
|
+
bun run compile
|
|
16
|
+
|
|
17
|
+
# Run migrations
|
|
18
|
+
bun run db:migrate
|
|
19
|
+
|
|
20
|
+
# Start development server
|
|
21
|
+
bun run dev
|
|
22
|
+
```
|
|
23
|
+
|
|
24
|
+
## Project Structure
|
|
25
|
+
|
|
26
|
+
```
|
|
27
|
+
├── domain.ts # Entity definitions and subscriptions
|
|
28
|
+
├── server.ts # Runtime server configuration
|
|
29
|
+
├── compose.yml # Docker Compose for PostgreSQL
|
|
30
|
+
├── dist/ # Generated output (after compile)
|
|
31
|
+
│ ├── db/ # SQL migrations
|
|
32
|
+
│ ├── runtime/ # Server manifest
|
|
33
|
+
│ └── client/ # TypeScript SDK & Pinia stores
|
|
34
|
+
└── .env # Environment variables
|
|
35
|
+
```
|
|
36
|
+
|
|
37
|
+
## Domain Definition
|
|
38
|
+
|
|
39
|
+
Edit `domain.ts` to define your entities:
|
|
40
|
+
|
|
41
|
+
```typescript
|
|
42
|
+
export const entities = {
|
|
43
|
+
posts: {
|
|
44
|
+
schema: {
|
|
45
|
+
id: 'serial PRIMARY KEY',
|
|
46
|
+
title: 'text NOT NULL',
|
|
47
|
+
content: 'text'
|
|
48
|
+
},
|
|
49
|
+
permissions: {
|
|
50
|
+
view: [],
|
|
51
|
+
create: [],
|
|
52
|
+
update: ['@author_id'],
|
|
53
|
+
delete: ['@author_id']
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
};
|
|
57
|
+
```
|
|
58
|
+
|
|
59
|
+
## API Endpoints
|
|
60
|
+
|
|
61
|
+
After starting the server:
|
|
62
|
+
|
|
63
|
+
- **WebSocket**: `ws://localhost:3000/ws`
|
|
64
|
+
- **Health**: `GET http://localhost:3000/health`
|
|
65
|
+
|
|
66
|
+
## Client Usage
|
|
67
|
+
|
|
68
|
+
```typescript
|
|
69
|
+
import { TzqlClient } from './dist/client';
|
|
70
|
+
|
|
71
|
+
const client = new TzqlClient('ws://localhost:3000/ws');
|
|
72
|
+
|
|
73
|
+
// Register
|
|
74
|
+
await client.api.register_user({
|
|
75
|
+
email: 'user@example.com',
|
|
76
|
+
password: 'secret'
|
|
77
|
+
});
|
|
78
|
+
|
|
79
|
+
// Login
|
|
80
|
+
const { token } = await client.api.login_user({
|
|
81
|
+
email: 'user@example.com',
|
|
82
|
+
password: 'secret'
|
|
83
|
+
});
|
|
84
|
+
|
|
85
|
+
// CRUD operations
|
|
86
|
+
const post = await client.api.save_posts({
|
|
87
|
+
title: 'Hello World'
|
|
88
|
+
});
|
|
89
|
+
|
|
90
|
+
// Real-time subscription
|
|
91
|
+
await client.api.subscribe_post_detail(
|
|
92
|
+
{ post_id: post.id },
|
|
93
|
+
(data) => console.log('Updated:', data)
|
|
94
|
+
);
|
|
95
|
+
```
|
|
96
|
+
|
|
97
|
+
## Learn More
|
|
98
|
+
|
|
99
|
+
- [DZQL Documentation](https://github.com/blueshed/dzql)
|
|
100
|
+
- [Graph Rules](https://github.com/blueshed/dzql/docs/guides/graph-rules.md)
|
|
101
|
+
- [Subscriptions](https://github.com/blueshed/dzql/docs/guides/subscriptions.md)
|