@quazardous/qdadm 2.16.0 → 2.16.1

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,65 @@
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
+ import type { UnifiedEntitySchema } from './schema';
11
+ /**
12
+ * Field Decorator Configuration
13
+ *
14
+ * Per-field overrides that can be applied to schema fields.
15
+ */
16
+ export interface FieldDecorator {
17
+ /** Override field visibility */
18
+ hidden?: boolean;
19
+ /** Override field label */
20
+ label?: string;
21
+ /** Override field read-only state */
22
+ readOnly?: boolean;
23
+ /** Override field display order */
24
+ order?: number;
25
+ }
26
+ /**
27
+ * Entity Decorator Configuration
28
+ *
29
+ * Decorator configuration for a single entity.
30
+ */
31
+ export interface EntityDecorator {
32
+ /** Field-level overrides keyed by field name */
33
+ fields?: Record<string, FieldDecorator>;
34
+ }
35
+ /**
36
+ * Apply decorators to a UnifiedEntitySchema
37
+ *
38
+ * Takes a schema and decorator configuration, returns a new schema with
39
+ * decorator properties merged onto matching fields. Original schema is
40
+ * not mutated.
41
+ *
42
+ * @param schema - Original entity schema
43
+ * @param decoratorConfig - Decorator configuration
44
+ * @returns New schema with decorators applied
45
+ *
46
+ * @example
47
+ * // Hide email field and rename created_at
48
+ * const decorated = applyDecorators(usersSchema, {
49
+ * fields: {
50
+ * email: { hidden: true },
51
+ * created_at: { label: 'Member Since', readOnly: true }
52
+ * }
53
+ * })
54
+ *
55
+ * @example
56
+ * // Set field display order
57
+ * const ordered = applyDecorators(postsSchema, {
58
+ * fields: {
59
+ * title: { order: 1 },
60
+ * body: { order: 2 },
61
+ * author_id: { order: 3 }
62
+ * }
63
+ * })
64
+ */
65
+ export declare function applyDecorators(schema: UnifiedEntitySchema, decoratorConfig?: EntityDecorator): UnifiedEntitySchema;
@@ -0,0 +1,87 @@
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
+ * Allowed decorator properties for field overrides.
12
+ * Unknown properties will trigger a console warning.
13
+ */
14
+ const ALLOWED_FIELD_DECORATORS = ['hidden', 'label', 'readOnly', 'order'];
15
+ /**
16
+ * Apply decorators to a UnifiedEntitySchema
17
+ *
18
+ * Takes a schema and decorator configuration, returns a new schema with
19
+ * decorator properties merged onto matching fields. Original schema is
20
+ * not mutated.
21
+ *
22
+ * @param schema - Original entity schema
23
+ * @param decoratorConfig - Decorator configuration
24
+ * @returns New schema with decorators applied
25
+ *
26
+ * @example
27
+ * // Hide email field and rename created_at
28
+ * const decorated = applyDecorators(usersSchema, {
29
+ * fields: {
30
+ * email: { hidden: true },
31
+ * created_at: { label: 'Member Since', readOnly: true }
32
+ * }
33
+ * })
34
+ *
35
+ * @example
36
+ * // Set field display order
37
+ * const ordered = applyDecorators(postsSchema, {
38
+ * fields: {
39
+ * title: { order: 1 },
40
+ * body: { order: 2 },
41
+ * author_id: { order: 3 }
42
+ * }
43
+ * })
44
+ */
45
+ export function applyDecorators(schema, decoratorConfig) {
46
+ // No decorators - return schema as-is (still immutable reference)
47
+ if (!decoratorConfig || !decoratorConfig.fields) {
48
+ return schema;
49
+ }
50
+ const { fields: fieldDecorators } = decoratorConfig;
51
+ // Build new fields object with decorators applied
52
+ const decoratedFields = {};
53
+ for (const [fieldName, fieldSchema] of Object.entries(schema.fields)) {
54
+ const decorator = fieldDecorators[fieldName];
55
+ if (!decorator) {
56
+ // No decorator for this field - copy as-is
57
+ decoratedFields[fieldName] = { ...fieldSchema };
58
+ continue;
59
+ }
60
+ // Validate decorator properties
61
+ for (const key of Object.keys(decorator)) {
62
+ if (!ALLOWED_FIELD_DECORATORS.includes(key)) {
63
+ console.warn(`[qdadm] Unknown decorator property "${key}" for field "${fieldName}" in entity "${schema.name}". Allowed: ${ALLOWED_FIELD_DECORATORS.join(', ')}`);
64
+ }
65
+ }
66
+ // Merge decorator properties onto field (only allowed properties)
67
+ const decoratedField = { ...fieldSchema };
68
+ for (const key of ALLOWED_FIELD_DECORATORS) {
69
+ if (key in decorator) {
70
+ ;
71
+ decoratedField[key] = decorator[key];
72
+ }
73
+ }
74
+ decoratedFields[fieldName] = decoratedField;
75
+ }
76
+ // Warn about decorators for non-existent fields
77
+ for (const fieldName of Object.keys(fieldDecorators)) {
78
+ if (!(fieldName in schema.fields)) {
79
+ console.warn(`[qdadm] Decorator defined for unknown field "${fieldName}" in entity "${schema.name}". Available fields: ${Object.keys(schema.fields).join(', ')}`);
80
+ }
81
+ }
82
+ // Return new schema with decorated fields
83
+ return {
84
+ ...schema,
85
+ fields: decoratedFields,
86
+ };
87
+ }
@@ -0,0 +1,107 @@
1
+ /**
2
+ * Build-time Manager File Generation
3
+ *
4
+ * Generates individual EntityManager files for each entity in the configuration.
5
+ * This enables better IDE support and static analysis by creating explicit files
6
+ * rather than relying on runtime factory creation.
7
+ *
8
+ * Outputs TypeScript (.ts) files with auto-generated entity interfaces derived
9
+ * from the field schemas, providing end-to-end type safety.
10
+ *
11
+ * @module gen/generateManagers
12
+ */
13
+ import type { UnifiedEntitySchema, UnifiedFieldSchema, UnifiedFieldType } from './schema';
14
+ import type { EntityDecorator } from './decorators';
15
+ /**
16
+ * Custom base class for generated managers (#1253).
17
+ *
18
+ * The class MUST extend EntityManager, stay generic over the entity type
19
+ * and pass constructor options through — the generated code does
20
+ * `class Generated<X>Manager extends <name><XEntity>` and calls
21
+ * `super({ ...schema options, storage, ...options })`.
22
+ */
23
+ export interface GenerateManagersBaseClass {
24
+ /** Import specifier (e.g. '@/managers/SkyEntityManager') */
25
+ import: string;
26
+ /** Exported class name (e.g. 'SkyEntityManager') */
27
+ name: string;
28
+ }
29
+ /**
30
+ * Entity configuration for generation
31
+ */
32
+ export interface GenerateManagersEntityConfig {
33
+ /** Entity schema */
34
+ schema: UnifiedEntitySchema;
35
+ /** API endpoint path (e.g., '/users') */
36
+ endpoint: string;
37
+ /** Import path for storage (e.g., 'qdadm' or './storage') */
38
+ storageImport: string;
39
+ /** Storage class name (e.g., 'ApiStorage') */
40
+ storageClass: string;
41
+ /** Additional storage constructor options */
42
+ storageOptions?: Record<string, unknown>;
43
+ /** Field decorators to apply */
44
+ decorators?: EntityDecorator;
45
+ /** Generate class instead of instance (for extension) */
46
+ classMode?: boolean;
47
+ /** Base class for this entity's generated manager (overrides global) */
48
+ baseClass?: GenerateManagersBaseClass;
49
+ }
50
+ /**
51
+ * Configuration for generateManagers
52
+ */
53
+ export interface GenerateManagersConfig {
54
+ /** Output directory (default: 'src/generated/managers/') */
55
+ output?: string;
56
+ /** Generate classes instead of instances (for extension pattern) */
57
+ classMode?: boolean;
58
+ /** Base class for all generated managers (default: EntityManager) */
59
+ baseClass?: GenerateManagersBaseClass;
60
+ /** Entity configurations keyed by entity name */
61
+ entities: Record<string, GenerateManagersEntityConfig>;
62
+ }
63
+ /**
64
+ * Map UnifiedFieldType to TypeScript type string
65
+ */
66
+ export declare function fieldTypeToTsType(type: UnifiedFieldType, required: boolean): string;
67
+ /**
68
+ * Generate a TypeScript interface from entity fields
69
+ */
70
+ export declare function generateEntityInterface(entityName: string, fields: Record<string, UnifiedFieldSchema>, idField: string): string;
71
+ /**
72
+ * Generate EntityManager files from configuration
73
+ *
74
+ * Creates one TypeScript file per entity in the output directory.
75
+ * Each file exports the entity's interface, schema, and a configured EntityManager instance.
76
+ *
77
+ * @param config - Configuration object
78
+ * @returns List of generated file paths
79
+ * @throws If config is invalid or file operations fail
80
+ *
81
+ * @example
82
+ * ```ts
83
+ * import { generateManagers } from '@quazardous/qdadm/gen'
84
+ *
85
+ * const generatedFiles = await generateManagers({
86
+ * output: 'src/generated/managers/',
87
+ * entities: {
88
+ * users: {
89
+ * schema: usersSchema,
90
+ * endpoint: '/api/users',
91
+ * storageImport: '@quazardous/qdadm',
92
+ * storageClass: 'ApiStorage',
93
+ * storageOptions: { client: 'apiClient' },
94
+ * decorators: {
95
+ * fields: {
96
+ * password: { hidden: true }
97
+ * }
98
+ * }
99
+ * }
100
+ * }
101
+ * })
102
+ *
103
+ * console.log('Generated:', generatedFiles)
104
+ * // ['src/generated/managers/usersManager.ts']
105
+ * ```
106
+ */
107
+ export declare function generateManagers(config: GenerateManagersConfig): Promise<string[]>;
@@ -0,0 +1,338 @@
1
+ /**
2
+ * Build-time Manager File Generation
3
+ *
4
+ * Generates individual EntityManager files for each entity in the configuration.
5
+ * This enables better IDE support and static analysis by creating explicit files
6
+ * rather than relying on runtime factory creation.
7
+ *
8
+ * Outputs TypeScript (.ts) files with auto-generated entity interfaces derived
9
+ * from the field schemas, providing end-to-end type safety.
10
+ *
11
+ * @module gen/generateManagers
12
+ */
13
+ import { mkdir, writeFile } from 'node:fs/promises';
14
+ import { join } from 'node:path';
15
+ /**
16
+ * Default output directory for generated managers
17
+ */
18
+ const DEFAULT_OUTPUT_DIR = 'src/generated/managers/';
19
+ /**
20
+ * Convert entity name to PascalCase for class name
21
+ *
22
+ * @param name - Entity name (e.g., 'users', 'blog_posts')
23
+ * @returns PascalCase name (e.g., 'Users', 'BlogPosts')
24
+ */
25
+ function toPascalCase(name) {
26
+ return name
27
+ .split(/[_-]/)
28
+ .map((part) => part.charAt(0).toUpperCase() + part.slice(1))
29
+ .join('');
30
+ }
31
+ /**
32
+ * Map UnifiedFieldType to TypeScript type string
33
+ */
34
+ export function fieldTypeToTsType(type, required) {
35
+ const tsType = (() => {
36
+ switch (type) {
37
+ case 'text':
38
+ case 'email':
39
+ case 'url':
40
+ case 'uuid':
41
+ case 'date':
42
+ case 'datetime':
43
+ return 'string';
44
+ case 'number':
45
+ return 'number';
46
+ case 'boolean':
47
+ return 'boolean';
48
+ case 'array':
49
+ return 'unknown[]';
50
+ case 'object':
51
+ return 'Record<string, unknown>';
52
+ default:
53
+ // Fallback for types widened beyond UnifiedFieldType at runtime
54
+ // (e.g. OpenAPI oneOf/discriminator that FieldMapper couldn't classify).
55
+ // Without this, the IIFE returns undefined and we'd emit the literal
56
+ // string "undefined" into the generated .ts file.
57
+ return 'unknown';
58
+ }
59
+ })();
60
+ return required ? tsType : `${tsType} | null`;
61
+ }
62
+ /**
63
+ * Generate a TypeScript interface from entity fields
64
+ */
65
+ export function generateEntityInterface(entityName, fields, idField) {
66
+ const className = toPascalCase(entityName);
67
+ const lines = [];
68
+ lines.push(`export interface ${className}Entity extends EntityRecord {`);
69
+ for (const [fieldName, field] of Object.entries(fields)) {
70
+ if (field.hidden)
71
+ continue;
72
+ // Connectors (e.g. OpenAPIConnector) flatten one level of nested object
73
+ // properties as dotted keys ("filter.botUuids") for runtime form/column
74
+ // metadata. The parent object field is already typed as
75
+ // Record<string, unknown>, and dotted keys are not legal at the top level
76
+ // of a TS interface body — skip them here.
77
+ if (fieldName.includes('.'))
78
+ continue;
79
+ const isRequired = field.required || fieldName === idField;
80
+ const tsType = fieldTypeToTsType(field.type, isRequired);
81
+ const optional = isRequired ? '' : '?';
82
+ lines.push(` ${fieldName}${optional}: ${tsType}`);
83
+ }
84
+ lines.push('}');
85
+ return lines.join('\n');
86
+ }
87
+ /**
88
+ * Serialize a value to JavaScript source code
89
+ *
90
+ * @param value - Value to serialize
91
+ * @param indent - Current indentation level
92
+ * @returns JavaScript source code
93
+ */
94
+ function serializeValue(value, indent = 0) {
95
+ const spaces = ' '.repeat(indent);
96
+ const innerSpaces = ' '.repeat(indent + 1);
97
+ if (value === null)
98
+ return 'null';
99
+ if (value === undefined)
100
+ return 'undefined';
101
+ if (typeof value === 'string')
102
+ return JSON.stringify(value);
103
+ if (typeof value === 'number' || typeof value === 'boolean')
104
+ return String(value);
105
+ if (Array.isArray(value)) {
106
+ if (value.length === 0)
107
+ return '[]';
108
+ const items = value.map((v) => serializeValue(v, indent + 1));
109
+ return `[\n${innerSpaces}${items.join(`,\n${innerSpaces}`)}\n${spaces}]`;
110
+ }
111
+ if (typeof value === 'object') {
112
+ const obj = value;
113
+ const keys = Object.keys(obj);
114
+ if (keys.length === 0)
115
+ return '{}';
116
+ const entries = keys.map((key) => {
117
+ const safeKey = /^[a-zA-Z_$][a-zA-Z0-9_$]*$/.test(key) ? key : JSON.stringify(key);
118
+ return `${innerSpaces}${safeKey}: ${serializeValue(obj[key], indent + 1)}`;
119
+ });
120
+ return `{\n${entries.join(',\n')}\n${spaces}}`;
121
+ }
122
+ return String(value);
123
+ }
124
+ // Note: ManagerOptions interface removed - using inline Record<string, unknown> type instead
125
+ /**
126
+ * Generate the source code for a single entity manager file
127
+ *
128
+ * @param entityName - Entity name
129
+ * @param entityConfig - Entity configuration
130
+ * @param globalOptions - Global generation options
131
+ * @returns Generated TypeScript source code
132
+ */
133
+ function generateManagerSource(entityName, entityConfig, globalOptions = {}) {
134
+ const { schema, endpoint, storageImport, storageClass, storageOptions = {}, decorators } = entityConfig;
135
+ const classMode = entityConfig.classMode ?? globalOptions.classMode ?? false;
136
+ const className = toPascalCase(entityName);
137
+ // Custom base class (#1253): per-entity wins over global, default EntityManager.
138
+ const baseClass = entityConfig.baseClass ?? globalOptions.baseClass;
139
+ const baseName = baseClass?.name ?? 'EntityManager';
140
+ const baseImport = baseClass
141
+ ? `import { ${baseClass.name} } from '${baseClass.import}'`
142
+ : `import { EntityManager } from '@quazardous/qdadm'`;
143
+ // Build storage options object
144
+ const fullStorageOptions = {
145
+ endpoint,
146
+ ...storageOptions,
147
+ };
148
+ // Apply decorators to schema if provided
149
+ let finalSchema = schema;
150
+ if (decorators?.fields) {
151
+ // Create decorated fields inline
152
+ const decoratedFields = {};
153
+ for (const [fieldName, fieldSchema] of Object.entries(schema.fields)) {
154
+ const decorator = decorators.fields[fieldName];
155
+ if (decorator) {
156
+ decoratedFields[fieldName] = { ...fieldSchema, ...decorator };
157
+ }
158
+ else {
159
+ decoratedFields[fieldName] = { ...fieldSchema };
160
+ }
161
+ }
162
+ finalSchema = { ...schema, fields: decoratedFields };
163
+ }
164
+ // Build EntityManager options
165
+ const managerOptions = {
166
+ name: finalSchema.name,
167
+ idField: finalSchema.idField || 'id',
168
+ label: finalSchema.label,
169
+ labelPlural: finalSchema.labelPlural,
170
+ labelField: finalSchema.labelField,
171
+ routePrefix: finalSchema.routePrefix,
172
+ readOnly: finalSchema.readOnly,
173
+ fields: finalSchema.fields,
174
+ };
175
+ // Remove undefined values
176
+ for (const key of Object.keys(managerOptions)) {
177
+ if (managerOptions[key] === undefined) {
178
+ delete managerOptions[key];
179
+ }
180
+ }
181
+ // Generate entity interface
182
+ const idField = finalSchema.idField || 'id';
183
+ const entityInterface = generateEntityInterface(entityName, finalSchema.fields, idField);
184
+ // Class-based template for extension pattern
185
+ if (classMode) {
186
+ return `/**
187
+ * Generated${className}Manager - Auto-generated base class
188
+ *
189
+ * Generated by qdadm/gen/generateManagers
190
+ * DO NOT EDIT MANUALLY - Changes will be overwritten
191
+ *
192
+ * Extend this class for custom logic.
193
+ *
194
+ * Entity: ${entityName}
195
+ * Endpoint: ${endpoint}
196
+ */
197
+
198
+ ${baseImport}
199
+ import type { EntityRecord, EntityManagerOptions } from '@quazardous/qdadm'
200
+ import { ${storageClass} } from '${storageImport}'
201
+
202
+ ${entityInterface}
203
+
204
+ /**
205
+ * Generated base manager for ${entityName}
206
+ *
207
+ * @extends ${baseName}
208
+ */
209
+ export class Generated${className}Manager extends ${baseName}<${className}Entity> {
210
+ constructor(options: Partial<EntityManagerOptions<${className}Entity>> = {}) {
211
+ super({
212
+ ...${serializeValue(managerOptions, 2)},
213
+ storage: new ${storageClass}<${className}Entity>(${serializeValue(fullStorageOptions, 2)}),
214
+ ...options
215
+ })
216
+ }
217
+ }
218
+ `;
219
+ }
220
+ // Instance-based template (default)
221
+ return `/**
222
+ * ${className}Manager - Auto-generated EntityManager
223
+ *
224
+ * Generated by qdadm/gen/generateManagers
225
+ * DO NOT EDIT MANUALLY - Changes will be overwritten
226
+ *
227
+ * Entity: ${entityName}
228
+ * Endpoint: ${endpoint}
229
+ */
230
+
231
+ ${baseImport}
232
+ import type { EntityRecord } from '@quazardous/qdadm'
233
+ import { ${storageClass} } from '${storageImport}'
234
+
235
+ ${entityInterface}
236
+
237
+ /**
238
+ * Schema definition for ${entityName}
239
+ */
240
+ export const ${entityName}Schema = ${serializeValue(finalSchema, 0)} as const
241
+
242
+ /**
243
+ * Storage options for ${entityName}
244
+ */
245
+ const storageOptions = ${serializeValue(fullStorageOptions, 0)}
246
+
247
+ /**
248
+ * ${className}Manager instance
249
+ *
250
+ * Provides CRUD operations for ${entityName} entity.
251
+ */
252
+ export const ${entityName}Manager = new ${baseName}<${className}Entity>({
253
+ ...${serializeValue(managerOptions, 0)},
254
+ storage: new ${storageClass}<${className}Entity>(storageOptions)
255
+ })
256
+ `;
257
+ }
258
+ /**
259
+ * Generate EntityManager files from configuration
260
+ *
261
+ * Creates one TypeScript file per entity in the output directory.
262
+ * Each file exports the entity's interface, schema, and a configured EntityManager instance.
263
+ *
264
+ * @param config - Configuration object
265
+ * @returns List of generated file paths
266
+ * @throws If config is invalid or file operations fail
267
+ *
268
+ * @example
269
+ * ```ts
270
+ * import { generateManagers } from '@quazardous/qdadm/gen'
271
+ *
272
+ * const generatedFiles = await generateManagers({
273
+ * output: 'src/generated/managers/',
274
+ * entities: {
275
+ * users: {
276
+ * schema: usersSchema,
277
+ * endpoint: '/api/users',
278
+ * storageImport: '@quazardous/qdadm',
279
+ * storageClass: 'ApiStorage',
280
+ * storageOptions: { client: 'apiClient' },
281
+ * decorators: {
282
+ * fields: {
283
+ * password: { hidden: true }
284
+ * }
285
+ * }
286
+ * }
287
+ * }
288
+ * })
289
+ *
290
+ * console.log('Generated:', generatedFiles)
291
+ * // ['src/generated/managers/usersManager.ts']
292
+ * ```
293
+ */
294
+ export async function generateManagers(config) {
295
+ // Validate config
296
+ if (!config || typeof config !== 'object') {
297
+ throw new Error('generateManagers: config is required and must be an object');
298
+ }
299
+ if (!config.entities || typeof config.entities !== 'object') {
300
+ throw new Error('generateManagers: config.entities is required and must be an object');
301
+ }
302
+ const outputDir = config.output || DEFAULT_OUTPUT_DIR;
303
+ const globalOptions = { classMode: config.classMode ?? false, baseClass: config.baseClass };
304
+ const generatedFiles = [];
305
+ // Validate each entity config
306
+ for (const [entityName, entityConfig] of Object.entries(config.entities)) {
307
+ if (!entityConfig || typeof entityConfig !== 'object') {
308
+ throw new Error(`generateManagers: entity '${entityName}' config must be an object`);
309
+ }
310
+ if (!entityConfig.schema || typeof entityConfig.schema !== 'object') {
311
+ throw new Error(`generateManagers: entity '${entityName}' requires 'schema' property`);
312
+ }
313
+ if (!entityConfig.endpoint || typeof entityConfig.endpoint !== 'string') {
314
+ throw new Error(`generateManagers: entity '${entityName}' requires 'endpoint' property`);
315
+ }
316
+ if (!entityConfig.storageImport || typeof entityConfig.storageImport !== 'string') {
317
+ throw new Error(`generateManagers: entity '${entityName}' requires 'storageImport' property`);
318
+ }
319
+ if (!entityConfig.storageClass || typeof entityConfig.storageClass !== 'string') {
320
+ throw new Error(`generateManagers: entity '${entityName}' requires 'storageClass' property`);
321
+ }
322
+ }
323
+ // Create output directory if it doesn't exist
324
+ await mkdir(outputDir, { recursive: true });
325
+ // Generate files for each entity
326
+ for (const [entityName, entityConfig] of Object.entries(config.entities)) {
327
+ const isClassMode = entityConfig.classMode ?? globalOptions.classMode;
328
+ // Use entity.ts for classMode (matches existing pattern), entityManager.ts for instance mode
329
+ const fileName = isClassMode ? `${entityName}.ts` : `${entityName}Manager.ts`;
330
+ const filePath = join(outputDir, fileName);
331
+ // Generate source code
332
+ const source = generateManagerSource(entityName, entityConfig, globalOptions);
333
+ // Write file
334
+ await writeFile(filePath, source, 'utf-8');
335
+ generatedFiles.push(filePath);
336
+ }
337
+ return generatedFiles;
338
+ }