@open-mercato/shared 0.6.2-develop.3461.1.605f31c2c9 → 0.6.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.
- package/.turbo/turbo-build.log +1 -1
- package/AGENTS.md +24 -0
- package/dist/lib/bootstrap/factory.js +3 -1
- package/dist/lib/bootstrap/factory.js.map +2 -2
- package/dist/lib/commands/command-interceptor-store.js +3 -1
- package/dist/lib/commands/command-interceptor-store.js.map +2 -2
- package/dist/lib/crud/enricher-registry.js +3 -1
- package/dist/lib/crud/enricher-registry.js.map +2 -2
- package/dist/lib/crud/factory.js.map +1 -1
- package/dist/lib/crud/interceptor-registry.js +3 -1
- package/dist/lib/crud/interceptor-registry.js.map +2 -2
- package/dist/lib/crud/sync-event-runner.js +2 -8
- package/dist/lib/crud/sync-event-runner.js.map +2 -2
- package/dist/lib/db/mikro.js +27 -3
- package/dist/lib/db/mikro.js.map +2 -2
- package/dist/lib/di/container.js +16 -0
- package/dist/lib/di/container.js.map +2 -2
- package/dist/lib/events/patterns.js +2 -10
- package/dist/lib/events/patterns.js.map +2 -2
- package/dist/lib/middleware/page-executor.js +2 -1
- package/dist/lib/middleware/page-executor.js.map +2 -2
- package/dist/lib/modules/registry.js +2 -1
- package/dist/lib/modules/registry.js.map +2 -2
- package/dist/lib/notifications/handler-registry.js +3 -1
- package/dist/lib/notifications/handler-registry.js.map +2 -2
- package/dist/lib/patterns/wildcard.js +42 -0
- package/dist/lib/patterns/wildcard.js.map +7 -0
- package/dist/lib/regex/linear.js +31 -0
- package/dist/lib/regex/linear.js.map +7 -0
- package/dist/lib/version.js +1 -1
- package/dist/lib/version.js.map +1 -1
- package/dist/modules/entities/validation.js +15 -7
- package/dist/modules/entities/validation.js.map +2 -2
- package/dist/modules/overrides.js +857 -0
- package/dist/modules/overrides.js.map +2 -2
- package/dist/modules/registry.js +17 -4
- package/dist/modules/registry.js.map +2 -2
- package/dist/modules/widgets/injection-loader.js +12 -6
- package/dist/modules/widgets/injection-loader.js.map +2 -2
- package/jest.config.cjs +3 -1
- package/package.json +8 -8
- package/src/lib/bootstrap/factory.ts +3 -1
- package/src/lib/commands/command-interceptor-store.ts +3 -1
- package/src/lib/crud/__tests__/sync-event-runner.test.ts +5 -0
- package/src/lib/crud/enricher-registry.ts +3 -1
- package/src/lib/crud/factory.ts +2 -2
- package/src/lib/crud/interceptor-registry.ts +3 -1
- package/src/lib/crud/sync-event-runner.ts +2 -11
- package/src/lib/db/mikro.ts +27 -2
- package/src/lib/di/container.ts +24 -4
- package/src/lib/events/__tests__/patterns.test.ts +5 -0
- package/src/lib/events/patterns.ts +3 -16
- package/src/lib/middleware/page-executor.ts +2 -1
- package/src/lib/modules/registry.ts +2 -1
- package/src/lib/notifications/handler-registry.ts +3 -1
- package/src/lib/patterns/__tests__/wildcard.test.ts +27 -0
- package/src/lib/patterns/wildcard.ts +59 -0
- package/src/lib/regex/linear.ts +48 -0
- package/src/modules/__tests__/contract-overrides.test.ts +332 -0
- package/src/modules/__tests__/overrides.test.ts +23 -22
- package/src/modules/__tests__/route-overrides.test.ts +441 -0
- package/src/modules/entities/__tests__/validation.test.ts +97 -2
- package/src/modules/entities/validation.ts +14 -9
- package/src/modules/overrides.ts +1297 -27
- package/src/modules/registry.ts +27 -4
- package/src/modules/widgets/__tests__/injection-loader.required-modules.test.ts +21 -0
- package/src/modules/widgets/injection-loader.ts +12 -6
package/src/modules/overrides.ts
CHANGED
|
@@ -12,19 +12,19 @@
|
|
|
12
12
|
* from: '@app',
|
|
13
13
|
* overrides: {
|
|
14
14
|
* ai: { agents: {...}, tools: {...} }, // Phase 1 — wired
|
|
15
|
-
* routes: { api: {...}, pages: {...} }, // Phase 2/3 —
|
|
16
|
-
* events: { subscribers: {...} }, // Phase 4 —
|
|
17
|
-
* workers: {...}, // Phase 5 —
|
|
15
|
+
* routes: { api: {...}, pages: {...} }, // Phase 2/3 — wired
|
|
16
|
+
* events: { subscribers: {...} }, // Phase 4 — wired
|
|
17
|
+
* workers: {...}, // Phase 5 — wired
|
|
18
18
|
* ...
|
|
19
19
|
* },
|
|
20
20
|
* }
|
|
21
21
|
*
|
|
22
22
|
* The umbrella shape is the union of every per-domain sub-shape. Per-
|
|
23
|
-
* domain runtime hooks
|
|
24
|
-
*
|
|
25
|
-
*
|
|
26
|
-
*
|
|
27
|
-
*
|
|
23
|
+
* domain runtime hooks own their composers and apply the override map
|
|
24
|
+
* against their registry. During the rollout the dispatcher emitted a
|
|
25
|
+
* one-shot warning for unwired domains; all domains in this umbrella are
|
|
26
|
+
* now wired, so warnings only indicate a missing custom applier in tests
|
|
27
|
+
* or future domain additions.
|
|
28
28
|
*
|
|
29
29
|
* Resolution order across all domains (highest precedence first):
|
|
30
30
|
*
|
|
@@ -59,33 +59,47 @@ export interface AiOverridesShape {
|
|
|
59
59
|
extensions?: unknown[]
|
|
60
60
|
}
|
|
61
61
|
|
|
62
|
-
|
|
62
|
+
type LooseOverrideMap = Record<string, unknown>
|
|
63
|
+
|
|
64
|
+
/**
|
|
65
|
+
* Phase 2 (api) / Phase 3 (pages).
|
|
66
|
+
*
|
|
67
|
+
* The `api` sub-shape is keyed by `'METHOD /api/path'` and accepts either
|
|
68
|
+
* a replacement definition (`{ handler, metadata? }`) or `null` to disable.
|
|
69
|
+
* See {@link ApiRouteOverridesMap} for the strongly-typed alias and
|
|
70
|
+
* {@link applyApiOverridesToManifests} for the runtime apply step.
|
|
71
|
+
*
|
|
72
|
+
* The `pages` sub-shape is keyed by `'/backend/path'` or
|
|
73
|
+
* `'/frontend/path'`; `null` disables the page and a definition replaces
|
|
74
|
+
* its loader and/or metadata.
|
|
75
|
+
*/
|
|
63
76
|
export interface RoutesOverridesShape {
|
|
64
|
-
api?:
|
|
65
|
-
pages?:
|
|
77
|
+
api?: ApiRouteOverridesMap | LooseOverrideMap
|
|
78
|
+
pages?: PageRouteOverridesMap | LooseOverrideMap
|
|
66
79
|
}
|
|
67
80
|
|
|
68
|
-
/** Phase 4 — event subscribers.
|
|
81
|
+
/** Phase 4 — event subscribers. */
|
|
69
82
|
export interface EventsOverridesShape {
|
|
70
|
-
subscribers?:
|
|
83
|
+
subscribers?: SubscriberOverridesMap | LooseOverrideMap
|
|
71
84
|
}
|
|
72
85
|
|
|
73
86
|
/** Phase 6/7/8 — widget injection, component handles, dashboard widgets. */
|
|
74
87
|
export interface WidgetsOverridesShape {
|
|
75
|
-
injection?:
|
|
76
|
-
components?:
|
|
77
|
-
dashboard?:
|
|
88
|
+
injection?: InjectionWidgetOverridesMap | LooseOverrideMap
|
|
89
|
+
components?: ComponentOverridesMap | LooseOverrideMap
|
|
90
|
+
dashboard?: DashboardWidgetOverridesMap | LooseOverrideMap
|
|
78
91
|
}
|
|
79
92
|
|
|
80
93
|
/** Phase 9 — notification types + handlers. */
|
|
81
94
|
export interface NotificationsOverridesShape {
|
|
82
|
-
types?:
|
|
83
|
-
handlers?:
|
|
95
|
+
types?: NotificationTypeOverridesMap | LooseOverrideMap
|
|
96
|
+
handlers?: NotificationHandlerOverridesMap | LooseOverrideMap
|
|
84
97
|
}
|
|
85
98
|
|
|
86
99
|
/** Phase 15 — setup lifecycle hooks. */
|
|
87
100
|
export interface SetupOverridesShape {
|
|
88
101
|
defaultRoleFeatures?: Record<string, readonly string[]>
|
|
102
|
+
defaultCustomerRoleFeatures?: Record<string, readonly string[]>
|
|
89
103
|
seedDefaults?: false
|
|
90
104
|
seedExamples?: false
|
|
91
105
|
onTenantCreated?: false
|
|
@@ -93,12 +107,12 @@ export interface SetupOverridesShape {
|
|
|
93
107
|
|
|
94
108
|
/** Phase 16 — ACL features (per-feature override). */
|
|
95
109
|
export interface AclOverridesShape {
|
|
96
|
-
features?:
|
|
110
|
+
features?: AclFeatureOverridesMap | LooseOverrideMap
|
|
97
111
|
}
|
|
98
112
|
|
|
99
113
|
/** Phase 18 — encryption maps per entity id. */
|
|
100
114
|
export interface EncryptionOverridesShape {
|
|
101
|
-
maps?:
|
|
115
|
+
maps?: EncryptionMapOverridesMap | LooseOverrideMap
|
|
102
116
|
}
|
|
103
117
|
|
|
104
118
|
/**
|
|
@@ -109,17 +123,17 @@ export interface ModuleOverrides {
|
|
|
109
123
|
ai?: AiOverridesShape
|
|
110
124
|
routes?: RoutesOverridesShape
|
|
111
125
|
events?: EventsOverridesShape
|
|
112
|
-
workers?:
|
|
126
|
+
workers?: WorkerOverridesMap | LooseOverrideMap
|
|
113
127
|
widgets?: WidgetsOverridesShape
|
|
114
128
|
notifications?: NotificationsOverridesShape
|
|
115
|
-
interceptors?:
|
|
116
|
-
commandInterceptors?:
|
|
117
|
-
enrichers?:
|
|
118
|
-
guards?:
|
|
119
|
-
cli?:
|
|
129
|
+
interceptors?: ApiInterceptorOverridesMap | LooseOverrideMap
|
|
130
|
+
commandInterceptors?: CommandInterceptorOverridesMap | LooseOverrideMap
|
|
131
|
+
enrichers?: ResponseEnricherOverridesMap | LooseOverrideMap
|
|
132
|
+
guards?: PageGuardOverridesMap | LooseOverrideMap
|
|
133
|
+
cli?: CliOverridesMap | LooseOverrideMap
|
|
120
134
|
setup?: SetupOverridesShape
|
|
121
135
|
acl?: AclOverridesShape
|
|
122
|
-
di?:
|
|
136
|
+
di?: DiOverridesMap | LooseOverrideMap
|
|
123
137
|
encryption?: EncryptionOverridesShape
|
|
124
138
|
}
|
|
125
139
|
|
|
@@ -189,6 +203,7 @@ export function registerModuleOverrideApplier<TShape>(
|
|
|
189
203
|
export function resetModuleOverrideAppliersForTests(): void {
|
|
190
204
|
appliers.clear()
|
|
191
205
|
warnedUnwiredDomains.clear()
|
|
206
|
+
registerBuiltInModuleOverrideAppliers()
|
|
192
207
|
}
|
|
193
208
|
|
|
194
209
|
// ---------------------------------------------------------------------------
|
|
@@ -264,3 +279,1258 @@ export function applyModuleOverridesFromEnabledModules(
|
|
|
264
279
|
applier(entries)
|
|
265
280
|
}
|
|
266
281
|
}
|
|
282
|
+
|
|
283
|
+
// ---------------------------------------------------------------------------
|
|
284
|
+
// Phase 2/3 — Routes (API + pages) applier
|
|
285
|
+
// ---------------------------------------------------------------------------
|
|
286
|
+
//
|
|
287
|
+
// Wires `entry.overrides.routes.api` so a downstream module can disable or
|
|
288
|
+
// replace any API route declared by another module. The override map is
|
|
289
|
+
// keyed by `'METHOD /api/path'` (case-insensitive method, leading slash on
|
|
290
|
+
// the path), and each value is either:
|
|
291
|
+
//
|
|
292
|
+
// - `null` — disable the matching method on the route.
|
|
293
|
+
// - `{ handler, metadata? }` — replace the matching method's handler
|
|
294
|
+
// (and optionally merge override metadata into the route's metadata).
|
|
295
|
+
//
|
|
296
|
+
// Resolution order (lowest precedence first):
|
|
297
|
+
//
|
|
298
|
+
// 1. `modules.ts` inline — every `entry.overrides.routes.api` on a
|
|
299
|
+
// `ModuleEntry` (last entry per key wins).
|
|
300
|
+
// 2. Programmatic — `applyApiRouteOverrides({...})` from anywhere at
|
|
301
|
+
// boot or test scaffolds (last call per key wins).
|
|
302
|
+
//
|
|
303
|
+
// A file-based tier (per-module `apiRouteOverrides` exports) is **not**
|
|
304
|
+
// part of Phase 2 — modules that ship a route override do so via
|
|
305
|
+
// `modules.ts` or programmatically. The umbrella spec keeps the file-based
|
|
306
|
+
// tier "where applicable".
|
|
307
|
+
//
|
|
308
|
+
// The applied overrides are consumed by `registerApiRouteManifests` (see
|
|
309
|
+
// `./registry.ts`) which rewrites the manifest array once before storing
|
|
310
|
+
// it. Calls to `applyApiRouteOverrides` made AFTER manifests are already
|
|
311
|
+
// registered do not retro-actively update the stored manifest — for
|
|
312
|
+
// runtime override scenarios call the dispatcher / programmatic API
|
|
313
|
+
// before `bootstrap.ts` triggers manifest registration.
|
|
314
|
+
//
|
|
315
|
+
// Spec: `.ai/specs/2026-05-04-modules-ts-unified-overrides.md` (Phases 2/3).
|
|
316
|
+
|
|
317
|
+
// ApiRouteManifestEntry / ApiHandler / HttpMethod live in ./registry. The
|
|
318
|
+
// type-only import is erased at runtime, so there is no cycle even though
|
|
319
|
+
// ./registry imports the runtime helpers below.
|
|
320
|
+
import type {
|
|
321
|
+
ApiHandler,
|
|
322
|
+
ApiRouteManifestEntry,
|
|
323
|
+
BackendRouteManifestEntry,
|
|
324
|
+
FrontendRouteManifestEntry,
|
|
325
|
+
HttpMethod,
|
|
326
|
+
Module,
|
|
327
|
+
ModuleCli,
|
|
328
|
+
ModuleDashboardWidgetEntry,
|
|
329
|
+
ModuleInjectionWidgetEntry,
|
|
330
|
+
ModuleSubscriber,
|
|
331
|
+
ModuleWorker,
|
|
332
|
+
} from './registry'
|
|
333
|
+
import type { ModuleInjectionTable } from './widgets/injection'
|
|
334
|
+
import type { ComponentOverride } from './widgets/component-registry'
|
|
335
|
+
import type { NotificationHandler } from './notifications/handler'
|
|
336
|
+
import type { NotificationTypeDefinition } from './notifications/types'
|
|
337
|
+
import type { ModuleEncryptionMap } from './encryption'
|
|
338
|
+
import type { ModuleSetupConfig } from './setup'
|
|
339
|
+
import type { ApiInterceptor } from '../lib/crud/api-interceptor'
|
|
340
|
+
import type { ResponseEnricher } from '../lib/crud/response-enricher'
|
|
341
|
+
import type { CommandInterceptor } from '../lib/commands/command-interceptor'
|
|
342
|
+
import type { PageMiddlewareRegistryEntry, PageRouteMiddleware } from './middleware/page'
|
|
343
|
+
|
|
344
|
+
/** Override for a single API route entry: replace handler/metadata, or `null` to disable. */
|
|
345
|
+
export interface ApiRouteOverrideDefinition {
|
|
346
|
+
/** Replacement handler. Same signature as the original API route handler. */
|
|
347
|
+
handler: ApiHandler
|
|
348
|
+
/** Optional metadata override — replaces the per-method metadata on the route. */
|
|
349
|
+
metadata?: unknown
|
|
350
|
+
}
|
|
351
|
+
|
|
352
|
+
/** Override value per `'METHOD /api/path'` key — definition or `null` to disable. */
|
|
353
|
+
export type ApiRouteOverride = ApiRouteOverrideDefinition | null
|
|
354
|
+
|
|
355
|
+
/** Map of `'METHOD /api/path'` → override. */
|
|
356
|
+
export type ApiRouteOverridesMap = Record<string, ApiRouteOverride>
|
|
357
|
+
|
|
358
|
+
type OverrideStore<T> = {
|
|
359
|
+
modules: Record<string, T | null>
|
|
360
|
+
programmatic: Record<string, T | null>
|
|
361
|
+
}
|
|
362
|
+
|
|
363
|
+
type OverrideValue<T> = T | null
|
|
364
|
+
type OverrideMap<T> = Record<string, OverrideValue<T>>
|
|
365
|
+
|
|
366
|
+
/** Override for a backend/frontend page route: replace loader/metadata, or `null` to disable. */
|
|
367
|
+
export interface PageRouteOverrideDefinition {
|
|
368
|
+
load?: BackendRouteManifestEntry['load']
|
|
369
|
+
Component?: Awaited<ReturnType<BackendRouteManifestEntry['load']>>
|
|
370
|
+
metadata?: Partial<Omit<BackendRouteManifestEntry | FrontendRouteManifestEntry, 'moduleId' | 'load'>>
|
|
371
|
+
}
|
|
372
|
+
|
|
373
|
+
export type PageRouteOverride = PageRouteOverrideDefinition | null
|
|
374
|
+
export type PageRouteOverridesMap = Record<string, PageRouteOverride>
|
|
375
|
+
|
|
376
|
+
export type SubscriberOverride = ModuleSubscriber | null
|
|
377
|
+
export type SubscriberOverridesMap = Record<string, SubscriberOverride>
|
|
378
|
+
|
|
379
|
+
export type WorkerOverride = ModuleWorker | null
|
|
380
|
+
export type WorkerOverridesMap = Record<string, WorkerOverride>
|
|
381
|
+
|
|
382
|
+
export type CliOverride = ModuleCli | null
|
|
383
|
+
export type CliOverridesMap = Record<string, CliOverride>
|
|
384
|
+
|
|
385
|
+
export type InjectionWidgetOverride = ModuleInjectionWidgetEntry | null
|
|
386
|
+
export type InjectionWidgetOverridesMap = Record<string, InjectionWidgetOverride>
|
|
387
|
+
|
|
388
|
+
export type DashboardWidgetOverride = ModuleDashboardWidgetEntry | null
|
|
389
|
+
export type DashboardWidgetOverridesMap = Record<string, DashboardWidgetOverride>
|
|
390
|
+
|
|
391
|
+
export type ComponentOverrideValue = ComponentOverride | ComponentOverride[] | null
|
|
392
|
+
export type ComponentOverridesMap = Record<string, ComponentOverrideValue>
|
|
393
|
+
|
|
394
|
+
export type NotificationTypeOverride = NotificationTypeDefinition | null
|
|
395
|
+
export type NotificationTypeOverridesMap = Record<string, NotificationTypeOverride>
|
|
396
|
+
|
|
397
|
+
export type NotificationHandlerOverride = NotificationHandler | null
|
|
398
|
+
export type NotificationHandlerOverridesMap = Record<string, NotificationHandlerOverride>
|
|
399
|
+
|
|
400
|
+
export type ApiInterceptorOverride = ApiInterceptor | null
|
|
401
|
+
export type ApiInterceptorOverridesMap = Record<string, ApiInterceptorOverride>
|
|
402
|
+
|
|
403
|
+
export type CommandInterceptorOverride = CommandInterceptor | null
|
|
404
|
+
export type CommandInterceptorOverridesMap = Record<string, CommandInterceptorOverride>
|
|
405
|
+
|
|
406
|
+
export type ResponseEnricherOverride = ResponseEnricher | null
|
|
407
|
+
export type ResponseEnricherOverridesMap = Record<string, ResponseEnricherOverride>
|
|
408
|
+
|
|
409
|
+
export type PageGuardOverride = PageRouteMiddleware | null
|
|
410
|
+
export type PageGuardOverridesMap = Record<string, PageGuardOverride>
|
|
411
|
+
|
|
412
|
+
export type AclFeatureOverride = string | { id: string; [key: string]: unknown } | null
|
|
413
|
+
export type AclFeatureOverridesMap = Record<string, AclFeatureOverride>
|
|
414
|
+
|
|
415
|
+
export type EncryptionMapOverride = ModuleEncryptionMap | null
|
|
416
|
+
export type EncryptionMapOverridesMap = Record<string, EncryptionMapOverride>
|
|
417
|
+
|
|
418
|
+
export type DiBindingOverrideDefinition = {
|
|
419
|
+
register: (container: { register: (registrations: Record<string, unknown>) => unknown }, key: string) => void
|
|
420
|
+
}
|
|
421
|
+
export type DiBindingOverride = DiBindingOverrideDefinition | unknown | null
|
|
422
|
+
export type DiOverridesMap = Record<string, DiBindingOverride>
|
|
423
|
+
|
|
424
|
+
const pageRouteOverrideStore: OverrideStore<PageRouteOverrideDefinition> = { modules: {}, programmatic: {} }
|
|
425
|
+
const subscriberOverrideStore: OverrideStore<ModuleSubscriber> = { modules: {}, programmatic: {} }
|
|
426
|
+
const workerOverrideStore: OverrideStore<ModuleWorker> = { modules: {}, programmatic: {} }
|
|
427
|
+
const cliOverrideStore: OverrideStore<ModuleCli> = { modules: {}, programmatic: {} }
|
|
428
|
+
const injectionWidgetOverrideStore: OverrideStore<ModuleInjectionWidgetEntry> = { modules: {}, programmatic: {} }
|
|
429
|
+
const dashboardWidgetOverrideStore: OverrideStore<ModuleDashboardWidgetEntry> = { modules: {}, programmatic: {} }
|
|
430
|
+
const componentOverrideStore: OverrideStore<ComponentOverride | ComponentOverride[]> = { modules: {}, programmatic: {} }
|
|
431
|
+
const notificationTypeOverrideStore: OverrideStore<NotificationTypeDefinition> = { modules: {}, programmatic: {} }
|
|
432
|
+
const notificationHandlerOverrideStore: OverrideStore<NotificationHandler> = { modules: {}, programmatic: {} }
|
|
433
|
+
const apiInterceptorOverrideStore: OverrideStore<ApiInterceptor> = { modules: {}, programmatic: {} }
|
|
434
|
+
const commandInterceptorOverrideStore: OverrideStore<CommandInterceptor> = { modules: {}, programmatic: {} }
|
|
435
|
+
const responseEnricherOverrideStore: OverrideStore<ResponseEnricher> = { modules: {}, programmatic: {} }
|
|
436
|
+
const pageGuardOverrideStore: OverrideStore<PageRouteMiddleware> = { modules: {}, programmatic: {} }
|
|
437
|
+
const aclFeatureOverrideStore: OverrideStore<Exclude<AclFeatureOverride, null>> = { modules: {}, programmatic: {} }
|
|
438
|
+
const encryptionMapOverrideStore: OverrideStore<ModuleEncryptionMap> = { modules: {}, programmatic: {} }
|
|
439
|
+
const diOverrideStore: OverrideStore<Exclude<DiBindingOverride, null>> = { modules: {}, programmatic: {} }
|
|
440
|
+
const setupOverridesByModule: Record<string, SetupOverridesShape> = {}
|
|
441
|
+
|
|
442
|
+
function normalizeIdOverrideKey(key: string, label: string): string | null {
|
|
443
|
+
if (typeof key !== 'string') return null
|
|
444
|
+
const trimmed = key.trim()
|
|
445
|
+
if (trimmed) return trimmed
|
|
446
|
+
console.warn(`[Module Overrides] Skipping malformed ${label} key "${key}" — expected a non-empty string.`)
|
|
447
|
+
return null
|
|
448
|
+
}
|
|
449
|
+
|
|
450
|
+
function clearStore<T>(store: OverrideStore<T>): void {
|
|
451
|
+
for (const key of Object.keys(store.modules)) delete store.modules[key]
|
|
452
|
+
for (const key of Object.keys(store.programmatic)) delete store.programmatic[key]
|
|
453
|
+
}
|
|
454
|
+
|
|
455
|
+
// Shared frozen empty override map. Returned from compose* helpers when both
|
|
456
|
+
// the modules-tier and programmatic-tier stores are empty so per-request hot
|
|
457
|
+
// paths (page guards, etc.) reuse one object instead of allocating fresh `{}`
|
|
458
|
+
// per call. Consumers MUST NOT mutate — internal applier loops iterate keys
|
|
459
|
+
// only, and the public API hands out shallow copies through composeApiRouteOverrides.
|
|
460
|
+
const EMPTY_OVERRIDE_MAP: Readonly<Record<string, never>> = Object.freeze({})
|
|
461
|
+
|
|
462
|
+
function applyStoreOverrides<T>(
|
|
463
|
+
store: OverrideStore<T>,
|
|
464
|
+
target: 'modules' | 'programmatic',
|
|
465
|
+
overrides: OverrideMap<T> | undefined,
|
|
466
|
+
options: {
|
|
467
|
+
label: string
|
|
468
|
+
normalizeKey?: (key: string) => string | null
|
|
469
|
+
},
|
|
470
|
+
): void {
|
|
471
|
+
if (!overrides || typeof overrides !== 'object') return
|
|
472
|
+
const normalize = options.normalizeKey ?? ((key: string) => normalizeIdOverrideKey(key, options.label))
|
|
473
|
+
for (const [rawKey, value] of Object.entries(overrides)) {
|
|
474
|
+
const key = normalize(rawKey)
|
|
475
|
+
if (!key) continue
|
|
476
|
+
store[target][key] = value
|
|
477
|
+
}
|
|
478
|
+
}
|
|
479
|
+
|
|
480
|
+
function composeStore<T>(store: OverrideStore<T>): OverrideMap<T> {
|
|
481
|
+
const modulesKeys = Object.keys(store.modules)
|
|
482
|
+
const programmaticKeys = Object.keys(store.programmatic)
|
|
483
|
+
if (modulesKeys.length === 0 && programmaticKeys.length === 0) {
|
|
484
|
+
return EMPTY_OVERRIDE_MAP as OverrideMap<T>
|
|
485
|
+
}
|
|
486
|
+
return { ...store.modules, ...store.programmatic }
|
|
487
|
+
}
|
|
488
|
+
|
|
489
|
+
function applyArrayOverrides<T>(
|
|
490
|
+
items: readonly T[] | undefined,
|
|
491
|
+
overrides: Readonly<OverrideMap<T>>,
|
|
492
|
+
options: {
|
|
493
|
+
label: string
|
|
494
|
+
getId: (value: T) => string | null
|
|
495
|
+
isReplacement?: (value: unknown) => value is T
|
|
496
|
+
},
|
|
497
|
+
): { items: T[] | undefined; consumed: Set<string>; changed: boolean } {
|
|
498
|
+
if (!items || Object.keys(overrides).length === 0) {
|
|
499
|
+
return { items: items ? Array.from(items) : items, consumed: new Set(), changed: false }
|
|
500
|
+
}
|
|
501
|
+
|
|
502
|
+
const consumed = new Set<string>()
|
|
503
|
+
const result: T[] = []
|
|
504
|
+
let changed = false
|
|
505
|
+
|
|
506
|
+
for (const item of items) {
|
|
507
|
+
const id = options.getId(item)
|
|
508
|
+
if (!id || !Object.prototype.hasOwnProperty.call(overrides, id)) {
|
|
509
|
+
result.push(item)
|
|
510
|
+
continue
|
|
511
|
+
}
|
|
512
|
+
consumed.add(id)
|
|
513
|
+
changed = true
|
|
514
|
+
const replacement = overrides[id]
|
|
515
|
+
if (replacement === null) continue
|
|
516
|
+
if (options.isReplacement && !options.isReplacement(replacement)) {
|
|
517
|
+
console.warn(
|
|
518
|
+
`[Module Overrides] Skipping malformed ${options.label} override for "${id}" — replacement has the wrong shape.`,
|
|
519
|
+
)
|
|
520
|
+
result.push(item)
|
|
521
|
+
continue
|
|
522
|
+
}
|
|
523
|
+
const replacementId = options.getId(replacement)
|
|
524
|
+
if (replacementId !== id) {
|
|
525
|
+
console.warn(
|
|
526
|
+
`[Module Overrides] Skipping malformed ${options.label} override for "${id}" — replacement id must match the override key.`,
|
|
527
|
+
)
|
|
528
|
+
result.push(item)
|
|
529
|
+
continue
|
|
530
|
+
}
|
|
531
|
+
result.push(replacement)
|
|
532
|
+
}
|
|
533
|
+
|
|
534
|
+
return { items: result, consumed, changed }
|
|
535
|
+
}
|
|
536
|
+
|
|
537
|
+
function warnStaleOverrides(label: string, overrides: Readonly<Record<string, unknown>>, consumed: ReadonlySet<string>): void {
|
|
538
|
+
for (const key of Object.keys(overrides)) {
|
|
539
|
+
if (!consumed.has(key)) {
|
|
540
|
+
console.warn(`[Module Overrides] ${label} override "${key}" did not match any registered entry — override skipped.`)
|
|
541
|
+
}
|
|
542
|
+
}
|
|
543
|
+
}
|
|
544
|
+
|
|
545
|
+
function isObjectWithId(value: unknown): value is { id: string } {
|
|
546
|
+
return !!value && typeof value === 'object' && typeof (value as { id?: unknown }).id === 'string'
|
|
547
|
+
}
|
|
548
|
+
|
|
549
|
+
function isModuleSubscriber(value: unknown): value is ModuleSubscriber {
|
|
550
|
+
return isObjectWithId(value)
|
|
551
|
+
&& typeof (value as ModuleSubscriber).event === 'string'
|
|
552
|
+
&& typeof (value as ModuleSubscriber).handler === 'function'
|
|
553
|
+
}
|
|
554
|
+
|
|
555
|
+
function isModuleWorker(value: unknown): value is ModuleWorker {
|
|
556
|
+
return isObjectWithId(value)
|
|
557
|
+
&& typeof (value as ModuleWorker).queue === 'string'
|
|
558
|
+
&& typeof (value as ModuleWorker).handler === 'function'
|
|
559
|
+
}
|
|
560
|
+
|
|
561
|
+
function isModuleCli(value: unknown): value is ModuleCli {
|
|
562
|
+
return !!value
|
|
563
|
+
&& typeof value === 'object'
|
|
564
|
+
&& typeof (value as ModuleCli).command === 'string'
|
|
565
|
+
&& typeof (value as ModuleCli).run === 'function'
|
|
566
|
+
}
|
|
567
|
+
|
|
568
|
+
function isInjectionWidgetEntry(value: unknown): value is ModuleInjectionWidgetEntry {
|
|
569
|
+
return !!value
|
|
570
|
+
&& typeof value === 'object'
|
|
571
|
+
&& typeof (value as ModuleInjectionWidgetEntry).key === 'string'
|
|
572
|
+
&& typeof (value as ModuleInjectionWidgetEntry).loader === 'function'
|
|
573
|
+
}
|
|
574
|
+
|
|
575
|
+
function isDashboardWidgetEntry(value: unknown): value is ModuleDashboardWidgetEntry {
|
|
576
|
+
return !!value
|
|
577
|
+
&& typeof value === 'object'
|
|
578
|
+
&& typeof (value as ModuleDashboardWidgetEntry).key === 'string'
|
|
579
|
+
&& typeof (value as ModuleDashboardWidgetEntry).loader === 'function'
|
|
580
|
+
}
|
|
581
|
+
|
|
582
|
+
function isComponentOverrideValue(value: unknown): value is ComponentOverride | ComponentOverride[] {
|
|
583
|
+
const items = Array.isArray(value) ? value : [value]
|
|
584
|
+
return items.every((item) =>
|
|
585
|
+
!!item
|
|
586
|
+
&& typeof item === 'object'
|
|
587
|
+
&& !!(item as ComponentOverride).target
|
|
588
|
+
&& typeof (item as ComponentOverride).target.componentId === 'string',
|
|
589
|
+
)
|
|
590
|
+
}
|
|
591
|
+
|
|
592
|
+
function isNotificationType(value: unknown): value is NotificationTypeDefinition {
|
|
593
|
+
return !!value && typeof value === 'object' && typeof (value as NotificationTypeDefinition).type === 'string'
|
|
594
|
+
}
|
|
595
|
+
|
|
596
|
+
function isNotificationHandler(value: unknown): value is NotificationHandler {
|
|
597
|
+
return isObjectWithId(value) && typeof (value as NotificationHandler).handle === 'function'
|
|
598
|
+
}
|
|
599
|
+
|
|
600
|
+
function isApiInterceptor(value: unknown): value is ApiInterceptor {
|
|
601
|
+
return isObjectWithId(value) && typeof (value as ApiInterceptor).targetRoute === 'string'
|
|
602
|
+
}
|
|
603
|
+
|
|
604
|
+
function isCommandInterceptor(value: unknown): value is CommandInterceptor {
|
|
605
|
+
return isObjectWithId(value) && typeof (value as CommandInterceptor).targetCommand === 'string'
|
|
606
|
+
}
|
|
607
|
+
|
|
608
|
+
function isResponseEnricher(value: unknown): value is ResponseEnricher {
|
|
609
|
+
return isObjectWithId(value)
|
|
610
|
+
&& typeof (value as ResponseEnricher).targetEntity === 'string'
|
|
611
|
+
&& typeof (value as ResponseEnricher).enrichOne === 'function'
|
|
612
|
+
}
|
|
613
|
+
|
|
614
|
+
function isPageRouteMiddleware(value: unknown): value is PageRouteMiddleware {
|
|
615
|
+
return isObjectWithId(value) && typeof (value as PageRouteMiddleware).run === 'function'
|
|
616
|
+
}
|
|
617
|
+
|
|
618
|
+
function isEncryptionMap(value: unknown): value is ModuleEncryptionMap {
|
|
619
|
+
return !!value
|
|
620
|
+
&& typeof value === 'object'
|
|
621
|
+
&& typeof (value as ModuleEncryptionMap).entityId === 'string'
|
|
622
|
+
&& Array.isArray((value as ModuleEncryptionMap).fields)
|
|
623
|
+
}
|
|
624
|
+
|
|
625
|
+
function getFeatureId(value: unknown): string | null {
|
|
626
|
+
if (typeof value === 'string' && value.trim()) return value.trim()
|
|
627
|
+
if (value && typeof value === 'object' && typeof (value as { id?: unknown }).id === 'string') {
|
|
628
|
+
return (value as { id: string }).id
|
|
629
|
+
}
|
|
630
|
+
return null
|
|
631
|
+
}
|
|
632
|
+
|
|
633
|
+
const VALID_HTTP_METHODS: readonly HttpMethod[] = ['GET', 'POST', 'PUT', 'PATCH', 'DELETE']
|
|
634
|
+
|
|
635
|
+
const programmaticApiRouteOverrides: ApiRouteOverridesMap = {}
|
|
636
|
+
const modulesConfigApiRouteOverrides: ApiRouteOverridesMap = {}
|
|
637
|
+
|
|
638
|
+
/**
|
|
639
|
+
* Normalize an override key to the canonical `'METHOD /api/path'` form.
|
|
640
|
+
* Returns `null` if the key is malformed (missing method, unknown method,
|
|
641
|
+
* empty path). Trailing slashes on the path are stripped.
|
|
642
|
+
*/
|
|
643
|
+
function normalizeApiRouteOverrideKey(key: string): string | null {
|
|
644
|
+
if (typeof key !== 'string') return null
|
|
645
|
+
const trimmed = key.trim()
|
|
646
|
+
if (!trimmed) return null
|
|
647
|
+
const parts = trimmed.split(/\s+/)
|
|
648
|
+
if (parts.length < 2) return null
|
|
649
|
+
const method = parts[0].toUpperCase() as HttpMethod
|
|
650
|
+
if (!VALID_HTTP_METHODS.includes(method)) return null
|
|
651
|
+
const rawPath = parts.slice(1).join(' ').trim()
|
|
652
|
+
if (!rawPath) return null
|
|
653
|
+
const withLead = rawPath.startsWith('/') ? rawPath : `/${rawPath}`
|
|
654
|
+
const normalized = withLead.replace(/\/+$/, '') || '/'
|
|
655
|
+
return `${method} ${normalized}`
|
|
656
|
+
}
|
|
657
|
+
|
|
658
|
+
function normalizePageRouteOverrideKey(key: string): string | null {
|
|
659
|
+
if (typeof key !== 'string') return null
|
|
660
|
+
const trimmed = key.trim()
|
|
661
|
+
if (!trimmed) return null
|
|
662
|
+
if (trimmed.startsWith('backend:') || trimmed.startsWith('frontend:')) return trimmed
|
|
663
|
+
const withLead = trimmed.startsWith('/') ? trimmed : `/${trimmed}`
|
|
664
|
+
const stripped = withLead.replace(/\/+$/, '') || '/'
|
|
665
|
+
if (stripped === '/frontend') return 'frontend:/'
|
|
666
|
+
if (stripped.startsWith('/frontend/')) return `frontend:/${stripped.slice('/frontend/'.length)}`
|
|
667
|
+
if (stripped === '/backend' || stripped.startsWith('/backend/')) return `backend:${stripped}`
|
|
668
|
+
return `frontend:${stripped}`
|
|
669
|
+
}
|
|
670
|
+
|
|
671
|
+
/**
|
|
672
|
+
* Programmatic API: apply API-route overrides. Supersedes any matching
|
|
673
|
+
* `modules.ts` inline overrides for the same key. Call before
|
|
674
|
+
* `registerApiRouteManifests` (i.e. before generated `apiRoutes` lands in
|
|
675
|
+
* the registry); calls after registration do not retro-actively update
|
|
676
|
+
* the stored manifest.
|
|
677
|
+
*
|
|
678
|
+
* @example
|
|
679
|
+
* applyApiRouteOverrides({
|
|
680
|
+
* 'GET /api/example/items': null, // disable
|
|
681
|
+
* 'POST /api/example/items': { handler: customHandler }, // replace
|
|
682
|
+
* })
|
|
683
|
+
*/
|
|
684
|
+
export function applyApiRouteOverrides(overrides: ApiRouteOverridesMap): void {
|
|
685
|
+
if (!overrides) return
|
|
686
|
+
for (const [rawKey, value] of Object.entries(overrides)) {
|
|
687
|
+
const key = normalizeApiRouteOverrideKey(rawKey)
|
|
688
|
+
if (!key) {
|
|
689
|
+
console.warn(
|
|
690
|
+
`[Module Overrides] Skipping malformed routes.api key "${rawKey}" — expected "METHOD /api/path".`,
|
|
691
|
+
)
|
|
692
|
+
continue
|
|
693
|
+
}
|
|
694
|
+
programmaticApiRouteOverrides[key] = value
|
|
695
|
+
}
|
|
696
|
+
}
|
|
697
|
+
|
|
698
|
+
/** @__internal Test-only hook — clear programmatic + modules.ts route override state. */
|
|
699
|
+
export function resetApiRouteOverridesForTests(): void {
|
|
700
|
+
for (const key of Object.keys(programmaticApiRouteOverrides)) {
|
|
701
|
+
delete programmaticApiRouteOverrides[key]
|
|
702
|
+
}
|
|
703
|
+
for (const key of Object.keys(modulesConfigApiRouteOverrides)) {
|
|
704
|
+
delete modulesConfigApiRouteOverrides[key]
|
|
705
|
+
}
|
|
706
|
+
}
|
|
707
|
+
|
|
708
|
+
/** @__internal Test-only hook — clear every per-domain override store. */
|
|
709
|
+
export function resetModuleContractOverridesForTests(): void {
|
|
710
|
+
resetApiRouteOverridesForTests()
|
|
711
|
+
clearStore(pageRouteOverrideStore)
|
|
712
|
+
clearStore(subscriberOverrideStore)
|
|
713
|
+
clearStore(workerOverrideStore)
|
|
714
|
+
clearStore(cliOverrideStore)
|
|
715
|
+
clearStore(injectionWidgetOverrideStore)
|
|
716
|
+
clearStore(dashboardWidgetOverrideStore)
|
|
717
|
+
clearStore(componentOverrideStore)
|
|
718
|
+
clearStore(notificationTypeOverrideStore)
|
|
719
|
+
clearStore(notificationHandlerOverrideStore)
|
|
720
|
+
clearStore(apiInterceptorOverrideStore)
|
|
721
|
+
clearStore(commandInterceptorOverrideStore)
|
|
722
|
+
clearStore(responseEnricherOverrideStore)
|
|
723
|
+
clearStore(pageGuardOverrideStore)
|
|
724
|
+
clearStore(aclFeatureOverrideStore)
|
|
725
|
+
clearStore(encryptionMapOverrideStore)
|
|
726
|
+
clearStore(diOverrideStore)
|
|
727
|
+
for (const key of Object.keys(setupOverridesByModule)) delete setupOverridesByModule[key]
|
|
728
|
+
}
|
|
729
|
+
|
|
730
|
+
/**
|
|
731
|
+
* Resolve the final API-route override map. Resolution order (lowest →
|
|
732
|
+
* highest precedence):
|
|
733
|
+
*
|
|
734
|
+
* 1. `modules.ts` inline (applied via the dispatcher).
|
|
735
|
+
* 2. Programmatic (`applyApiRouteOverrides`).
|
|
736
|
+
*/
|
|
737
|
+
export function composeApiRouteOverrides(): ApiRouteOverridesMap {
|
|
738
|
+
const modulesKeys = Object.keys(modulesConfigApiRouteOverrides)
|
|
739
|
+
const programmaticKeys = Object.keys(programmaticApiRouteOverrides)
|
|
740
|
+
if (modulesKeys.length === 0 && programmaticKeys.length === 0) {
|
|
741
|
+
return EMPTY_OVERRIDE_MAP as ApiRouteOverridesMap
|
|
742
|
+
}
|
|
743
|
+
const out: ApiRouteOverridesMap = {}
|
|
744
|
+
for (const k of modulesKeys) out[k] = modulesConfigApiRouteOverrides[k]
|
|
745
|
+
for (const k of programmaticKeys) out[k] = programmaticApiRouteOverrides[k]
|
|
746
|
+
return out
|
|
747
|
+
}
|
|
748
|
+
|
|
749
|
+
export function applyPageRouteOverrides(overrides: PageRouteOverridesMap): void {
|
|
750
|
+
applyStoreOverrides(pageRouteOverrideStore, 'programmatic', overrides as OverrideMap<PageRouteOverrideDefinition>, {
|
|
751
|
+
label: 'routes.pages',
|
|
752
|
+
normalizeKey: normalizePageRouteOverrideKey,
|
|
753
|
+
})
|
|
754
|
+
}
|
|
755
|
+
|
|
756
|
+
export function composePageRouteOverrides(): PageRouteOverridesMap {
|
|
757
|
+
return composeStore(pageRouteOverrideStore) as PageRouteOverridesMap
|
|
758
|
+
}
|
|
759
|
+
|
|
760
|
+
export function applySubscriberOverrides(overrides: SubscriberOverridesMap): void {
|
|
761
|
+
applyStoreOverrides(subscriberOverrideStore, 'programmatic', overrides as OverrideMap<ModuleSubscriber>, { label: 'events.subscribers' })
|
|
762
|
+
}
|
|
763
|
+
|
|
764
|
+
export function composeSubscriberOverrides(): SubscriberOverridesMap {
|
|
765
|
+
return composeStore(subscriberOverrideStore) as SubscriberOverridesMap
|
|
766
|
+
}
|
|
767
|
+
|
|
768
|
+
export function applyWorkerOverrides(overrides: WorkerOverridesMap): void {
|
|
769
|
+
applyStoreOverrides(workerOverrideStore, 'programmatic', overrides as OverrideMap<ModuleWorker>, { label: 'workers' })
|
|
770
|
+
}
|
|
771
|
+
|
|
772
|
+
export function composeWorkerOverrides(): WorkerOverridesMap {
|
|
773
|
+
return composeStore(workerOverrideStore) as WorkerOverridesMap
|
|
774
|
+
}
|
|
775
|
+
|
|
776
|
+
export function applyCliOverrides(overrides: CliOverridesMap): void {
|
|
777
|
+
applyStoreOverrides(cliOverrideStore, 'programmatic', overrides as OverrideMap<ModuleCli>, { label: 'cli' })
|
|
778
|
+
}
|
|
779
|
+
|
|
780
|
+
export function composeCliOverrides(): CliOverridesMap {
|
|
781
|
+
return composeStore(cliOverrideStore) as CliOverridesMap
|
|
782
|
+
}
|
|
783
|
+
|
|
784
|
+
export function applyInjectionWidgetOverrides(overrides: InjectionWidgetOverridesMap): void {
|
|
785
|
+
applyStoreOverrides(injectionWidgetOverrideStore, 'programmatic', overrides as OverrideMap<ModuleInjectionWidgetEntry>, { label: 'widgets.injection' })
|
|
786
|
+
}
|
|
787
|
+
|
|
788
|
+
export function composeInjectionWidgetOverrides(): InjectionWidgetOverridesMap {
|
|
789
|
+
return composeStore(injectionWidgetOverrideStore) as InjectionWidgetOverridesMap
|
|
790
|
+
}
|
|
791
|
+
|
|
792
|
+
export function applyDashboardWidgetOverrides(overrides: DashboardWidgetOverridesMap): void {
|
|
793
|
+
applyStoreOverrides(dashboardWidgetOverrideStore, 'programmatic', overrides as OverrideMap<ModuleDashboardWidgetEntry>, { label: 'widgets.dashboard' })
|
|
794
|
+
}
|
|
795
|
+
|
|
796
|
+
export function composeDashboardWidgetOverrides(): DashboardWidgetOverridesMap {
|
|
797
|
+
return composeStore(dashboardWidgetOverrideStore) as DashboardWidgetOverridesMap
|
|
798
|
+
}
|
|
799
|
+
|
|
800
|
+
export function applyComponentOverrides(overrides: ComponentOverridesMap): void {
|
|
801
|
+
applyStoreOverrides(componentOverrideStore, 'programmatic', overrides as OverrideMap<ComponentOverride | ComponentOverride[]>, { label: 'widgets.components' })
|
|
802
|
+
}
|
|
803
|
+
|
|
804
|
+
export function composeComponentOverrides(): ComponentOverridesMap {
|
|
805
|
+
return composeStore(componentOverrideStore) as ComponentOverridesMap
|
|
806
|
+
}
|
|
807
|
+
|
|
808
|
+
export function applyNotificationTypeOverrides(overrides: NotificationTypeOverridesMap): void {
|
|
809
|
+
applyStoreOverrides(notificationTypeOverrideStore, 'programmatic', overrides as OverrideMap<NotificationTypeDefinition>, { label: 'notifications.types' })
|
|
810
|
+
}
|
|
811
|
+
|
|
812
|
+
export function composeNotificationTypeOverrides(): NotificationTypeOverridesMap {
|
|
813
|
+
return composeStore(notificationTypeOverrideStore) as NotificationTypeOverridesMap
|
|
814
|
+
}
|
|
815
|
+
|
|
816
|
+
export function applyNotificationHandlerOverrides(overrides: NotificationHandlerOverridesMap): void {
|
|
817
|
+
applyStoreOverrides(notificationHandlerOverrideStore, 'programmatic', overrides as OverrideMap<NotificationHandler>, { label: 'notifications.handlers' })
|
|
818
|
+
}
|
|
819
|
+
|
|
820
|
+
export function composeNotificationHandlerOverrides(): NotificationHandlerOverridesMap {
|
|
821
|
+
return composeStore(notificationHandlerOverrideStore) as NotificationHandlerOverridesMap
|
|
822
|
+
}
|
|
823
|
+
|
|
824
|
+
export function applyApiInterceptorOverrides(overrides: ApiInterceptorOverridesMap): void {
|
|
825
|
+
applyStoreOverrides(apiInterceptorOverrideStore, 'programmatic', overrides as OverrideMap<ApiInterceptor>, { label: 'interceptors' })
|
|
826
|
+
}
|
|
827
|
+
|
|
828
|
+
export function composeApiInterceptorOverrides(): ApiInterceptorOverridesMap {
|
|
829
|
+
return composeStore(apiInterceptorOverrideStore) as ApiInterceptorOverridesMap
|
|
830
|
+
}
|
|
831
|
+
|
|
832
|
+
export function applyCommandInterceptorOverrides(overrides: CommandInterceptorOverridesMap): void {
|
|
833
|
+
applyStoreOverrides(commandInterceptorOverrideStore, 'programmatic', overrides as OverrideMap<CommandInterceptor>, { label: 'commandInterceptors' })
|
|
834
|
+
}
|
|
835
|
+
|
|
836
|
+
export function composeCommandInterceptorOverrides(): CommandInterceptorOverridesMap {
|
|
837
|
+
return composeStore(commandInterceptorOverrideStore) as CommandInterceptorOverridesMap
|
|
838
|
+
}
|
|
839
|
+
|
|
840
|
+
export function applyResponseEnricherOverrides(overrides: ResponseEnricherOverridesMap): void {
|
|
841
|
+
applyStoreOverrides(responseEnricherOverrideStore, 'programmatic', overrides as OverrideMap<ResponseEnricher>, { label: 'enrichers' })
|
|
842
|
+
}
|
|
843
|
+
|
|
844
|
+
export function composeResponseEnricherOverrides(): ResponseEnricherOverridesMap {
|
|
845
|
+
return composeStore(responseEnricherOverrideStore) as ResponseEnricherOverridesMap
|
|
846
|
+
}
|
|
847
|
+
|
|
848
|
+
export function applyPageGuardOverrides(overrides: PageGuardOverridesMap): void {
|
|
849
|
+
applyStoreOverrides(pageGuardOverrideStore, 'programmatic', overrides as OverrideMap<PageRouteMiddleware>, { label: 'guards' })
|
|
850
|
+
}
|
|
851
|
+
|
|
852
|
+
export function composePageGuardOverrides(): PageGuardOverridesMap {
|
|
853
|
+
return composeStore(pageGuardOverrideStore) as PageGuardOverridesMap
|
|
854
|
+
}
|
|
855
|
+
|
|
856
|
+
export function applyAclFeatureOverrides(overrides: AclFeatureOverridesMap): void {
|
|
857
|
+
applyStoreOverrides(aclFeatureOverrideStore, 'programmatic', overrides as OverrideMap<Exclude<AclFeatureOverride, null>>, { label: 'acl.features' })
|
|
858
|
+
}
|
|
859
|
+
|
|
860
|
+
export function composeAclFeatureOverrides(): AclFeatureOverridesMap {
|
|
861
|
+
return composeStore(aclFeatureOverrideStore) as AclFeatureOverridesMap
|
|
862
|
+
}
|
|
863
|
+
|
|
864
|
+
export function applyEncryptionMapOverrides(overrides: EncryptionMapOverridesMap): void {
|
|
865
|
+
applyStoreOverrides(encryptionMapOverrideStore, 'programmatic', overrides as OverrideMap<ModuleEncryptionMap>, { label: 'encryption.maps' })
|
|
866
|
+
}
|
|
867
|
+
|
|
868
|
+
export function composeEncryptionMapOverrides(): EncryptionMapOverridesMap {
|
|
869
|
+
return composeStore(encryptionMapOverrideStore) as EncryptionMapOverridesMap
|
|
870
|
+
}
|
|
871
|
+
|
|
872
|
+
export function applyDiOverrides(overrides: DiOverridesMap): void {
|
|
873
|
+
applyStoreOverrides(diOverrideStore, 'programmatic', overrides as OverrideMap<Exclude<DiBindingOverride, null>>, { label: 'di' })
|
|
874
|
+
}
|
|
875
|
+
|
|
876
|
+
export function composeDiOverrides(): DiOverridesMap {
|
|
877
|
+
return composeStore(diOverrideStore) as DiOverridesMap
|
|
878
|
+
}
|
|
879
|
+
|
|
880
|
+
/**
|
|
881
|
+
* Apply an API-route override map to a list of registered manifest entries.
|
|
882
|
+
* Returns a NEW array — never mutates input.
|
|
883
|
+
*
|
|
884
|
+
* Behavior:
|
|
885
|
+
* - For each entry, walk every method in `entry.methods`.
|
|
886
|
+
* - If the override key `'METHOD path'` is `null`, drop that method from
|
|
887
|
+
* the entry's `methods` array.
|
|
888
|
+
* - If the override key is a definition, wrap `entry.load()` to return a
|
|
889
|
+
* module that exposes the override handler at `module[METHOD]` (top-level)
|
|
890
|
+
* and the override metadata at `module.metadata[METHOD]`.
|
|
891
|
+
* - If every method on the entry is disabled, the entry is dropped.
|
|
892
|
+
* - Overrides naming a key that does not match any entry log a single
|
|
893
|
+
* warning so an operator notices a stale override.
|
|
894
|
+
*/
|
|
895
|
+
export function applyApiOverridesToManifests<T extends ApiRouteManifestEntry>(
|
|
896
|
+
routes: readonly T[],
|
|
897
|
+
overrides: Readonly<ApiRouteOverridesMap>,
|
|
898
|
+
): T[] {
|
|
899
|
+
if (!routes || routes.length === 0) return Array.from(routes)
|
|
900
|
+
const overrideKeys = Object.keys(overrides)
|
|
901
|
+
if (overrideKeys.length === 0) return Array.from(routes)
|
|
902
|
+
|
|
903
|
+
const consumedKeys = new Set<string>()
|
|
904
|
+
const result: T[] = []
|
|
905
|
+
|
|
906
|
+
for (const entry of routes) {
|
|
907
|
+
const path = entry.path
|
|
908
|
+
const remainingMethods: HttpMethod[] = []
|
|
909
|
+
const methodOverrides = new Map<HttpMethod, ApiRouteOverrideDefinition>()
|
|
910
|
+
|
|
911
|
+
for (const method of entry.methods) {
|
|
912
|
+
const manifestKey = `${method} ${path}`
|
|
913
|
+
const publicKey = path.startsWith('/api/')
|
|
914
|
+
? manifestKey
|
|
915
|
+
: `${method} /api${path === '/' ? '' : path}`
|
|
916
|
+
const key = Object.prototype.hasOwnProperty.call(overrides, manifestKey)
|
|
917
|
+
? manifestKey
|
|
918
|
+
: Object.prototype.hasOwnProperty.call(overrides, publicKey)
|
|
919
|
+
? publicKey
|
|
920
|
+
: null
|
|
921
|
+
if (!key) {
|
|
922
|
+
remainingMethods.push(method)
|
|
923
|
+
continue
|
|
924
|
+
}
|
|
925
|
+
consumedKeys.add(key)
|
|
926
|
+
const value = overrides[key]
|
|
927
|
+
if (value === null) continue
|
|
928
|
+
if (value && typeof value === 'object' && typeof value.handler === 'function') {
|
|
929
|
+
methodOverrides.set(method, value)
|
|
930
|
+
remainingMethods.push(method)
|
|
931
|
+
} else {
|
|
932
|
+
console.warn(
|
|
933
|
+
`[Module Overrides] Skipping malformed routes.api override for "${key}" — expected { handler, metadata? } or null.`,
|
|
934
|
+
)
|
|
935
|
+
remainingMethods.push(method)
|
|
936
|
+
}
|
|
937
|
+
}
|
|
938
|
+
|
|
939
|
+
if (remainingMethods.length === 0) continue
|
|
940
|
+
|
|
941
|
+
const methodsChanged = remainingMethods.length !== entry.methods.length
|
|
942
|
+
const hasReplacements = methodOverrides.size > 0
|
|
943
|
+
|
|
944
|
+
if (!methodsChanged && !hasReplacements) {
|
|
945
|
+
result.push(entry)
|
|
946
|
+
continue
|
|
947
|
+
}
|
|
948
|
+
|
|
949
|
+
const originalLoad = entry.load
|
|
950
|
+
const wrappedLoad: T['load'] = hasReplacements
|
|
951
|
+
? async () => {
|
|
952
|
+
const original = await originalLoad()
|
|
953
|
+
const out: Record<string, unknown> = { ...original }
|
|
954
|
+
const baseMetadata = original && typeof original === 'object' && original.metadata !== undefined
|
|
955
|
+
? original.metadata
|
|
956
|
+
: null
|
|
957
|
+
const mergedMetadata: Record<string, unknown> = baseMetadata && typeof baseMetadata === 'object'
|
|
958
|
+
? { ...(baseMetadata as Record<string, unknown>) }
|
|
959
|
+
: {}
|
|
960
|
+
for (const [method, def] of methodOverrides) {
|
|
961
|
+
out[method] = def.handler
|
|
962
|
+
if (def.metadata !== undefined) mergedMetadata[method] = def.metadata
|
|
963
|
+
}
|
|
964
|
+
if (Object.keys(mergedMetadata).length > 0) out.metadata = mergedMetadata
|
|
965
|
+
return out
|
|
966
|
+
}
|
|
967
|
+
: originalLoad
|
|
968
|
+
|
|
969
|
+
result.push({ ...entry, methods: remainingMethods, load: wrappedLoad })
|
|
970
|
+
}
|
|
971
|
+
|
|
972
|
+
for (const key of overrideKeys) {
|
|
973
|
+
if (!consumedKeys.has(key)) {
|
|
974
|
+
console.warn(
|
|
975
|
+
`[Module Overrides] routes.api override "${key}" did not match any registered API route — override skipped.`,
|
|
976
|
+
)
|
|
977
|
+
}
|
|
978
|
+
}
|
|
979
|
+
|
|
980
|
+
return result
|
|
981
|
+
}
|
|
982
|
+
|
|
983
|
+
export function applyPageOverridesToManifests<T extends BackendRouteManifestEntry | FrontendRouteManifestEntry>(
|
|
984
|
+
routes: readonly T[],
|
|
985
|
+
overrides: Readonly<PageRouteOverridesMap>,
|
|
986
|
+
kind: 'backend' | 'frontend',
|
|
987
|
+
): T[] {
|
|
988
|
+
if (!routes || routes.length === 0) return Array.from(routes)
|
|
989
|
+
const normalizedOverrides: PageRouteOverridesMap = {}
|
|
990
|
+
for (const [rawKey, value] of Object.entries(overrides)) {
|
|
991
|
+
const key = normalizePageRouteOverrideKey(rawKey)
|
|
992
|
+
if (!key) {
|
|
993
|
+
console.warn(
|
|
994
|
+
`[Module Overrides] Skipping malformed routes.pages key "${rawKey}" — expected "/backend/path" or "/frontend/path".`,
|
|
995
|
+
)
|
|
996
|
+
continue
|
|
997
|
+
}
|
|
998
|
+
normalizedOverrides[key] = value
|
|
999
|
+
}
|
|
1000
|
+
const overrideKeys = Object.keys(normalizedOverrides).filter((key) => key.startsWith(`${kind}:`))
|
|
1001
|
+
if (overrideKeys.length === 0) return Array.from(routes)
|
|
1002
|
+
|
|
1003
|
+
const consumed = new Set<string>()
|
|
1004
|
+
const result: T[] = []
|
|
1005
|
+
|
|
1006
|
+
for (const entry of routes) {
|
|
1007
|
+
const path = entry.pattern ?? entry.path ?? '/'
|
|
1008
|
+
const key = `${kind}:${path.replace(/\/+$/, '') || '/'}`
|
|
1009
|
+
if (!Object.prototype.hasOwnProperty.call(normalizedOverrides, key)) {
|
|
1010
|
+
result.push(entry)
|
|
1011
|
+
continue
|
|
1012
|
+
}
|
|
1013
|
+
|
|
1014
|
+
consumed.add(key)
|
|
1015
|
+
const override = normalizedOverrides[key]
|
|
1016
|
+
if (override === null) continue
|
|
1017
|
+
if (!override || typeof override !== 'object') {
|
|
1018
|
+
console.warn(
|
|
1019
|
+
`[Module Overrides] Skipping malformed routes.pages override for "${key}" — expected { load?, Component?, metadata? } or null.`,
|
|
1020
|
+
)
|
|
1021
|
+
result.push(entry)
|
|
1022
|
+
continue
|
|
1023
|
+
}
|
|
1024
|
+
|
|
1025
|
+
const hasLoad = typeof override.load === 'function'
|
|
1026
|
+
const hasComponent = typeof override.Component === 'function'
|
|
1027
|
+
if (!hasLoad && !hasComponent && override.metadata === undefined) {
|
|
1028
|
+
console.warn(
|
|
1029
|
+
`[Module Overrides] Skipping malformed routes.pages override for "${key}" — expected a loader, component, metadata, or null.`,
|
|
1030
|
+
)
|
|
1031
|
+
result.push(entry)
|
|
1032
|
+
continue
|
|
1033
|
+
}
|
|
1034
|
+
|
|
1035
|
+
const metadata = override.metadata && typeof override.metadata === 'object'
|
|
1036
|
+
? override.metadata
|
|
1037
|
+
: {}
|
|
1038
|
+
const load = hasLoad
|
|
1039
|
+
? override.load!
|
|
1040
|
+
: hasComponent
|
|
1041
|
+
? async () => override.Component!
|
|
1042
|
+
: entry.load
|
|
1043
|
+
|
|
1044
|
+
result.push({ ...entry, ...metadata, load })
|
|
1045
|
+
}
|
|
1046
|
+
|
|
1047
|
+
warnStaleOverrides('routes.pages', Object.fromEntries(
|
|
1048
|
+
overrideKeys.map((key) => [key, normalizedOverrides[key]]),
|
|
1049
|
+
), consumed)
|
|
1050
|
+
return result
|
|
1051
|
+
}
|
|
1052
|
+
|
|
1053
|
+
function applyEntryListOverrides<TEntry extends { moduleId: string }, TValue>(
|
|
1054
|
+
entries: readonly TEntry[] | undefined,
|
|
1055
|
+
overrides: Readonly<OverrideMap<TValue>>,
|
|
1056
|
+
options: {
|
|
1057
|
+
listKey: keyof TEntry
|
|
1058
|
+
label: string
|
|
1059
|
+
getId: (value: TValue) => string | null
|
|
1060
|
+
isReplacement: (value: unknown) => value is TValue
|
|
1061
|
+
},
|
|
1062
|
+
): TEntry[] | undefined {
|
|
1063
|
+
// Fast path: when no overrides are registered, return the input directly.
|
|
1064
|
+
// All known callers iterate the result (.forEach/.flatMap/.map/storage),
|
|
1065
|
+
// never mutate it. This avoids a per-request Array.from copy on hot paths
|
|
1066
|
+
// like executePageMiddleware → applyPageGuardOverridesToEntries.
|
|
1067
|
+
if (!entries || Object.keys(overrides).length === 0) return entries as TEntry[] | undefined
|
|
1068
|
+
const consumed = new Set<string>()
|
|
1069
|
+
let changed = false
|
|
1070
|
+
const result = entries.map((entry) => {
|
|
1071
|
+
const list = entry[options.listKey]
|
|
1072
|
+
if (!Array.isArray(list)) return entry
|
|
1073
|
+
const applied = applyArrayOverrides(list as TValue[], overrides, {
|
|
1074
|
+
label: options.label,
|
|
1075
|
+
getId: options.getId,
|
|
1076
|
+
isReplacement: options.isReplacement,
|
|
1077
|
+
})
|
|
1078
|
+
for (const key of applied.consumed) consumed.add(key)
|
|
1079
|
+
if (!applied.changed) return entry
|
|
1080
|
+
changed = true
|
|
1081
|
+
return { ...entry, [options.listKey]: applied.items ?? [] }
|
|
1082
|
+
})
|
|
1083
|
+
warnStaleOverrides(options.label, overrides, consumed)
|
|
1084
|
+
return changed ? result : Array.from(entries)
|
|
1085
|
+
}
|
|
1086
|
+
|
|
1087
|
+
export function applyInjectionWidgetOverridesToEntries(
|
|
1088
|
+
entries: readonly ModuleInjectionWidgetEntry[],
|
|
1089
|
+
overrides: Readonly<InjectionWidgetOverridesMap> = composeInjectionWidgetOverrides(),
|
|
1090
|
+
): ModuleInjectionWidgetEntry[] {
|
|
1091
|
+
const applied = applyArrayOverrides(entries, overrides as OverrideMap<ModuleInjectionWidgetEntry>, {
|
|
1092
|
+
label: 'widgets.injection',
|
|
1093
|
+
getId: (entry) => entry.key,
|
|
1094
|
+
isReplacement: isInjectionWidgetEntry,
|
|
1095
|
+
})
|
|
1096
|
+
warnStaleOverrides('widgets.injection', overrides, applied.consumed)
|
|
1097
|
+
return applied.items ?? []
|
|
1098
|
+
}
|
|
1099
|
+
|
|
1100
|
+
export function applyDashboardWidgetOverridesToEntries(
|
|
1101
|
+
entries: readonly ModuleDashboardWidgetEntry[],
|
|
1102
|
+
overrides: Readonly<DashboardWidgetOverridesMap> = composeDashboardWidgetOverrides(),
|
|
1103
|
+
): ModuleDashboardWidgetEntry[] {
|
|
1104
|
+
const applied = applyArrayOverrides(entries, overrides as OverrideMap<ModuleDashboardWidgetEntry>, {
|
|
1105
|
+
label: 'widgets.dashboard',
|
|
1106
|
+
getId: (entry) => entry.key,
|
|
1107
|
+
isReplacement: isDashboardWidgetEntry,
|
|
1108
|
+
})
|
|
1109
|
+
warnStaleOverrides('widgets.dashboard', overrides, applied.consumed)
|
|
1110
|
+
return applied.items ?? []
|
|
1111
|
+
}
|
|
1112
|
+
|
|
1113
|
+
export function applyWorkerOverridesToDescriptors<T extends { id: string; queue: string; concurrency: number; handler: unknown }>(
|
|
1114
|
+
entries: readonly T[],
|
|
1115
|
+
overrides: Readonly<WorkerOverridesMap> = composeWorkerOverrides(),
|
|
1116
|
+
): T[] {
|
|
1117
|
+
const applied = applyArrayOverrides(entries, overrides as OverrideMap<T>, {
|
|
1118
|
+
label: 'workers',
|
|
1119
|
+
getId: (entry) => entry.id,
|
|
1120
|
+
isReplacement: (value): value is T =>
|
|
1121
|
+
!!value
|
|
1122
|
+
&& typeof value === 'object'
|
|
1123
|
+
&& typeof (value as { id?: unknown }).id === 'string'
|
|
1124
|
+
&& typeof (value as { queue?: unknown }).queue === 'string'
|
|
1125
|
+
&& typeof (value as { concurrency?: unknown }).concurrency === 'number'
|
|
1126
|
+
&& typeof (value as { handler?: unknown }).handler === 'function',
|
|
1127
|
+
})
|
|
1128
|
+
warnStaleOverrides('workers', overrides, applied.consumed)
|
|
1129
|
+
return applied.items ?? []
|
|
1130
|
+
}
|
|
1131
|
+
|
|
1132
|
+
export function applyInjectionWidgetOverridesToTables(
|
|
1133
|
+
tables: readonly { moduleId: string; table: ModuleInjectionTable }[],
|
|
1134
|
+
overrides: Readonly<InjectionWidgetOverridesMap> = composeInjectionWidgetOverrides(),
|
|
1135
|
+
): Array<{ moduleId: string; table: ModuleInjectionTable }> {
|
|
1136
|
+
const disabled = new Set(Object.entries(overrides).filter(([, value]) => value === null).map(([key]) => key))
|
|
1137
|
+
if (disabled.size === 0) return Array.from(tables)
|
|
1138
|
+
|
|
1139
|
+
const filterSlot = (slot: unknown): unknown => {
|
|
1140
|
+
if (typeof slot === 'string') return disabled.has(slot) ? null : slot
|
|
1141
|
+
if (slot && typeof slot === 'object') {
|
|
1142
|
+
const widgetId = (slot as { widgetId?: unknown }).widgetId
|
|
1143
|
+
return typeof widgetId === 'string' && disabled.has(widgetId) ? null : slot
|
|
1144
|
+
}
|
|
1145
|
+
return slot
|
|
1146
|
+
}
|
|
1147
|
+
|
|
1148
|
+
return tables.map((entry) => {
|
|
1149
|
+
const nextTable: ModuleInjectionTable = {}
|
|
1150
|
+
for (const [spotId, rawSlot] of Object.entries(entry.table)) {
|
|
1151
|
+
const filtered = Array.isArray(rawSlot)
|
|
1152
|
+
? rawSlot.map(filterSlot).filter((slot): slot is NonNullable<typeof slot> => slot !== null)
|
|
1153
|
+
: filterSlot(rawSlot)
|
|
1154
|
+
if (Array.isArray(filtered)) {
|
|
1155
|
+
if (filtered.length > 0) nextTable[spotId] = filtered as ModuleInjectionTable[string]
|
|
1156
|
+
} else if (filtered !== null) {
|
|
1157
|
+
nextTable[spotId] = filtered as ModuleInjectionTable[string]
|
|
1158
|
+
}
|
|
1159
|
+
}
|
|
1160
|
+
return { ...entry, table: nextTable }
|
|
1161
|
+
})
|
|
1162
|
+
}
|
|
1163
|
+
|
|
1164
|
+
export function applyComponentOverridesToEntries(
|
|
1165
|
+
entries: readonly { moduleId: string; componentOverrides: ComponentOverride[] }[],
|
|
1166
|
+
overrides: Readonly<ComponentOverridesMap> = composeComponentOverrides(),
|
|
1167
|
+
): Array<{ moduleId: string; componentOverrides: ComponentOverride[] }> {
|
|
1168
|
+
const overrideKeys = Object.keys(overrides)
|
|
1169
|
+
if (overrideKeys.length === 0) return Array.from(entries)
|
|
1170
|
+
const consumed = new Set<string>()
|
|
1171
|
+
const additions: ComponentOverride[] = []
|
|
1172
|
+
const result = entries.map((entry) => {
|
|
1173
|
+
const next = entry.componentOverrides.filter((override) => {
|
|
1174
|
+
const id = override.target.componentId
|
|
1175
|
+
if (!Object.prototype.hasOwnProperty.call(overrides, id)) return true
|
|
1176
|
+
consumed.add(id)
|
|
1177
|
+
return false
|
|
1178
|
+
})
|
|
1179
|
+
return { ...entry, componentOverrides: next }
|
|
1180
|
+
})
|
|
1181
|
+
for (const key of overrideKeys) {
|
|
1182
|
+
const value = overrides[key]
|
|
1183
|
+
if (value === null) continue
|
|
1184
|
+
if (!isComponentOverrideValue(value)) {
|
|
1185
|
+
console.warn(`[Module Overrides] Skipping malformed widgets.components override for "${key}" — expected ComponentOverride, ComponentOverride[], or null.`)
|
|
1186
|
+
continue
|
|
1187
|
+
}
|
|
1188
|
+
const values = Array.isArray(value) ? value : [value]
|
|
1189
|
+
for (const override of values) {
|
|
1190
|
+
if (override.target.componentId !== key) {
|
|
1191
|
+
console.warn(`[Module Overrides] Skipping malformed widgets.components override for "${key}" — target.componentId must match the override key.`)
|
|
1192
|
+
continue
|
|
1193
|
+
}
|
|
1194
|
+
additions.push(override)
|
|
1195
|
+
consumed.add(key)
|
|
1196
|
+
}
|
|
1197
|
+
}
|
|
1198
|
+
if (additions.length > 0) {
|
|
1199
|
+
result.push({ moduleId: 'overrides', componentOverrides: additions })
|
|
1200
|
+
}
|
|
1201
|
+
warnStaleOverrides('widgets.components', overrides, consumed)
|
|
1202
|
+
return result
|
|
1203
|
+
}
|
|
1204
|
+
|
|
1205
|
+
export function applyNotificationTypeOverridesToEntries(
|
|
1206
|
+
entries: readonly { moduleId: string; types: NotificationTypeDefinition[] }[],
|
|
1207
|
+
overrides: Readonly<NotificationTypeOverridesMap> = composeNotificationTypeOverrides(),
|
|
1208
|
+
): Array<{ moduleId: string; types: NotificationTypeDefinition[] }> {
|
|
1209
|
+
return applyEntryListOverrides(entries, overrides as OverrideMap<NotificationTypeDefinition>, {
|
|
1210
|
+
listKey: 'types',
|
|
1211
|
+
label: 'notifications.types',
|
|
1212
|
+
getId: (entry) => entry.type,
|
|
1213
|
+
isReplacement: isNotificationType,
|
|
1214
|
+
}) ?? []
|
|
1215
|
+
}
|
|
1216
|
+
|
|
1217
|
+
export function applyNotificationHandlerOverridesToEntries(
|
|
1218
|
+
entries: readonly { moduleId: string; handlers: NotificationHandler[] }[],
|
|
1219
|
+
overrides: Readonly<NotificationHandlerOverridesMap> = composeNotificationHandlerOverrides(),
|
|
1220
|
+
): Array<{ moduleId: string; handlers: NotificationHandler[] }> {
|
|
1221
|
+
return applyEntryListOverrides(entries, overrides as OverrideMap<NotificationHandler>, {
|
|
1222
|
+
listKey: 'handlers',
|
|
1223
|
+
label: 'notifications.handlers',
|
|
1224
|
+
getId: (entry) => entry.id,
|
|
1225
|
+
isReplacement: isNotificationHandler,
|
|
1226
|
+
}) ?? []
|
|
1227
|
+
}
|
|
1228
|
+
|
|
1229
|
+
export function applyApiInterceptorOverridesToEntries(
|
|
1230
|
+
entries: readonly { moduleId: string; interceptors: ApiInterceptor[] }[],
|
|
1231
|
+
overrides: Readonly<ApiInterceptorOverridesMap> = composeApiInterceptorOverrides(),
|
|
1232
|
+
): Array<{ moduleId: string; interceptors: ApiInterceptor[] }> {
|
|
1233
|
+
return applyEntryListOverrides(entries, overrides as OverrideMap<ApiInterceptor>, {
|
|
1234
|
+
listKey: 'interceptors',
|
|
1235
|
+
label: 'interceptors',
|
|
1236
|
+
getId: (entry) => entry.id,
|
|
1237
|
+
isReplacement: isApiInterceptor,
|
|
1238
|
+
}) ?? []
|
|
1239
|
+
}
|
|
1240
|
+
|
|
1241
|
+
export function applyCommandInterceptorOverridesToEntries(
|
|
1242
|
+
entries: readonly { moduleId: string; interceptors: CommandInterceptor[] }[],
|
|
1243
|
+
overrides: Readonly<CommandInterceptorOverridesMap> = composeCommandInterceptorOverrides(),
|
|
1244
|
+
): Array<{ moduleId: string; interceptors: CommandInterceptor[] }> {
|
|
1245
|
+
return applyEntryListOverrides(entries, overrides as OverrideMap<CommandInterceptor>, {
|
|
1246
|
+
listKey: 'interceptors',
|
|
1247
|
+
label: 'commandInterceptors',
|
|
1248
|
+
getId: (entry) => entry.id,
|
|
1249
|
+
isReplacement: isCommandInterceptor,
|
|
1250
|
+
}) ?? []
|
|
1251
|
+
}
|
|
1252
|
+
|
|
1253
|
+
export function applyResponseEnricherOverridesToEntries(
|
|
1254
|
+
entries: readonly { moduleId: string; enrichers: ResponseEnricher[] }[],
|
|
1255
|
+
overrides: Readonly<ResponseEnricherOverridesMap> = composeResponseEnricherOverrides(),
|
|
1256
|
+
): Array<{ moduleId: string; enrichers: ResponseEnricher[] }> {
|
|
1257
|
+
return applyEntryListOverrides(entries, overrides as OverrideMap<ResponseEnricher>, {
|
|
1258
|
+
listKey: 'enrichers',
|
|
1259
|
+
label: 'enrichers',
|
|
1260
|
+
getId: (entry) => entry.id,
|
|
1261
|
+
isReplacement: isResponseEnricher,
|
|
1262
|
+
}) ?? []
|
|
1263
|
+
}
|
|
1264
|
+
|
|
1265
|
+
export function applyPageGuardOverridesToEntries(
|
|
1266
|
+
entries: readonly PageMiddlewareRegistryEntry[],
|
|
1267
|
+
overrides: Readonly<PageGuardOverridesMap> = composePageGuardOverrides(),
|
|
1268
|
+
): PageMiddlewareRegistryEntry[] {
|
|
1269
|
+
return applyEntryListOverrides(entries, overrides as OverrideMap<PageRouteMiddleware>, {
|
|
1270
|
+
listKey: 'middleware',
|
|
1271
|
+
label: 'guards',
|
|
1272
|
+
getId: (entry) => entry.id,
|
|
1273
|
+
isReplacement: isPageRouteMiddleware,
|
|
1274
|
+
}) ?? []
|
|
1275
|
+
}
|
|
1276
|
+
|
|
1277
|
+
function applySetupOverride(setup: ModuleSetupConfig | undefined, override: SetupOverridesShape | undefined): ModuleSetupConfig | undefined {
|
|
1278
|
+
if (!override) return setup
|
|
1279
|
+
const next: ModuleSetupConfig = { ...(setup ?? {}) }
|
|
1280
|
+
if (override.defaultRoleFeatures) {
|
|
1281
|
+
next.defaultRoleFeatures = Object.fromEntries(
|
|
1282
|
+
Object.entries(override.defaultRoleFeatures).map(([role, features]) => [role, Array.from(features)]),
|
|
1283
|
+
)
|
|
1284
|
+
}
|
|
1285
|
+
if (override.defaultCustomerRoleFeatures) {
|
|
1286
|
+
next.defaultCustomerRoleFeatures = Object.fromEntries(
|
|
1287
|
+
Object.entries(override.defaultCustomerRoleFeatures).map(([role, features]) => [role, Array.from(features)]),
|
|
1288
|
+
)
|
|
1289
|
+
}
|
|
1290
|
+
if (override.seedDefaults === false) delete next.seedDefaults
|
|
1291
|
+
if (override.seedExamples === false) delete next.seedExamples
|
|
1292
|
+
if (override.onTenantCreated === false) delete next.onTenantCreated
|
|
1293
|
+
return next
|
|
1294
|
+
}
|
|
1295
|
+
|
|
1296
|
+
export function applyModuleOverridesToModules(modules: readonly Module[]): Module[] {
|
|
1297
|
+
const subscriberOverrides = composeSubscriberOverrides()
|
|
1298
|
+
const workerOverrides = composeWorkerOverrides()
|
|
1299
|
+
const cliOverrides = composeCliOverrides()
|
|
1300
|
+
const aclOverrides = composeAclFeatureOverrides()
|
|
1301
|
+
const encryptionOverrides = composeEncryptionMapOverrides()
|
|
1302
|
+
const hasSetupOverrides = Object.keys(setupOverridesByModule).length > 0
|
|
1303
|
+
const hasAny =
|
|
1304
|
+
Object.keys(subscriberOverrides).length > 0
|
|
1305
|
+
|| Object.keys(workerOverrides).length > 0
|
|
1306
|
+
|| Object.keys(cliOverrides).length > 0
|
|
1307
|
+
|| Object.keys(aclOverrides).length > 0
|
|
1308
|
+
|| Object.keys(encryptionOverrides).length > 0
|
|
1309
|
+
|| hasSetupOverrides
|
|
1310
|
+
|
|
1311
|
+
if (!hasAny) return modules as Module[]
|
|
1312
|
+
|
|
1313
|
+
const subscriberConsumed = new Set<string>()
|
|
1314
|
+
const workerConsumed = new Set<string>()
|
|
1315
|
+
const cliConsumed = new Set<string>()
|
|
1316
|
+
const aclConsumed = new Set<string>()
|
|
1317
|
+
const encryptionConsumed = new Set<string>()
|
|
1318
|
+
|
|
1319
|
+
const result = modules.map((module) => {
|
|
1320
|
+
let next = module
|
|
1321
|
+
|
|
1322
|
+
const subscribers = applyArrayOverrides(module.subscribers, subscriberOverrides as OverrideMap<ModuleSubscriber>, {
|
|
1323
|
+
label: 'events.subscribers',
|
|
1324
|
+
getId: (entry) => entry.id,
|
|
1325
|
+
isReplacement: isModuleSubscriber,
|
|
1326
|
+
})
|
|
1327
|
+
for (const key of subscribers.consumed) subscriberConsumed.add(key)
|
|
1328
|
+
if (subscribers.changed) next = { ...next, subscribers: subscribers.items }
|
|
1329
|
+
|
|
1330
|
+
const workers = applyArrayOverrides(module.workers, workerOverrides as OverrideMap<ModuleWorker>, {
|
|
1331
|
+
label: 'workers',
|
|
1332
|
+
getId: (entry) => entry.id,
|
|
1333
|
+
isReplacement: isModuleWorker,
|
|
1334
|
+
})
|
|
1335
|
+
for (const key of workers.consumed) workerConsumed.add(key)
|
|
1336
|
+
if (workers.changed) next = { ...next, workers: workers.items }
|
|
1337
|
+
|
|
1338
|
+
const cli = applyArrayOverrides(module.cli, cliOverrides as OverrideMap<ModuleCli>, {
|
|
1339
|
+
label: 'cli',
|
|
1340
|
+
getId: (entry) => entry.command,
|
|
1341
|
+
isReplacement: isModuleCli,
|
|
1342
|
+
})
|
|
1343
|
+
for (const key of cli.consumed) cliConsumed.add(key)
|
|
1344
|
+
if (cli.changed) next = { ...next, cli: cli.items }
|
|
1345
|
+
|
|
1346
|
+
const features = applyArrayOverrides(module.features, aclOverrides as OverrideMap<NonNullable<Module['features']>[number]>, {
|
|
1347
|
+
label: 'acl.features',
|
|
1348
|
+
getId: getFeatureId,
|
|
1349
|
+
isReplacement: (value): value is NonNullable<Module['features']>[number] => getFeatureId(value) !== null,
|
|
1350
|
+
})
|
|
1351
|
+
for (const key of features.consumed) aclConsumed.add(key)
|
|
1352
|
+
if (features.changed) next = { ...next, features: features.items }
|
|
1353
|
+
|
|
1354
|
+
const maps = applyArrayOverrides(module.defaultEncryptionMaps, encryptionOverrides as OverrideMap<ModuleEncryptionMap>, {
|
|
1355
|
+
label: 'encryption.maps',
|
|
1356
|
+
getId: (entry) => entry.entityId,
|
|
1357
|
+
isReplacement: isEncryptionMap,
|
|
1358
|
+
})
|
|
1359
|
+
for (const key of maps.consumed) encryptionConsumed.add(key)
|
|
1360
|
+
if (maps.changed) next = { ...next, defaultEncryptionMaps: maps.items }
|
|
1361
|
+
|
|
1362
|
+
const setupOverride = setupOverridesByModule[module.id]
|
|
1363
|
+
if (setupOverride) {
|
|
1364
|
+
next = { ...next, setup: applySetupOverride(module.setup, setupOverride) }
|
|
1365
|
+
}
|
|
1366
|
+
|
|
1367
|
+
return next
|
|
1368
|
+
})
|
|
1369
|
+
|
|
1370
|
+
warnStaleOverrides('events.subscribers', subscriberOverrides, subscriberConsumed)
|
|
1371
|
+
warnStaleOverrides('workers', workerOverrides, workerConsumed)
|
|
1372
|
+
warnStaleOverrides('cli', cliOverrides, cliConsumed)
|
|
1373
|
+
warnStaleOverrides('acl.features', aclOverrides, aclConsumed)
|
|
1374
|
+
warnStaleOverrides('encryption.maps', encryptionOverrides, encryptionConsumed)
|
|
1375
|
+
|
|
1376
|
+
return result
|
|
1377
|
+
}
|
|
1378
|
+
|
|
1379
|
+
export function applyDiOverridesToContainer(container: {
|
|
1380
|
+
register: (registrations: Record<string, unknown>) => unknown
|
|
1381
|
+
unregister?: (name: string) => unknown
|
|
1382
|
+
registrations?: Record<string, unknown>
|
|
1383
|
+
}): void {
|
|
1384
|
+
const overrides = composeDiOverrides()
|
|
1385
|
+
for (const [key, value] of Object.entries(overrides)) {
|
|
1386
|
+
if (value === null) {
|
|
1387
|
+
if (typeof container.unregister === 'function') {
|
|
1388
|
+
container.unregister(key)
|
|
1389
|
+
} else if (container.registrations && key in container.registrations) {
|
|
1390
|
+
delete container.registrations[key]
|
|
1391
|
+
}
|
|
1392
|
+
continue
|
|
1393
|
+
}
|
|
1394
|
+
if (value && typeof value === 'object' && typeof (value as DiBindingOverrideDefinition).register === 'function') {
|
|
1395
|
+
;(value as DiBindingOverrideDefinition).register(container, key)
|
|
1396
|
+
continue
|
|
1397
|
+
}
|
|
1398
|
+
container.register({ [key]: value })
|
|
1399
|
+
}
|
|
1400
|
+
}
|
|
1401
|
+
|
|
1402
|
+
/**
|
|
1403
|
+
* Dispatcher applier for the `'routes'` domain. Buckets every entry's
|
|
1404
|
+
* `overrides.routes.api` and `overrides.routes.pages` into the
|
|
1405
|
+
* `modules.ts` tier.
|
|
1406
|
+
*/
|
|
1407
|
+
function routesOverridesApplier(
|
|
1408
|
+
entries: ReadonlyArray<ModuleOverrideEntry<RoutesOverridesShape>>,
|
|
1409
|
+
): void {
|
|
1410
|
+
for (const entry of entries) {
|
|
1411
|
+
const shape = entry.overrides
|
|
1412
|
+
if (!shape || typeof shape !== 'object') continue
|
|
1413
|
+
const api = shape.api
|
|
1414
|
+
if (api && typeof api === 'object') {
|
|
1415
|
+
for (const [rawKey, value] of Object.entries(api)) {
|
|
1416
|
+
const key = normalizeApiRouteOverrideKey(rawKey)
|
|
1417
|
+
if (!key) {
|
|
1418
|
+
console.warn(
|
|
1419
|
+
`[Module Overrides] Skipping malformed routes.api key "${rawKey}" — expected "METHOD /api/path".`,
|
|
1420
|
+
)
|
|
1421
|
+
continue
|
|
1422
|
+
}
|
|
1423
|
+
modulesConfigApiRouteOverrides[key] = value as ApiRouteOverride
|
|
1424
|
+
}
|
|
1425
|
+
}
|
|
1426
|
+
const pages = shape.pages
|
|
1427
|
+
if (pages && typeof pages === 'object' && Object.keys(pages).length > 0) {
|
|
1428
|
+
applyStoreOverrides(pageRouteOverrideStore, 'modules', pages as OverrideMap<PageRouteOverrideDefinition>, {
|
|
1429
|
+
label: 'routes.pages',
|
|
1430
|
+
normalizeKey: normalizePageRouteOverrideKey,
|
|
1431
|
+
})
|
|
1432
|
+
}
|
|
1433
|
+
}
|
|
1434
|
+
}
|
|
1435
|
+
|
|
1436
|
+
function eventsOverridesApplier(entries: ReadonlyArray<ModuleOverrideEntry<EventsOverridesShape>>): void {
|
|
1437
|
+
for (const entry of entries) {
|
|
1438
|
+
const subscribers = entry.overrides?.subscribers
|
|
1439
|
+
applyStoreOverrides(subscriberOverrideStore, 'modules', subscribers as OverrideMap<ModuleSubscriber>, { label: 'events.subscribers' })
|
|
1440
|
+
}
|
|
1441
|
+
}
|
|
1442
|
+
|
|
1443
|
+
function workersOverridesApplier(entries: ReadonlyArray<ModuleOverrideEntry<WorkerOverridesMap>>): void {
|
|
1444
|
+
for (const entry of entries) {
|
|
1445
|
+
applyStoreOverrides(workerOverrideStore, 'modules', entry.overrides as OverrideMap<ModuleWorker>, { label: 'workers' })
|
|
1446
|
+
}
|
|
1447
|
+
}
|
|
1448
|
+
|
|
1449
|
+
function widgetsOverridesApplier(entries: ReadonlyArray<ModuleOverrideEntry<WidgetsOverridesShape>>): void {
|
|
1450
|
+
for (const entry of entries) {
|
|
1451
|
+
applyStoreOverrides(injectionWidgetOverrideStore, 'modules', entry.overrides?.injection as OverrideMap<ModuleInjectionWidgetEntry>, { label: 'widgets.injection' })
|
|
1452
|
+
applyStoreOverrides(componentOverrideStore, 'modules', entry.overrides?.components as OverrideMap<ComponentOverride | ComponentOverride[]>, { label: 'widgets.components' })
|
|
1453
|
+
applyStoreOverrides(dashboardWidgetOverrideStore, 'modules', entry.overrides?.dashboard as OverrideMap<ModuleDashboardWidgetEntry>, { label: 'widgets.dashboard' })
|
|
1454
|
+
}
|
|
1455
|
+
}
|
|
1456
|
+
|
|
1457
|
+
function notificationsOverridesApplier(entries: ReadonlyArray<ModuleOverrideEntry<NotificationsOverridesShape>>): void {
|
|
1458
|
+
for (const entry of entries) {
|
|
1459
|
+
applyStoreOverrides(notificationTypeOverrideStore, 'modules', entry.overrides?.types as OverrideMap<NotificationTypeDefinition>, { label: 'notifications.types' })
|
|
1460
|
+
applyStoreOverrides(notificationHandlerOverrideStore, 'modules', entry.overrides?.handlers as OverrideMap<NotificationHandler>, { label: 'notifications.handlers' })
|
|
1461
|
+
}
|
|
1462
|
+
}
|
|
1463
|
+
|
|
1464
|
+
function interceptorsOverridesApplier(entries: ReadonlyArray<ModuleOverrideEntry<ApiInterceptorOverridesMap>>): void {
|
|
1465
|
+
for (const entry of entries) {
|
|
1466
|
+
applyStoreOverrides(apiInterceptorOverrideStore, 'modules', entry.overrides as OverrideMap<ApiInterceptor>, { label: 'interceptors' })
|
|
1467
|
+
}
|
|
1468
|
+
}
|
|
1469
|
+
|
|
1470
|
+
function commandInterceptorsOverridesApplier(entries: ReadonlyArray<ModuleOverrideEntry<CommandInterceptorOverridesMap>>): void {
|
|
1471
|
+
for (const entry of entries) {
|
|
1472
|
+
applyStoreOverrides(commandInterceptorOverrideStore, 'modules', entry.overrides as OverrideMap<CommandInterceptor>, { label: 'commandInterceptors' })
|
|
1473
|
+
}
|
|
1474
|
+
}
|
|
1475
|
+
|
|
1476
|
+
function enrichersOverridesApplier(entries: ReadonlyArray<ModuleOverrideEntry<ResponseEnricherOverridesMap>>): void {
|
|
1477
|
+
for (const entry of entries) {
|
|
1478
|
+
applyStoreOverrides(responseEnricherOverrideStore, 'modules', entry.overrides as OverrideMap<ResponseEnricher>, { label: 'enrichers' })
|
|
1479
|
+
}
|
|
1480
|
+
}
|
|
1481
|
+
|
|
1482
|
+
function guardsOverridesApplier(entries: ReadonlyArray<ModuleOverrideEntry<PageGuardOverridesMap>>): void {
|
|
1483
|
+
for (const entry of entries) {
|
|
1484
|
+
applyStoreOverrides(pageGuardOverrideStore, 'modules', entry.overrides as OverrideMap<PageRouteMiddleware>, { label: 'guards' })
|
|
1485
|
+
}
|
|
1486
|
+
}
|
|
1487
|
+
|
|
1488
|
+
function cliOverridesApplier(entries: ReadonlyArray<ModuleOverrideEntry<CliOverridesMap>>): void {
|
|
1489
|
+
for (const entry of entries) {
|
|
1490
|
+
applyStoreOverrides(cliOverrideStore, 'modules', entry.overrides as OverrideMap<ModuleCli>, { label: 'cli' })
|
|
1491
|
+
}
|
|
1492
|
+
}
|
|
1493
|
+
|
|
1494
|
+
function setupOverridesApplier(entries: ReadonlyArray<ModuleOverrideEntry<SetupOverridesShape>>): void {
|
|
1495
|
+
for (const entry of entries) {
|
|
1496
|
+
if (!entry.overrides || typeof entry.overrides !== 'object') continue
|
|
1497
|
+
setupOverridesByModule[entry.moduleId] = entry.overrides
|
|
1498
|
+
}
|
|
1499
|
+
}
|
|
1500
|
+
|
|
1501
|
+
function aclOverridesApplier(entries: ReadonlyArray<ModuleOverrideEntry<AclOverridesShape>>): void {
|
|
1502
|
+
for (const entry of entries) {
|
|
1503
|
+
applyStoreOverrides(aclFeatureOverrideStore, 'modules', entry.overrides?.features as OverrideMap<Exclude<AclFeatureOverride, null>>, { label: 'acl.features' })
|
|
1504
|
+
}
|
|
1505
|
+
}
|
|
1506
|
+
|
|
1507
|
+
function diOverridesApplier(entries: ReadonlyArray<ModuleOverrideEntry<DiOverridesMap>>): void {
|
|
1508
|
+
for (const entry of entries) {
|
|
1509
|
+
applyStoreOverrides(diOverrideStore, 'modules', entry.overrides as OverrideMap<Exclude<DiBindingOverride, null>>, { label: 'di' })
|
|
1510
|
+
}
|
|
1511
|
+
}
|
|
1512
|
+
|
|
1513
|
+
function encryptionOverridesApplier(entries: ReadonlyArray<ModuleOverrideEntry<EncryptionOverridesShape>>): void {
|
|
1514
|
+
for (const entry of entries) {
|
|
1515
|
+
applyStoreOverrides(encryptionMapOverrideStore, 'modules', entry.overrides?.maps as OverrideMap<ModuleEncryptionMap>, { label: 'encryption.maps' })
|
|
1516
|
+
}
|
|
1517
|
+
}
|
|
1518
|
+
|
|
1519
|
+
function registerBuiltInModuleOverrideAppliers(): void {
|
|
1520
|
+
registerModuleOverrideApplier<RoutesOverridesShape>('routes', routesOverridesApplier)
|
|
1521
|
+
registerModuleOverrideApplier<EventsOverridesShape>('events', eventsOverridesApplier)
|
|
1522
|
+
registerModuleOverrideApplier<WorkerOverridesMap>('workers', workersOverridesApplier)
|
|
1523
|
+
registerModuleOverrideApplier<WidgetsOverridesShape>('widgets', widgetsOverridesApplier)
|
|
1524
|
+
registerModuleOverrideApplier<NotificationsOverridesShape>('notifications', notificationsOverridesApplier)
|
|
1525
|
+
registerModuleOverrideApplier<ApiInterceptorOverridesMap>('interceptors', interceptorsOverridesApplier)
|
|
1526
|
+
registerModuleOverrideApplier<CommandInterceptorOverridesMap>('commandInterceptors', commandInterceptorsOverridesApplier)
|
|
1527
|
+
registerModuleOverrideApplier<ResponseEnricherOverridesMap>('enrichers', enrichersOverridesApplier)
|
|
1528
|
+
registerModuleOverrideApplier<PageGuardOverridesMap>('guards', guardsOverridesApplier)
|
|
1529
|
+
registerModuleOverrideApplier<CliOverridesMap>('cli', cliOverridesApplier)
|
|
1530
|
+
registerModuleOverrideApplier<SetupOverridesShape>('setup', setupOverridesApplier)
|
|
1531
|
+
registerModuleOverrideApplier<AclOverridesShape>('acl', aclOverridesApplier)
|
|
1532
|
+
registerModuleOverrideApplier<DiOverridesMap>('di', diOverridesApplier)
|
|
1533
|
+
registerModuleOverrideApplier<EncryptionOverridesShape>('encryption', encryptionOverridesApplier)
|
|
1534
|
+
}
|
|
1535
|
+
|
|
1536
|
+
registerBuiltInModuleOverrideAppliers()
|