@open-mercato/shared 0.9.1-develop.7574.1.837288eec1 → 0.10.1-develop.7576.1.21d5b50435

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 +6 -46
  6. package/dist/lib/bootstrap/dynamicLoader.js.map +2 -2
  7. package/dist/lib/bootstrap/factory.js +3 -4
  8. package/dist/lib/bootstrap/factory.js.map +2 -2
  9. package/dist/lib/catalog-visibility/index.js +0 -4
  10. package/dist/lib/catalog-visibility/index.js.map +2 -2
  11. package/dist/lib/commands/command-bus.js +13 -45
  12. package/dist/lib/commands/command-bus.js.map +2 -2
  13. package/dist/lib/commands/types.js.map +2 -2
  14. package/dist/lib/crud/exporters.js +1 -1
  15. package/dist/lib/crud/exporters.js.map +2 -2
  16. package/dist/lib/crud/ids.js +1 -13
  17. package/dist/lib/crud/ids.js.map +2 -2
  18. package/dist/lib/db/pg-errors.js +0 -14
  19. package/dist/lib/db/pg-errors.js.map +2 -2
  20. package/dist/lib/localization/index.js +1 -9
  21. package/dist/lib/localization/index.js.map +2 -2
  22. package/dist/lib/localization/translatable-fields.js +0 -26
  23. package/dist/lib/localization/translatable-fields.js.map +2 -2
  24. package/dist/lib/number.js +0 -6
  25. package/dist/lib/number.js.map +2 -2
  26. package/dist/lib/openapi/generator.js +67 -84
  27. package/dist/lib/openapi/generator.js.map +2 -2
  28. package/dist/lib/query/engine.js +10 -45
  29. package/dist/lib/query/engine.js.map +2 -2
  30. package/dist/lib/query/join-utils.js +1 -1
  31. package/dist/lib/query/join-utils.js.map +2 -2
  32. package/dist/lib/query/types.js.map +2 -2
  33. package/dist/lib/url.js +0 -24
  34. package/dist/lib/url.js.map +2 -2
  35. package/dist/lib/version.js +1 -1
  36. package/dist/lib/version.js.map +1 -1
  37. package/dist/lib/webhooks/body.js +3 -7
  38. package/dist/lib/webhooks/body.js.map +2 -2
  39. package/dist/lib/webhooks/index.js +0 -2
  40. package/dist/lib/webhooks/index.js.map +2 -2
  41. package/dist/modules/integrations/types.js.map +2 -2
  42. package/dist/modules/notifications/types.js +0 -4
  43. package/dist/modules/notifications/types.js.map +3 -3
  44. package/dist/modules/payment_gateways/types.js +1 -14
  45. package/dist/modules/payment_gateways/types.js.map +2 -2
  46. package/dist/modules/registry.js.map +2 -2
  47. package/dist/modules/search.js.map +2 -2
  48. package/dist/modules/widgets/injection-loader.js +3 -14
  49. package/dist/modules/widgets/injection-loader.js.map +2 -2
  50. package/package.json +3 -10
  51. package/src/lib/__tests__/number.test.ts +1 -24
  52. package/src/lib/auth/__tests__/server.test.ts +0 -14
  53. package/src/lib/auth/server.ts +1 -1
  54. package/src/lib/bootstrap/__tests__/dynamicLoader.appDi.test.ts +2 -2
  55. package/src/lib/bootstrap/__tests__/dynamicLoader.appModuleOverrides.test.ts +2 -2
  56. package/src/lib/bootstrap/__tests__/dynamicLoader.cacheRecovery.test.ts +2 -2
  57. package/src/lib/bootstrap/__tests__/dynamicLoader.commandInterceptors.test.ts +2 -2
  58. package/src/lib/bootstrap/__tests__/dynamicLoader.tsconfig.test.ts +2 -2
  59. package/src/lib/bootstrap/__tests__/factory.test.ts +6 -18
  60. package/src/lib/bootstrap/dynamicLoader.ts +5 -63
  61. package/src/lib/bootstrap/factory.ts +3 -8
  62. package/src/lib/bootstrap/types.ts +1 -5
  63. package/src/lib/catalog-visibility/index.ts +0 -2
  64. package/src/lib/commands/__tests__/command-bus.test.ts +1 -100
  65. package/src/lib/commands/command-bus.ts +10 -42
  66. package/src/lib/commands/command-interceptor.ts +0 -4
  67. package/src/lib/commands/types.ts +0 -14
  68. package/src/lib/crud/__tests__/exporters.test.ts +0 -18
  69. package/src/lib/crud/__tests__/ids.test.ts +1 -34
  70. package/src/lib/crud/exporters.ts +1 -1
  71. package/src/lib/crud/ids.ts +0 -24
  72. package/src/lib/db/__tests__/pg-errors.test.ts +1 -68
  73. package/src/lib/db/pg-errors.ts +0 -35
  74. package/src/lib/encryption/__tests__/tenantDataEncryptionService.test.ts +0 -23
  75. package/src/lib/localization/__tests__/translatable-fields.test.ts +0 -17
  76. package/src/lib/localization/index.ts +1 -8
  77. package/src/lib/localization/translatable-fields.ts +0 -46
  78. package/src/lib/number.ts +0 -13
  79. package/src/lib/openapi/generator.ts +77 -106
  80. package/src/lib/query/engine.ts +8 -58
  81. package/src/lib/query/join-utils.ts +1 -1
  82. package/src/lib/query/types.ts +1 -13
  83. package/src/lib/url.ts +0 -30
  84. package/src/lib/webhooks/__tests__/body.test.ts +0 -48
  85. package/src/lib/webhooks/body.ts +4 -11
  86. package/src/lib/webhooks/index.ts +0 -1
  87. package/src/modules/integrations/types.ts +0 -6
  88. package/src/modules/notifications/types.ts +0 -7
  89. package/src/modules/payment_gateways/__tests__/types.test.ts +0 -44
  90. package/src/modules/payment_gateways/types.ts +6 -52
  91. package/src/modules/registry.ts +0 -5
  92. package/src/modules/search.ts +0 -39
  93. package/src/modules/widgets/injection-loader.ts +3 -33
  94. package/dist/lib/bootstrap/next-subpath-resolve-hook-worker.js +0 -6
  95. package/dist/lib/bootstrap/next-subpath-resolve-hook-worker.js.map +0 -7
  96. package/dist/lib/bootstrap/next-subpath-resolve-hook.js +0 -62
  97. package/dist/lib/bootstrap/next-subpath-resolve-hook.js.map +0 -7
  98. package/dist/lib/catalog-visibility/canonicalizeEffectiveScope.js +0 -33
  99. package/dist/lib/catalog-visibility/canonicalizeEffectiveScope.js.map +0 -7
  100. package/dist/lib/catalog-visibility/hashEffectiveScope.js +0 -9
  101. package/dist/lib/catalog-visibility/hashEffectiveScope.js.map +0 -7
  102. package/dist/lib/db/advisoryLock.js +0 -172
  103. package/dist/lib/db/advisoryLock.js.map +0 -7
  104. package/dist/lib/query/overlap.js +0 -58
  105. package/dist/lib/query/overlap.js.map +0 -7
  106. package/dist/modules/notifications/safeHref.js +0 -20
  107. package/dist/modules/notifications/safeHref.js.map +0 -7
  108. package/src/lib/__tests__/url.cross-site.test.ts +0 -140
  109. package/src/lib/bootstrap/__tests__/dynamicLoader.externalSubpath.test.ts +0 -141
  110. package/src/lib/bootstrap/__tests__/next-subpath-resolve-hook.test.ts +0 -174
  111. package/src/lib/bootstrap/next-subpath-resolve-hook-worker.ts +0 -3
  112. package/src/lib/bootstrap/next-subpath-resolve-hook.ts +0 -130
  113. package/src/lib/catalog-visibility/__tests__/hashEffectiveScope.test.ts +0 -121
  114. package/src/lib/catalog-visibility/canonicalizeEffectiveScope.ts +0 -41
  115. package/src/lib/catalog-visibility/hashEffectiveScope.ts +0 -8
  116. package/src/lib/db/__tests__/advisoryLock.integration.test.ts +0 -343
  117. package/src/lib/db/__tests__/advisoryLock.test.ts +0 -564
  118. package/src/lib/db/advisoryLock.ts +0 -271
  119. package/src/lib/openapi/__tests__/optional-path-params.test.ts +0 -102
  120. package/src/lib/query/__tests__/empty-ids-filter.pg.test.ts +0 -89
  121. package/src/lib/query/__tests__/engine.empty-in-filter.test.ts +0 -79
  122. package/src/lib/query/__tests__/overlap.test.ts +0 -219
  123. package/src/lib/query/overlap.ts +0 -106
  124. package/src/modules/notifications/safeHref.ts +0 -19
  125. package/src/modules/widgets/__tests__/injection-loader.partitioned-registration.test.ts +0 -78
@@ -1,219 +0,0 @@
1
- import type { EntityManager } from '@mikro-orm/postgresql'
2
- import {
3
- DummyDriver,
4
- Kysely,
5
- PostgresAdapter,
6
- PostgresIntrospector,
7
- PostgresQueryCompiler,
8
- sql,
9
- type RawBuilder,
10
- } from 'kysely'
11
- import { BasicQueryEngine } from '../engine'
12
- import { normalizeFilters } from '../join-utils'
13
- import {
14
- buildArrayColumnNoOverlapPredicate,
15
- buildArrayColumnOverlapPredicate,
16
- buildJsonbNoOverlapPredicate,
17
- buildJsonbOverlapPredicate,
18
- buildScalarOverlapPredicate,
19
- jsonbDocKeyExpression,
20
- normalizeOverlapValues,
21
- } from '../overlap'
22
- import type { FilterOp } from '../types'
23
-
24
- function makeCompilingDb(): Kysely<any> {
25
- return new Kysely<any>({
26
- dialect: {
27
- createAdapter: () => new PostgresAdapter(),
28
- createDriver: () => new DummyDriver(),
29
- createIntrospector: (db) => new PostgresIntrospector(db),
30
- createQueryCompiler: () => new PostgresQueryCompiler(),
31
- },
32
- })
33
- }
34
-
35
- const db = makeCompilingDb()
36
-
37
- function compileWhere(predicate: RawBuilder<boolean>): { sql: string; parameters: readonly unknown[] } {
38
- const compiled = db.selectFrom('entity_indexes as ei').select('ei.id').where(predicate).compile()
39
- return { sql: compiled.sql, parameters: compiled.parameters }
40
- }
41
-
42
- type EngineInternals = {
43
- applyColumnOp: (builder: unknown, column: string, op: string, value: unknown) => { compile: () => { sql: string; parameters: readonly unknown[] } }
44
- buildColumnOpExpression: (eb: unknown, column: string, op: string, value: unknown) => RawBuilder<boolean>
45
- buildIndexDocOpExpression: (eb: unknown, opts: {
46
- entity: string
47
- field: string
48
- op: FilterOp
49
- value: unknown
50
- recordIdColumn: string
51
- tenantId?: string | null
52
- withDeleted: boolean
53
- }) => RawBuilder<boolean>
54
- }
55
-
56
- const engine = new BasicQueryEngine({} as EntityManager, () => db) as unknown as EngineInternals
57
-
58
- describe('normalizeOverlapValues', () => {
59
- it('flattens, stringifies, drops nullish and de-duplicates', () => {
60
- expect(normalizeOverlapValues(['a', 'b', 'a', null, undefined, 3])).toEqual(['a', 'b', '3'])
61
- expect(normalizeOverlapValues('a')).toEqual(['a'])
62
- expect(normalizeOverlapValues(undefined)).toEqual([])
63
- expect(normalizeOverlapValues(null)).toEqual([])
64
- })
65
- })
66
-
67
- describe('overlap predicate builders', () => {
68
- it('compiles a jsonb has-any-of with the values bound as one array parameter', () => {
69
- const compiled = compileWhere(buildJsonbOverlapPredicate('ei.doc', 'scope_keys', ['cat:1', 'tag:2']))
70
- expect(compiled.sql).toContain(`("ei"."doc" -> 'scope_keys') ?| $1::text[]`)
71
- expect(compiled.parameters).toEqual([['cat:1', 'tag:2']])
72
- })
73
-
74
- it('never interpolates values into the SQL text', () => {
75
- const hostile = "x'); drop table entity_indexes; --"
76
- const compiled = compileWhere(buildJsonbOverlapPredicate('ei.doc', 'scope_keys', [hostile]))
77
- expect(compiled.sql).not.toContain('drop table')
78
- expect(compiled.parameters).toEqual([[hostile]])
79
- })
80
-
81
- it('binds a key that is not a plain identifier instead of inlining it', () => {
82
- const key = "scope'keys"
83
- const expression = db.selectFrom('entity_indexes as ei').select(jsonbDocKeyExpression('ei.doc', key).as('value')).compile()
84
- expect(expression.sql).toContain('("ei"."doc" -> $1)')
85
- expect(expression.parameters).toEqual([key])
86
- })
87
-
88
- it.each([
89
- ['jsonb', () => buildJsonbOverlapPredicate('ei.doc', 'scope_keys', [])],
90
- ['array column', () => buildArrayColumnOverlapPredicate('ei.tags', [])],
91
- ['scalar', () => buildScalarOverlapPredicate(sql`ei.value`, [null, undefined])],
92
- ])('compiles an empty %s value list to false (matches nothing)', (_label, build) => {
93
- const compiled = compileWhere(build())
94
- expect(compiled.sql).toMatch(/where false$/)
95
- expect(compiled.parameters).toEqual([])
96
- })
97
-
98
- it('compiles array-column overlap to && with a bound array', () => {
99
- const compiled = compileWhere(buildArrayColumnOverlapPredicate('ei.tags', ['a', 'b']))
100
- expect(compiled.sql).toContain('"ei"."tags" && $1')
101
- expect(compiled.parameters).toEqual([['a', 'b']])
102
- })
103
-
104
- it('compiles scalar overlap to an IN list of bound values', () => {
105
- const compiled = compileWhere(buildScalarOverlapPredicate(sql`ei.value`, ['a', 'b']))
106
- expect(compiled.sql).toContain('ei.value in ($1, $2)')
107
- expect(compiled.parameters).toEqual(['a', 'b'])
108
- })
109
- })
110
-
111
- describe('BasicQueryEngine overlap', () => {
112
- it('normalizes $overlap from the object filter syntax', () => {
113
- const filters = normalizeFilters({ scope_keys: { $overlap: ['cat:1'] } })
114
- expect(filters).toEqual([expect.objectContaining({ field: 'scope_keys', op: 'overlap', value: ['cat:1'] })])
115
- })
116
-
117
- it('applies && on a base column', () => {
118
- const builder = db.selectFrom('catalog_products as b').select('b.id')
119
- const compiled = engine.applyColumnOp(builder, 'b.tags', 'overlap', ['x', 'y']).compile()
120
- expect(compiled.sql).toContain('"b"."tags" && $1')
121
- expect(compiled.parameters).toEqual([['x', 'y']])
122
- })
123
-
124
- it('compiles an index-document overlap inside the entity_indexes EXISTS', () => {
125
- const compiled = db
126
- .selectFrom('catalog_products as b')
127
- .select('b.id')
128
- .where((eb) => engine.buildIndexDocOpExpression(eb, {
129
- entity: 'catalog:catalog_product',
130
- field: 'scope_keys',
131
- op: 'overlap',
132
- value: ['cat:1'],
133
- recordIdColumn: 'b.id',
134
- tenantId: 'tenant-1',
135
- withDeleted: false,
136
- }))
137
- .compile()
138
- expect(compiled.sql).toMatch(/exists \(select 1 as "one" from "entity_indexes" as "ei_\d+"/)
139
- expect(compiled.sql).toMatch(/\("ei_\d+"\."doc" -> 'scope_keys'\) \?\| \$\d+::text\[\]/)
140
- expect(compiled.parameters).toContainEqual(['cat:1'])
141
- })
142
-
143
- it('leaves existing operators unchanged', () => {
144
- const compiled = db
145
- .selectFrom('catalog_products as b')
146
- .select('b.id')
147
- .where((eb) => eb.and([
148
- engine.buildColumnOpExpression(eb, 'b.sku', 'in', ['a', 'b']),
149
- engine.buildColumnOpExpression(eb, 'b.title', 'ilike', '%x%'),
150
- engine.buildColumnOpExpression(eb, 'b.deleted_at', 'exists', false),
151
- ]))
152
- .compile()
153
- expect(compiled.sql).toContain('"b"."sku" in ($1, $2)')
154
- expect(compiled.sql).toContain('"b"."title" ilike $3')
155
- expect(compiled.sql).toContain('"b"."deleted_at" is null')
156
- expect(compiled.sql).not.toContain('&&')
157
- expect(compiled.sql).not.toContain('?|')
158
- })
159
- })
160
-
161
- describe('noverlap predicate builders', () => {
162
- it('compiles a jsonb has-none-of that requires a present array', () => {
163
- const compiled = compileWhere(buildJsonbNoOverlapPredicate('ei.doc', 'scope_keys', ['cat:1', 'tag:2']))
164
- expect(compiled.sql).toContain(
165
- `(jsonb_typeof(("ei"."doc" -> 'scope_keys')) = 'array' and not (("ei"."doc" -> 'scope_keys') ?| $1::text[]))`,
166
- )
167
- expect(compiled.parameters).toEqual([['cat:1', 'tag:2']])
168
- })
169
-
170
- it('keeps only the array check for an empty jsonb value list', () => {
171
- const compiled = compileWhere(buildJsonbNoOverlapPredicate('ei.doc', 'scope_keys', []))
172
- expect(compiled.sql).toMatch(/where jsonb_typeof\(\("ei"\."doc" -> 'scope_keys'\)\) = 'array'$/)
173
- expect(compiled.parameters).toEqual([])
174
- })
175
-
176
- it('compiles array-column has-none-of to not && and an empty list to is not null', () => {
177
- const compiled = compileWhere(buildArrayColumnNoOverlapPredicate('ei.tags', ['a']))
178
- expect(compiled.sql).toContain('not ("ei"."tags" && $1)')
179
- expect(compiled.parameters).toEqual([['a']])
180
- expect(compileWhere(buildArrayColumnNoOverlapPredicate('ei.tags', [])).sql).toContain('"ei"."tags" is not null')
181
- })
182
- })
183
-
184
- describe('BasicQueryEngine noverlap', () => {
185
- it('normalizes $noverlap from the object filter syntax', () => {
186
- const filters = normalizeFilters({ scope_keys: { $noverlap: ['cat:1'] } })
187
- expect(filters).toEqual([expect.objectContaining({ field: 'scope_keys', op: 'noverlap', value: ['cat:1'] })])
188
- })
189
-
190
- it('compiles an index-document noverlap inside the entity_indexes EXISTS', () => {
191
- const compiled = db
192
- .selectFrom('catalog_products as b')
193
- .select('b.id')
194
- .where((eb) => engine.buildIndexDocOpExpression(eb, {
195
- entity: 'catalog:catalog_product',
196
- field: 'scope_keys',
197
- op: 'noverlap',
198
- value: ['cat:1'],
199
- recordIdColumn: 'b.id',
200
- tenantId: 'tenant-1',
201
- withDeleted: false,
202
- }))
203
- .compile()
204
- expect(compiled.sql).toMatch(/exists \(select 1 as "one" from "entity_indexes" as "ei_\d+"/)
205
- expect(compiled.sql).toMatch(/not \(\("ei_\d+"\."doc" -> 'scope_keys'\) \?\| \$\d+::text\[\]\)/)
206
- })
207
-
208
- it('applies not && on a base column', () => {
209
- const builder = db.selectFrom('catalog_products as b').select('b.id')
210
- const compiled = engine.applyColumnOp(builder, 'b.tags', 'noverlap', ['x']).compile()
211
- expect(compiled.sql).toContain('not ("b"."tags" && $1)')
212
- const expression = db
213
- .selectFrom('catalog_products as b')
214
- .select('b.id')
215
- .where((eb) => engine.buildColumnOpExpression(eb, 'b.tags', 'noverlap', ['x']))
216
- .compile()
217
- expect(expression.sql).toContain('not ("b"."tags" && $1)')
218
- })
219
- })
@@ -1,106 +0,0 @@
1
- import { sql, type RawBuilder } from 'kysely'
2
-
3
- const LITERAL_SAFE_DOC_KEY = /^[A-Za-z0-9_:.-]{1,128}$/
4
-
5
- /**
6
- * Normalizes the right-hand side of an `overlap` filter into a de-duplicated list of strings.
7
- *
8
- * `null`/`undefined` entries are dropped; a scalar is treated as a one-element list. An empty
9
- * result means the filter matches nothing — every `overlap` builder compiles it to `false`
10
- * rather than dropping the predicate, so a caller that computes an empty inclusion set can
11
- * never widen a query by accident.
12
- */
13
- export function normalizeOverlapValues(value: unknown): string[] {
14
- const list: readonly unknown[] = Array.isArray(value)
15
- ? value
16
- : value === undefined || value === null
17
- ? []
18
- : [value]
19
- const out: string[] = []
20
- const seen = new Set<string>()
21
- for (const entry of list) {
22
- if (entry === undefined || entry === null) continue
23
- const normalized = String(entry)
24
- if (seen.has(normalized)) continue
25
- seen.add(normalized)
26
- out.push(normalized)
27
- }
28
- return out
29
- }
30
-
31
- /**
32
- * `(<docColumn> -> '<key>')` — the jsonb sub-document of an index document.
33
- *
34
- * The key is emitted as an escaped SQL literal when it is a plain identifier so the expression is
35
- * textually identical to a GIN expression index such as `((doc -> 'scope_keys'))` and the planner
36
- * can match it. Any other key falls back to a bound parameter (still correct, just not indexable).
37
- */
38
- export function jsonbDocKeyExpression(docColumnRef: string, key: string): RawBuilder<unknown> {
39
- const keyExpr = LITERAL_SAFE_DOC_KEY.test(key) ? sql.lit(key) : sql`${key}`
40
- return sql`(${sql.ref(docColumnRef)} -> ${keyExpr})`
41
- }
42
-
43
- /**
44
- * jsonb "has any of" over a string-array key of an index document:
45
- * `(<docColumn> -> '<key>') ?| $n::text[]`. The values are bound as one array parameter.
46
- * A missing key or a JSON `null` evaluates to SQL NULL, i.e. the row does not match.
47
- */
48
- export function buildJsonbOverlapPredicate(docColumnRef: string, key: string, value: unknown): RawBuilder<boolean> {
49
- const values = normalizeOverlapValues(value)
50
- if (!values.length) return sql<boolean>`false`
51
- return sql<boolean>`${jsonbDocKeyExpression(docColumnRef, key)} ?| ${values}::text[]`
52
- }
53
-
54
- /**
55
- * jsonb "has none of" over a string-array key of an index document (`noverlap`):
56
- * `(jsonb_typeof(<expr>) = 'array' and not (<expr> ?| $n::text[]))`.
57
- *
58
- * Only a document whose key holds a JSON array can match: a missing key, a JSON `null` or a
59
- * scalar never satisfies it, so an unindexed document can never slip through an exclusion.
60
- * An empty value list excludes nothing (only the array check remains).
61
- */
62
- export function buildJsonbNoOverlapPredicate(docColumnRef: string, key: string, value: unknown): RawBuilder<boolean> {
63
- const values = normalizeOverlapValues(value)
64
- const keyExpr = jsonbDocKeyExpression(docColumnRef, key)
65
- const isArray = sql<boolean>`jsonb_typeof(${keyExpr}) = 'array'`
66
- if (!values.length) return isArray
67
- return sql<boolean>`(${isArray} and not (${keyExpr} ?| ${values}::text[]))`
68
- }
69
-
70
- /**
71
- * Postgres array overlap for array-typed columns: `<column> && $n`. The bound array takes its
72
- * element type from the column, so `text[]`, `varchar[]` and `uuid[]` columns all work.
73
- */
74
- export function buildArrayColumnOverlapPredicate(
75
- column: string | RawBuilder<unknown>,
76
- value: unknown,
77
- ): RawBuilder<boolean> {
78
- const values = normalizeOverlapValues(value)
79
- if (!values.length) return sql<boolean>`false`
80
- const columnExpr = typeof column === 'string' ? sql.ref(column) : column
81
- return sql<boolean>`${columnExpr} && ${values}`
82
- }
83
-
84
- /**
85
- * Postgres array "has none of" for array-typed columns (`noverlap`): `not (<column> && $n)`.
86
- * A NULL column never matches; an empty value list excludes nothing (`<column> is not null`).
87
- */
88
- export function buildArrayColumnNoOverlapPredicate(
89
- column: string | RawBuilder<unknown>,
90
- value: unknown,
91
- ): RawBuilder<boolean> {
92
- const values = normalizeOverlapValues(value)
93
- const columnExpr = typeof column === 'string' ? sql.ref(column) : column
94
- if (!values.length) return sql<boolean>`${columnExpr} is not null`
95
- return sql<boolean>`not (${columnExpr} && ${values})`
96
- }
97
-
98
- /**
99
- * `overlap` over a scalar expression evaluated once per stored value (custom-field value rows):
100
- * the record matches when any of its values is one of the requested ones — `<expr> in (...)`.
101
- */
102
- export function buildScalarOverlapPredicate(expression: RawBuilder<unknown>, value: unknown): RawBuilder<boolean> {
103
- const values = normalizeOverlapValues(value)
104
- if (!values.length) return sql<boolean>`false`
105
- return sql<boolean>`${expression} in (${sql.join(values.map((entry) => sql`${entry}`), sql`, `)})`
106
- }
@@ -1,19 +0,0 @@
1
- const SENTINEL_ORIGIN = 'https://notifications.open-mercato.local'
2
- const CONTROL_OR_BACKSLASH = /[\u0000-\u001F\u007F\\]/
3
-
4
- export function isSafeNotificationHref(href: string): boolean {
5
- if (!href.startsWith('/') || href.startsWith('//')) {
6
- return false
7
- }
8
-
9
- if (CONTROL_OR_BACKSLASH.test(href)) {
10
- return false
11
- }
12
-
13
- try {
14
- const parsed = new URL(href, SENTINEL_ORIGIN)
15
- return parsed.origin === SENTINEL_ORIGIN && !parsed.pathname.startsWith('//')
16
- } catch {
17
- return false
18
- }
19
- }
@@ -1,78 +0,0 @@
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
- })