@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,607 @@
|
|
|
1
|
+
import type { CustomFieldSet, EntityId } from '@open-mercato/shared/modules/entities'
|
|
2
|
+
import type { EntityManager } from '@mikro-orm/core'
|
|
3
|
+
import { CustomFieldDef, CustomFieldValue } from '@open-mercato/core/modules/entities/data/entities'
|
|
4
|
+
import type { WhereValue } from '@open-mercato/shared/lib/query/types'
|
|
5
|
+
import type { TenantDataEncryptionService } from '../encryption/tenantDataEncryptionService'
|
|
6
|
+
import { decryptCustomFieldValue, resolveTenantEncryptionService } from '../encryption/customFieldValues'
|
|
7
|
+
import { parseBooleanToken } from '../boolean'
|
|
8
|
+
import { extractCustomFieldEntries } from './custom-fields-client'
|
|
9
|
+
|
|
10
|
+
export type CustomFieldSelectors = {
|
|
11
|
+
keys: string[]
|
|
12
|
+
selectors: string[] // e.g. ['cf:priority', 'cf:severity']
|
|
13
|
+
outputKeys: string[] // e.g. ['cf_priority', 'cf_severity']
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
export type SplitCustomFieldPayload = {
|
|
17
|
+
base: Record<string, unknown>
|
|
18
|
+
custom: Record<string, unknown>
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
export type CustomFieldDefinitionSummary = {
|
|
22
|
+
key: string
|
|
23
|
+
label: string | null
|
|
24
|
+
kind: string | null
|
|
25
|
+
multi: boolean
|
|
26
|
+
dictionaryId?: string | null
|
|
27
|
+
organizationId?: string | null
|
|
28
|
+
tenantId?: string | null
|
|
29
|
+
priority: number
|
|
30
|
+
updatedAt: number
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
export type CustomFieldDefinitionIndex = Map<string, CustomFieldDefinitionSummary[]>
|
|
34
|
+
|
|
35
|
+
export type CustomFieldDisplayEntry = {
|
|
36
|
+
key: string
|
|
37
|
+
label: string | null
|
|
38
|
+
value: unknown
|
|
39
|
+
kind: string | null
|
|
40
|
+
multi: boolean
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
export type CustomFieldDisplayPayload = {
|
|
44
|
+
customValues: Record<string, unknown> | null
|
|
45
|
+
customFields: CustomFieldDisplayEntry[]
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
export type CustomFieldSnapshot = {
|
|
49
|
+
entries: Record<string, unknown>
|
|
50
|
+
customValues: Record<string, unknown> | null
|
|
51
|
+
customFields: CustomFieldDisplayEntry[]
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
export function buildCustomFieldSelectorsForEntity(entityId: EntityId, sets: CustomFieldSet[]): CustomFieldSelectors {
|
|
55
|
+
const keys = Array.from(new Set(
|
|
56
|
+
(sets || [])
|
|
57
|
+
.filter((s) => s.entity === entityId)
|
|
58
|
+
.flatMap((s) => (s.fields || []).map((f) => f.key))
|
|
59
|
+
))
|
|
60
|
+
const selectors = keys.map((k) => `cf:${k}`)
|
|
61
|
+
const outputKeys = keys.map((k) => `cf_${k}`)
|
|
62
|
+
return { keys, selectors, outputKeys }
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
export function normalizeCustomFieldValue(val: unknown): unknown {
|
|
66
|
+
if (Array.isArray(val)) return val
|
|
67
|
+
if (typeof val === 'string') {
|
|
68
|
+
const s = val.trim()
|
|
69
|
+
// Parse Postgres array-like '{a,b,c}' to string[] when present
|
|
70
|
+
if (s.startsWith('{') && s.endsWith('}')) {
|
|
71
|
+
const inner = s.slice(1, -1).trim()
|
|
72
|
+
if (!inner) return []
|
|
73
|
+
return inner.split(/[\s,]+/).map((x) => x.trim()).filter(Boolean)
|
|
74
|
+
}
|
|
75
|
+
return s
|
|
76
|
+
}
|
|
77
|
+
return val as any
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
// Extracts cf_* fields from a record that may contain both 'cf:<key>' and/or 'cf_<key>'
|
|
81
|
+
export function extractCustomFieldsFromItem(item: Record<string, unknown>, keys: string[]): Record<string, unknown> {
|
|
82
|
+
const out: Record<string, unknown> = {}
|
|
83
|
+
for (const key of keys) {
|
|
84
|
+
const colon = item[`cf:${key}` as keyof typeof item]
|
|
85
|
+
const snake = item[`cf_${key}` as keyof typeof item]
|
|
86
|
+
const value = colon !== undefined ? colon : snake
|
|
87
|
+
if (value !== undefined) out[`cf_${key}`] = normalizeCustomFieldValue(value)
|
|
88
|
+
}
|
|
89
|
+
return out
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
export function extractAllCustomFieldEntries(item: Record<string, unknown>): Record<string, unknown> {
|
|
93
|
+
return extractCustomFieldEntries(item)
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
function normalizeFieldsetFilter(input?: string | string[] | null): Set<string | null> | null {
|
|
97
|
+
if (input == null) return null
|
|
98
|
+
const values = Array.isArray(input) ? input : [input]
|
|
99
|
+
const normalized = new Set<string | null>()
|
|
100
|
+
for (const raw of values) {
|
|
101
|
+
if (raw == null) continue
|
|
102
|
+
const trimmed = String(raw).trim()
|
|
103
|
+
if (!trimmed) {
|
|
104
|
+
normalized.add(null)
|
|
105
|
+
} else {
|
|
106
|
+
normalized.add(trimmed)
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
return normalized.size ? normalized : null
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
export async function buildCustomFieldFiltersFromQuery(opts: {
|
|
113
|
+
entityId?: EntityId
|
|
114
|
+
entityIds?: EntityId[]
|
|
115
|
+
query: Record<string, unknown>
|
|
116
|
+
em: EntityManager
|
|
117
|
+
tenantId: string | null | undefined
|
|
118
|
+
fieldset?: string | string[] | null
|
|
119
|
+
}): Promise<Record<string, WhereValue>> {
|
|
120
|
+
const out: Record<string, WhereValue> = {}
|
|
121
|
+
const entries = Object.entries(opts.query).filter(([k]) => k.startsWith('cf_'))
|
|
122
|
+
if (!entries.length) return out
|
|
123
|
+
|
|
124
|
+
const entityIdList = Array.isArray(opts.entityIds) && opts.entityIds.length
|
|
125
|
+
? opts.entityIds
|
|
126
|
+
: opts.entityId
|
|
127
|
+
? [opts.entityId]
|
|
128
|
+
: []
|
|
129
|
+
if (!entityIdList.length) return out
|
|
130
|
+
|
|
131
|
+
// Tenant-only scope: allow global (null) or tenant match; ignore organization here
|
|
132
|
+
const defs = await opts.em.find(CustomFieldDef, {
|
|
133
|
+
entityId: { $in: entityIdList as any },
|
|
134
|
+
isActive: true,
|
|
135
|
+
$and: [
|
|
136
|
+
{ $or: [ { tenantId: opts.tenantId as any }, { tenantId: null } ] },
|
|
137
|
+
],
|
|
138
|
+
})
|
|
139
|
+
const fieldsetFilter = normalizeFieldsetFilter(opts.fieldset)
|
|
140
|
+
const order = new Map<string, number>()
|
|
141
|
+
entityIdList.map(String).forEach((id, index) => order.set(id, index))
|
|
142
|
+
const byKey: Record<string, { kind: string; multi?: boolean; entityId: string }> = {}
|
|
143
|
+
for (const d of defs) {
|
|
144
|
+
if (fieldsetFilter) {
|
|
145
|
+
const rawFieldset = typeof d.configJson?.fieldset === 'string' ? d.configJson.fieldset.trim() : ''
|
|
146
|
+
const normalizedFieldset = rawFieldset.length ? rawFieldset : null
|
|
147
|
+
if (!fieldsetFilter.has(normalizedFieldset)) continue
|
|
148
|
+
}
|
|
149
|
+
const key = d.key
|
|
150
|
+
const entityId = String(d.entityId)
|
|
151
|
+
const current = byKey[key]
|
|
152
|
+
const rankNew = order.get(entityId) ?? Number.MAX_SAFE_INTEGER
|
|
153
|
+
if (!current) {
|
|
154
|
+
byKey[key] = { kind: d.kind, multi: Boolean((d as any).configJson?.multi), entityId }
|
|
155
|
+
continue
|
|
156
|
+
}
|
|
157
|
+
const rankOld = order.get(current.entityId) ?? Number.MAX_SAFE_INTEGER
|
|
158
|
+
if (rankNew < rankOld) {
|
|
159
|
+
byKey[key] = { kind: d.kind, multi: Boolean((d as any).configJson?.multi), entityId }
|
|
160
|
+
}
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
const coerce = (kind: string, v: unknown) => {
|
|
164
|
+
if (v == null) return v as undefined
|
|
165
|
+
switch (kind) {
|
|
166
|
+
case 'integer': return Number.parseInt(String(v), 10)
|
|
167
|
+
case 'float': return Number.parseFloat(String(v))
|
|
168
|
+
case 'boolean': return parseBooleanToken(String(v)) === true
|
|
169
|
+
default: return String(v)
|
|
170
|
+
}
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
for (const [rawKey, rawVal] of entries) {
|
|
174
|
+
const isIn = rawKey.endsWith('In')
|
|
175
|
+
const key = isIn ? rawKey.replace(/^cf_/, '').replace(/In$/, '') : rawKey.replace(/^cf_/, '')
|
|
176
|
+
const def = byKey[key]
|
|
177
|
+
const fieldId = `cf:${key}`
|
|
178
|
+
if (!def) continue
|
|
179
|
+
if (isIn) {
|
|
180
|
+
const list = Array.isArray(rawVal)
|
|
181
|
+
? (rawVal as unknown[])
|
|
182
|
+
: String(rawVal)
|
|
183
|
+
.split(',')
|
|
184
|
+
.map((s) => s.trim())
|
|
185
|
+
.filter(Boolean)
|
|
186
|
+
if (list.length) out[fieldId] = { $in: list.map((x) => coerce(def.kind, x)) as (string[] | number[] | boolean[]) }
|
|
187
|
+
} else {
|
|
188
|
+
out[fieldId] = coerce(def.kind, rawVal)
|
|
189
|
+
}
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
return out
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
export function splitCustomFieldPayload(raw: unknown): SplitCustomFieldPayload {
|
|
196
|
+
const base: Record<string, unknown> = {}
|
|
197
|
+
const custom: Record<string, unknown> = {}
|
|
198
|
+
if (!raw || typeof raw !== 'object') return { base, custom }
|
|
199
|
+
for (const [key, value] of Object.entries(raw as Record<string, unknown>)) {
|
|
200
|
+
if (key === 'customFields') {
|
|
201
|
+
if (Array.isArray(value)) {
|
|
202
|
+
value.forEach((entry) => {
|
|
203
|
+
if (!entry || typeof entry !== 'object') return
|
|
204
|
+
const entryKey = typeof (entry as any).key === 'string' ? (entry as any).key.trim() : ''
|
|
205
|
+
if (!entryKey) return
|
|
206
|
+
custom[entryKey] = (entry as any).value
|
|
207
|
+
})
|
|
208
|
+
continue
|
|
209
|
+
}
|
|
210
|
+
if (value && typeof value === 'object') {
|
|
211
|
+
for (const [ck, cv] of Object.entries(value as Record<string, unknown>)) {
|
|
212
|
+
const normalizedKey = typeof ck === 'string' ? ck.trim() : ''
|
|
213
|
+
if (!normalizedKey) continue
|
|
214
|
+
custom[normalizedKey] = cv
|
|
215
|
+
}
|
|
216
|
+
continue
|
|
217
|
+
}
|
|
218
|
+
}
|
|
219
|
+
if (key === 'customValues' && value && typeof value === 'object' && !Array.isArray(value)) {
|
|
220
|
+
for (const [ck, cv] of Object.entries(value as Record<string, unknown>)) {
|
|
221
|
+
custom[String(ck)] = cv
|
|
222
|
+
}
|
|
223
|
+
continue
|
|
224
|
+
}
|
|
225
|
+
if (key.startsWith('cf_')) {
|
|
226
|
+
custom[key.slice(3)] = value
|
|
227
|
+
continue
|
|
228
|
+
}
|
|
229
|
+
if (key.startsWith('cf:')) {
|
|
230
|
+
custom[key.slice(3)] = value
|
|
231
|
+
continue
|
|
232
|
+
}
|
|
233
|
+
base[key] = value
|
|
234
|
+
}
|
|
235
|
+
return { base, custom }
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
export function extractCustomFieldValuesFromPayload(raw: Record<string, unknown>): Record<string, unknown> {
|
|
239
|
+
return splitCustomFieldPayload(raw).custom
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
function normalizeDefinitionKey(key: unknown): string {
|
|
243
|
+
if (typeof key !== 'string') return ''
|
|
244
|
+
const trimmed = key.trim()
|
|
245
|
+
return trimmed.length ? trimmed.toLowerCase() : ''
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
function normalizeDefinitionConfig(raw: unknown): Record<string, any> {
|
|
249
|
+
if (!raw) return {}
|
|
250
|
+
if (typeof raw === 'string') {
|
|
251
|
+
try {
|
|
252
|
+
const parsed = JSON.parse(raw)
|
|
253
|
+
if (parsed && typeof parsed === 'object' && !Array.isArray(parsed)) {
|
|
254
|
+
return { ...(parsed as Record<string, any>) }
|
|
255
|
+
}
|
|
256
|
+
return {}
|
|
257
|
+
} catch {
|
|
258
|
+
return {}
|
|
259
|
+
}
|
|
260
|
+
}
|
|
261
|
+
if (typeof raw === 'object' && !Array.isArray(raw)) {
|
|
262
|
+
return { ...(raw as Record<string, any>) }
|
|
263
|
+
}
|
|
264
|
+
return {}
|
|
265
|
+
}
|
|
266
|
+
|
|
267
|
+
function summarizeDefinition(def: CustomFieldDef): CustomFieldDefinitionSummary | null {
|
|
268
|
+
const normalizedKey = normalizeDefinitionKey(def.key)
|
|
269
|
+
if (!normalizedKey) return null
|
|
270
|
+
const cfg = normalizeDefinitionConfig((def as any).configJson)
|
|
271
|
+
const label =
|
|
272
|
+
typeof cfg.label === 'string' && cfg.label.trim().length
|
|
273
|
+
? cfg.label.trim()
|
|
274
|
+
: def.key
|
|
275
|
+
const dictionaryId =
|
|
276
|
+
typeof cfg.dictionaryId === 'string' && cfg.dictionaryId.trim().length
|
|
277
|
+
? cfg.dictionaryId.trim()
|
|
278
|
+
: null
|
|
279
|
+
const multi =
|
|
280
|
+
cfg.multi !== undefined ? Boolean(cfg.multi) : false
|
|
281
|
+
const priority =
|
|
282
|
+
typeof cfg.priority === 'number' ? cfg.priority : 0
|
|
283
|
+
const updatedAt =
|
|
284
|
+
def.updatedAt instanceof Date
|
|
285
|
+
? def.updatedAt.getTime()
|
|
286
|
+
: new Date(def.updatedAt as any).getTime()
|
|
287
|
+
return {
|
|
288
|
+
key: def.key,
|
|
289
|
+
label,
|
|
290
|
+
kind: typeof def.kind === 'string' ? def.kind : null,
|
|
291
|
+
multi,
|
|
292
|
+
dictionaryId,
|
|
293
|
+
organizationId: def.organizationId ?? null,
|
|
294
|
+
tenantId: def.tenantId ?? null,
|
|
295
|
+
priority,
|
|
296
|
+
updatedAt: Number.isNaN(updatedAt) ? 0 : updatedAt,
|
|
297
|
+
}
|
|
298
|
+
}
|
|
299
|
+
|
|
300
|
+
function sortDefinitionSummaries(defs: CustomFieldDefinitionSummary[]): CustomFieldDefinitionSummary[] {
|
|
301
|
+
return [...defs].sort((a, b) => {
|
|
302
|
+
const priorityDiff = (a.priority ?? 0) - (b.priority ?? 0)
|
|
303
|
+
if (priorityDiff !== 0) return priorityDiff
|
|
304
|
+
const updatedDiff = (b.updatedAt ?? 0) - (a.updatedAt ?? 0)
|
|
305
|
+
if (updatedDiff !== 0) return updatedDiff
|
|
306
|
+
return a.key.localeCompare(b.key)
|
|
307
|
+
})
|
|
308
|
+
}
|
|
309
|
+
|
|
310
|
+
function selectDefinitionForRecord(
|
|
311
|
+
defs: CustomFieldDefinitionSummary[],
|
|
312
|
+
organizationId: string | null,
|
|
313
|
+
tenantId: string | null,
|
|
314
|
+
): CustomFieldDefinitionSummary | null {
|
|
315
|
+
if (!defs.length) return null
|
|
316
|
+
const prioritizedForOrg = defs.filter(
|
|
317
|
+
(def) => def.organizationId && organizationId && def.organizationId === organizationId,
|
|
318
|
+
)
|
|
319
|
+
if (prioritizedForOrg.length) return sortDefinitionSummaries(prioritizedForOrg)[0]
|
|
320
|
+
const prioritizedForTenant = defs.filter(
|
|
321
|
+
(def) => def.tenantId && tenantId && def.tenantId === tenantId && !def.organizationId,
|
|
322
|
+
)
|
|
323
|
+
if (prioritizedForTenant.length) return sortDefinitionSummaries(prioritizedForTenant)[0]
|
|
324
|
+
const global = defs.filter((def) => !def.organizationId)
|
|
325
|
+
if (global.length) return sortDefinitionSummaries(global)[0]
|
|
326
|
+
return sortDefinitionSummaries(defs)[0] ?? null
|
|
327
|
+
}
|
|
328
|
+
|
|
329
|
+
export async function loadCustomFieldDefinitionIndex(opts: {
|
|
330
|
+
em: EntityManager
|
|
331
|
+
entityIds: string | string[]
|
|
332
|
+
tenantId?: string | null | undefined
|
|
333
|
+
organizationIds?: Array<string | null | undefined> | null
|
|
334
|
+
}): Promise<CustomFieldDefinitionIndex> {
|
|
335
|
+
const list = Array.isArray(opts.entityIds) ? opts.entityIds : [opts.entityIds]
|
|
336
|
+
const entityIds = list
|
|
337
|
+
.map((id) => (typeof id === 'string' ? id.trim() : String(id ?? '')))
|
|
338
|
+
.filter((id) => id.length > 0)
|
|
339
|
+
if (!entityIds.length) return new Map()
|
|
340
|
+
const tenantId = opts.tenantId ?? null
|
|
341
|
+
const orgCandidates = Array.isArray(opts.organizationIds)
|
|
342
|
+
? opts.organizationIds
|
|
343
|
+
.map((id) => (typeof id === 'string' ? id.trim() : id))
|
|
344
|
+
.filter((id): id is string => typeof id === 'string' && id.length > 0)
|
|
345
|
+
: []
|
|
346
|
+
const scopeClauses: Record<string, unknown>[] = [
|
|
347
|
+
tenantId
|
|
348
|
+
? { $or: [{ tenantId: tenantId as any }, { tenantId: null }] }
|
|
349
|
+
: { tenantId: null },
|
|
350
|
+
]
|
|
351
|
+
if (orgCandidates.length) {
|
|
352
|
+
scopeClauses.push({
|
|
353
|
+
$or: [{ organizationId: { $in: orgCandidates as any } }, { organizationId: null }],
|
|
354
|
+
})
|
|
355
|
+
} else {
|
|
356
|
+
scopeClauses.push({ organizationId: null })
|
|
357
|
+
}
|
|
358
|
+
const where: Record<string, unknown> = {
|
|
359
|
+
entityId: { $in: entityIds as any },
|
|
360
|
+
deletedAt: null,
|
|
361
|
+
isActive: true,
|
|
362
|
+
$and: scopeClauses,
|
|
363
|
+
}
|
|
364
|
+
const defs = await opts.em.find(CustomFieldDef, where as any)
|
|
365
|
+
const index: CustomFieldDefinitionIndex = new Map()
|
|
366
|
+
defs.forEach((def) => {
|
|
367
|
+
const summary = summarizeDefinition(def)
|
|
368
|
+
if (!summary) return
|
|
369
|
+
const normalizedKey = normalizeDefinitionKey(summary.key)
|
|
370
|
+
if (!normalizedKey) return
|
|
371
|
+
if (!index.has(normalizedKey)) index.set(normalizedKey, [])
|
|
372
|
+
index.get(normalizedKey)!.push(summary)
|
|
373
|
+
})
|
|
374
|
+
index.forEach((entries, key) => {
|
|
375
|
+
index.set(key, sortDefinitionSummaries(entries))
|
|
376
|
+
})
|
|
377
|
+
return index
|
|
378
|
+
}
|
|
379
|
+
|
|
380
|
+
export function decorateRecordWithCustomFields(
|
|
381
|
+
record: Record<string, unknown>,
|
|
382
|
+
definitions: CustomFieldDefinitionIndex,
|
|
383
|
+
context: {
|
|
384
|
+
organizationId?: string | null
|
|
385
|
+
tenantId?: string | null
|
|
386
|
+
} = {},
|
|
387
|
+
): CustomFieldDisplayPayload {
|
|
388
|
+
const rawEntries = extractAllCustomFieldEntries(record)
|
|
389
|
+
if (!Object.keys(rawEntries).length) {
|
|
390
|
+
return { customValues: null, customFields: [] }
|
|
391
|
+
}
|
|
392
|
+
const values: Record<string, unknown> = {}
|
|
393
|
+
const entries: Array<{ entry: CustomFieldDisplayEntry; priority: number; updatedAt: number }> = []
|
|
394
|
+
const organizationId = context.organizationId ?? null
|
|
395
|
+
const tenantId = context.tenantId ?? null
|
|
396
|
+
|
|
397
|
+
Object.entries(rawEntries).forEach(([prefixedKey, value]) => {
|
|
398
|
+
const bareKey = prefixedKey.replace(/^cf_/, '')
|
|
399
|
+
const normalizedKey = normalizeDefinitionKey(bareKey)
|
|
400
|
+
if (!normalizedKey) return
|
|
401
|
+
values[bareKey] = value
|
|
402
|
+
const defsForKey = definitions.get(normalizedKey) ?? []
|
|
403
|
+
const resolvedDef = selectDefinitionForRecord(defsForKey, organizationId, tenantId)
|
|
404
|
+
const entry: CustomFieldDisplayEntry = {
|
|
405
|
+
key: bareKey,
|
|
406
|
+
label: resolvedDef?.label ?? bareKey,
|
|
407
|
+
value,
|
|
408
|
+
kind: resolvedDef?.kind ?? null,
|
|
409
|
+
multi: resolvedDef?.multi ?? Array.isArray(value),
|
|
410
|
+
}
|
|
411
|
+
entries.push({
|
|
412
|
+
entry,
|
|
413
|
+
priority: resolvedDef?.priority ?? Number.MAX_SAFE_INTEGER,
|
|
414
|
+
updatedAt: resolvedDef?.updatedAt ?? 0,
|
|
415
|
+
})
|
|
416
|
+
})
|
|
417
|
+
|
|
418
|
+
const ordered = entries
|
|
419
|
+
.sort((a, b) => {
|
|
420
|
+
const priorityDiff = a.priority - b.priority
|
|
421
|
+
if (priorityDiff !== 0) return priorityDiff
|
|
422
|
+
const updatedDiff = b.updatedAt - a.updatedAt
|
|
423
|
+
if (updatedDiff !== 0) return updatedDiff
|
|
424
|
+
return a.entry.key.localeCompare(b.entry.key)
|
|
425
|
+
})
|
|
426
|
+
.map((item) => item.entry)
|
|
427
|
+
|
|
428
|
+
return {
|
|
429
|
+
customValues: Object.keys(values).length ? values : null,
|
|
430
|
+
customFields: ordered,
|
|
431
|
+
}
|
|
432
|
+
}
|
|
433
|
+
|
|
434
|
+
export async function loadCustomFieldValues(opts: {
|
|
435
|
+
em: EntityManager
|
|
436
|
+
entityId: EntityId
|
|
437
|
+
recordIds: string[]
|
|
438
|
+
tenantIdByRecord?: Record<string, string | null | undefined>
|
|
439
|
+
organizationIdByRecord?: Record<string, string | null | undefined>
|
|
440
|
+
tenantFallbacks?: (string | null | undefined)[]
|
|
441
|
+
encryptionService?: TenantDataEncryptionService | null
|
|
442
|
+
}): Promise<Record<string, Record<string, unknown>>> {
|
|
443
|
+
const { em, entityId, recordIds } = opts
|
|
444
|
+
if (!Array.isArray(recordIds) || recordIds.length === 0) return {}
|
|
445
|
+
|
|
446
|
+
const normalizedRecordIds = recordIds.map((id) => String(id))
|
|
447
|
+
let encryptionService: TenantDataEncryptionService | null | undefined
|
|
448
|
+
const encryptionCache = new Map<string | null, string | null>()
|
|
449
|
+
const getEncryptionService = () => {
|
|
450
|
+
if (encryptionService !== undefined) return encryptionService
|
|
451
|
+
encryptionService = resolveTenantEncryptionService(em, opts.encryptionService)
|
|
452
|
+
return encryptionService
|
|
453
|
+
}
|
|
454
|
+
const tenantCandidates = new Set<string | null>()
|
|
455
|
+
tenantCandidates.add(null)
|
|
456
|
+
if (opts.tenantIdByRecord) {
|
|
457
|
+
for (const val of Object.values(opts.tenantIdByRecord)) {
|
|
458
|
+
tenantCandidates.add(val ? String(val) : null)
|
|
459
|
+
}
|
|
460
|
+
}
|
|
461
|
+
if (opts.tenantFallbacks) {
|
|
462
|
+
for (const val of opts.tenantFallbacks) tenantCandidates.add(val ? String(val) : null)
|
|
463
|
+
}
|
|
464
|
+
const fallbackTenant = (opts.tenantFallbacks || []).find((t) => t != null) ?? null
|
|
465
|
+
|
|
466
|
+
const tenantList = Array.from(tenantCandidates)
|
|
467
|
+
const tenantNonNull = tenantList.filter((t): t is string => t !== null)
|
|
468
|
+
const tenantFilter = tenantNonNull.length
|
|
469
|
+
? { tenantId: { $in: [...tenantNonNull, null] as any } }
|
|
470
|
+
: { tenantId: null }
|
|
471
|
+
const cfRows = await em.find(CustomFieldValue, {
|
|
472
|
+
entityId: entityId as any,
|
|
473
|
+
recordId: { $in: normalizedRecordIds as any },
|
|
474
|
+
deletedAt: null,
|
|
475
|
+
...(tenantList.length ? tenantFilter : {}),
|
|
476
|
+
})
|
|
477
|
+
|
|
478
|
+
if (!cfRows.length) return {}
|
|
479
|
+
|
|
480
|
+
const allKeys = Array.from(new Set(cfRows.map((row) => String(row.fieldKey))))
|
|
481
|
+
const organizationCandidates = new Set<string | null>()
|
|
482
|
+
organizationCandidates.add(null)
|
|
483
|
+
if (opts.organizationIdByRecord) {
|
|
484
|
+
for (const val of Object.values(opts.organizationIdByRecord)) {
|
|
485
|
+
organizationCandidates.add(val ? String(val) : null)
|
|
486
|
+
}
|
|
487
|
+
}
|
|
488
|
+
for (const row of cfRows) {
|
|
489
|
+
organizationCandidates.add(row.organizationId ? String(row.organizationId) : null)
|
|
490
|
+
}
|
|
491
|
+
const orgList = Array.from(organizationCandidates)
|
|
492
|
+
|
|
493
|
+
const defs = allKeys.length
|
|
494
|
+
? await em.find(CustomFieldDef, {
|
|
495
|
+
entityId: entityId as any,
|
|
496
|
+
key: { $in: allKeys as any },
|
|
497
|
+
deletedAt: null,
|
|
498
|
+
isActive: true,
|
|
499
|
+
...(tenantList.length ? { tenantId: tenantFilter.tenantId } : {}),
|
|
500
|
+
organizationId: { $in: orgList as any },
|
|
501
|
+
})
|
|
502
|
+
: []
|
|
503
|
+
|
|
504
|
+
const defsByKey = new Map<string, CustomFieldDef[]>()
|
|
505
|
+
for (const def of defs) {
|
|
506
|
+
const list = defsByKey.get(def.key) || []
|
|
507
|
+
list.push(def)
|
|
508
|
+
defsByKey.set(def.key, list)
|
|
509
|
+
}
|
|
510
|
+
|
|
511
|
+
const pickDefinition = (fieldKey: string, organizationId: string | null, tenantId: string | null) => {
|
|
512
|
+
const candidates = defsByKey.get(fieldKey)
|
|
513
|
+
if (!candidates || candidates.length === 0) return null
|
|
514
|
+
const active = candidates.filter((opt) => opt.isActive !== false && !opt.deletedAt)
|
|
515
|
+
const list = active.length ? active : candidates
|
|
516
|
+
if (organizationId && tenantId) {
|
|
517
|
+
const exact = list.find((opt) => opt.organizationId === organizationId && opt.tenantId === tenantId)
|
|
518
|
+
if (exact) return exact
|
|
519
|
+
}
|
|
520
|
+
if (organizationId) {
|
|
521
|
+
const orgMatch = list.find((opt) => opt.organizationId === organizationId && (!tenantId || opt.tenantId == null || opt.tenantId === tenantId))
|
|
522
|
+
if (orgMatch) return orgMatch
|
|
523
|
+
}
|
|
524
|
+
if (tenantId) {
|
|
525
|
+
const tenantMatch = list.find((opt) => opt.organizationId == null && opt.tenantId === tenantId)
|
|
526
|
+
if (tenantMatch) return tenantMatch
|
|
527
|
+
}
|
|
528
|
+
const global = list.find((opt) => opt.organizationId == null && opt.tenantId == null)
|
|
529
|
+
return global ?? list[0]
|
|
530
|
+
}
|
|
531
|
+
|
|
532
|
+
const valueFromRow = (row: CustomFieldValue): unknown => {
|
|
533
|
+
if (row.valueMultiline !== null && row.valueMultiline !== undefined) return row.valueMultiline
|
|
534
|
+
if (row.valueText !== null && row.valueText !== undefined) return row.valueText
|
|
535
|
+
if (row.valueInt !== null && row.valueInt !== undefined) return row.valueInt
|
|
536
|
+
if (row.valueFloat !== null && row.valueFloat !== undefined) return row.valueFloat
|
|
537
|
+
if (row.valueBool !== null && row.valueBool !== undefined) return row.valueBool
|
|
538
|
+
return null
|
|
539
|
+
}
|
|
540
|
+
|
|
541
|
+
type Bucket = { orgId: string | null; tenantId: string | null; values: unknown[]; def?: CustomFieldDef | null; encrypted?: boolean }
|
|
542
|
+
const buckets = new Map<string, Bucket>()
|
|
543
|
+
|
|
544
|
+
for (const row of cfRows) {
|
|
545
|
+
const recordId = String(row.recordId)
|
|
546
|
+
const key = String(row.fieldKey)
|
|
547
|
+
const bucketKey = `${recordId}::${key}`
|
|
548
|
+
const orgId = row.organizationId ? String(row.organizationId) : null
|
|
549
|
+
const tenantId = row.tenantId ? String(row.tenantId) : null
|
|
550
|
+
const resolvedOrgId = orgId ?? (opts.organizationIdByRecord?.[recordId] ?? null)
|
|
551
|
+
const resolvedTenantId = tenantId ?? (opts.tenantIdByRecord?.[recordId] ?? fallbackTenant)
|
|
552
|
+
const def = pickDefinition(key, resolvedOrgId, resolvedTenantId)
|
|
553
|
+
const encrypted = Boolean(def?.configJson && (def as any).configJson?.encrypted)
|
|
554
|
+
const value = valueFromRow(row)
|
|
555
|
+
const decrypted = encrypted
|
|
556
|
+
? await decryptCustomFieldValue(value, resolvedTenantId ?? tenantId ?? null, getEncryptionService(), encryptionCache)
|
|
557
|
+
: value
|
|
558
|
+
const existing = buckets.get(bucketKey)
|
|
559
|
+
if (existing) {
|
|
560
|
+
if (existing.orgId == null && resolvedOrgId) existing.orgId = resolvedOrgId
|
|
561
|
+
if (existing.tenantId == null && resolvedTenantId) existing.tenantId = resolvedTenantId
|
|
562
|
+
if (existing.def == null && def) existing.def = def
|
|
563
|
+
existing.encrypted = existing.encrypted || encrypted
|
|
564
|
+
existing.values.push(decrypted)
|
|
565
|
+
} else {
|
|
566
|
+
buckets.set(bucketKey, { orgId: resolvedOrgId, tenantId: resolvedTenantId, values: [decrypted], def: def ?? null, encrypted })
|
|
567
|
+
}
|
|
568
|
+
}
|
|
569
|
+
|
|
570
|
+
const result: Record<string, Record<string, unknown>> = {}
|
|
571
|
+
for (const [compoundKey, bucket] of buckets.entries()) {
|
|
572
|
+
const [recordId, fieldKey] = compoundKey.split('::')
|
|
573
|
+
if (!result[recordId]) result[recordId] = {}
|
|
574
|
+
const prefixed = `cf_${fieldKey}`
|
|
575
|
+
const def = bucket.def ?? pickDefinition(fieldKey, bucket.orgId ?? (opts.organizationIdByRecord?.[recordId] ?? null), bucket.tenantId ?? (opts.tenantIdByRecord?.[recordId] ?? null))
|
|
576
|
+
if (def && def.configJson && typeof def.configJson === 'object' && (def.configJson as any).multi) {
|
|
577
|
+
const cleaned = bucket.values.filter((v) => v !== undefined && v !== null)
|
|
578
|
+
result[recordId][prefixed] = cleaned
|
|
579
|
+
} else if (bucket.values.length > 1) {
|
|
580
|
+
const cleaned = bucket.values.filter((v) => v !== undefined)
|
|
581
|
+
result[recordId][prefixed] = cleaned
|
|
582
|
+
} else {
|
|
583
|
+
result[recordId][prefixed] = bucket.values[0] ?? null
|
|
584
|
+
}
|
|
585
|
+
}
|
|
586
|
+
|
|
587
|
+
return result
|
|
588
|
+
}
|
|
589
|
+
|
|
590
|
+
export function summarizeCustomFields(record: Record<string, unknown>): CustomFieldSnapshot {
|
|
591
|
+
const entries = extractAllCustomFieldEntries(record)
|
|
592
|
+
const values = Object.fromEntries(
|
|
593
|
+
Object.entries(entries).map(([prefixedKey, value]) => [
|
|
594
|
+
prefixedKey.replace(/^cf_/, ''),
|
|
595
|
+
value,
|
|
596
|
+
]),
|
|
597
|
+
)
|
|
598
|
+
const customValues = Object.keys(values).length ? values : null
|
|
599
|
+
const customFields = Object.entries(values).map(([key, value]) => ({
|
|
600
|
+
key,
|
|
601
|
+
label: key,
|
|
602
|
+
value,
|
|
603
|
+
kind: null,
|
|
604
|
+
multi: Array.isArray(value),
|
|
605
|
+
}))
|
|
606
|
+
return { entries, customValues, customFields }
|
|
607
|
+
}
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
export class CrudHttpError extends Error {
|
|
2
|
+
status: number
|
|
3
|
+
body: Record<string, any>
|
|
4
|
+
|
|
5
|
+
constructor(status: number, body?: Record<string, any> | string) {
|
|
6
|
+
const normalizedBody = typeof body === 'string' ? { error: body } : body ?? {}
|
|
7
|
+
super(typeof body === 'string' ? body : normalizedBody.error ?? 'Request failed')
|
|
8
|
+
this.status = status
|
|
9
|
+
this.body = normalizedBody
|
|
10
|
+
}
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
export function badRequest(message: string): CrudHttpError {
|
|
14
|
+
return new CrudHttpError(400, { error: message })
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
export function forbidden(message = 'Forbidden'): CrudHttpError {
|
|
18
|
+
return new CrudHttpError(403, { error: message })
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
export function notFound(message = 'Not found'): CrudHttpError {
|
|
22
|
+
return new CrudHttpError(404, { error: message })
|
|
23
|
+
}
|