@open-mercato/shared 0.7.0 → 0.7.1-develop.7103.1.41ff100d93
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 +12 -1
- package/dist/lib/auth/jwt.js +6 -0
- package/dist/lib/auth/jwt.js.map +2 -2
- package/dist/lib/auth/mfaPendingAccess.js +42 -0
- package/dist/lib/auth/mfaPendingAccess.js.map +7 -0
- package/dist/lib/auth/organizationAccess.js +7 -4
- package/dist/lib/auth/organizationAccess.js.map +2 -2
- package/dist/lib/auth/principal-service.js +1 -0
- package/dist/lib/auth/principal-service.js.map +7 -0
- package/dist/lib/auth/server.js +38 -7
- package/dist/lib/auth/server.js.map +2 -2
- package/dist/lib/commands/command-bus.js +6 -1
- package/dist/lib/commands/command-bus.js.map +2 -2
- package/dist/lib/crud/factory.js +29 -12
- package/dist/lib/crud/factory.js.map +2 -2
- package/dist/lib/crud/ids.js +6 -3
- package/dist/lib/crud/ids.js.map +2 -2
- package/dist/lib/crud/query-params.js +33 -0
- package/dist/lib/crud/query-params.js.map +7 -0
- package/dist/lib/data/engine.js +8 -2
- package/dist/lib/data/engine.js.map +2 -2
- package/dist/lib/html/htmlToPlainText.js +16 -0
- package/dist/lib/html/htmlToPlainText.js.map +7 -0
- package/dist/lib/location/countries.js +12 -0
- package/dist/lib/location/countries.js.map +2 -2
- package/dist/lib/openapi/crud.js +4 -1
- package/dist/lib/openapi/crud.js.map +2 -2
- package/dist/lib/query/count-cap.js +11 -0
- package/dist/lib/query/count-cap.js.map +7 -0
- package/dist/lib/query/engine.js +270 -34
- package/dist/lib/query/engine.js.map +3 -3
- package/dist/lib/query/types.js.map +1 -1
- package/dist/lib/queue/dispatchOrigin.js +20 -0
- package/dist/lib/queue/dispatchOrigin.js.map +7 -0
- package/dist/lib/search/config.js +1 -0
- package/dist/lib/search/config.js.map +2 -2
- package/dist/lib/search/entityAccess.js +44 -0
- package/dist/lib/search/entityAccess.js.map +7 -0
- package/dist/lib/version.js +1 -1
- package/dist/lib/version.js.map +1 -1
- package/dist/modules/events/factory.js +69 -15
- package/dist/modules/events/factory.js.map +2 -2
- package/dist/modules/registry.js +15 -0
- package/dist/modules/registry.js.map +2 -2
- package/dist/modules/widgets/component-registry.js.map +2 -2
- package/package.json +10 -3
- package/src/lib/auth/__tests__/jwt.test.ts +13 -0
- package/src/lib/auth/__tests__/mfaPendingAccess.test.ts +69 -0
- package/src/lib/auth/__tests__/organizationAccess.test.ts +36 -1
- package/src/lib/auth/__tests__/principalServiceExport.test.ts +67 -0
- package/src/lib/auth/__tests__/server.apiKeyCache.test.ts +324 -0
- package/src/lib/auth/__tests__/server.test.ts +104 -0
- package/src/lib/auth/jwt.ts +17 -0
- package/src/lib/auth/mfaPendingAccess.ts +70 -0
- package/src/lib/auth/organizationAccess.ts +11 -3
- package/src/lib/auth/principal-service.ts +110 -0
- package/src/lib/auth/server.ts +78 -8
- package/src/lib/commands/__tests__/command-bus.test.ts +31 -0
- package/src/lib/commands/command-bus.ts +8 -1
- package/src/lib/crud/__tests__/crud-factory.test.ts +236 -0
- package/src/lib/crud/__tests__/ids.test.ts +29 -0
- package/src/lib/crud/__tests__/query-params.test.ts +98 -0
- package/src/lib/crud/factory.ts +38 -11
- package/src/lib/crud/ids.ts +11 -8
- package/src/lib/crud/query-params.ts +75 -0
- package/src/lib/data/__tests__/engine.event-validation.test.ts +9 -1
- package/src/lib/data/engine.ts +7 -1
- package/src/lib/html/__tests__/htmlToPlainText.test.ts +59 -0
- package/src/lib/html/htmlToPlainText.ts +17 -0
- package/src/lib/location/__tests__/countries.test.ts +15 -0
- package/src/lib/location/countries.ts +17 -0
- package/src/lib/openapi/crud.ts +3 -0
- package/src/lib/query/__tests__/count-cap-plan.test.ts +240 -0
- package/src/lib/query/__tests__/count-cap.test.ts +41 -0
- package/src/lib/query/__tests__/engine.count-distinct.test.ts +162 -15
- package/src/lib/query/__tests__/engine.scope-and-or.test.ts +11 -1
- package/src/lib/query/__tests__/engine.test.ts +445 -7
- package/src/lib/query/count-cap.ts +19 -0
- package/src/lib/query/engine.ts +434 -54
- package/src/lib/query/types.ts +15 -0
- package/src/lib/queue/dispatchOrigin.ts +35 -0
- package/src/lib/search/config.ts +10 -0
- package/src/lib/search/entityAccess.ts +132 -0
- package/src/modules/events/__tests__/factory.test.ts +88 -0
- package/src/modules/events/factory.ts +111 -19
- package/src/modules/events/types.ts +17 -0
- package/src/modules/registry.ts +40 -0
- package/src/modules/widgets/component-registry.ts +14 -0
|
@@ -1,6 +1,13 @@
|
|
|
1
|
-
import { BasicQueryEngine } from '../engine'
|
|
1
|
+
import { BasicQueryEngine, clearColumnExistsCache } from '../engine'
|
|
2
2
|
import { registerModules } from '../../i18n/server'
|
|
3
3
|
|
|
4
|
+
// The column-existence answer is memoized on the module, not the instance (#5605), so
|
|
5
|
+
// the per-test fake schemas below would otherwise inherit whatever the first test
|
|
6
|
+
// probed for the same table names.
|
|
7
|
+
beforeEach(() => {
|
|
8
|
+
clearColumnExistsCache()
|
|
9
|
+
})
|
|
10
|
+
|
|
4
11
|
// One entity extension on auth:user so includeExtensions exercises the joined-aggregate path.
|
|
5
12
|
registerModules([
|
|
6
13
|
{ id: 'auth', entityExtensions: [{ base: 'auth:user', extension: 'my_module:user_profile', join: { baseKey: 'id', extensionKey: 'user_id' } }] },
|
|
@@ -139,6 +146,22 @@ function createFakeKysely(selectsSink: any[], overrides?: FakeData) {
|
|
|
139
146
|
}
|
|
140
147
|
|
|
141
148
|
function builderFor(tableArg: any): any {
|
|
149
|
+
if (tableArg && typeof tableArg === 'object' && tableArg._ops) {
|
|
150
|
+
// selectFrom(subquery.as(alias)) — the capped-count probe shape.
|
|
151
|
+
const ops = {
|
|
152
|
+
table: '__subquery__',
|
|
153
|
+
alias: tableArg._ops.alias ?? null,
|
|
154
|
+
subquery: tableArg._ops,
|
|
155
|
+
wheres: [] as any[],
|
|
156
|
+
joins: [] as any[],
|
|
157
|
+
selects: [] as any[],
|
|
158
|
+
orderBys: [] as any[],
|
|
159
|
+
groups: [] as any[],
|
|
160
|
+
limits: 0,
|
|
161
|
+
offsets: 0,
|
|
162
|
+
}
|
|
163
|
+
return makeBuilder(ops, true)
|
|
164
|
+
}
|
|
142
165
|
const parsed = parseTableSpec(tableArg)
|
|
143
166
|
const ops = {
|
|
144
167
|
table: parsed.table,
|
|
@@ -173,8 +196,28 @@ function findCountSql(selectsSink: any[]): string {
|
|
|
173
196
|
return rawSqlText(countExprs[countExprs.length - 1]).toLowerCase()
|
|
174
197
|
}
|
|
175
198
|
|
|
176
|
-
|
|
177
|
-
|
|
199
|
+
// The count query is rebuilt from scope + filters only (#4552 Phase 2): cf
|
|
200
|
+
// filters compile to correlated EXISTS semi-joins, projection joins are
|
|
201
|
+
// dropped, so the count never needs DISTINCT or GROUP BY — completing the
|
|
202
|
+
// direction #2227 started — and the LIMIT cap+1 probe actually bounds the scan.
|
|
203
|
+
describe('BasicQueryEngine — rebuilt list COUNT query (#4552 Phase 2)', () => {
|
|
204
|
+
const originalCap = process.env.OM_LIST_COUNT_CAP
|
|
205
|
+
afterEach(() => {
|
|
206
|
+
if (originalCap === undefined) delete process.env.OM_LIST_COUNT_CAP
|
|
207
|
+
else process.env.OM_LIST_COUNT_CAP = originalCap
|
|
208
|
+
})
|
|
209
|
+
|
|
210
|
+
// The count-shape builder is the base-table call carrying the probe LIMIT
|
|
211
|
+
// (cap + 1); with the cap disabled it is the base call whose recorded select
|
|
212
|
+
// is the bare count aggregate.
|
|
213
|
+
function findCountShapeCall(fakeDb: any, table: string) {
|
|
214
|
+
const baseCalls = fakeDb._calls.filter((b: any) => b._ops.table === table)
|
|
215
|
+
const probed = baseCalls.find((b: any) => b._ops.limits === 10_001)
|
|
216
|
+
if (probed) return probed
|
|
217
|
+
return baseCalls.find((b: any) => b._ops.selects.some((s: any) => aliasName(s) === 'count'))
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
test('uses count(*) with no DISTINCT and no GROUP BY when nothing joins', async () => {
|
|
178
221
|
const selects: any[] = []
|
|
179
222
|
const fakeDb = createFakeKysely(selects)
|
|
180
223
|
const engine = new BasicQueryEngine({} as any, () => fakeDb as any)
|
|
@@ -184,11 +227,11 @@ describe('BasicQueryEngine — list COUNT query (issue #2227)', () => {
|
|
|
184
227
|
expect(countSql).toContain('count(*)')
|
|
185
228
|
expect(countSql).not.toContain('distinct')
|
|
186
229
|
|
|
187
|
-
const
|
|
188
|
-
expect(
|
|
230
|
+
const countCall = findCountShapeCall(fakeDb, 'scheduled_jobs')
|
|
231
|
+
expect(countCall._ops.groups.length).toBe(0)
|
|
189
232
|
})
|
|
190
233
|
|
|
191
|
-
test('
|
|
234
|
+
test('drops extension projection joins from the count: count(*) over the bare base table', async () => {
|
|
192
235
|
const selects: any[] = []
|
|
193
236
|
const fakeDb = createFakeKysely(selects)
|
|
194
237
|
const engine = new BasicQueryEngine({} as any, () => fakeDb as any)
|
|
@@ -201,14 +244,20 @@ describe('BasicQueryEngine — list COUNT query (issue #2227)', () => {
|
|
|
201
244
|
})
|
|
202
245
|
|
|
203
246
|
const countSql = findCountSql(selects)
|
|
204
|
-
expect(countSql).toContain('count(
|
|
205
|
-
expect(countSql).not.toContain('
|
|
247
|
+
expect(countSql).toContain('count(*)')
|
|
248
|
+
expect(countSql).not.toContain('distinct')
|
|
206
249
|
|
|
207
|
-
|
|
208
|
-
|
|
250
|
+
// The display query keeps the extension join + GROUP BY …
|
|
251
|
+
const dataCall = fakeDb._calls.find((b: any) => b._ops.table === 'users' && b._ops.joins.length > 0)
|
|
252
|
+
expect(dataCall._ops.groups.length).toBeGreaterThan(0)
|
|
253
|
+
// … while the count shape carries neither: no join can multiply base rows,
|
|
254
|
+
// so no barrier sits between the probe LIMIT and the scan.
|
|
255
|
+
const countCall = findCountShapeCall(fakeDb, 'users')
|
|
256
|
+
expect(countCall._ops.joins.length).toBe(0)
|
|
257
|
+
expect(countCall._ops.groups.length).toBe(0)
|
|
209
258
|
})
|
|
210
259
|
|
|
211
|
-
test('
|
|
260
|
+
test('explicit relation joins never reach the count: count(*), join filters stay EXISTS', async () => {
|
|
212
261
|
const selects: any[] = []
|
|
213
262
|
const fakeDb = createFakeKysely(selects)
|
|
214
263
|
const engine = new BasicQueryEngine({} as any, () => fakeDb as any)
|
|
@@ -216,14 +265,112 @@ describe('BasicQueryEngine — list COUNT query (issue #2227)', () => {
|
|
|
216
265
|
tenantId: 't1',
|
|
217
266
|
fields: ['id'],
|
|
218
267
|
joins: [{ alias: 'owner', table: 'users', from: { field: 'owner_id' }, to: { field: 'id' } }],
|
|
268
|
+
filters: { 'owner.email': { $eq: 'a@b.c' } },
|
|
269
|
+
page: { page: 1, pageSize: 20 },
|
|
270
|
+
})
|
|
271
|
+
|
|
272
|
+
const countSql = findCountSql(selects)
|
|
273
|
+
expect(countSql).toContain('count(*)')
|
|
274
|
+
expect(countSql).not.toContain('distinct')
|
|
275
|
+
|
|
276
|
+
const countCall = findCountShapeCall(fakeDb, 'scheduled_jobs')
|
|
277
|
+
expect(countCall._ops.groups.length).toBe(0)
|
|
278
|
+
expect(countCall._ops.joins.length).toBe(0)
|
|
279
|
+
// The join filter is a semi-join on the count shape, not a join.
|
|
280
|
+
expect(countCall._ops.wheres.some((w: any) => Array.isArray(w) && w[0] === 'exists')).toBe(true)
|
|
281
|
+
})
|
|
282
|
+
|
|
283
|
+
test('cf filters compile to EXISTS over custom_field_values on the count shape', async () => {
|
|
284
|
+
const selects: any[] = []
|
|
285
|
+
const fakeDb = createFakeKysely(selects, {
|
|
286
|
+
custom_field_defs: [
|
|
287
|
+
{ key: 'color', entity_id: 'scheduler:scheduled_job', is_active: true, tenant_id: null, kind: 'text', config_json: '{}' },
|
|
288
|
+
],
|
|
289
|
+
})
|
|
290
|
+
const engine = new BasicQueryEngine({} as any, () => fakeDb as any)
|
|
291
|
+
await engine.query('scheduler:scheduled_job', {
|
|
292
|
+
tenantId: 't1',
|
|
293
|
+
fields: ['id'],
|
|
294
|
+
filters: { cf_color: { $eq: 'red' } },
|
|
295
|
+
page: { page: 1, pageSize: 20 },
|
|
296
|
+
})
|
|
297
|
+
|
|
298
|
+
const countSql = findCountSql(selects)
|
|
299
|
+
expect(countSql).toContain('count(*)')
|
|
300
|
+
expect(countSql).not.toContain('distinct')
|
|
301
|
+
|
|
302
|
+
const countCall = findCountShapeCall(fakeDb, 'scheduled_jobs')
|
|
303
|
+
expect(countCall._ops.groups.length).toBe(0)
|
|
304
|
+
// No custom_field_values join on the count shape — the filter is an EXISTS
|
|
305
|
+
// whose subquery selects from custom_field_values.
|
|
306
|
+
expect(countCall._ops.joins.length).toBe(0)
|
|
307
|
+
const existsEntries = countCall._ops.wheres.filter((w: any) => Array.isArray(w) && w[0] === 'exists')
|
|
308
|
+
expect(existsEntries.length).toBeGreaterThan(0)
|
|
309
|
+
const existsSub = existsEntries[0][1]
|
|
310
|
+
expect(existsSub._ops.table).toBe('custom_field_values')
|
|
311
|
+
// The display query still resolves the same filter through its value join.
|
|
312
|
+
const dataCall = fakeDb._calls.find((b: any) =>
|
|
313
|
+
b._ops.table === 'scheduled_jobs' &&
|
|
314
|
+
b._ops.joins.some((j: any) => Object.values(j.aliasObj).includes('custom_field_values')))
|
|
315
|
+
expect(dataCall).toBeTruthy()
|
|
316
|
+
})
|
|
317
|
+
|
|
318
|
+
test('a cf leaf inside $or compiles to EXISTS on the count shape instead of being dropped (#5039)', async () => {
|
|
319
|
+
const selects: any[] = []
|
|
320
|
+
const fakeDb = createFakeKysely(selects, {
|
|
321
|
+
custom_field_defs: [
|
|
322
|
+
{ key: 'color', entity_id: 'scheduler:scheduled_job', is_active: true, tenant_id: null, kind: 'text', config_json: '{}' },
|
|
323
|
+
],
|
|
324
|
+
'information_schema.columns': [
|
|
325
|
+
{ table_name: 'scheduled_jobs', column_name: 'id' },
|
|
326
|
+
{ table_name: 'scheduled_jobs', column_name: 'tenant_id' },
|
|
327
|
+
{ table_name: 'scheduled_jobs', column_name: 'status' },
|
|
328
|
+
],
|
|
329
|
+
})
|
|
330
|
+
const engine = new BasicQueryEngine({} as any, () => fakeDb as any)
|
|
331
|
+
await engine.query('scheduler:scheduled_job', {
|
|
332
|
+
tenantId: 't1',
|
|
333
|
+
fields: ['id'],
|
|
334
|
+
filters: { $or: [{ status: { $eq: 'closed' } }, { cf_color: { $eq: 'red' } }] },
|
|
219
335
|
page: { page: 1, pageSize: 20 },
|
|
220
336
|
})
|
|
221
337
|
|
|
338
|
+
const countCall = findCountShapeCall(fakeDb, 'scheduled_jobs')
|
|
339
|
+
expect(countCall._ops.joins.length).toBe(0)
|
|
340
|
+
// The whole disjunction lands in one OR where-entry whose parts include an
|
|
341
|
+
// EXISTS (the cf leaf) — dropping it would narrow the OR and undercount.
|
|
342
|
+
const orEntries = countCall._ops.wheres.filter((w: any) => Array.isArray(w) && w[0] === 'or')
|
|
343
|
+
expect(orEntries.length).toBeGreaterThan(0)
|
|
344
|
+
const hasExistsPart = orEntries.some((entry: any) =>
|
|
345
|
+
(entry[1] ?? []).some((part: any) => part?.kind === 'exists' || (part?.kind === 'and' && part.parts?.some((p: any) => p?.kind === 'exists'))))
|
|
346
|
+
expect(hasExistsPart).toBe(true)
|
|
347
|
+
})
|
|
348
|
+
|
|
349
|
+
test('the probe LIMIT is cap + 1 on the row-producing inner query, counted by an outer aggregate', async () => {
|
|
350
|
+
process.env.OM_LIST_COUNT_CAP = '100'
|
|
351
|
+
const selects: any[] = []
|
|
352
|
+
const fakeDb = createFakeKysely(selects)
|
|
353
|
+
const engine = new BasicQueryEngine({} as any, () => fakeDb as any)
|
|
354
|
+
await engine.query('scheduler:scheduled_job', { tenantId: 't1', fields: ['id'], page: { page: 1, pageSize: 20 } })
|
|
355
|
+
|
|
356
|
+
const outer = fakeDb._calls.find((b: any) => b._ops.table === '__subquery__')
|
|
357
|
+
expect(outer).toBeTruthy()
|
|
358
|
+
expect(outer._ops.subquery.limits).toBe(101)
|
|
359
|
+
expect(outer._ops.subquery.groups.length).toBe(0)
|
|
222
360
|
const countSql = findCountSql(selects)
|
|
223
|
-
expect(countSql).toContain('count(
|
|
224
|
-
|
|
361
|
+
expect(countSql).toContain('count(*)')
|
|
362
|
+
})
|
|
225
363
|
|
|
226
|
-
|
|
227
|
-
|
|
364
|
+
test('OM_LIST_COUNT_CAP=0 disables the probe: a direct unbounded count(*)', async () => {
|
|
365
|
+
process.env.OM_LIST_COUNT_CAP = '0'
|
|
366
|
+
const selects: any[] = []
|
|
367
|
+
const fakeDb = createFakeKysely(selects)
|
|
368
|
+
const engine = new BasicQueryEngine({} as any, () => fakeDb as any)
|
|
369
|
+
const result = await engine.query('scheduler:scheduled_job', { tenantId: 't1', fields: ['id'], page: { page: 1, pageSize: 20 } })
|
|
370
|
+
|
|
371
|
+
expect(fakeDb._calls.some((b: any) => b._ops.table === '__subquery__')).toBe(false)
|
|
372
|
+
const countSql = findCountSql(selects)
|
|
373
|
+
expect(countSql).toContain('count(*)')
|
|
374
|
+
expect(result.meta?.listCountCapWarning).toBeUndefined()
|
|
228
375
|
})
|
|
229
376
|
})
|
|
@@ -1,6 +1,16 @@
|
|
|
1
|
-
import { BasicQueryEngine } from '../engine'
|
|
1
|
+
import { BasicQueryEngine, clearColumnExistsCache } from '../engine'
|
|
2
2
|
import { normalizeFilters } from '../join-utils'
|
|
3
3
|
|
|
4
|
+
// The column-existence answer is memoized on the module, not the instance (#5605), and
|
|
5
|
+
// the fixtures below declare mutually contradictory `information_schema.columns` shapes
|
|
6
|
+
// for the same `scheduled_jobs` table. Without this clear, whichever test ran first
|
|
7
|
+
// decides the scoping every later test sees — the automatic tenant/organization guard
|
|
8
|
+
// would silently drop out of a query and the assertion for it would still be reading a
|
|
9
|
+
// stale `false`.
|
|
10
|
+
beforeEach(() => {
|
|
11
|
+
clearColumnExistsCache()
|
|
12
|
+
})
|
|
13
|
+
|
|
4
14
|
type FakeData = Record<string, any[]>
|
|
5
15
|
|
|
6
16
|
function cloneRows(rows: any[] | undefined): any[] {
|