@open-mercato/shared 0.10.1-develop.7576.1.21d5b50435 → 0.10.1-develop.7577.1.477f2486e5

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 (125) hide show
  1. package/.turbo/turbo-build.log +1 -1
  2. package/AGENTS.md +4 -4
  3. package/dist/lib/auth/server.js +1 -1
  4. package/dist/lib/auth/server.js.map +2 -2
  5. package/dist/lib/bootstrap/dynamicLoader.js +46 -6
  6. package/dist/lib/bootstrap/dynamicLoader.js.map +2 -2
  7. package/dist/lib/bootstrap/factory.js +4 -3
  8. package/dist/lib/bootstrap/factory.js.map +2 -2
  9. package/dist/lib/bootstrap/next-subpath-resolve-hook-worker.js +6 -0
  10. package/dist/lib/bootstrap/next-subpath-resolve-hook-worker.js.map +7 -0
  11. package/dist/lib/bootstrap/next-subpath-resolve-hook.js +62 -0
  12. package/dist/lib/bootstrap/next-subpath-resolve-hook.js.map +7 -0
  13. package/dist/lib/catalog-visibility/canonicalizeEffectiveScope.js +33 -0
  14. package/dist/lib/catalog-visibility/canonicalizeEffectiveScope.js.map +7 -0
  15. package/dist/lib/catalog-visibility/hashEffectiveScope.js +9 -0
  16. package/dist/lib/catalog-visibility/hashEffectiveScope.js.map +7 -0
  17. package/dist/lib/catalog-visibility/index.js +4 -0
  18. package/dist/lib/catalog-visibility/index.js.map +2 -2
  19. package/dist/lib/commands/command-bus.js +45 -13
  20. package/dist/lib/commands/command-bus.js.map +2 -2
  21. package/dist/lib/commands/types.js.map +2 -2
  22. package/dist/lib/crud/exporters.js +1 -1
  23. package/dist/lib/crud/exporters.js.map +2 -2
  24. package/dist/lib/crud/ids.js +13 -1
  25. package/dist/lib/crud/ids.js.map +2 -2
  26. package/dist/lib/db/advisoryLock.js +172 -0
  27. package/dist/lib/db/advisoryLock.js.map +7 -0
  28. package/dist/lib/db/pg-errors.js +14 -0
  29. package/dist/lib/db/pg-errors.js.map +2 -2
  30. package/dist/lib/localization/index.js +9 -1
  31. package/dist/lib/localization/index.js.map +2 -2
  32. package/dist/lib/localization/translatable-fields.js +26 -0
  33. package/dist/lib/localization/translatable-fields.js.map +2 -2
  34. package/dist/lib/number.js +6 -0
  35. package/dist/lib/number.js.map +2 -2
  36. package/dist/lib/openapi/generator.js +84 -67
  37. package/dist/lib/openapi/generator.js.map +2 -2
  38. package/dist/lib/query/engine.js +45 -10
  39. package/dist/lib/query/engine.js.map +2 -2
  40. package/dist/lib/query/join-utils.js +1 -1
  41. package/dist/lib/query/join-utils.js.map +2 -2
  42. package/dist/lib/query/overlap.js +58 -0
  43. package/dist/lib/query/overlap.js.map +7 -0
  44. package/dist/lib/query/types.js.map +2 -2
  45. package/dist/lib/url.js +24 -0
  46. package/dist/lib/url.js.map +2 -2
  47. package/dist/lib/version.js +1 -1
  48. package/dist/lib/version.js.map +1 -1
  49. package/dist/lib/webhooks/body.js +7 -3
  50. package/dist/lib/webhooks/body.js.map +2 -2
  51. package/dist/lib/webhooks/index.js +2 -0
  52. package/dist/lib/webhooks/index.js.map +2 -2
  53. package/dist/modules/integrations/types.js.map +2 -2
  54. package/dist/modules/notifications/safeHref.js +20 -0
  55. package/dist/modules/notifications/safeHref.js.map +7 -0
  56. package/dist/modules/notifications/types.js +4 -0
  57. package/dist/modules/notifications/types.js.map +3 -3
  58. package/dist/modules/payment_gateways/types.js +14 -1
  59. package/dist/modules/payment_gateways/types.js.map +2 -2
  60. package/dist/modules/registry.js.map +2 -2
  61. package/dist/modules/search.js.map +2 -2
  62. package/dist/modules/widgets/injection-loader.js +14 -3
  63. package/dist/modules/widgets/injection-loader.js.map +2 -2
  64. package/package.json +9 -2
  65. package/src/lib/__tests__/number.test.ts +24 -1
  66. package/src/lib/__tests__/url.cross-site.test.ts +140 -0
  67. package/src/lib/auth/__tests__/server.test.ts +14 -0
  68. package/src/lib/auth/server.ts +1 -1
  69. package/src/lib/bootstrap/__tests__/dynamicLoader.appDi.test.ts +2 -2
  70. package/src/lib/bootstrap/__tests__/dynamicLoader.appModuleOverrides.test.ts +2 -2
  71. package/src/lib/bootstrap/__tests__/dynamicLoader.cacheRecovery.test.ts +2 -2
  72. package/src/lib/bootstrap/__tests__/dynamicLoader.commandInterceptors.test.ts +2 -2
  73. package/src/lib/bootstrap/__tests__/dynamicLoader.externalSubpath.test.ts +141 -0
  74. package/src/lib/bootstrap/__tests__/dynamicLoader.tsconfig.test.ts +2 -2
  75. package/src/lib/bootstrap/__tests__/factory.test.ts +18 -6
  76. package/src/lib/bootstrap/__tests__/next-subpath-resolve-hook.test.ts +174 -0
  77. package/src/lib/bootstrap/dynamicLoader.ts +63 -5
  78. package/src/lib/bootstrap/factory.ts +8 -3
  79. package/src/lib/bootstrap/next-subpath-resolve-hook-worker.ts +3 -0
  80. package/src/lib/bootstrap/next-subpath-resolve-hook.ts +130 -0
  81. package/src/lib/bootstrap/types.ts +5 -1
  82. package/src/lib/catalog-visibility/__tests__/hashEffectiveScope.test.ts +121 -0
  83. package/src/lib/catalog-visibility/canonicalizeEffectiveScope.ts +41 -0
  84. package/src/lib/catalog-visibility/hashEffectiveScope.ts +8 -0
  85. package/src/lib/catalog-visibility/index.ts +2 -0
  86. package/src/lib/commands/__tests__/command-bus.test.ts +100 -1
  87. package/src/lib/commands/command-bus.ts +42 -10
  88. package/src/lib/commands/command-interceptor.ts +4 -0
  89. package/src/lib/commands/types.ts +14 -0
  90. package/src/lib/crud/__tests__/exporters.test.ts +18 -0
  91. package/src/lib/crud/__tests__/ids.test.ts +34 -1
  92. package/src/lib/crud/exporters.ts +1 -1
  93. package/src/lib/crud/ids.ts +24 -0
  94. package/src/lib/db/__tests__/advisoryLock.integration.test.ts +343 -0
  95. package/src/lib/db/__tests__/advisoryLock.test.ts +564 -0
  96. package/src/lib/db/__tests__/pg-errors.test.ts +68 -1
  97. package/src/lib/db/advisoryLock.ts +271 -0
  98. package/src/lib/db/pg-errors.ts +35 -0
  99. package/src/lib/encryption/__tests__/tenantDataEncryptionService.test.ts +23 -0
  100. package/src/lib/localization/__tests__/translatable-fields.test.ts +17 -0
  101. package/src/lib/localization/index.ts +8 -1
  102. package/src/lib/localization/translatable-fields.ts +46 -0
  103. package/src/lib/number.ts +13 -0
  104. package/src/lib/openapi/__tests__/optional-path-params.test.ts +102 -0
  105. package/src/lib/openapi/generator.ts +106 -77
  106. package/src/lib/query/__tests__/empty-ids-filter.pg.test.ts +89 -0
  107. package/src/lib/query/__tests__/engine.empty-in-filter.test.ts +79 -0
  108. package/src/lib/query/__tests__/overlap.test.ts +219 -0
  109. package/src/lib/query/engine.ts +58 -8
  110. package/src/lib/query/join-utils.ts +1 -1
  111. package/src/lib/query/overlap.ts +106 -0
  112. package/src/lib/query/types.ts +13 -1
  113. package/src/lib/url.ts +30 -0
  114. package/src/lib/webhooks/__tests__/body.test.ts +48 -0
  115. package/src/lib/webhooks/body.ts +11 -4
  116. package/src/lib/webhooks/index.ts +1 -0
  117. package/src/modules/integrations/types.ts +6 -0
  118. package/src/modules/notifications/safeHref.ts +19 -0
  119. package/src/modules/notifications/types.ts +7 -0
  120. package/src/modules/payment_gateways/__tests__/types.test.ts +44 -0
  121. package/src/modules/payment_gateways/types.ts +52 -6
  122. package/src/modules/registry.ts +5 -0
  123. package/src/modules/search.ts +39 -0
  124. package/src/modules/widgets/__tests__/injection-loader.partitioned-registration.test.ts +78 -0
  125. package/src/modules/widgets/injection-loader.ts +33 -3
@@ -94,6 +94,39 @@ export type SearchOptions = {
94
94
  offset?: number
95
95
  /** How to combine results: 'or' merges all, 'and' requires match in all strategies */
96
96
  combineMode?: 'or' | 'and'
97
+ /**
98
+ * Restricts results to records whose query-index row (`entity_indexes`) matches this predicate,
99
+ * evaluated inside the strategy's ranking query so a restrictive filter never starves the result
100
+ * set (additive, 2026-10-06). Only strategies with `supportsIndexDocFilter: true` receive a
101
+ * filtered search; any other strategy returns no results for it rather than unfiltered ones.
102
+ */
103
+ indexDocFilter?: SearchIndexDocFilter
104
+ }
105
+
106
+ /**
107
+ * One condition over a record's query-index document (`entity_indexes.doc`), with the query
108
+ * engine's index-document semantics:
109
+ * - `exists`: the key is present and not JSON `null`;
110
+ * - `eq`: the key's text value equals `value` (booleans and numbers compare by their JSON text);
111
+ * - `overlap`: the key holds an array sharing at least one entry with `values`; empty `values`
112
+ * matches nothing;
113
+ * - `noverlap`: the key holds an array sharing no entry with `values`; a missing key never matches;
114
+ * - `recordIdNotIn`: the record id is none of `values`.
115
+ */
116
+ export type SearchIndexDocCondition =
117
+ | { op: 'exists'; key: string }
118
+ | { op: 'eq'; key: string; value: string | number | boolean }
119
+ | { op: 'overlap'; key: string; values: string[] }
120
+ | { op: 'noverlap'; key: string; values: string[] }
121
+ | { op: 'recordIdNotIn'; values: string[] }
122
+
123
+ /**
124
+ * A predicate over a record's `entity_indexes` row in disjunctive normal form: the record matches
125
+ * when its index row is not deleted and every condition of at least one `anyOf` branch holds.
126
+ * An empty `anyOf` matches nothing; an empty branch matches every indexed record.
127
+ */
128
+ export type SearchIndexDocFilter = {
129
+ anyOf: SearchIndexDocCondition[][]
97
130
  }
98
131
 
99
132
  // =============================================================================
@@ -144,6 +177,12 @@ export interface SearchStrategy {
144
177
  /** Priority for result merging (higher = more prominent in results) */
145
178
  readonly priority: number
146
179
 
180
+ /**
181
+ * Whether `search()` applies `SearchOptions.indexDocFilter` inside its ranking query. Absent or
182
+ * `false` means the strategy is skipped for filtered searches (fail closed).
183
+ */
184
+ readonly supportsIndexDocFilter?: boolean
185
+
147
186
  /** Check if strategy is available and configured */
148
187
  isAvailable(): Promise<boolean>
149
188
 
@@ -0,0 +1,78 @@
1
+ /**
2
+ * Regression coverage for the partitioned-bootstrap registry contract.
3
+ *
4
+ * A Node process can run more than one bootstrap partition: the API-only one
5
+ * (`skipCoreInjectionWidgets: true`) and the full page one. Two invariants have to hold
6
+ * together, and skipping registration outright only satisfied the first:
7
+ *
8
+ * 1. the partition that registers second must never shrink the registry the first published;
9
+ * 2. an API-only process must still be able to READ the registry, because module API routes
10
+ * resolve widget contributions on the request path.
11
+ */
12
+ import { describe, it, expect, beforeEach, jest } from '@jest/globals'
13
+ import type { ModuleInjectionWidgetEntry } from '@open-mercato/shared/modules/registry'
14
+
15
+ const GLOBAL_INJECTION_WIDGETS_KEY = '__openMercatoCoreInjectionWidgetEntries__'
16
+
17
+ function entry(key: string): ModuleInjectionWidgetEntry {
18
+ return { moduleId: key.split(':')[0]!, key, source: 'package', widgetId: key, loader: async () => ({}) as never }
19
+ }
20
+
21
+ type Loader = typeof import('@open-mercato/shared/modules/widgets/injection-loader')
22
+
23
+ function freshLoader(): Loader {
24
+ delete (globalThis as Record<string, unknown>)[GLOBAL_INJECTION_WIDGETS_KEY]
25
+ let loader!: Loader
26
+ jest.isolateModules(() => {
27
+ loader = require('@open-mercato/shared/modules/widgets/injection-loader') as Loader
28
+ })
29
+ return loader
30
+ }
31
+
32
+ describe('core injection widget registration modes', () => {
33
+ beforeEach(() => {
34
+ delete (globalThis as Record<string, unknown>)[GLOBAL_INJECTION_WIDGETS_KEY]
35
+ })
36
+
37
+ it('replaces the registry by default', () => {
38
+ const loader = freshLoader()
39
+ loader.registerCoreInjectionWidgets([entry('a:one'), entry('b:two')])
40
+ loader.registerCoreInjectionWidgets([entry('a:one')])
41
+
42
+ expect(loader.getCoreInjectionWidgets().map((it) => it.key)).toEqual(['a:one'])
43
+ })
44
+
45
+ it('populates an unregistered registry in merge mode instead of leaving it throwing', () => {
46
+ const loader = freshLoader()
47
+ expect(() => loader.getCoreInjectionWidgets()).toThrow(/not registered/)
48
+
49
+ loader.registerCoreInjectionWidgets([entry('a:one')], { mode: 'merge' })
50
+
51
+ expect(loader.getCoreInjectionWidgets().map((it) => it.key)).toEqual(['a:one'])
52
+ })
53
+
54
+ it('never shrinks a registry a fuller partition already published', () => {
55
+ const loader = freshLoader()
56
+ loader.registerCoreInjectionWidgets([entry('a:one'), entry('b:two'), entry('c:three')])
57
+ loader.registerCoreInjectionWidgets([entry('a:one')], { mode: 'merge' })
58
+
59
+ expect(loader.getCoreInjectionWidgets().map((it) => it.key)).toEqual(['a:one', 'b:two', 'c:three'])
60
+ })
61
+
62
+ it('adds only the entries the registry does not already carry', () => {
63
+ const loader = freshLoader()
64
+ loader.registerCoreInjectionWidgets([entry('a:one')])
65
+ loader.registerCoreInjectionWidgets([entry('a:one'), entry('d:four')], { mode: 'merge' })
66
+
67
+ expect(loader.getCoreInjectionWidgets().map((it) => it.key)).toEqual(['a:one', 'd:four'])
68
+ })
69
+
70
+ it('leaves an unregistered registry untouched when it has nothing to contribute', () => {
71
+ const loader = freshLoader()
72
+ loader.registerCoreInjectionWidgets([], { mode: 'merge' })
73
+
74
+ // Publishing an empty array here would turn the loud "not registered" bootstrap error into
75
+ // silently missing contributions on every request.
76
+ expect(() => loader.getCoreInjectionWidgets()).toThrow(/not registered/)
77
+ })
78
+ })
@@ -141,11 +141,41 @@ function notifyInjectionRegistryChanged() {
141
141
  }))
142
142
  }
143
143
 
144
- export function registerCoreInjectionWidgets(entries: ModuleInjectionWidgetEntry[]) {
145
- if (_coreInjectionWidgetEntries !== null && process.env.NODE_ENV === 'development') {
144
+ export type RegisterCoreInjectionWidgetsOptions = {
145
+ /**
146
+ * `replace` (default) publishes `entries` as the whole registry.
147
+ *
148
+ * `merge` adds only the entries the registry does not already carry, keeping whatever a
149
+ * previous bootstrap published. A process may run more than one bootstrap partition (the
150
+ * API-only one and the full page one share a Node process in a mixed web host), and the
151
+ * partition that registers second must not shrink the registry the first one populated.
152
+ */
153
+ mode?: 'replace' | 'merge'
154
+ }
155
+
156
+ export function registerCoreInjectionWidgets(
157
+ entries: ModuleInjectionWidgetEntry[],
158
+ options: RegisterCoreInjectionWidgetsOptions = {},
159
+ ) {
160
+ if (_coreInjectionWidgetEntries !== null && process.env.NODE_ENV === 'development' && options.mode !== 'merge') {
146
161
  logger.debug('Core injection widgets re-registered (this may occur during HMR)')
147
162
  }
148
- const finalEntries = applyInjectionWidgetOverridesToEntries(entries)
163
+ const incoming = applyInjectionWidgetOverridesToEntries(entries)
164
+ if (options.mode !== 'merge') {
165
+ _coreInjectionWidgetEntries = incoming
166
+ writeGlobalInjectionWidgets(incoming)
167
+ notifyInjectionRegistryChanged()
168
+ return
169
+ }
170
+
171
+ const existing = readGlobalInjectionWidgets() ?? _coreInjectionWidgetEntries ?? []
172
+ const known = new Set(existing.map((entry) => entry.key))
173
+ const added = incoming.filter((entry) => !known.has(entry.key))
174
+ // Nothing to contribute: leave the registry exactly as it was. Publishing an empty array here
175
+ // would turn "never registered" into "registered with no widgets", which downgrades the loud
176
+ // bootstrap error into silently missing contributions.
177
+ if (!added.length) return
178
+ const finalEntries = [...existing, ...added]
149
179
  _coreInjectionWidgetEntries = finalEntries
150
180
  writeGlobalInjectionWidgets(finalEntries)
151
181
  notifyInjectionRegistryChanged()