qdadm 0.27.0 → 0.29.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 +4 -2
- 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/gen/FieldMapper.js +116 -0
- package/src/gen/StorageProfileFactory.js +109 -0
- package/src/gen/connectors/BaseConnector.js +142 -0
- package/src/gen/connectors/ManualConnector.js +385 -0
- package/src/gen/connectors/ManualConnector.test.js +499 -0
- package/src/gen/connectors/OpenAPIConnector.js +568 -0
- package/src/gen/connectors/OpenAPIConnector.test.js +737 -0
- package/src/gen/connectors/__fixtures__/sample-openapi.json +311 -0
- package/src/gen/connectors/index.js +11 -0
- package/src/gen/createManagers.js +224 -0
- package/src/gen/decorators.js +129 -0
- package/src/gen/generateManagers.js +266 -0
- package/src/gen/generateManagers.test.js +358 -0
- package/src/gen/index.js +45 -0
- package/src/gen/schema.js +221 -0
- package/src/gen/vite-plugin.js +105 -0
- package/src/generated/managers/testManager.js +45 -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,311 @@
|
|
|
1
|
+
{
|
|
2
|
+
"openapi": "3.0.0",
|
|
3
|
+
"info": {
|
|
4
|
+
"title": "Sample API",
|
|
5
|
+
"version": "1.0.0"
|
|
6
|
+
},
|
|
7
|
+
"paths": {
|
|
8
|
+
"/api/users": {
|
|
9
|
+
"get": {
|
|
10
|
+
"summary": "List users",
|
|
11
|
+
"responses": {
|
|
12
|
+
"200": {
|
|
13
|
+
"description": "Success",
|
|
14
|
+
"content": {
|
|
15
|
+
"application/json": {
|
|
16
|
+
"schema": {
|
|
17
|
+
"type": "object",
|
|
18
|
+
"properties": {
|
|
19
|
+
"data": {
|
|
20
|
+
"type": "array",
|
|
21
|
+
"items": {
|
|
22
|
+
"$ref": "#/components/schemas/User"
|
|
23
|
+
}
|
|
24
|
+
}
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
},
|
|
32
|
+
"post": {
|
|
33
|
+
"summary": "Create user",
|
|
34
|
+
"requestBody": {
|
|
35
|
+
"content": {
|
|
36
|
+
"application/json": {
|
|
37
|
+
"schema": {
|
|
38
|
+
"$ref": "#/components/schemas/CreateUserRequest"
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
},
|
|
43
|
+
"responses": {
|
|
44
|
+
"201": {
|
|
45
|
+
"description": "Created",
|
|
46
|
+
"content": {
|
|
47
|
+
"application/json": {
|
|
48
|
+
"schema": {
|
|
49
|
+
"type": "object",
|
|
50
|
+
"properties": {
|
|
51
|
+
"data": {
|
|
52
|
+
"$ref": "#/components/schemas/User"
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
},
|
|
62
|
+
"/api/users/{id}": {
|
|
63
|
+
"get": {
|
|
64
|
+
"summary": "Get user by ID",
|
|
65
|
+
"responses": {
|
|
66
|
+
"200": {
|
|
67
|
+
"description": "Success",
|
|
68
|
+
"content": {
|
|
69
|
+
"application/json": {
|
|
70
|
+
"schema": {
|
|
71
|
+
"type": "object",
|
|
72
|
+
"properties": {
|
|
73
|
+
"data": {
|
|
74
|
+
"$ref": "#/components/schemas/User"
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
},
|
|
84
|
+
"/api/posts": {
|
|
85
|
+
"get": {
|
|
86
|
+
"summary": "List posts",
|
|
87
|
+
"responses": {
|
|
88
|
+
"200": {
|
|
89
|
+
"description": "Success",
|
|
90
|
+
"content": {
|
|
91
|
+
"application/json": {
|
|
92
|
+
"schema": {
|
|
93
|
+
"type": "object",
|
|
94
|
+
"properties": {
|
|
95
|
+
"data": {
|
|
96
|
+
"type": "array",
|
|
97
|
+
"items": {
|
|
98
|
+
"$ref": "#/components/schemas/Post"
|
|
99
|
+
}
|
|
100
|
+
},
|
|
101
|
+
"pagination": {
|
|
102
|
+
"type": "object",
|
|
103
|
+
"properties": {
|
|
104
|
+
"page": { "type": "integer" },
|
|
105
|
+
"total": { "type": "integer" }
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
},
|
|
116
|
+
"/api/posts/{id}": {
|
|
117
|
+
"get": {
|
|
118
|
+
"summary": "Get post by ID",
|
|
119
|
+
"responses": {
|
|
120
|
+
"200": {
|
|
121
|
+
"description": "Success",
|
|
122
|
+
"content": {
|
|
123
|
+
"application/json": {
|
|
124
|
+
"schema": {
|
|
125
|
+
"type": "object",
|
|
126
|
+
"properties": {
|
|
127
|
+
"data": {
|
|
128
|
+
"$ref": "#/components/schemas/Post"
|
|
129
|
+
}
|
|
130
|
+
}
|
|
131
|
+
}
|
|
132
|
+
}
|
|
133
|
+
}
|
|
134
|
+
}
|
|
135
|
+
}
|
|
136
|
+
}
|
|
137
|
+
},
|
|
138
|
+
"/api/categories": {
|
|
139
|
+
"get": {
|
|
140
|
+
"summary": "List categories",
|
|
141
|
+
"responses": {
|
|
142
|
+
"200": {
|
|
143
|
+
"description": "Success",
|
|
144
|
+
"content": {
|
|
145
|
+
"application/json": {
|
|
146
|
+
"schema": {
|
|
147
|
+
"type": "object",
|
|
148
|
+
"properties": {
|
|
149
|
+
"data": {
|
|
150
|
+
"type": "array",
|
|
151
|
+
"items": {
|
|
152
|
+
"$ref": "#/components/schemas/Category"
|
|
153
|
+
}
|
|
154
|
+
}
|
|
155
|
+
}
|
|
156
|
+
}
|
|
157
|
+
}
|
|
158
|
+
}
|
|
159
|
+
}
|
|
160
|
+
}
|
|
161
|
+
}
|
|
162
|
+
},
|
|
163
|
+
"/internal/metrics": {
|
|
164
|
+
"get": {
|
|
165
|
+
"summary": "Internal metrics",
|
|
166
|
+
"tags": ["internal"],
|
|
167
|
+
"responses": {
|
|
168
|
+
"200": {
|
|
169
|
+
"description": "Success",
|
|
170
|
+
"content": {
|
|
171
|
+
"application/json": {
|
|
172
|
+
"schema": {
|
|
173
|
+
"type": "object"
|
|
174
|
+
}
|
|
175
|
+
}
|
|
176
|
+
}
|
|
177
|
+
}
|
|
178
|
+
}
|
|
179
|
+
}
|
|
180
|
+
}
|
|
181
|
+
},
|
|
182
|
+
"components": {
|
|
183
|
+
"schemas": {
|
|
184
|
+
"User": {
|
|
185
|
+
"type": "object",
|
|
186
|
+
"required": ["email"],
|
|
187
|
+
"properties": {
|
|
188
|
+
"id": {
|
|
189
|
+
"type": "integer",
|
|
190
|
+
"readOnly": true,
|
|
191
|
+
"description": "User ID"
|
|
192
|
+
},
|
|
193
|
+
"email": {
|
|
194
|
+
"type": "string",
|
|
195
|
+
"format": "email",
|
|
196
|
+
"description": "Email address"
|
|
197
|
+
},
|
|
198
|
+
"name": {
|
|
199
|
+
"type": "string",
|
|
200
|
+
"description": "Full name"
|
|
201
|
+
},
|
|
202
|
+
"created_at": {
|
|
203
|
+
"type": "string",
|
|
204
|
+
"format": "date-time",
|
|
205
|
+
"readOnly": true,
|
|
206
|
+
"description": "Creation date"
|
|
207
|
+
},
|
|
208
|
+
"website": {
|
|
209
|
+
"type": "string",
|
|
210
|
+
"format": "uri",
|
|
211
|
+
"description": "Personal website"
|
|
212
|
+
},
|
|
213
|
+
"uuid": {
|
|
214
|
+
"type": "string",
|
|
215
|
+
"format": "uuid",
|
|
216
|
+
"description": "Unique identifier"
|
|
217
|
+
},
|
|
218
|
+
"active": {
|
|
219
|
+
"type": "boolean",
|
|
220
|
+
"default": true,
|
|
221
|
+
"description": "Is active"
|
|
222
|
+
},
|
|
223
|
+
"role": {
|
|
224
|
+
"type": "string",
|
|
225
|
+
"enum": ["admin", "user", "guest"],
|
|
226
|
+
"default": "user",
|
|
227
|
+
"description": "User role"
|
|
228
|
+
},
|
|
229
|
+
"tags": {
|
|
230
|
+
"type": "array",
|
|
231
|
+
"items": { "type": "string" },
|
|
232
|
+
"description": "User tags"
|
|
233
|
+
},
|
|
234
|
+
"metadata": {
|
|
235
|
+
"type": "object",
|
|
236
|
+
"description": "Extra metadata"
|
|
237
|
+
},
|
|
238
|
+
"profile": {
|
|
239
|
+
"type": "object",
|
|
240
|
+
"properties": {
|
|
241
|
+
"bio": { "type": "string" },
|
|
242
|
+
"avatar": { "type": "string", "format": "uri" }
|
|
243
|
+
}
|
|
244
|
+
}
|
|
245
|
+
}
|
|
246
|
+
},
|
|
247
|
+
"CreateUserRequest": {
|
|
248
|
+
"type": "object",
|
|
249
|
+
"required": ["email", "name"],
|
|
250
|
+
"properties": {
|
|
251
|
+
"email": {
|
|
252
|
+
"type": "string",
|
|
253
|
+
"format": "email"
|
|
254
|
+
},
|
|
255
|
+
"name": {
|
|
256
|
+
"type": "string"
|
|
257
|
+
},
|
|
258
|
+
"password": {
|
|
259
|
+
"type": "string",
|
|
260
|
+
"format": "password"
|
|
261
|
+
}
|
|
262
|
+
}
|
|
263
|
+
},
|
|
264
|
+
"Post": {
|
|
265
|
+
"type": "object",
|
|
266
|
+
"required": ["title"],
|
|
267
|
+
"properties": {
|
|
268
|
+
"id": {
|
|
269
|
+
"type": "integer",
|
|
270
|
+
"readOnly": true
|
|
271
|
+
},
|
|
272
|
+
"title": {
|
|
273
|
+
"type": "string",
|
|
274
|
+
"description": "Post title"
|
|
275
|
+
},
|
|
276
|
+
"content": {
|
|
277
|
+
"type": "string"
|
|
278
|
+
},
|
|
279
|
+
"author_id": {
|
|
280
|
+
"type": "integer",
|
|
281
|
+
"description": "Author reference"
|
|
282
|
+
},
|
|
283
|
+
"published_at": {
|
|
284
|
+
"type": "string",
|
|
285
|
+
"format": "date"
|
|
286
|
+
},
|
|
287
|
+
"status": {
|
|
288
|
+
"type": "string",
|
|
289
|
+
"enum": ["draft", "published", "archived"],
|
|
290
|
+
"default": "draft"
|
|
291
|
+
}
|
|
292
|
+
}
|
|
293
|
+
},
|
|
294
|
+
"Category": {
|
|
295
|
+
"type": "object",
|
|
296
|
+
"properties": {
|
|
297
|
+
"id": {
|
|
298
|
+
"type": "integer",
|
|
299
|
+
"readOnly": true
|
|
300
|
+
},
|
|
301
|
+
"name": {
|
|
302
|
+
"type": "string"
|
|
303
|
+
},
|
|
304
|
+
"slug": {
|
|
305
|
+
"type": "string"
|
|
306
|
+
}
|
|
307
|
+
}
|
|
308
|
+
}
|
|
309
|
+
}
|
|
310
|
+
}
|
|
311
|
+
}
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Schema Connectors
|
|
3
|
+
*
|
|
4
|
+
* Pluggable connectors that parse various schema sources into UnifiedEntitySchema format.
|
|
5
|
+
*
|
|
6
|
+
* @module gen/connectors
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
export { BaseConnector } from './BaseConnector.js'
|
|
10
|
+
export { ManualConnector } from './ManualConnector.js'
|
|
11
|
+
export { OpenAPIConnector } from './OpenAPIConnector.js'
|
|
@@ -0,0 +1,224 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Runtime Factory for EntityManager Creation
|
|
3
|
+
*
|
|
4
|
+
* Creates EntityManager instances on-the-fly from configuration.
|
|
5
|
+
* This is the core factory function that wires schemas, storage profiles,
|
|
6
|
+
* and decorators together at runtime.
|
|
7
|
+
*
|
|
8
|
+
* @module gen/createManagers
|
|
9
|
+
*/
|
|
10
|
+
|
|
11
|
+
import { EntityManager } from '../entity/EntityManager.js'
|
|
12
|
+
import { applyDecorators } from './decorators.js'
|
|
13
|
+
|
|
14
|
+
/**
|
|
15
|
+
* Entity configuration in the config.entities map
|
|
16
|
+
*
|
|
17
|
+
* @typedef {object} EntityConfig
|
|
18
|
+
* @property {string} schema - Schema source name (key in config.schemas)
|
|
19
|
+
* @property {string} storage - Storage profile name (key in config.storages)
|
|
20
|
+
* @property {string} endpoint - API endpoint path (e.g., '/users')
|
|
21
|
+
* @property {import('./StorageProfileFactory.js').StorageProfileOptions} [options] - Per-entity storage options
|
|
22
|
+
*/
|
|
23
|
+
|
|
24
|
+
/**
|
|
25
|
+
* Configuration for createManagers factory
|
|
26
|
+
*
|
|
27
|
+
* @typedef {object} CreateManagersConfig
|
|
28
|
+
* @property {Record<string, import('./schema.js').UnifiedEntitySchema[]>} schemas - Schema sources (connector results) keyed by name
|
|
29
|
+
* @property {Record<string, import('./StorageProfileFactory.js').StorageProfileFactory>} storages - Storage profile factories keyed by name
|
|
30
|
+
* @property {Record<string, EntityConfig>} entities - Entity configurations keyed by entity name
|
|
31
|
+
* @property {Record<string, object>} [decorators] - Per-entity decorators (fields, labels, permissions)
|
|
32
|
+
*/
|
|
33
|
+
|
|
34
|
+
/**
|
|
35
|
+
* Validate config structure early with clear error messages
|
|
36
|
+
*
|
|
37
|
+
* @param {CreateManagersConfig} config - Configuration to validate
|
|
38
|
+
* @throws {Error} - If config is invalid
|
|
39
|
+
* @private
|
|
40
|
+
*/
|
|
41
|
+
function validateConfig(config) {
|
|
42
|
+
if (!config || typeof config !== 'object') {
|
|
43
|
+
throw new Error('createManagers: config is required and must be an object')
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
if (!config.schemas || typeof config.schemas !== 'object') {
|
|
47
|
+
throw new Error('createManagers: config.schemas is required and must be an object')
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
if (!config.storages || typeof config.storages !== 'object') {
|
|
51
|
+
throw new Error('createManagers: config.storages is required and must be an object')
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
if (!config.entities || typeof config.entities !== 'object') {
|
|
55
|
+
throw new Error('createManagers: config.entities is required and must be an object')
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
// Validate each entity config
|
|
59
|
+
for (const [entityName, entityConfig] of Object.entries(config.entities)) {
|
|
60
|
+
if (!entityConfig || typeof entityConfig !== 'object') {
|
|
61
|
+
throw new Error(`createManagers: entity '${entityName}' config must be an object`)
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
if (!entityConfig.schema || typeof entityConfig.schema !== 'string') {
|
|
65
|
+
throw new Error(`createManagers: entity '${entityName}' requires 'schema' property`)
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
if (!entityConfig.storage || typeof entityConfig.storage !== 'string') {
|
|
69
|
+
throw new Error(`createManagers: entity '${entityName}' requires 'storage' property`)
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
if (!entityConfig.endpoint || typeof entityConfig.endpoint !== 'string') {
|
|
73
|
+
throw new Error(`createManagers: entity '${entityName}' requires 'endpoint' property`)
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
// Validate references exist
|
|
77
|
+
if (!(entityConfig.schema in config.schemas)) {
|
|
78
|
+
throw new Error(`createManagers: entity '${entityName}' references unknown schema '${entityConfig.schema}'`)
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
if (!(entityConfig.storage in config.storages)) {
|
|
82
|
+
throw new Error(`createManagers: entity '${entityName}' references unknown storage '${entityConfig.storage}'`)
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
/**
|
|
88
|
+
* Find schema for entity from connector result
|
|
89
|
+
*
|
|
90
|
+
* Connector results are arrays of UnifiedEntitySchema. This finds the schema
|
|
91
|
+
* matching the entity name, or uses the first schema if none matches.
|
|
92
|
+
*
|
|
93
|
+
* @param {import('./schema.js').UnifiedEntitySchema[]} schemas - Connector result (array of schemas)
|
|
94
|
+
* @param {string} entityName - Entity name to find
|
|
95
|
+
* @param {string} schemaSourceName - Schema source name for error messages
|
|
96
|
+
* @returns {import('./schema.js').UnifiedEntitySchema} - Found schema
|
|
97
|
+
* @throws {Error} - If schema not found
|
|
98
|
+
* @private
|
|
99
|
+
*/
|
|
100
|
+
function findSchemaForEntity(schemas, entityName, schemaSourceName) {
|
|
101
|
+
if (!Array.isArray(schemas)) {
|
|
102
|
+
throw new Error(`createManagers: schema source '${schemaSourceName}' must be an array of UnifiedEntitySchema`)
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
if (schemas.length === 0) {
|
|
106
|
+
throw new Error(`createManagers: schema source '${schemaSourceName}' is empty`)
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
// Find by entity name
|
|
110
|
+
const schema = schemas.find(s => s.name === entityName)
|
|
111
|
+
if (schema) {
|
|
112
|
+
return schema
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
// If only one schema in source, use it (common for single-entity sources)
|
|
116
|
+
if (schemas.length === 1) {
|
|
117
|
+
return schemas[0]
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
// Multiple schemas but none match
|
|
121
|
+
const available = schemas.map(s => s.name).join(', ')
|
|
122
|
+
throw new Error(`createManagers: entity '${entityName}' not found in schema source '${schemaSourceName}'. Available: ${available}`)
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
/**
|
|
126
|
+
* Create EntityManager instances from configuration
|
|
127
|
+
*
|
|
128
|
+
* This is the runtime factory that wires schemas, storage profiles, and
|
|
129
|
+
* decorators together at runtime without code generation.
|
|
130
|
+
*
|
|
131
|
+
* @param {CreateManagersConfig} config - Configuration object
|
|
132
|
+
* @returns {Map<string, EntityManager>} - Map of entity name to EntityManager instance
|
|
133
|
+
* @throws {Error} - If config is invalid or references missing schemas/storages
|
|
134
|
+
*
|
|
135
|
+
* @example
|
|
136
|
+
* import { createManagers } from 'qdadm/gen'
|
|
137
|
+
* import { ManualConnector } from 'qdadm/gen'
|
|
138
|
+
* import { ApiStorage } from 'qdadm'
|
|
139
|
+
* import axios from 'axios'
|
|
140
|
+
*
|
|
141
|
+
* // Define schemas using connectors
|
|
142
|
+
* const connector = new ManualConnector()
|
|
143
|
+
* const schemas = connector.parse([
|
|
144
|
+
* {
|
|
145
|
+
* name: 'users',
|
|
146
|
+
* endpoint: '/users',
|
|
147
|
+
* fields: {
|
|
148
|
+
* id: { name: 'id', type: 'number', readOnly: true },
|
|
149
|
+
* email: { name: 'email', type: 'email', required: true }
|
|
150
|
+
* }
|
|
151
|
+
* }
|
|
152
|
+
* ])
|
|
153
|
+
*
|
|
154
|
+
* // Define storage profile factory
|
|
155
|
+
* const apiClient = axios.create({ baseURL: 'https://api.example.com' })
|
|
156
|
+
* const apiProfile = (endpoint, options) => new ApiStorage({
|
|
157
|
+
* endpoint,
|
|
158
|
+
* client: apiClient,
|
|
159
|
+
* ...options
|
|
160
|
+
* })
|
|
161
|
+
*
|
|
162
|
+
* // Create managers
|
|
163
|
+
* const managers = createManagers({
|
|
164
|
+
* schemas: { api: schemas },
|
|
165
|
+
* storages: { jsonplaceholder: apiProfile },
|
|
166
|
+
* entities: {
|
|
167
|
+
* users: { schema: 'api', storage: 'jsonplaceholder', endpoint: '/users' }
|
|
168
|
+
* },
|
|
169
|
+
* decorators: {
|
|
170
|
+
* users: { fields: { email: { hidden: true } } }
|
|
171
|
+
* }
|
|
172
|
+
* })
|
|
173
|
+
*
|
|
174
|
+
* const usersManager = managers.get('users')
|
|
175
|
+
* const users = await usersManager.list()
|
|
176
|
+
*/
|
|
177
|
+
export function createManagers(config) {
|
|
178
|
+
// Validate config structure early
|
|
179
|
+
validateConfig(config)
|
|
180
|
+
|
|
181
|
+
/** @type {Map<string, EntityManager>} */
|
|
182
|
+
const managers = new Map()
|
|
183
|
+
|
|
184
|
+
// Process each entity configuration
|
|
185
|
+
for (const [entityName, entityConfig] of Object.entries(config.entities)) {
|
|
186
|
+
// 1. Get schema from connector result
|
|
187
|
+
const schemaSource = config.schemas[entityConfig.schema]
|
|
188
|
+
let schema = findSchemaForEntity(schemaSource, entityName, entityConfig.schema)
|
|
189
|
+
|
|
190
|
+
// 2. Apply decorators if defined
|
|
191
|
+
const decorator = config.decorators?.[entityName]
|
|
192
|
+
schema = applyDecorators(schema, decorator)
|
|
193
|
+
|
|
194
|
+
// 3. Get storage factory and create storage instance
|
|
195
|
+
const storageFactory = config.storages[entityConfig.storage]
|
|
196
|
+
if (typeof storageFactory !== 'function') {
|
|
197
|
+
throw new Error(`createManagers: storage '${entityConfig.storage}' must be a factory function`)
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
const storageOptions = {
|
|
201
|
+
entity: entityName,
|
|
202
|
+
...entityConfig.options
|
|
203
|
+
}
|
|
204
|
+
const storage = storageFactory(entityConfig.endpoint, storageOptions)
|
|
205
|
+
|
|
206
|
+
// 4. Create EntityManager with schema + storage
|
|
207
|
+
const manager = new EntityManager({
|
|
208
|
+
name: schema.name,
|
|
209
|
+
storage,
|
|
210
|
+
idField: schema.idField || 'id',
|
|
211
|
+
label: schema.label,
|
|
212
|
+
labelPlural: schema.labelPlural,
|
|
213
|
+
labelField: schema.labelField,
|
|
214
|
+
routePrefix: schema.routePrefix,
|
|
215
|
+
readOnly: schema.readOnly,
|
|
216
|
+
fields: schema.fields
|
|
217
|
+
})
|
|
218
|
+
|
|
219
|
+
// 5. Store in result Map
|
|
220
|
+
managers.set(entityName, manager)
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
return managers
|
|
224
|
+
}
|
|
@@ -0,0 +1,129 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Decorators Layer
|
|
3
|
+
*
|
|
4
|
+
* Applies per-entity field customizations to UnifiedEntitySchema.
|
|
5
|
+
* Decorators allow overriding field properties (hidden, label, readonly, order)
|
|
6
|
+
* without modifying the base schema from connectors.
|
|
7
|
+
*
|
|
8
|
+
* @module gen/decorators
|
|
9
|
+
*/
|
|
10
|
+
|
|
11
|
+
/**
|
|
12
|
+
* Allowed decorator properties for field overrides.
|
|
13
|
+
* Unknown properties will trigger a console warning.
|
|
14
|
+
*
|
|
15
|
+
* @type {readonly string[]}
|
|
16
|
+
*/
|
|
17
|
+
const ALLOWED_FIELD_DECORATORS = Object.freeze([
|
|
18
|
+
'hidden',
|
|
19
|
+
'label',
|
|
20
|
+
'readOnly',
|
|
21
|
+
'order'
|
|
22
|
+
])
|
|
23
|
+
|
|
24
|
+
/**
|
|
25
|
+
* Field Decorator Configuration
|
|
26
|
+
*
|
|
27
|
+
* Per-field overrides that can be applied to schema fields.
|
|
28
|
+
*
|
|
29
|
+
* @typedef {object} FieldDecorator
|
|
30
|
+
* @property {boolean} [hidden] - Override field visibility
|
|
31
|
+
* @property {string} [label] - Override field label
|
|
32
|
+
* @property {boolean} [readOnly] - Override field read-only state
|
|
33
|
+
* @property {number} [order] - Override field display order
|
|
34
|
+
*/
|
|
35
|
+
|
|
36
|
+
/**
|
|
37
|
+
* Entity Decorator Configuration
|
|
38
|
+
*
|
|
39
|
+
* Decorator configuration for a single entity.
|
|
40
|
+
*
|
|
41
|
+
* @typedef {object} EntityDecorator
|
|
42
|
+
* @property {Record<string, FieldDecorator>} [fields] - Field-level overrides keyed by field name
|
|
43
|
+
*/
|
|
44
|
+
|
|
45
|
+
/**
|
|
46
|
+
* Apply decorators to a UnifiedEntitySchema
|
|
47
|
+
*
|
|
48
|
+
* Takes a schema and decorator configuration, returns a new schema with
|
|
49
|
+
* decorator properties merged onto matching fields. Original schema is
|
|
50
|
+
* not mutated.
|
|
51
|
+
*
|
|
52
|
+
* @param {import('./schema.js').UnifiedEntitySchema} schema - Original entity schema
|
|
53
|
+
* @param {EntityDecorator} [decoratorConfig] - Decorator configuration
|
|
54
|
+
* @returns {import('./schema.js').UnifiedEntitySchema} New schema with decorators applied
|
|
55
|
+
*
|
|
56
|
+
* @example
|
|
57
|
+
* // Hide email field and rename created_at
|
|
58
|
+
* const decorated = applyDecorators(usersSchema, {
|
|
59
|
+
* fields: {
|
|
60
|
+
* email: { hidden: true },
|
|
61
|
+
* created_at: { label: 'Member Since', readOnly: true }
|
|
62
|
+
* }
|
|
63
|
+
* })
|
|
64
|
+
*
|
|
65
|
+
* @example
|
|
66
|
+
* // Set field display order
|
|
67
|
+
* const ordered = applyDecorators(postsSchema, {
|
|
68
|
+
* fields: {
|
|
69
|
+
* title: { order: 1 },
|
|
70
|
+
* body: { order: 2 },
|
|
71
|
+
* author_id: { order: 3 }
|
|
72
|
+
* }
|
|
73
|
+
* })
|
|
74
|
+
*/
|
|
75
|
+
export function applyDecorators(schema, decoratorConfig) {
|
|
76
|
+
// No decorators - return schema as-is (still immutable reference)
|
|
77
|
+
if (!decoratorConfig || !decoratorConfig.fields) {
|
|
78
|
+
return schema
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
const { fields: fieldDecorators } = decoratorConfig
|
|
82
|
+
|
|
83
|
+
// Build new fields object with decorators applied
|
|
84
|
+
const decoratedFields = {}
|
|
85
|
+
|
|
86
|
+
for (const [fieldName, fieldSchema] of Object.entries(schema.fields)) {
|
|
87
|
+
const decorator = fieldDecorators[fieldName]
|
|
88
|
+
|
|
89
|
+
if (!decorator) {
|
|
90
|
+
// No decorator for this field - copy as-is
|
|
91
|
+
decoratedFields[fieldName] = { ...fieldSchema }
|
|
92
|
+
continue
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
// Validate decorator properties
|
|
96
|
+
for (const key of Object.keys(decorator)) {
|
|
97
|
+
if (!ALLOWED_FIELD_DECORATORS.includes(key)) {
|
|
98
|
+
console.warn(
|
|
99
|
+
`[qdadm] Unknown decorator property "${key}" for field "${fieldName}" in entity "${schema.name}". Allowed: ${ALLOWED_FIELD_DECORATORS.join(', ')}`
|
|
100
|
+
)
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
// Merge decorator properties onto field (only allowed properties)
|
|
105
|
+
const decoratedField = { ...fieldSchema }
|
|
106
|
+
for (const key of ALLOWED_FIELD_DECORATORS) {
|
|
107
|
+
if (key in decorator) {
|
|
108
|
+
decoratedField[key] = decorator[key]
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
decoratedFields[fieldName] = decoratedField
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
// Warn about decorators for non-existent fields
|
|
116
|
+
for (const fieldName of Object.keys(fieldDecorators)) {
|
|
117
|
+
if (!(fieldName in schema.fields)) {
|
|
118
|
+
console.warn(
|
|
119
|
+
`[qdadm] Decorator defined for unknown field "${fieldName}" in entity "${schema.name}". Available fields: ${Object.keys(schema.fields).join(', ')}`
|
|
120
|
+
)
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
// Return new schema with decorated fields
|
|
125
|
+
return {
|
|
126
|
+
...schema,
|
|
127
|
+
fields: decoratedFields
|
|
128
|
+
}
|
|
129
|
+
}
|