@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,127 @@
|
|
|
1
|
+
import type { CacheStrategy } from '@open-mercato/cache'
|
|
2
|
+
import { analyzeCacheSegments, purgeCacheSegment } from '../cache/segments'
|
|
3
|
+
|
|
4
|
+
export type CrudCacheSegmentInfo = {
|
|
5
|
+
segment: string
|
|
6
|
+
resource: string | null
|
|
7
|
+
method: string | null
|
|
8
|
+
path: string | null
|
|
9
|
+
keyCount: number
|
|
10
|
+
keys: string[]
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
export type CrudCacheStats = {
|
|
14
|
+
generatedAt: string
|
|
15
|
+
segments: CrudCacheSegmentInfo[]
|
|
16
|
+
totalKeys: number
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
const CRUD_CACHE_PATTERN = 'crud|*'
|
|
20
|
+
export const CRUD_CACHE_STATS_KEY = 'crud-cache-stats'
|
|
21
|
+
|
|
22
|
+
function sanitizeSegment(value: string): string {
|
|
23
|
+
return value
|
|
24
|
+
.replace(/[^a-zA-Z0-9:_/|-]/g, '-')
|
|
25
|
+
.replace(/-{2,}/g, '-')
|
|
26
|
+
.replace(/^-+|-+$/g, '')
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
function normalizePathSegment(path: string | null | undefined, resource: string | null | undefined): string | null {
|
|
30
|
+
if (!path) return null
|
|
31
|
+
let trimmed = path.trim()
|
|
32
|
+
if (!trimmed) return null
|
|
33
|
+
trimmed = trimmed.replace(/^https?:\/\/[^/]+/i, '')
|
|
34
|
+
trimmed = trimmed.replace(/^\/+/, '')
|
|
35
|
+
if (trimmed.startsWith('backend/')) trimmed = trimmed.slice('backend/'.length)
|
|
36
|
+
if (trimmed.startsWith('api/')) trimmed = trimmed.slice('api/'.length)
|
|
37
|
+
trimmed = trimmed.replace(/^\/+/, '').replace(/\/+/g, '/')
|
|
38
|
+
trimmed = trimmed.replace(/\/$/, '')
|
|
39
|
+
if (!trimmed) return null
|
|
40
|
+
const segments = trimmed.split('/').filter(Boolean)
|
|
41
|
+
if (!segments.length) return null
|
|
42
|
+
const resourceSegments = resource ? resource.split('.').filter(Boolean) : []
|
|
43
|
+
if (resourceSegments.length && segments[0] === resourceSegments[0]) {
|
|
44
|
+
segments.shift()
|
|
45
|
+
}
|
|
46
|
+
const formatted = segments.join('-')
|
|
47
|
+
return formatted.length ? sanitizeSegment(formatted) : null
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
function parseCrudCacheKey(key: string): {
|
|
51
|
+
resource: string | null
|
|
52
|
+
method: string | null
|
|
53
|
+
path: string | null
|
|
54
|
+
segment: string | null
|
|
55
|
+
} {
|
|
56
|
+
const parts = key.split('|')
|
|
57
|
+
if (parts.length < 4) {
|
|
58
|
+
return { resource: null, method: null, path: null, segment: null }
|
|
59
|
+
}
|
|
60
|
+
const resource = parts[1] ?? null
|
|
61
|
+
const method = parts[2] ?? null
|
|
62
|
+
const path = parts[3] ?? null
|
|
63
|
+
const normalizedPath = normalizePathSegment(path, resource)
|
|
64
|
+
const fallback = resource ? sanitizeSegment(resource.replace(/[.]/g, '-')) : null
|
|
65
|
+
const segment = normalizedPath ?? fallback
|
|
66
|
+
return {
|
|
67
|
+
resource,
|
|
68
|
+
method,
|
|
69
|
+
path,
|
|
70
|
+
segment,
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
export function deriveCrudSegmentTag(resource: string, request: Request): string {
|
|
75
|
+
const url = new URL(request.url)
|
|
76
|
+
const pathSegment = normalizePathSegment(url.pathname, resource)
|
|
77
|
+
const fallback = sanitizeSegment(resource.replace(/[.]/g, '-'))
|
|
78
|
+
const finalSegment = pathSegment ?? fallback
|
|
79
|
+
return finalSegment || 'crud-root'
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
export async function collectCrudCacheStats(cache: CacheStrategy): Promise<CrudCacheStats> {
|
|
83
|
+
const analyses = await analyzeCacheSegments(cache, {
|
|
84
|
+
keysPattern: CRUD_CACHE_PATTERN,
|
|
85
|
+
deriveSegment: (key) => parseCrudCacheKey(key).segment,
|
|
86
|
+
filterKey: (key) => key !== CRUD_CACHE_STATS_KEY,
|
|
87
|
+
})
|
|
88
|
+
|
|
89
|
+
const segments: CrudCacheSegmentInfo[] = analyses.map(({ segment, keys }) => {
|
|
90
|
+
const sample = keys[0] ?? null
|
|
91
|
+
const details = sample ? parseCrudCacheKey(sample) : { resource: null, method: null, path: null, segment: null }
|
|
92
|
+
return {
|
|
93
|
+
segment,
|
|
94
|
+
resource: details.resource,
|
|
95
|
+
method: details.method,
|
|
96
|
+
path: details.path,
|
|
97
|
+
keyCount: keys.length,
|
|
98
|
+
keys,
|
|
99
|
+
}
|
|
100
|
+
})
|
|
101
|
+
|
|
102
|
+
const stats: CrudCacheStats = {
|
|
103
|
+
generatedAt: new Date().toISOString(),
|
|
104
|
+
segments,
|
|
105
|
+
totalKeys: segments.reduce((sum, entry) => sum + entry.keyCount, 0),
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
try {
|
|
109
|
+
await cache.set(CRUD_CACHE_STATS_KEY, stats, { tags: ['crud-cache-stats'] })
|
|
110
|
+
} catch {
|
|
111
|
+
// best effort write; ignore failure
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
return stats
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
export async function purgeCrudCacheSegment(cache: CacheStrategy, segment: string): Promise<{ deleted: number; keys: string[] }> {
|
|
118
|
+
return purgeCacheSegment(
|
|
119
|
+
cache,
|
|
120
|
+
{
|
|
121
|
+
keysPattern: CRUD_CACHE_PATTERN,
|
|
122
|
+
deriveSegment: (key) => parseCrudCacheKey(key).segment,
|
|
123
|
+
filterKey: (key) => key !== CRUD_CACHE_STATS_KEY,
|
|
124
|
+
},
|
|
125
|
+
segment,
|
|
126
|
+
)
|
|
127
|
+
}
|
|
@@ -0,0 +1,205 @@
|
|
|
1
|
+
import type { AwilixContainer } from 'awilix'
|
|
2
|
+
import type { CacheStrategy } from '@open-mercato/cache'
|
|
3
|
+
import { parseBooleanToken } from '../boolean'
|
|
4
|
+
|
|
5
|
+
export type CrudCacheIdentifiers = {
|
|
6
|
+
id?: string | null
|
|
7
|
+
organizationId?: string | null
|
|
8
|
+
tenantId?: string | null
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
let crudCacheEnabledFlag: boolean | null = null
|
|
12
|
+
export function isCrudCacheEnabled(): boolean {
|
|
13
|
+
if (crudCacheEnabledFlag !== null) return crudCacheEnabledFlag
|
|
14
|
+
crudCacheEnabledFlag = parseBooleanToken(process.env.ENABLE_CRUD_API_CACHE ?? '') === true
|
|
15
|
+
return crudCacheEnabledFlag
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
let crudCacheDebugFlag: boolean | null = null
|
|
19
|
+
export function isCrudCacheDebugEnabled(): boolean {
|
|
20
|
+
if (crudCacheDebugFlag !== null) return crudCacheDebugFlag
|
|
21
|
+
crudCacheDebugFlag = parseBooleanToken(process.env.QUERY_ENGINE_DEBUG_SQL ?? '') === true
|
|
22
|
+
return crudCacheDebugFlag
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
export function debugCrudCache(event: string, context: Record<string, unknown>) {
|
|
26
|
+
if (!isCrudCacheDebugEnabled()) return
|
|
27
|
+
try {
|
|
28
|
+
console.debug('[crud][cache]', event, context)
|
|
29
|
+
} catch {}
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
export function resolveCrudCache(container: AwilixContainer): CacheStrategy | null {
|
|
33
|
+
try {
|
|
34
|
+
const cache = (container.resolve('cache') as CacheStrategy)
|
|
35
|
+
if (cache && typeof cache.get === 'function' && typeof cache.set === 'function') {
|
|
36
|
+
return cache
|
|
37
|
+
}
|
|
38
|
+
} catch {}
|
|
39
|
+
return null
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
export function normalizeTagSegment(value: string | null | undefined): string {
|
|
43
|
+
if (value === null || value === undefined || value === '') return 'null'
|
|
44
|
+
return value.toString().trim().replace(/[^a-zA-Z0-9._-]/g, '-')
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
export function canonicalizeResourceTag(value: string | null | undefined): string | null {
|
|
48
|
+
if (value === null || value === undefined) return null
|
|
49
|
+
const trimmed = String(value).trim()
|
|
50
|
+
if (!trimmed.length) return null
|
|
51
|
+
const withSeparators = trimmed
|
|
52
|
+
.replace(/::/g, '.')
|
|
53
|
+
.replace(/[/\\]+/g, '.')
|
|
54
|
+
.replace(/[\s]+/g, '.')
|
|
55
|
+
.replace(/_/g, '.')
|
|
56
|
+
.replace(/-+/g, '.')
|
|
57
|
+
const withCamelBreaks = withSeparators.replace(/([a-z0-9])([A-Z])/g, '$1.$2')
|
|
58
|
+
const collapsed = withCamelBreaks.replace(/\.{2,}/g, '.').replace(/^\.+|\.+$/g, '')
|
|
59
|
+
const lowered = collapsed.toLowerCase()
|
|
60
|
+
return lowered.length ? lowered : null
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
export function buildRecordTag(resource: string, tenantId: string | null, recordId: string): string {
|
|
64
|
+
return [
|
|
65
|
+
'crud',
|
|
66
|
+
normalizeTagSegment(resource),
|
|
67
|
+
'tenant',
|
|
68
|
+
normalizeTagSegment(tenantId),
|
|
69
|
+
'record',
|
|
70
|
+
normalizeTagSegment(recordId),
|
|
71
|
+
].join(':')
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
export function buildCollectionTags(
|
|
75
|
+
resource: string,
|
|
76
|
+
tenantId: string | null,
|
|
77
|
+
organizationIds: Array<string | null>
|
|
78
|
+
): string[] {
|
|
79
|
+
const normalizedResource = normalizeTagSegment(resource)
|
|
80
|
+
const normalizedTenant = normalizeTagSegment(tenantId)
|
|
81
|
+
const tags = new Set<string>()
|
|
82
|
+
if (!organizationIds.length) {
|
|
83
|
+
tags.add(['crud', normalizedResource, 'tenant', normalizedTenant, 'org', 'null', 'collection'].join(':'))
|
|
84
|
+
return Array.from(tags)
|
|
85
|
+
}
|
|
86
|
+
for (const orgId of organizationIds) {
|
|
87
|
+
tags.add(['crud', normalizedResource, 'tenant', normalizedTenant, 'org', normalizeTagSegment(orgId), 'collection'].join(':'))
|
|
88
|
+
}
|
|
89
|
+
return Array.from(tags)
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
export function normalizeIdentifierValue(value: unknown): string | null {
|
|
93
|
+
if (value === null || value === undefined) return null
|
|
94
|
+
if (typeof value === 'string') return value
|
|
95
|
+
if (typeof value === 'number' || typeof value === 'bigint') return String(value)
|
|
96
|
+
if (typeof value === 'object') {
|
|
97
|
+
if (value instanceof Date) return value.toISOString()
|
|
98
|
+
if (value && typeof (value as { id?: unknown }).id !== 'undefined') {
|
|
99
|
+
return normalizeIdentifierValue((value as { id?: unknown }).id)
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
return String(value)
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
export function pickFirstIdentifier(...values: Array<unknown>): string | null {
|
|
106
|
+
for (const value of values) {
|
|
107
|
+
const normalized = normalizeIdentifierValue(value)
|
|
108
|
+
if (normalized) return normalized
|
|
109
|
+
}
|
|
110
|
+
return null
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
function singularizeSegment(segment: string): string {
|
|
114
|
+
const lower = segment.toLowerCase()
|
|
115
|
+
if (lower.endsWith('ies') && lower.length > 3) return lower.slice(0, -3) + 'y'
|
|
116
|
+
if (lower.endsWith('ses') && lower.length > 3) return lower.slice(0, -2)
|
|
117
|
+
if (
|
|
118
|
+
(lower.endsWith('xes') ||
|
|
119
|
+
lower.endsWith('zes') ||
|
|
120
|
+
lower.endsWith('ches') ||
|
|
121
|
+
lower.endsWith('shes')) &&
|
|
122
|
+
lower.length > 3
|
|
123
|
+
) {
|
|
124
|
+
return lower.slice(0, -2)
|
|
125
|
+
}
|
|
126
|
+
if (lower.endsWith('s') && !lower.endsWith('ss') && lower.length > 1) return lower.slice(0, -1)
|
|
127
|
+
return lower
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
function singularizeResourceSegment(segment: string): string {
|
|
131
|
+
return segment
|
|
132
|
+
.split('-')
|
|
133
|
+
.map((part) => singularizeSegment(part))
|
|
134
|
+
.join('-')
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
export function deriveResourceFromCommandId(commandId: string | undefined | null): string | null {
|
|
138
|
+
if (!commandId || typeof commandId !== 'string') return null
|
|
139
|
+
const parts = commandId.split('.')
|
|
140
|
+
if (parts.length < 2) return null
|
|
141
|
+
const modulePart = parts[0]
|
|
142
|
+
const entityPart = singularizeResourceSegment(parts[1])
|
|
143
|
+
if (!modulePart || !entityPart) return null
|
|
144
|
+
return `${modulePart}.${entityPart}`
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
export function expandResourceAliases(resource: string, aliases?: string[]): string[] {
|
|
148
|
+
const set = new Set<string>()
|
|
149
|
+
const inputs = [resource, ...(aliases ?? [])]
|
|
150
|
+
for (const candidate of inputs) {
|
|
151
|
+
const canonical = canonicalizeResourceTag(candidate)
|
|
152
|
+
if (canonical) set.add(canonical)
|
|
153
|
+
}
|
|
154
|
+
if (!set.size) return []
|
|
155
|
+
return Array.from(set)
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
export async function invalidateCrudCache(
|
|
159
|
+
container: AwilixContainer,
|
|
160
|
+
resource: string,
|
|
161
|
+
identifiers: CrudCacheIdentifiers,
|
|
162
|
+
fallbackTenant: string | null,
|
|
163
|
+
reason: string,
|
|
164
|
+
aliases?: string[]
|
|
165
|
+
): Promise<void> {
|
|
166
|
+
if (!isCrudCacheEnabled()) return
|
|
167
|
+
const cache = resolveCrudCache(container)
|
|
168
|
+
if (!cache || typeof cache.deleteByTags !== 'function') return
|
|
169
|
+
const resources = expandResourceAliases(resource, aliases)
|
|
170
|
+
const tenantId = identifiers.tenantId ?? fallbackTenant ?? null
|
|
171
|
+
const recordId = normalizeIdentifierValue(identifiers.id)
|
|
172
|
+
const tags = new Set<string>()
|
|
173
|
+
for (const key of resources) {
|
|
174
|
+
if (recordId) {
|
|
175
|
+
tags.add(buildRecordTag(key, tenantId, recordId))
|
|
176
|
+
}
|
|
177
|
+
const organizationIds: Array<string | null> = []
|
|
178
|
+
if (identifiers.organizationId !== undefined) {
|
|
179
|
+
organizationIds.push(identifiers.organizationId ?? null)
|
|
180
|
+
}
|
|
181
|
+
if (!organizationIds.length) organizationIds.push(null)
|
|
182
|
+
for (const tag of buildCollectionTags(key, tenantId, organizationIds)) {
|
|
183
|
+
tags.add(tag)
|
|
184
|
+
}
|
|
185
|
+
}
|
|
186
|
+
if (!tags.size) return
|
|
187
|
+
const tagList = Array.from(tags)
|
|
188
|
+
debugCrudCache('invalidate', {
|
|
189
|
+
resource,
|
|
190
|
+
aliases: resources,
|
|
191
|
+
reason,
|
|
192
|
+
tenantId: tenantId ?? 'null',
|
|
193
|
+
tags: tagList,
|
|
194
|
+
action: 'clearing',
|
|
195
|
+
})
|
|
196
|
+
const deleted = await cache.deleteByTags(tagList)
|
|
197
|
+
debugCrudCache('invalidate', {
|
|
198
|
+
resource,
|
|
199
|
+
reason,
|
|
200
|
+
tenantId: tenantId ?? 'null',
|
|
201
|
+
tags: tagList,
|
|
202
|
+
action: 'cleared',
|
|
203
|
+
deleted,
|
|
204
|
+
})
|
|
205
|
+
}
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
export function extractCustomFieldEntries(item: Record<string, unknown>): Record<string, unknown> {
|
|
2
|
+
const out: Record<string, unknown> = {}
|
|
3
|
+
if (!item || typeof item !== 'object') return out
|
|
4
|
+
|
|
5
|
+
const normalizeValue = (value: unknown): unknown => {
|
|
6
|
+
if (Array.isArray(value)) return value
|
|
7
|
+
if (typeof value !== 'string') return value
|
|
8
|
+
const trimmed = value.trim()
|
|
9
|
+
if (trimmed.startsWith('{') && trimmed.endsWith('}')) {
|
|
10
|
+
const inner = trimmed.slice(1, -1).trim()
|
|
11
|
+
if (!inner) return []
|
|
12
|
+
return inner.split(/[\s,]+/).map((entry) => entry.trim()).filter(Boolean)
|
|
13
|
+
}
|
|
14
|
+
return trimmed
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
const assign = (rawKey: unknown, rawValue: unknown) => {
|
|
18
|
+
if (typeof rawKey !== 'string') return
|
|
19
|
+
const trimmed = rawKey.trim()
|
|
20
|
+
if (!trimmed) return
|
|
21
|
+
out[`cf_${trimmed}`] = normalizeValue(rawValue)
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
for (const [rawKey, rawValue] of Object.entries(item)) {
|
|
25
|
+
if (rawKey.startsWith('cf_')) {
|
|
26
|
+
if (rawKey.endsWith('__is_multi')) continue
|
|
27
|
+
out[rawKey] = normalizeValue(rawValue)
|
|
28
|
+
} else if (rawKey.startsWith('cf:')) {
|
|
29
|
+
assign(rawKey.slice(3), rawValue)
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
const customValues = (item as any).customValues
|
|
34
|
+
if (customValues && typeof customValues === 'object' && !Array.isArray(customValues)) {
|
|
35
|
+
for (const [key, value] of Object.entries(customValues as Record<string, unknown>)) {
|
|
36
|
+
assign(key, value)
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
const customFields = (item as any).customFields
|
|
41
|
+
if (Array.isArray(customFields)) {
|
|
42
|
+
for (const entry of customFields as Array<Record<string, unknown>>) {
|
|
43
|
+
const key = entry && typeof entry.key === 'string' ? entry.key : null
|
|
44
|
+
if (!key) continue
|
|
45
|
+
assign(key, 'value' in entry ? (entry as any).value : undefined)
|
|
46
|
+
}
|
|
47
|
+
} else if (customFields && typeof customFields === 'object') {
|
|
48
|
+
for (const [key, value] of Object.entries(customFields as Record<string, unknown>)) {
|
|
49
|
+
assign(key, value)
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
return out
|
|
54
|
+
}
|