dzql 0.6.3 → 0.6.6
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_store.ts +5 -5
- package/src/runtime/ws.ts +16 -15
- package/.env.sample +0 -28
- package/compose.yml +0 -28
- package/dist/client/index.ts +0 -1
- package/dist/client/stores/useMyProfileStore.ts +0 -114
- package/dist/client/stores/useOrgDashboardStore.ts +0 -131
- package/dist/client/stores/useVenueDetailStore.ts +0 -117
- package/dist/client/ws.ts +0 -716
- package/dist/db/migrations/000_core.sql +0 -92
- package/dist/db/migrations/20260101T235039268Z_schema.sql +0 -3020
- package/dist/db/migrations/20260101T235039268Z_subscribables.sql +0 -371
- package/dist/runtime/manifest.json +0 -1562
- 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.6",
|
|
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
|
}
|
|
@@ -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;
|
|
@@ -173,17 +185,6 @@ export class WebSocketServer {
|
|
|
173
185
|
console.log(`[WS] Client ${ws.data.id} disconnected`);
|
|
174
186
|
}
|
|
175
187
|
|
|
176
|
-
private heartbeat() {
|
|
177
|
-
const now = Date.now();
|
|
178
|
-
for (const [id, ws] of this.connections) {
|
|
179
|
-
if (now - ws.data.lastPing > 60000) {
|
|
180
|
-
console.log(`[WS] Client ${id} timed out`);
|
|
181
|
-
ws.close();
|
|
182
|
-
this.connections.delete(id);
|
|
183
|
-
}
|
|
184
|
-
}
|
|
185
|
-
}
|
|
186
|
-
|
|
187
188
|
public broadcast(message: string) {
|
|
188
189
|
// Use Bun's native publish for efficiency
|
|
189
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/dist/client/index.ts
DELETED
|
@@ -1 +0,0 @@
|
|
|
1
|
-
export * from './ws.js';
|
|
@@ -1,114 +0,0 @@
|
|
|
1
|
-
// Generated by DZQL Compiler v2.0.0
|
|
2
|
-
// Do not edit this file directly.
|
|
3
|
-
|
|
4
|
-
import { defineStore } from 'pinia';
|
|
5
|
-
import { ref, type Ref } from 'vue';
|
|
6
|
-
import { ws } from '../index.js';
|
|
7
|
-
|
|
8
|
-
/** Parameters for my_profile subscription */
|
|
9
|
-
export interface MyProfileParams {
|
|
10
|
-
[key: string]: unknown;
|
|
11
|
-
}
|
|
12
|
-
|
|
13
|
-
/** Event from server for patching */
|
|
14
|
-
export interface PatchEvent {
|
|
15
|
-
table: string;
|
|
16
|
-
op: 'insert' | 'update' | 'delete';
|
|
17
|
-
pk: { id: number };
|
|
18
|
-
data: Record<string, unknown> | null;
|
|
19
|
-
}
|
|
20
|
-
|
|
21
|
-
/** Document wrapper with loading state */
|
|
22
|
-
export interface DocumentWrapper<T> {
|
|
23
|
-
data: T;
|
|
24
|
-
loading: boolean;
|
|
25
|
-
ready: Promise<void>;
|
|
26
|
-
}
|
|
27
|
-
|
|
28
|
-
export const useMyProfileStore = defineStore('sub-my_profile', () => {
|
|
29
|
-
const documents: Ref<Record<string, DocumentWrapper<Record<string, unknown>>>> = ref({});
|
|
30
|
-
const unsubscribers = new Map<string, () => void>();
|
|
31
|
-
|
|
32
|
-
async function bind(params: MyProfileParams): Promise<DocumentWrapper<Record<string, unknown>>> {
|
|
33
|
-
const key = JSON.stringify(params);
|
|
34
|
-
|
|
35
|
-
if (documents.value[key]) {
|
|
36
|
-
const existing = documents.value[key];
|
|
37
|
-
if (existing.loading) {
|
|
38
|
-
await existing.ready;
|
|
39
|
-
}
|
|
40
|
-
return existing;
|
|
41
|
-
}
|
|
42
|
-
|
|
43
|
-
let resolveReady!: () => void;
|
|
44
|
-
const ready = new Promise<void>((resolve) => { resolveReady = resolve; });
|
|
45
|
-
|
|
46
|
-
documents.value[key] = { data: {}, loading: true, ready };
|
|
47
|
-
let isFirst = true;
|
|
48
|
-
|
|
49
|
-
const unsubscribe = await ws.api.subscribe_my_profile(params, (eventData: unknown) => {
|
|
50
|
-
if (isFirst) {
|
|
51
|
-
// Initial data - merge into existing object to preserve reactivity
|
|
52
|
-
Object.assign(documents.value[key].data, eventData as Record<string, unknown>);
|
|
53
|
-
documents.value[key].loading = false;
|
|
54
|
-
isFirst = false;
|
|
55
|
-
resolveReady();
|
|
56
|
-
} else {
|
|
57
|
-
// Patch event
|
|
58
|
-
applyPatch(documents.value[key].data, eventData as PatchEvent);
|
|
59
|
-
}
|
|
60
|
-
});
|
|
61
|
-
|
|
62
|
-
unsubscribers.set(key, unsubscribe as () => void);
|
|
63
|
-
await ready;
|
|
64
|
-
return documents.value[key];
|
|
65
|
-
}
|
|
66
|
-
|
|
67
|
-
function unbind(params: MyProfileParams): void {
|
|
68
|
-
const key = JSON.stringify(params);
|
|
69
|
-
const unsubscribe = unsubscribers.get(key);
|
|
70
|
-
if (unsubscribe) {
|
|
71
|
-
unsubscribe();
|
|
72
|
-
unsubscribers.delete(key);
|
|
73
|
-
delete documents.value[key];
|
|
74
|
-
}
|
|
75
|
-
}
|
|
76
|
-
|
|
77
|
-
function applyPatch(doc: Record<string, unknown>, event: PatchEvent): void {
|
|
78
|
-
if (!doc) return;
|
|
79
|
-
switch (event.table) {
|
|
80
|
-
case 'users':
|
|
81
|
-
if (event.op === 'update' && doc.users) {
|
|
82
|
-
Object.assign(doc.users, event.data);
|
|
83
|
-
}
|
|
84
|
-
break;
|
|
85
|
-
case 'acts_for':
|
|
86
|
-
handleArrayPatch(doc.memberships, event);
|
|
87
|
-
break;
|
|
88
|
-
case 'organisations':
|
|
89
|
-
if (event.data && event.data.membership_id) {
|
|
90
|
-
const parent = doc.memberships?.find((p: { id: number }) => p.id === event.data.membership_id);
|
|
91
|
-
if (parent && parent.org) {
|
|
92
|
-
handleArrayPatch(parent.org, event);
|
|
93
|
-
}
|
|
94
|
-
}
|
|
95
|
-
break;
|
|
96
|
-
}
|
|
97
|
-
}
|
|
98
|
-
|
|
99
|
-
function handleArrayPatch(arr: unknown[] | undefined, event: PatchEvent): void {
|
|
100
|
-
if (!arr || !Array.isArray(arr)) return;
|
|
101
|
-
const pkValue = event.pk?.id;
|
|
102
|
-
const idx = arr.findIndex((i: unknown) => (i as { id: number }).id === pkValue);
|
|
103
|
-
|
|
104
|
-
if (event.op === 'insert') {
|
|
105
|
-
if (idx === -1 && event.data) arr.push(event.data);
|
|
106
|
-
} else if (event.op === 'update') {
|
|
107
|
-
if (idx !== -1 && event.data) Object.assign(arr[idx] as object, event.data);
|
|
108
|
-
} else if (event.op === 'delete') {
|
|
109
|
-
if (idx !== -1) arr.splice(idx, 1);
|
|
110
|
-
}
|
|
111
|
-
}
|
|
112
|
-
|
|
113
|
-
return { bind, unbind, documents };
|
|
114
|
-
});
|
|
@@ -1,131 +0,0 @@
|
|
|
1
|
-
// Generated by DZQL Compiler v2.0.0
|
|
2
|
-
// Do not edit this file directly.
|
|
3
|
-
|
|
4
|
-
import { defineStore } from 'pinia';
|
|
5
|
-
import { ref, type Ref } from 'vue';
|
|
6
|
-
import { ws } from '../index.js';
|
|
7
|
-
|
|
8
|
-
/** Parameters for org_dashboard subscription */
|
|
9
|
-
export interface OrgDashboardParams {
|
|
10
|
-
org_id: number;
|
|
11
|
-
}
|
|
12
|
-
|
|
13
|
-
/** Event from server for patching */
|
|
14
|
-
export interface PatchEvent {
|
|
15
|
-
table: string;
|
|
16
|
-
op: 'insert' | 'update' | 'delete';
|
|
17
|
-
pk: { id: number };
|
|
18
|
-
data: Record<string, unknown> | null;
|
|
19
|
-
}
|
|
20
|
-
|
|
21
|
-
/** Document wrapper with loading state */
|
|
22
|
-
export interface DocumentWrapper<T> {
|
|
23
|
-
data: T;
|
|
24
|
-
loading: boolean;
|
|
25
|
-
ready: Promise<void>;
|
|
26
|
-
}
|
|
27
|
-
|
|
28
|
-
export const useOrgDashboardStore = defineStore('sub-org_dashboard', () => {
|
|
29
|
-
const documents: Ref<Record<string, DocumentWrapper<Record<string, unknown>>>> = ref({});
|
|
30
|
-
const unsubscribers = new Map<string, () => void>();
|
|
31
|
-
|
|
32
|
-
async function bind(params: OrgDashboardParams): Promise<DocumentWrapper<Record<string, unknown>>> {
|
|
33
|
-
const key = JSON.stringify(params);
|
|
34
|
-
|
|
35
|
-
if (documents.value[key]) {
|
|
36
|
-
const existing = documents.value[key];
|
|
37
|
-
if (existing.loading) {
|
|
38
|
-
await existing.ready;
|
|
39
|
-
}
|
|
40
|
-
return existing;
|
|
41
|
-
}
|
|
42
|
-
|
|
43
|
-
let resolveReady!: () => void;
|
|
44
|
-
const ready = new Promise<void>((resolve) => { resolveReady = resolve; });
|
|
45
|
-
|
|
46
|
-
documents.value[key] = { data: {}, loading: true, ready };
|
|
47
|
-
let isFirst = true;
|
|
48
|
-
|
|
49
|
-
const unsubscribe = await ws.api.subscribe_org_dashboard(params, (eventData: unknown) => {
|
|
50
|
-
if (isFirst) {
|
|
51
|
-
// Initial data - merge into existing object to preserve reactivity
|
|
52
|
-
Object.assign(documents.value[key].data, eventData as Record<string, unknown>);
|
|
53
|
-
documents.value[key].loading = false;
|
|
54
|
-
isFirst = false;
|
|
55
|
-
resolveReady();
|
|
56
|
-
} else {
|
|
57
|
-
// Patch event
|
|
58
|
-
applyPatch(documents.value[key].data, eventData as PatchEvent);
|
|
59
|
-
}
|
|
60
|
-
});
|
|
61
|
-
|
|
62
|
-
unsubscribers.set(key, unsubscribe as () => void);
|
|
63
|
-
await ready;
|
|
64
|
-
return documents.value[key];
|
|
65
|
-
}
|
|
66
|
-
|
|
67
|
-
function unbind(params: OrgDashboardParams): void {
|
|
68
|
-
const key = JSON.stringify(params);
|
|
69
|
-
const unsubscribe = unsubscribers.get(key);
|
|
70
|
-
if (unsubscribe) {
|
|
71
|
-
unsubscribe();
|
|
72
|
-
unsubscribers.delete(key);
|
|
73
|
-
delete documents.value[key];
|
|
74
|
-
}
|
|
75
|
-
}
|
|
76
|
-
|
|
77
|
-
function applyPatch(doc: Record<string, unknown>, event: PatchEvent): void {
|
|
78
|
-
if (!doc) return;
|
|
79
|
-
switch (event.table) {
|
|
80
|
-
case 'organisations':
|
|
81
|
-
if (event.op === 'update' && doc.organisations) {
|
|
82
|
-
Object.assign(doc.organisations, event.data);
|
|
83
|
-
}
|
|
84
|
-
break;
|
|
85
|
-
case 'venues':
|
|
86
|
-
handleArrayPatch(doc.venues, event);
|
|
87
|
-
break;
|
|
88
|
-
case 'sites':
|
|
89
|
-
if (event.data && event.data.venue_id) {
|
|
90
|
-
const parent = doc.venues?.find((p: { id: number }) => p.id === event.data.venue_id);
|
|
91
|
-
if (parent && parent.sites) {
|
|
92
|
-
handleArrayPatch(parent.sites, event);
|
|
93
|
-
}
|
|
94
|
-
}
|
|
95
|
-
break;
|
|
96
|
-
case 'products':
|
|
97
|
-
handleArrayPatch(doc.products, event);
|
|
98
|
-
break;
|
|
99
|
-
case 'packages':
|
|
100
|
-
handleArrayPatch(doc.packages, event);
|
|
101
|
-
break;
|
|
102
|
-
case 'brands':
|
|
103
|
-
handleArrayPatch(doc.brands, event);
|
|
104
|
-
break;
|
|
105
|
-
case 'artwork':
|
|
106
|
-
if (event.data && event.data.brand_id) {
|
|
107
|
-
const parent = doc.brands?.find((p: { id: number }) => p.id === event.data.brand_id);
|
|
108
|
-
if (parent && parent.artwork) {
|
|
109
|
-
handleArrayPatch(parent.artwork, event);
|
|
110
|
-
}
|
|
111
|
-
}
|
|
112
|
-
break;
|
|
113
|
-
}
|
|
114
|
-
}
|
|
115
|
-
|
|
116
|
-
function handleArrayPatch(arr: unknown[] | undefined, event: PatchEvent): void {
|
|
117
|
-
if (!arr || !Array.isArray(arr)) return;
|
|
118
|
-
const pkValue = event.pk?.id;
|
|
119
|
-
const idx = arr.findIndex((i: unknown) => (i as { id: number }).id === pkValue);
|
|
120
|
-
|
|
121
|
-
if (event.op === 'insert') {
|
|
122
|
-
if (idx === -1 && event.data) arr.push(event.data);
|
|
123
|
-
} else if (event.op === 'update') {
|
|
124
|
-
if (idx !== -1 && event.data) Object.assign(arr[idx] as object, event.data);
|
|
125
|
-
} else if (event.op === 'delete') {
|
|
126
|
-
if (idx !== -1) arr.splice(idx, 1);
|
|
127
|
-
}
|
|
128
|
-
}
|
|
129
|
-
|
|
130
|
-
return { bind, unbind, documents };
|
|
131
|
-
});
|