@quazardous/qdadm 2.4.4 → 2.5.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/README.md +2 -0
- package/package.json +1 -1
- package/src/components/editors/VanillaJsonEditor.vue +11 -5
- package/src/composables/useListPage.ts +38 -0
- package/src/composables/useListPage.types.ts +2 -0
- package/src/editors/index.ts +5 -0
- package/src/entity/EntityManager.interface.ts +2 -0
- package/src/entity/EntityManager.ts +3 -4
- package/src/entity/auth/EntityAuthAdapter.ts +10 -0
- package/src/entity/auth/SecurityChecker.ts +6 -13
- package/src/gen/connectors/OpenAPIConnector.test.js +253 -1
- package/src/gen/connectors/OpenAPIConnector.ts +110 -4
- package/src/gen/generateManagers.test.js +78 -0
- package/src/gen/generateManagers.ts +33 -7
- package/src/gen/index.ts +1 -0
- package/src/index.ts +26 -0
- package/src/kernel/Kernel.api.ts +5 -5
- package/src/kernel/Kernel.auth.ts +5 -5
- package/src/kernel/Kernel.i18n.ts +5 -5
- package/src/kernel/Kernel.modules.ts +5 -5
- package/src/kernel/Kernel.registries.ts +5 -5
- package/src/kernel/Kernel.routing.ts +5 -5
- package/src/kernel/Kernel.vue.ts +5 -5
- package/src/kernel/KernelContext.entities.ts +5 -5
- package/src/kernel/KernelContext.events.ts +5 -5
- package/src/kernel/KernelContext.i18n.ts +5 -5
- package/src/kernel/KernelContext.permissions.ts +5 -5
- package/src/kernel/KernelContext.routing.ts +5 -5
- package/src/kernel/KernelContext.types.ts +5 -0
- package/src/kernel/KernelContext.zones.ts +5 -5
- package/src/orchestrator/useOrchestrator.ts +20 -5
- package/src/utils/humanize.ts +26 -0
- package/src/utils/index.ts +3 -0
|
@@ -9,6 +9,7 @@
|
|
|
9
9
|
|
|
10
10
|
import { BaseConnector, type ConnectorOptions, type ParseResult, type ParseWarning } from './BaseConnector'
|
|
11
11
|
import { getDefaultType, type CustomMappings } from '../FieldMapper'
|
|
12
|
+
import { humanizeFieldName } from '../../utils/humanize'
|
|
12
13
|
import type { UnifiedEntitySchema, UnifiedFieldSchema, UnifiedFieldType } from '../schema'
|
|
13
14
|
|
|
14
15
|
/**
|
|
@@ -48,6 +49,19 @@ export interface OpenAPIConnectorOptions extends ConnectorOptions {
|
|
|
48
49
|
operationFilter?: OperationFilter | null
|
|
49
50
|
/** Custom type mappings for FieldMapper */
|
|
50
51
|
customMappings?: CustomMappings
|
|
52
|
+
/**
|
|
53
|
+
* Infer a label from the field name when the schema has no `description`
|
|
54
|
+
* (#1255): 'humanize' turns `botUuid` into "Bot Uuid". Opt-in — it changes
|
|
55
|
+
* generated output. Default: off (label only from `description`).
|
|
56
|
+
*/
|
|
57
|
+
inferLabels?: 'humanize' | false
|
|
58
|
+
/**
|
|
59
|
+
* Infer `readOnly` from operation shape (#1255): a field that appears in
|
|
60
|
+
* responses but in no request-body schema is marked `readOnly: true`
|
|
61
|
+
* (an entity with no write operations gets all fields readOnly). Opt-in —
|
|
62
|
+
* it changes generated output. Schema-declared `readOnly` always wins.
|
|
63
|
+
*/
|
|
64
|
+
inferReadOnly?: boolean
|
|
51
65
|
}
|
|
52
66
|
|
|
53
67
|
/**
|
|
@@ -64,6 +78,8 @@ interface EntityWorkingData {
|
|
|
64
78
|
rawSchema: OpenAPISchema | null
|
|
65
79
|
/** Extracted fields */
|
|
66
80
|
fields: Map<string, UnifiedFieldSchema>
|
|
81
|
+
/** Field names seen in request-body schemas (for readOnly inference, #1255) */
|
|
82
|
+
requestFields: Set<string>
|
|
67
83
|
}
|
|
68
84
|
|
|
69
85
|
/**
|
|
@@ -173,6 +189,12 @@ export class OpenAPIConnector extends BaseConnector {
|
|
|
173
189
|
/** Custom type mappings */
|
|
174
190
|
customMappings: CustomMappings
|
|
175
191
|
|
|
192
|
+
/** Label inference mode (#1255) */
|
|
193
|
+
inferLabels: 'humanize' | false
|
|
194
|
+
|
|
195
|
+
/** Whether to infer readOnly from operation shape (#1255) */
|
|
196
|
+
inferReadOnly: boolean
|
|
197
|
+
|
|
176
198
|
/**
|
|
177
199
|
* Create a new OpenAPI connector
|
|
178
200
|
*
|
|
@@ -185,6 +207,8 @@ export class OpenAPIConnector extends BaseConnector {
|
|
|
185
207
|
this.dataWrapper = options.dataWrapper ?? 'data'
|
|
186
208
|
this.operationFilter = options.operationFilter || null
|
|
187
209
|
this.customMappings = options.customMappings || {}
|
|
210
|
+
this.inferLabels = options.inferLabels ?? false
|
|
211
|
+
this.inferReadOnly = options.inferReadOnly ?? false
|
|
188
212
|
}
|
|
189
213
|
|
|
190
214
|
/**
|
|
@@ -201,10 +225,11 @@ export class OpenAPIConnector extends BaseConnector {
|
|
|
201
225
|
* })
|
|
202
226
|
*/
|
|
203
227
|
parse(source: unknown): UnifiedEntitySchema[] {
|
|
204
|
-
const
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
228
|
+
const { schemas, warnings } = this.parseWithWarnings(source)
|
|
229
|
+
for (const warning of warnings) {
|
|
230
|
+
console.warn(`⚠ [${this.name}] ${warning.path}: ${warning.message}`)
|
|
231
|
+
}
|
|
232
|
+
return schemas
|
|
208
233
|
}
|
|
209
234
|
|
|
210
235
|
/**
|
|
@@ -267,6 +292,7 @@ export class OpenAPIConnector extends BaseConnector {
|
|
|
267
292
|
idField: null,
|
|
268
293
|
rawSchema: null,
|
|
269
294
|
fields: new Map(),
|
|
295
|
+
requestFields: new Set(),
|
|
270
296
|
})
|
|
271
297
|
}
|
|
272
298
|
|
|
@@ -371,6 +397,8 @@ export class OpenAPIConnector extends BaseConnector {
|
|
|
371
397
|
const requestSchema = this._extractRequestSchema(op, spec)
|
|
372
398
|
if (requestSchema) {
|
|
373
399
|
this._extractFields(requestSchema, entity.fields, spec, warnings, path)
|
|
400
|
+
// Track writable field names for readOnly inference (#1255)
|
|
401
|
+
this._collectFieldNames(requestSchema, spec, entity.requestFields)
|
|
374
402
|
}
|
|
375
403
|
}
|
|
376
404
|
}
|
|
@@ -565,6 +593,20 @@ export class OpenAPIConnector extends BaseConnector {
|
|
|
565
593
|
continue
|
|
566
594
|
}
|
|
567
595
|
|
|
596
|
+
// Bare object schema: no contract at all. fast-json-stringify (Fastify)
|
|
597
|
+
// serializes such a field to `{}` at runtime, stripping every key (#1240).
|
|
598
|
+
if (this._isBareObjectSchema(resolvedSchema)) {
|
|
599
|
+
warnings.push({
|
|
600
|
+
path: `${sourcePath}#${fieldName}`,
|
|
601
|
+
message:
|
|
602
|
+
`object schema has no \`properties\` and \`additionalProperties\` is not set. ` +
|
|
603
|
+
`Under Fastify (fast-json-stringify) this serializes to \`{}\` at runtime ` +
|
|
604
|
+
`and the generated field type is unusable. ` +
|
|
605
|
+
`For a free-form JSON blob, declare \`additionalProperties: true\`.`,
|
|
606
|
+
code: 'EMPTY_OBJECT_SCHEMA',
|
|
607
|
+
})
|
|
608
|
+
}
|
|
609
|
+
|
|
568
610
|
// Use FieldMapper for type conversion
|
|
569
611
|
const type = getDefaultType(resolvedSchema, this.customMappings)
|
|
570
612
|
|
|
@@ -578,6 +620,8 @@ export class OpenAPIConnector extends BaseConnector {
|
|
|
578
620
|
// Add optional properties
|
|
579
621
|
if (resolvedSchema.description) {
|
|
580
622
|
field.label = resolvedSchema.description
|
|
623
|
+
} else if (this.inferLabels === 'humanize') {
|
|
624
|
+
field.label = humanizeFieldName(fieldName)
|
|
581
625
|
}
|
|
582
626
|
if (resolvedSchema.format) {
|
|
583
627
|
field.format = resolvedSchema.format
|
|
@@ -604,6 +648,57 @@ export class OpenAPIConnector extends BaseConnector {
|
|
|
604
648
|
}
|
|
605
649
|
}
|
|
606
650
|
|
|
651
|
+
/**
|
|
652
|
+
* Collect field names from a schema the same way _extractFields names them
|
|
653
|
+
* (top-level + one-level-nested dotted keys). Used to track which fields
|
|
654
|
+
* appear in request bodies, for readOnly inference (#1255).
|
|
655
|
+
*
|
|
656
|
+
* @private
|
|
657
|
+
* @param schema - JSON Schema object
|
|
658
|
+
* @param spec - Full spec for ref resolution
|
|
659
|
+
* @param into - Set collecting the names
|
|
660
|
+
* @param prefix - Prefix for nested field names
|
|
661
|
+
*/
|
|
662
|
+
private _collectFieldNames(
|
|
663
|
+
schema: OpenAPISchema,
|
|
664
|
+
spec: OpenAPISpec,
|
|
665
|
+
into: Set<string>,
|
|
666
|
+
prefix: string = ''
|
|
667
|
+
): void {
|
|
668
|
+
if (!schema || !schema.properties) return
|
|
669
|
+
|
|
670
|
+
for (const [name, propSchema] of Object.entries(schema.properties)) {
|
|
671
|
+
const fieldName = prefix ? `${prefix}.${name}` : name
|
|
672
|
+
into.add(fieldName)
|
|
673
|
+
|
|
674
|
+
const resolvedSchema = propSchema.$ref ? this._resolveRef(propSchema.$ref, spec) : propSchema
|
|
675
|
+
if (resolvedSchema?.type === 'object' && resolvedSchema.properties && !prefix) {
|
|
676
|
+
this._collectFieldNames(resolvedSchema, spec, into, fieldName)
|
|
677
|
+
}
|
|
678
|
+
}
|
|
679
|
+
}
|
|
680
|
+
|
|
681
|
+
/**
|
|
682
|
+
* Detect a "bare" object schema: type object with no properties, no
|
|
683
|
+
* additionalProperties contract, and no composition/reference that could
|
|
684
|
+
* supply one. Such a schema is the quasi-always-footgun case (#1240) —
|
|
685
|
+
* objects with `properties`, a sub-schema `additionalProperties`, `$ref`
|
|
686
|
+
* or oneOf/anyOf/allOf are deliberately excluded.
|
|
687
|
+
*
|
|
688
|
+
* @private
|
|
689
|
+
* @param schema - Resolved property schema
|
|
690
|
+
* @returns True if the schema is a contract-less object
|
|
691
|
+
*/
|
|
692
|
+
private _isBareObjectSchema(schema: OpenAPISchema): boolean {
|
|
693
|
+
if (schema.type !== 'object') return false
|
|
694
|
+
if (schema.properties && Object.keys(schema.properties).length > 0) return false
|
|
695
|
+
const additionalProperties = schema.additionalProperties
|
|
696
|
+
if (additionalProperties === true) return false
|
|
697
|
+
if (additionalProperties && typeof additionalProperties === 'object') return false
|
|
698
|
+
if (schema.$ref || schema.oneOf || schema.anyOf || schema.allOf) return false
|
|
699
|
+
return true
|
|
700
|
+
}
|
|
701
|
+
|
|
607
702
|
/**
|
|
608
703
|
* Merge field info from secondary source into primary
|
|
609
704
|
*
|
|
@@ -667,6 +762,17 @@ export class OpenAPIConnector extends BaseConnector {
|
|
|
667
762
|
const schemas: Map<string, UnifiedEntitySchema> = new Map()
|
|
668
763
|
|
|
669
764
|
for (const [name, data] of entityData) {
|
|
765
|
+
// readOnly inference (#1255): a field never seen in a request body is
|
|
766
|
+
// not writable through this API. Entities without write operations end
|
|
767
|
+
// up with every field readOnly. Schema-declared readOnly is preserved.
|
|
768
|
+
if (this.inferReadOnly) {
|
|
769
|
+
for (const field of data.fields.values()) {
|
|
770
|
+
if (!field.readOnly && !data.requestFields.has(field.name)) {
|
|
771
|
+
field.readOnly = true
|
|
772
|
+
}
|
|
773
|
+
}
|
|
774
|
+
}
|
|
775
|
+
|
|
670
776
|
const schema: UnifiedEntitySchema = {
|
|
671
777
|
name: data.name,
|
|
672
778
|
endpoint: data.endpoint,
|
|
@@ -436,6 +436,84 @@ describe('generateManagers', () => {
|
|
|
436
436
|
})
|
|
437
437
|
})
|
|
438
438
|
|
|
439
|
+
describe('baseClass option (#1253)', () => {
|
|
440
|
+
function entityConfig(extra = {}) {
|
|
441
|
+
return {
|
|
442
|
+
schema: {
|
|
443
|
+
name: 'users',
|
|
444
|
+
endpoint: '/users',
|
|
445
|
+
fields: {
|
|
446
|
+
id: { name: 'id', type: 'number' },
|
|
447
|
+
name: { name: 'name', type: 'text', required: true }
|
|
448
|
+
}
|
|
449
|
+
},
|
|
450
|
+
endpoint: '/users',
|
|
451
|
+
storageImport: '@quazardous/qdadm',
|
|
452
|
+
storageClass: 'ApiStorage',
|
|
453
|
+
...extra
|
|
454
|
+
}
|
|
455
|
+
}
|
|
456
|
+
|
|
457
|
+
it('class mode extends the global baseClass and imports it', async () => {
|
|
458
|
+
await generateManagers({
|
|
459
|
+
output: testOutputDir,
|
|
460
|
+
classMode: true,
|
|
461
|
+
baseClass: { import: '@/managers/SkyEntityManager', name: 'SkyEntityManager' },
|
|
462
|
+
entities: { users: entityConfig() }
|
|
463
|
+
})
|
|
464
|
+
|
|
465
|
+
const content = await readFile(join(testOutputDir, 'users.ts'), 'utf-8')
|
|
466
|
+
expect(content).toContain("import { SkyEntityManager } from '@/managers/SkyEntityManager'")
|
|
467
|
+
expect(content).toContain('extends SkyEntityManager<UsersEntity>')
|
|
468
|
+
expect(content).not.toContain("import { EntityManager } from '@quazardous/qdadm'")
|
|
469
|
+
// Options/record types still come from qdadm
|
|
470
|
+
expect(content).toContain("import type { EntityRecord, EntityManagerOptions } from '@quazardous/qdadm'")
|
|
471
|
+
})
|
|
472
|
+
|
|
473
|
+
it('instance mode instantiates the baseClass', async () => {
|
|
474
|
+
await generateManagers({
|
|
475
|
+
output: testOutputDir,
|
|
476
|
+
baseClass: { import: './base', name: 'MyBase' },
|
|
477
|
+
entities: { users: entityConfig() }
|
|
478
|
+
})
|
|
479
|
+
|
|
480
|
+
const content = await readFile(join(testOutputDir, 'usersManager.ts'), 'utf-8')
|
|
481
|
+
expect(content).toContain("import { MyBase } from './base'")
|
|
482
|
+
expect(content).toContain('new MyBase<UsersEntity>(')
|
|
483
|
+
expect(content).not.toContain('new EntityManager<UsersEntity>(')
|
|
484
|
+
})
|
|
485
|
+
|
|
486
|
+
it('per-entity baseClass overrides the global one', async () => {
|
|
487
|
+
await generateManagers({
|
|
488
|
+
output: testOutputDir,
|
|
489
|
+
classMode: true,
|
|
490
|
+
baseClass: { import: './globalBase', name: 'GlobalBase' },
|
|
491
|
+
entities: {
|
|
492
|
+
users: entityConfig({
|
|
493
|
+
baseClass: { import: './localBase', name: 'LocalBase' }
|
|
494
|
+
})
|
|
495
|
+
}
|
|
496
|
+
})
|
|
497
|
+
|
|
498
|
+
const content = await readFile(join(testOutputDir, 'users.ts'), 'utf-8')
|
|
499
|
+
expect(content).toContain("import { LocalBase } from './localBase'")
|
|
500
|
+
expect(content).toContain('extends LocalBase<UsersEntity>')
|
|
501
|
+
expect(content).not.toContain('GlobalBase')
|
|
502
|
+
})
|
|
503
|
+
|
|
504
|
+
it('defaults to EntityManager when no baseClass is given', async () => {
|
|
505
|
+
await generateManagers({
|
|
506
|
+
output: testOutputDir,
|
|
507
|
+
classMode: true,
|
|
508
|
+
entities: { users: entityConfig() }
|
|
509
|
+
})
|
|
510
|
+
|
|
511
|
+
const content = await readFile(join(testOutputDir, 'users.ts'), 'utf-8')
|
|
512
|
+
expect(content).toContain("import { EntityManager } from '@quazardous/qdadm'")
|
|
513
|
+
expect(content).toContain('extends EntityManager<UsersEntity>')
|
|
514
|
+
})
|
|
515
|
+
})
|
|
516
|
+
|
|
439
517
|
describe('default output directory', () => {
|
|
440
518
|
it('uses default output when not specified', async () => {
|
|
441
519
|
// This test would write to actual src/generated/managers/
|
|
@@ -22,6 +22,21 @@ import type { EntityDecorator } from './decorators'
|
|
|
22
22
|
*/
|
|
23
23
|
const DEFAULT_OUTPUT_DIR = 'src/generated/managers/'
|
|
24
24
|
|
|
25
|
+
/**
|
|
26
|
+
* Custom base class for generated managers (#1253).
|
|
27
|
+
*
|
|
28
|
+
* The class MUST extend EntityManager, stay generic over the entity type
|
|
29
|
+
* and pass constructor options through — the generated code does
|
|
30
|
+
* `class Generated<X>Manager extends <name><XEntity>` and calls
|
|
31
|
+
* `super({ ...schema options, storage, ...options })`.
|
|
32
|
+
*/
|
|
33
|
+
export interface GenerateManagersBaseClass {
|
|
34
|
+
/** Import specifier (e.g. '@/managers/SkyEntityManager') */
|
|
35
|
+
import: string
|
|
36
|
+
/** Exported class name (e.g. 'SkyEntityManager') */
|
|
37
|
+
name: string
|
|
38
|
+
}
|
|
39
|
+
|
|
25
40
|
/**
|
|
26
41
|
* Entity configuration for generation
|
|
27
42
|
*/
|
|
@@ -40,6 +55,8 @@ export interface GenerateManagersEntityConfig {
|
|
|
40
55
|
decorators?: EntityDecorator
|
|
41
56
|
/** Generate class instead of instance (for extension) */
|
|
42
57
|
classMode?: boolean
|
|
58
|
+
/** Base class for this entity's generated manager (overrides global) */
|
|
59
|
+
baseClass?: GenerateManagersBaseClass
|
|
43
60
|
}
|
|
44
61
|
|
|
45
62
|
/**
|
|
@@ -50,6 +67,8 @@ export interface GenerateManagersConfig {
|
|
|
50
67
|
output?: string
|
|
51
68
|
/** Generate classes instead of instances (for extension pattern) */
|
|
52
69
|
classMode?: boolean
|
|
70
|
+
/** Base class for all generated managers (default: EntityManager) */
|
|
71
|
+
baseClass?: GenerateManagersBaseClass
|
|
53
72
|
/** Entity configurations keyed by entity name */
|
|
54
73
|
entities: Record<string, GenerateManagersEntityConfig>
|
|
55
74
|
}
|
|
@@ -175,12 +194,19 @@ function serializeValue(value: unknown, indent: number = 0): string {
|
|
|
175
194
|
function generateManagerSource(
|
|
176
195
|
entityName: string,
|
|
177
196
|
entityConfig: GenerateManagersEntityConfig,
|
|
178
|
-
globalOptions: { classMode?: boolean } = {}
|
|
197
|
+
globalOptions: { classMode?: boolean; baseClass?: GenerateManagersBaseClass } = {}
|
|
179
198
|
): string {
|
|
180
199
|
const { schema, endpoint, storageImport, storageClass, storageOptions = {}, decorators } = entityConfig
|
|
181
200
|
const classMode = entityConfig.classMode ?? globalOptions.classMode ?? false
|
|
182
201
|
const className = toPascalCase(entityName)
|
|
183
202
|
|
|
203
|
+
// Custom base class (#1253): per-entity wins over global, default EntityManager.
|
|
204
|
+
const baseClass = entityConfig.baseClass ?? globalOptions.baseClass
|
|
205
|
+
const baseName = baseClass?.name ?? 'EntityManager'
|
|
206
|
+
const baseImport = baseClass
|
|
207
|
+
? `import { ${baseClass.name} } from '${baseClass.import}'`
|
|
208
|
+
: `import { EntityManager } from '@quazardous/qdadm'`
|
|
209
|
+
|
|
184
210
|
// Build storage options object
|
|
185
211
|
const fullStorageOptions: Record<string, unknown> = {
|
|
186
212
|
endpoint,
|
|
@@ -240,7 +266,7 @@ function generateManagerSource(
|
|
|
240
266
|
* Endpoint: ${endpoint}
|
|
241
267
|
*/
|
|
242
268
|
|
|
243
|
-
|
|
269
|
+
${baseImport}
|
|
244
270
|
import type { EntityRecord, EntityManagerOptions } from '@quazardous/qdadm'
|
|
245
271
|
import { ${storageClass} } from '${storageImport}'
|
|
246
272
|
|
|
@@ -249,9 +275,9 @@ ${entityInterface}
|
|
|
249
275
|
/**
|
|
250
276
|
* Generated base manager for ${entityName}
|
|
251
277
|
*
|
|
252
|
-
* @extends
|
|
278
|
+
* @extends ${baseName}
|
|
253
279
|
*/
|
|
254
|
-
export class Generated${className}Manager extends
|
|
280
|
+
export class Generated${className}Manager extends ${baseName}<${className}Entity> {
|
|
255
281
|
constructor(options: Partial<EntityManagerOptions<${className}Entity>> = {}) {
|
|
256
282
|
super({
|
|
257
283
|
...${serializeValue(managerOptions, 2)},
|
|
@@ -274,7 +300,7 @@ export class Generated${className}Manager extends EntityManager<${className}Enti
|
|
|
274
300
|
* Endpoint: ${endpoint}
|
|
275
301
|
*/
|
|
276
302
|
|
|
277
|
-
|
|
303
|
+
${baseImport}
|
|
278
304
|
import type { EntityRecord } from '@quazardous/qdadm'
|
|
279
305
|
import { ${storageClass} } from '${storageImport}'
|
|
280
306
|
|
|
@@ -295,7 +321,7 @@ const storageOptions = ${serializeValue(fullStorageOptions, 0)}
|
|
|
295
321
|
*
|
|
296
322
|
* Provides CRUD operations for ${entityName} entity.
|
|
297
323
|
*/
|
|
298
|
-
export const ${entityName}Manager = new
|
|
324
|
+
export const ${entityName}Manager = new ${baseName}<${className}Entity>({
|
|
299
325
|
...${serializeValue(managerOptions, 0)},
|
|
300
326
|
storage: new ${storageClass}<${className}Entity>(storageOptions)
|
|
301
327
|
})
|
|
@@ -349,7 +375,7 @@ export async function generateManagers(config: GenerateManagersConfig): Promise<
|
|
|
349
375
|
}
|
|
350
376
|
|
|
351
377
|
const outputDir = config.output || DEFAULT_OUTPUT_DIR
|
|
352
|
-
const globalOptions = { classMode: config.classMode ?? false }
|
|
378
|
+
const globalOptions = { classMode: config.classMode ?? false, baseClass: config.baseClass }
|
|
353
379
|
const generatedFiles: string[] = []
|
|
354
380
|
|
|
355
381
|
// Validate each entity config
|
package/src/gen/index.ts
CHANGED
package/src/index.ts
CHANGED
|
@@ -96,6 +96,8 @@ export {
|
|
|
96
96
|
type SeverityMapValue,
|
|
97
97
|
type SeverityMap,
|
|
98
98
|
type RoutingContext,
|
|
99
|
+
type StorageResolution,
|
|
100
|
+
type ResolvedStorage,
|
|
99
101
|
type PresaveContext,
|
|
100
102
|
type PostsaveContext,
|
|
101
103
|
type PredeleteContext,
|
|
@@ -150,6 +152,30 @@ export type {
|
|
|
150
152
|
// ════════════════════════════════════════════════════════════════════════════
|
|
151
153
|
export * from './orchestrator/index'
|
|
152
154
|
|
|
155
|
+
/**
|
|
156
|
+
* Typed manager registry (#1253) — augment it from your app to make
|
|
157
|
+
* `getManager()` / `useEntity()` return your concrete EntityManager
|
|
158
|
+
* subclasses instead of `EntityManager<EntityRecord>`:
|
|
159
|
+
*
|
|
160
|
+
* ```ts
|
|
161
|
+
* declare module '@quazardous/qdadm' {
|
|
162
|
+
* interface QdadmManagerRegistry {
|
|
163
|
+
* bots: BotsManager
|
|
164
|
+
* botPools: BotPoolsManager
|
|
165
|
+
* }
|
|
166
|
+
* }
|
|
167
|
+
* ```
|
|
168
|
+
*
|
|
169
|
+
* Entity names not declared here keep the current fallback behavior.
|
|
170
|
+
*
|
|
171
|
+
* NOTE: deliberately declared in this file (not re-exported from a
|
|
172
|
+
* sub-module) — TypeScript module augmentation does not merge through
|
|
173
|
+
* re-export aliases, and consumers augment '@quazardous/qdadm', which
|
|
174
|
+
* resolves here.
|
|
175
|
+
*/
|
|
176
|
+
// eslint-disable-next-line @typescript-eslint/no-empty-object-type
|
|
177
|
+
export interface QdadmManagerRegistry {}
|
|
178
|
+
|
|
153
179
|
// ════════════════════════════════════════════════════════════════════════════
|
|
154
180
|
// COMPOSABLES (excluding types that conflict with other modules)
|
|
155
181
|
// ════════════════════════════════════════════════════════════════════════════
|
package/src/kernel/Kernel.api.ts
CHANGED
|
@@ -1,14 +1,14 @@
|
|
|
1
1
|
import type { AxiosLikeClient, AxiosError } from './Kernel.types'
|
|
2
|
+
import type { Kernel } from './Kernel'
|
|
2
3
|
|
|
3
|
-
//
|
|
4
|
-
type Self =
|
|
4
|
+
// #1196 Phase B — this-typing against the real Kernel shape (was Self = any)
|
|
5
|
+
type Self = Kernel
|
|
5
6
|
|
|
6
7
|
/**
|
|
7
8
|
* Patch Kernel prototype with API client methods.
|
|
8
9
|
*/
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
const proto = KernelClass.prototype as Self
|
|
10
|
+
export function applyApiMethods(KernelClass: { prototype: Kernel }): void {
|
|
11
|
+
const proto = KernelClass.prototype
|
|
12
12
|
|
|
13
13
|
/**
|
|
14
14
|
* Setup an axios client with automatic auth and error handling
|
|
@@ -1,14 +1,14 @@
|
|
|
1
1
|
import { authFactory, CompositeAuthAdapter } from '../entity/auth'
|
|
2
|
+
import type { Kernel } from './Kernel'
|
|
2
3
|
|
|
3
|
-
//
|
|
4
|
-
type Self =
|
|
4
|
+
// #1196 Phase B — this-typing against the real Kernel shape (was Self = any)
|
|
5
|
+
type Self = Kernel
|
|
5
6
|
|
|
6
7
|
/**
|
|
7
8
|
* Patch Kernel prototype with auth-related methods.
|
|
8
9
|
*/
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
const proto = KernelClass.prototype as Self
|
|
10
|
+
export function applyAuthMethods(KernelClass: { prototype: Kernel }): void {
|
|
11
|
+
const proto = KernelClass.prototype
|
|
12
12
|
|
|
13
13
|
/**
|
|
14
14
|
* Resolve entityAuthAdapter through authFactory
|
|
@@ -9,13 +9,13 @@
|
|
|
9
9
|
|
|
10
10
|
import { I18n } from '../i18n/I18n'
|
|
11
11
|
import type { I18nOptions } from '../i18n/types'
|
|
12
|
+
import type { Kernel } from './Kernel'
|
|
12
13
|
|
|
13
|
-
//
|
|
14
|
-
type Self =
|
|
14
|
+
// #1196 Phase B — this-typing against the real Kernel shape (was Self = any)
|
|
15
|
+
type Self = Kernel
|
|
15
16
|
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
const proto = KernelClass.prototype as Self
|
|
17
|
+
export function applyI18nMethods(KernelClass: { prototype: Kernel }): void {
|
|
18
|
+
const proto = KernelClass.prototype
|
|
19
19
|
|
|
20
20
|
proto._createI18n = function (this: Self): void {
|
|
21
21
|
const opts = (this.options.i18n as I18nOptions | undefined) ?? {}
|
|
@@ -1,15 +1,15 @@
|
|
|
1
1
|
import { initModules, setSectionOrder } from '../module/moduleRegistry'
|
|
2
2
|
import { createModuleLoader, type ModuleLike } from './ModuleLoader'
|
|
3
3
|
import { createKernelContext, type KernelContext } from './KernelContext'
|
|
4
|
-
|
|
5
|
-
|
|
4
|
+
import type { Kernel } from './Kernel'
|
|
5
|
+
// #1196 Phase B — this-typing against the real Kernel shape (was Self = any)
|
|
6
|
+
type Self = Kernel
|
|
6
7
|
|
|
7
8
|
/**
|
|
8
9
|
* Patch Kernel prototype with module-related methods.
|
|
9
10
|
*/
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
const proto = KernelClass.prototype as Self
|
|
11
|
+
export function applyModuleMethods(KernelClass: { prototype: Kernel }): void {
|
|
12
|
+
const proto = KernelClass.prototype
|
|
13
13
|
|
|
14
14
|
/**
|
|
15
15
|
* Fire entity cache warmups
|
|
@@ -13,15 +13,15 @@ import { ActiveStack } from '../chain/ActiveStack.js'
|
|
|
13
13
|
import { StackHydrator } from '../chain/StackHydrator.js'
|
|
14
14
|
import { Orchestrator } from '../orchestrator/Orchestrator'
|
|
15
15
|
import type { EntityAuthAdapter } from '../entity/auth/EntityAuthAdapter'
|
|
16
|
-
|
|
17
|
-
|
|
16
|
+
import type { Kernel } from './Kernel'
|
|
17
|
+
// #1196 Phase B — this-typing against the real Kernel shape (was Self = any)
|
|
18
|
+
type Self = Kernel
|
|
18
19
|
|
|
19
20
|
/**
|
|
20
21
|
* Patch Kernel prototype with registry-related methods.
|
|
21
22
|
*/
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
const proto = KernelClass.prototype as Self
|
|
23
|
+
export function applyRegistryMethods(KernelClass: { prototype: Kernel }): void {
|
|
24
|
+
const proto = KernelClass.prototype
|
|
25
25
|
|
|
26
26
|
/**
|
|
27
27
|
* Create signal bus for event-driven communication.
|
|
@@ -8,15 +8,15 @@ import {
|
|
|
8
8
|
type RouteRecordNormalized,
|
|
9
9
|
} from 'vue-router'
|
|
10
10
|
import { getRoutes } from '../module/moduleRegistry'
|
|
11
|
-
|
|
12
|
-
|
|
11
|
+
import type { Kernel } from './Kernel'
|
|
12
|
+
// #1196 Phase B — this-typing against the real Kernel shape (was Self = any)
|
|
13
|
+
type Self = Kernel
|
|
13
14
|
|
|
14
15
|
/**
|
|
15
16
|
* Patch Kernel prototype with routing-related methods.
|
|
16
17
|
*/
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
const proto = KernelClass.prototype as Self
|
|
18
|
+
export function applyRoutingMethods(KernelClass: { prototype: Kernel }): void {
|
|
19
|
+
const proto = KernelClass.prototype
|
|
20
20
|
|
|
21
21
|
/**
|
|
22
22
|
* Create Vue Router.
|
package/src/kernel/Kernel.vue.ts
CHANGED
|
@@ -16,8 +16,9 @@ import ToastListener from '../toast/ToastListener.vue'
|
|
|
16
16
|
import { createQdadm } from '../plugin.js'
|
|
17
17
|
import { createNotificationStore, NOTIFICATION_KEY } from '../notifications/NotificationStore'
|
|
18
18
|
import { I18N_INJECTION_KEY } from '../i18n/useI18n'
|
|
19
|
-
|
|
20
|
-
|
|
19
|
+
import type { Kernel } from './Kernel'
|
|
20
|
+
// #1196 Phase B — this-typing against the real Kernel shape (was Self = any)
|
|
21
|
+
type Self = Kernel
|
|
21
22
|
|
|
22
23
|
/**
|
|
23
24
|
* Reactive reference to the debug bar component, set by the Kernel
|
|
@@ -57,9 +58,8 @@ export function setQdadmDebugBar(component: Component | null): void {
|
|
|
57
58
|
/**
|
|
58
59
|
* Patch Kernel prototype with Vue app creation methods.
|
|
59
60
|
*/
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
const proto = KernelClass.prototype as Self
|
|
61
|
+
export function applyVueMethods(KernelClass: { prototype: Kernel }): void {
|
|
62
|
+
const proto = KernelClass.prototype
|
|
63
63
|
|
|
64
64
|
/**
|
|
65
65
|
* Create layout components map for useLayoutResolver
|
|
@@ -8,13 +8,13 @@ import { managerFactory, type ManagerFactoryContext } from '../entity/factory.js
|
|
|
8
8
|
import type { EntityManager } from '../entity/EntityManager'
|
|
9
9
|
import { UsersManager } from '../security/UsersManager'
|
|
10
10
|
import type { UserEntityOptions } from './KernelContext.types'
|
|
11
|
+
import type { KernelContext } from './KernelContext'
|
|
11
12
|
|
|
12
|
-
//
|
|
13
|
-
type Self =
|
|
13
|
+
// #1196 Phase B — this-typing against the real KernelContext shape (was Self = any)
|
|
14
|
+
type Self = KernelContext
|
|
14
15
|
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
const proto = KernelContextClass.prototype as Self
|
|
16
|
+
export function applyEntityMethods(KernelContextClass: { prototype: KernelContext }): void {
|
|
17
|
+
const proto = KernelContextClass.prototype
|
|
18
18
|
|
|
19
19
|
/**
|
|
20
20
|
* Register an entity manager.
|
|
@@ -12,13 +12,13 @@ import type {
|
|
|
12
12
|
HookHandler,
|
|
13
13
|
SignalHandler,
|
|
14
14
|
} from './KernelContext.types'
|
|
15
|
+
import type { KernelContext } from './KernelContext'
|
|
15
16
|
|
|
16
|
-
//
|
|
17
|
-
type Self =
|
|
17
|
+
// #1196 Phase B — this-typing against the real KernelContext shape (was Self = any)
|
|
18
|
+
type Self = KernelContext
|
|
18
19
|
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
const proto = KernelContextClass.prototype as Self
|
|
20
|
+
export function applyEventMethods(KernelContextClass: { prototype: KernelContext }): void {
|
|
21
|
+
const proto = KernelContextClass.prototype
|
|
22
22
|
|
|
23
23
|
proto.on = function (
|
|
24
24
|
this: Self,
|
|
@@ -6,13 +6,13 @@
|
|
|
6
6
|
*/
|
|
7
7
|
|
|
8
8
|
import type { AliasPattern, MessagesBundle, TranslationProvider } from '../i18n/types'
|
|
9
|
+
import type { KernelContext } from './KernelContext'
|
|
9
10
|
|
|
10
|
-
//
|
|
11
|
-
type Self =
|
|
11
|
+
// #1196 Phase B — this-typing against the real KernelContext shape (was Self = any)
|
|
12
|
+
type Self = KernelContext
|
|
12
13
|
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
const proto = KernelContextClass.prototype as Self
|
|
14
|
+
export function applyI18nMethods(KernelContextClass: { prototype: KernelContext }): void {
|
|
15
|
+
const proto = KernelContextClass.prototype
|
|
16
16
|
|
|
17
17
|
proto.messages = function (this: Self, locale: string, bundle: MessagesBundle): Self {
|
|
18
18
|
if (this._kernel.i18nInstance) {
|
|
@@ -5,13 +5,13 @@
|
|
|
5
5
|
*/
|
|
6
6
|
|
|
7
7
|
import type { PermissionMeta, PermissionOptions } from './KernelContext.types'
|
|
8
|
+
import type { KernelContext } from './KernelContext'
|
|
8
9
|
|
|
9
|
-
//
|
|
10
|
-
type Self =
|
|
10
|
+
// #1196 Phase B — this-typing against the real KernelContext shape (was Self = any)
|
|
11
|
+
type Self = KernelContext
|
|
11
12
|
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
const proto = KernelContextClass.prototype as Self
|
|
13
|
+
export function applyPermissionMethods(KernelContextClass: { prototype: KernelContext }): void {
|
|
14
|
+
const proto = KernelContextClass.prototype
|
|
15
15
|
|
|
16
16
|
proto.permissions = function (
|
|
17
17
|
this: Self,
|