@open-mercato/shared 0.6.2-develop.3467.1.2a1818709d → 0.6.3-canary.3483.1.98d4b4ecc7

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 (43) hide show
  1. package/AGENTS.md +24 -0
  2. package/dist/lib/bootstrap/factory.js +3 -1
  3. package/dist/lib/bootstrap/factory.js.map +2 -2
  4. package/dist/lib/commands/command-interceptor-store.js +3 -1
  5. package/dist/lib/commands/command-interceptor-store.js.map +2 -2
  6. package/dist/lib/crud/enricher-registry.js +3 -1
  7. package/dist/lib/crud/enricher-registry.js.map +2 -2
  8. package/dist/lib/crud/factory.js.map +1 -1
  9. package/dist/lib/crud/interceptor-registry.js +3 -1
  10. package/dist/lib/crud/interceptor-registry.js.map +2 -2
  11. package/dist/lib/di/container.js +16 -0
  12. package/dist/lib/di/container.js.map +2 -2
  13. package/dist/lib/middleware/page-executor.js +2 -1
  14. package/dist/lib/middleware/page-executor.js.map +2 -2
  15. package/dist/lib/modules/registry.js +2 -1
  16. package/dist/lib/modules/registry.js.map +2 -2
  17. package/dist/lib/notifications/handler-registry.js +3 -1
  18. package/dist/lib/notifications/handler-registry.js.map +2 -2
  19. package/dist/lib/version.js +1 -1
  20. package/dist/lib/version.js.map +1 -1
  21. package/dist/modules/overrides.js +857 -0
  22. package/dist/modules/overrides.js.map +2 -2
  23. package/dist/modules/registry.js +17 -4
  24. package/dist/modules/registry.js.map +2 -2
  25. package/dist/modules/widgets/injection-loader.js +10 -4
  26. package/dist/modules/widgets/injection-loader.js.map +2 -2
  27. package/jest.config.cjs +3 -1
  28. package/package.json +7 -7
  29. package/src/lib/bootstrap/factory.ts +3 -1
  30. package/src/lib/commands/command-interceptor-store.ts +3 -1
  31. package/src/lib/crud/enricher-registry.ts +3 -1
  32. package/src/lib/crud/factory.ts +2 -2
  33. package/src/lib/crud/interceptor-registry.ts +3 -1
  34. package/src/lib/di/container.ts +24 -4
  35. package/src/lib/middleware/page-executor.ts +2 -1
  36. package/src/lib/modules/registry.ts +2 -1
  37. package/src/lib/notifications/handler-registry.ts +3 -1
  38. package/src/modules/__tests__/contract-overrides.test.ts +332 -0
  39. package/src/modules/__tests__/overrides.test.ts +23 -22
  40. package/src/modules/__tests__/route-overrides.test.ts +441 -0
  41. package/src/modules/overrides.ts +1297 -27
  42. package/src/modules/registry.ts +27 -4
  43. package/src/modules/widgets/injection-loader.ts +10 -4
@@ -0,0 +1,332 @@
1
+ import {
2
+ applyApiInterceptorOverridesToEntries,
3
+ applyCommandInterceptorOverridesToEntries,
4
+ applyComponentOverridesToEntries,
5
+ applyDashboardWidgetOverridesToEntries,
6
+ applyDiOverridesToContainer,
7
+ applyInjectionWidgetOverridesToEntries,
8
+ applyInjectionWidgetOverridesToTables,
9
+ applyModuleOverridesFromEnabledModules,
10
+ applyModuleOverridesToModules,
11
+ applyNotificationHandlerOverridesToEntries,
12
+ applyNotificationTypeOverridesToEntries,
13
+ applyPageGuardOverridesToEntries,
14
+ applyResponseEnricherOverridesToEntries,
15
+ resetModuleContractOverridesForTests,
16
+ resetModuleOverrideAppliersForTests,
17
+ type ModuleEntryWithOverrides,
18
+ } from '../overrides'
19
+ import type { Module } from '../registry'
20
+
21
+ function makeEnabledModule(overrides: ModuleEntryWithOverrides['overrides']): ModuleEntryWithOverrides {
22
+ return { id: 'example', from: '@app', overrides }
23
+ }
24
+
25
+ beforeEach(() => {
26
+ resetModuleOverrideAppliersForTests()
27
+ resetModuleContractOverridesForTests()
28
+ })
29
+
30
+ describe('module contract override helpers', () => {
31
+ it('applies module-list overrides for subscribers, workers, CLI, setup, ACL and encryption maps', () => {
32
+ const replacementSubscriber = {
33
+ id: 'example.todo.audit',
34
+ event: 'example.todo.updated',
35
+ handler: jest.fn(),
36
+ }
37
+ const replacementWorker = {
38
+ id: 'example:sync',
39
+ queue: 'example-sync-v2',
40
+ concurrency: 4,
41
+ handler: jest.fn(),
42
+ }
43
+ const replacementCli = {
44
+ command: 'example seed',
45
+ run: jest.fn(),
46
+ }
47
+
48
+ applyModuleOverridesFromEnabledModules([
49
+ makeEnabledModule({
50
+ events: { subscribers: { 'example.todo.audit': replacementSubscriber } },
51
+ workers: { 'example:sync': replacementWorker },
52
+ cli: { 'example seed': replacementCli },
53
+ setup: {
54
+ defaultRoleFeatures: { admin: ['example.view'] },
55
+ seedDefaults: false,
56
+ seedExamples: false,
57
+ onTenantCreated: false,
58
+ },
59
+ acl: {
60
+ features: {
61
+ 'example.manage': null,
62
+ 'example.view': { id: 'example.view', title: 'View example', module: 'example' },
63
+ },
64
+ },
65
+ encryption: {
66
+ maps: {
67
+ 'example:item': null,
68
+ 'example:secret': { entityId: 'example:secret', fields: [{ field: 'token' }] },
69
+ },
70
+ },
71
+ }),
72
+ ])
73
+
74
+ const module: Module = {
75
+ id: 'example',
76
+ subscribers: [
77
+ { id: 'example.todo.audit', event: 'example.todo.created', handler: jest.fn() },
78
+ ],
79
+ workers: [
80
+ { id: 'example:sync', queue: 'example-sync', concurrency: 1, handler: jest.fn() },
81
+ ],
82
+ cli: [
83
+ { command: 'example seed', run: jest.fn() },
84
+ ],
85
+ setup: {
86
+ onTenantCreated: jest.fn(),
87
+ seedDefaults: jest.fn(),
88
+ seedExamples: jest.fn(),
89
+ defaultRoleFeatures: { admin: ['example.manage'] },
90
+ },
91
+ features: [
92
+ { id: 'example.manage', title: 'Manage example', module: 'example' },
93
+ { id: 'example.view', title: 'Old view example', module: 'example' },
94
+ ],
95
+ defaultEncryptionMaps: [
96
+ { entityId: 'example:item', fields: [{ field: 'name' }] },
97
+ { entityId: 'example:secret', fields: [{ field: 'oldToken' }] },
98
+ ],
99
+ }
100
+
101
+ const [overridden] = applyModuleOverridesToModules([module])
102
+
103
+ expect(overridden.subscribers).toEqual([replacementSubscriber])
104
+ expect(overridden.workers).toEqual([replacementWorker])
105
+ expect(overridden.cli).toEqual([replacementCli])
106
+ expect(overridden.features).toEqual([
107
+ { id: 'example.view', title: 'View example', module: 'example' },
108
+ ])
109
+ expect(overridden.defaultEncryptionMaps).toEqual([
110
+ { entityId: 'example:secret', fields: [{ field: 'token' }] },
111
+ ])
112
+ expect(overridden.setup?.defaultRoleFeatures).toEqual({ admin: ['example.view'] })
113
+ expect(overridden.setup?.seedDefaults).toBeUndefined()
114
+ expect(overridden.setup?.seedExamples).toBeUndefined()
115
+ expect(overridden.setup?.onTenantCreated).toBeUndefined()
116
+ })
117
+
118
+ it('applies registry-entry overrides for widgets, notifications, interceptors, enrichers and guards', () => {
119
+ const injectionReplacement = {
120
+ moduleId: 'override',
121
+ key: 'example.toolbar',
122
+ source: 'app' as const,
123
+ loader: jest.fn(async () => ({ metadata: { id: 'example.toolbar' } })),
124
+ }
125
+ const dashboardReplacement = {
126
+ moduleId: 'override',
127
+ key: 'example.kpi',
128
+ source: 'app' as const,
129
+ loader: jest.fn(async () => ({ metadata: { id: 'example.kpi' } })),
130
+ }
131
+ const componentReplacement = {
132
+ target: { componentId: 'page:/backend/example' },
133
+ priority: 1,
134
+ propsTransform: (props: unknown) => props,
135
+ }
136
+ const notificationTypeReplacement = {
137
+ type: 'example.notice',
138
+ module: 'example',
139
+ titleKey: 'example.notifications.notice.title',
140
+ icon: 'bell',
141
+ severity: 'success' as const,
142
+ actions: [],
143
+ }
144
+ const notificationHandlerReplacement = {
145
+ id: 'example.notice.toast',
146
+ notificationType: 'example.notice',
147
+ handle: jest.fn(),
148
+ }
149
+ const apiInterceptorReplacement = {
150
+ id: 'example.items.interceptor',
151
+ targetRoute: '/api/example/items',
152
+ methods: ['GET' as const],
153
+ before: jest.fn(async () => ({ ok: true })),
154
+ }
155
+ const commandInterceptorReplacement = {
156
+ id: 'example.command.interceptor',
157
+ targetCommand: 'example.*',
158
+ beforeExecute: jest.fn(async () => ({ ok: true })),
159
+ }
160
+ const enricherReplacement = {
161
+ id: 'example.items.enricher',
162
+ targetEntity: 'example.item',
163
+ enrichOne: jest.fn(async (record: Record<string, unknown>) => ({ ...record, _example: true })),
164
+ }
165
+ const guardReplacement = {
166
+ id: 'example.backend.guard',
167
+ target: '/backend/example',
168
+ run: jest.fn(() => ({ action: 'continue' as const })),
169
+ }
170
+
171
+ applyModuleOverridesFromEnabledModules([
172
+ makeEnabledModule({
173
+ widgets: {
174
+ injection: {
175
+ 'example.toolbar': injectionReplacement,
176
+ 'example.sidebar': null,
177
+ },
178
+ dashboard: {
179
+ 'example.kpi': dashboardReplacement,
180
+ },
181
+ components: {
182
+ 'page:/backend/example': componentReplacement,
183
+ },
184
+ },
185
+ notifications: {
186
+ types: { 'example.notice': notificationTypeReplacement },
187
+ handlers: {
188
+ 'example.notice.toast': notificationHandlerReplacement,
189
+ 'example.notice.popup': null,
190
+ },
191
+ },
192
+ interceptors: { 'example.items.interceptor': apiInterceptorReplacement },
193
+ commandInterceptors: {
194
+ 'example.command.interceptor': commandInterceptorReplacement,
195
+ },
196
+ enrichers: { 'example.items.enricher': enricherReplacement },
197
+ guards: { 'example.backend.guard': guardReplacement },
198
+ }),
199
+ ])
200
+
201
+ expect(applyInjectionWidgetOverridesToEntries([
202
+ { moduleId: 'base', key: 'example.toolbar', source: 'package', loader: jest.fn() },
203
+ { moduleId: 'base', key: 'example.sidebar', source: 'package', loader: jest.fn() },
204
+ ])).toEqual([injectionReplacement])
205
+
206
+ expect(applyDashboardWidgetOverridesToEntries([
207
+ { moduleId: 'base', key: 'example.kpi', source: 'package', loader: jest.fn() },
208
+ ])).toEqual([dashboardReplacement])
209
+
210
+ const componentEntries = applyComponentOverridesToEntries([
211
+ {
212
+ moduleId: 'base',
213
+ componentOverrides: [
214
+ {
215
+ target: { componentId: 'page:/backend/example' },
216
+ priority: 20,
217
+ propsTransform: (props: unknown) => props,
218
+ },
219
+ ],
220
+ },
221
+ ])
222
+ expect(componentEntries.flatMap((entry) => entry.componentOverrides)).toEqual([componentReplacement])
223
+
224
+ expect(applyNotificationTypeOverridesToEntries([
225
+ {
226
+ moduleId: 'base',
227
+ types: [
228
+ { type: 'example.notice', module: 'example', titleKey: 'old', icon: 'bell', severity: 'info', actions: [] },
229
+ ],
230
+ },
231
+ ])).toEqual([{ moduleId: 'base', types: [notificationTypeReplacement] }])
232
+
233
+ expect(applyNotificationHandlerOverridesToEntries([
234
+ {
235
+ moduleId: 'base',
236
+ handlers: [
237
+ { id: 'example.notice.toast', notificationType: 'example.notice', handle: jest.fn() },
238
+ { id: 'example.notice.popup', notificationType: 'example.notice', handle: jest.fn() },
239
+ ],
240
+ },
241
+ ])).toEqual([{ moduleId: 'base', handlers: [notificationHandlerReplacement] }])
242
+
243
+ expect(applyApiInterceptorOverridesToEntries([
244
+ {
245
+ moduleId: 'base',
246
+ interceptors: [
247
+ { id: 'example.items.interceptor', targetRoute: '/api/example/items', methods: ['POST'] },
248
+ ],
249
+ },
250
+ ])).toEqual([{ moduleId: 'base', interceptors: [apiInterceptorReplacement] }])
251
+
252
+ expect(applyCommandInterceptorOverridesToEntries([
253
+ {
254
+ moduleId: 'base',
255
+ interceptors: [
256
+ { id: 'example.command.interceptor', targetCommand: 'example.old' },
257
+ ],
258
+ },
259
+ ])).toEqual([{ moduleId: 'base', interceptors: [commandInterceptorReplacement] }])
260
+
261
+ expect(applyResponseEnricherOverridesToEntries([
262
+ {
263
+ moduleId: 'base',
264
+ enrichers: [
265
+ { id: 'example.items.enricher', targetEntity: 'example.item', enrichOne: jest.fn() },
266
+ ],
267
+ },
268
+ ])).toEqual([{ moduleId: 'base', enrichers: [enricherReplacement] }])
269
+
270
+ expect(applyPageGuardOverridesToEntries([
271
+ {
272
+ moduleId: 'base',
273
+ middleware: [
274
+ { id: 'example.backend.guard', target: '/backend/example', run: jest.fn() },
275
+ ],
276
+ },
277
+ ])).toEqual([{ moduleId: 'base', middleware: [guardReplacement] }])
278
+ })
279
+
280
+ it('removes disabled injection widgets from injection tables', () => {
281
+ applyModuleOverridesFromEnabledModules([
282
+ makeEnabledModule({
283
+ widgets: {
284
+ injection: { 'example.sidebar': null },
285
+ },
286
+ }),
287
+ ])
288
+
289
+ expect(applyInjectionWidgetOverridesToTables([
290
+ {
291
+ moduleId: 'example',
292
+ table: {
293
+ 'backend.sidebar': [
294
+ 'example.sidebar',
295
+ { widgetId: 'example.toolbar', priority: 10 },
296
+ ],
297
+ 'backend.footer': 'example.sidebar',
298
+ },
299
+ },
300
+ ])).toEqual([
301
+ {
302
+ moduleId: 'example',
303
+ table: {
304
+ 'backend.sidebar': [{ widgetId: 'example.toolbar', priority: 10 }],
305
+ },
306
+ },
307
+ ])
308
+ })
309
+
310
+ it('applies DI binding overrides as the last container mutation step', () => {
311
+ const register = jest.fn()
312
+ const unregister = jest.fn()
313
+
314
+ applyModuleOverridesFromEnabledModules([
315
+ makeEnabledModule({
316
+ di: {
317
+ disabledService: null,
318
+ valueService: { enabled: true },
319
+ customService: {
320
+ register: (container, key) => container.register({ [key]: 'registered by override' }),
321
+ },
322
+ },
323
+ }),
324
+ ])
325
+
326
+ applyDiOverridesToContainer({ register, unregister })
327
+
328
+ expect(unregister).toHaveBeenCalledWith('disabledService')
329
+ expect(register).toHaveBeenCalledWith({ valueService: { enabled: true } })
330
+ expect(register).toHaveBeenCalledWith({ customService: 'registered by override' })
331
+ })
332
+ })
@@ -2,15 +2,18 @@
2
2
  * Coverage for the unified `modules.ts` override dispatcher. The
3
3
  * dispatcher walks `enabledModules`, buckets every entry's
4
4
  * `overrides.<domain>` shape by domain, and forwards each domain's
5
- * bucket to the registered per-domain applier. Unwired domains emit a
6
- * one-shot structured warning so an early adopter notices instead of
7
- * silently no-opping.
5
+ * bucket to the registered per-domain applier. Built-in domains are wired
6
+ * by shared's default appliers; custom/future domains still warn if no
7
+ * applier is registered.
8
8
  *
9
9
  * Spec: `.ai/specs/2026-05-04-modules-ts-unified-overrides.md`.
10
10
  */
11
11
  import {
12
12
  applyModuleOverridesFromEnabledModules,
13
+ composeInjectionWidgetOverrides,
14
+ composeSubscriberOverrides,
13
15
  registerModuleOverrideApplier,
16
+ resetModuleContractOverridesForTests,
14
17
  resetModuleOverrideAppliersForTests,
15
18
  type ModuleEntryWithOverrides,
16
19
  type ModuleOverrideEntry,
@@ -18,6 +21,7 @@ import {
18
21
 
19
22
  beforeEach(() => {
20
23
  resetModuleOverrideAppliersForTests()
24
+ resetModuleContractOverridesForTests()
21
25
  })
22
26
 
23
27
  describe('applyModuleOverridesFromEnabledModules', () => {
@@ -72,37 +76,34 @@ describe('applyModuleOverridesFromEnabledModules', () => {
72
76
  expect(applier).not.toHaveBeenCalled()
73
77
  })
74
78
 
75
- it('emits a one-shot structured warning per unwired domain', () => {
79
+ it('routes built-in domains to default appliers without unwired warnings', () => {
76
80
  const warnSpy = jest.spyOn(console, 'warn').mockImplementation(() => {})
77
81
 
78
82
  applyModuleOverridesFromEnabledModules([
79
83
  {
80
84
  id: 'a',
81
- overrides: { routes: { api: { 'GET /api/x': null } } },
85
+ overrides: {
86
+ widgets: { injection: { 'spot.foo': null } },
87
+ events: { subscribers: { 'example.todo.created.notify': null } },
88
+ },
82
89
  },
83
90
  {
84
91
  id: 'b',
85
- overrides: { routes: { api: { 'POST /api/y': null } } },
92
+ overrides: { widgets: { injection: { 'spot.bar': null } } },
86
93
  },
87
94
  ])
88
95
 
89
- // Same domain hit twice — only one warning per process.
90
- const routesCalls = warnSpy.mock.calls.filter((args) =>
91
- typeof args[0] === 'string' && args[0].includes('Domain "routes"'),
92
- )
93
- expect(routesCalls).toHaveLength(1)
94
- expect(routesCalls[0][0]).toContain('not yet wired')
95
- expect(routesCalls[0][0]).toContain('module(s) [a, b]')
96
- expect(routesCalls[0][0]).toContain('issues/1787')
97
-
98
- // Different unwired domain — separate one-shot warning.
99
- applyModuleOverridesFromEnabledModules([
100
- { id: 'c', overrides: { events: { subscribers: { 'x': null } } } },
101
- ])
102
- const eventsCalls = warnSpy.mock.calls.filter((args) =>
103
- typeof args[0] === 'string' && args[0].includes('Domain "events"'),
96
+ const unwiredCalls = warnSpy.mock.calls.filter((args) =>
97
+ typeof args[0] === 'string' && args[0].includes('not yet wired'),
104
98
  )
105
- expect(eventsCalls).toHaveLength(1)
99
+ expect(unwiredCalls).toHaveLength(0)
100
+ expect(composeInjectionWidgetOverrides()).toMatchObject({
101
+ 'spot.foo': null,
102
+ 'spot.bar': null,
103
+ })
104
+ expect(composeSubscriberOverrides()).toMatchObject({
105
+ 'example.todo.created.notify': null,
106
+ })
106
107
 
107
108
  warnSpy.mockRestore()
108
109
  })