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,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
|
+
})
|
package/src/entity/index.js
CHANGED
|
@@ -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
|
+
}
|