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.
@@ -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
+ }
@@ -0,0 +1,189 @@
1
+ import { describe, it, expect, vi } from 'vitest'
2
+ import {
3
+ managerFactory,
4
+ defaultManagerResolver,
5
+ createManagerFactory,
6
+ createManagers
7
+ } from './factory.js'
8
+ import { EntityManager } from './EntityManager.js'
9
+ import { ApiStorage } from './storage/ApiStorage.js'
10
+ import { MemoryStorage } from './storage/MemoryStorage.js'
11
+
12
+ describe('defaultManagerResolver', () => {
13
+ it('creates EntityManager with config', () => {
14
+ const storage = new MemoryStorage()
15
+ const config = { storage, label: 'Bot' }
16
+
17
+ const manager = defaultManagerResolver(config, 'bots')
18
+
19
+ expect(manager).toBeInstanceOf(EntityManager)
20
+ expect(manager.name).toBe('bots')
21
+ expect(manager.storage).toBe(storage)
22
+ })
23
+
24
+ it('uses registered manager class from registry', () => {
25
+ class CustomManager extends EntityManager {}
26
+ const storage = new MemoryStorage()
27
+ const config = { storage }
28
+ const context = { managerRegistry: { bots: CustomManager } }
29
+
30
+ const manager = defaultManagerResolver(config, 'bots', context)
31
+
32
+ expect(manager).toBeInstanceOf(CustomManager)
33
+ })
34
+
35
+ it('falls back to EntityManager when not in registry', () => {
36
+ const storage = new MemoryStorage()
37
+ const config = { storage }
38
+ const context = { managerRegistry: { other: class extends EntityManager {} } }
39
+
40
+ const manager = defaultManagerResolver(config, 'bots', context)
41
+
42
+ expect(manager).toBeInstanceOf(EntityManager)
43
+ expect(manager.constructor).toBe(EntityManager)
44
+ })
45
+ })
46
+
47
+ describe('managerFactory', () => {
48
+ it('returns EntityManager instance directly', () => {
49
+ const manager = new EntityManager({ name: 'test', storage: new MemoryStorage() })
50
+ const result = managerFactory(manager, 'test')
51
+ expect(result).toBe(manager)
52
+ })
53
+
54
+ it('returns duck-typed manager directly', () => {
55
+ const duckManager = {
56
+ storage: new MemoryStorage(),
57
+ list: () => Promise.resolve({ items: [], total: 0 }),
58
+ get: () => Promise.resolve(null)
59
+ }
60
+ const result = managerFactory(duckManager, 'test')
61
+ expect(result).toBe(duckManager)
62
+ })
63
+
64
+ it('creates manager from string pattern', () => {
65
+ const result = managerFactory('api:/api/bots', 'bots')
66
+ expect(result).toBeInstanceOf(EntityManager)
67
+ expect(result.storage).toBeInstanceOf(ApiStorage)
68
+ expect(result.storage.endpoint).toBe('/api/bots')
69
+ })
70
+
71
+ it('creates manager from config object with storage string', () => {
72
+ const result = managerFactory({
73
+ storage: 'api:/api/tasks',
74
+ label: 'Task'
75
+ }, 'tasks')
76
+
77
+ expect(result).toBeInstanceOf(EntityManager)
78
+ expect(result.storage).toBeInstanceOf(ApiStorage)
79
+ })
80
+
81
+ it('uses storage instance directly from config', () => {
82
+ const storage = new MemoryStorage()
83
+ const result = managerFactory({ storage }, 'items')
84
+
85
+ expect(result).toBeInstanceOf(EntityManager)
86
+ expect(result.storage).toBe(storage)
87
+ })
88
+
89
+ it('uses custom storageResolver', () => {
90
+ const customStorage = new MemoryStorage()
91
+ const storageResolver = vi.fn().mockReturnValue(customStorage)
92
+ const context = { storageResolver }
93
+
94
+ const result = managerFactory('api:/test', 'test', context)
95
+
96
+ expect(storageResolver).toHaveBeenCalled()
97
+ expect(result.storage).toBe(customStorage)
98
+ })
99
+
100
+ it('uses custom managerResolver', () => {
101
+ class CustomManager extends EntityManager {}
102
+ const managerResolver = vi.fn().mockImplementation((config, name) => {
103
+ return new CustomManager({ name, ...config })
104
+ })
105
+ const context = { managerResolver }
106
+
107
+ const result = managerFactory('api:/test', 'test', context)
108
+
109
+ expect(managerResolver).toHaveBeenCalled()
110
+ expect(result).toBeInstanceOf(CustomManager)
111
+ })
112
+
113
+ it('uses managerRegistry for generated classes', () => {
114
+ class BotManager extends EntityManager {}
115
+ const context = { managerRegistry: { bots: BotManager } }
116
+
117
+ const result = managerFactory('api:/api/bots', 'bots', context)
118
+
119
+ expect(result).toBeInstanceOf(BotManager)
120
+ })
121
+
122
+ it('throws for invalid config', () => {
123
+ expect(() => managerFactory(123, 'test'))
124
+ .toThrow('Invalid manager config')
125
+ })
126
+ })
127
+
128
+ describe('createManagerFactory', () => {
129
+ it('creates factory with bound context', () => {
130
+ class CustomManager extends EntityManager {}
131
+ const context = { managerRegistry: { bots: CustomManager } }
132
+ const factory = createManagerFactory(context)
133
+
134
+ const result = factory('api:/api/bots', 'bots')
135
+
136
+ expect(result).toBeInstanceOf(CustomManager)
137
+ })
138
+ })
139
+
140
+ describe('createManagers', () => {
141
+ it('creates managers from config object', () => {
142
+ const config = {
143
+ bots: 'api:/api/bots',
144
+ tasks: { storage: 'memory:tasks', label: 'Task' }
145
+ }
146
+
147
+ const managers = createManagers(config)
148
+
149
+ expect(managers.bots).toBeInstanceOf(EntityManager)
150
+ expect(managers.bots.storage).toBeInstanceOf(ApiStorage)
151
+ expect(managers.tasks).toBeInstanceOf(EntityManager)
152
+ expect(managers.tasks.storage).toBeInstanceOf(MemoryStorage)
153
+ })
154
+
155
+ it('uses context for all managers', () => {
156
+ class BotManager extends EntityManager {}
157
+ class TaskManager extends EntityManager {}
158
+ const context = {
159
+ managerRegistry: { bots: BotManager, tasks: TaskManager }
160
+ }
161
+ const config = {
162
+ bots: 'api:/api/bots',
163
+ tasks: 'api:/api/tasks'
164
+ }
165
+
166
+ const managers = createManagers(config, context)
167
+
168
+ expect(managers.bots).toBeInstanceOf(BotManager)
169
+ expect(managers.tasks).toBeInstanceOf(TaskManager)
170
+ })
171
+
172
+ it('passes through existing manager instances', () => {
173
+ const existingManager = new EntityManager({ name: 'existing', storage: new MemoryStorage() })
174
+ const config = {
175
+ bots: 'api:/api/bots',
176
+ existing: existingManager
177
+ }
178
+
179
+ const managers = createManagers(config)
180
+
181
+ expect(managers.existing).toBe(existingManager)
182
+ expect(managers.bots).toBeInstanceOf(EntityManager)
183
+ })
184
+
185
+ it('returns empty object for empty config', () => {
186
+ const managers = createManagers({})
187
+ expect(managers).toEqual({})
188
+ })
189
+ })
@@ -7,6 +7,14 @@
7
7
  // EntityManager
8
8
  export { EntityManager, createEntityManager } from './EntityManager.js'
9
9
 
10
+ // Manager Factory
11
+ export {
12
+ managerFactory,
13
+ defaultManagerResolver,
14
+ createManagerFactory,
15
+ createManagers
16
+ } from './factory.js'
17
+
10
18
  // Storage adapters
11
19
  export * from './storage/index.js'
12
20
 
@@ -1,3 +1,5 @@
1
+ import { IStorage } from './IStorage.js'
2
+
1
3
  /**
2
4
  * ApiStorage - REST API storage adapter
3
5
  *
@@ -20,7 +22,7 @@
20
22
  * })
21
23
  * ```
22
24
  */
23
- export class ApiStorage {
25
+ export class ApiStorage extends IStorage {
24
26
  /**
25
27
  * Storage capabilities declaration.
26
28
  * Describes what features this storage adapter supports.
@@ -44,6 +46,7 @@ export class ApiStorage {
44
46
  }
45
47
 
46
48
  constructor(options = {}) {
49
+ super()
47
50
  const {
48
51
  endpoint,
49
52
  client = null,
@@ -0,0 +1,76 @@
1
+ /**
2
+ * IStorage - Base class for storage adapters
3
+ *
4
+ * Provides instanceof checking for storageFactory.
5
+ * All storage adapters should extend this class.
6
+ *
7
+ * Usage:
8
+ * ```js
9
+ * class MyStorage extends IStorage {
10
+ * static capabilities = { supportsTotal: true, ... }
11
+ * async list(params) { ... }
12
+ * async get(id) { ... }
13
+ * async create(data) { ... }
14
+ * async update(id, data) { ... }
15
+ * async delete(id) { ... }
16
+ * }
17
+ * ```
18
+ */
19
+ export class IStorage {
20
+ /**
21
+ * Default capabilities - override in subclass
22
+ * @type {import('./index.js').StorageCapabilities}
23
+ */
24
+ static capabilities = {
25
+ supportsTotal: false,
26
+ supportsFilters: false,
27
+ supportsPagination: false,
28
+ supportsCaching: false
29
+ }
30
+
31
+ /**
32
+ * List entities with optional filtering/pagination
33
+ * @param {object} params - Query parameters
34
+ * @returns {Promise<{items: Array, total?: number}>}
35
+ */
36
+ async list(params = {}) {
37
+ throw new Error('list() not implemented')
38
+ }
39
+
40
+ /**
41
+ * Get single entity by ID
42
+ * @param {string} id - Entity ID
43
+ * @returns {Promise<object|null>}
44
+ */
45
+ async get(id) {
46
+ throw new Error('get() not implemented')
47
+ }
48
+
49
+ /**
50
+ * Create new entity
51
+ * @param {object} data - Entity data
52
+ * @returns {Promise<object>} Created entity with ID
53
+ */
54
+ async create(data) {
55
+ throw new Error('create() not implemented')
56
+ }
57
+
58
+ /**
59
+ * Update entity by ID
60
+ * @param {string} id - Entity ID
61
+ * @param {object} data - Updated data
62
+ * @returns {Promise<object>} Updated entity
63
+ */
64
+ async update(id, data) {
65
+ throw new Error('update() not implemented')
66
+ }
67
+
68
+ /**
69
+ * Delete entity by ID
70
+ * @param {string} id - Entity ID
71
+ * @returns {Promise<void>}
72
+ */
73
+ async delete(id) {
74
+ throw new Error('delete() not implemented')
75
+ }
76
+ }
@@ -1,3 +1,5 @@
1
+ import { IStorage } from './IStorage.js'
2
+
1
3
  /**
2
4
  * LocalStorage - Browser localStorage storage adapter
3
5
  *
@@ -17,7 +19,7 @@
17
19
  * })
18
20
  * ```
19
21
  */
20
- export class LocalStorage {
22
+ export class LocalStorage extends IStorage {
21
23
  /**
22
24
  * Storage capabilities declaration.
23
25
  * Describes what features this storage adapter supports.
@@ -47,6 +49,7 @@ export class LocalStorage {
47
49
  }
48
50
 
49
51
  constructor(options = {}) {
52
+ super()
50
53
  const {
51
54
  key,
52
55
  idField = 'id',
@@ -1,3 +1,5 @@
1
+ import { IStorage } from './IStorage.js'
2
+
1
3
  /**
2
4
  * MemoryStorage - In-memory storage adapter
3
5
  *
@@ -17,7 +19,7 @@
17
19
  * })
18
20
  * ```
19
21
  */
20
- export class MemoryStorage {
22
+ export class MemoryStorage extends IStorage {
21
23
  /**
22
24
  * Storage capabilities declaration.
23
25
  * Describes what features this storage adapter supports.
@@ -47,6 +49,7 @@ export class MemoryStorage {
47
49
  }
48
50
 
49
51
  constructor(options = {}) {
52
+ super()
50
53
  const {
51
54
  idField = 'id',
52
55
  generateId = () => Date.now().toString(36) + Math.random().toString(36).substr(2),
@@ -1,3 +1,5 @@
1
+ import { IStorage } from './IStorage.js'
2
+
1
3
  /**
2
4
  * MockApiStorage - In-memory storage with localStorage persistence
3
5
  *
@@ -18,7 +20,7 @@
18
20
  *
19
21
  * localStorage key pattern: mockapi_${entityName}_data
20
22
  */
21
- export class MockApiStorage {
23
+ export class MockApiStorage extends IStorage {
22
24
  /**
23
25
  * Storage capabilities declaration.
24
26
  * Describes what features this storage adapter supports.
@@ -48,6 +50,7 @@ export class MockApiStorage {
48
50
  }
49
51
 
50
52
  constructor(options = {}) {
53
+ super()
51
54
  const {
52
55
  entityName,
53
56
  idField = 'id',
@@ -1,3 +1,5 @@
1
+ import { IStorage } from './IStorage.js'
2
+
1
3
  /**
2
4
  * SdkStorage - hey-api SDK storage adapter
3
5
  *
@@ -93,7 +95,7 @@
93
95
  * })
94
96
  * ```
95
97
  */
96
- export class SdkStorage {
98
+ export class SdkStorage extends IStorage {
97
99
  /**
98
100
  * Storage capabilities declaration
99
101
  * @type {import('./index.js').StorageCapabilities}
@@ -141,6 +143,7 @@ export class SdkStorage {
141
143
  * @param {boolean} [options.clientSidePagination=false] - Enable client-side pagination for list()
142
144
  */
143
145
  constructor(options = {}) {
146
+ super()
144
147
  const {
145
148
  sdk = null,
146
149
  getSdk = null,