dzql 0.6.2 → 0.6.5
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +33 -0
- package/docs/for_ai.md +14 -18
- package/docs/project-setup.md +15 -14
- package/package.json +28 -6
- package/src/cli/codegen/client.ts +5 -6
- package/src/cli/codegen/subscribable_sql.ts +64 -3
- package/src/cli/codegen/subscribable_store.ts +5 -5
- package/src/runtime/ws.ts +18 -15
- package/.env.sample +0 -28
- package/compose.yml +0 -28
- package/examples/blog.ts +0 -50
- package/examples/invalid.ts +0 -18
- package/examples/venues.js +0 -485
- package/tests/client.test.ts +0 -38
- package/tests/codegen.test.ts +0 -71
- package/tests/compiler.test.ts +0 -45
- package/tests/graph_rules.test.ts +0 -173
- package/tests/integration/db.test.ts +0 -174
- package/tests/integration/e2e.test.ts +0 -65
- package/tests/integration/features.test.ts +0 -922
- package/tests/integration/full_stack.test.ts +0 -262
- package/tests/integration/setup.ts +0 -45
- package/tests/ir.test.ts +0 -32
- package/tests/namespace.test.ts +0 -395
- package/tests/permissions.test.ts +0 -55
- package/tests/pinia.test.ts +0 -48
- package/tests/realtime.test.ts +0 -22
- package/tests/runtime.test.ts +0 -80
- package/tests/subscribable_gen.test.ts +0 -72
- package/tests/subscribable_reactivity.test.ts +0 -258
- package/tests/venues_gen.test.ts +0 -25
- package/tsconfig.json +0 -20
- package/tsconfig.tsbuildinfo +0 -1
package/README.md
ADDED
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
# DZQL
|
|
2
|
+
|
|
3
|
+
Database-first real-time framework with TypeScript support.
|
|
4
|
+
|
|
5
|
+
## Quick Start
|
|
6
|
+
|
|
7
|
+
```bash
|
|
8
|
+
bun create dzql my-app
|
|
9
|
+
cd my-app
|
|
10
|
+
bun install
|
|
11
|
+
bun run db:rebuild
|
|
12
|
+
bun run dev
|
|
13
|
+
```
|
|
14
|
+
|
|
15
|
+
## Documentation
|
|
16
|
+
|
|
17
|
+
See the [full documentation](./docs/README.md) for:
|
|
18
|
+
|
|
19
|
+
- [Project Setup Guide](./docs/project-setup.md)
|
|
20
|
+
- [AI Assistant Guide](./docs/for_ai.md)
|
|
21
|
+
|
|
22
|
+
## Package Exports
|
|
23
|
+
|
|
24
|
+
```typescript
|
|
25
|
+
import { createServer } from 'dzql'; // Runtime server
|
|
26
|
+
import { ws } from 'dzql/client'; // WebSocket client
|
|
27
|
+
import { compile } from 'dzql/compiler'; // CLI compiler
|
|
28
|
+
import { DzqlNamespace } from 'dzql/namespace'; // Direct DB access
|
|
29
|
+
```
|
|
30
|
+
|
|
31
|
+
## License
|
|
32
|
+
|
|
33
|
+
MIT
|
package/docs/for_ai.md
CHANGED
|
@@ -535,28 +535,24 @@ When a client connects to the WebSocket server, it immediately receives a `conne
|
|
|
535
535
|
### WebSocketManager API
|
|
536
536
|
|
|
537
537
|
```typescript
|
|
538
|
-
import {
|
|
538
|
+
import { ws } from '@generated/client/ws';
|
|
539
539
|
|
|
540
|
-
|
|
541
|
-
await ws.connect('ws://localhost:3000/ws');
|
|
540
|
+
await ws.connect('/ws');
|
|
542
541
|
|
|
543
|
-
//
|
|
544
|
-
ws.
|
|
545
|
-
|
|
542
|
+
// Authentication via typed API
|
|
543
|
+
const user = await ws.api.login_user({ email: '...', password: '...' });
|
|
544
|
+
// Token is returned in response - store in localStorage
|
|
545
|
+
if (user.token) {
|
|
546
|
+
localStorage.setItem('dzql_token', user.token);
|
|
547
|
+
}
|
|
546
548
|
|
|
547
|
-
|
|
548
|
-
|
|
549
|
-
if (user) {
|
|
550
|
-
console.log('Authenticated as:', user.email);
|
|
551
|
-
} else {
|
|
552
|
-
console.log('Anonymous connection');
|
|
553
|
-
}
|
|
554
|
-
});
|
|
549
|
+
const newUser = await ws.api.register_user({ name: '...', email: '...', password: '...' });
|
|
550
|
+
// Token is returned in response
|
|
555
551
|
|
|
556
|
-
//
|
|
557
|
-
|
|
558
|
-
|
|
559
|
-
await ws.
|
|
552
|
+
// Logout - clear token and disconnect
|
|
553
|
+
localStorage.removeItem('dzql_token');
|
|
554
|
+
ws.disconnect();
|
|
555
|
+
await ws.connect('/ws'); // Reconnect without token
|
|
560
556
|
```
|
|
561
557
|
|
|
562
558
|
### Vue/Pinia Usage Pattern
|
package/docs/project-setup.md
CHANGED
|
@@ -231,7 +231,7 @@ export default defineConfig({
|
|
|
231
231
|
|
|
232
232
|
```typescript
|
|
233
233
|
import { ref } from 'vue'
|
|
234
|
-
import { ws } from '@generated/client'
|
|
234
|
+
import { ws } from '@generated/client/ws'
|
|
235
235
|
|
|
236
236
|
const ready = ref(false)
|
|
237
237
|
const user = ref<any>(null)
|
|
@@ -242,16 +242,9 @@ export function useDzql() {
|
|
|
242
242
|
try {
|
|
243
243
|
connectionError.value = null
|
|
244
244
|
ready.value = false
|
|
245
|
-
|
|
246
|
-
ws.
|
|
247
|
-
|
|
248
|
-
localStorage.removeItem('token')
|
|
249
|
-
}
|
|
250
|
-
user.value = profile
|
|
251
|
-
ready.value = true
|
|
252
|
-
})
|
|
253
|
-
|
|
254
|
-
await ws.connect(url)
|
|
245
|
+
|
|
246
|
+
await ws.connect(url || '/ws')
|
|
247
|
+
ready.value = true
|
|
255
248
|
} catch (e: any) {
|
|
256
249
|
connectionError.value = e.message
|
|
257
250
|
throw e
|
|
@@ -259,24 +252,32 @@ export function useDzql() {
|
|
|
259
252
|
}
|
|
260
253
|
|
|
261
254
|
async function login(email: string, password: string) {
|
|
262
|
-
const result = await ws.
|
|
255
|
+
const result = await ws.api.login_user({ email, password }) as any
|
|
263
256
|
if (result?.user_id) {
|
|
264
257
|
user.value = result
|
|
258
|
+
if (result.token) {
|
|
259
|
+
localStorage.setItem('dzql_token', result.token)
|
|
260
|
+
}
|
|
265
261
|
}
|
|
266
262
|
return result
|
|
267
263
|
}
|
|
268
264
|
|
|
269
265
|
async function register(name: string, email: string, password: string) {
|
|
270
|
-
const result = await ws.
|
|
266
|
+
const result = await ws.api.register_user({ name, email, password }) as any
|
|
271
267
|
if (result?.user_id) {
|
|
272
268
|
user.value = result
|
|
269
|
+
if (result.token) {
|
|
270
|
+
localStorage.setItem('dzql_token', result.token)
|
|
271
|
+
}
|
|
273
272
|
}
|
|
274
273
|
return result
|
|
275
274
|
}
|
|
276
275
|
|
|
277
276
|
async function logout() {
|
|
278
|
-
|
|
277
|
+
localStorage.removeItem('dzql_token')
|
|
279
278
|
user.value = null
|
|
279
|
+
ws.disconnect()
|
|
280
|
+
await connect()
|
|
280
281
|
}
|
|
281
282
|
|
|
282
283
|
return { ws, ready, user, connectionError, connect, login, register, logout }
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "dzql",
|
|
3
|
-
"version": "0.6.
|
|
3
|
+
"version": "0.6.5",
|
|
4
4
|
"description": "Database-first real-time framework with TypeScript support",
|
|
5
5
|
"repository": {
|
|
6
6
|
"type": "git",
|
|
@@ -10,17 +10,36 @@
|
|
|
10
10
|
"bin": {
|
|
11
11
|
"dzql": "./src/cli/index.ts"
|
|
12
12
|
},
|
|
13
|
-
"scripts": {
|
|
14
|
-
"dev": "bun run src/cli/index.ts",
|
|
15
|
-
"test": "bun test",
|
|
16
|
-
"build": "bun build ./src/cli/index.ts --outdir ./dist/cli --target node"
|
|
17
|
-
},
|
|
18
13
|
"exports": {
|
|
19
14
|
".": "./src/runtime/index.ts",
|
|
20
15
|
"./client": "./src/client/index.ts",
|
|
21
16
|
"./compiler": "./src/cli/index.ts",
|
|
22
17
|
"./namespace": "./src/runtime/namespace.ts"
|
|
23
18
|
},
|
|
19
|
+
"files": [
|
|
20
|
+
"src",
|
|
21
|
+
"docs",
|
|
22
|
+
"README.md"
|
|
23
|
+
],
|
|
24
|
+
"scripts": {
|
|
25
|
+
"test": "bun test"
|
|
26
|
+
},
|
|
27
|
+
"keywords": [
|
|
28
|
+
"postgresql",
|
|
29
|
+
"postgres",
|
|
30
|
+
"websocket",
|
|
31
|
+
"real-time",
|
|
32
|
+
"realtime",
|
|
33
|
+
"database",
|
|
34
|
+
"bun",
|
|
35
|
+
"vue",
|
|
36
|
+
"pinia",
|
|
37
|
+
"typescript",
|
|
38
|
+
"compiler",
|
|
39
|
+
"codegen"
|
|
40
|
+
],
|
|
41
|
+
"author": "Peter Bunyan",
|
|
42
|
+
"license": "MIT",
|
|
24
43
|
"dependencies": {
|
|
25
44
|
"postgres": "^3.4.3",
|
|
26
45
|
"dotenv": "^16.4.5",
|
|
@@ -34,5 +53,8 @@
|
|
|
34
53
|
},
|
|
35
54
|
"peerDependencies": {
|
|
36
55
|
"typescript": "^5.0.0"
|
|
56
|
+
},
|
|
57
|
+
"engines": {
|
|
58
|
+
"bun": ">=1.0.0"
|
|
37
59
|
}
|
|
38
60
|
}
|
|
@@ -35,8 +35,7 @@ export function generateClientSDK(manifest: Manifest): string {
|
|
|
35
35
|
if (isSubscription) {
|
|
36
36
|
// subscribe_venue_detail -> VenueDetailParams
|
|
37
37
|
paramType = `${pascalEntity}Params`;
|
|
38
|
-
|
|
39
|
-
return ` ${funcName}: (params: ${paramType}, callback: (data: unknown) => void) => Promise<() => ${returnType}>;`;
|
|
38
|
+
return ` ${funcName}: (params: ${paramType}, callback: (data: unknown) => void) => Promise<{ data: unknown; subscription_id: string; schema: unknown; unsubscribe: () => Promise<void> }>;`;
|
|
40
39
|
} else if (op === 'get' && entityExists) {
|
|
41
40
|
// get_venue_detail (subscribable getter) vs get_venues (entity getter)
|
|
42
41
|
if (manifest.subscribables?.[entity]) {
|
|
@@ -78,16 +77,16 @@ ${apiMethods}
|
|
|
78
77
|
|
|
79
78
|
/** Extended WebSocket manager with typed API */
|
|
80
79
|
export class GeneratedWebSocketManager extends WebSocketManager {
|
|
81
|
-
api: DzqlAPI;
|
|
80
|
+
declare api: DzqlAPI;
|
|
82
81
|
|
|
83
|
-
constructor(options: {
|
|
82
|
+
constructor(options: { maxReconnectAttempts?: number; tokenName?: string } = {}) {
|
|
84
83
|
super(options);
|
|
85
84
|
this.api = {
|
|
86
85
|
${regularFunctions.map(([funcName]) =>
|
|
87
|
-
` ${funcName}: (params:
|
|
86
|
+
` ${funcName}: (params: any) => this.call('${funcName}', params),`
|
|
88
87
|
).join('\n')}
|
|
89
88
|
${subscriptionFunctions.map(([funcName]) =>
|
|
90
|
-
` ${funcName}: (params:
|
|
89
|
+
` ${funcName}: (params: any, callback: (data: any) => void) => this.subscribe('${funcName}', params, callback),`
|
|
91
90
|
).join('\n')}
|
|
92
91
|
} as DzqlAPI;
|
|
93
92
|
}
|
|
@@ -223,7 +223,7 @@ function generateGetFunction(name: string, sub: SubscribableIR, entities: Record
|
|
|
223
223
|
// Handle special @user_id root key
|
|
224
224
|
const rootKey = sub.root.key;
|
|
225
225
|
const isUserIdRoot = rootKey === '@user_id';
|
|
226
|
-
const
|
|
226
|
+
const isList = !rootKey; // No key means it's a list subscribable
|
|
227
227
|
|
|
228
228
|
// Build root select expression excluding hidden fields
|
|
229
229
|
const rootSelectExpr = buildVisibleRowJson('root', sub.root.entity, entities);
|
|
@@ -239,7 +239,67 @@ function generateGetFunction(name: string, sub: SubscribableIR, entities: Record
|
|
|
239
239
|
scopeTables: sub.scopeTables
|
|
240
240
|
}).replace(/'/g, "''"); // Escape single quotes for SQL
|
|
241
241
|
|
|
242
|
-
|
|
242
|
+
// Build WHERE clause based on root filter and key
|
|
243
|
+
const whereConditions: string[] = [];
|
|
244
|
+
if (rootKey) {
|
|
245
|
+
const rootWhereValue = isUserIdRoot ? 'p_user_id' : `v_${rootKey}`;
|
|
246
|
+
whereConditions.push(`root.id = ${rootWhereValue}`);
|
|
247
|
+
}
|
|
248
|
+
if (sub.root.filter) {
|
|
249
|
+
for (const [field, value] of Object.entries(sub.root.filter)) {
|
|
250
|
+
if (typeof value === 'boolean') {
|
|
251
|
+
whereConditions.push(`root.${field} = ${value}`);
|
|
252
|
+
} else if (typeof value === 'string') {
|
|
253
|
+
whereConditions.push(`root.${field} = '${value}'`);
|
|
254
|
+
} else {
|
|
255
|
+
whereConditions.push(`root.${field} = ${value}`);
|
|
256
|
+
}
|
|
257
|
+
}
|
|
258
|
+
}
|
|
259
|
+
const whereClause = whereConditions.length > 0
|
|
260
|
+
? `WHERE ${whereConditions.join(' AND ')}`
|
|
261
|
+
: '';
|
|
262
|
+
|
|
263
|
+
if (isList) {
|
|
264
|
+
// List subscribable - return array of records with nested includes
|
|
265
|
+
return `CREATE OR REPLACE FUNCTION dzql_v2.get_${name}(
|
|
266
|
+
p_params JSONB,
|
|
267
|
+
p_user_id INT
|
|
268
|
+
) RETURNS JSONB
|
|
269
|
+
LANGUAGE plpgsql
|
|
270
|
+
SECURITY DEFINER
|
|
271
|
+
SET search_path = dzql_v2, public
|
|
272
|
+
AS $$
|
|
273
|
+
DECLARE
|
|
274
|
+
${paramDecls}
|
|
275
|
+
v_data JSONB;
|
|
276
|
+
BEGIN
|
|
277
|
+
-- Extract parameters
|
|
278
|
+
${paramExtracts}
|
|
279
|
+
|
|
280
|
+
-- Check access control
|
|
281
|
+
IF NOT dzql_v2.${name}_can_subscribe(p_user_id, p_params) THEN
|
|
282
|
+
RAISE EXCEPTION 'permission_denied';
|
|
283
|
+
END IF;
|
|
284
|
+
|
|
285
|
+
-- Build list of documents with nested relations
|
|
286
|
+
SELECT COALESCE(jsonb_agg(
|
|
287
|
+
to_jsonb(root.*) || jsonb_build_object(${relationSelects ? relationSelects.slice(1) : ''})
|
|
288
|
+
), '[]'::jsonb)
|
|
289
|
+
INTO v_data
|
|
290
|
+
FROM ${sub.root.entity} root
|
|
291
|
+
${whereClause};
|
|
292
|
+
|
|
293
|
+
-- Return data with embedded schema for atomic updates
|
|
294
|
+
RETURN jsonb_build_object(
|
|
295
|
+
'${sub.root.entity}', v_data,
|
|
296
|
+
'schema', '${schemaJson}'::jsonb
|
|
297
|
+
);
|
|
298
|
+
END;
|
|
299
|
+
$$;`;
|
|
300
|
+
} else {
|
|
301
|
+
// Single record subscribable
|
|
302
|
+
return `CREATE OR REPLACE FUNCTION dzql_v2.get_${name}(
|
|
243
303
|
p_params JSONB,
|
|
244
304
|
p_user_id INT
|
|
245
305
|
) RETURNS JSONB
|
|
@@ -265,7 +325,7 @@ ${paramExtracts}
|
|
|
265
325
|
)
|
|
266
326
|
INTO v_data
|
|
267
327
|
FROM ${sub.root.entity} root
|
|
268
|
-
|
|
328
|
+
${whereClause};
|
|
269
329
|
|
|
270
330
|
-- Return data with embedded schema for atomic updates
|
|
271
331
|
RETURN jsonb_build_object(
|
|
@@ -274,6 +334,7 @@ ${paramExtracts}
|
|
|
274
334
|
);
|
|
275
335
|
END;
|
|
276
336
|
$$;`;
|
|
337
|
+
}
|
|
277
338
|
}
|
|
278
339
|
|
|
279
340
|
function singularize(name: string): string {
|
|
@@ -59,8 +59,8 @@ export function generateSubscribableStore(manifest: Manifest, subName: string):
|
|
|
59
59
|
|
|
60
60
|
cases.push(
|
|
61
61
|
` case '${entityName}':\n` +
|
|
62
|
-
` if (event.data && event.data.${fkField}) {\n` +
|
|
63
|
-
` const parent = doc.${level1Rel}?.find((p:
|
|
62
|
+
` if (event.data && (event.data as any).${fkField}) {\n` +
|
|
63
|
+
` const parent = (doc.${level1Rel} as any[])?.find((p: any) => p.id === (event.data as any).${fkField});\n` +
|
|
64
64
|
` if (parent && parent.${key}) {\n` +
|
|
65
65
|
` handleArrayPatch(parent.${key}, event);\n` +
|
|
66
66
|
` }\n` +
|
|
@@ -164,15 +164,15 @@ ${patchCases}
|
|
|
164
164
|
}
|
|
165
165
|
}
|
|
166
166
|
|
|
167
|
-
function handleArrayPatch(arr:
|
|
167
|
+
function handleArrayPatch(arr: any, event: PatchEvent): void {
|
|
168
168
|
if (!arr || !Array.isArray(arr)) return;
|
|
169
169
|
const pkValue = event.pk?.id;
|
|
170
|
-
const idx = arr.findIndex((i:
|
|
170
|
+
const idx = arr.findIndex((i: any) => i?.id === pkValue);
|
|
171
171
|
|
|
172
172
|
if (event.op === 'insert') {
|
|
173
173
|
if (idx === -1 && event.data) arr.push(event.data);
|
|
174
174
|
} else if (event.op === 'update') {
|
|
175
|
-
if (idx !== -1 && event.data) Object.assign(arr[idx]
|
|
175
|
+
if (idx !== -1 && event.data) Object.assign(arr[idx], event.data);
|
|
176
176
|
} else if (event.op === 'delete') {
|
|
177
177
|
if (idx !== -1) arr.splice(idx, 1);
|
|
178
178
|
}
|
package/src/runtime/ws.ts
CHANGED
|
@@ -3,6 +3,9 @@ import { handleRequest } from "./server.js"; // The secure router
|
|
|
3
3
|
import { verifyToken, signToken } from "./auth.js";
|
|
4
4
|
import { Database } from "./db.js";
|
|
5
5
|
|
|
6
|
+
// WebSocket configuration
|
|
7
|
+
const WS_MAX_MESSAGE_SIZE = parseInt(process.env.WS_MAX_MESSAGE_SIZE || "1048576", 10); // 1MB default
|
|
8
|
+
|
|
6
9
|
interface WSContext {
|
|
7
10
|
id: string;
|
|
8
11
|
userId?: number;
|
|
@@ -17,13 +20,17 @@ export class WebSocketServer {
|
|
|
17
20
|
|
|
18
21
|
constructor(db: Database) {
|
|
19
22
|
this.db = db;
|
|
20
|
-
// Start heartbeat interval
|
|
21
|
-
setInterval(() => this.heartbeat(), 30000);
|
|
22
23
|
}
|
|
23
24
|
|
|
24
|
-
// Bun.serve websocket
|
|
25
|
-
get
|
|
25
|
+
// Bun.serve websocket configuration object
|
|
26
|
+
get websocket() {
|
|
26
27
|
return {
|
|
28
|
+
// WebSocket options for Bun
|
|
29
|
+
perMessageDeflate: true,
|
|
30
|
+
maxPayloadLength: WS_MAX_MESSAGE_SIZE,
|
|
31
|
+
idleTimeout: 0, // No idle timeout - realtime connections stay open indefinitely
|
|
32
|
+
|
|
33
|
+
// Handler hooks
|
|
27
34
|
open: (ws: ServerWebSocket<WSContext>) => this.handleOpen(ws),
|
|
28
35
|
message: (ws: ServerWebSocket<WSContext>, message: string) => this.handleMessage(ws, message),
|
|
29
36
|
close: (ws: ServerWebSocket<WSContext>) => this.handleClose(ws),
|
|
@@ -31,6 +38,11 @@ export class WebSocketServer {
|
|
|
31
38
|
};
|
|
32
39
|
}
|
|
33
40
|
|
|
41
|
+
// Legacy alias for backwards compatibility
|
|
42
|
+
get handlers() {
|
|
43
|
+
return this.websocket;
|
|
44
|
+
}
|
|
45
|
+
|
|
34
46
|
private async handleOpen(ws: ServerWebSocket<WSContext>) {
|
|
35
47
|
const id = Math.random().toString(36).slice(2);
|
|
36
48
|
const token = ws.data?.token;
|
|
@@ -148,6 +160,8 @@ export class WebSocketServer {
|
|
|
148
160
|
// Auto-generate token for auth methods
|
|
149
161
|
if (req.method === 'login_user' || req.method === 'register_user') {
|
|
150
162
|
const token = await signToken({ user_id: result.user_id, role: 'user' });
|
|
163
|
+
// Update connection's userId for subsequent calls
|
|
164
|
+
ws.data.userId = result.user_id;
|
|
151
165
|
// Return profile + token
|
|
152
166
|
ws.send(JSON.stringify({ id: req.id, result: { ...result, token } }));
|
|
153
167
|
} else {
|
|
@@ -171,17 +185,6 @@ export class WebSocketServer {
|
|
|
171
185
|
console.log(`[WS] Client ${ws.data.id} disconnected`);
|
|
172
186
|
}
|
|
173
187
|
|
|
174
|
-
private heartbeat() {
|
|
175
|
-
const now = Date.now();
|
|
176
|
-
for (const [id, ws] of this.connections) {
|
|
177
|
-
if (now - ws.data.lastPing > 60000) {
|
|
178
|
-
console.log(`[WS] Client ${id} timed out`);
|
|
179
|
-
ws.close();
|
|
180
|
-
this.connections.delete(id);
|
|
181
|
-
}
|
|
182
|
-
}
|
|
183
|
-
}
|
|
184
|
-
|
|
185
188
|
public broadcast(message: string) {
|
|
186
189
|
// Use Bun's native publish for efficiency
|
|
187
190
|
// 'broadcast' topic is subscribed by all on connect
|
package/.env.sample
DELETED
|
@@ -1,28 +0,0 @@
|
|
|
1
|
-
# TZQL Environment Variables
|
|
2
|
-
# Copy this file to .env and modify as needed
|
|
3
|
-
|
|
4
|
-
# ===================
|
|
5
|
-
# Server (Runtime)
|
|
6
|
-
# ===================
|
|
7
|
-
|
|
8
|
-
# PostgreSQL connection string
|
|
9
|
-
DATABASE_URL=postgres://user:password@localhost:5432/database
|
|
10
|
-
|
|
11
|
-
# Server port
|
|
12
|
-
PORT=3000
|
|
13
|
-
|
|
14
|
-
# Path to compiled manifest (relative to working directory)
|
|
15
|
-
MANIFEST_PATH=./dist/runtime/manifest.json
|
|
16
|
-
|
|
17
|
-
# JWT secret for authentication (CHANGE IN PRODUCTION!)
|
|
18
|
-
JWT_SECRET=your-secure-secret-key-here
|
|
19
|
-
|
|
20
|
-
# ===================
|
|
21
|
-
# Client (Browser)
|
|
22
|
-
# ===================
|
|
23
|
-
# These are injected at build time by your bundler (Vite, Webpack, etc.)
|
|
24
|
-
|
|
25
|
-
# Token name for localStorage (Vite: prefix with VITE_)
|
|
26
|
-
# VITE_TZQL_TOKEN_NAME=myapp_token
|
|
27
|
-
# Or for other bundlers:
|
|
28
|
-
# TZQL_TOKEN_NAME=myapp_token
|
package/compose.yml
DELETED
|
@@ -1,28 +0,0 @@
|
|
|
1
|
-
services:
|
|
2
|
-
postgres:
|
|
3
|
-
image: postgres:16-alpine
|
|
4
|
-
container_name: tzql-test-db
|
|
5
|
-
environment:
|
|
6
|
-
POSTGRES_USER: dzql_test
|
|
7
|
-
POSTGRES_PASSWORD: dzql_test
|
|
8
|
-
POSTGRES_DB: dzql_test
|
|
9
|
-
ports:
|
|
10
|
-
- "5433:5432" # Map container's 5432 to host's 5433
|
|
11
|
-
volumes:
|
|
12
|
-
- tzql-test-data:/var/lib/postgresql/data
|
|
13
|
-
healthcheck:
|
|
14
|
-
test: ["CMD-SHELL", "pg_isready -U dzql_test -d dzql_test"]
|
|
15
|
-
interval: 5s
|
|
16
|
-
timeout: 5s
|
|
17
|
-
retries: 5
|
|
18
|
-
command:
|
|
19
|
-
- "postgres"
|
|
20
|
-
- "-c"
|
|
21
|
-
- "log_statement=all"
|
|
22
|
-
- "-c"
|
|
23
|
-
- "log_destination=stderr"
|
|
24
|
-
- "-c"
|
|
25
|
-
- "logging_collector=off"
|
|
26
|
-
|
|
27
|
-
volumes:
|
|
28
|
-
tzql-test-data:
|
package/examples/blog.ts
DELETED
|
@@ -1,50 +0,0 @@
|
|
|
1
|
-
// TZQL Entity Definition Example
|
|
2
|
-
|
|
3
|
-
export const entities = {
|
|
4
|
-
posts: {
|
|
5
|
-
schema: {
|
|
6
|
-
id: 'serial PRIMARY KEY',
|
|
7
|
-
title: 'text NOT NULL',
|
|
8
|
-
content: 'text',
|
|
9
|
-
author_id: 'int NOT NULL', // In a real app, this would reference users(id)
|
|
10
|
-
created_at: 'timestamptz DEFAULT now()'
|
|
11
|
-
},
|
|
12
|
-
permissions: {
|
|
13
|
-
view: [], // Public
|
|
14
|
-
create: ['@author_id == @user_id'], // Only create for self
|
|
15
|
-
update: ['@author_id == @user_id'], // Only owner
|
|
16
|
-
delete: ['@author_id == @user_id'] // Only owner
|
|
17
|
-
},
|
|
18
|
-
graphRules: {
|
|
19
|
-
on_create: {
|
|
20
|
-
actions: [
|
|
21
|
-
{ type: 'reactor', name: 'notify_subscribers', params: { post_id: '@id' } }
|
|
22
|
-
]
|
|
23
|
-
}
|
|
24
|
-
}
|
|
25
|
-
},
|
|
26
|
-
comments: {
|
|
27
|
-
schema: {
|
|
28
|
-
id: 'serial PRIMARY KEY',
|
|
29
|
-
post_id: 'int NOT NULL REFERENCES posts(id) ON DELETE CASCADE',
|
|
30
|
-
content: 'text NOT NULL',
|
|
31
|
-
author_id: 'int NOT NULL'
|
|
32
|
-
},
|
|
33
|
-
permissions: {
|
|
34
|
-
view: [],
|
|
35
|
-
create: [],
|
|
36
|
-
delete: ['@author_id == @user_id']
|
|
37
|
-
}
|
|
38
|
-
}
|
|
39
|
-
};
|
|
40
|
-
|
|
41
|
-
export const subscribables = {
|
|
42
|
-
post_detail: {
|
|
43
|
-
params: { post_id: 'int' },
|
|
44
|
-
root: { entity: 'posts', key: 'post_id' },
|
|
45
|
-
includes: {
|
|
46
|
-
comments: { entity: 'comments', filter: { post_id: '@id' } }
|
|
47
|
-
},
|
|
48
|
-
scopeTables: ['posts', 'comments']
|
|
49
|
-
}
|
|
50
|
-
};
|
package/examples/invalid.ts
DELETED
|
@@ -1,18 +0,0 @@
|
|
|
1
|
-
// TZQL Entity Definition Example (INVALID)
|
|
2
|
-
|
|
3
|
-
export const entities = {
|
|
4
|
-
posts: {
|
|
5
|
-
schema: { id: 'serial PRIMARY KEY' },
|
|
6
|
-
permissions: {}
|
|
7
|
-
}
|
|
8
|
-
};
|
|
9
|
-
|
|
10
|
-
export const subscribables = {
|
|
11
|
-
broken_feed: {
|
|
12
|
-
params: {},
|
|
13
|
-
root: { entity: 'posts' },
|
|
14
|
-
includes: {
|
|
15
|
-
comments: { entity: 'missing_table' } // <--- Error: 'missing_table' does not exist
|
|
16
|
-
}
|
|
17
|
-
}
|
|
18
|
-
};
|