dzql 0.5.33 → 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,153 @@
1
+ // domain.ts - DZQL Domain Definition
2
+ // Run: bunx dzql domain.ts
3
+
4
+ export const entities = {
5
+
6
+ // Users table with authentication
7
+ users: {
8
+ schema: {
9
+ id: 'serial PRIMARY KEY',
10
+ name: 'text NOT NULL',
11
+ email: 'text UNIQUE NOT NULL',
12
+ password_hash: 'text NOT NULL',
13
+ created_at: 'timestamptz DEFAULT now()'
14
+ },
15
+ label: 'name',
16
+ searchable: ['name', 'email'],
17
+ hidden: ['password_hash'],
18
+ permissions: {
19
+ view: [],
20
+ create: [],
21
+ update: ['@id'], // Users can only update themselves
22
+ delete: []
23
+ }
24
+ },
25
+
26
+ // Posts table with author relationship
27
+ posts: {
28
+ schema: {
29
+ id: 'serial PRIMARY KEY',
30
+ author_id: 'int NOT NULL REFERENCES users(id)',
31
+ title: 'text NOT NULL',
32
+ content: 'text',
33
+ published: 'boolean DEFAULT false',
34
+ created_at: 'timestamptz DEFAULT now()',
35
+ updated_at: 'timestamptz'
36
+ },
37
+ label: 'title',
38
+ searchable: ['title', 'content'],
39
+ includes: {
40
+ author: 'users'
41
+ },
42
+ fieldDefaults: {
43
+ author_id: '@user_id',
44
+ created_at: '@now'
45
+ },
46
+ permissions: {
47
+ view: [], // Anyone can view
48
+ create: [], // Anyone logged in can create
49
+ update: ['@author_id'], // Only author can update
50
+ delete: ['@author_id'] // Only author can delete
51
+ },
52
+ graphRules: {
53
+ on_create: {
54
+ notify_followers: {
55
+ description: 'Notify when new post is created',
56
+ actions: [
57
+ {
58
+ type: 'reactor',
59
+ name: 'new_post',
60
+ params: { post_id: '@id', author_id: '@author_id' }
61
+ }
62
+ ]
63
+ }
64
+ },
65
+ on_update: {
66
+ track_updates: {
67
+ description: 'Set updated_at on edit',
68
+ condition: "@before.title != @after.title OR @before.content != @after.content",
69
+ actions: [
70
+ {
71
+ type: 'update',
72
+ target: 'posts',
73
+ data: { updated_at: '@now' },
74
+ match: { id: '@id' }
75
+ }
76
+ ]
77
+ }
78
+ }
79
+ }
80
+ },
81
+
82
+ // Comments on posts
83
+ comments: {
84
+ schema: {
85
+ id: 'serial PRIMARY KEY',
86
+ post_id: 'int NOT NULL REFERENCES posts(id) ON DELETE CASCADE',
87
+ author_id: 'int NOT NULL REFERENCES users(id)',
88
+ content: 'text NOT NULL',
89
+ created_at: 'timestamptz DEFAULT now()'
90
+ },
91
+ label: 'content',
92
+ searchable: ['content'],
93
+ includes: {
94
+ post: 'posts',
95
+ author: 'users'
96
+ },
97
+ fieldDefaults: {
98
+ author_id: '@user_id',
99
+ created_at: '@now'
100
+ },
101
+ permissions: {
102
+ view: [],
103
+ create: [],
104
+ update: ['@author_id'],
105
+ delete: ['@author_id', '@post_id->posts.author_id'] // Author or post owner can delete
106
+ }
107
+ }
108
+
109
+ };
110
+
111
+ // Real-time subscriptions
112
+ export const subscribables = {
113
+
114
+ // Post detail with comments
115
+ post_detail: {
116
+ params: {
117
+ post_id: 'int'
118
+ },
119
+ root: {
120
+ entity: 'posts',
121
+ key: 'post_id'
122
+ },
123
+ includes: {
124
+ author: 'users',
125
+ comments: {
126
+ entity: 'comments',
127
+ includes: {
128
+ author: 'users'
129
+ }
130
+ }
131
+ },
132
+ scopeTables: ['posts', 'users', 'comments'],
133
+ canSubscribe: [] // Anyone can subscribe
134
+ },
135
+
136
+ // User's posts feed
137
+ my_posts: {
138
+ params: {},
139
+ root: {
140
+ entity: 'users',
141
+ key: '@user_id'
142
+ },
143
+ includes: {
144
+ posts: {
145
+ entity: 'posts',
146
+ filter: { author_id: '@user_id' }
147
+ }
148
+ },
149
+ scopeTables: ['users', 'posts'],
150
+ canSubscribe: []
151
+ }
152
+
153
+ };
@@ -0,0 +1,24 @@
1
+ {
2
+ "name": "{{name}}",
3
+ "version": "0.0.1",
4
+ "type": "module",
5
+ "scripts": {
6
+ "dev": "bun run --watch server.ts",
7
+ "compile": "bunx dzql domain.ts",
8
+ "db:up": "docker compose up -d",
9
+ "db:down": "docker compose down",
10
+ "db:migrate": "bun run compile && psql $DATABASE_URL -f dist/db/migrations/*.sql"
11
+ },
12
+ "dependencies": {
13
+ "dzql": "^0.6.0",
14
+ "postgres": "^3.4.3"
15
+ },
16
+ "devDependencies": {
17
+ "@types/bun": "latest",
18
+ "typescript": "^5.0.0"
19
+ },
20
+ "bun-create": {
21
+ "postinstall": ["./setup.sh", "rm setup.sh"],
22
+ "start": "bun run dev"
23
+ }
24
+ }
@@ -0,0 +1,18 @@
1
+ // server.ts - DZQL Runtime Server
2
+ import { createServer } from "dzql";
3
+
4
+ const server = createServer({
5
+ manifestPath: "./dist/runtime/manifest.json",
6
+ jwtSecret: process.env.JWT_SECRET || "dev-secret-change-in-production",
7
+ });
8
+
9
+ const port = process.env.PORT || 3000;
10
+
11
+ console.log(`DZQL Server running at http://localhost:${port}`);
12
+ console.log(`WebSocket endpoint: ws://localhost:${port}/ws`);
13
+
14
+ export default {
15
+ port,
16
+ fetch: server.fetch,
17
+ websocket: server.websocket,
18
+ };
@@ -0,0 +1,11 @@
1
+ #!/bin/bash
2
+ # Replace {{name}} with project name from package.json
3
+ NAME=$(grep -o '"name": *"[^"]*"' package.json | head -1 | cut -d'"' -f4)
4
+ if [ -z "$NAME" ] || [ "$NAME" = "{{name}}" ]; then
5
+ NAME=$(basename "$PWD")
6
+ fi
7
+ sed -i.bak "s/{{name}}/$NAME/g" compose.yml .env.example README.md package.json 2>/dev/null || \
8
+ sed -i "s/{{name}}/$NAME/g" compose.yml .env.example README.md package.json
9
+ cp .env.example .env
10
+ rm -f compose.yml.bak .env.example.bak README.md.bak package.json.bak
11
+ echo "✓ Configured project: $NAME"
@@ -0,0 +1,15 @@
1
+ {
2
+ "compilerOptions": {
3
+ "target": "ESNext",
4
+ "module": "ESNext",
5
+ "moduleResolution": "bundler",
6
+ "strict": true,
7
+ "esModuleInterop": true,
8
+ "skipLibCheck": true,
9
+ "outDir": "./dist",
10
+ "rootDir": ".",
11
+ "types": ["bun-types"]
12
+ },
13
+ "include": ["*.ts", "domain.ts"],
14
+ "exclude": ["node_modules", "dist"]
15
+ }
@@ -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}`);
@@ -0,0 +1,63 @@
1
+ // JavaScript Custom Function Registry
2
+ // Allows registering JS/Bun functions that can be called via RPC
3
+
4
+ export interface JsFunctionContext {
5
+ userId: number;
6
+ params: any;
7
+ db: {
8
+ query(sql: string, params?: any[]): Promise<any[]>;
9
+ };
10
+ }
11
+
12
+ export type JsFunctionHandler = (ctx: JsFunctionContext) => Promise<any>;
13
+
14
+ // Registry of JS function handlers
15
+ const jsHandlers: Map<string, JsFunctionHandler> = new Map();
16
+
17
+ /**
18
+ * Register a JavaScript function that can be called via RPC.
19
+ * JS functions take precedence over SQL functions with the same name.
20
+ *
21
+ * @param name - The function name (used in RPC calls)
22
+ * @param handler - Async function that receives context and returns result
23
+ *
24
+ * @example
25
+ * registerJsFunction('calculate_stats', async (ctx) => {
26
+ * const { userId, params, db } = ctx;
27
+ * const rows = await db.query('SELECT COUNT(*) as cnt FROM venues WHERE org_id = $1', [params.org_id]);
28
+ * return { venue_count: rows[0].cnt };
29
+ * });
30
+ */
31
+ export function registerJsFunction(name: string, handler: JsFunctionHandler): void {
32
+ jsHandlers.set(name, handler);
33
+ console.log(`[Runtime] Registered JS function: ${name}`);
34
+ }
35
+
36
+ /**
37
+ * Get a registered JS function handler by name.
38
+ * @returns The handler function or undefined if not registered
39
+ */
40
+ export function getJsFunction(name: string): JsFunctionHandler | undefined {
41
+ return jsHandlers.get(name);
42
+ }
43
+
44
+ /**
45
+ * Check if a JS function is registered.
46
+ */
47
+ export function hasJsFunction(name: string): boolean {
48
+ return jsHandlers.has(name);
49
+ }
50
+
51
+ /**
52
+ * Clear all registered JS functions (useful for testing).
53
+ */
54
+ export function clearJsFunctions(): void {
55
+ jsHandlers.clear();
56
+ }
57
+
58
+ /**
59
+ * Get all registered JS function names.
60
+ */
61
+ export function getJsFunctionNames(): string[] {
62
+ return Array.from(jsHandlers.keys());
63
+ }
@@ -0,0 +1,29 @@
1
+ import { Manifest } from "../cli/codegen/manifest.js";
2
+
3
+ // Global cache for the loaded manifest
4
+ let activeManifest: Manifest | null = null;
5
+
6
+ export function loadManifest(manifest: Manifest) {
7
+ console.log(`[Runtime] Loading manifest v${manifest.version}`);
8
+ activeManifest = manifest;
9
+ }
10
+
11
+ export function getManifest(): Manifest {
12
+ if (!activeManifest) {
13
+ throw new Error("[Runtime] Manifest not loaded.");
14
+ }
15
+ return activeManifest;
16
+ }
17
+
18
+ export function resolveFunction(name: string) {
19
+ const manifest = getManifest();
20
+ const fn = manifest.functions[name];
21
+
22
+ if (!fn) {
23
+ return null;
24
+ }
25
+
26
+ // In a real DB-connected runtime, we would resolve OID here.
27
+ // For now, we return the schema-qualified name.
28
+ return `${fn.schema}.${fn.name}`;
29
+ }