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