@quazardous/qdadm 2.15.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.
- package/dist/node/gen/decorators.d.ts +65 -0
- package/dist/node/gen/decorators.js +87 -0
- package/dist/node/gen/generateManagers.d.ts +107 -0
- package/dist/node/gen/generateManagers.js +338 -0
- package/dist/node/gen/schema.d.ts +195 -0
- package/dist/node/gen/schema.js +91 -0
- package/dist/node/gen/vite-plugin.d.ts +64 -0
- package/dist/node/gen/vite-plugin.js +99 -0
- package/dist/node/vite/qdadmDebugPlugin.d.ts +60 -0
- package/dist/node/vite/qdadmDebugPlugin.js +462 -0
- package/dist/node/vite/qdadmPlugin.d.ts +29 -0
- package/dist/node/vite/qdadmPlugin.js +91 -0
- package/package.json +10 -7
- package/src/gen/vite-plugin.ts +4 -1
- package/src/i18n/I18n.ts +25 -1
- package/src/i18n/__tests__/I18n.missingCap.test.ts +82 -0
|
@@ -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
|
+
}
|