qdadm 0.27.0 → 0.28.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/package.json +1 -1
- package/src/composables/index.js +1 -0
- package/src/composables/useDeferred.js +85 -0
- package/src/composables/useListPageBuilder.js +3 -0
- package/src/deferred/DeferredRegistry.js +323 -0
- package/src/deferred/index.js +7 -0
- package/src/entity/EntityManager.js +82 -14
- package/src/entity/factory.js +155 -0
- package/src/entity/factory.test.js +189 -0
- package/src/entity/index.js +8 -0
- package/src/entity/storage/ApiStorage.js +4 -1
- package/src/entity/storage/IStorage.js +76 -0
- package/src/entity/storage/LocalStorage.js +4 -1
- package/src/entity/storage/MemoryStorage.js +4 -1
- package/src/entity/storage/MockApiStorage.js +4 -1
- package/src/entity/storage/SdkStorage.js +4 -1
- package/src/entity/storage/factory.js +193 -0
- package/src/entity/storage/factory.test.js +159 -0
- package/src/entity/storage/index.js +13 -0
- package/src/index.js +3 -0
- package/src/kernel/EventRouter.js +264 -0
- package/src/kernel/Kernel.js +123 -8
- package/src/kernel/index.js +4 -0
- package/src/orchestrator/Orchestrator.js +60 -0
- package/src/query/FilterQuery.js +9 -4
package/src/kernel/Kernel.js
CHANGED
|
@@ -52,6 +52,10 @@ import { createZoneRegistry } from '../zones/ZoneRegistry.js'
|
|
|
52
52
|
import { registerStandardZones } from '../zones/zones.js'
|
|
53
53
|
import { createHookRegistry } from '../hooks/HookRegistry.js'
|
|
54
54
|
import { createSecurityChecker } from '../entity/auth/SecurityChecker.js'
|
|
55
|
+
import { createManagers } from '../entity/factory.js'
|
|
56
|
+
import { defaultStorageResolver } from '../entity/storage/factory.js'
|
|
57
|
+
import { createDeferredRegistry } from '../deferred/DeferredRegistry.js'
|
|
58
|
+
import { createEventRouter } from './EventRouter.js'
|
|
55
59
|
|
|
56
60
|
export class Kernel {
|
|
57
61
|
/**
|
|
@@ -60,7 +64,10 @@ export class Kernel {
|
|
|
60
64
|
* @param {object} options.modules - Result of import.meta.glob for module init files
|
|
61
65
|
* @param {object} options.modulesOptions - Options for initModules (e.g., { coreNavItems })
|
|
62
66
|
* @param {string[]} options.sectionOrder - Navigation section order
|
|
63
|
-
* @param {object} options.managers - Entity managers { name:
|
|
67
|
+
* @param {object} options.managers - Entity managers { name: config } - can be instances, strings, or config objects
|
|
68
|
+
* @param {object} options.managerRegistry - Registry of manager classes from qdadm-gen { name: ManagerClass }
|
|
69
|
+
* @param {function} options.storageResolver - Custom storage resolver (config, entityName) => Storage
|
|
70
|
+
* @param {function} options.managerResolver - Custom manager resolver (config, entityName, context) => Manager
|
|
64
71
|
* @param {object} options.authAdapter - Auth adapter for login/logout (app-level authentication)
|
|
65
72
|
* @param {object} options.entityAuthAdapter - Auth adapter for entity permissions (scope/silo checks)
|
|
66
73
|
* @param {object} options.pages - Page components { login, layout }
|
|
@@ -73,6 +80,8 @@ export class Kernel {
|
|
|
73
80
|
* @param {object} options.primevue - PrimeVue config { plugin, theme, options }
|
|
74
81
|
* @param {object} options.layouts - Layout components { list, form, dashboard, base }
|
|
75
82
|
* @param {object} options.security - Security config { role_hierarchy, role_permissions, entity_permissions }
|
|
83
|
+
* @param {boolean} options.warmup - Enable warmup at boot (default: true)
|
|
84
|
+
* @param {object} options.eventRouter - EventRouter config { 'source:signal': ['target:signal', ...] }
|
|
76
85
|
*/
|
|
77
86
|
constructor(options) {
|
|
78
87
|
this.options = options
|
|
@@ -82,6 +91,8 @@ export class Kernel {
|
|
|
82
91
|
this.orchestrator = null
|
|
83
92
|
this.zoneRegistry = null
|
|
84
93
|
this.hookRegistry = null
|
|
94
|
+
this.deferred = null
|
|
95
|
+
this.eventRouter = null
|
|
85
96
|
this.layoutComponents = null
|
|
86
97
|
this.securityChecker = null
|
|
87
98
|
}
|
|
@@ -95,19 +106,57 @@ export class Kernel {
|
|
|
95
106
|
this._createSignalBus()
|
|
96
107
|
this._createHookRegistry()
|
|
97
108
|
this._createZoneRegistry()
|
|
98
|
-
|
|
109
|
+
this._createDeferredRegistry()
|
|
110
|
+
// 2. Register auth:ready deferred (if auth configured)
|
|
111
|
+
this._registerAuthDeferred()
|
|
112
|
+
// 3. Initialize modules (can use all services, registers routes)
|
|
99
113
|
this._initModules()
|
|
100
|
-
//
|
|
114
|
+
// 4. Create router (needs routes from modules)
|
|
101
115
|
this._createRouter()
|
|
102
|
-
//
|
|
116
|
+
// 5. Create orchestrator and remaining components
|
|
103
117
|
this._createOrchestrator()
|
|
118
|
+
// 6. Create EventRouter (needs signals + orchestrator)
|
|
119
|
+
this._createEventRouter()
|
|
104
120
|
this._setupSecurity()
|
|
105
121
|
this._createLayoutComponents()
|
|
106
122
|
this._createVueApp()
|
|
107
123
|
this._installPlugins()
|
|
124
|
+
// 6. Fire warmups (fire-and-forget, pages await via DeferredRegistry)
|
|
125
|
+
this._fireWarmups()
|
|
108
126
|
return this.vueApp
|
|
109
127
|
}
|
|
110
128
|
|
|
129
|
+
/**
|
|
130
|
+
* Register auth:ready deferred if auth is configured
|
|
131
|
+
* This allows warmup and other services to await authentication.
|
|
132
|
+
*/
|
|
133
|
+
_registerAuthDeferred() {
|
|
134
|
+
const { authAdapter } = this.options
|
|
135
|
+
if (!authAdapter) return
|
|
136
|
+
|
|
137
|
+
// Create a promise that resolves on first auth:login
|
|
138
|
+
this.deferred.queue('auth:ready', () => {
|
|
139
|
+
return new Promise(resolve => {
|
|
140
|
+
this.signals.once('auth:login', ({ user }) => {
|
|
141
|
+
resolve(user)
|
|
142
|
+
})
|
|
143
|
+
})
|
|
144
|
+
})
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
/**
|
|
148
|
+
* Fire entity cache warmups
|
|
149
|
+
* Fire-and-forget: pages that need cache will await via DeferredRegistry.
|
|
150
|
+
* Controlled by options.warmup (default: true).
|
|
151
|
+
*/
|
|
152
|
+
_fireWarmups() {
|
|
153
|
+
const warmup = this.options.warmup ?? true
|
|
154
|
+
if (!warmup) return
|
|
155
|
+
|
|
156
|
+
// Fire-and-forget: each manager awaits its dependencies (auth:ready, etc.)
|
|
157
|
+
this.orchestrator.fireWarmups()
|
|
158
|
+
}
|
|
159
|
+
|
|
111
160
|
/**
|
|
112
161
|
* Initialize modules from glob import
|
|
113
162
|
* Passes services to modules for zone/signal/hook registration
|
|
@@ -121,7 +170,8 @@ export class Kernel {
|
|
|
121
170
|
...this.options.modulesOptions,
|
|
122
171
|
zones: this.zoneRegistry,
|
|
123
172
|
signals: this.signals,
|
|
124
|
-
hooks: this.hookRegistry
|
|
173
|
+
hooks: this.hookRegistry,
|
|
174
|
+
deferred: this.deferred
|
|
125
175
|
})
|
|
126
176
|
}
|
|
127
177
|
}
|
|
@@ -215,12 +265,28 @@ export class Kernel {
|
|
|
215
265
|
* Create orchestrator with managers and signal bus
|
|
216
266
|
* Injects entityAuthAdapter and hookRegistry into all managers for permission checks
|
|
217
267
|
* and lifecycle hook support.
|
|
268
|
+
*
|
|
269
|
+
* Uses createManagers() to resolve manager configs through the factory pattern:
|
|
270
|
+
* - String patterns ('api:/api/bots') → creates storage + manager
|
|
271
|
+
* - Config objects ({ storage: '...', label: '...' }) → resolved
|
|
272
|
+
* - Manager instances → passed through directly
|
|
218
273
|
*/
|
|
219
274
|
_createOrchestrator() {
|
|
275
|
+
// Build factory context with resolvers and registry
|
|
276
|
+
const factoryContext = {
|
|
277
|
+
storageResolver: this.options.storageResolver || defaultStorageResolver,
|
|
278
|
+
managerResolver: this.options.managerResolver,
|
|
279
|
+
managerRegistry: this.options.managerRegistry || {}
|
|
280
|
+
}
|
|
281
|
+
|
|
282
|
+
// Resolve all managers through factory
|
|
283
|
+
const managers = createManagers(this.options.managers || {}, factoryContext)
|
|
284
|
+
|
|
220
285
|
this.orchestrator = new Orchestrator({
|
|
221
|
-
managers
|
|
286
|
+
managers,
|
|
222
287
|
signals: this.signals,
|
|
223
288
|
hooks: this.hookRegistry,
|
|
289
|
+
deferred: this.deferred,
|
|
224
290
|
entityAuthAdapter: this.options.entityAuthAdapter || null
|
|
225
291
|
})
|
|
226
292
|
}
|
|
@@ -273,6 +339,35 @@ export class Kernel {
|
|
|
273
339
|
registerStandardZones(this.zoneRegistry)
|
|
274
340
|
}
|
|
275
341
|
|
|
342
|
+
/**
|
|
343
|
+
* Create deferred registry for async service loading
|
|
344
|
+
* Enables loose coupling between services and components via named promises.
|
|
345
|
+
*/
|
|
346
|
+
_createDeferredRegistry() {
|
|
347
|
+
const debug = this.options.debug ?? false
|
|
348
|
+
this.deferred = createDeferredRegistry({
|
|
349
|
+
kernel: this.signals?.getKernel(),
|
|
350
|
+
debug
|
|
351
|
+
})
|
|
352
|
+
}
|
|
353
|
+
|
|
354
|
+
/**
|
|
355
|
+
* Create EventRouter for declarative signal routing
|
|
356
|
+
* Transforms high-level events into targeted signals.
|
|
357
|
+
*/
|
|
358
|
+
_createEventRouter() {
|
|
359
|
+
const { eventRouter: routes } = this.options
|
|
360
|
+
if (!routes || Object.keys(routes).length === 0) return
|
|
361
|
+
|
|
362
|
+
const debug = this.options.debug ?? false
|
|
363
|
+
this.eventRouter = createEventRouter({
|
|
364
|
+
signals: this.signals,
|
|
365
|
+
orchestrator: this.orchestrator,
|
|
366
|
+
routes,
|
|
367
|
+
debug
|
|
368
|
+
})
|
|
369
|
+
}
|
|
370
|
+
|
|
276
371
|
/**
|
|
277
372
|
* Create layout components map for useLayoutResolver
|
|
278
373
|
* Maps layout types to their Vue components.
|
|
@@ -302,7 +397,7 @@ export class Kernel {
|
|
|
302
397
|
*/
|
|
303
398
|
_installPlugins() {
|
|
304
399
|
const app = this.vueApp
|
|
305
|
-
const {
|
|
400
|
+
const { authAdapter, features, primevue } = this.options
|
|
306
401
|
|
|
307
402
|
// Pinia
|
|
308
403
|
app.use(createPinia())
|
|
@@ -346,13 +441,17 @@ export class Kernel {
|
|
|
346
441
|
// Hook registry injection
|
|
347
442
|
app.provide('qdadmHooks', this.hookRegistry)
|
|
348
443
|
|
|
444
|
+
// Deferred registry injection
|
|
445
|
+
app.provide('qdadmDeferred', this.deferred)
|
|
446
|
+
|
|
349
447
|
// Layout components injection for useLayoutResolver
|
|
350
448
|
app.provide('qdadmLayoutComponents', this.layoutComponents)
|
|
351
449
|
|
|
352
450
|
// qdadm plugin
|
|
451
|
+
// Note: Don't pass managers here - orchestrator already has resolved managers
|
|
452
|
+
// from createManagers(). Passing raw configs would overwrite them.
|
|
353
453
|
app.use(createQdadm({
|
|
354
454
|
orchestrator: this.orchestrator,
|
|
355
|
-
managers,
|
|
356
455
|
authAdapter,
|
|
357
456
|
router: this.router,
|
|
358
457
|
toast: app.config.globalProperties.$toast,
|
|
@@ -423,6 +522,22 @@ export class Kernel {
|
|
|
423
522
|
return this.hookRegistry
|
|
424
523
|
}
|
|
425
524
|
|
|
525
|
+
/**
|
|
526
|
+
* Get the DeferredRegistry instance
|
|
527
|
+
* @returns {import('../deferred/DeferredRegistry.js').DeferredRegistry}
|
|
528
|
+
*/
|
|
529
|
+
getDeferredRegistry() {
|
|
530
|
+
return this.deferred
|
|
531
|
+
}
|
|
532
|
+
|
|
533
|
+
/**
|
|
534
|
+
* Get the EventRouter instance
|
|
535
|
+
* @returns {import('./EventRouter.js').EventRouter|null}
|
|
536
|
+
*/
|
|
537
|
+
getEventRouter() {
|
|
538
|
+
return this.eventRouter
|
|
539
|
+
}
|
|
540
|
+
|
|
426
541
|
/**
|
|
427
542
|
* Get the layout components map
|
|
428
543
|
* @returns {object} Layout components by type
|
package/src/kernel/index.js
CHANGED
|
@@ -40,6 +40,8 @@ export class Orchestrator {
|
|
|
40
40
|
signals = null,
|
|
41
41
|
// HookRegistry instance for lifecycle hooks
|
|
42
42
|
hooks = null,
|
|
43
|
+
// DeferredRegistry instance for async warmup
|
|
44
|
+
deferred = null,
|
|
43
45
|
// Optional: AuthAdapter for entity permission checks (scope/silo)
|
|
44
46
|
entityAuthAdapter = null
|
|
45
47
|
} = options
|
|
@@ -48,6 +50,7 @@ export class Orchestrator {
|
|
|
48
50
|
this._managers = new Map()
|
|
49
51
|
this._signals = signals
|
|
50
52
|
this._hooks = hooks
|
|
53
|
+
this._deferred = deferred
|
|
51
54
|
this._entityAuthAdapter = entityAuthAdapter
|
|
52
55
|
|
|
53
56
|
// Register pre-provided managers
|
|
@@ -106,6 +109,22 @@ export class Orchestrator {
|
|
|
106
109
|
return this._hooks
|
|
107
110
|
}
|
|
108
111
|
|
|
112
|
+
/**
|
|
113
|
+
* Set the DeferredRegistry instance
|
|
114
|
+
* @param {DeferredRegistry} deferred
|
|
115
|
+
*/
|
|
116
|
+
setDeferred(deferred) {
|
|
117
|
+
this._deferred = deferred
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
/**
|
|
121
|
+
* Get the DeferredRegistry instance
|
|
122
|
+
* @returns {DeferredRegistry|null}
|
|
123
|
+
*/
|
|
124
|
+
get deferred() {
|
|
125
|
+
return this._deferred
|
|
126
|
+
}
|
|
127
|
+
|
|
109
128
|
/**
|
|
110
129
|
* Set the entity factory
|
|
111
130
|
* @param {function} factory - (entityName, entityConfig) => EntityManager
|
|
@@ -192,6 +211,47 @@ export class Orchestrator {
|
|
|
192
211
|
return Array.from(this._managers.keys())
|
|
193
212
|
}
|
|
194
213
|
|
|
214
|
+
/**
|
|
215
|
+
* Fire warmup for all managers (fire-and-forget)
|
|
216
|
+
*
|
|
217
|
+
* Triggers cache preloading for all managers with warmupEnabled.
|
|
218
|
+
* Each manager registers its warmup in the DeferredRegistry, allowing
|
|
219
|
+
* pages to await specific entities before rendering.
|
|
220
|
+
*
|
|
221
|
+
* IMPORTANT: This is fire-and-forget by design. Don't await this method.
|
|
222
|
+
* Components await what they need via DeferredRegistry:
|
|
223
|
+
* `await deferred.await('entity:books:cache')`
|
|
224
|
+
*
|
|
225
|
+
* @returns {Promise<Map<string, any>>} For debugging/logging only
|
|
226
|
+
*
|
|
227
|
+
* @example
|
|
228
|
+
* ```js
|
|
229
|
+
* // At boot (fire-and-forget)
|
|
230
|
+
* orchestrator.fireWarmups()
|
|
231
|
+
*
|
|
232
|
+
* // In component - await specific entity cache
|
|
233
|
+
* await deferred.await('entity:books:cache')
|
|
234
|
+
* const { items } = await booksManager.list() // Uses local cache
|
|
235
|
+
* ```
|
|
236
|
+
*/
|
|
237
|
+
fireWarmups() {
|
|
238
|
+
const results = new Map()
|
|
239
|
+
|
|
240
|
+
for (const [name, manager] of this._managers) {
|
|
241
|
+
if (manager.warmup && manager.warmupEnabled) {
|
|
242
|
+
// Fire each warmup - they register themselves in DeferredRegistry
|
|
243
|
+
manager.warmup().then(result => {
|
|
244
|
+
results.set(name, result)
|
|
245
|
+
}).catch(error => {
|
|
246
|
+
console.warn(`[Orchestrator] Warmup failed for ${name}:`, error.message)
|
|
247
|
+
results.set(name, error)
|
|
248
|
+
})
|
|
249
|
+
}
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
return results
|
|
253
|
+
}
|
|
254
|
+
|
|
195
255
|
/**
|
|
196
256
|
* Dispose all managers
|
|
197
257
|
*/
|
package/src/query/FilterQuery.js
CHANGED
|
@@ -115,9 +115,12 @@ export class FilterQuery {
|
|
|
115
115
|
/**
|
|
116
116
|
* Set the SignalBus for cache invalidation
|
|
117
117
|
*
|
|
118
|
-
*
|
|
119
|
-
*
|
|
120
|
-
*
|
|
118
|
+
* Subscribes to entity CRUD signals ({entity}:created, {entity}:updated, {entity}:deleted)
|
|
119
|
+
* to invalidate cached options when the source entity changes.
|
|
120
|
+
*
|
|
121
|
+
* Note: Auth changes are handled by Vue component lifecycle - when user logs out,
|
|
122
|
+
* router guard redirects to login, component unmounts, FilterQuery is disposed.
|
|
123
|
+
* On re-login, new FilterQuery is created with empty cache.
|
|
121
124
|
*
|
|
122
125
|
* @param {SignalBus} signals
|
|
123
126
|
* @returns {FilterQuery} this for chaining
|
|
@@ -128,8 +131,10 @@ export class FilterQuery {
|
|
|
128
131
|
|
|
129
132
|
this._signals = signals
|
|
130
133
|
|
|
134
|
+
if (!signals) return this
|
|
135
|
+
|
|
131
136
|
// Subscribe to entity CRUD signals for cache invalidation
|
|
132
|
-
if (this.source === 'entity' && this.entity
|
|
137
|
+
if (this.source === 'entity' && this.entity) {
|
|
133
138
|
const actions = ['created', 'updated', 'deleted']
|
|
134
139
|
for (const action of actions) {
|
|
135
140
|
const signalName = `${this.entity}:${action}`
|