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.
Files changed (41) hide show
  1. package/README.md +33 -0
  2. package/docs/for_ai.md +14 -18
  3. package/docs/project-setup.md +15 -14
  4. package/package.json +28 -6
  5. package/src/cli/codegen/client.ts +5 -6
  6. package/src/cli/codegen/subscribable_store.ts +5 -5
  7. package/src/runtime/ws.ts +16 -15
  8. package/.env.sample +0 -28
  9. package/compose.yml +0 -28
  10. package/dist/client/index.ts +0 -1
  11. package/dist/client/stores/useMyProfileStore.ts +0 -114
  12. package/dist/client/stores/useOrgDashboardStore.ts +0 -131
  13. package/dist/client/stores/useVenueDetailStore.ts +0 -117
  14. package/dist/client/ws.ts +0 -716
  15. package/dist/db/migrations/000_core.sql +0 -92
  16. package/dist/db/migrations/20260101T235039268Z_schema.sql +0 -3020
  17. package/dist/db/migrations/20260101T235039268Z_subscribables.sql +0 -371
  18. package/dist/runtime/manifest.json +0 -1562
  19. package/examples/blog.ts +0 -50
  20. package/examples/invalid.ts +0 -18
  21. package/examples/venues.js +0 -485
  22. package/tests/client.test.ts +0 -38
  23. package/tests/codegen.test.ts +0 -71
  24. package/tests/compiler.test.ts +0 -45
  25. package/tests/graph_rules.test.ts +0 -173
  26. package/tests/integration/db.test.ts +0 -174
  27. package/tests/integration/e2e.test.ts +0 -65
  28. package/tests/integration/features.test.ts +0 -922
  29. package/tests/integration/full_stack.test.ts +0 -262
  30. package/tests/integration/setup.ts +0 -45
  31. package/tests/ir.test.ts +0 -32
  32. package/tests/namespace.test.ts +0 -395
  33. package/tests/permissions.test.ts +0 -55
  34. package/tests/pinia.test.ts +0 -48
  35. package/tests/realtime.test.ts +0 -22
  36. package/tests/runtime.test.ts +0 -80
  37. package/tests/subscribable_gen.test.ts +0 -72
  38. package/tests/subscribable_reactivity.test.ts +0 -258
  39. package/tests/venues_gen.test.ts +0 -25
  40. package/tsconfig.json +0 -20
  41. 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 { WebSocketManager } from 'dzql/client';
538
+ import { ws } from '@generated/client/ws';
539
539
 
540
- const ws = new WebSocketManager();
541
- await ws.connect('ws://localhost:3000/ws');
540
+ await ws.connect('/ws');
542
541
 
543
- // Connection state properties
544
- ws.ready // boolean - true after connection:ready received
545
- ws.user // user profile object or null if anonymous
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
- // Register callback for ready state (called immediately if already ready)
548
- const unsubscribe = ws.onReady((user) => {
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
- // Authentication methods
557
- await ws.login({ email: '...', password: '...' }); // Stores token in localStorage
558
- await ws.register({ email: '...', password: '...' }); // Stores token in localStorage
559
- await ws.logout(); // Clears token, user state, and reconnects
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
@@ -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.onReady((profile: any) => {
247
- if (!profile && localStorage.getItem('token')) {
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.login({ email, password })
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.register({ name, email, password })
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
- await ws.logout()
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",
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
- returnType = 'void';
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: { url?: string; reconnect?: boolean } = {}) {
82
+ constructor(options: { maxReconnectAttempts?: number; tokenName?: string } = {}) {
84
83
  super(options);
85
84
  this.api = {
86
85
  ${regularFunctions.map(([funcName]) =>
87
- ` ${funcName}: (params: Record<string, unknown>) => this.call('${funcName}', params),`
86
+ ` ${funcName}: (params: any) => this.call('${funcName}', params),`
88
87
  ).join('\n')}
89
88
  ${subscriptionFunctions.map(([funcName]) =>
90
- ` ${funcName}: (params: Record<string, unknown>, callback: (data: unknown) => void) => this.subscribe('${funcName}', params, callback),`
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: { id: number }) => p.id === event.data.${fkField});\n` +
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: unknown[] | undefined, event: PatchEvent): void {
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: unknown) => (i as { id: number }).id === pkValue);
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] as object, event.data);
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 handler hooks
25
- get handlers() {
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:
@@ -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
- });