@open-mercato/shared 0.4.2-canary-c02407ff85
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/build.mjs +101 -0
- package/dist/index.js +1 -0
- package/dist/index.js.map +7 -0
- package/dist/lib/api/crud.js +47 -0
- package/dist/lib/api/crud.js.map +7 -0
- package/dist/lib/api/scoped.js +140 -0
- package/dist/lib/api/scoped.js.map +7 -0
- package/dist/lib/auth/jwt.js +34 -0
- package/dist/lib/auth/jwt.js.map +7 -0
- package/dist/lib/auth/server.js +157 -0
- package/dist/lib/auth/server.js.map +7 -0
- package/dist/lib/boolean.js +22 -0
- package/dist/lib/boolean.js.map +7 -0
- package/dist/lib/bootstrap/appResolver.js +43 -0
- package/dist/lib/bootstrap/appResolver.js.map +7 -0
- package/dist/lib/bootstrap/dynamicLoader.js +108 -0
- package/dist/lib/bootstrap/dynamicLoader.js.map +7 -0
- package/dist/lib/bootstrap/factory.js +59 -0
- package/dist/lib/bootstrap/factory.js.map +7 -0
- package/dist/lib/bootstrap/index.js +11 -0
- package/dist/lib/bootstrap/index.js.map +7 -0
- package/dist/lib/bootstrap/types.js +1 -0
- package/dist/lib/bootstrap/types.js.map +7 -0
- package/dist/lib/cache/segments.js +36 -0
- package/dist/lib/cache/segments.js.map +7 -0
- package/dist/lib/cli/progress.js +46 -0
- package/dist/lib/cli/progress.js.map +7 -0
- package/dist/lib/commands/command-bus.js +285 -0
- package/dist/lib/commands/command-bus.js.map +7 -0
- package/dist/lib/commands/customFieldSnapshots.js +66 -0
- package/dist/lib/commands/customFieldSnapshots.js.map +7 -0
- package/dist/lib/commands/helpers.js +98 -0
- package/dist/lib/commands/helpers.js.map +7 -0
- package/dist/lib/commands/index.js +8 -0
- package/dist/lib/commands/index.js.map +7 -0
- package/dist/lib/commands/operationMetadata.js +32 -0
- package/dist/lib/commands/operationMetadata.js.map +7 -0
- package/dist/lib/commands/registry.js +43 -0
- package/dist/lib/commands/registry.js.map +7 -0
- package/dist/lib/commands/scope.js +44 -0
- package/dist/lib/commands/scope.js.map +7 -0
- package/dist/lib/commands/types.js +8 -0
- package/dist/lib/commands/types.js.map +7 -0
- package/dist/lib/crud/cache-stats.js +98 -0
- package/dist/lib/crud/cache-stats.js.map +7 -0
- package/dist/lib/crud/cache.js +175 -0
- package/dist/lib/crud/cache.js.map +7 -0
- package/dist/lib/crud/custom-fields-client.js +52 -0
- package/dist/lib/crud/custom-fields-client.js.map +7 -0
- package/dist/lib/crud/custom-fields.js +467 -0
- package/dist/lib/crud/custom-fields.js.map +7 -0
- package/dist/lib/crud/errors.js +24 -0
- package/dist/lib/crud/errors.js.map +7 -0
- package/dist/lib/crud/exporters.js +154 -0
- package/dist/lib/crud/exporters.js.map +7 -0
- package/dist/lib/crud/factory.js +1311 -0
- package/dist/lib/crud/factory.js.map +7 -0
- package/dist/lib/crud/types.js +1 -0
- package/dist/lib/crud/types.js.map +7 -0
- package/dist/lib/custom-fields/normalize.js +36 -0
- package/dist/lib/custom-fields/normalize.js.map +7 -0
- package/dist/lib/data/engine.js +396 -0
- package/dist/lib/data/engine.js.map +7 -0
- package/dist/lib/db/escapeLikePattern.js +5 -0
- package/dist/lib/db/escapeLikePattern.js.map +7 -0
- package/dist/lib/db/mikro.js +82 -0
- package/dist/lib/db/mikro.js.map +7 -0
- package/dist/lib/di/container.js +94 -0
- package/dist/lib/di/container.js.map +7 -0
- package/dist/lib/email/send.js +12 -0
- package/dist/lib/email/send.js.map +7 -0
- package/dist/lib/encryption/aes.js +58 -0
- package/dist/lib/encryption/aes.js.map +7 -0
- package/dist/lib/encryption/customFieldValues.js +49 -0
- package/dist/lib/encryption/customFieldValues.js.map +7 -0
- package/dist/lib/encryption/entityFields.js +26 -0
- package/dist/lib/encryption/entityFields.js.map +7 -0
- package/dist/lib/encryption/entityIds.js +80 -0
- package/dist/lib/encryption/entityIds.js.map +7 -0
- package/dist/lib/encryption/find.js +45 -0
- package/dist/lib/encryption/find.js.map +7 -0
- package/dist/lib/encryption/indexDoc.js +69 -0
- package/dist/lib/encryption/indexDoc.js.map +7 -0
- package/dist/lib/encryption/kms.js +282 -0
- package/dist/lib/encryption/kms.js.map +7 -0
- package/dist/lib/encryption/subscriber.js +330 -0
- package/dist/lib/encryption/subscriber.js.map +7 -0
- package/dist/lib/encryption/tenantDataEncryptionService.js +252 -0
- package/dist/lib/encryption/tenantDataEncryptionService.js.map +7 -0
- package/dist/lib/encryption/toggles.js +18 -0
- package/dist/lib/encryption/toggles.js.map +7 -0
- package/dist/lib/entities/naming.js +9 -0
- package/dist/lib/entities/naming.js.map +7 -0
- package/dist/lib/entities/system-entities.js +43 -0
- package/dist/lib/entities/system-entities.js.map +7 -0
- package/dist/lib/frontend/organizationEvents.js +41 -0
- package/dist/lib/frontend/organizationEvents.js.map +7 -0
- package/dist/lib/frontend/useOrganizationScope.js +32 -0
- package/dist/lib/frontend/useOrganizationScope.js.map +7 -0
- package/dist/lib/hotkeys/index.js +128 -0
- package/dist/lib/hotkeys/index.js.map +7 -0
- package/dist/lib/i18n/app-dictionaries.js +17 -0
- package/dist/lib/i18n/app-dictionaries.js.map +7 -0
- package/dist/lib/i18n/config.js +7 -0
- package/dist/lib/i18n/config.js.map +7 -0
- package/dist/lib/i18n/context.js +50 -0
- package/dist/lib/i18n/context.js.map +7 -0
- package/dist/lib/i18n/server.js +68 -0
- package/dist/lib/i18n/server.js.map +7 -0
- package/dist/lib/i18n/translate.js +45 -0
- package/dist/lib/i18n/translate.js.map +7 -0
- package/dist/lib/indexers/error-log.js +82 -0
- package/dist/lib/indexers/error-log.js.map +7 -0
- package/dist/lib/indexers/status-log.js +80 -0
- package/dist/lib/indexers/status-log.js.map +7 -0
- package/dist/lib/lib/auth/jwt.js +34 -0
- package/dist/lib/lib/auth/jwt.js.map +7 -0
- package/dist/lib/lib/auth/server.js +77 -0
- package/dist/lib/lib/auth/server.js.map +7 -0
- package/dist/lib/lib/email/send.js +12 -0
- package/dist/lib/lib/email/send.js.map +7 -0
- package/dist/lib/lib/i18n/config.js +7 -0
- package/dist/lib/lib/i18n/config.js.map +7 -0
- package/dist/lib/lib/i18n/context.js +31 -0
- package/dist/lib/lib/i18n/context.js.map +7 -0
- package/dist/lib/lib/utils.js +9 -0
- package/dist/lib/lib/utils.js.map +7 -0
- package/dist/lib/location/countries.js +68 -0
- package/dist/lib/location/countries.js.map +7 -0
- package/dist/lib/modules/index.js +6 -0
- package/dist/lib/modules/index.js.map +7 -0
- package/dist/lib/modules/registry.js +18 -0
- package/dist/lib/modules/registry.js.map +7 -0
- package/dist/lib/openapi/crud.js +137 -0
- package/dist/lib/openapi/crud.js.map +7 -0
- package/dist/lib/openapi/generator.js +1131 -0
- package/dist/lib/openapi/generator.js.map +7 -0
- package/dist/lib/openapi/index.js +10 -0
- package/dist/lib/openapi/index.js.map +7 -0
- package/dist/lib/openapi/sanitize.js +110 -0
- package/dist/lib/openapi/sanitize.js.map +7 -0
- package/dist/lib/openapi/types.js +1 -0
- package/dist/lib/openapi/types.js.map +7 -0
- package/dist/lib/profiler/index.js +258 -0
- package/dist/lib/profiler/index.js.map +7 -0
- package/dist/lib/query/engine.js +729 -0
- package/dist/lib/query/engine.js.map +7 -0
- package/dist/lib/query/join-utils.js +195 -0
- package/dist/lib/query/join-utils.js.map +7 -0
- package/dist/lib/query/types.js +9 -0
- package/dist/lib/query/types.js.map +7 -0
- package/dist/lib/search/config.js +32 -0
- package/dist/lib/search/config.js.map +7 -0
- package/dist/lib/search/tokenize.js +34 -0
- package/dist/lib/search/tokenize.js.map +7 -0
- package/dist/lib/slugify.js +24 -0
- package/dist/lib/slugify.js.map +7 -0
- package/dist/lib/testing/bootstrap.js +51 -0
- package/dist/lib/testing/bootstrap.js.map +7 -0
- package/dist/lib/testing/index.js +17 -0
- package/dist/lib/testing/index.js.map +7 -0
- package/dist/lib/testing/renderWithProviders.js +15 -0
- package/dist/lib/testing/renderWithProviders.js.map +7 -0
- package/dist/lib/url.js +12 -0
- package/dist/lib/url.js.map +7 -0
- package/dist/lib/utils.js +13 -0
- package/dist/lib/utils.js.map +7 -0
- package/dist/lib/version.js +7 -0
- package/dist/lib/version.js.map +7 -0
- package/dist/modules/dashboard/widgets.js +1 -0
- package/dist/modules/dashboard/widgets.js.map +7 -0
- package/dist/modules/dsl.js +30 -0
- package/dist/modules/dsl.js.map +7 -0
- package/dist/modules/entities/kinds.js +22 -0
- package/dist/modules/entities/kinds.js.map +7 -0
- package/dist/modules/entities/options.js +26 -0
- package/dist/modules/entities/options.js.map +7 -0
- package/dist/modules/entities/validation.js +102 -0
- package/dist/modules/entities/validation.js.map +7 -0
- package/dist/modules/entities/validators.js +88 -0
- package/dist/modules/entities/validators.js.map +7 -0
- package/dist/modules/entities.js +1 -0
- package/dist/modules/entities.js.map +7 -0
- package/dist/modules/navigation/sidebarPreferences.js +50 -0
- package/dist/modules/navigation/sidebarPreferences.js.map +7 -0
- package/dist/modules/perspectives/types.js +1 -0
- package/dist/modules/perspectives/types.js.map +7 -0
- package/dist/modules/registry.js +96 -0
- package/dist/modules/registry.js.map +7 -0
- package/dist/modules/search.js +15 -0
- package/dist/modules/search.js.map +7 -0
- package/dist/modules/vector.js +1 -0
- package/dist/modules/vector.js.map +7 -0
- package/dist/modules/widgets/injection-loader.js +180 -0
- package/dist/modules/widgets/injection-loader.js.map +7 -0
- package/dist/modules/widgets/injection.js +1 -0
- package/dist/modules/widgets/injection.js.map +7 -0
- package/dist/security/features.js +23 -0
- package/dist/security/features.js.map +7 -0
- package/dist/types/pg.d.js +1 -0
- package/dist/types/pg.d.js.map +7 -0
- package/dist/types/react-email.d.js +1 -0
- package/dist/types/react-email.d.js.map +7 -0
- package/dist/types/resend.d.js +1 -0
- package/dist/types/resend.d.js.map +7 -0
- package/jest.config.cjs +22 -0
- package/package.json +88 -0
- package/src/index.ts +0 -0
- package/src/lib/api/__tests__/scoped.test.ts +38 -0
- package/src/lib/api/crud.ts +59 -0
- package/src/lib/api/scoped.ts +239 -0
- package/src/lib/auth/jwt.ts +39 -0
- package/src/lib/auth/server.ts +199 -0
- package/src/lib/boolean.ts +17 -0
- package/src/lib/bootstrap/appResolver.ts +85 -0
- package/src/lib/bootstrap/dynamicLoader.ts +177 -0
- package/src/lib/bootstrap/factory.ts +108 -0
- package/src/lib/bootstrap/index.ts +23 -0
- package/src/lib/bootstrap/types.ts +31 -0
- package/src/lib/cache/segments.ts +56 -0
- package/src/lib/cli/progress.ts +55 -0
- package/src/lib/commands/__tests__/command-bus.test.ts +84 -0
- package/src/lib/commands/__tests__/helpers.test.ts +42 -0
- package/src/lib/commands/command-bus.ts +349 -0
- package/src/lib/commands/customFieldSnapshots.ts +86 -0
- package/src/lib/commands/helpers.ts +143 -0
- package/src/lib/commands/index.ts +4 -0
- package/src/lib/commands/operationMetadata.ts +40 -0
- package/src/lib/commands/registry.ts +46 -0
- package/src/lib/commands/scope.ts +59 -0
- package/src/lib/commands/types.ts +63 -0
- package/src/lib/crud/__tests__/crud-factory.test.ts +333 -0
- package/src/lib/crud/__tests__/custom-fields.test.ts +150 -0
- package/src/lib/crud/cache-stats.ts +127 -0
- package/src/lib/crud/cache.ts +205 -0
- package/src/lib/crud/custom-fields-client.ts +54 -0
- package/src/lib/crud/custom-fields.ts +607 -0
- package/src/lib/crud/errors.ts +23 -0
- package/src/lib/crud/exporters.ts +188 -0
- package/src/lib/crud/factory.ts +1622 -0
- package/src/lib/crud/types.ts +29 -0
- package/src/lib/custom-fields/normalize.ts +45 -0
- package/src/lib/data/engine.ts +562 -0
- package/src/lib/db/escapeLikePattern.ts +2 -0
- package/src/lib/db/mikro.ts +100 -0
- package/src/lib/di/container.ts +105 -0
- package/src/lib/email/send.ts +18 -0
- package/src/lib/encryption/__tests__/customFieldValues.test.ts +63 -0
- package/src/lib/encryption/__tests__/indexDoc.test.ts +115 -0
- package/src/lib/encryption/aes.ts +64 -0
- package/src/lib/encryption/customFieldValues.ts +67 -0
- package/src/lib/encryption/entityFields.ts +39 -0
- package/src/lib/encryption/entityIds.ts +107 -0
- package/src/lib/encryption/find.ts +81 -0
- package/src/lib/encryption/indexDoc.ts +104 -0
- package/src/lib/encryption/kms.ts +337 -0
- package/src/lib/encryption/subscriber.ts +416 -0
- package/src/lib/encryption/tenantDataEncryptionService.ts +313 -0
- package/src/lib/encryption/toggles.ts +15 -0
- package/src/lib/entities/naming.ts +6 -0
- package/src/lib/entities/system-entities.ts +43 -0
- package/src/lib/frontend/organizationEvents.ts +55 -0
- package/src/lib/frontend/useOrganizationScope.ts +30 -0
- package/src/lib/hotkeys/index.ts +168 -0
- package/src/lib/i18n/app-dictionaries.ts +18 -0
- package/src/lib/i18n/config.ts +4 -0
- package/src/lib/i18n/context.tsx +66 -0
- package/src/lib/i18n/server.ts +74 -0
- package/src/lib/i18n/translate.ts +54 -0
- package/src/lib/indexers/error-log.ts +106 -0
- package/src/lib/indexers/status-log.ts +119 -0
- package/src/lib/lib/auth/jwt.ts +39 -0
- package/src/lib/lib/auth/server.ts +94 -0
- package/src/lib/lib/email/send.ts +18 -0
- package/src/lib/lib/i18n/config.ts +4 -0
- package/src/lib/lib/i18n/context.tsx +38 -0
- package/src/lib/lib/utils.ts +6 -0
- package/src/lib/location/countries.ts +97 -0
- package/src/lib/modules/index.ts +1 -0
- package/src/lib/modules/registry.ts +18 -0
- package/src/lib/openapi/crud.ts +218 -0
- package/src/lib/openapi/generator.ts +1311 -0
- package/src/lib/openapi/index.ts +4 -0
- package/src/lib/openapi/sanitize.ts +137 -0
- package/src/lib/openapi/types.ts +79 -0
- package/src/lib/profiler/index.ts +371 -0
- package/src/lib/query/__tests__/engine.test.ts +274 -0
- package/src/lib/query/engine.ts +837 -0
- package/src/lib/query/join-utils.ts +238 -0
- package/src/lib/query/types.ts +121 -0
- package/src/lib/search/config.ts +49 -0
- package/src/lib/search/tokenize.ts +45 -0
- package/src/lib/slugify.ts +28 -0
- package/src/lib/testing/bootstrap.ts +124 -0
- package/src/lib/testing/index.ts +15 -0
- package/src/lib/testing/renderWithProviders.tsx +31 -0
- package/src/lib/url.ts +12 -0
- package/src/lib/utils.ts +17 -0
- package/src/lib/version.ts +5 -0
- package/src/modules/__tests__/dsl.test.ts +35 -0
- package/src/modules/__tests__/registry.test.ts +300 -0
- package/src/modules/dashboard/widgets.ts +57 -0
- package/src/modules/dsl.ts +32 -0
- package/src/modules/entities/__tests__/validation.test.ts +52 -0
- package/src/modules/entities/kinds.ts +20 -0
- package/src/modules/entities/options.ts +36 -0
- package/src/modules/entities/validation.ts +118 -0
- package/src/modules/entities/validators.ts +93 -0
- package/src/modules/entities.ts +102 -0
- package/src/modules/navigation/sidebarPreferences.ts +62 -0
- package/src/modules/perspectives/types.ts +40 -0
- package/src/modules/registry.ts +249 -0
- package/src/modules/search.ts +325 -0
- package/src/modules/vector.ts +122 -0
- package/src/modules/widgets/__tests__/injection.test.ts +48 -0
- package/src/modules/widgets/injection-loader.ts +235 -0
- package/src/modules/widgets/injection.ts +120 -0
- package/src/security/features.ts +22 -0
- package/src/types/pg.d.ts +2 -0
- package/src/types/react-email.d.ts +2 -0
- package/src/types/resend.d.ts +2 -0
- package/tsconfig.build.json +11 -0
- package/tsconfig.json +9 -0
- package/watch.mjs +6 -0
|
@@ -0,0 +1,238 @@
|
|
|
1
|
+
import type { Knex } from 'knex'
|
|
2
|
+
import type { QueryOptions, QueryJoinEdge } from './types'
|
|
3
|
+
import type { FilterOp } from './types'
|
|
4
|
+
|
|
5
|
+
export type NormalizedFilter = { field: string; op: FilterOp; value?: unknown }
|
|
6
|
+
|
|
7
|
+
export function normalizeFilters(filters?: QueryOptions['filters']): NormalizedFilter[] {
|
|
8
|
+
if (!filters) return []
|
|
9
|
+
const normalizeField = (key: string) => (key.startsWith('cf_') ? `cf:${key.slice(3)}` : key)
|
|
10
|
+
if (Array.isArray(filters)) {
|
|
11
|
+
return (filters as any[]).map((f) => ({
|
|
12
|
+
...f,
|
|
13
|
+
field: normalizeField(String((f as any).field)),
|
|
14
|
+
}))
|
|
15
|
+
}
|
|
16
|
+
const out: NormalizedFilter[] = []
|
|
17
|
+
const obj = filters as Record<string, unknown>
|
|
18
|
+
const push = (field: string, op: FilterOp, value?: unknown) => {
|
|
19
|
+
out.push({ field, op, value })
|
|
20
|
+
}
|
|
21
|
+
for (const [rawKey, rawVal] of Object.entries(obj)) {
|
|
22
|
+
const field = normalizeField(rawKey)
|
|
23
|
+
if (rawVal !== null && typeof rawVal === 'object' && !Array.isArray(rawVal)) {
|
|
24
|
+
for (const [opKey, opVal] of Object.entries(rawVal as Record<string, unknown>)) {
|
|
25
|
+
switch (opKey) {
|
|
26
|
+
case '$eq':
|
|
27
|
+
push(field, 'eq', opVal)
|
|
28
|
+
break
|
|
29
|
+
case '$ne':
|
|
30
|
+
push(field, 'ne', opVal)
|
|
31
|
+
break
|
|
32
|
+
case '$gt':
|
|
33
|
+
push(field, 'gt', opVal)
|
|
34
|
+
break
|
|
35
|
+
case '$gte':
|
|
36
|
+
push(field, 'gte', opVal)
|
|
37
|
+
break
|
|
38
|
+
case '$lt':
|
|
39
|
+
push(field, 'lt', opVal)
|
|
40
|
+
break
|
|
41
|
+
case '$lte':
|
|
42
|
+
push(field, 'lte', opVal)
|
|
43
|
+
break
|
|
44
|
+
case '$in':
|
|
45
|
+
push(field, 'in', opVal)
|
|
46
|
+
break
|
|
47
|
+
case '$nin':
|
|
48
|
+
push(field, 'nin', opVal)
|
|
49
|
+
break
|
|
50
|
+
case '$like':
|
|
51
|
+
push(field, 'like', opVal)
|
|
52
|
+
break
|
|
53
|
+
case '$ilike':
|
|
54
|
+
push(field, 'ilike', opVal)
|
|
55
|
+
break
|
|
56
|
+
case '$exists':
|
|
57
|
+
push(field, 'exists', opVal)
|
|
58
|
+
break
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
} else {
|
|
62
|
+
push(field, 'eq', rawVal)
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
return out
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
export type ResolvedJoin = {
|
|
69
|
+
alias: string
|
|
70
|
+
table: string
|
|
71
|
+
fromAlias: string
|
|
72
|
+
fromField: string
|
|
73
|
+
toField: string
|
|
74
|
+
type: 'left' | 'inner'
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
export type BaseFilter = NormalizedFilter & { qualified?: string }
|
|
78
|
+
export type JoinFilter = { alias: string; column: string; op: FilterOp; value?: unknown }
|
|
79
|
+
|
|
80
|
+
export function resolveJoins(
|
|
81
|
+
baseTable: string,
|
|
82
|
+
joins: QueryJoinEdge[] | null | undefined,
|
|
83
|
+
resolveTable: (entityId: string) => string | null,
|
|
84
|
+
): ResolvedJoin[] {
|
|
85
|
+
if (!joins || joins.length === 0) return []
|
|
86
|
+
const resolved: ResolvedJoin[] = []
|
|
87
|
+
const seen = new Set<string>()
|
|
88
|
+
for (const entry of joins) {
|
|
89
|
+
if (!entry || typeof entry !== 'object') continue
|
|
90
|
+
const alias = typeof entry.alias === 'string' ? entry.alias.trim() : ''
|
|
91
|
+
if (!alias) continue
|
|
92
|
+
if (seen.has(alias)) continue
|
|
93
|
+
const table =
|
|
94
|
+
entry.table ??
|
|
95
|
+
(entry.entityId ? resolveTable(String(entry.entityId)) : null)
|
|
96
|
+
if (!table) continue
|
|
97
|
+
const fromField = entry.from?.field?.trim()
|
|
98
|
+
const toField = entry.to?.field?.trim()
|
|
99
|
+
if (!fromField || !toField) continue
|
|
100
|
+
const fromAliasRaw = entry.from?.alias?.trim()
|
|
101
|
+
const fromAlias = fromAliasRaw && fromAliasRaw.length > 0 ? fromAliasRaw : 'base'
|
|
102
|
+
const type: 'left' | 'inner' = entry.type === 'inner' ? 'inner' : 'left'
|
|
103
|
+
resolved.push({ alias, table, fromAlias, fromField, toField, type })
|
|
104
|
+
seen.add(alias)
|
|
105
|
+
}
|
|
106
|
+
return resolved
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
export function buildJoinChain(
|
|
110
|
+
alias: string,
|
|
111
|
+
joinMap: Map<string, ResolvedJoin>,
|
|
112
|
+
baseTable: string,
|
|
113
|
+
visited: Set<string> = new Set(),
|
|
114
|
+
): ResolvedJoin[] {
|
|
115
|
+
if (visited.has(alias)) {
|
|
116
|
+
throw new Error(`QueryEngine: circular join reference detected for alias ${alias}`)
|
|
117
|
+
}
|
|
118
|
+
const cfg = joinMap.get(alias)
|
|
119
|
+
if (!cfg) return []
|
|
120
|
+
visited.add(alias)
|
|
121
|
+
if (!cfg.fromAlias || cfg.fromAlias === 'base' || cfg.fromAlias === baseTable) {
|
|
122
|
+
return [cfg]
|
|
123
|
+
}
|
|
124
|
+
const parentChain = buildJoinChain(cfg.fromAlias, joinMap, baseTable, visited)
|
|
125
|
+
if (parentChain.length === 0) return []
|
|
126
|
+
return [...parentChain, cfg]
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
export function partitionFilters(
|
|
130
|
+
baseTable: string,
|
|
131
|
+
filters: NormalizedFilter[],
|
|
132
|
+
joinMap: Map<string, ResolvedJoin>,
|
|
133
|
+
): { baseFilters: BaseFilter[]; joinFilters: Map<string, JoinFilter[]> } {
|
|
134
|
+
const baseFilters: BaseFilter[] = []
|
|
135
|
+
const joinFilters = new Map<string, JoinFilter[]>()
|
|
136
|
+
for (const filter of filters) {
|
|
137
|
+
const field = String(filter.field)
|
|
138
|
+
if (field.startsWith('cf:')) continue
|
|
139
|
+
const parts = field.split('.')
|
|
140
|
+
if (parts.length === 2) {
|
|
141
|
+
const [aliasNameRaw, column] = parts
|
|
142
|
+
const aliasName = aliasNameRaw || ''
|
|
143
|
+
if (joinMap.has(aliasName)) {
|
|
144
|
+
const list = joinFilters.get(aliasName) ?? []
|
|
145
|
+
list.push({ alias: aliasName, column, op: filter.op, value: filter.value })
|
|
146
|
+
joinFilters.set(aliasName, list)
|
|
147
|
+
continue
|
|
148
|
+
}
|
|
149
|
+
if (aliasName === baseTable || aliasName === 'base') {
|
|
150
|
+
baseFilters.push({
|
|
151
|
+
field: column,
|
|
152
|
+
op: filter.op,
|
|
153
|
+
value: filter.value,
|
|
154
|
+
qualified: `${baseTable}.${column}`,
|
|
155
|
+
})
|
|
156
|
+
continue
|
|
157
|
+
}
|
|
158
|
+
}
|
|
159
|
+
baseFilters.push({ ...filter })
|
|
160
|
+
}
|
|
161
|
+
return { baseFilters, joinFilters }
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
type ApplyJoinFiltersOptions = {
|
|
165
|
+
knex: Knex
|
|
166
|
+
baseTable: string
|
|
167
|
+
builder: Knex.QueryBuilder
|
|
168
|
+
joinMap: Map<string, ResolvedJoin>
|
|
169
|
+
joinFilters: Map<string, JoinFilter[]>
|
|
170
|
+
aliasTables: Map<string, string>
|
|
171
|
+
qualifyBase: (column: string) => string
|
|
172
|
+
applyAliasScope: (builder: Knex.QueryBuilder, alias: string, table: string) => Promise<void> | void
|
|
173
|
+
applyFilterOp: (builder: Knex.QueryBuilder, column: string, op: FilterOp, value?: unknown) => void
|
|
174
|
+
columnExists?: (table: string, column: string) => Promise<boolean> | boolean
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
export async function applyJoinFilters({
|
|
178
|
+
knex,
|
|
179
|
+
baseTable,
|
|
180
|
+
builder,
|
|
181
|
+
joinMap,
|
|
182
|
+
joinFilters,
|
|
183
|
+
aliasTables,
|
|
184
|
+
qualifyBase,
|
|
185
|
+
applyAliasScope,
|
|
186
|
+
applyFilterOp,
|
|
187
|
+
columnExists,
|
|
188
|
+
}: ApplyJoinFiltersOptions): Promise<Knex.QueryBuilder> {
|
|
189
|
+
const resolveAliasName = (aliasName?: string | null) => {
|
|
190
|
+
if (!aliasName || aliasName === 'base') return baseTable
|
|
191
|
+
return aliasName
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
for (const [alias, filtersForAlias] of joinFilters.entries()) {
|
|
195
|
+
const chain = buildJoinChain(alias, joinMap, baseTable)
|
|
196
|
+
if (!chain.length) continue
|
|
197
|
+
const first = chain[0]
|
|
198
|
+
const sub = knex({ [first.alias]: first.table }).select(1)
|
|
199
|
+
await applyAliasScope(sub, first.alias, first.table)
|
|
200
|
+
const parentAlias = resolveAliasName(first.fromAlias)
|
|
201
|
+
if (parentAlias === baseTable) {
|
|
202
|
+
sub.whereRaw('?? = ??', [`${first.alias}.${first.toField}`, qualifyBase(first.fromField)])
|
|
203
|
+
} else {
|
|
204
|
+
sub.whereRaw('?? = ??', [`${first.alias}.${first.toField}`, `${parentAlias}.${first.fromField}`])
|
|
205
|
+
}
|
|
206
|
+
for (const cfg of chain.slice(1)) {
|
|
207
|
+
const joinArgs = { [cfg.alias]: cfg.table }
|
|
208
|
+
const parent = resolveAliasName(cfg.fromAlias)
|
|
209
|
+
const joinFn = function (this: Knex.JoinClause) {
|
|
210
|
+
const left = `${cfg.alias}.${cfg.toField}`
|
|
211
|
+
const right = parent === baseTable ? qualifyBase(cfg.fromField) : `${parent}.${cfg.fromField}`
|
|
212
|
+
this.on(knex.raw('?? = ??', [left, right]))
|
|
213
|
+
}
|
|
214
|
+
if (cfg.type === 'inner') sub.join(joinArgs, joinFn)
|
|
215
|
+
else sub.leftJoin(joinArgs, joinFn)
|
|
216
|
+
await applyAliasScope(sub, cfg.alias, cfg.table)
|
|
217
|
+
}
|
|
218
|
+
let existsDirective: boolean | null = null
|
|
219
|
+
for (const filter of filtersForAlias) {
|
|
220
|
+
if (filter.op === 'exists') {
|
|
221
|
+
if (filter.value === false) existsDirective = false
|
|
222
|
+
else if (existsDirective === null) existsDirective = true
|
|
223
|
+
continue
|
|
224
|
+
}
|
|
225
|
+
const targetTable = aliasTables.get(filter.alias)
|
|
226
|
+
if (!targetTable) continue
|
|
227
|
+
if (columnExists) {
|
|
228
|
+
const exists = await columnExists(targetTable, filter.column)
|
|
229
|
+
if (!exists) continue
|
|
230
|
+
}
|
|
231
|
+
const qualified = `${filter.alias}.${filter.column}`
|
|
232
|
+
applyFilterOp(sub, qualified, filter.op, filter.value)
|
|
233
|
+
}
|
|
234
|
+
if (existsDirective === false) builder = builder.whereNotExists(sub)
|
|
235
|
+
else builder = builder.whereExists(sub)
|
|
236
|
+
}
|
|
237
|
+
return builder
|
|
238
|
+
}
|
|
@@ -0,0 +1,121 @@
|
|
|
1
|
+
import type { EntityId } from '@open-mercato/shared/modules/entities'
|
|
2
|
+
import type { Profiler } from '../profiler'
|
|
3
|
+
|
|
4
|
+
export type FilterOp = 'eq' | 'ne' | 'gt' | 'gte' | 'lt' | 'lte' | 'in' | 'nin' | 'like' | 'ilike' | 'exists'
|
|
5
|
+
|
|
6
|
+
export enum SortDir {
|
|
7
|
+
Asc = 'asc',
|
|
8
|
+
Desc = 'desc',
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
export type FieldSelector = string // base field or custom field key (prefixed with 'cf:')
|
|
12
|
+
|
|
13
|
+
export type Filter = {
|
|
14
|
+
field: FieldSelector
|
|
15
|
+
op: FilterOp
|
|
16
|
+
value?: any
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
export type Sort = { field: FieldSelector; dir?: SortDir }
|
|
20
|
+
|
|
21
|
+
export type Page = { page?: number; pageSize?: number }
|
|
22
|
+
|
|
23
|
+
// Mongo/Medusa-style filter operators (typed)
|
|
24
|
+
export type WhereOps<T> = {
|
|
25
|
+
$eq?: T
|
|
26
|
+
$ne?: T | null
|
|
27
|
+
$gt?: T extends number | Date ? T : never
|
|
28
|
+
$gte?: T extends number | Date ? T : never
|
|
29
|
+
$lt?: T extends number | Date ? T : never
|
|
30
|
+
$lte?: T extends number | Date ? T : never
|
|
31
|
+
$in?: T[]
|
|
32
|
+
$nin?: T[]
|
|
33
|
+
$like?: T extends string ? string : never
|
|
34
|
+
$ilike?: T extends string ? string : never
|
|
35
|
+
$exists?: boolean
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
// A field filter can be a direct value (equals) or ops object
|
|
39
|
+
export type WhereValue<T = any> = T | WhereOps<T>
|
|
40
|
+
|
|
41
|
+
// Generic shape for object filters. If you have a typed map of field→type,
|
|
42
|
+
// pass it as the generic to get end-to-end typing.
|
|
43
|
+
// Example: Where<{
|
|
44
|
+
// id: string; title: string; created_at: Date; 'cf:severity': number
|
|
45
|
+
// }>
|
|
46
|
+
export type Where<Fields extends Record<string, any> = Record<string, any>> =
|
|
47
|
+
Partial<{ [K in keyof Fields]: WhereValue<Fields[K]> }> & Record<string, WhereValue>
|
|
48
|
+
|
|
49
|
+
export type QueryCustomFieldJoin = {
|
|
50
|
+
fromField: string
|
|
51
|
+
toField: string
|
|
52
|
+
type?: 'left' | 'inner'
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
export type QueryCustomFieldSource = {
|
|
56
|
+
entityId: EntityId
|
|
57
|
+
table?: string
|
|
58
|
+
alias?: string
|
|
59
|
+
recordIdColumn?: string
|
|
60
|
+
join?: QueryCustomFieldJoin
|
|
61
|
+
tenantField?: string
|
|
62
|
+
organizationField?: string
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
export type QueryJoinEdge = {
|
|
66
|
+
alias: string
|
|
67
|
+
table?: string
|
|
68
|
+
entityId?: EntityId
|
|
69
|
+
from: {
|
|
70
|
+
alias?: string
|
|
71
|
+
field: string
|
|
72
|
+
}
|
|
73
|
+
to: {
|
|
74
|
+
field: string
|
|
75
|
+
}
|
|
76
|
+
type?: 'left' | 'inner'
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
export type QueryOptions = {
|
|
80
|
+
fields?: FieldSelector[] // base fields and/or 'cf:<key>' for custom fields
|
|
81
|
+
includeExtensions?: boolean | string[] // include all registered extensions or only specific ones by entity id
|
|
82
|
+
includeCustomFields?: boolean | string[] // include all CFs or specific keys
|
|
83
|
+
// Accept classic array syntax or Mongo-style object syntax
|
|
84
|
+
filters?: Filter[] | Where
|
|
85
|
+
sort?: Sort[]
|
|
86
|
+
page?: Page
|
|
87
|
+
organizationId?: string // enforce multi-tenant scope
|
|
88
|
+
tenantId?: string // enforce tenant scope
|
|
89
|
+
// Optional list of organization ids to scope results. Takes precedence over organizationId.
|
|
90
|
+
organizationIds?: string[]
|
|
91
|
+
// Soft-delete behavior: when false (default), rows with non-null deleted_at
|
|
92
|
+
// are excluded if the base table has that column. Set true to include them.
|
|
93
|
+
withDeleted?: boolean
|
|
94
|
+
customFieldSources?: QueryCustomFieldSource[]
|
|
95
|
+
joins?: QueryJoinEdge[]
|
|
96
|
+
profiler?: Profiler
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
export type PartialIndexWarning = {
|
|
100
|
+
entity: EntityId
|
|
101
|
+
entityLabel?: string | null
|
|
102
|
+
baseCount?: number | null
|
|
103
|
+
indexedCount?: number | null
|
|
104
|
+
scope?: 'scoped' | 'global'
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
export type QueryResultMeta = {
|
|
108
|
+
partialIndexWarning?: PartialIndexWarning
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
export type QueryResult<T = any> = {
|
|
112
|
+
items: T[]
|
|
113
|
+
page: number
|
|
114
|
+
pageSize: number
|
|
115
|
+
total: number
|
|
116
|
+
meta?: QueryResultMeta
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
export interface QueryEngine {
|
|
120
|
+
query<T = any>(entity: EntityId, opts?: QueryOptions): Promise<QueryResult<T>>
|
|
121
|
+
}
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
import { parseBooleanWithDefault } from '@open-mercato/shared/lib/boolean'
|
|
2
|
+
|
|
3
|
+
export type SearchConfig = {
|
|
4
|
+
enabled: boolean
|
|
5
|
+
minTokenLength: number
|
|
6
|
+
enablePartials: boolean
|
|
7
|
+
hashAlgorithm: 'sha256' | 'sha1' | 'md5'
|
|
8
|
+
storeRawTokens: boolean
|
|
9
|
+
blocklistedFields: string[]
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
const DEFAULT_BLOCKLIST = ['password', 'token', 'secret', 'hash']
|
|
13
|
+
|
|
14
|
+
function parseBoolean(raw: string | undefined, fallback: boolean): boolean {
|
|
15
|
+
return parseBooleanWithDefault(raw, fallback)
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
function parseNumber(raw: string | undefined, fallback: number, min = 1): number {
|
|
19
|
+
if (raw == null) return fallback
|
|
20
|
+
const value = Number.parseInt(raw, 10)
|
|
21
|
+
if (!Number.isFinite(value)) return fallback
|
|
22
|
+
if (value < min) return fallback
|
|
23
|
+
return value
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
function parseHashAlgorithm(raw: string | undefined): 'sha256' | 'sha1' | 'md5' {
|
|
27
|
+
const value = (raw ?? '').trim().toLowerCase()
|
|
28
|
+
if (value === 'sha1') return 'sha1'
|
|
29
|
+
if (value === 'md5') return 'md5'
|
|
30
|
+
return 'sha256'
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
export function resolveSearchConfig(): SearchConfig {
|
|
34
|
+
return {
|
|
35
|
+
enabled: parseBoolean(process.env.OM_SEARCH_ENABLED, true),
|
|
36
|
+
minTokenLength: parseNumber(process.env.OM_SEARCH_MIN_LEN, 3, 1),
|
|
37
|
+
enablePartials: parseBoolean(process.env.OM_SEARCH_ENABLE_PARTIAL, true),
|
|
38
|
+
hashAlgorithm: parseHashAlgorithm(process.env.OM_SEARCH_HASH_ALGO),
|
|
39
|
+
storeRawTokens: parseBoolean(process.env.OM_SEARCH_STORE_RAW_TOKENS, false),
|
|
40
|
+
blocklistedFields: (process.env.OM_SEARCH_FIELD_BLOCKLIST ?? '')
|
|
41
|
+
.split(',')
|
|
42
|
+
.map((entry) => entry.trim())
|
|
43
|
+
.filter((entry) => entry.length > 0)
|
|
44
|
+
.filter((value, index, arr) => arr.indexOf(value) === index)
|
|
45
|
+
.map((entry) => entry.toLowerCase())
|
|
46
|
+
.concat(DEFAULT_BLOCKLIST)
|
|
47
|
+
.filter((value, index, arr) => arr.indexOf(value) === index),
|
|
48
|
+
}
|
|
49
|
+
}
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
import crypto from 'crypto'
|
|
2
|
+
import { resolveSearchConfig, type SearchConfig } from './config'
|
|
3
|
+
|
|
4
|
+
export type TokenizationResult = {
|
|
5
|
+
tokens: string[]
|
|
6
|
+
hashes: string[]
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
function normalizeText(text: string): string {
|
|
10
|
+
return text
|
|
11
|
+
.normalize('NFKD')
|
|
12
|
+
.replace(/[\u0300-\u036f]/g, '')
|
|
13
|
+
.replace(/[%_]/g, ' ')
|
|
14
|
+
.toLowerCase()
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
function splitTokens(text: string, minLength: number): string[] {
|
|
18
|
+
return normalizeText(text)
|
|
19
|
+
.split(/[^a-z0-9]+/i)
|
|
20
|
+
.filter((token) => token.length >= minLength)
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
function expandToken(token: string, config: SearchConfig): string[] {
|
|
24
|
+
if (!config.enablePartials) return [token]
|
|
25
|
+
const results: string[] = []
|
|
26
|
+
for (let i = config.minTokenLength; i <= token.length; i += 1) {
|
|
27
|
+
results.push(token.slice(0, i))
|
|
28
|
+
}
|
|
29
|
+
return results
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
export function hashToken(token: string, config?: SearchConfig): string {
|
|
33
|
+
const cfg = config ?? resolveSearchConfig()
|
|
34
|
+
return crypto.createHash(cfg.hashAlgorithm).update(token).digest('hex')
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
export function tokenizeText(text: string, config?: SearchConfig): TokenizationResult {
|
|
38
|
+
const cfg = config ?? resolveSearchConfig()
|
|
39
|
+
const baseTokens = splitTokens(text, cfg.minTokenLength)
|
|
40
|
+
const expanded = baseTokens.flatMap((token) => expandToken(token, cfg))
|
|
41
|
+
const unique = Array.from(new Set(expanded))
|
|
42
|
+
const tokens = unique.filter((token) => token.length >= cfg.minTokenLength)
|
|
43
|
+
const hashes = tokens.map((token) => hashToken(token, cfg))
|
|
44
|
+
return { tokens, hashes }
|
|
45
|
+
}
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
export type SlugifyOptions = {
|
|
2
|
+
replacement?: string
|
|
3
|
+
allowedChars?: string
|
|
4
|
+
trimReplacement?: boolean
|
|
5
|
+
}
|
|
6
|
+
|
|
7
|
+
const DEFAULT_REPLACEMENT = '-'
|
|
8
|
+
const DEFAULT_ALLOWED_CHARS = '-'
|
|
9
|
+
|
|
10
|
+
const escapeRegex = (value: string): string => value.replace(/[\\^$.*+?()[\]{}|\-]/g, '\\$&')
|
|
11
|
+
|
|
12
|
+
export function slugify(value: string, options: SlugifyOptions = {}): string {
|
|
13
|
+
const replacement = options.replacement ?? DEFAULT_REPLACEMENT
|
|
14
|
+
const allowedChars = options.allowedChars ?? DEFAULT_ALLOWED_CHARS
|
|
15
|
+
const trimReplacement = options.trimReplacement ?? true
|
|
16
|
+
const normalized = value.toLowerCase().trim()
|
|
17
|
+
if (!normalized) return ''
|
|
18
|
+
const escapedAllowed = escapeRegex(allowedChars)
|
|
19
|
+
const invalidPattern = new RegExp(`[^a-z0-9${escapedAllowed}]+`, 'g')
|
|
20
|
+
const replaced = normalized.replace(invalidPattern, replacement)
|
|
21
|
+
if (!trimReplacement || replacement.length !== 1 || !replaced) return replaced
|
|
22
|
+
const char = replacement
|
|
23
|
+
let start = 0
|
|
24
|
+
let end = replaced.length
|
|
25
|
+
while (start < end && replaced[start] === char) start += 1
|
|
26
|
+
while (end > start && replaced[end - 1] === char) end -= 1
|
|
27
|
+
return replaced.slice(start, end)
|
|
28
|
+
}
|
|
@@ -0,0 +1,124 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Test Bootstrap Utility
|
|
3
|
+
*
|
|
4
|
+
* Provides a centralized way to bootstrap dependencies for tests.
|
|
5
|
+
* This utility allows tests to register only the dependencies they need
|
|
6
|
+
* without importing the full app bootstrap.
|
|
7
|
+
*
|
|
8
|
+
* Usage in tests:
|
|
9
|
+
*
|
|
10
|
+
* ```typescript
|
|
11
|
+
* import { bootstrapTest, resetTestBootstrap } from '@open-mercato/shared/lib/testing/bootstrap'
|
|
12
|
+
*
|
|
13
|
+
* beforeEach(async () => {
|
|
14
|
+
* resetTestBootstrap()
|
|
15
|
+
* await bootstrapTest({
|
|
16
|
+
* modules: mockModules,
|
|
17
|
+
* entityIds: mockEntityIds,
|
|
18
|
+
* })
|
|
19
|
+
* })
|
|
20
|
+
* ```
|
|
21
|
+
*
|
|
22
|
+
* For tests that use jest.resetModules(), import dynamically:
|
|
23
|
+
*
|
|
24
|
+
* ```typescript
|
|
25
|
+
* beforeEach(async () => {
|
|
26
|
+
* jest.resetModules()
|
|
27
|
+
* const { registerModules } = await import('@open-mercato/shared/modules/registry')
|
|
28
|
+
* registerModules(mockModules as any)
|
|
29
|
+
* })
|
|
30
|
+
* ```
|
|
31
|
+
*/
|
|
32
|
+
|
|
33
|
+
import type { Module } from '../../modules/registry'
|
|
34
|
+
|
|
35
|
+
export type EntityIds = Record<string, Record<string, string>>
|
|
36
|
+
|
|
37
|
+
export interface TestBootstrapOptions {
|
|
38
|
+
/** Modules to register (for i18n, query engine, etc.) */
|
|
39
|
+
modules?: Module[]
|
|
40
|
+
/** Entity IDs to register (for encryption, indexing) */
|
|
41
|
+
entityIds?: EntityIds
|
|
42
|
+
/** ORM entities to register (rarely needed in unit tests) */
|
|
43
|
+
ormEntities?: any[]
|
|
44
|
+
/** DI registrars to register (rarely needed in unit tests) */
|
|
45
|
+
diRegistrars?: Array<(container: any) => void>
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
let _testBootstrapped = false
|
|
49
|
+
|
|
50
|
+
/**
|
|
51
|
+
* Bootstrap dependencies for tests.
|
|
52
|
+
* Call this in beforeEach or beforeAll to set up required registrations.
|
|
53
|
+
*/
|
|
54
|
+
export async function bootstrapTest(options: TestBootstrapOptions = {}): Promise<void> {
|
|
55
|
+
const { modules, entityIds, ormEntities, diRegistrars } = options
|
|
56
|
+
|
|
57
|
+
if (modules !== undefined) {
|
|
58
|
+
// Import lazily to avoid circular dependencies
|
|
59
|
+
const { registerModules } = await import('../modules/registry.js')
|
|
60
|
+
registerModules(modules)
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
if (entityIds !== undefined) {
|
|
64
|
+
const { registerEntityIds } = await import('../encryption/entityIds.js')
|
|
65
|
+
registerEntityIds(entityIds)
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
if (ormEntities !== undefined) {
|
|
69
|
+
const { registerOrmEntities } = await import('../db/mikro.js')
|
|
70
|
+
registerOrmEntities(ormEntities)
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
if (diRegistrars !== undefined) {
|
|
74
|
+
const { registerDiRegistrars } = await import('../di/container.js')
|
|
75
|
+
registerDiRegistrars(diRegistrars)
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
_testBootstrapped = true
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
/**
|
|
82
|
+
* Reset the test bootstrap state.
|
|
83
|
+
* Call this in beforeEach when you need fresh state between tests.
|
|
84
|
+
*
|
|
85
|
+
* Note: This only resets the test bootstrap flag. To fully reset
|
|
86
|
+
* registration state, you may need to use jest.resetModules() and
|
|
87
|
+
* re-import the registration functions.
|
|
88
|
+
*/
|
|
89
|
+
export function resetTestBootstrap(): void {
|
|
90
|
+
_testBootstrapped = false
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
/**
|
|
94
|
+
* Check if test bootstrap has been called.
|
|
95
|
+
*/
|
|
96
|
+
export function isTestBootstrapped(): boolean {
|
|
97
|
+
return _testBootstrapped
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
/**
|
|
101
|
+
* Helper to create minimal mock modules for testing.
|
|
102
|
+
*/
|
|
103
|
+
export function createMockModules(overrides: Partial<Module>[] = []): Module[] {
|
|
104
|
+
return overrides.map((override, index) => ({
|
|
105
|
+
id: override.id || `test-module-${index}`,
|
|
106
|
+
...override,
|
|
107
|
+
})) as Module[]
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
/**
|
|
111
|
+
* Helper to create minimal mock entity IDs for testing.
|
|
112
|
+
*/
|
|
113
|
+
export function createMockEntityIds(
|
|
114
|
+
entities: Record<string, string[]>
|
|
115
|
+
): EntityIds {
|
|
116
|
+
const result: EntityIds = {}
|
|
117
|
+
for (const [module, entityNames] of Object.entries(entities)) {
|
|
118
|
+
result[module] = {}
|
|
119
|
+
for (const name of entityNames) {
|
|
120
|
+
result[module][name] = `${module}:${name}`
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
return result
|
|
124
|
+
}
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Testing utilities for @open-mercato packages
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
export {
|
|
6
|
+
bootstrapTest,
|
|
7
|
+
resetTestBootstrap,
|
|
8
|
+
isTestBootstrapped,
|
|
9
|
+
createMockModules,
|
|
10
|
+
createMockEntityIds,
|
|
11
|
+
type TestBootstrapOptions,
|
|
12
|
+
type EntityIds,
|
|
13
|
+
} from './bootstrap'
|
|
14
|
+
|
|
15
|
+
export { renderWithProviders } from './renderWithProviders'
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
import * as React from 'react'
|
|
2
|
+
import type { RenderOptions } from '@testing-library/react'
|
|
3
|
+
import { render } from '@testing-library/react'
|
|
4
|
+
import { QueryClient, QueryClientProvider } from '@tanstack/react-query'
|
|
5
|
+
import { I18nProvider } from '@open-mercato/shared/lib/i18n/context'
|
|
6
|
+
|
|
7
|
+
type ProviderOptions = {
|
|
8
|
+
locale?: string
|
|
9
|
+
dict?: Record<string, unknown>
|
|
10
|
+
queryClient?: QueryClient
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
export function renderWithProviders(
|
|
14
|
+
ui: React.ReactElement,
|
|
15
|
+
options?: RenderOptions & ProviderOptions,
|
|
16
|
+
) {
|
|
17
|
+
const { locale = 'en', dict = {}, queryClient = new QueryClient(), ...rest } = options ?? {}
|
|
18
|
+
|
|
19
|
+
function Wrapper({ children }: { children: React.ReactNode }) {
|
|
20
|
+
return (
|
|
21
|
+
<QueryClientProvider client={queryClient}>
|
|
22
|
+
{/* @ts-expect-error shared provider accepts loose dict shape */}
|
|
23
|
+
<I18nProvider locale={locale} dict={dict}>
|
|
24
|
+
{children}
|
|
25
|
+
</I18nProvider>
|
|
26
|
+
</QueryClientProvider>
|
|
27
|
+
)
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
return render(ui, { wrapper: Wrapper, ...rest })
|
|
31
|
+
}
|
package/src/lib/url.ts
ADDED
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
export function getAppBaseUrl(req: Request): string {
|
|
2
|
+
const url = new URL(req.url)
|
|
3
|
+
return (
|
|
4
|
+
process.env.NEXT_PUBLIC_APP_URL ||
|
|
5
|
+
process.env.APP_URL ||
|
|
6
|
+
`${url.protocol}//${url.host}`
|
|
7
|
+
)
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
export function toAbsoluteUrl(req: Request, path: string): string {
|
|
11
|
+
return new URL(path, getAppBaseUrl(req)).toString()
|
|
12
|
+
}
|
package/src/lib/utils.ts
ADDED
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
import { clsx, type ClassValue } from "clsx"
|
|
2
|
+
import { twMerge } from "tailwind-merge"
|
|
3
|
+
|
|
4
|
+
export function cn(...inputs: ClassValue[]) {
|
|
5
|
+
return twMerge(clsx(inputs))
|
|
6
|
+
}
|
|
7
|
+
|
|
8
|
+
export function slugifyTagLabel(label: string): string {
|
|
9
|
+
return (
|
|
10
|
+
label
|
|
11
|
+
.toLowerCase()
|
|
12
|
+
.trim()
|
|
13
|
+
.replace(/[^a-z0-9]+/g, "-")
|
|
14
|
+
.replace(/^-+|-+$/g, "")
|
|
15
|
+
.slice(0, 80) || `tag-${Math.random().toString(36).slice(2, 10)}`
|
|
16
|
+
)
|
|
17
|
+
}
|
|
@@ -0,0 +1,5 @@
|
|
|
1
|
+
// Application version - replaced at build time by esbuild plugin
|
|
2
|
+
// The actual version is injected during the build process in build.mjs
|
|
3
|
+
// This source file uses a placeholder that gets replaced in dist/
|
|
4
|
+
export const APP_VERSION = '0.0.0-dev'
|
|
5
|
+
export const appVersion = APP_VERSION
|