dzql 0.1.5 → 0.2.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/README.md +8 -1
- package/docs/CLIENT-QUICK-START.md +183 -0
- package/docs/CLIENT-STORES.md +730 -0
- package/docs/REFERENCE.md +139 -0
- package/package.json +5 -2
- package/src/client/stores/README.md +95 -0
- package/src/client/stores/index.js +8 -0
- package/src/client/stores/useAppStore.js +285 -0
- package/src/client/stores/useWsStore.js +289 -0
- package/src/client/ws.js +87 -2
- package/src/compiler/codegen/operation-codegen.js +28 -3
- package/src/compiler/codegen/subscribable-codegen.js +396 -0
- package/src/compiler/compiler.js +115 -0
- package/src/compiler/parser/subscribable-parser.js +242 -0
- package/src/database/migrations/009_subscriptions.sql +230 -0
- package/src/index.js +35 -14
- package/src/server/index.js +90 -1
- package/src/server/subscriptions.js +209 -0
- package/src/server/ws.js +78 -2
- package/src/client/ui-configs/sample-2.js +0 -207
- package/src/client/ui-loader.js +0 -618
- package/src/client/ui.js +0 -990
- package/src/client.js +0 -9
package/docs/REFERENCE.md
CHANGED
|
@@ -12,6 +12,7 @@ Complete API documentation for DZQL framework. For tutorials, see [GETTING_START
|
|
|
12
12
|
- [Custom Functions](#custom-functions)
|
|
13
13
|
- [Authentication](#authentication)
|
|
14
14
|
- [Real-time Events](#real-time-events)
|
|
15
|
+
- [Live Query Subscriptions](#live-query-subscriptions)
|
|
15
16
|
- [Temporal Relationships](#temporal-relationships)
|
|
16
17
|
- [Error Messages](#error-messages)
|
|
17
18
|
|
|
@@ -864,6 +865,144 @@ ws.onBroadcast((method, params) => {
|
|
|
864
865
|
|
|
865
866
|
---
|
|
866
867
|
|
|
868
|
+
## Live Query Subscriptions
|
|
869
|
+
|
|
870
|
+
Subscribe to denormalized documents and receive automatic updates when underlying data changes. Subscriptions use a PostgreSQL-first architecture where all change detection happens in the database.
|
|
871
|
+
|
|
872
|
+
For complete documentation, see **[Live Query Subscriptions Guide](../../../docs/LIVE_QUERY_SUBSCRIPTIONS.md)** and **[Quick Start](../../../docs/SUBSCRIPTIONS_QUICK_START.md)**.
|
|
873
|
+
|
|
874
|
+
### Quick Example
|
|
875
|
+
|
|
876
|
+
```javascript
|
|
877
|
+
// Subscribe to venue with all related data
|
|
878
|
+
const { data, unsubscribe } = await ws.api.subscribe_venue_detail(
|
|
879
|
+
{ venue_id: 123 },
|
|
880
|
+
(updatedVenue) => {
|
|
881
|
+
// Called automatically when venue, org, or sites change
|
|
882
|
+
console.log('Updated:', updatedVenue);
|
|
883
|
+
// updatedVenue = { id: 123, name: '...', org: {...}, sites: [...] }
|
|
884
|
+
}
|
|
885
|
+
);
|
|
886
|
+
|
|
887
|
+
// Initial data available immediately
|
|
888
|
+
console.log('Initial:', data);
|
|
889
|
+
|
|
890
|
+
// Later: cleanup
|
|
891
|
+
await unsubscribe();
|
|
892
|
+
```
|
|
893
|
+
|
|
894
|
+
### Creating a Subscribable
|
|
895
|
+
|
|
896
|
+
Define subscribables in SQL:
|
|
897
|
+
|
|
898
|
+
```sql
|
|
899
|
+
SELECT dzql.register_subscribable(
|
|
900
|
+
'venue_detail', -- Name
|
|
901
|
+
'{"subscribe": ["@org_id->acts_for[org_id=$]{active}.user_id"]}'::jsonb, -- Permissions
|
|
902
|
+
'{"venue_id": "int"}'::jsonb, -- Parameters
|
|
903
|
+
'venues', -- Root table
|
|
904
|
+
'{
|
|
905
|
+
"org": "organisations",
|
|
906
|
+
"sites": {"entity": "sites", "filter": "venue_id=$venue_id"}
|
|
907
|
+
}'::jsonb -- Relations
|
|
908
|
+
);
|
|
909
|
+
```
|
|
910
|
+
|
|
911
|
+
### Compile and Deploy
|
|
912
|
+
|
|
913
|
+
```bash
|
|
914
|
+
# Compile subscribable to PostgreSQL functions
|
|
915
|
+
node packages/dzql/compile-subscribable.js venue.sql | psql $DATABASE_URL
|
|
916
|
+
```
|
|
917
|
+
|
|
918
|
+
This generates three functions:
|
|
919
|
+
- `venue_detail_can_subscribe(user_id, params)` - Permission check
|
|
920
|
+
- `get_venue_detail(params, user_id)` - Query builder
|
|
921
|
+
- `venue_detail_affected_documents(table, op, old, new)` - Change detector
|
|
922
|
+
|
|
923
|
+
### Subscription Lifecycle
|
|
924
|
+
|
|
925
|
+
1. **Subscribe**: Client calls `ws.api.subscribe_<name>(params, callback)`
|
|
926
|
+
2. **Permission Check**: `<name>_can_subscribe()` validates access
|
|
927
|
+
3. **Initial Query**: `get_<name>()` returns denormalized document
|
|
928
|
+
4. **Register**: Server stores subscription in-memory
|
|
929
|
+
5. **Database Change**: Any relevant table modification
|
|
930
|
+
6. **Detect**: `<name>_affected_documents()` identifies affected subscriptions
|
|
931
|
+
7. **Re-query**: `get_<name>()` fetches fresh data
|
|
932
|
+
8. **Update**: Callback invoked with new data
|
|
933
|
+
|
|
934
|
+
### Unsubscribe
|
|
935
|
+
|
|
936
|
+
```javascript
|
|
937
|
+
// Method 1: Use returned unsubscribe function
|
|
938
|
+
const { unsubscribe } = await ws.api.subscribe_venue_detail(...);
|
|
939
|
+
await unsubscribe();
|
|
940
|
+
|
|
941
|
+
// Method 2: Direct unsubscribe call
|
|
942
|
+
await ws.api.unsubscribe_venue_detail({ venue_id: 123 });
|
|
943
|
+
```
|
|
944
|
+
|
|
945
|
+
### Architecture Benefits
|
|
946
|
+
|
|
947
|
+
- **PostgreSQL-First**: All logic executes in database, not application code
|
|
948
|
+
- **Zero Configuration**: Pattern matching on method names - no server changes needed
|
|
949
|
+
- **Type Safe**: Compiled functions validated at deploy time
|
|
950
|
+
- **Efficient**: In-memory registry, PostgreSQL does matching
|
|
951
|
+
- **Secure**: Permission paths enforced at database level
|
|
952
|
+
- **Scalable**: Stateless server, can add instances freely
|
|
953
|
+
|
|
954
|
+
### Common Patterns
|
|
955
|
+
|
|
956
|
+
**Single Table:**
|
|
957
|
+
```sql
|
|
958
|
+
SELECT dzql.register_subscribable(
|
|
959
|
+
'user_settings',
|
|
960
|
+
'{"subscribe": ["@user_id"]}'::jsonb,
|
|
961
|
+
'{"user_id": "int"}'::jsonb,
|
|
962
|
+
'user_settings',
|
|
963
|
+
'{}'::jsonb
|
|
964
|
+
);
|
|
965
|
+
```
|
|
966
|
+
|
|
967
|
+
**With Relations:**
|
|
968
|
+
```sql
|
|
969
|
+
SELECT dzql.register_subscribable(
|
|
970
|
+
'booking_detail',
|
|
971
|
+
'{"subscribe": ["@user_id"]}'::jsonb,
|
|
972
|
+
'{"booking_id": "int"}'::jsonb,
|
|
973
|
+
'bookings',
|
|
974
|
+
'{
|
|
975
|
+
"venue": "venues",
|
|
976
|
+
"customer": "users",
|
|
977
|
+
"items": {"entity": "booking_items", "filter": "booking_id=$booking_id"}
|
|
978
|
+
}'::jsonb
|
|
979
|
+
);
|
|
980
|
+
```
|
|
981
|
+
|
|
982
|
+
**Multiple Permission Paths (OR logic):**
|
|
983
|
+
```sql
|
|
984
|
+
SELECT dzql.register_subscribable(
|
|
985
|
+
'venue_admin',
|
|
986
|
+
'{
|
|
987
|
+
"subscribe": [
|
|
988
|
+
"@owner_id",
|
|
989
|
+
"@org_id->acts_for[org_id=$]{active}.user_id"
|
|
990
|
+
]
|
|
991
|
+
}'::jsonb,
|
|
992
|
+
'{"venue_id": "int"}'::jsonb,
|
|
993
|
+
'venues',
|
|
994
|
+
'{"sites": {"entity": "sites", "filter": "venue_id=$venue_id"}}'::jsonb
|
|
995
|
+
);
|
|
996
|
+
```
|
|
997
|
+
|
|
998
|
+
### See Also
|
|
999
|
+
|
|
1000
|
+
- **[Live Query Subscriptions Guide](../../../docs/LIVE_QUERY_SUBSCRIPTIONS.md)** - Complete reference
|
|
1001
|
+
- **[Quick Start Guide](../../../docs/SUBSCRIPTIONS_QUICK_START.md)** - 5-minute tutorial
|
|
1002
|
+
- **[Permission Paths](#permission--notification-paths)** - Path DSL syntax
|
|
1003
|
+
|
|
1004
|
+
---
|
|
1005
|
+
|
|
867
1006
|
## Temporal Relationships
|
|
868
1007
|
|
|
869
1008
|
Handle time-based relationships with `valid_from`/`valid_to` fields.
|
package/package.json
CHANGED
|
@@ -1,12 +1,14 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "dzql",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.2.0",
|
|
4
4
|
"description": "PostgreSQL-powered framework with zero boilerplate CRUD operations and real-time WebSocket synchronization",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "src/server/index.js",
|
|
7
7
|
"exports": {
|
|
8
8
|
".": "./src/server/index.js",
|
|
9
9
|
"./client": "./src/client/ws.js",
|
|
10
|
+
"./client/stores": "./src/client/stores/index.js",
|
|
11
|
+
"./client/templates": "./src/client/templates/App.vue",
|
|
10
12
|
"./server": "./src/server/index.js",
|
|
11
13
|
"./db": "./src/server/db.js",
|
|
12
14
|
"./compiler": "./src/compiler/index.js"
|
|
@@ -20,12 +22,13 @@
|
|
|
20
22
|
],
|
|
21
23
|
"scripts": {
|
|
22
24
|
"test": "bun test",
|
|
23
|
-
"prepublishOnly": "echo '✅ Publishing DZQL v0.
|
|
25
|
+
"prepublishOnly": "echo '✅ Publishing DZQL v0.2.0...'"
|
|
24
26
|
},
|
|
25
27
|
"dependencies": {
|
|
26
28
|
"jose": "^6.1.0",
|
|
27
29
|
"postgres": "^3.4.7"
|
|
28
30
|
},
|
|
31
|
+
|
|
29
32
|
"keywords": [
|
|
30
33
|
"postgresql",
|
|
31
34
|
"postgres",
|
|
@@ -0,0 +1,95 @@
|
|
|
1
|
+
# DZQL Canonical Pinia Stores
|
|
2
|
+
|
|
3
|
+
**The official, AI-friendly Pinia stores for DZQL Vue.js applications.**
|
|
4
|
+
|
|
5
|
+
## Why These Stores Exist
|
|
6
|
+
|
|
7
|
+
When building DZQL apps, developers (and AI assistants) often struggle with:
|
|
8
|
+
|
|
9
|
+
1. **Three-phase lifecycle** - connecting → login → ready
|
|
10
|
+
2. **WebSocket connection management** - reconnection, error handling
|
|
11
|
+
3. **Authentication flow** - token storage, profile management
|
|
12
|
+
4. **Router integration** - navigation, state synchronization
|
|
13
|
+
5. **Inconsistent patterns** - every project does it differently
|
|
14
|
+
|
|
15
|
+
These canonical stores solve all of these problems with a **simple, consistent pattern** that AI can easily understand and replicate.
|
|
16
|
+
|
|
17
|
+
## The Stores
|
|
18
|
+
|
|
19
|
+
### `useWsStore` - WebSocket & Auth
|
|
20
|
+
|
|
21
|
+
Manages:
|
|
22
|
+
- WebSocket connection (with auto-reconnect)
|
|
23
|
+
- User authentication (login/register/logout)
|
|
24
|
+
- Connection state tracking
|
|
25
|
+
- Three-phase app lifecycle
|
|
26
|
+
|
|
27
|
+
### `useAppStore` - Application State
|
|
28
|
+
|
|
29
|
+
Manages:
|
|
30
|
+
- App initialization
|
|
31
|
+
- Router integration
|
|
32
|
+
- Entity metadata caching
|
|
33
|
+
- Navigation helpers
|
|
34
|
+
- UI state (sidebars, panels)
|
|
35
|
+
|
|
36
|
+
## Quick Example
|
|
37
|
+
|
|
38
|
+
```vue
|
|
39
|
+
<script setup>
|
|
40
|
+
import { computed } from 'vue'
|
|
41
|
+
import { useWsStore, useAppStore } from 'dzql/client/stores'
|
|
42
|
+
|
|
43
|
+
const wsStore = useWsStore()
|
|
44
|
+
const appStore = useAppStore()
|
|
45
|
+
|
|
46
|
+
const state = computed(() => wsStore.appState)
|
|
47
|
+
const ws = wsStore.getWs()
|
|
48
|
+
|
|
49
|
+
// Use DZQL API
|
|
50
|
+
const venues = await ws.api.search.venues({ limit: 50 })
|
|
51
|
+
</script>
|
|
52
|
+
|
|
53
|
+
<template>
|
|
54
|
+
<!-- Three-phase lifecycle -->
|
|
55
|
+
<div v-if="state === 'connecting'">Connecting...</div>
|
|
56
|
+
<LoginView v-else-if="state === 'login'" />
|
|
57
|
+
<MainApp v-else-if="state === 'ready'" />
|
|
58
|
+
</template>
|
|
59
|
+
```
|
|
60
|
+
|
|
61
|
+
## Documentation
|
|
62
|
+
|
|
63
|
+
- **[Quick Start](../../../docs/CLIENT-QUICK-START.md)** - Get running in 5 minutes
|
|
64
|
+
- **[Complete Guide](../../../docs/CLIENT-STORES.md)** - Full API reference and patterns
|
|
65
|
+
|
|
66
|
+
## Files
|
|
67
|
+
|
|
68
|
+
- `useWsStore.js` - WebSocket connection and authentication
|
|
69
|
+
- `useAppStore.js` - Application state and navigation
|
|
70
|
+
- `index.js` - Exports both stores
|
|
71
|
+
|
|
72
|
+
## For AI Assistants
|
|
73
|
+
|
|
74
|
+
When helping users with DZQL apps:
|
|
75
|
+
|
|
76
|
+
1. **Always use these stores** - Don't create custom connection logic
|
|
77
|
+
2. **Follow the three-phase lifecycle** - connecting → login → ready
|
|
78
|
+
3. **Use computed for reactive state** - `const profile = computed(() => wsStore.profile)`
|
|
79
|
+
4. **Get WS instance for API calls** - `const ws = wsStore.getWs()`
|
|
80
|
+
|
|
81
|
+
**Example prompt for AI:**
|
|
82
|
+
|
|
83
|
+
> "I'm using the canonical DZQL stores from `dzql/client/stores`. The pattern is:
|
|
84
|
+
> 1. useWsStore for WebSocket connection (three phases: connecting, login, ready)
|
|
85
|
+
> 2. useAppStore for app state and navigation
|
|
86
|
+
> 3. Access DZQL API via `wsStore.getWs().api.get.venues({ id: 1 })`
|
|
87
|
+
> Please follow this pattern."
|
|
88
|
+
|
|
89
|
+
## Version
|
|
90
|
+
|
|
91
|
+
These stores are available in DZQL v0.1.6+
|
|
92
|
+
|
|
93
|
+
## License
|
|
94
|
+
|
|
95
|
+
MIT
|
|
@@ -0,0 +1,285 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Canonical DZQL App Pinia Store
|
|
3
|
+
*
|
|
4
|
+
* Manages application-level state including:
|
|
5
|
+
* - App initialization
|
|
6
|
+
* - Router integration
|
|
7
|
+
* - Global UI state
|
|
8
|
+
* - Entity metadata caching
|
|
9
|
+
*
|
|
10
|
+
* Works with useWsStore to provide complete app lifecycle management.
|
|
11
|
+
*
|
|
12
|
+
* @example
|
|
13
|
+
* // In main.js
|
|
14
|
+
* import { createApp } from 'vue'
|
|
15
|
+
* import { createPinia } from 'pinia'
|
|
16
|
+
* import { useAppStore } from 'dzql/client/stores'
|
|
17
|
+
* import App from './App.vue'
|
|
18
|
+
*
|
|
19
|
+
* const pinia = createPinia()
|
|
20
|
+
* const app = createApp(App)
|
|
21
|
+
* app.use(pinia)
|
|
22
|
+
*
|
|
23
|
+
* const appStore = useAppStore()
|
|
24
|
+
* await appStore.initialize()
|
|
25
|
+
*
|
|
26
|
+
* app.mount('#app')
|
|
27
|
+
*/
|
|
28
|
+
import { defineStore } from 'pinia'
|
|
29
|
+
import { ref, computed } from 'vue'
|
|
30
|
+
import { useWsStore } from './useWsStore.js'
|
|
31
|
+
|
|
32
|
+
export const useAppStore = defineStore('dzql-app', () => {
|
|
33
|
+
// ===== State =====
|
|
34
|
+
|
|
35
|
+
/**
|
|
36
|
+
* App title
|
|
37
|
+
*/
|
|
38
|
+
const title = ref('DZQL App')
|
|
39
|
+
|
|
40
|
+
/**
|
|
41
|
+
* Current route/entity context
|
|
42
|
+
*/
|
|
43
|
+
const currentEntity = ref(null)
|
|
44
|
+
const currentId = ref(null)
|
|
45
|
+
|
|
46
|
+
/**
|
|
47
|
+
* Entity metadata cache
|
|
48
|
+
* Maps entity name -> metadata object
|
|
49
|
+
*/
|
|
50
|
+
const entityMetadata = ref({})
|
|
51
|
+
|
|
52
|
+
/**
|
|
53
|
+
* Loading states
|
|
54
|
+
*/
|
|
55
|
+
const isLoadingMetadata = ref(false)
|
|
56
|
+
|
|
57
|
+
/**
|
|
58
|
+
* UI state
|
|
59
|
+
*/
|
|
60
|
+
const sidebarOpen = ref(true)
|
|
61
|
+
const propertiesPanelOpen = ref(true)
|
|
62
|
+
|
|
63
|
+
/**
|
|
64
|
+
* Router instance (set via setRouter)
|
|
65
|
+
*/
|
|
66
|
+
let routerInstance = null
|
|
67
|
+
|
|
68
|
+
// ===== Computed =====
|
|
69
|
+
|
|
70
|
+
const hasMetadata = computed(() => Object.keys(entityMetadata.value).length > 0)
|
|
71
|
+
|
|
72
|
+
const entityList = computed(() => {
|
|
73
|
+
return Object.keys(entityMetadata.value).sort()
|
|
74
|
+
})
|
|
75
|
+
|
|
76
|
+
const currentEntityMeta = computed(() => {
|
|
77
|
+
if (!currentEntity.value) return null
|
|
78
|
+
return entityMetadata.value[currentEntity.value] || null
|
|
79
|
+
})
|
|
80
|
+
|
|
81
|
+
// ===== Actions =====
|
|
82
|
+
|
|
83
|
+
/**
|
|
84
|
+
* Initialize the app
|
|
85
|
+
*
|
|
86
|
+
* Connects to WebSocket and sets up app lifecycle.
|
|
87
|
+
*
|
|
88
|
+
* @param {Object} options
|
|
89
|
+
* @param {string} [options.wsUrl] - WebSocket URL (auto-detected if not provided)
|
|
90
|
+
* @param {string} [options.title] - App title
|
|
91
|
+
* @returns {Promise<void>}
|
|
92
|
+
*
|
|
93
|
+
* @example
|
|
94
|
+
* await appStore.initialize()
|
|
95
|
+
*
|
|
96
|
+
* @example
|
|
97
|
+
* await appStore.initialize({
|
|
98
|
+
* wsUrl: 'ws://localhost:3000/ws',
|
|
99
|
+
* title: 'My DZQL App'
|
|
100
|
+
* })
|
|
101
|
+
*/
|
|
102
|
+
async function initialize(options = {}) {
|
|
103
|
+
const wsStore = useWsStore()
|
|
104
|
+
|
|
105
|
+
// Set app title if provided
|
|
106
|
+
if (options.title) {
|
|
107
|
+
title.value = options.title
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
// Connect to WebSocket
|
|
111
|
+
await wsStore.connect(options.wsUrl)
|
|
112
|
+
|
|
113
|
+
// If authenticated, fetch metadata
|
|
114
|
+
if (wsStore.isAuthenticated) {
|
|
115
|
+
await fetchMetadata()
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
console.log('[AppStore] Initialized')
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
/**
|
|
122
|
+
* Fetch entity metadata from server
|
|
123
|
+
*
|
|
124
|
+
* Called automatically after authentication.
|
|
125
|
+
*
|
|
126
|
+
* @returns {Promise<void>}
|
|
127
|
+
*/
|
|
128
|
+
async function fetchMetadata() {
|
|
129
|
+
const wsStore = useWsStore()
|
|
130
|
+
const ws = wsStore.getWs()
|
|
131
|
+
|
|
132
|
+
if (!wsStore.isConnected) {
|
|
133
|
+
console.warn('[AppStore] Cannot fetch metadata: not connected')
|
|
134
|
+
return
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
try {
|
|
138
|
+
isLoadingMetadata.value = true
|
|
139
|
+
|
|
140
|
+
// Call the meta endpoint
|
|
141
|
+
const result = await ws.call('meta')
|
|
142
|
+
|
|
143
|
+
if (result && result.entities) {
|
|
144
|
+
entityMetadata.value = result.entities
|
|
145
|
+
console.log('[AppStore] Metadata loaded:', Object.keys(result.entities))
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
} catch (err) {
|
|
149
|
+
console.error('[AppStore] Failed to fetch metadata:', err)
|
|
150
|
+
} finally {
|
|
151
|
+
isLoadingMetadata.value = false
|
|
152
|
+
}
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
/**
|
|
156
|
+
* Set the router instance
|
|
157
|
+
*
|
|
158
|
+
* Call this in main.js after creating the router to enable
|
|
159
|
+
* programmatic navigation from the store.
|
|
160
|
+
*
|
|
161
|
+
* @param {Router} router - Vue Router instance
|
|
162
|
+
*
|
|
163
|
+
* @example
|
|
164
|
+
* import { createRouter } from 'vue-router'
|
|
165
|
+
* import { useAppStore } from 'dzql/client/stores'
|
|
166
|
+
*
|
|
167
|
+
* const router = createRouter({ ... })
|
|
168
|
+
* const appStore = useAppStore()
|
|
169
|
+
* appStore.setRouter(router)
|
|
170
|
+
*/
|
|
171
|
+
function setRouter(router) {
|
|
172
|
+
routerInstance = router
|
|
173
|
+
|
|
174
|
+
// Watch route changes to update current context
|
|
175
|
+
if (router) {
|
|
176
|
+
router.afterEach((to) => {
|
|
177
|
+
currentEntity.value = to.params.entity || null
|
|
178
|
+
currentId.value = to.params.id || null
|
|
179
|
+
})
|
|
180
|
+
}
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
/**
|
|
184
|
+
* Navigate to entity list
|
|
185
|
+
*
|
|
186
|
+
* @param {string} entity - Entity name
|
|
187
|
+
*
|
|
188
|
+
* @example
|
|
189
|
+
* appStore.navigateToEntity('venues')
|
|
190
|
+
*/
|
|
191
|
+
function navigateToEntity(entity) {
|
|
192
|
+
if (routerInstance) {
|
|
193
|
+
routerInstance.push({ name: 'entity-list', params: { entity } })
|
|
194
|
+
}
|
|
195
|
+
currentEntity.value = entity
|
|
196
|
+
currentId.value = null
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
/**
|
|
200
|
+
* Navigate to entity detail/edit
|
|
201
|
+
*
|
|
202
|
+
* @param {string} entity - Entity name
|
|
203
|
+
* @param {number|string} id - Record ID or 'new'
|
|
204
|
+
*
|
|
205
|
+
* @example
|
|
206
|
+
* appStore.navigateToEntityDetail('venues', 123)
|
|
207
|
+
* appStore.navigateToEntityDetail('venues', 'new')
|
|
208
|
+
*/
|
|
209
|
+
function navigateToEntityDetail(entity, id) {
|
|
210
|
+
if (routerInstance) {
|
|
211
|
+
const routeName = id === 'new' ? 'entity-create' : 'entity-edit'
|
|
212
|
+
routerInstance.push({ name: routeName, params: { entity, id } })
|
|
213
|
+
}
|
|
214
|
+
currentEntity.value = entity
|
|
215
|
+
currentId.value = id
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
/**
|
|
219
|
+
* Navigate to home
|
|
220
|
+
*
|
|
221
|
+
* @example
|
|
222
|
+
* appStore.navigateToHome()
|
|
223
|
+
*/
|
|
224
|
+
function navigateToHome() {
|
|
225
|
+
if (routerInstance) {
|
|
226
|
+
routerInstance.push({ name: 'home' })
|
|
227
|
+
}
|
|
228
|
+
currentEntity.value = null
|
|
229
|
+
currentId.value = null
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
/**
|
|
233
|
+
* Toggle sidebar
|
|
234
|
+
*/
|
|
235
|
+
function toggleSidebar() {
|
|
236
|
+
sidebarOpen.value = !sidebarOpen.value
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
/**
|
|
240
|
+
* Toggle properties panel
|
|
241
|
+
*/
|
|
242
|
+
function togglePropertiesPanel() {
|
|
243
|
+
propertiesPanelOpen.value = !propertiesPanelOpen.value
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
/**
|
|
247
|
+
* Set current context (useful for manual navigation)
|
|
248
|
+
*
|
|
249
|
+
* @param {string|null} entity - Entity name
|
|
250
|
+
* @param {number|string|null} id - Record ID
|
|
251
|
+
*/
|
|
252
|
+
function setContext(entity, id = null) {
|
|
253
|
+
currentEntity.value = entity
|
|
254
|
+
currentId.value = id
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
// ===== Return Public API =====
|
|
258
|
+
|
|
259
|
+
return {
|
|
260
|
+
// State
|
|
261
|
+
title,
|
|
262
|
+
currentEntity,
|
|
263
|
+
currentId,
|
|
264
|
+
entityMetadata,
|
|
265
|
+
isLoadingMetadata,
|
|
266
|
+
sidebarOpen,
|
|
267
|
+
propertiesPanelOpen,
|
|
268
|
+
|
|
269
|
+
// Computed
|
|
270
|
+
hasMetadata,
|
|
271
|
+
entityList,
|
|
272
|
+
currentEntityMeta,
|
|
273
|
+
|
|
274
|
+
// Actions
|
|
275
|
+
initialize,
|
|
276
|
+
fetchMetadata,
|
|
277
|
+
setRouter,
|
|
278
|
+
navigateToEntity,
|
|
279
|
+
navigateToEntityDetail,
|
|
280
|
+
navigateToHome,
|
|
281
|
+
toggleSidebar,
|
|
282
|
+
togglePropertiesPanel,
|
|
283
|
+
setContext
|
|
284
|
+
}
|
|
285
|
+
})
|