@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.
- package/.turbo/turbo-build.log +1 -1
- package/AGENTS.md +4 -4
- package/dist/lib/auth/server.js +1 -1
- package/dist/lib/auth/server.js.map +2 -2
- package/dist/lib/bootstrap/dynamicLoader.js +46 -6
- package/dist/lib/bootstrap/dynamicLoader.js.map +2 -2
- package/dist/lib/bootstrap/factory.js +4 -3
- package/dist/lib/bootstrap/factory.js.map +2 -2
- package/dist/lib/bootstrap/next-subpath-resolve-hook-worker.js +6 -0
- package/dist/lib/bootstrap/next-subpath-resolve-hook-worker.js.map +7 -0
- package/dist/lib/bootstrap/next-subpath-resolve-hook.js +62 -0
- package/dist/lib/bootstrap/next-subpath-resolve-hook.js.map +7 -0
- package/dist/lib/catalog-visibility/canonicalizeEffectiveScope.js +33 -0
- package/dist/lib/catalog-visibility/canonicalizeEffectiveScope.js.map +7 -0
- package/dist/lib/catalog-visibility/hashEffectiveScope.js +9 -0
- package/dist/lib/catalog-visibility/hashEffectiveScope.js.map +7 -0
- package/dist/lib/catalog-visibility/index.js +4 -0
- package/dist/lib/catalog-visibility/index.js.map +2 -2
- package/dist/lib/commands/command-bus.js +45 -13
- package/dist/lib/commands/command-bus.js.map +2 -2
- package/dist/lib/commands/types.js.map +2 -2
- package/dist/lib/crud/exporters.js +1 -1
- package/dist/lib/crud/exporters.js.map +2 -2
- package/dist/lib/crud/ids.js +13 -1
- package/dist/lib/crud/ids.js.map +2 -2
- package/dist/lib/db/advisoryLock.js +172 -0
- package/dist/lib/db/advisoryLock.js.map +7 -0
- package/dist/lib/db/pg-errors.js +14 -0
- package/dist/lib/db/pg-errors.js.map +2 -2
- package/dist/lib/localization/index.js +9 -1
- package/dist/lib/localization/index.js.map +2 -2
- package/dist/lib/localization/translatable-fields.js +26 -0
- package/dist/lib/localization/translatable-fields.js.map +2 -2
- package/dist/lib/number.js +6 -0
- package/dist/lib/number.js.map +2 -2
- package/dist/lib/openapi/generator.js +84 -67
- package/dist/lib/openapi/generator.js.map +2 -2
- package/dist/lib/query/engine.js +45 -10
- package/dist/lib/query/engine.js.map +2 -2
- package/dist/lib/query/join-utils.js +1 -1
- package/dist/lib/query/join-utils.js.map +2 -2
- package/dist/lib/query/overlap.js +58 -0
- package/dist/lib/query/overlap.js.map +7 -0
- package/dist/lib/query/types.js.map +2 -2
- package/dist/lib/url.js +24 -0
- package/dist/lib/url.js.map +2 -2
- package/dist/lib/version.js +1 -1
- package/dist/lib/version.js.map +1 -1
- package/dist/lib/webhooks/body.js +7 -3
- package/dist/lib/webhooks/body.js.map +2 -2
- package/dist/lib/webhooks/index.js +2 -0
- package/dist/lib/webhooks/index.js.map +2 -2
- package/dist/modules/integrations/types.js.map +2 -2
- package/dist/modules/notifications/safeHref.js +20 -0
- package/dist/modules/notifications/safeHref.js.map +7 -0
- package/dist/modules/notifications/types.js +4 -0
- package/dist/modules/notifications/types.js.map +3 -3
- package/dist/modules/payment_gateways/types.js +14 -1
- package/dist/modules/payment_gateways/types.js.map +2 -2
- package/dist/modules/registry.js.map +2 -2
- package/dist/modules/search.js.map +2 -2
- package/dist/modules/widgets/injection-loader.js +14 -3
- package/dist/modules/widgets/injection-loader.js.map +2 -2
- package/package.json +9 -2
- package/src/lib/__tests__/number.test.ts +24 -1
- package/src/lib/__tests__/url.cross-site.test.ts +140 -0
- package/src/lib/auth/__tests__/server.test.ts +14 -0
- package/src/lib/auth/server.ts +1 -1
- package/src/lib/bootstrap/__tests__/dynamicLoader.appDi.test.ts +2 -2
- package/src/lib/bootstrap/__tests__/dynamicLoader.appModuleOverrides.test.ts +2 -2
- package/src/lib/bootstrap/__tests__/dynamicLoader.cacheRecovery.test.ts +2 -2
- package/src/lib/bootstrap/__tests__/dynamicLoader.commandInterceptors.test.ts +2 -2
- package/src/lib/bootstrap/__tests__/dynamicLoader.externalSubpath.test.ts +141 -0
- package/src/lib/bootstrap/__tests__/dynamicLoader.tsconfig.test.ts +2 -2
- package/src/lib/bootstrap/__tests__/factory.test.ts +18 -6
- package/src/lib/bootstrap/__tests__/next-subpath-resolve-hook.test.ts +174 -0
- package/src/lib/bootstrap/dynamicLoader.ts +63 -5
- package/src/lib/bootstrap/factory.ts +8 -3
- package/src/lib/bootstrap/next-subpath-resolve-hook-worker.ts +3 -0
- package/src/lib/bootstrap/next-subpath-resolve-hook.ts +130 -0
- package/src/lib/bootstrap/types.ts +5 -1
- package/src/lib/catalog-visibility/__tests__/hashEffectiveScope.test.ts +121 -0
- package/src/lib/catalog-visibility/canonicalizeEffectiveScope.ts +41 -0
- package/src/lib/catalog-visibility/hashEffectiveScope.ts +8 -0
- package/src/lib/catalog-visibility/index.ts +2 -0
- package/src/lib/commands/__tests__/command-bus.test.ts +100 -1
- package/src/lib/commands/command-bus.ts +42 -10
- package/src/lib/commands/command-interceptor.ts +4 -0
- package/src/lib/commands/types.ts +14 -0
- package/src/lib/crud/__tests__/exporters.test.ts +18 -0
- package/src/lib/crud/__tests__/ids.test.ts +34 -1
- package/src/lib/crud/exporters.ts +1 -1
- package/src/lib/crud/ids.ts +24 -0
- package/src/lib/db/__tests__/advisoryLock.integration.test.ts +343 -0
- package/src/lib/db/__tests__/advisoryLock.test.ts +564 -0
- package/src/lib/db/__tests__/pg-errors.test.ts +68 -1
- package/src/lib/db/advisoryLock.ts +271 -0
- package/src/lib/db/pg-errors.ts +35 -0
- package/src/lib/encryption/__tests__/tenantDataEncryptionService.test.ts +23 -0
- package/src/lib/localization/__tests__/translatable-fields.test.ts +17 -0
- package/src/lib/localization/index.ts +8 -1
- package/src/lib/localization/translatable-fields.ts +46 -0
- package/src/lib/number.ts +13 -0
- package/src/lib/openapi/__tests__/optional-path-params.test.ts +102 -0
- package/src/lib/openapi/generator.ts +106 -77
- package/src/lib/query/__tests__/empty-ids-filter.pg.test.ts +89 -0
- package/src/lib/query/__tests__/engine.empty-in-filter.test.ts +79 -0
- package/src/lib/query/__tests__/overlap.test.ts +219 -0
- package/src/lib/query/engine.ts +58 -8
- package/src/lib/query/join-utils.ts +1 -1
- package/src/lib/query/overlap.ts +106 -0
- package/src/lib/query/types.ts +13 -1
- package/src/lib/url.ts +30 -0
- package/src/lib/webhooks/__tests__/body.test.ts +48 -0
- package/src/lib/webhooks/body.ts +11 -4
- package/src/lib/webhooks/index.ts +1 -0
- package/src/modules/integrations/types.ts +6 -0
- package/src/modules/notifications/safeHref.ts +19 -0
- package/src/modules/notifications/types.ts +7 -0
- package/src/modules/payment_gateways/__tests__/types.test.ts +44 -0
- package/src/modules/payment_gateways/types.ts +52 -6
- package/src/modules/registry.ts +5 -0
- package/src/modules/search.ts +39 -0
- package/src/modules/widgets/__tests__/injection-loader.partitioned-registration.test.ts +78 -0
- package/src/modules/widgets/injection-loader.ts +33 -3
package/src/modules/search.ts
CHANGED
|
@@ -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
|
|
145
|
-
|
|
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
|
|
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()
|