qdadm 0.28.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.
@@ -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
+ }