qdadm 0.26.3 → 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/components/index.js +3 -0
- package/src/components/pages/LoginPage.vue +267 -0
- package/src/composables/index.js +2 -0
- package/src/composables/useDeferred.js +85 -0
- package/src/composables/useListPageBuilder.js +3 -0
- package/src/composables/useSSE.js +212 -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
|
@@ -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
|
+
}
|
|
@@ -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
|
|
1044
|
+
* Set up signal listeners for cache invalidation
|
|
1043
1045
|
*
|
|
1044
|
-
*
|
|
1045
|
-
*
|
|
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
|
|
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
|
-
|
|
1059
|
-
const parentEntities = Object.values(this._parents).map(p => p.entity)
|
|
1059
|
+
const cleanups = []
|
|
1060
1060
|
|
|
1061
|
-
// Listen for parent cache invalidation
|
|
1062
|
-
this.
|
|
1063
|
-
|
|
1064
|
-
|
|
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
|
*
|
|
@@ -0,0 +1,155 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Manager Factory/Resolver Pattern
|
|
3
|
+
*
|
|
4
|
+
* Enables declarative manager configuration with auto-resolution.
|
|
5
|
+
* Works with storageFactory for complete auto-wiring.
|
|
6
|
+
*
|
|
7
|
+
* Usage:
|
|
8
|
+
* ```js
|
|
9
|
+
* // String pattern → factory creates manager with storage
|
|
10
|
+
* managerFactory('api:/api/bots', 'bots', context) // → EntityManager + ApiStorage
|
|
11
|
+
*
|
|
12
|
+
* // Config object → factory normalizes, resolver creates
|
|
13
|
+
* managerFactory({ storage: 'api:/api/bots', label: 'Bot' }, 'bots', context)
|
|
14
|
+
*
|
|
15
|
+
* // Manager instance → returned directly
|
|
16
|
+
* managerFactory(myManagerInstance, 'bots', context) // → myManagerInstance
|
|
17
|
+
* ```
|
|
18
|
+
*/
|
|
19
|
+
|
|
20
|
+
import { EntityManager } from './EntityManager.js'
|
|
21
|
+
import { storageFactory } from './storage/factory.js'
|
|
22
|
+
|
|
23
|
+
/**
|
|
24
|
+
* Default manager resolver - creates manager instance from config
|
|
25
|
+
*
|
|
26
|
+
* Override this via Kernel config for custom manager classes.
|
|
27
|
+
*
|
|
28
|
+
* @param {object} config - Normalized manager config with resolved storage
|
|
29
|
+
* @param {string} entityName - Entity name
|
|
30
|
+
* @param {object} context - Context with managerRegistry
|
|
31
|
+
* @returns {EntityManager} Manager instance
|
|
32
|
+
*/
|
|
33
|
+
export function defaultManagerResolver(config, entityName, context = {}) {
|
|
34
|
+
const { managerRegistry = {} } = context
|
|
35
|
+
|
|
36
|
+
// Look up registered manager class (e.g., from qdadm-gen)
|
|
37
|
+
const ManagerClass = managerRegistry[entityName] || EntityManager
|
|
38
|
+
|
|
39
|
+
// Ensure name is set
|
|
40
|
+
const managerConfig = {
|
|
41
|
+
name: entityName,
|
|
42
|
+
...config
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
return new ManagerClass(managerConfig)
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
/**
|
|
49
|
+
* Manager factory - normalizes input and delegates to resolver
|
|
50
|
+
*
|
|
51
|
+
* Handles:
|
|
52
|
+
* - EntityManager instance → return directly
|
|
53
|
+
* - String pattern 'type:endpoint' → create storage, then manager
|
|
54
|
+
* - Config object → resolve storage, then manager
|
|
55
|
+
*
|
|
56
|
+
* @param {EntityManager | string | object} config - Manager config
|
|
57
|
+
* @param {string} entityName - Entity name
|
|
58
|
+
* @param {object} context - Context with storageFactory, storageResolver, managerResolver, managerRegistry
|
|
59
|
+
* @returns {EntityManager} Manager instance
|
|
60
|
+
*
|
|
61
|
+
* @example
|
|
62
|
+
* // Instance passthrough
|
|
63
|
+
* managerFactory(myManager, 'bots', ctx) // → myManager
|
|
64
|
+
*
|
|
65
|
+
* // String pattern (shorthand for storage)
|
|
66
|
+
* managerFactory('api:/api/bots', 'bots', ctx) // → EntityManager + ApiStorage
|
|
67
|
+
*
|
|
68
|
+
* // Config object
|
|
69
|
+
* managerFactory({
|
|
70
|
+
* storage: 'api:/api/bots',
|
|
71
|
+
* label: 'Bot',
|
|
72
|
+
* fields: { name: { type: 'text' } }
|
|
73
|
+
* }, 'bots', ctx)
|
|
74
|
+
*/
|
|
75
|
+
export function managerFactory(config, entityName, context = {}) {
|
|
76
|
+
const {
|
|
77
|
+
storageResolver,
|
|
78
|
+
managerResolver = defaultManagerResolver
|
|
79
|
+
} = context
|
|
80
|
+
|
|
81
|
+
// Already a Manager instance → return directly
|
|
82
|
+
if (config instanceof EntityManager) {
|
|
83
|
+
return config
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
// Also check for duck-typed manager (has storage property or list/get methods)
|
|
87
|
+
if (config && typeof config === 'object' && config.storage && typeof config.list === 'function') {
|
|
88
|
+
return config
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
// String pattern → treat as storage config, create default manager
|
|
92
|
+
if (typeof config === 'string') {
|
|
93
|
+
const storage = storageFactory(config, entityName, storageResolver)
|
|
94
|
+
return managerResolver({ storage }, entityName, context)
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
// Config object → resolve storage first, then manager
|
|
98
|
+
if (config && typeof config === 'object') {
|
|
99
|
+
let resolvedConfig = { ...config }
|
|
100
|
+
|
|
101
|
+
// Resolve storage if provided as string/config
|
|
102
|
+
if (config.storage && !(config.storage instanceof Object && typeof config.storage.list === 'function')) {
|
|
103
|
+
resolvedConfig.storage = storageFactory(config.storage, entityName, storageResolver)
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
return managerResolver(resolvedConfig, entityName, context)
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
throw new Error(`Invalid manager config for "${entityName}": ${typeof config}. Expected string, object, or Manager instance.`)
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
/**
|
|
113
|
+
* Create a custom manager factory with context
|
|
114
|
+
*
|
|
115
|
+
* @param {object} context - Context with storageResolver, managerResolver, managerRegistry
|
|
116
|
+
* @returns {function} Manager factory with bound context
|
|
117
|
+
*
|
|
118
|
+
* @example
|
|
119
|
+
* const myFactory = createManagerFactory({
|
|
120
|
+
* managerRegistry: { bots: BotManager },
|
|
121
|
+
* managerResolver: (config, name, ctx) => {
|
|
122
|
+
* // Custom logic
|
|
123
|
+
* return defaultManagerResolver(config, name, ctx)
|
|
124
|
+
* }
|
|
125
|
+
* })
|
|
126
|
+
*
|
|
127
|
+
* const botsManager = myFactory('api:/api/bots', 'bots')
|
|
128
|
+
*/
|
|
129
|
+
export function createManagerFactory(context) {
|
|
130
|
+
return (config, entityName) => managerFactory(config, entityName, context)
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
/**
|
|
134
|
+
* Create all managers from a config object
|
|
135
|
+
*
|
|
136
|
+
* @param {object} managersConfig - { entityName: config, ... }
|
|
137
|
+
* @param {object} context - Factory context
|
|
138
|
+
* @returns {object} { entityName: managerInstance, ... }
|
|
139
|
+
*
|
|
140
|
+
* @example
|
|
141
|
+
* const managers = createManagers({
|
|
142
|
+
* bots: 'api:/api/bots',
|
|
143
|
+
* tasks: { storage: 'api:/api/tasks', label: 'Task' },
|
|
144
|
+
* settings: new SettingsManager({...})
|
|
145
|
+
* }, { managerRegistry })
|
|
146
|
+
*/
|
|
147
|
+
export function createManagers(managersConfig, context = {}) {
|
|
148
|
+
const managers = {}
|
|
149
|
+
|
|
150
|
+
for (const [entityName, config] of Object.entries(managersConfig)) {
|
|
151
|
+
managers[entityName] = managerFactory(config, entityName, context)
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
return managers
|
|
155
|
+
}
|