@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,29 @@
|
|
|
1
|
+
export type CrudEventAction = 'created' | 'updated' | 'deleted'
|
|
2
|
+
|
|
3
|
+
export type CrudEntityIdentifiers = {
|
|
4
|
+
id: string
|
|
5
|
+
organizationId: string | null
|
|
6
|
+
tenantId: string | null
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
export type CrudEmitContext<TEntity = unknown> = {
|
|
10
|
+
action: CrudEventAction
|
|
11
|
+
entity: TEntity
|
|
12
|
+
identifiers: CrudEntityIdentifiers
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
export type CrudEventsConfig<TEntity = unknown> = {
|
|
16
|
+
module: string
|
|
17
|
+
entity: string
|
|
18
|
+
persistent?: boolean
|
|
19
|
+
buildPayload?: (ctx: CrudEmitContext<TEntity>) => unknown
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
export type CrudIndexerConfig<TEntity = unknown> = {
|
|
23
|
+
entityType: string
|
|
24
|
+
buildUpsertPayload?: (ctx: CrudEmitContext<TEntity>) => unknown
|
|
25
|
+
buildDeletePayload?: (ctx: CrudEmitContext<TEntity>) => unknown
|
|
26
|
+
cacheAliases?: string[]
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
export type CrudIdentifierResolver<TEntity = unknown> = (entity: TEntity, action: CrudEventAction) => CrudEntityIdentifiers
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
import type { DataEngine } from '../data/engine'
|
|
2
|
+
|
|
3
|
+
type CustomFieldValueInput = Parameters<DataEngine['setCustomFields']>[0]['values']
|
|
4
|
+
|
|
5
|
+
export function normalizeCustomFieldValues(values: Record<string, unknown>): CustomFieldValueInput {
|
|
6
|
+
const result: CustomFieldValueInput = {}
|
|
7
|
+
for (const [key, value] of Object.entries(values)) {
|
|
8
|
+
if (Array.isArray(value)) {
|
|
9
|
+
result[key] = value.map((entry) => normalizePrimitive(entry)) as CustomFieldValueInput[string]
|
|
10
|
+
} else {
|
|
11
|
+
result[key] = normalizePrimitive(value)
|
|
12
|
+
}
|
|
13
|
+
}
|
|
14
|
+
return result
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
function normalizePrimitive(value: unknown): CustomFieldValueInput[string] {
|
|
18
|
+
if (
|
|
19
|
+
value === null ||
|
|
20
|
+
value === undefined ||
|
|
21
|
+
typeof value === 'string' ||
|
|
22
|
+
typeof value === 'number' ||
|
|
23
|
+
typeof value === 'boolean'
|
|
24
|
+
) {
|
|
25
|
+
return value as CustomFieldValueInput[string]
|
|
26
|
+
}
|
|
27
|
+
return String(value) as CustomFieldValueInput[string]
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
export function normalizeCustomFieldResponse(
|
|
31
|
+
values: Record<string, unknown> | null | undefined,
|
|
32
|
+
): Record<string, unknown> | undefined {
|
|
33
|
+
if (!values) return undefined
|
|
34
|
+
const entries: Record<string, unknown> = {}
|
|
35
|
+
for (const [key, value] of Object.entries(values)) {
|
|
36
|
+
if (value === undefined) continue
|
|
37
|
+
if (key.startsWith('cf_') || key.startsWith('cf:')) {
|
|
38
|
+
const normalized = key.slice(3)
|
|
39
|
+
if (normalized) entries[normalized] = value
|
|
40
|
+
continue
|
|
41
|
+
}
|
|
42
|
+
entries[key] = value
|
|
43
|
+
}
|
|
44
|
+
return Object.keys(entries).length ? entries : undefined
|
|
45
|
+
}
|
|
@@ -0,0 +1,562 @@
|
|
|
1
|
+
import type { EntityData, EntityName, FilterQuery, RequiredEntityData } from '@mikro-orm/core'
|
|
2
|
+
import type { EntityManager } from '@mikro-orm/postgresql'
|
|
3
|
+
import type { AwilixContainer } from 'awilix'
|
|
4
|
+
import { setRecordCustomFields } from '@open-mercato/core/modules/entities/lib/helpers'
|
|
5
|
+
import { validateCustomFieldValuesServer } from '@open-mercato/core/modules/entities/lib/validation'
|
|
6
|
+
import type { EventBus } from '@open-mercato/events/types'
|
|
7
|
+
import type {
|
|
8
|
+
CrudEventAction,
|
|
9
|
+
CrudEventsConfig,
|
|
10
|
+
CrudIndexerConfig,
|
|
11
|
+
CrudEntityIdentifiers,
|
|
12
|
+
} from '../crud/types'
|
|
13
|
+
import { CrudHttpError } from '../crud/errors'
|
|
14
|
+
import { normalizeCustomFieldValues } from '../custom-fields/normalize'
|
|
15
|
+
import { parseBooleanToken } from '../boolean'
|
|
16
|
+
|
|
17
|
+
const COVERAGE_REFRESH_INTERVAL_MS = 5 * 60 * 1000
|
|
18
|
+
const coverageRefreshTracker = new Map<string, number>()
|
|
19
|
+
|
|
20
|
+
function shouldTriggerCoverageRefresh(entityType: string | undefined, tenantId: string | null): boolean {
|
|
21
|
+
if (!entityType) return false
|
|
22
|
+
const key = `${entityType}|${tenantId ?? '__null__'}`
|
|
23
|
+
const now = Date.now()
|
|
24
|
+
const last = coverageRefreshTracker.get(key) ?? 0
|
|
25
|
+
if (now - last < COVERAGE_REFRESH_INTERVAL_MS) return false
|
|
26
|
+
coverageRefreshTracker.set(key, now)
|
|
27
|
+
return true
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
type CustomEntityValues = Record<string, unknown>
|
|
31
|
+
|
|
32
|
+
type QueuedCrudSideEffect = {
|
|
33
|
+
action: CrudEventAction
|
|
34
|
+
entity: unknown
|
|
35
|
+
identifiers: CrudEntityIdentifiers
|
|
36
|
+
events?: CrudEventsConfig<unknown>
|
|
37
|
+
indexer?: CrudIndexerConfig<unknown>
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
export interface DataEngine {
|
|
41
|
+
setCustomFields(opts: {
|
|
42
|
+
entityId: string
|
|
43
|
+
recordId: string
|
|
44
|
+
organizationId?: string | null
|
|
45
|
+
tenantId?: string | null
|
|
46
|
+
values: Record<string, string | number | boolean | null | undefined | Array<string | number | boolean | null | undefined>>
|
|
47
|
+
notify?: boolean // default true -> emit '<module>.<entity>.updated'
|
|
48
|
+
}): Promise<void>
|
|
49
|
+
|
|
50
|
+
// Storage for user-defined entities (doc-based)
|
|
51
|
+
createCustomEntityRecord(opts: {
|
|
52
|
+
entityId: string // '<module>:<entity>'
|
|
53
|
+
recordId?: string // optional; auto-generate if not provided
|
|
54
|
+
organizationId?: string | null
|
|
55
|
+
tenantId?: string | null
|
|
56
|
+
values: CustomEntityValues
|
|
57
|
+
notify?: boolean // keep event emitting as it is via setCustomFields (updated)
|
|
58
|
+
}): Promise<{ id: string }>
|
|
59
|
+
|
|
60
|
+
updateCustomEntityRecord(opts: {
|
|
61
|
+
entityId: string
|
|
62
|
+
recordId: string
|
|
63
|
+
organizationId?: string | null
|
|
64
|
+
tenantId?: string | null
|
|
65
|
+
values: CustomEntityValues
|
|
66
|
+
notify?: boolean // keep event emitting as it is via setCustomFields (updated)
|
|
67
|
+
}): Promise<void>
|
|
68
|
+
|
|
69
|
+
deleteCustomEntityRecord(opts: {
|
|
70
|
+
entityId: string
|
|
71
|
+
recordId: string
|
|
72
|
+
organizationId?: string | null
|
|
73
|
+
tenantId?: string | null
|
|
74
|
+
soft?: boolean // default true: sets deleted_at
|
|
75
|
+
notify?: boolean // keep event emitting as it is (no extra events here)
|
|
76
|
+
}): Promise<void>
|
|
77
|
+
|
|
78
|
+
// Generic ORM-backed entity operations used by CrudFactory
|
|
79
|
+
createOrmEntity<T extends object>(opts: {
|
|
80
|
+
entity: EntityName<T>
|
|
81
|
+
data: EntityData<T>
|
|
82
|
+
}): Promise<T>
|
|
83
|
+
|
|
84
|
+
updateOrmEntity<T extends object>(opts: {
|
|
85
|
+
entity: EntityName<T>
|
|
86
|
+
where: FilterQuery<T>
|
|
87
|
+
apply: (current: T) => Promise<void> | void
|
|
88
|
+
}): Promise<T | null>
|
|
89
|
+
|
|
90
|
+
deleteOrmEntity<T extends object>(opts: {
|
|
91
|
+
entity: EntityName<T>
|
|
92
|
+
where: FilterQuery<T>
|
|
93
|
+
soft?: boolean
|
|
94
|
+
softDeleteField?: keyof T & string
|
|
95
|
+
}): Promise<T | null>
|
|
96
|
+
|
|
97
|
+
emitOrmEntityEvent<T>(opts: {
|
|
98
|
+
action: CrudEventAction
|
|
99
|
+
entity: T
|
|
100
|
+
events?: CrudEventsConfig<T>
|
|
101
|
+
indexer?: CrudIndexerConfig<T>
|
|
102
|
+
identifiers: CrudEntityIdentifiers
|
|
103
|
+
}): Promise<void>
|
|
104
|
+
|
|
105
|
+
markOrmEntityChange<T>(opts: {
|
|
106
|
+
action: CrudEventAction
|
|
107
|
+
entity: T | null | undefined
|
|
108
|
+
events?: CrudEventsConfig<T>
|
|
109
|
+
indexer?: CrudIndexerConfig<T>
|
|
110
|
+
identifiers: CrudEntityIdentifiers
|
|
111
|
+
}): void
|
|
112
|
+
|
|
113
|
+
flushOrmEntityChanges(): Promise<void>
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
export class DefaultDataEngine implements DataEngine {
|
|
117
|
+
private pendingSideEffects = new Map<string, QueuedCrudSideEffect>()
|
|
118
|
+
constructor(private em: EntityManager, private container: AwilixContainer) {}
|
|
119
|
+
|
|
120
|
+
async setCustomFields(opts: Parameters<DataEngine['setCustomFields']>[0]): Promise<void> {
|
|
121
|
+
const { entityId, recordId, organizationId = null, tenantId = null, values } = opts
|
|
122
|
+
await this.validateCustomFieldValues(entityId, organizationId, tenantId, values as Record<string, unknown>)
|
|
123
|
+
let encryptionService: any = null
|
|
124
|
+
try {
|
|
125
|
+
encryptionService = this.container.resolve('tenantEncryptionService') as any
|
|
126
|
+
} catch {
|
|
127
|
+
encryptionService = null
|
|
128
|
+
}
|
|
129
|
+
await setRecordCustomFields(this.em, {
|
|
130
|
+
entityId,
|
|
131
|
+
recordId,
|
|
132
|
+
organizationId,
|
|
133
|
+
tenantId,
|
|
134
|
+
values,
|
|
135
|
+
encryptionService,
|
|
136
|
+
})
|
|
137
|
+
if (opts.notify !== false) {
|
|
138
|
+
let bus: EventBus | null = null
|
|
139
|
+
try {
|
|
140
|
+
bus = (this.container.resolve('eventBus') as EventBus)
|
|
141
|
+
} catch {
|
|
142
|
+
bus = null
|
|
143
|
+
}
|
|
144
|
+
if (bus) {
|
|
145
|
+
const [mod, ent] = (entityId || '').split(':')
|
|
146
|
+
if (mod && ent) {
|
|
147
|
+
try {
|
|
148
|
+
await bus.emitEvent(`${mod}.${ent}.updated`, { id: recordId, organizationId, tenantId }, { persistent: true })
|
|
149
|
+
} catch {
|
|
150
|
+
// non-blocking
|
|
151
|
+
}
|
|
152
|
+
}
|
|
153
|
+
}
|
|
154
|
+
}
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
private normalizeDocValues(values: CustomEntityValues): CustomEntityValues {
|
|
158
|
+
const out: CustomEntityValues = {}
|
|
159
|
+
for (const [k, v] of Object.entries(values || {})) {
|
|
160
|
+
// Never allow callers to override reserved identifiers in the doc
|
|
161
|
+
if (k === 'id' || k === 'entity_id' || k === 'entityId') continue
|
|
162
|
+
// Accept both 'cf_<key>' and 'cf:<key>' inputs and normalize to 'cf:<key>'
|
|
163
|
+
if (k.startsWith('cf_')) out[`cf:${k.slice(3)}`] = v
|
|
164
|
+
else out[k] = v
|
|
165
|
+
}
|
|
166
|
+
return out
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
private backcompatEavEnabled(): boolean {
|
|
170
|
+
try {
|
|
171
|
+
return parseBooleanToken(process.env.ENTITIES_BACKCOMPAT_EAV_FOR_CUSTOM ?? '') === true
|
|
172
|
+
} catch { return false }
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
private async ensureStorageTableExists(): Promise<void> {
|
|
176
|
+
const knex = this.em.getConnection().getKnex()
|
|
177
|
+
const exists = await knex('information_schema.tables')
|
|
178
|
+
.where({ table_name: 'custom_entities_storage' })
|
|
179
|
+
.first()
|
|
180
|
+
if (!exists) {
|
|
181
|
+
throw new Error('custom_entities_storage table is missing. Run migrations (yarn db:migrate).')
|
|
182
|
+
}
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
private normalizeValuesForValidation(values: Record<string, unknown> | undefined | null): Record<string, unknown> {
|
|
186
|
+
if (!values) return {}
|
|
187
|
+
const out: Record<string, unknown> = {}
|
|
188
|
+
for (const [key, value] of Object.entries(values)) {
|
|
189
|
+
if (value === undefined) continue
|
|
190
|
+
if (key.startsWith('cf_') || key.startsWith('cf:')) {
|
|
191
|
+
const normalized = key.slice(3)
|
|
192
|
+
if (normalized) out[normalized] = value
|
|
193
|
+
continue
|
|
194
|
+
}
|
|
195
|
+
out[key] = value
|
|
196
|
+
}
|
|
197
|
+
return out
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
private async validateCustomFieldValues(
|
|
201
|
+
entityId: string,
|
|
202
|
+
organizationId: string | null,
|
|
203
|
+
tenantId: string | null,
|
|
204
|
+
values: Record<string, unknown> | undefined | null,
|
|
205
|
+
): Promise<void> {
|
|
206
|
+
const prepared = this.normalizeValuesForValidation(values)
|
|
207
|
+
if (!entityId || Object.keys(prepared).length === 0) return
|
|
208
|
+
const result = await validateCustomFieldValuesServer(this.em, {
|
|
209
|
+
entityId,
|
|
210
|
+
organizationId,
|
|
211
|
+
tenantId,
|
|
212
|
+
values: prepared,
|
|
213
|
+
})
|
|
214
|
+
if (!result.ok) {
|
|
215
|
+
throw new CrudHttpError(400, { error: 'Validation failed', fields: result.fieldErrors })
|
|
216
|
+
}
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
async createCustomEntityRecord(opts: Parameters<DataEngine['createCustomEntityRecord']>[0]): Promise<{ id: string }> {
|
|
220
|
+
const knex = this.em.getConnection().getKnex()
|
|
221
|
+
await this.ensureStorageTableExists()
|
|
222
|
+
await this.validateCustomFieldValues(opts.entityId, opts.organizationId ?? null, opts.tenantId ?? null, opts.values)
|
|
223
|
+
const rawId = String(opts.recordId ?? '').trim()
|
|
224
|
+
const isUuid = /^[0-9a-f]{8}-[0-9a-f]{4}-[1-5][0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/i.test(rawId)
|
|
225
|
+
const sentinel = rawId.toLowerCase()
|
|
226
|
+
const shouldGenerate = !rawId || !isUuid || sentinel === 'create' || sentinel === 'new' || sentinel === 'null' || sentinel === 'undefined'
|
|
227
|
+
const id = shouldGenerate ? ((): string => {
|
|
228
|
+
const g = globalThis as { crypto?: { randomUUID?: () => string } }
|
|
229
|
+
if (g.crypto?.randomUUID) return g.crypto.randomUUID()
|
|
230
|
+
// Fallback UUIDv4 generator
|
|
231
|
+
return 'xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx'.replace(/[xy]/g, (c) => {
|
|
232
|
+
const r = (Math.random() * 16) | 0
|
|
233
|
+
const v = c === 'x' ? r : (r & 0x3) | 0x8
|
|
234
|
+
return v.toString(16)
|
|
235
|
+
})
|
|
236
|
+
})() : rawId
|
|
237
|
+
const orgId = opts.organizationId ?? null
|
|
238
|
+
const tenantId = opts.tenantId ?? null
|
|
239
|
+
const doc: Record<string, unknown> = { id, ...this.normalizeDocValues(opts.values || {}) }
|
|
240
|
+
|
|
241
|
+
const payload = {
|
|
242
|
+
entity_type: opts.entityId,
|
|
243
|
+
entity_id: id,
|
|
244
|
+
organization_id: orgId,
|
|
245
|
+
tenant_id: tenantId,
|
|
246
|
+
doc,
|
|
247
|
+
updated_at: knex.fn.now(),
|
|
248
|
+
created_at: knex.fn.now(),
|
|
249
|
+
deleted_at: null,
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
// Upsert by scoped uniqueness
|
|
253
|
+
try {
|
|
254
|
+
await knex('custom_entities_storage')
|
|
255
|
+
.insert(payload)
|
|
256
|
+
.onConflict(['entity_type', 'entity_id', 'organization_id'])
|
|
257
|
+
.merge({ doc: payload.doc, updated_at: knex.fn.now(), deleted_at: null })
|
|
258
|
+
} catch {
|
|
259
|
+
// Fallback for global scope uniqueness
|
|
260
|
+
try {
|
|
261
|
+
const updated = await knex('custom_entities_storage')
|
|
262
|
+
.where({ entity_type: opts.entityId, entity_id: id, organization_id: orgId })
|
|
263
|
+
.update({ doc: payload.doc, updated_at: knex.fn.now(), deleted_at: null })
|
|
264
|
+
if (!updated) {
|
|
265
|
+
await knex('custom_entities_storage').insert(payload)
|
|
266
|
+
}
|
|
267
|
+
} catch (err) {
|
|
268
|
+
// Surface a clear error so it doesn't silently fall back only to EAV
|
|
269
|
+
throw err
|
|
270
|
+
}
|
|
271
|
+
}
|
|
272
|
+
|
|
273
|
+
// Optional EAV backward compatibility (disabled by default)
|
|
274
|
+
if (this.backcompatEavEnabled() && opts.values && Object.keys(opts.values).length > 0) {
|
|
275
|
+
await this.setCustomFields({
|
|
276
|
+
entityId: opts.entityId,
|
|
277
|
+
recordId: id,
|
|
278
|
+
organizationId: orgId,
|
|
279
|
+
tenantId: tenantId,
|
|
280
|
+
values: normalizeCustomFieldValues(opts.values),
|
|
281
|
+
notify: opts.notify, // defaults to true
|
|
282
|
+
})
|
|
283
|
+
}
|
|
284
|
+
|
|
285
|
+
return { id }
|
|
286
|
+
}
|
|
287
|
+
|
|
288
|
+
async updateCustomEntityRecord(opts: Parameters<DataEngine['updateCustomEntityRecord']>[0]): Promise<void> {
|
|
289
|
+
const knex = this.em.getConnection().getKnex()
|
|
290
|
+
await this.validateCustomFieldValues(opts.entityId, opts.organizationId ?? null, opts.tenantId ?? null, opts.values)
|
|
291
|
+
const id = String(opts.recordId)
|
|
292
|
+
const orgId = opts.organizationId ?? null
|
|
293
|
+
const tenantId = opts.tenantId ?? null
|
|
294
|
+
|
|
295
|
+
// Merge doc shallowly: load existing doc and overlay
|
|
296
|
+
await this.ensureStorageTableExists()
|
|
297
|
+
const row = await knex('custom_entities_storage')
|
|
298
|
+
.where({ entity_type: opts.entityId, entity_id: id, organization_id: orgId })
|
|
299
|
+
.first()
|
|
300
|
+
const prevDoc: Record<string, unknown> = row?.doc || { id }
|
|
301
|
+
const nextDoc: Record<string, unknown> = { ...prevDoc, ...this.normalizeDocValues(opts.values || {}), id }
|
|
302
|
+
try {
|
|
303
|
+
const updated = await knex('custom_entities_storage')
|
|
304
|
+
.where({ entity_type: opts.entityId, entity_id: id, organization_id: orgId })
|
|
305
|
+
.update({ doc: nextDoc, updated_at: knex.fn.now(), deleted_at: null })
|
|
306
|
+
if (!updated) {
|
|
307
|
+
await knex('custom_entities_storage').insert({
|
|
308
|
+
entity_type: opts.entityId,
|
|
309
|
+
entity_id: id,
|
|
310
|
+
organization_id: orgId,
|
|
311
|
+
tenant_id: tenantId,
|
|
312
|
+
doc: nextDoc,
|
|
313
|
+
created_at: knex.fn.now(),
|
|
314
|
+
updated_at: knex.fn.now(),
|
|
315
|
+
deleted_at: null,
|
|
316
|
+
})
|
|
317
|
+
}
|
|
318
|
+
} catch (err) {
|
|
319
|
+
throw err
|
|
320
|
+
}
|
|
321
|
+
|
|
322
|
+
// Optional EAV backward compatibility (disabled by default)
|
|
323
|
+
if (this.backcompatEavEnabled() && opts.values && Object.keys(opts.values).length > 0) {
|
|
324
|
+
await this.setCustomFields({
|
|
325
|
+
entityId: opts.entityId,
|
|
326
|
+
recordId: id,
|
|
327
|
+
organizationId: orgId,
|
|
328
|
+
tenantId: tenantId,
|
|
329
|
+
values: normalizeCustomFieldValues(opts.values),
|
|
330
|
+
notify: opts.notify, // defaults to true
|
|
331
|
+
})
|
|
332
|
+
}
|
|
333
|
+
}
|
|
334
|
+
|
|
335
|
+
async deleteCustomEntityRecord(opts: Parameters<DataEngine['deleteCustomEntityRecord']>[0]): Promise<void> {
|
|
336
|
+
const knex = this.em.getConnection().getKnex()
|
|
337
|
+
const id = String(opts.recordId)
|
|
338
|
+
const orgId = opts.organizationId ?? null
|
|
339
|
+
const soft = opts.soft !== false
|
|
340
|
+
|
|
341
|
+
if (soft) {
|
|
342
|
+
await knex('custom_entities_storage')
|
|
343
|
+
.where({ entity_type: opts.entityId, entity_id: id, organization_id: orgId })
|
|
344
|
+
.update({ deleted_at: knex.fn.now(), updated_at: knex.fn.now() })
|
|
345
|
+
} else {
|
|
346
|
+
await knex('custom_entities_storage')
|
|
347
|
+
.where({ entity_type: opts.entityId, entity_id: id, organization_id: orgId })
|
|
348
|
+
.delete()
|
|
349
|
+
}
|
|
350
|
+
|
|
351
|
+
// Soft-delete EAV values to preserve current behavior
|
|
352
|
+
try {
|
|
353
|
+
const { CustomFieldValue } = await import('@open-mercato/core/modules/entities/data/entities')
|
|
354
|
+
const values = await this.em.find(CustomFieldValue, {
|
|
355
|
+
entityId: opts.entityId,
|
|
356
|
+
recordId: id,
|
|
357
|
+
organizationId: orgId,
|
|
358
|
+
tenantId: opts.tenantId ?? null,
|
|
359
|
+
})
|
|
360
|
+
const now = new Date()
|
|
361
|
+
const mutated = values.filter((record) => {
|
|
362
|
+
if (record.deletedAt) return false
|
|
363
|
+
record.deletedAt = now
|
|
364
|
+
return true
|
|
365
|
+
})
|
|
366
|
+
if (mutated.length) await this.em.persistAndFlush(values)
|
|
367
|
+
} catch { /* non-blocking */ }
|
|
368
|
+
}
|
|
369
|
+
|
|
370
|
+
async createOrmEntity<T extends object>(opts: { entity: EntityName<T>; data: EntityData<T> }): Promise<T> {
|
|
371
|
+
const entity = this.em.create(
|
|
372
|
+
opts.entity,
|
|
373
|
+
opts.data as RequiredEntityData<T, never, true>
|
|
374
|
+
)
|
|
375
|
+
await this.em.persistAndFlush(entity)
|
|
376
|
+
return entity
|
|
377
|
+
}
|
|
378
|
+
|
|
379
|
+
async updateOrmEntity<T extends object>(opts: {
|
|
380
|
+
entity: EntityName<T>
|
|
381
|
+
where: FilterQuery<T>
|
|
382
|
+
apply: (current: T) => Promise<void> | void
|
|
383
|
+
}): Promise<T | null> {
|
|
384
|
+
const current = await this.em.findOne(opts.entity, opts.where)
|
|
385
|
+
if (!current) return null
|
|
386
|
+
await opts.apply(current)
|
|
387
|
+
await this.em.persistAndFlush(current)
|
|
388
|
+
return current
|
|
389
|
+
}
|
|
390
|
+
|
|
391
|
+
async deleteOrmEntity<T extends object>(opts: {
|
|
392
|
+
entity: EntityName<T>
|
|
393
|
+
where: FilterQuery<T>
|
|
394
|
+
soft?: boolean
|
|
395
|
+
softDeleteField?: keyof T & string
|
|
396
|
+
}): Promise<T | null> {
|
|
397
|
+
const current = await this.em.findOne(opts.entity, opts.where)
|
|
398
|
+
if (!current) return null
|
|
399
|
+
if (opts.soft !== false) {
|
|
400
|
+
const field = opts.softDeleteField || ('deletedAt' as keyof T & string)
|
|
401
|
+
if (typeof current === 'object' && current !== null) {
|
|
402
|
+
;(current as Record<string, unknown>)[field] = new Date()
|
|
403
|
+
await this.em.persistAndFlush(current)
|
|
404
|
+
}
|
|
405
|
+
} else {
|
|
406
|
+
await this.em.removeAndFlush(current)
|
|
407
|
+
}
|
|
408
|
+
return current
|
|
409
|
+
}
|
|
410
|
+
|
|
411
|
+
async emitOrmEntityEvent<T>(opts: { action: CrudEventAction; entity: T; events?: CrudEventsConfig<T>; indexer?: CrudIndexerConfig<T>; identifiers: CrudEntityIdentifiers }): Promise<void> {
|
|
412
|
+
const { action, entity, events, indexer, identifiers } = opts
|
|
413
|
+
if (!events && !indexer) return
|
|
414
|
+
if (!identifiers?.id) return
|
|
415
|
+
|
|
416
|
+
let bus: EventBus | null = null
|
|
417
|
+
try {
|
|
418
|
+
bus = (this.container.resolve('eventBus') as EventBus)
|
|
419
|
+
} catch {
|
|
420
|
+
bus = null
|
|
421
|
+
}
|
|
422
|
+
if (!bus) return
|
|
423
|
+
|
|
424
|
+
const ctx = {
|
|
425
|
+
action,
|
|
426
|
+
entity,
|
|
427
|
+
identifiers: {
|
|
428
|
+
id: identifiers.id,
|
|
429
|
+
organizationId: identifiers.organizationId ?? null,
|
|
430
|
+
tenantId: identifiers.tenantId ?? null,
|
|
431
|
+
},
|
|
432
|
+
}
|
|
433
|
+
|
|
434
|
+
if (events) {
|
|
435
|
+
const eventName = `${events.module}.${events.entity}.${action}`
|
|
436
|
+
const payload = events.buildPayload
|
|
437
|
+
? events.buildPayload(ctx)
|
|
438
|
+
: {
|
|
439
|
+
id: ctx.identifiers.id,
|
|
440
|
+
organizationId: ctx.identifiers.organizationId,
|
|
441
|
+
tenantId: ctx.identifiers.tenantId,
|
|
442
|
+
}
|
|
443
|
+
try {
|
|
444
|
+
await bus.emitEvent(eventName, payload, { persistent: !!events.persistent })
|
|
445
|
+
} catch {
|
|
446
|
+
// non-blocking
|
|
447
|
+
}
|
|
448
|
+
}
|
|
449
|
+
|
|
450
|
+
if (indexer) {
|
|
451
|
+
const resolveCoverageBaseDelta = (): number | undefined => {
|
|
452
|
+
if (action === 'created') return 1
|
|
453
|
+
if (action === 'deleted') return -1
|
|
454
|
+
return undefined
|
|
455
|
+
}
|
|
456
|
+
const coverageBaseDelta = resolveCoverageBaseDelta()
|
|
457
|
+
|
|
458
|
+
if (action === 'deleted') {
|
|
459
|
+
const payload = indexer.buildDeletePayload
|
|
460
|
+
? indexer.buildDeletePayload(ctx)
|
|
461
|
+
: {
|
|
462
|
+
entityType: indexer.entityType,
|
|
463
|
+
recordId: ctx.identifiers.id,
|
|
464
|
+
organizationId: ctx.identifiers.organizationId,
|
|
465
|
+
tenantId: ctx.identifiers.tenantId,
|
|
466
|
+
}
|
|
467
|
+
const enrichedPayload = payload as Record<string, unknown>
|
|
468
|
+
enrichedPayload.crudAction = action
|
|
469
|
+
if (coverageBaseDelta !== undefined) enrichedPayload.coverageBaseDelta = coverageBaseDelta
|
|
470
|
+
try {
|
|
471
|
+
await bus.emitEvent('query_index.delete_one', enrichedPayload)
|
|
472
|
+
} catch {
|
|
473
|
+
// non-blocking
|
|
474
|
+
}
|
|
475
|
+
} else {
|
|
476
|
+
const payload = indexer.buildUpsertPayload
|
|
477
|
+
? indexer.buildUpsertPayload(ctx)
|
|
478
|
+
: {
|
|
479
|
+
entityType: indexer.entityType,
|
|
480
|
+
recordId: ctx.identifiers.id,
|
|
481
|
+
organizationId: ctx.identifiers.organizationId,
|
|
482
|
+
tenantId: ctx.identifiers.tenantId,
|
|
483
|
+
}
|
|
484
|
+
const enrichedPayload = payload as Record<string, unknown>
|
|
485
|
+
enrichedPayload.crudAction = action
|
|
486
|
+
if (coverageBaseDelta !== undefined) enrichedPayload.coverageBaseDelta = coverageBaseDelta
|
|
487
|
+
try {
|
|
488
|
+
await bus.emitEvent('query_index.upsert_one', enrichedPayload)
|
|
489
|
+
} catch {
|
|
490
|
+
// non-blocking
|
|
491
|
+
}
|
|
492
|
+
}
|
|
493
|
+
|
|
494
|
+
if (shouldTriggerCoverageRefresh(indexer.entityType, ctx.identifiers.tenantId ?? null)) {
|
|
495
|
+
void bus.emitEvent('query_index.coverage.refresh', {
|
|
496
|
+
entityType: indexer.entityType,
|
|
497
|
+
tenantId: ctx.identifiers.tenantId ?? null,
|
|
498
|
+
organizationId: null,
|
|
499
|
+
delayMs: 0,
|
|
500
|
+
}).catch(() => undefined)
|
|
501
|
+
}
|
|
502
|
+
}
|
|
503
|
+
}
|
|
504
|
+
|
|
505
|
+
markOrmEntityChange<T>(opts: { action: CrudEventAction; entity: T | null | undefined; events?: CrudEventsConfig<T>; indexer?: CrudIndexerConfig<T>; identifiers: CrudEntityIdentifiers }): void {
|
|
506
|
+
const { entity, identifiers } = opts
|
|
507
|
+
if (!entity) return
|
|
508
|
+
if (!identifiers?.id) return
|
|
509
|
+
const key = this.buildSideEffectKey(opts.action, identifiers)
|
|
510
|
+
const existing = this.pendingSideEffects.get(key)
|
|
511
|
+
if (existing) {
|
|
512
|
+
existing.entity = entity
|
|
513
|
+
existing.identifiers = {
|
|
514
|
+
id: identifiers.id,
|
|
515
|
+
organizationId: identifiers.organizationId ?? null,
|
|
516
|
+
tenantId: identifiers.tenantId ?? null,
|
|
517
|
+
}
|
|
518
|
+
if (opts.events) existing.events = opts.events as CrudEventsConfig<unknown>
|
|
519
|
+
if (opts.indexer) existing.indexer = opts.indexer as CrudIndexerConfig<unknown>
|
|
520
|
+
this.pendingSideEffects.set(key, existing)
|
|
521
|
+
return
|
|
522
|
+
}
|
|
523
|
+
const entry: QueuedCrudSideEffect = {
|
|
524
|
+
action: opts.action,
|
|
525
|
+
entity,
|
|
526
|
+
identifiers: {
|
|
527
|
+
id: identifiers.id,
|
|
528
|
+
organizationId: identifiers.organizationId ?? null,
|
|
529
|
+
tenantId: identifiers.tenantId ?? null,
|
|
530
|
+
},
|
|
531
|
+
}
|
|
532
|
+
if (opts.events) entry.events = opts.events as CrudEventsConfig<unknown>
|
|
533
|
+
if (opts.indexer) entry.indexer = opts.indexer as CrudIndexerConfig<unknown>
|
|
534
|
+
this.pendingSideEffects.set(key, entry)
|
|
535
|
+
}
|
|
536
|
+
|
|
537
|
+
async flushOrmEntityChanges(): Promise<void> {
|
|
538
|
+
if (!this.pendingSideEffects.size) return
|
|
539
|
+
const entries = Array.from(this.pendingSideEffects.values())
|
|
540
|
+
this.pendingSideEffects.clear()
|
|
541
|
+
for (const entry of entries) {
|
|
542
|
+
try {
|
|
543
|
+
await this.emitOrmEntityEvent({
|
|
544
|
+
action: entry.action,
|
|
545
|
+
entity: entry.entity,
|
|
546
|
+
identifiers: entry.identifiers,
|
|
547
|
+
events: entry.events as CrudEventsConfig<unknown>,
|
|
548
|
+
indexer: entry.indexer as CrudIndexerConfig<unknown>,
|
|
549
|
+
})
|
|
550
|
+
} catch {
|
|
551
|
+
// best-effort; continue with remaining side effects
|
|
552
|
+
}
|
|
553
|
+
}
|
|
554
|
+
}
|
|
555
|
+
|
|
556
|
+
private buildSideEffectKey(action: CrudEventAction, identifiers: CrudEntityIdentifiers): string {
|
|
557
|
+
const id = identifiers.id ?? ''
|
|
558
|
+
const org = identifiers.organizationId ?? ''
|
|
559
|
+
const tenant = identifiers.tenantId ?? ''
|
|
560
|
+
return [action, id, org, tenant].join('|')
|
|
561
|
+
}
|
|
562
|
+
}
|