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
package/.env.sample
ADDED
|
@@ -0,0 +1,28 @@
|
|
|
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
ADDED
|
@@ -0,0 +1,28 @@
|
|
|
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:
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export * from './ws.js';
|
|
@@ -0,0 +1,114 @@
|
|
|
1
|
+
// Generated by TZQL 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
|
+
});
|
|
@@ -0,0 +1,131 @@
|
|
|
1
|
+
// Generated by TZQL 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
|
+
});
|
|
@@ -0,0 +1,117 @@
|
|
|
1
|
+
// Generated by TZQL 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 venue_detail subscription */
|
|
9
|
+
export interface VenueDetailParams {
|
|
10
|
+
venue_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 useVenueDetailStore = defineStore('sub-venue_detail', () => {
|
|
29
|
+
const documents: Ref<Record<string, DocumentWrapper<Record<string, unknown>>>> = ref({});
|
|
30
|
+
const unsubscribers = new Map<string, () => void>();
|
|
31
|
+
|
|
32
|
+
async function bind(params: VenueDetailParams): 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_venue_detail(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: VenueDetailParams): 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 'venues':
|
|
81
|
+
if (event.op === 'update' && doc.venues) {
|
|
82
|
+
Object.assign(doc.venues, event.data);
|
|
83
|
+
}
|
|
84
|
+
break;
|
|
85
|
+
case 'organisations':
|
|
86
|
+
handleArrayPatch(doc.org, event);
|
|
87
|
+
break;
|
|
88
|
+
case 'sites':
|
|
89
|
+
handleArrayPatch(doc.sites, event);
|
|
90
|
+
break;
|
|
91
|
+
case 'allocations':
|
|
92
|
+
if (event.data && event.data.site_id) {
|
|
93
|
+
const parent = doc.sites?.find((p: { id: number }) => p.id === event.data.site_id);
|
|
94
|
+
if (parent && parent.allocations) {
|
|
95
|
+
handleArrayPatch(parent.allocations, event);
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
break;
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
function handleArrayPatch(arr: unknown[] | undefined, event: PatchEvent): void {
|
|
103
|
+
if (!arr || !Array.isArray(arr)) return;
|
|
104
|
+
const pkValue = event.pk?.id;
|
|
105
|
+
const idx = arr.findIndex((i: unknown) => (i as { id: number }).id === pkValue);
|
|
106
|
+
|
|
107
|
+
if (event.op === 'insert') {
|
|
108
|
+
if (idx === -1 && event.data) arr.push(event.data);
|
|
109
|
+
} else if (event.op === 'update') {
|
|
110
|
+
if (idx !== -1 && event.data) Object.assign(arr[idx] as object, event.data);
|
|
111
|
+
} else if (event.op === 'delete') {
|
|
112
|
+
if (idx !== -1) arr.splice(idx, 1);
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
return { bind, unbind, documents };
|
|
117
|
+
});
|