qdadm 1.13.1 → 1.19.2

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.
Files changed (86) hide show
  1. package/package.json +7 -3
  2. package/src/chain/ActiveStack.ts +79 -98
  3. package/src/chain/StackHydrator.ts +3 -2
  4. package/src/chain/index.ts +7 -1
  5. package/src/components/QdadmRoot.vue +52 -0
  6. package/src/components/edit/FormActions.vue +9 -6
  7. package/src/components/edit/LookupPickerDialog.vue +6 -3
  8. package/src/components/index.ts +6 -0
  9. package/src/composables/useEntityItemFormPage.ts +1 -0
  10. package/src/composables/useEntityItemShowPage.ts +1 -0
  11. package/src/composables/useFieldManager.ts +100 -3
  12. package/src/composables/useListPage.ts +50 -59
  13. package/src/composables/useListPage.utils.ts +101 -0
  14. package/src/composables/useNavigation.ts +26 -3
  15. package/src/composables/useOptionsLookup.ts +5 -1
  16. package/src/gen/generateManagers.test.js +27 -0
  17. package/src/gen/generateManagers.ts +12 -0
  18. package/src/hooks/HookRegistry.ts +14 -435
  19. package/src/i18n/I18n.ts +344 -0
  20. package/src/i18n/IncrementalDomainProvider.ts +153 -0
  21. package/src/i18n/InlineTranslationProvider.ts +4 -0
  22. package/src/i18n/LazyTranslationProvider.ts +102 -0
  23. package/src/i18n/MessagesRegistry.ts +4 -0
  24. package/src/i18n/Resolver.ts +4 -0
  25. package/src/i18n/__tests__/I18n.test.ts +169 -0
  26. package/src/i18n/__tests__/IncrementalDomainProvider.test.ts +146 -0
  27. package/src/i18n/__tests__/LazyTranslationProvider.test.ts +100 -0
  28. package/src/i18n/__tests__/Resolver.test.ts +271 -0
  29. package/src/i18n/defaults/DefaultCoreProvider.ts +28 -0
  30. package/src/i18n/defaults/core.en.yml +55 -0
  31. package/src/i18n/defaults/core.fr.yml +55 -0
  32. package/src/i18n/index.ts +55 -0
  33. package/src/i18n/loaders/raw-modules.d.ts +15 -0
  34. package/src/i18n/loaders/yaml.ts +35 -0
  35. package/src/i18n/strategies.ts +4 -0
  36. package/src/i18n/types.ts +34 -0
  37. package/src/i18n/useI18n.ts +34 -0
  38. package/src/index.ts +37 -0
  39. package/src/kernel/EventRouter.ts +17 -300
  40. package/src/kernel/Kernel.i18n.ts +29 -0
  41. package/src/kernel/Kernel.modules.ts +6 -0
  42. package/src/kernel/Kernel.registries.ts +10 -2
  43. package/src/kernel/Kernel.routing.ts +43 -1
  44. package/src/kernel/Kernel.ts +43 -0
  45. package/src/kernel/Kernel.types.ts +52 -1
  46. package/src/kernel/Kernel.vue.ts +121 -15
  47. package/src/kernel/KernelContext.entities.ts +80 -0
  48. package/src/kernel/KernelContext.events.ts +57 -0
  49. package/src/kernel/KernelContext.i18n.ts +37 -0
  50. package/src/kernel/KernelContext.permissions.ts +38 -0
  51. package/src/kernel/KernelContext.routing.ts +280 -0
  52. package/src/kernel/KernelContext.ts +125 -834
  53. package/src/kernel/KernelContext.types.ts +173 -0
  54. package/src/kernel/KernelContext.zones.ts +54 -0
  55. package/src/kernel/SSEBridge.ts +7 -362
  56. package/src/kernel/SignalBus.ts +24 -148
  57. package/src/modules/debug/AuthCollector.ts +48 -1
  58. package/src/modules/debug/Collector.ts +16 -302
  59. package/src/modules/debug/DebugBridge.ts +10 -171
  60. package/src/modules/debug/DebugModule.ts +35 -5
  61. package/src/modules/debug/EntitiesCollector.ts +97 -1
  62. package/src/modules/debug/ErrorCollector.ts +2 -77
  63. package/src/modules/debug/I18nCollector.ts +9 -0
  64. package/src/modules/debug/LocalStorageAdapter.ts +3 -147
  65. package/src/modules/debug/RouterCollector.ts +101 -1
  66. package/src/modules/debug/SignalCollector.ts +2 -150
  67. package/src/modules/debug/ToastCollector.ts +2 -91
  68. package/src/modules/debug/ZonesCollector.ts +93 -1
  69. package/src/modules/debug/components/DebugBar.vue +19 -775
  70. package/src/modules/debug/components/adminPanelsConfig.ts +42 -0
  71. package/src/modules/debug/components/index.ts +4 -3
  72. package/src/modules/debug/components/panels/AuthPanel.vue +1 -1
  73. package/src/modules/debug/components/panels/EntitiesPanel.vue +5 -5
  74. package/src/modules/debug/components/panels/I18nPanel.vue +738 -0
  75. package/src/modules/debug/components/panels/RouterPanel.vue +1 -1
  76. package/src/modules/debug/components/panels/index.ts +10 -4
  77. package/src/modules/debug/index.ts +15 -0
  78. package/src/modules/debug/styles.scss +22 -18
  79. package/src/utils/index.ts +0 -3
  80. package/src/vite/qdadmDebugPlugin.ts +401 -0
  81. package/src/vite-env.d.ts +16 -0
  82. package/src/modules/debug/components/ObjectTree.vue +0 -123
  83. package/src/modules/debug/components/panels/EntriesPanel.vue +0 -100
  84. package/src/modules/debug/components/panels/SignalsPanel.vue +0 -188
  85. package/src/modules/debug/components/panels/ToastsPanel.vue +0 -45
  86. package/src/utils/debugInjector.ts +0 -306
@@ -1,8 +1,20 @@
1
1
  /**
2
- * KernelContext - Fluent API wrapper for module registration
2
+ * KernelContext - Fluent API wrapper for module registration.
3
3
  *
4
4
  * Provides a chainable interface for modules to register entities, routes,
5
- * navigation items, zones, blocks, and signal handlers.
5
+ * navigation items, zones, blocks, signal handlers, hooks, permissions and
6
+ * translations.
7
+ *
8
+ * The class shell here keeps the constructor + service getters; the fluent
9
+ * registration methods live in dedicated patch modules following the same
10
+ * pattern as `Kernel.ts`:
11
+ *
12
+ * - KernelContext.entities.ts — entity, userEntity
13
+ * - KernelContext.routing.ts — routes, navItem, routeFamily, crud, childPage
14
+ * - KernelContext.zones.ts — zone, block, provide, component
15
+ * - KernelContext.events.ts — on, hook, defer
16
+ * - KernelContext.permissions.ts — permissions, entityPermissions
17
+ * - KernelContext.i18n.ts — messages, aliases, messagesProvider
6
18
  *
7
19
  * Usage in a module:
8
20
  * ```ts
@@ -15,7 +27,6 @@
15
27
  * { path: ':id', name: 'users-edit', component: UserEdit }
16
28
  * ])
17
29
  * .navItem({ section: 'Admin', route: 'users', icon: 'pi pi-users', label: 'Users' })
18
- * .routeFamily('users', ['users-'])
19
30
  * .zone('users-list-header')
20
31
  * .block('users-list-header', { component: UserStats, weight: 10 })
21
32
  * .on('users:created', (event) => console.log('User created:', event))
@@ -26,11 +37,8 @@
26
37
 
27
38
  import type { App, Component } from 'vue'
28
39
  import type { Router, RouteRecordRaw } from 'vue-router'
29
- import { managerFactory, type ManagerFactoryContext } from '../entity/factory.js'
30
- import { registry, getRoutes } from '../module/moduleRegistry'
31
- import { UsersManager, type UsersManagerOptions } from '../security/UsersManager'
32
- import type { SignalBus } from './SignalBus'
33
40
  import type { ListenerOptions } from '@quazardous/quarkernel'
41
+ import type { SignalBus } from './SignalBus'
34
42
  import type { Orchestrator } from '../orchestrator/Orchestrator'
35
43
  import type { HookRegistry } from '../hooks/HookRegistry'
36
44
  import type { ZoneRegistry } from '../zones/ZoneRegistry'
@@ -38,192 +46,60 @@ import type { DeferredRegistry } from '../deferred/DeferredRegistry.js'
38
46
  import type { SecurityChecker } from '../entity/auth/SecurityChecker'
39
47
  import type { PermissionRegistry } from '../security/PermissionRegistry'
40
48
  import type { EntityManager } from '../entity/EntityManager'
41
-
42
- // ─────────────────────────────────────────────────────────────────────────────
43
- // Types
44
- // ─────────────────────────────────────────────────────────────────────────────
45
-
46
- /**
47
- * Extended Module interface with internal cleanup method
48
- */
49
- interface ModuleWithCleanup {
50
- constructor?: { name?: string }
51
- _addSignalCleanup?: (cleanup: () => void) => void
52
- }
53
-
54
- /**
55
- * Auth adapter interface
56
- */
57
- interface AuthAdapter {
58
- getUser(): unknown
59
- [key: string]: unknown
60
- }
61
-
62
- /**
63
- * Kernel options interface
64
- */
65
- interface KernelOptions {
66
- debug?: boolean
67
- authAdapter?: AuthAdapter | null
68
- storageResolver?: unknown
69
- managerResolver?: unknown
70
- managerRegistry?: Record<string, unknown>
71
- [key: string]: unknown
72
- }
73
-
74
- /**
75
- * Kernel interface for type safety
76
- */
77
- interface KernelInterface {
78
- vueApp: App | null
79
- router: Router | null
80
- signals: SignalBus | null
81
- orchestrator: Orchestrator | null
82
- zoneRegistry: ZoneRegistry | null
83
- hookRegistry: HookRegistry | null
84
- deferred: DeferredRegistry | null
85
- securityChecker: SecurityChecker | null
86
- permissionRegistry: PermissionRegistry | null
87
- activeStack: unknown
88
- stackHydrator: unknown
89
- options: KernelOptions
90
- _pendingProvides: Map<string | symbol, unknown>
91
- _pendingComponents: Map<string, Component>
92
- }
93
-
94
- /**
95
- * Navigation item configuration
96
- */
97
- export interface NavItem {
98
- section: string
99
- route: string
100
- label: string
101
- icon?: string
102
- exact?: boolean
103
- entity?: string
104
- }
105
-
106
- /**
107
- * Zone configuration
108
- */
109
- export interface ZoneOptions {
110
- default?: Component
111
- }
112
-
113
- /**
114
- * Block configuration
115
- */
116
- export interface BlockConfig {
117
- component: Component
118
- weight?: number
119
- props?: Record<string, unknown>
120
- id?: string
121
- operation?: 'add' | 'replace' | 'extend' | 'wrap'
122
- /** Block ID to replace (required if operation='replace') */
123
- replaces?: string
124
- /** Block ID to insert before (for operation='extend') */
125
- before?: string
126
- /** Block ID to insert after (for operation='extend') */
127
- after?: string
128
- /** Block ID to wrap (required if operation='wrap') */
129
- wraps?: string
130
- }
131
-
132
- /**
133
- * Route options
134
- */
135
- export interface RouteOptions {
136
- entity?: string
137
- parent?: ParentConfig
138
- label?: string
139
- layout?: string
140
- }
141
-
142
- /**
143
- * Parent route configuration
144
- */
145
- interface ParentConfig {
146
- entity: string
147
- param: string
148
- foreignKey?: string
149
- }
150
-
151
- /**
152
- * CRUD page components
153
- */
154
- export interface CrudPages {
155
- list?: () => Promise<{ default: Component }>
156
- show?: () => Promise<{ default: Component }>
157
- form?: () => Promise<{ default: Component }>
158
- create?: () => Promise<{ default: Component }>
159
- edit?: () => Promise<{ default: Component }>
160
- }
161
-
162
- /**
163
- * CRUD options
164
- */
165
- export interface CrudOptions {
166
- nav?: {
167
- section: string
168
- icon?: string
169
- label?: string
170
- }
171
- routePrefix?: string
172
- /** Override the URL path segment (defaults to entity name). Useful for camelCase entities like 'jobTasks' → 'tasks'. */
173
- pathSegment?: string
174
- parentRoute?: string
175
- foreignKey?: string
176
- label?: string
177
- }
178
-
179
- /**
180
- * Child page options for non-entity child routes
181
- */
182
- export interface ChildPageOptions {
183
- component: () => Promise<{ default: Component }>
184
- label?: string
185
- icon?: string
186
- meta?: Record<string, unknown>
187
- }
188
-
189
- /**
190
- * User entity options
191
- */
192
- export type UserEntityOptions = UsersManagerOptions
193
-
194
- /**
195
- * Permission metadata
196
- */
197
- export interface PermissionMeta {
198
- label: string
199
- description?: string
200
- }
201
-
202
- /**
203
- * Permission options
204
- */
205
- export interface PermissionOptions {
206
- isEntity?: boolean
207
- }
208
-
209
- /**
210
- * Signal handler function type
211
- */
212
- type SignalHandler = (event: { data: unknown; [key: string]: unknown }) => void
213
-
214
- /**
215
- * Hook handler function type
216
- */
217
- type HookHandler = (context: unknown) => unknown | Promise<unknown>
218
-
219
- /**
220
- * Deferred factory function type
221
- */
222
- type DeferredFactory = () => Promise<void>
49
+ import type { I18n } from '../i18n/I18n'
50
+ import type { AliasPattern, MessagesBundle, TranslationProvider } from '../i18n/types'
51
+
52
+ // Internal types (shared with patch modules, not exported here).
53
+ import type {
54
+ AuthAdapter,
55
+ DeferredFactory,
56
+ HookHandler,
57
+ KernelInterface,
58
+ ModuleWithCleanup,
59
+ SignalHandler,
60
+ } from './KernelContext.types'
61
+
62
+ // Public types — re-exported to keep the existing barrel API stable.
63
+ export type {
64
+ BlockConfig,
65
+ ChildPageOptions,
66
+ CrudOptions,
67
+ CrudPages,
68
+ NavItem,
69
+ ParentConfig,
70
+ PermissionMeta,
71
+ PermissionOptions,
72
+ RouteOptions,
73
+ UserEntityOptions,
74
+ ZoneOptions,
75
+ } from './KernelContext.types'
76
+ import type {
77
+ BlockConfig,
78
+ ChildPageOptions,
79
+ CrudOptions,
80
+ CrudPages,
81
+ NavItem,
82
+ PermissionMeta,
83
+ PermissionOptions,
84
+ RouteOptions,
85
+ UserEntityOptions,
86
+ ZoneOptions,
87
+ } from './KernelContext.types'
88
+
89
+ // Prototype-patched method groups
90
+ import { applyEntityMethods } from './KernelContext.entities'
91
+ import { applyRoutingMethods } from './KernelContext.routing'
92
+ import { applyZoneMethods } from './KernelContext.zones'
93
+ import { applyEventMethods } from './KernelContext.events'
94
+ import { applyPermissionMethods } from './KernelContext.permissions'
95
+ import { applyI18nMethods } from './KernelContext.i18n'
223
96
 
224
97
  export class KernelContext {
225
- private _kernel: KernelInterface
226
- private _module: ModuleWithCleanup | null
98
+ // Marked public so the patch modules (which read this.* via Self=any) can
99
+ // see them. They were `private` in the previous monolithic file but were
100
+ // already accessed across patch boundaries the moment we split.
101
+ _kernel: KernelInterface
102
+ _module: ModuleWithCleanup | null
227
103
 
228
104
  constructor(kernel: KernelInterface, module: ModuleWithCleanup | null) {
229
105
  this._kernel = kernel
@@ -234,724 +110,139 @@ export class KernelContext {
234
110
  // Getters for kernel services
235
111
  // ─────────────────────────────────────────────────────────────────────────────
236
112
 
237
- /**
238
- * Get Vue app instance
239
- */
113
+ /** Vue app instance */
240
114
  get app(): App | null {
241
115
  return this._kernel.vueApp
242
116
  }
243
117
 
244
- /**
245
- * Get Vue router instance
246
- */
118
+ /** Vue router instance */
247
119
  get router(): Router | null {
248
120
  return this._kernel.router
249
121
  }
250
122
 
251
- /**
252
- * Get SignalBus instance
253
- */
123
+ /** SignalBus instance */
254
124
  get signals(): SignalBus | null {
255
125
  return this._kernel.signals
256
126
  }
257
127
 
258
- /**
259
- * Get Orchestrator instance (entity managers)
260
- */
128
+ /** Orchestrator instance (entity managers) */
261
129
  get orchestrator(): Orchestrator | null {
262
130
  return this._kernel.orchestrator
263
131
  }
264
132
 
265
- /**
266
- * Get ZoneRegistry instance
267
- */
133
+ /** ZoneRegistry instance */
268
134
  get zones(): ZoneRegistry | null {
269
135
  return this._kernel.zoneRegistry
270
136
  }
271
137
 
272
- /**
273
- * Get HookRegistry instance
274
- */
138
+ /** HookRegistry instance */
275
139
  get hooks(): HookRegistry | null {
276
140
  return this._kernel.hookRegistry
277
141
  }
278
142
 
279
- /**
280
- * Get DeferredRegistry instance
281
- */
143
+ /** DeferredRegistry instance */
282
144
  get deferred(): DeferredRegistry | null {
283
145
  return this._kernel.deferred
284
146
  }
285
147
 
286
- /**
287
- * Check if running in development mode
288
- */
148
+ /** Whether running in development mode */
289
149
  get isDev(): boolean {
290
150
  return (import.meta as unknown as { env?: { DEV?: boolean } }).env?.DEV ?? false
291
151
  }
292
152
 
293
- /**
294
- * Get debug mode from kernel options
295
- */
153
+ /** Debug mode from kernel options */
296
154
  get debug(): boolean {
297
155
  return this._kernel.options?.debug ?? false
298
156
  }
299
157
 
300
- /**
301
- * Get auth adapter
302
- */
158
+ /** Auth adapter */
303
159
  get authAdapter(): AuthAdapter | null {
304
160
  return this._kernel.options?.authAdapter ?? null
305
161
  }
306
162
 
307
- /**
308
- * Get security checker (role hierarchy, permissions)
309
- */
163
+ /** Security checker (role hierarchy, permissions) */
310
164
  get security(): SecurityChecker | null {
311
165
  return this._kernel.securityChecker
312
166
  }
313
167
 
314
- /**
315
- * Get permission registry
316
- */
168
+ /** Permission registry */
317
169
  get permissionRegistry(): PermissionRegistry | null {
318
170
  return this._kernel.permissionRegistry
319
171
  }
320
172
 
321
- /**
322
- * Get auth adapter shortcut
323
- */
173
+ /** Auth adapter shortcut */
324
174
  get auth(): AuthAdapter | null {
325
175
  return this._kernel.options?.authAdapter ?? null
326
176
  }
327
177
 
328
- /**
329
- * Get ActiveStack instance (sync navigation context)
330
- */
178
+ /** ActiveStack instance (sync navigation context) */
331
179
  get activeStack(): unknown {
332
180
  return this._kernel.activeStack ?? null
333
181
  }
334
182
 
335
- /**
336
- * Get StackHydrator instance (async data loading)
337
- */
183
+ /** StackHydrator instance (async data loading) */
338
184
  get stackHydrator(): unknown {
339
185
  return this._kernel.stackHydrator ?? null
340
186
  }
341
187
 
342
- // ─────────────────────────────────────────────────────────────────────────────
343
- // Fluent registration methods (return this for chaining)
344
- // ─────────────────────────────────────────────────────────────────────────────
345
-
346
- /**
347
- * Register an entity manager
348
- *
349
- * Uses managerFactory to resolve config into a manager instance.
350
- * Registers with Orchestrator for CRUD operations.
351
- *
352
- * @param name - Entity name (e.g., 'users', 'books')
353
- * @param config - Manager config
354
- * - String pattern: 'api:/api/users' → creates ApiStorage + EntityManager
355
- * - Object: { storage: '...', label: '...', fields: {...} }
356
- * - Manager instance: passed through directly
357
- *
358
- * @example
359
- * ctx.entity('users', 'api:/api/users')
360
- * ctx.entity('books', { storage: 'api:/api/books', label: 'Book' })
361
- * ctx.entity('settings', new SettingsManager({...}))
362
- */
363
- entity(name: string, config: string | Record<string, unknown> | EntityManager): this {
364
- // Build factory context from kernel options
365
- const factoryContext = {
366
- storageResolver: this._kernel.options.storageResolver,
367
- managerResolver: this._kernel.options.managerResolver,
368
- managerRegistry: this._kernel.options.managerRegistry || {},
369
- } as ManagerFactoryContext
370
-
371
- // Create manager via factory
372
- // eslint-disable-next-line @typescript-eslint/no-explicit-any
373
- const manager = managerFactory(config as any, name, factoryContext)
374
-
375
- // Register with orchestrator
376
- if (this._kernel.orchestrator) {
377
- this._kernel.orchestrator.register(name, manager)
378
- }
379
-
380
- // Auto-register CRUD permissions for this entity
381
- if (this._kernel.permissionRegistry) {
382
- this._kernel.permissionRegistry.registerEntity(name, {
383
- module: (this._module?.constructor as { name?: string })?.name || 'unknown',
384
- // Register entity-own:* permissions if manager has isOwn configured
385
- hasOwnership: !!(manager as unknown as { _isOwn?: unknown })._isOwn,
386
- })
387
- }
388
-
389
- return this
390
- }
391
-
392
- /**
393
- * Register a users entity with standard fields and role linking
394
- *
395
- * Creates a UsersManager with:
396
- * - username, password, role fields (role linked to roles entity)
397
- * - System entity flag
398
- * - Admin-only access by default
399
- *
400
- * @param options - Configuration options
401
- *
402
- * @example
403
- * // Basic usage with MockApiStorage
404
- * ctx.userEntity({
405
- * storage: new MockApiStorage({ entityName: 'users', initialData: usersFixture })
406
- * })
407
- */
408
- userEntity(options: UserEntityOptions): this {
409
- const manager = new UsersManager(options)
410
-
411
- // Register with orchestrator
412
- if (this._kernel.orchestrator) {
413
- this._kernel.orchestrator.register('users', manager)
414
- }
415
-
416
- // Auto-register CRUD permissions for users entity
417
- if (this._kernel.permissionRegistry) {
418
- this._kernel.permissionRegistry.registerEntity('users', {
419
- module: (this._module?.constructor as { name?: string })?.name || 'unknown',
420
- })
421
- }
422
-
423
- return this
424
- }
425
-
426
- /**
427
- * Register routes for this module
428
- *
429
- * Delegates to moduleRegistry.addRoutes with prefix application.
430
- *
431
- * @param basePath - Base path prefix (e.g., 'users')
432
- * @param routes - Route definitions with relative paths
433
- * @param opts - Route options
434
- *
435
- * @example
436
- * ctx.routes('users', [
437
- * { path: '', name: 'users', component: UserList },
438
- * { path: ':id', name: 'users-edit', component: UserEdit }
439
- * ], { entity: 'users' })
440
- */
441
- routes(basePath: string, routes: RouteRecordRaw[], opts: RouteOptions = {}): this {
442
- registry.addRoutes(basePath, routes, opts)
443
- return this
444
- }
445
-
446
- /**
447
- * Add a navigation item to a section
448
- *
449
- * @param item - Navigation item config
450
- *
451
- * @example
452
- * ctx.navItem({ section: 'Admin', route: 'users', icon: 'pi pi-users', label: 'Users' })
453
- */
454
- navItem(item: NavItem): this {
455
- registry.addNavItem(item)
456
- return this
457
- }
458
-
459
- /**
460
- * Add route family mapping for active state detection
461
- *
462
- * @param base - Parent route name (e.g., 'users')
463
- * @param prefixes - Child route prefixes (e.g., ['users-', 'user-'])
464
- *
465
- * @example
466
- * ctx.routeFamily('users', ['users-', 'user-'])
467
- */
468
- routeFamily(base: string, prefixes: string[]): this {
469
- registry.addRouteFamily(base, prefixes)
470
- return this
471
- }
472
-
473
- /**
474
- * Register standard CRUD routes with naming conventions
475
- *
476
- * Generates routes following qdadm conventions:
477
- * - Entity 'books' → route prefix 'book'
478
- * - List: /books → name 'book'
479
- * - Show: /books/:id → name 'book-show' (optional)
480
- * - Create: /books/create → name 'book-create'
481
- * - Edit: /books/:id/edit → name 'book-edit'
482
- *
483
- * Also registers route family and optional nav item.
484
- *
485
- * @param entity - Entity name (plural, e.g., 'books', 'users')
486
- * @param pages - Page components (lazy imports)
487
- * @param options - Additional options
488
- *
489
- * @example
490
- * // Single form pattern (recommended)
491
- * ctx.crud('books', {
492
- * list: () => import('./pages/BookList.vue'),
493
- * form: () => import('./pages/BookForm.vue')
494
- * }, {
495
- * nav: { section: 'Library', icon: 'pi pi-book' }
496
- * })
497
- */
498
- crud(entity: string, pages: CrudPages, options: CrudOptions = {}): this {
499
- // Entity name is always used for permission binding
500
- // Manager may not be registered yet (child entity before parent module loads)
501
- const entityBinding = entity
502
- const manager = this._kernel.orchestrator?.isRegistered(entity)
503
- ? this._kernel.orchestrator.get(entity)
504
- : null
505
- const idParam = manager?.idField || 'id'
506
-
507
- // Handle parent route configuration
508
- const urlSegment = options.pathSegment || this._toKebab(entity)
509
- let basePath = urlSegment
510
- let parentConfig: ParentConfig | null = null
511
- let parentRoutePrefix: string | null = null
512
-
513
- if (options.parentRoute) {
514
- // Find parent route info from registered routes
515
- const parentRouteName = options.parentRoute
516
- const allRoutes = getRoutes()
517
- const parentRoute = allRoutes.find((r) => r.name === parentRouteName)
518
-
519
- if (parentRoute) {
520
- // Get parent entity from route meta
521
- const parentEntityName = parentRoute.meta?.entity
522
- const parentManager = parentEntityName
523
- ? this._kernel.orchestrator?.get(parentEntityName)
524
- : null
525
- const parentIdParam = parentManager?.idField || 'id'
526
-
527
- // Build base path: parentPath/:parentId/entity
528
- // e.g., books/:bookId/loans
529
- const parentBasePath =
530
- parentRoute.path.replace(/\/(create|:.*)?$/, '') || parentEntityName
531
- basePath = `${parentBasePath}/:${parentIdParam}/${urlSegment}`
532
-
533
- // Build parent config for route meta (only if parent entity is defined)
534
- if (parentEntityName) {
535
- parentConfig = {
536
- entity: parentEntityName,
537
- param: parentIdParam,
538
- foreignKey:
539
- options.foreignKey || `${this._singularize(parentEntityName)}_id`,
540
- }
541
- }
542
-
543
- // Store parent route prefix for derived naming
544
- parentRoutePrefix = parentRouteName
545
- }
546
- }
547
-
548
- // Derive route prefix
549
- // - With parent: 'book' + '-loan' → 'book-loan'
550
- // - Without parent: 'books' → 'book'
551
- const routePrefix =
552
- options.routePrefix ||
553
- (parentRoutePrefix
554
- ? `${parentRoutePrefix}-${this._singularize(entity)}`
555
- : this._singularize(entity))
556
-
557
- // Build routes array
558
- const routes: RouteRecordRaw[] = []
559
-
560
- // List route
561
- if (pages.list) {
562
- routes.push({
563
- path: '',
564
- name: routePrefix,
565
- component: pages.list,
566
- meta: { layout: 'list' },
567
- })
568
- }
569
-
570
- // Show route (read-only detail view)
571
- if (pages.show) {
572
- routes.push({
573
- path: `:${idParam}`,
574
- name: `${routePrefix}-show`,
575
- component: pages.show,
576
- meta: { layout: 'show' },
577
- })
578
- }
579
-
580
- // Form routes - single form or separate create/edit
581
- if (pages.form) {
582
- // Single form pattern (recommended)
583
- routes.push({
584
- path: 'create',
585
- name: `${routePrefix}-create`,
586
- component: pages.form,
587
- })
588
- routes.push({
589
- path: `:${idParam}/edit`,
590
- name: `${routePrefix}-edit`,
591
- component: pages.form,
592
- })
593
- } else {
594
- // Separate create/edit pages
595
- if (pages.create) {
596
- routes.push({
597
- path: 'create',
598
- name: `${routePrefix}-create`,
599
- component: pages.create,
600
- })
601
- }
602
- if (pages.edit) {
603
- routes.push({
604
- path: `:${idParam}/edit`,
605
- name: `${routePrefix}-edit`,
606
- component: pages.edit,
607
- })
608
- }
609
- }
610
-
611
- // Build route options
612
- const routeOpts: RouteOptions = {}
613
- // Set entity if:
614
- // 1. Entity is registered (manager exists), OR
615
- // 2. This is a child route (parentConfig) - needs entity binding for permission checks
616
- if (manager || parentConfig) {
617
- routeOpts.entity = entityBinding
618
- }
619
- if (parentConfig) {
620
- routeOpts.parent = parentConfig
621
- }
622
- if (options.label) {
623
- routeOpts.label = options.label
624
- }
625
-
626
- // Register routes
627
- this.routes(basePath, routes, routeOpts)
628
-
629
- // Register route family for active state detection
630
- this.routeFamily(routePrefix, [`${routePrefix}-`])
631
-
632
- // Register nav item if provided (typically not for child entities)
633
- if (options.nav) {
634
- const label = options.nav.label || this._capitalize(entity)
635
- const navItem: NavItem = {
636
- section: options.nav.section,
637
- route: routePrefix,
638
- icon: options.nav.icon,
639
- label,
640
- }
641
- // Only set entity on nav item if registered (to avoid permission check failure)
642
- // Routes always get entity binding, but nav items need it to be resolvable
643
- if (manager) {
644
- navItem.entity = entityBinding
645
- }
646
- this.navItem(navItem)
647
- }
648
-
649
- return this
650
- }
651
-
652
- /**
653
- * Register a custom child page on an entity item
654
- *
655
- * Adds a non-entity page as a tab alongside entity child routes.
656
- * The page appears in PageNav navlinks next to CRUD children (e.g., Loans).
657
- *
658
- * @param parentRouteName - Parent route name (e.g., 'book')
659
- * @param pageName - Page slug used in URL and route name (e.g., 'statistics')
660
- * @param options - Page options (component, label, icon, meta)
661
- *
662
- * @example
663
- * ctx.childPage('book', 'statistics', {
664
- * component: () => import('./pages/BookStatistics.vue'),
665
- * label: 'Statistics',
666
- * icon: 'pi pi-chart-bar'
667
- * })
668
- * // → Route: /books/:bookId/statistics, name: book-statistics
669
- * // → Appears as tab in PageNav alongside entity child routes
670
- */
671
- childPage(parentRouteName: string, pageName: string, options: ChildPageOptions): this {
672
- const allRoutes = getRoutes()
673
- const parentRoute = allRoutes.find((r) => r.name === parentRouteName)
674
-
675
- if (!parentRoute) {
676
- console.warn(`[qdadm] childPage: parent route '${parentRouteName}' not found`)
677
- return this
678
- }
679
-
680
- // Get parent entity info from route meta
681
- const parentEntityName = parentRoute.meta?.entity as string | undefined
682
- const parentManager = parentEntityName
683
- ? this._kernel.orchestrator?.get(parentEntityName)
684
- : null
685
- const parentIdParam = parentManager?.idField || 'id'
686
-
687
- // Build path: parentBasePath/:parentId/pageName
688
- const parentBasePath =
689
- parentRoute.path.replace(/\/(create|:.*)?$/, '') || parentEntityName
690
- const basePath = `${parentBasePath}/:${parentIdParam}/${pageName}`
691
-
692
- // Route name: parentRouteName-pageName
693
- const routeName = `${parentRouteName}-${pageName}`
694
-
695
- // Build parent config (no foreignKey for non-entity pages)
696
- const parentConfig: ParentConfig | undefined = parentEntityName
697
- ? { entity: parentEntityName, param: parentIdParam }
698
- : undefined
699
-
700
- // Build route options
701
- const routeOpts: RouteOptions = {}
702
- if (parentConfig) {
703
- routeOpts.parent = parentConfig
704
- }
705
- if (options.label) {
706
- routeOpts.label = options.label
707
- }
708
-
709
- // Register route with layout='page'
710
- this.routes(basePath, [
711
- {
712
- path: '',
713
- name: routeName,
714
- component: options.component,
715
- meta: {
716
- layout: 'page',
717
- ...(options.icon && { icon: options.icon }),
718
- ...options.meta,
719
- },
720
- },
721
- ], routeOpts)
722
-
723
- return this
724
- }
725
-
726
- /**
727
- * Singularize a plural word (simple English rules)
728
- */
729
- private _singularize(plural: string): string {
730
- if (plural.endsWith('ies')) return plural.slice(0, -3) + 'y'
731
- if (
732
- plural.endsWith('ses') ||
733
- plural.endsWith('xes') ||
734
- plural.endsWith('zes')
735
- ) {
736
- return plural.slice(0, -2)
737
- }
738
- if (plural.endsWith('s') && !plural.endsWith('ss')) return plural.slice(0, -1)
739
- return plural
740
- }
741
-
742
- /**
743
- * Convert camelCase to kebab-case (e.g. 'botTasks' → 'bot-tasks')
744
- */
745
- private _toKebab(str: string): string {
746
- return str.replace(/([a-z])([A-Z])/g, '$1-$2').toLowerCase()
747
- }
748
-
749
- /**
750
- * Capitalize first letter
751
- */
752
- private _capitalize(str: string): string {
753
- return str.charAt(0).toUpperCase() + str.slice(1)
754
- }
755
-
756
- /**
757
- * Define a zone for extensible UI composition
758
- *
759
- * @param name - Zone name (e.g., 'users-list-header')
760
- * @param options - Zone options
761
- *
762
- * @example
763
- * ctx.zone('users-list-header')
764
- * ctx.zone('users-sidebar', { default: DefaultSidebar })
765
- */
766
- zone(name: string, options: ZoneOptions = {}): this {
767
- if (this._kernel.zoneRegistry) {
768
- this._kernel.zoneRegistry.defineZone(name, options)
769
- }
770
- return this
771
- }
772
-
773
- /**
774
- * Register a block in a zone
775
- *
776
- * @param zoneName - Target zone name
777
- * @param config - Block configuration
778
- *
779
- * @example
780
- * ctx.block('users-list-header', { component: UserStats, weight: 10, id: 'user-stats' })
781
- */
782
- block(zoneName: string, config: BlockConfig): this {
783
- if (this._kernel.zoneRegistry) {
784
- this._kernel.zoneRegistry.registerBlock(zoneName, config)
785
- }
786
- return this
787
- }
788
-
789
- /**
790
- * Provide a value to Vue's dependency injection system
791
- *
792
- * @param key - Injection key
793
- * @param value - Value to provide
794
- *
795
- * @example
796
- * ctx.provide('myService', new MyService())
797
- */
798
- provide(key: string | symbol, value: unknown): this {
799
- if (this._kernel.vueApp) {
800
- this._kernel.vueApp.provide(key, value)
801
- } else {
802
- // Store for later application when vueApp is created
803
- this._kernel._pendingProvides.set(key, value)
804
- }
805
- return this
806
- }
807
-
808
- /**
809
- * Register a global Vue component
810
- *
811
- * @param name - Component name
812
- * @param component - Vue component
813
- *
814
- * @example
815
- * ctx.component('MyGlobalComponent', MyComponent)
816
- */
817
- component(name: string, component: Component): this {
818
- if (this._kernel.vueApp) {
819
- this._kernel.vueApp.component(name, component)
820
- } else {
821
- // Store for later registration when vueApp is created
822
- this._kernel._pendingComponents.set(name, component)
823
- }
824
- return this
188
+ /** I18n subsystem */
189
+ get i18n(): I18n | null {
190
+ return this._kernel.i18nInstance ?? null
825
191
  }
192
+ }
826
193
 
827
- /**
828
- * Subscribe to a signal with automatic cleanup on module disconnect
829
- *
830
- * The handler will be automatically unsubscribed when the module's
831
- * disconnect() method is called.
832
- *
833
- * @param signal - Signal name (supports wildcards via SignalBus)
834
- * @param handler - Handler function (event, ctx) => void
835
- * @param options - Listener options
836
- *
837
- * @example
838
- * ctx.on('users:created', (event) => console.log('User created:', event.data))
839
- * ctx.on('entity:*', (event) => console.log('Entity event:', event))
840
- */
841
- on(signal: string, handler: SignalHandler, options: ListenerOptions = {}): this {
842
- if (this._kernel.signals) {
843
- const cleanup = this._kernel.signals.on(signal, handler, options)
844
- // Register cleanup with module for automatic unsubscribe on disconnect
845
- if (this._module && typeof this._module._addSignalCleanup === 'function') {
846
- this._module._addSignalCleanup(cleanup)
847
- }
848
- }
849
- return this
850
- }
194
+ // Declare prototype-patched methods (declaration merging)
195
+ // eslint-disable-next-line @typescript-eslint/no-empty-object-type
196
+ export interface KernelContext {
197
+ // Entities (KernelContext.entities.ts)
198
+ entity(name: string, config: string | Record<string, unknown> | EntityManager): this
199
+ userEntity(options: UserEntityOptions): this
851
200
 
852
- /**
853
- * Register a hook handler
854
- *
855
- * @param hookName - Hook name (e.g., 'users:presave')
856
- * @param handler - Handler function
857
- * @param options - Hook options
858
- *
859
- * @example
860
- * ctx.hook('users:presave', (context) => {
861
- * context.data.updated_at = new Date().toISOString()
862
- * return context
863
- * })
864
- */
865
- hook(hookName: string, handler: HookHandler, options: Record<string, unknown> = {}): this {
866
- if (this._kernel.hookRegistry) {
867
- this._kernel.hookRegistry.register(hookName, handler, options)
868
- }
869
- return this
870
- }
201
+ // Routing (KernelContext.routing.ts)
202
+ routes(basePath: string, routes: RouteRecordRaw[], opts?: RouteOptions): this
203
+ navItem(item: NavItem): this
204
+ routeFamily(base: string, prefixes: string[]): this
205
+ crud(entity: string, pages: CrudPages, options?: CrudOptions): this
206
+ childPage(parentRouteName: string, pageName: string, options: ChildPageOptions): this
871
207
 
872
- /**
873
- * Queue a deferred task
874
- *
875
- * @param name - Deferred task name
876
- * @param factory - Factory function returning a promise
877
- *
878
- * @example
879
- * ctx.deferred('users:warmup', async () => {
880
- * await ctx.orchestrator.get('users').warmup()
881
- * })
882
- */
883
- defer(name: string, factory: DeferredFactory): this {
884
- if (this._kernel.deferred) {
885
- this._kernel.deferred.queue(name, factory)
886
- }
887
- return this
888
- }
208
+ // Zones / Vue (KernelContext.zones.ts)
209
+ zone(name: string, options?: ZoneOptions): this
210
+ block(zoneName: string, config: BlockConfig): this
211
+ provide(key: string | symbol, value: unknown): this
212
+ component(name: string, component: Component): this
889
213
 
890
- // ─────────────────────────────────────────────────────────────────────────────
891
- // Permission registration
892
- // ─────────────────────────────────────────────────────────────────────────────
214
+ // Events (KernelContext.events.ts)
215
+ on(signal: string, handler: SignalHandler, options?: ListenerOptions): this
216
+ hook(hookName: string, handler: HookHandler, options?: Record<string, unknown>): this
217
+ defer(name: string, factory: DeferredFactory): this
893
218
 
894
- /**
895
- * Register permissions for this module
896
- *
897
- * Permissions are namespaced and collected in the central PermissionRegistry.
898
- * Use this to declare what permissions your module provides.
899
- *
900
- * Permission format: namespace:action
901
- * - For entity CRUD: automatically prefixed with 'entity:'
902
- * - For system features: use any namespace (auth:, admin:, feature:, etc.)
903
- *
904
- * @param namespace - Permission namespace (e.g., 'books', 'auth', 'admin:config')
905
- * @param permissions - Permission definitions
906
- * @param options - Permission options
907
- *
908
- * @example
909
- * // Entity custom permissions (auto-prefixed with 'entity:')
910
- * ctx.permissions('books', {
911
- * checkout: 'Checkout a book',
912
- * reserve: { label: 'Reserve', description: 'Reserve a book for later' }
913
- * }, { isEntity: true })
914
- * // → entity:books:checkout, entity:books:reserve
915
- */
219
+ // Permissions (KernelContext.permissions.ts)
916
220
  permissions(
917
221
  namespace: string,
918
222
  permissions: Record<string, string | PermissionMeta>,
919
- options: PermissionOptions = {}
920
- ): this {
921
- if (this._kernel.permissionRegistry) {
922
- this._kernel.permissionRegistry.register(namespace, permissions, {
923
- ...options,
924
- module: (this._module?.constructor as { name?: string })?.name || 'unknown',
925
- })
926
- }
927
- return this
928
- }
929
-
930
- /**
931
- * Register entity permissions (convenience method)
932
- *
933
- * Shorthand for registering entity-namespaced permissions.
934
- * Equivalent to: ctx.permissions(entity, perms, { isEntity: true })
935
- *
936
- * @param entity - Entity name
937
- * @param permissions - Permission definitions
938
- *
939
- * @example
940
- * ctx.entityPermissions('books', {
941
- * checkout: 'Checkout a book'
942
- * })
943
- * // → entity:books:checkout
944
- */
223
+ options?: PermissionOptions
224
+ ): this
945
225
  entityPermissions(
946
226
  entity: string,
947
227
  permissions: Record<string, string | PermissionMeta>
948
- ): this {
949
- return this.permissions(entity, permissions, { isEntity: true })
950
- }
228
+ ): this
229
+
230
+ // i18n (KernelContext.i18n.ts)
231
+ messages(locale: string, bundle: MessagesBundle): this
232
+ aliases(patterns: AliasPattern[]): this
233
+ messagesProvider(provider: TranslationProvider): this
951
234
  }
952
235
 
236
+ // Apply prototype patches
237
+ applyEntityMethods(KernelContext)
238
+ applyRoutingMethods(KernelContext)
239
+ applyZoneMethods(KernelContext)
240
+ applyEventMethods(KernelContext)
241
+ applyPermissionMethods(KernelContext)
242
+ applyI18nMethods(KernelContext)
243
+
953
244
  /**
954
- * Factory function to create a KernelContext instance
245
+ * Factory function to create a KernelContext instance.
955
246
  */
956
247
  export function createKernelContext(
957
248
  kernel: KernelInterface,