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.
@@ -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,
@@ -0,0 +1,193 @@
1
+ /**
2
+ * Storage Factory/Resolver Pattern
3
+ *
4
+ * Enables declarative storage configuration with auto-resolution.
5
+ *
6
+ * Usage:
7
+ * ```js
8
+ * // String pattern → factory parses, resolver creates
9
+ * storageFactory('api:/api/bots', 'bots') // → ApiStorage
10
+ * storageFactory('local:settings', 'settings') // → LocalStorage
11
+ * storageFactory('mock:books', 'books') // → MockApiStorage
12
+ *
13
+ * // Config object → factory normalizes, resolver creates
14
+ * storageFactory({ endpoint: '/api/bots' }, 'bots') // → ApiStorage
15
+ * storageFactory({ type: 'local', key: 'my-key' }, 'settings') // → LocalStorage
16
+ *
17
+ * // Storage instance → returned directly
18
+ * storageFactory(myStorageInstance, 'bots') // → myStorageInstance
19
+ * ```
20
+ */
21
+
22
+ import { IStorage } from './IStorage.js'
23
+ import { ApiStorage } from './ApiStorage.js'
24
+ import { LocalStorage } from './LocalStorage.js'
25
+ import { MemoryStorage } from './MemoryStorage.js'
26
+ import { MockApiStorage } from './MockApiStorage.js'
27
+ import { SdkStorage } from './SdkStorage.js'
28
+
29
+ /**
30
+ * Storage type registry - maps type names to storage classes
31
+ * @type {Record<string, typeof IStorage>}
32
+ */
33
+ export const storageTypes = {
34
+ api: ApiStorage,
35
+ local: LocalStorage,
36
+ memory: MemoryStorage,
37
+ mock: MockApiStorage,
38
+ sdk: SdkStorage
39
+ }
40
+
41
+ /**
42
+ * Parse string pattern 'type:endpoint' into config object
43
+ *
44
+ * @param {string} pattern - Storage pattern (e.g., 'api:/api/bots', 'local:settings')
45
+ * @returns {{type: string, endpoint?: string, key?: string} | null} Parsed config or null if invalid
46
+ *
47
+ * @example
48
+ * parseStoragePattern('api:/api/bots') // → { type: 'api', endpoint: '/api/bots' }
49
+ * parseStoragePattern('local:settings') // → { type: 'local', key: 'settings' }
50
+ * parseStoragePattern('/api/bots') // → { type: 'api', endpoint: '/api/bots' }
51
+ */
52
+ export function parseStoragePattern(pattern) {
53
+ if (typeof pattern !== 'string') return null
54
+
55
+ // Pattern: 'type:value'
56
+ const match = pattern.match(/^(\w+):(.+)$/)
57
+ if (match) {
58
+ const [, type, value] = match
59
+ // For local/mock, value is key/name; for api/sdk, value is endpoint
60
+ if (type === 'local' || type === 'memory') {
61
+ return { type, key: value }
62
+ }
63
+ if (type === 'mock') {
64
+ return { type, entityName: value }
65
+ }
66
+ return { type, endpoint: value }
67
+ }
68
+
69
+ // Plain string starting with '/' → assume API endpoint
70
+ if (pattern.startsWith('/')) {
71
+ return { type: 'api', endpoint: pattern }
72
+ }
73
+
74
+ return null
75
+ }
76
+
77
+ /**
78
+ * Default storage resolver - creates storage instance from config
79
+ *
80
+ * Override this via Kernel config for custom storage types.
81
+ *
82
+ * @param {object} config - Normalized storage config with `type` property
83
+ * @param {string} entityName - Entity name (used for key generation)
84
+ * @returns {IStorage} Storage instance
85
+ *
86
+ * @example
87
+ * defaultStorageResolver({ type: 'api', endpoint: '/api/bots' }, 'bots')
88
+ * defaultStorageResolver({ type: 'local', key: 'settings' }, 'settings')
89
+ */
90
+ export function defaultStorageResolver(config, entityName) {
91
+ const { type, ...rest } = config
92
+
93
+ const StorageClass = storageTypes[type]
94
+ if (!StorageClass) {
95
+ throw new Error(`Unknown storage type: "${type}". Available: ${Object.keys(storageTypes).join(', ')}`)
96
+ }
97
+
98
+ // Add entityName to config if not present (useful for mock/memory)
99
+ if (!rest.entityName && !rest.key) {
100
+ if (type === 'local' || type === 'memory') {
101
+ rest.key = entityName
102
+ } else if (type === 'mock') {
103
+ rest.entityName = entityName
104
+ }
105
+ }
106
+
107
+ return new StorageClass(rest)
108
+ }
109
+
110
+ /**
111
+ * Storage factory - normalizes input and delegates to resolver
112
+ *
113
+ * Handles:
114
+ * - IStorage instance → return directly
115
+ * - String pattern 'type:endpoint' → parse and resolve
116
+ * - Plain string '/endpoint' → treat as API endpoint
117
+ * - Config object → normalize and resolve
118
+ *
119
+ * @param {IStorage | string | object} config - Storage config (instance, pattern, or object)
120
+ * @param {string} entityName - Entity name for key generation
121
+ * @param {function} [resolver=defaultStorageResolver] - Custom resolver function
122
+ * @returns {IStorage} Storage instance
123
+ *
124
+ * @example
125
+ * // Instance passthrough
126
+ * storageFactory(myStorage, 'bots') // → myStorage
127
+ *
128
+ * // String patterns
129
+ * storageFactory('api:/api/bots', 'bots') // → ApiStorage
130
+ * storageFactory('/api/bots', 'bots') // → ApiStorage (default type)
131
+ * storageFactory('local:settings', 'settings') // → LocalStorage
132
+ *
133
+ * // Config objects
134
+ * storageFactory({ endpoint: '/api/bots' }, 'bots') // → ApiStorage
135
+ * storageFactory({ type: 'local' }, 'settings') // → LocalStorage
136
+ */
137
+ export function storageFactory(config, entityName, resolver = defaultStorageResolver) {
138
+ // Already a Storage instance → return directly
139
+ if (config instanceof IStorage) {
140
+ return config
141
+ }
142
+
143
+ // Also check for duck-typed storage (has list method and isn't a class)
144
+ if (config && typeof config === 'object' && typeof config.list === 'function' && typeof config.get === 'function') {
145
+ return config
146
+ }
147
+
148
+ // String pattern → parse
149
+ if (typeof config === 'string') {
150
+ const parsed = parseStoragePattern(config)
151
+ if (!parsed) {
152
+ throw new Error(`Invalid storage pattern: "${config}". Use 'type:value' or '/api/endpoint'`)
153
+ }
154
+ return resolver(parsed, entityName)
155
+ }
156
+
157
+ // Config object → normalize
158
+ if (config && typeof config === 'object') {
159
+ // If no type, infer from properties
160
+ if (!config.type) {
161
+ if (config.endpoint) {
162
+ config = { type: 'api', ...config }
163
+ } else if (config.key) {
164
+ config = { type: 'local', ...config }
165
+ } else if (config.initialData) {
166
+ config = { type: 'mock', ...config }
167
+ } else {
168
+ throw new Error(`Cannot infer storage type from config: ${JSON.stringify(config)}. Add 'type' property.`)
169
+ }
170
+ }
171
+ return resolver(config, entityName)
172
+ }
173
+
174
+ throw new Error(`Invalid storage config: ${typeof config}. Expected string, object, or Storage instance.`)
175
+ }
176
+
177
+ /**
178
+ * Create a custom storage factory with overridden resolver
179
+ *
180
+ * @param {function} customResolver - Custom resolver that handles additional types
181
+ * @returns {function} Storage factory with custom resolver
182
+ *
183
+ * @example
184
+ * const myFactory = createStorageFactory((config, entityName) => {
185
+ * if (config.type === 'graphql') {
186
+ * return new GraphQLStorage(config)
187
+ * }
188
+ * return defaultStorageResolver(config, entityName)
189
+ * })
190
+ */
191
+ export function createStorageFactory(customResolver) {
192
+ return (config, entityName) => storageFactory(config, entityName, customResolver)
193
+ }