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.
- 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
|
@@ -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
|
+
}
|