@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.
Files changed (67) hide show
  1. package/.turbo/turbo-build.log +1 -1
  2. package/AGENTS.md +24 -0
  3. package/dist/lib/bootstrap/factory.js +3 -1
  4. package/dist/lib/bootstrap/factory.js.map +2 -2
  5. package/dist/lib/commands/command-interceptor-store.js +3 -1
  6. package/dist/lib/commands/command-interceptor-store.js.map +2 -2
  7. package/dist/lib/crud/enricher-registry.js +3 -1
  8. package/dist/lib/crud/enricher-registry.js.map +2 -2
  9. package/dist/lib/crud/factory.js.map +1 -1
  10. package/dist/lib/crud/interceptor-registry.js +3 -1
  11. package/dist/lib/crud/interceptor-registry.js.map +2 -2
  12. package/dist/lib/crud/sync-event-runner.js +2 -8
  13. package/dist/lib/crud/sync-event-runner.js.map +2 -2
  14. package/dist/lib/db/mikro.js +27 -3
  15. package/dist/lib/db/mikro.js.map +2 -2
  16. package/dist/lib/di/container.js +16 -0
  17. package/dist/lib/di/container.js.map +2 -2
  18. package/dist/lib/events/patterns.js +2 -10
  19. package/dist/lib/events/patterns.js.map +2 -2
  20. package/dist/lib/middleware/page-executor.js +2 -1
  21. package/dist/lib/middleware/page-executor.js.map +2 -2
  22. package/dist/lib/modules/registry.js +2 -1
  23. package/dist/lib/modules/registry.js.map +2 -2
  24. package/dist/lib/notifications/handler-registry.js +3 -1
  25. package/dist/lib/notifications/handler-registry.js.map +2 -2
  26. package/dist/lib/patterns/wildcard.js +42 -0
  27. package/dist/lib/patterns/wildcard.js.map +7 -0
  28. package/dist/lib/regex/linear.js +31 -0
  29. package/dist/lib/regex/linear.js.map +7 -0
  30. package/dist/lib/version.js +1 -1
  31. package/dist/lib/version.js.map +1 -1
  32. package/dist/modules/entities/validation.js +15 -7
  33. package/dist/modules/entities/validation.js.map +2 -2
  34. package/dist/modules/overrides.js +857 -0
  35. package/dist/modules/overrides.js.map +2 -2
  36. package/dist/modules/registry.js +17 -4
  37. package/dist/modules/registry.js.map +2 -2
  38. package/dist/modules/widgets/injection-loader.js +12 -6
  39. package/dist/modules/widgets/injection-loader.js.map +2 -2
  40. package/jest.config.cjs +3 -1
  41. package/package.json +8 -8
  42. package/src/lib/bootstrap/factory.ts +3 -1
  43. package/src/lib/commands/command-interceptor-store.ts +3 -1
  44. package/src/lib/crud/__tests__/sync-event-runner.test.ts +5 -0
  45. package/src/lib/crud/enricher-registry.ts +3 -1
  46. package/src/lib/crud/factory.ts +2 -2
  47. package/src/lib/crud/interceptor-registry.ts +3 -1
  48. package/src/lib/crud/sync-event-runner.ts +2 -11
  49. package/src/lib/db/mikro.ts +27 -2
  50. package/src/lib/di/container.ts +24 -4
  51. package/src/lib/events/__tests__/patterns.test.ts +5 -0
  52. package/src/lib/events/patterns.ts +3 -16
  53. package/src/lib/middleware/page-executor.ts +2 -1
  54. package/src/lib/modules/registry.ts +2 -1
  55. package/src/lib/notifications/handler-registry.ts +3 -1
  56. package/src/lib/patterns/__tests__/wildcard.test.ts +27 -0
  57. package/src/lib/patterns/wildcard.ts +59 -0
  58. package/src/lib/regex/linear.ts +48 -0
  59. package/src/modules/__tests__/contract-overrides.test.ts +332 -0
  60. package/src/modules/__tests__/overrides.test.ts +23 -22
  61. package/src/modules/__tests__/route-overrides.test.ts +441 -0
  62. package/src/modules/entities/__tests__/validation.test.ts +97 -2
  63. package/src/modules/entities/validation.ts +14 -9
  64. package/src/modules/overrides.ts +1297 -27
  65. package/src/modules/registry.ts +27 -4
  66. package/src/modules/widgets/__tests__/injection-loader.required-modules.test.ts +21 -0
  67. package/src/modules/widgets/injection-loader.ts +12 -6
@@ -0,0 +1,48 @@
1
+ import { RE2JS } from 're2js'
2
+
3
+ const DEFAULT_MAX_PATTERN_LENGTH = 500
4
+ const DEFAULT_MAX_INPUT_LENGTH = 10_000
5
+ const MAX_REGEX_CACHE_SIZE = 256
6
+
7
+ const compiledRegexCache = new Map<string, RE2JS>()
8
+
9
+ export type LinearRegexTestOptions = {
10
+ maxPatternLength?: number
11
+ maxInputLength?: number
12
+ }
13
+
14
+ export type LinearRegexTestResult =
15
+ | { ok: true; matched: boolean }
16
+ | { ok: false; reason: 'pattern_too_long' | 'input_too_long' | 'invalid_pattern' }
17
+
18
+ function getCachedRegex(pattern: string): RE2JS {
19
+ const cached = compiledRegexCache.get(pattern)
20
+ if (cached) return cached
21
+
22
+ const compiled = RE2JS.compile(pattern)
23
+ if (compiledRegexCache.size >= MAX_REGEX_CACHE_SIZE) {
24
+ const oldestKey = compiledRegexCache.keys().next().value
25
+ if (oldestKey) compiledRegexCache.delete(oldestKey)
26
+ }
27
+ compiledRegexCache.set(pattern, compiled)
28
+ return compiled
29
+ }
30
+
31
+ export function testLinearRegex(
32
+ pattern: string,
33
+ input: string,
34
+ options: LinearRegexTestOptions = {},
35
+ ): LinearRegexTestResult {
36
+ const maxPatternLength = options.maxPatternLength ?? DEFAULT_MAX_PATTERN_LENGTH
37
+ const maxInputLength = options.maxInputLength ?? DEFAULT_MAX_INPUT_LENGTH
38
+
39
+ if (pattern.length > maxPatternLength) return { ok: false, reason: 'pattern_too_long' }
40
+ if (input.length > maxInputLength) return { ok: false, reason: 'input_too_long' }
41
+
42
+ try {
43
+ return { ok: true, matched: getCachedRegex(pattern).test(input) }
44
+ } catch {
45
+ return { ok: false, reason: 'invalid_pattern' }
46
+ }
47
+ }
48
+
@@ -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
  })