qdadm 0.27.0 → 0.29.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.
Files changed (42) hide show
  1. package/package.json +4 -2
  2. package/src/composables/index.js +1 -0
  3. package/src/composables/useDeferred.js +85 -0
  4. package/src/composables/useListPageBuilder.js +3 -0
  5. package/src/deferred/DeferredRegistry.js +323 -0
  6. package/src/deferred/index.js +7 -0
  7. package/src/entity/EntityManager.js +82 -14
  8. package/src/entity/factory.js +155 -0
  9. package/src/entity/factory.test.js +189 -0
  10. package/src/entity/index.js +8 -0
  11. package/src/entity/storage/ApiStorage.js +4 -1
  12. package/src/entity/storage/IStorage.js +76 -0
  13. package/src/entity/storage/LocalStorage.js +4 -1
  14. package/src/entity/storage/MemoryStorage.js +4 -1
  15. package/src/entity/storage/MockApiStorage.js +4 -1
  16. package/src/entity/storage/SdkStorage.js +4 -1
  17. package/src/entity/storage/factory.js +193 -0
  18. package/src/entity/storage/factory.test.js +159 -0
  19. package/src/entity/storage/index.js +13 -0
  20. package/src/gen/FieldMapper.js +116 -0
  21. package/src/gen/StorageProfileFactory.js +109 -0
  22. package/src/gen/connectors/BaseConnector.js +142 -0
  23. package/src/gen/connectors/ManualConnector.js +385 -0
  24. package/src/gen/connectors/ManualConnector.test.js +499 -0
  25. package/src/gen/connectors/OpenAPIConnector.js +568 -0
  26. package/src/gen/connectors/OpenAPIConnector.test.js +737 -0
  27. package/src/gen/connectors/__fixtures__/sample-openapi.json +311 -0
  28. package/src/gen/connectors/index.js +11 -0
  29. package/src/gen/createManagers.js +224 -0
  30. package/src/gen/decorators.js +129 -0
  31. package/src/gen/generateManagers.js +266 -0
  32. package/src/gen/generateManagers.test.js +358 -0
  33. package/src/gen/index.js +45 -0
  34. package/src/gen/schema.js +221 -0
  35. package/src/gen/vite-plugin.js +105 -0
  36. package/src/generated/managers/testManager.js +45 -0
  37. package/src/index.js +3 -0
  38. package/src/kernel/EventRouter.js +264 -0
  39. package/src/kernel/Kernel.js +123 -8
  40. package/src/kernel/index.js +4 -0
  41. package/src/orchestrator/Orchestrator.js +60 -0
  42. package/src/query/FilterQuery.js +9 -4
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "qdadm",
3
- "version": "0.27.0",
3
+ "version": "0.29.0",
4
4
  "description": "Vue 3 framework for admin dashboards with PrimeVue",
5
5
  "author": "quazardous",
6
6
  "license": "MIT",
@@ -25,7 +25,9 @@
25
25
  "./module": "./src/module/index.js",
26
26
  "./utils": "./src/utils/index.js",
27
27
  "./styles": "./src/styles/index.scss",
28
- "./styles/breakpoints": "./src/styles/_breakpoints.scss"
28
+ "./styles/breakpoints": "./src/styles/_breakpoints.scss",
29
+ "./gen": "./src/gen/index.js",
30
+ "./gen/vite-plugin": "./src/gen/vite-plugin.js"
29
31
  },
30
32
  "files": [
31
33
  "src",
@@ -21,3 +21,4 @@ export { useZoneRegistry } from './useZoneRegistry'
21
21
  export { useHooks } from './useHooks'
22
22
  export { useLayoutResolver, createLayoutComponents, layoutMeta, LAYOUT_TYPES } from './useLayoutResolver'
23
23
  export { useSSE } from './useSSE'
24
+ export { useDeferred, useDeferredValue, DEFERRED_INJECTION_KEY } from './useDeferred'
@@ -0,0 +1,85 @@
1
+ /**
2
+ * useDeferred - Access the DeferredRegistry from components
3
+ *
4
+ * Provides loose async coupling between services and components.
5
+ * Components can await dependencies without knowing when they're loaded.
6
+ *
7
+ * @example
8
+ * ```js
9
+ * import { useDeferred } from 'qdadm/composables'
10
+ *
11
+ * // In a component
12
+ * const deferred = useDeferred()
13
+ *
14
+ * // Await a single dependency
15
+ * const config = await deferred.await('config')
16
+ *
17
+ * // Await multiple dependencies
18
+ * const [users, settings] = await Promise.all([
19
+ * deferred.await('users-service'),
20
+ * deferred.await('settings')
21
+ * ])
22
+ *
23
+ * // Check status without waiting
24
+ * if (deferred.isSettled('config')) {
25
+ * const config = deferred.value('config')
26
+ * }
27
+ * ```
28
+ */
29
+
30
+ import { inject } from 'vue'
31
+
32
+ /**
33
+ * Injection key for DeferredRegistry
34
+ */
35
+ export const DEFERRED_INJECTION_KEY = 'qdadmDeferred'
36
+
37
+ /**
38
+ * Get the DeferredRegistry instance
39
+ * @returns {import('../deferred/DeferredRegistry.js').DeferredRegistry}
40
+ */
41
+ export function useDeferred() {
42
+ const deferred = inject(DEFERRED_INJECTION_KEY)
43
+
44
+ if (!deferred) {
45
+ throw new Error(
46
+ '[useDeferred] DeferredRegistry not found. ' +
47
+ 'Make sure you are using the Kernel to bootstrap your app.'
48
+ )
49
+ }
50
+
51
+ return deferred
52
+ }
53
+
54
+ /**
55
+ * Helper: await a deferred value in setup
56
+ * Returns a ref that updates when the deferred resolves
57
+ *
58
+ * @example
59
+ * ```js
60
+ * const { data: config, loading, error } = useDeferredValue('config')
61
+ * ```
62
+ *
63
+ * @param {string} key - Deferred key
64
+ * @returns {{ data: Ref, loading: Ref<boolean>, error: Ref<Error|null> }}
65
+ */
66
+ export function useDeferredValue(key) {
67
+ const deferred = useDeferred()
68
+ const data = ref(null)
69
+ const loading = ref(true)
70
+ const error = ref(null)
71
+
72
+ deferred.await(key)
73
+ .then(value => {
74
+ data.value = value
75
+ loading.value = false
76
+ })
77
+ .catch(err => {
78
+ error.value = err
79
+ loading.value = false
80
+ })
81
+
82
+ return { data, loading, error }
83
+ }
84
+
85
+ import { ref } from 'vue'
@@ -994,6 +994,9 @@ export function useListPageBuilder(config = {}) {
994
994
  const displayItems = computed(() => filteredItems.value)
995
995
 
996
996
  // ============ LOADING ============
997
+ // Note: Auth changes are handled by Vue component lifecycle - when user logs out,
998
+ // router guard redirects to login, component unmounts. On re-login, new component
999
+ // instance is created with fresh state (filterOptionsLoaded = false).
997
1000
  let filterOptionsLoaded = false
998
1001
 
999
1002
  async function loadItems(extraParams = {}, { force = false } = {}) {
@@ -0,0 +1,323 @@
1
+ /**
2
+ * DeferredRegistry - Named promise registry for loose coupling
3
+ *
4
+ * Enables async dependencies between services/components:
5
+ * - Services queue work during warmup
6
+ * - Components await dependencies without tight coupling
7
+ * - External signals (SSE) can resolve promises
8
+ *
9
+ * Key insight: await() can be called BEFORE queue() - the promise
10
+ * is created on first access and resolved when the task completes.
11
+ *
12
+ * @example
13
+ * ```js
14
+ * // During app boot - queue lazy services
15
+ * deferred.queue('users-service', () => usersService.init())
16
+ * deferred.queue('config', () => configService.load())
17
+ *
18
+ * // In component - await dependencies (can be called before queue!)
19
+ * const config = await deferred.await('config')
20
+ *
21
+ * // Wait for multiple with Promise.all
22
+ * const [users, config] = await Promise.all([
23
+ * deferred.await('users-service'),
24
+ * deferred.await('config')
25
+ * ])
26
+ *
27
+ * // External resolution (SSE, webhooks)
28
+ * deferred.resolve('job-123', { status: 'done', data: result })
29
+ * deferred.reject('job-456', new Error('Failed'))
30
+ * ```
31
+ */
32
+
33
+ /**
34
+ * @typedef {'pending' | 'running' | 'completed' | 'failed'} DeferredStatus
35
+ */
36
+
37
+ /**
38
+ * @typedef {object} DeferredEntry
39
+ * @property {Promise} promise - The deferred promise
40
+ * @property {function} resolve - Resolve function
41
+ * @property {function} reject - Reject function
42
+ * @property {DeferredStatus} status - Current status
43
+ * @property {any} value - Resolved value or error
44
+ * @property {number} timestamp - Creation timestamp
45
+ */
46
+
47
+ export class DeferredRegistry {
48
+ /**
49
+ * @param {object} options
50
+ * @param {object} [options.kernel] - Optional QuarKernel for event emission
51
+ * @param {boolean} [options.debug=false] - Enable debug logging
52
+ */
53
+ constructor(options = {}) {
54
+ this._entries = new Map()
55
+ this._kernel = options.kernel || null
56
+ this._debug = options.debug || false
57
+ }
58
+
59
+ /**
60
+ * Get or create a deferred entry for a key
61
+ * @param {string} key - Unique identifier
62
+ * @returns {DeferredEntry}
63
+ */
64
+ _getOrCreate(key) {
65
+ if (!this._entries.has(key)) {
66
+ let resolve, reject
67
+ const promise = new Promise((res, rej) => {
68
+ resolve = res
69
+ reject = rej
70
+ })
71
+
72
+ const entry = {
73
+ promise,
74
+ resolve,
75
+ reject,
76
+ status: 'pending',
77
+ value: undefined,
78
+ timestamp: Date.now()
79
+ }
80
+
81
+ this._entries.set(key, entry)
82
+
83
+ if (this._debug) {
84
+ console.debug(`[deferred] Created entry: ${key}`)
85
+ }
86
+ }
87
+
88
+ return this._entries.get(key)
89
+ }
90
+
91
+ /**
92
+ * Get the promise for a key (creates if doesn't exist)
93
+ * Can be called BEFORE queue() - promise resolves when task completes
94
+ *
95
+ * @param {string} key - Unique identifier
96
+ * @returns {Promise<any>}
97
+ */
98
+ await(key) {
99
+ return this._getOrCreate(key).promise
100
+ }
101
+
102
+ /**
103
+ * Queue a task for execution
104
+ * If already running/completed, returns existing promise (deduplication)
105
+ *
106
+ * @param {string} key - Unique identifier
107
+ * @param {function(): Promise<any>} executor - Async function to execute
108
+ * @returns {Promise<any>} Promise that resolves with task result
109
+ */
110
+ queue(key, executor) {
111
+ const entry = this._getOrCreate(key)
112
+
113
+ // Already started - return existing promise (idempotent)
114
+ if (entry.status !== 'pending') {
115
+ if (this._debug) {
116
+ console.debug(`[deferred] Already ${entry.status}: ${key}`)
117
+ }
118
+ return entry.promise
119
+ }
120
+
121
+ entry.status = 'running'
122
+ this._emit('deferred:started', { key })
123
+
124
+ if (this._debug) {
125
+ console.debug(`[deferred] Started: ${key}`)
126
+ }
127
+
128
+ // Execute and handle result
129
+ Promise.resolve()
130
+ .then(() => executor())
131
+ .then(value => {
132
+ entry.status = 'completed'
133
+ entry.value = value
134
+ entry.resolve(value)
135
+ this._emit('deferred:completed', { key, value })
136
+
137
+ if (this._debug) {
138
+ console.debug(`[deferred] Completed: ${key}`)
139
+ }
140
+ })
141
+ .catch(error => {
142
+ entry.status = 'failed'
143
+ entry.value = error
144
+ entry.reject(error)
145
+ this._emit('deferred:failed', { key, error })
146
+
147
+ if (this._debug) {
148
+ console.debug(`[deferred] Failed: ${key}`, error)
149
+ }
150
+ })
151
+
152
+ return entry.promise
153
+ }
154
+
155
+ /**
156
+ * Resolve a deferred externally (for SSE, webhooks, etc.)
157
+ * No-op if already completed/failed
158
+ *
159
+ * @param {string} key - Unique identifier
160
+ * @param {any} value - Value to resolve with
161
+ * @returns {boolean} True if resolved, false if already settled
162
+ */
163
+ resolve(key, value) {
164
+ const entry = this._getOrCreate(key)
165
+
166
+ if (entry.status === 'completed' || entry.status === 'failed') {
167
+ if (this._debug) {
168
+ console.debug(`[deferred] Cannot resolve (already ${entry.status}): ${key}`)
169
+ }
170
+ return false
171
+ }
172
+
173
+ entry.status = 'completed'
174
+ entry.value = value
175
+ entry.resolve(value)
176
+ this._emit('deferred:completed', { key, value })
177
+
178
+ if (this._debug) {
179
+ console.debug(`[deferred] Resolved externally: ${key}`)
180
+ }
181
+
182
+ return true
183
+ }
184
+
185
+ /**
186
+ * Reject a deferred externally (for SSE, webhooks, etc.)
187
+ * No-op if already completed/failed
188
+ *
189
+ * @param {string} key - Unique identifier
190
+ * @param {Error} error - Error to reject with
191
+ * @returns {boolean} True if rejected, false if already settled
192
+ */
193
+ reject(key, error) {
194
+ const entry = this._getOrCreate(key)
195
+
196
+ if (entry.status === 'completed' || entry.status === 'failed') {
197
+ if (this._debug) {
198
+ console.debug(`[deferred] Cannot reject (already ${entry.status}): ${key}`)
199
+ }
200
+ return false
201
+ }
202
+
203
+ entry.status = 'failed'
204
+ entry.value = error
205
+ entry.reject(error)
206
+ this._emit('deferred:failed', { key, error })
207
+
208
+ if (this._debug) {
209
+ console.debug(`[deferred] Rejected externally: ${key}`)
210
+ }
211
+
212
+ return true
213
+ }
214
+
215
+ /**
216
+ * Check if a key exists in the registry
217
+ * @param {string} key
218
+ * @returns {boolean}
219
+ */
220
+ has(key) {
221
+ return this._entries.has(key)
222
+ }
223
+
224
+ /**
225
+ * Get the status of a deferred
226
+ * @param {string} key
227
+ * @returns {DeferredStatus|null} Status or null if not found
228
+ */
229
+ status(key) {
230
+ return this._entries.get(key)?.status || null
231
+ }
232
+
233
+ /**
234
+ * Get the resolved value (only if completed)
235
+ * @param {string} key
236
+ * @returns {any} Value or undefined
237
+ */
238
+ value(key) {
239
+ const entry = this._entries.get(key)
240
+ return entry?.status === 'completed' ? entry.value : undefined
241
+ }
242
+
243
+ /**
244
+ * Check if a deferred is settled (completed or failed)
245
+ * @param {string} key
246
+ * @returns {boolean}
247
+ */
248
+ isSettled(key) {
249
+ const status = this.status(key)
250
+ return status === 'completed' || status === 'failed'
251
+ }
252
+
253
+ /**
254
+ * Get all keys in the registry
255
+ * @returns {string[]}
256
+ */
257
+ keys() {
258
+ return Array.from(this._entries.keys())
259
+ }
260
+
261
+ /**
262
+ * Get all entries with their status
263
+ * @returns {Array<{key: string, status: DeferredStatus, timestamp: number}>}
264
+ */
265
+ entries() {
266
+ return Array.from(this._entries.entries()).map(([key, entry]) => ({
267
+ key,
268
+ status: entry.status,
269
+ timestamp: entry.timestamp
270
+ }))
271
+ }
272
+
273
+ /**
274
+ * Clear a specific entry (for cleanup/retry)
275
+ * @param {string} key
276
+ * @returns {boolean} True if cleared
277
+ */
278
+ clear(key) {
279
+ return this._entries.delete(key)
280
+ }
281
+
282
+ /**
283
+ * Clear all entries
284
+ */
285
+ clearAll() {
286
+ this._entries.clear()
287
+ }
288
+
289
+ /**
290
+ * Emit event via kernel (if provided)
291
+ * @private
292
+ */
293
+ _emit(event, data) {
294
+ if (this._kernel) {
295
+ this._kernel.emit(event, data)
296
+ }
297
+ }
298
+
299
+ /**
300
+ * Set kernel for event emission
301
+ * @param {object} kernel - QuarKernel instance
302
+ */
303
+ setKernel(kernel) {
304
+ this._kernel = kernel
305
+ }
306
+
307
+ /**
308
+ * Enable/disable debug mode
309
+ * @param {boolean} enabled
310
+ */
311
+ debug(enabled) {
312
+ this._debug = enabled
313
+ }
314
+ }
315
+
316
+ /**
317
+ * Factory function
318
+ * @param {object} options
319
+ * @returns {DeferredRegistry}
320
+ */
321
+ export function createDeferredRegistry(options = {}) {
322
+ return new DeferredRegistry(options)
323
+ }
@@ -0,0 +1,7 @@
1
+ /**
2
+ * Deferred Module
3
+ *
4
+ * Named promise registry for loose async coupling between services and components.
5
+ */
6
+
7
+ export { DeferredRegistry, createDeferredRegistry } from './DeferredRegistry.js'
@@ -58,6 +58,7 @@ export class EntityManager {
58
58
  // List behavior
59
59
  localFilterThreshold = null, // Items threshold to switch to local filtering (null = use default)
60
60
  readOnly = false, // If true, canCreate/canUpdate/canDelete return false
61
+ warmup = true, // If true, cache is preloaded at boot via DeferredRegistry
61
62
  // Scope control
62
63
  scopeWhitelist = null, // Array of scopes/modules that can bypass restrictions
63
64
  // Relations
@@ -83,6 +84,7 @@ export class EntityManager {
83
84
  // List behavior
84
85
  this.localFilterThreshold = localFilterThreshold
85
86
  this._readOnly = readOnly
87
+ this._warmup = warmup
86
88
 
87
89
  // Scope control
88
90
  this._scopeWhitelist = scopeWhitelist
@@ -1039,31 +1041,48 @@ export class EntityManager {
1039
1041
  }
1040
1042
 
1041
1043
  /**
1042
- * Set up signal listeners for parent entity cache invalidation
1044
+ * Set up signal listeners for cache invalidation
1043
1045
  *
1044
- * When a parent entity is modified, clears the _search cache on cached items
1045
- * so that next list() will re-resolve with fresh parent data.
1046
+ * Listens for:
1047
+ * - Parent entity invalidation: clears _search cache and invalidates
1048
+ * - Auth changes: invalidates cache on login/logout (user context changed)
1046
1049
  */
1047
1050
  _setupCacheListeners() {
1048
- // Nothing to do if no parents or no signals
1049
- if (!this._parents || Object.keys(this._parents).length === 0) return
1050
1051
  if (!this._signals) return
1051
1052
 
1052
- // Clean up existing listener if any
1053
+ // Clean up existing listeners if any
1053
1054
  if (this._signalCleanup) {
1054
1055
  this._signalCleanup()
1055
1056
  this._signalCleanup = null
1056
1057
  }
1057
1058
 
1058
- // Get parent entity names
1059
- const parentEntities = Object.values(this._parents).map(p => p.entity)
1059
+ const cleanups = []
1060
1060
 
1061
- // Listen for parent cache invalidation
1062
- this._signalCleanup = this._signals.on('cache:entity:invalidated', ({ entity }) => {
1063
- if (parentEntities.includes(entity)) {
1064
- this._clearSearchCache()
1065
- }
1066
- })
1061
+ // Listen for parent cache invalidation (if parents defined)
1062
+ if (this._parents && Object.keys(this._parents).length > 0) {
1063
+ const parentEntities = Object.values(this._parents).map(p => p.entity)
1064
+ cleanups.push(
1065
+ this._signals.on('cache:entity:invalidated', ({ entity }) => {
1066
+ if (parentEntities.includes(entity)) {
1067
+ this._clearSearchCache()
1068
+ }
1069
+ })
1070
+ )
1071
+ }
1072
+
1073
+ // Listen for targeted cache invalidation (routed by EventRouter)
1074
+ // EntityManager only listens to its own signal, staying simple.
1075
+ // EventRouter transforms high-level events (auth:impersonate) into targeted signals.
1076
+ cleanups.push(
1077
+ this._signals.on(`cache:entity:invalidate:${this.name}`, () => {
1078
+ this.invalidateCache()
1079
+ })
1080
+ )
1081
+
1082
+ // Combined cleanup function
1083
+ this._signalCleanup = () => {
1084
+ cleanups.forEach(cleanup => cleanup())
1085
+ }
1067
1086
  }
1068
1087
 
1069
1088
  /**
@@ -1178,6 +1197,55 @@ export class EntityManager {
1178
1197
  return true
1179
1198
  }
1180
1199
 
1200
+ /**
1201
+ * Warmup: preload cache via DeferredRegistry for loose async coupling
1202
+ *
1203
+ * Called by Orchestrator.fireWarmups() at boot. Registers the cache loading
1204
+ * in DeferredRegistry so pages can await it before rendering.
1205
+ *
1206
+ * @returns {Promise<boolean>|null} Promise if warmup started, null if disabled/not applicable
1207
+ *
1208
+ * @example
1209
+ * ```js
1210
+ * // At boot (automatic via Kernel)
1211
+ * orchestrator.warmupAll()
1212
+ *
1213
+ * // In component - await cache ready
1214
+ * await deferred.await('entity:books:cache')
1215
+ * const { items } = await booksManager.list() // Uses local cache
1216
+ * ```
1217
+ */
1218
+ async warmup() {
1219
+ // Skip if warmup disabled or caching not applicable
1220
+ if (!this._warmup) return null
1221
+ if (!this.isCacheEnabled) return null
1222
+
1223
+ const deferred = this._orchestrator?.deferred
1224
+
1225
+ // Wait for auth if app uses authentication (user context affects cache)
1226
+ if (deferred?.has('auth:ready')) {
1227
+ await deferred.await('auth:ready')
1228
+ }
1229
+
1230
+ const key = `entity:${this.name}:cache`
1231
+
1232
+ if (!deferred) {
1233
+ // Fallback: direct cache load without DeferredRegistry
1234
+ return this.ensureCache()
1235
+ }
1236
+
1237
+ // Register in DeferredRegistry for loose coupling
1238
+ return deferred.queue(key, () => this.ensureCache())
1239
+ }
1240
+
1241
+ /**
1242
+ * Check if warmup is enabled for this manager
1243
+ * @returns {boolean}
1244
+ */
1245
+ get warmupEnabled() {
1246
+ return this._warmup && this.isCacheEnabled
1247
+ }
1248
+
1181
1249
  /**
1182
1250
  * Query entities with automatic cache/API decision
1183
1251
  *