@open-mercato/shared 0.6.6-develop.6338.1.c32cd17821 → 0.6.6-develop.6343.1.8120f84f30
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.turbo/turbo-build.log +1 -1
- package/dist/lib/bootstrap/generatedCacheRecovery.js +1 -1
- package/dist/lib/bootstrap/generatedCacheRecovery.js.map +2 -2
- package/dist/lib/crud/custom-fields.js +3 -3
- package/dist/lib/crud/custom-fields.js.map +2 -2
- package/dist/lib/crud/mutation-guard.js.map +2 -2
- package/dist/lib/crud/route-mutation-guard.js +84 -0
- package/dist/lib/crud/route-mutation-guard.js.map +7 -0
- package/dist/lib/version.js +1 -1
- package/dist/lib/version.js.map +1 -1
- package/package.json +2 -2
- package/src/lib/bootstrap/generatedCacheRecovery.ts +1 -1
- package/src/lib/crud/__tests__/route-mutation-guard.test.ts +260 -0
- package/src/lib/crud/custom-fields.ts +3 -3
- package/src/lib/crud/mutation-guard.ts +15 -4
- package/src/lib/crud/route-mutation-guard.ts +181 -0
package/.turbo/turbo-build.log
CHANGED
|
@@ -1,2 +1,2 @@
|
|
|
1
|
-
[build:shared] found
|
|
1
|
+
[build:shared] found 226 entry points
|
|
2
2
|
[build:shared] built successfully
|
|
@@ -46,7 +46,7 @@ function walkFiles(dirPath) {
|
|
|
46
46
|
return files;
|
|
47
47
|
}
|
|
48
48
|
function listGeneratedCacheFiles(appRoot) {
|
|
49
|
-
return walkFiles(getGeneratedDir(appRoot)).filter((filePath) => filePath.endsWith(".mjs")).sort();
|
|
49
|
+
return walkFiles(getGeneratedDir(appRoot)).filter((filePath) => filePath.endsWith(".mjs")).sort((a, b) => a < b ? -1 : a > b ? 1 : 0);
|
|
50
50
|
}
|
|
51
51
|
function findStaleGeneratedCacheFiles(appRoot) {
|
|
52
52
|
const generatedFiles = listGeneratedCacheFiles(appRoot);
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"version": 3,
|
|
3
3
|
"sources": ["../../../src/lib/bootstrap/generatedCacheRecovery.ts"],
|
|
4
|
-
"sourcesContent": ["import fs from 'node:fs'\nimport path from 'node:path'\n\nconst DECORATOR_EXPORT_NAMES = [\n 'Entity',\n 'PrimaryKey',\n 'Property',\n 'ManyToOne',\n 'OneToMany',\n 'OneToOne',\n 'ManyToMany',\n 'Enum',\n 'Index',\n 'Unique',\n 'Embeddable',\n 'Embedded',\n 'Formula',\n] as const\n\nconst RECOVERY_VERSION = 'mikro-orm-v7-generated-cache-recovery-v1'\nconst RECOVERY_MARKER_FILE = '.mikro-orm-v7-cache-recovery.json'\nconst GENERATED_DIR_SEGMENTS = ['.mercato', 'generated'] as const\n\nconst staleDecoratorImportPattern = new RegExp(\n String.raw`import\\s*\\{[^}]*\\b(?:${DECORATOR_EXPORT_NAMES.join('|')})\\b[^}]*\\}\\s*from\\s*['\"]@mikro-orm/core['\"]`,\n 'm',\n)\n\nconst decoratorExportErrorPattern = /@mikro-orm\\/core' does not provide an export named '(?:Entity|PrimaryKey|Property|ManyToOne|OneToMany|OneToOne|ManyToMany|Enum|Index|Unique|Embeddable|Embedded|Formula)'/\n\ntype RecoveryLogger = {\n warn: (message: string) => void\n}\n\ntype RecoveryReason = 'stale-generated-cache-scan' | 'runtime-import-error'\n\ntype RecoveryMarker = {\n version: string\n reason: RecoveryReason\n createdAt: string\n deletedFiles: string[]\n}\n\nexport type GeneratedCacheRecoveryResult = {\n applied: boolean\n deletedFiles: string[]\n markerPath: string | null\n}\n\nfunction getGeneratedDir(appRoot: string): string {\n return path.join(appRoot, ...GENERATED_DIR_SEGMENTS)\n}\n\nfunction getRecoveryMarkerPath(appRoot: string): string {\n return path.join(getGeneratedDir(appRoot), RECOVERY_MARKER_FILE)\n}\n\nfunction walkFiles(dirPath: string): string[] {\n if (!fs.existsSync(dirPath)) return []\n\n const entries = fs.readdirSync(dirPath, { withFileTypes: true })\n const files: string[] = []\n for (const entry of entries) {\n const absolutePath = path.join(dirPath, entry.name)\n if (entry.isDirectory()) {\n files.push(...walkFiles(absolutePath))\n continue\n }\n if (entry.isFile()) {\n files.push(absolutePath)\n }\n }\n return files\n}\n\nfunction listGeneratedCacheFiles(appRoot: string): string[] {\n return walkFiles(getGeneratedDir(appRoot))\n .filter((filePath) => filePath.endsWith('.mjs'))\n .sort()\n}\n\nfunction findStaleGeneratedCacheFiles(appRoot: string): string[] {\n const generatedFiles = listGeneratedCacheFiles(appRoot)\n return generatedFiles.filter((filePath) => {\n const source = fs.readFileSync(filePath, 'utf8')\n return staleDecoratorImportPattern.test(source)\n })\n}\n\nfunction writeRecoveryMarker(appRoot: string, marker: RecoveryMarker): string {\n const markerPath = getRecoveryMarkerPath(appRoot)\n fs.writeFileSync(markerPath, JSON.stringify(marker, null, 2))\n return markerPath\n}\n\nfunction logRecoveryMessage(logger: RecoveryLogger, reason: RecoveryReason): void {\n const header =\n reason === 'runtime-import-error'\n ? '\u26A0\uFE0F Open Mercato detected a stale generated cache while bootstrapping the app.'\n : '\u26A0\uFE0F Open Mercato detected stale generated cache from the MikroORM v7 migration.'\n\n logger.warn('')\n logger.warn(header)\n logger.warn('\uD83D\uDCD8 Open Mercato migrated MikroORM to version 7. Please review UPGRADE_NOTES.md and the `migrate-mikro-orm` skill if your code still imports decorators from `@mikro-orm/core`.')\n logger.warn('\uD83E\uDDF9 Cleaning generated compilation cache and recompiling generated code now...')\n logger.warn('')\n}\n\nfunction deleteGeneratedCacheFiles(filePaths: string[]): string[] {\n const deletedFiles: string[] = []\n for (const filePath of filePaths) {\n if (!fs.existsSync(filePath)) continue\n fs.rmSync(filePath, { force: true })\n deletedFiles.push(filePath)\n }\n return deletedFiles\n}\n\nfunction applyGeneratedCacheRecovery(\n appRoot: string,\n staleFiles: string[],\n reason: RecoveryReason,\n logger: RecoveryLogger,\n): GeneratedCacheRecoveryResult {\n if (staleFiles.length === 0) {\n return { applied: false, deletedFiles: [], markerPath: null }\n }\n\n logRecoveryMessage(logger, reason)\n\n const generatedCacheFiles = listGeneratedCacheFiles(appRoot)\n const deletedFiles = deleteGeneratedCacheFiles(generatedCacheFiles)\n const markerPath = writeRecoveryMarker(appRoot, {\n version: RECOVERY_VERSION,\n reason,\n createdAt: new Date().toISOString(),\n deletedFiles,\n })\n\n return {\n applied: true,\n deletedFiles,\n markerPath,\n }\n}\n\nexport function shouldRecoverMikroOrmV7GeneratedCache(error: unknown): boolean {\n const message = error instanceof Error ? error.message : String(error)\n return decoratorExportErrorPattern.test(message)\n}\n\nexport function ensureMikroOrmV7GeneratedCacheCompatibility(\n appRoot: string,\n options: { logger?: RecoveryLogger } = {},\n): GeneratedCacheRecoveryResult {\n const logger = options.logger ?? console\n const staleFiles = findStaleGeneratedCacheFiles(appRoot)\n return applyGeneratedCacheRecovery(appRoot, staleFiles, 'stale-generated-cache-scan', logger)\n}\n\nexport function recoverMikroOrmV7GeneratedCacheFromImportError(\n appRoot: string,\n error: unknown,\n options: { logger?: RecoveryLogger } = {},\n): GeneratedCacheRecoveryResult {\n if (!shouldRecoverMikroOrmV7GeneratedCache(error)) {\n return { applied: false, deletedFiles: [], markerPath: null }\n }\n\n const logger = options.logger ?? console\n const staleFiles = findStaleGeneratedCacheFiles(appRoot)\n return applyGeneratedCacheRecovery(appRoot, staleFiles, 'runtime-import-error', logger)\n}\n"],
|
|
5
|
-
"mappings": "AAAA,OAAO,QAAQ;AACf,OAAO,UAAU;AAEjB,MAAM,yBAAyB;AAAA,EAC7B;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AACF;AAEA,MAAM,mBAAmB;AACzB,MAAM,uBAAuB;AAC7B,MAAM,yBAAyB,CAAC,YAAY,WAAW;AAEvD,MAAM,8BAA8B,IAAI;AAAA,EACtC,OAAO,2BAA2B,uBAAuB,KAAK,GAAG,CAAC;AAAA,EAClE;AACF;AAEA,MAAM,8BAA8B;AAqBpC,SAAS,gBAAgB,SAAyB;AAChD,SAAO,KAAK,KAAK,SAAS,GAAG,sBAAsB;AACrD;AAEA,SAAS,sBAAsB,SAAyB;AACtD,SAAO,KAAK,KAAK,gBAAgB,OAAO,GAAG,oBAAoB;AACjE;AAEA,SAAS,UAAU,SAA2B;AAC5C,MAAI,CAAC,GAAG,WAAW,OAAO,EAAG,QAAO,CAAC;AAErC,QAAM,UAAU,GAAG,YAAY,SAAS,EAAE,eAAe,KAAK,CAAC;AAC/D,QAAM,QAAkB,CAAC;AACzB,aAAW,SAAS,SAAS;AAC3B,UAAM,eAAe,KAAK,KAAK,SAAS,MAAM,IAAI;AAClD,QAAI,MAAM,YAAY,GAAG;AACvB,YAAM,KAAK,GAAG,UAAU,YAAY,CAAC;AACrC;AAAA,IACF;AACA,QAAI,MAAM,OAAO,GAAG;AAClB,YAAM,KAAK,YAAY;AAAA,IACzB;AAAA,EACF;AACA,SAAO;AACT;AAEA,SAAS,wBAAwB,SAA2B;AAC1D,SAAO,UAAU,gBAAgB,OAAO,CAAC,EACtC,OAAO,CAAC,aAAa,SAAS,SAAS,MAAM,CAAC,EAC9C,KAAK;
|
|
4
|
+
"sourcesContent": ["import fs from 'node:fs'\nimport path from 'node:path'\n\nconst DECORATOR_EXPORT_NAMES = [\n 'Entity',\n 'PrimaryKey',\n 'Property',\n 'ManyToOne',\n 'OneToMany',\n 'OneToOne',\n 'ManyToMany',\n 'Enum',\n 'Index',\n 'Unique',\n 'Embeddable',\n 'Embedded',\n 'Formula',\n] as const\n\nconst RECOVERY_VERSION = 'mikro-orm-v7-generated-cache-recovery-v1'\nconst RECOVERY_MARKER_FILE = '.mikro-orm-v7-cache-recovery.json'\nconst GENERATED_DIR_SEGMENTS = ['.mercato', 'generated'] as const\n\nconst staleDecoratorImportPattern = new RegExp(\n String.raw`import\\s*\\{[^}]*\\b(?:${DECORATOR_EXPORT_NAMES.join('|')})\\b[^}]*\\}\\s*from\\s*['\"]@mikro-orm/core['\"]`,\n 'm',\n)\n\nconst decoratorExportErrorPattern = /@mikro-orm\\/core' does not provide an export named '(?:Entity|PrimaryKey|Property|ManyToOne|OneToMany|OneToOne|ManyToMany|Enum|Index|Unique|Embeddable|Embedded|Formula)'/\n\ntype RecoveryLogger = {\n warn: (message: string) => void\n}\n\ntype RecoveryReason = 'stale-generated-cache-scan' | 'runtime-import-error'\n\ntype RecoveryMarker = {\n version: string\n reason: RecoveryReason\n createdAt: string\n deletedFiles: string[]\n}\n\nexport type GeneratedCacheRecoveryResult = {\n applied: boolean\n deletedFiles: string[]\n markerPath: string | null\n}\n\nfunction getGeneratedDir(appRoot: string): string {\n return path.join(appRoot, ...GENERATED_DIR_SEGMENTS)\n}\n\nfunction getRecoveryMarkerPath(appRoot: string): string {\n return path.join(getGeneratedDir(appRoot), RECOVERY_MARKER_FILE)\n}\n\nfunction walkFiles(dirPath: string): string[] {\n if (!fs.existsSync(dirPath)) return []\n\n const entries = fs.readdirSync(dirPath, { withFileTypes: true })\n const files: string[] = []\n for (const entry of entries) {\n const absolutePath = path.join(dirPath, entry.name)\n if (entry.isDirectory()) {\n files.push(...walkFiles(absolutePath))\n continue\n }\n if (entry.isFile()) {\n files.push(absolutePath)\n }\n }\n return files\n}\n\nfunction listGeneratedCacheFiles(appRoot: string): string[] {\n return walkFiles(getGeneratedDir(appRoot))\n .filter((filePath) => filePath.endsWith('.mjs'))\n .sort((a, b) => (a < b ? -1 : a > b ? 1 : 0))\n}\n\nfunction findStaleGeneratedCacheFiles(appRoot: string): string[] {\n const generatedFiles = listGeneratedCacheFiles(appRoot)\n return generatedFiles.filter((filePath) => {\n const source = fs.readFileSync(filePath, 'utf8')\n return staleDecoratorImportPattern.test(source)\n })\n}\n\nfunction writeRecoveryMarker(appRoot: string, marker: RecoveryMarker): string {\n const markerPath = getRecoveryMarkerPath(appRoot)\n fs.writeFileSync(markerPath, JSON.stringify(marker, null, 2))\n return markerPath\n}\n\nfunction logRecoveryMessage(logger: RecoveryLogger, reason: RecoveryReason): void {\n const header =\n reason === 'runtime-import-error'\n ? '\u26A0\uFE0F Open Mercato detected a stale generated cache while bootstrapping the app.'\n : '\u26A0\uFE0F Open Mercato detected stale generated cache from the MikroORM v7 migration.'\n\n logger.warn('')\n logger.warn(header)\n logger.warn('\uD83D\uDCD8 Open Mercato migrated MikroORM to version 7. Please review UPGRADE_NOTES.md and the `migrate-mikro-orm` skill if your code still imports decorators from `@mikro-orm/core`.')\n logger.warn('\uD83E\uDDF9 Cleaning generated compilation cache and recompiling generated code now...')\n logger.warn('')\n}\n\nfunction deleteGeneratedCacheFiles(filePaths: string[]): string[] {\n const deletedFiles: string[] = []\n for (const filePath of filePaths) {\n if (!fs.existsSync(filePath)) continue\n fs.rmSync(filePath, { force: true })\n deletedFiles.push(filePath)\n }\n return deletedFiles\n}\n\nfunction applyGeneratedCacheRecovery(\n appRoot: string,\n staleFiles: string[],\n reason: RecoveryReason,\n logger: RecoveryLogger,\n): GeneratedCacheRecoveryResult {\n if (staleFiles.length === 0) {\n return { applied: false, deletedFiles: [], markerPath: null }\n }\n\n logRecoveryMessage(logger, reason)\n\n const generatedCacheFiles = listGeneratedCacheFiles(appRoot)\n const deletedFiles = deleteGeneratedCacheFiles(generatedCacheFiles)\n const markerPath = writeRecoveryMarker(appRoot, {\n version: RECOVERY_VERSION,\n reason,\n createdAt: new Date().toISOString(),\n deletedFiles,\n })\n\n return {\n applied: true,\n deletedFiles,\n markerPath,\n }\n}\n\nexport function shouldRecoverMikroOrmV7GeneratedCache(error: unknown): boolean {\n const message = error instanceof Error ? error.message : String(error)\n return decoratorExportErrorPattern.test(message)\n}\n\nexport function ensureMikroOrmV7GeneratedCacheCompatibility(\n appRoot: string,\n options: { logger?: RecoveryLogger } = {},\n): GeneratedCacheRecoveryResult {\n const logger = options.logger ?? console\n const staleFiles = findStaleGeneratedCacheFiles(appRoot)\n return applyGeneratedCacheRecovery(appRoot, staleFiles, 'stale-generated-cache-scan', logger)\n}\n\nexport function recoverMikroOrmV7GeneratedCacheFromImportError(\n appRoot: string,\n error: unknown,\n options: { logger?: RecoveryLogger } = {},\n): GeneratedCacheRecoveryResult {\n if (!shouldRecoverMikroOrmV7GeneratedCache(error)) {\n return { applied: false, deletedFiles: [], markerPath: null }\n }\n\n const logger = options.logger ?? console\n const staleFiles = findStaleGeneratedCacheFiles(appRoot)\n return applyGeneratedCacheRecovery(appRoot, staleFiles, 'runtime-import-error', logger)\n}\n"],
|
|
5
|
+
"mappings": "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",
|
|
6
6
|
"names": []
|
|
7
7
|
}
|
|
@@ -167,8 +167,8 @@ function resolveCfDefIndexCacheTtlMs() {
|
|
|
167
167
|
}
|
|
168
168
|
function buildCfDefIndexCacheKey(opts) {
|
|
169
169
|
const tenant = opts.tenantId ?? "global";
|
|
170
|
-
const entities = opts.entityIds.slice().sort().join("|");
|
|
171
|
-
const orgs = opts.organizationIds.length ? opts.organizationIds.slice().sort().join("|") : "none";
|
|
170
|
+
const entities = opts.entityIds.slice().sort((a, b) => a < b ? -1 : a > b ? 1 : 0).join("|");
|
|
171
|
+
const orgs = opts.organizationIds.length ? opts.organizationIds.slice().sort((a, b) => a < b ? -1 : a > b ? 1 : 0).join("|") : "none";
|
|
172
172
|
const fieldset = opts.fieldsetKey ?? "all";
|
|
173
173
|
return `${CF_DEF_INDEX_CACHE_KEY_PREFIX}:${tenant}:${entities}:${orgs}:${fieldset}`;
|
|
174
174
|
}
|
|
@@ -186,7 +186,7 @@ function normalizeFieldsetKey(value) {
|
|
|
186
186
|
if (Array.isArray(value)) {
|
|
187
187
|
const cleaned = value.map((entry) => typeof entry === "string" ? entry.trim() : "").filter((entry) => entry.length > 0);
|
|
188
188
|
if (!cleaned.length) return null;
|
|
189
|
-
return cleaned.sort().join(",");
|
|
189
|
+
return cleaned.sort((a, b) => a < b ? -1 : a > b ? 1 : 0).join(",");
|
|
190
190
|
}
|
|
191
191
|
if (typeof value !== "string") return null;
|
|
192
192
|
const trimmed = value.trim();
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"version": 3,
|
|
3
3
|
"sources": ["../../../src/lib/crud/custom-fields.ts"],
|
|
4
|
-
"sourcesContent": ["import type { CustomFieldSet, EntityId } from '@open-mercato/shared/modules/entities'\nimport type { EntityManager } from '@mikro-orm/core'\nimport { CustomFieldDef, CustomFieldValue } from '@open-mercato/core/modules/entities/data/entities'\nimport type { WhereValue } from '@open-mercato/shared/lib/query/types'\nimport type { TenantDataEncryptionService } from '../encryption/tenantDataEncryptionService'\nimport { decryptCustomFieldValue, resolveTenantEncryptionService } from '../encryption/customFieldValues'\nimport { parseBooleanToken } from '../boolean'\nimport { extractCustomFieldEntries } from './custom-fields-client'\nimport {\n buildCustomFieldDefinitionIndexFromRows,\n normalizeDefinitionKey,\n normalizeFieldsetFilter,\n selectDefinitionForRecord,\n type CustomFieldDefinitionIndex,\n type CustomFieldDefinitionRow,\n type CustomFieldDefinitionSummary,\n} from './custom-field-definition-index'\n\nexport type { CustomFieldDefinitionSummary, CustomFieldDefinitionIndex } from './custom-field-definition-index'\n\nexport type CustomFieldSelectors = {\n keys: string[]\n selectors: string[] // e.g. ['cf:priority', 'cf:severity']\n outputKeys: string[] // e.g. ['cf_priority', 'cf_severity']\n}\n\nexport type SplitCustomFieldPayload = {\n base: Record<string, unknown>\n custom: Record<string, unknown>\n}\n\nexport type CustomFieldDisplayEntry = {\n key: string\n label: string | null\n value: unknown\n kind: string | null\n multi: boolean\n}\n\nexport type CustomFieldDisplayPayload = {\n customValues: Record<string, unknown> | null\n customFields: CustomFieldDisplayEntry[]\n}\n\nexport type CustomFieldSnapshot = {\n entries: Record<string, unknown>\n customValues: Record<string, unknown> | null\n customFields: CustomFieldDisplayEntry[]\n}\n\nexport function buildCustomFieldSelectorsForEntity(entityId: EntityId, sets: CustomFieldSet[]): CustomFieldSelectors {\n const keys = Array.from(new Set(\n (sets || [])\n .filter((s) => s.entity === entityId)\n .flatMap((s) => (s.fields || []).map((f) => f.key))\n ))\n const selectors = keys.map((k) => `cf:${k}`)\n const outputKeys = keys.map((k) => `cf_${k}`)\n return { keys, selectors, outputKeys }\n}\n\nexport function normalizeCustomFieldValue(val: unknown): unknown {\n if (Array.isArray(val)) return val\n if (typeof val === 'string') {\n const s = val.trim()\n // Parse Postgres array-like '{a,b,c}' to string[] when present\n if (s.startsWith('{') && s.endsWith('}')) {\n const inner = s.slice(1, -1).trim()\n if (!inner) return []\n return inner.split(/[\\s,]+/).map((x) => x.trim()).filter(Boolean)\n }\n return s\n }\n return val as any\n}\n\n// Extracts cf_* fields from a record that may contain both 'cf:<key>' and/or 'cf_<key>'\nexport function extractCustomFieldsFromItem(item: Record<string, unknown>, keys: string[]): Record<string, unknown> {\n const out: Record<string, unknown> = {}\n for (const key of keys) {\n const colon = item[`cf:${key}` as keyof typeof item]\n const snake = item[`cf_${key}` as keyof typeof item]\n const value = colon !== undefined ? colon : snake\n if (value !== undefined) out[`cf_${key}`] = normalizeCustomFieldValue(value)\n }\n return out\n}\n\nexport function extractAllCustomFieldEntries(item: Record<string, unknown>): Record<string, unknown> {\n return extractCustomFieldEntries(item)\n}\n\nexport async function buildCustomFieldFiltersFromQuery(opts: {\n entityId?: EntityId\n entityIds?: EntityId[]\n query: Record<string, unknown>\n em: EntityManager\n tenantId: string | null | undefined\n fieldset?: string | string[] | null\n}): Promise<Record<string, WhereValue>> {\n const out: Record<string, WhereValue> = {}\n const entries = Object.entries(opts.query).filter(([k]) => k.startsWith('cf_'))\n if (!entries.length) return out\n\n const entityIdList = Array.isArray(opts.entityIds) && opts.entityIds.length\n ? opts.entityIds\n : opts.entityId\n ? [opts.entityId]\n : []\n if (!entityIdList.length) return out\n\n // Tenant-only scope: allow global (null) or tenant match; ignore organization here\n const defs = await opts.em.find(CustomFieldDef, {\n entityId: { $in: entityIdList as any },\n isActive: true,\n $and: [\n { $or: [ { tenantId: opts.tenantId as any }, { tenantId: null } ] },\n ],\n })\n const fieldsetFilter = normalizeFieldsetFilter(opts.fieldset)\n const order = new Map<string, number>()\n entityIdList.map(String).forEach((id, index) => order.set(id, index))\n const byKey: Record<string, { kind: string; multi?: boolean; entityId: string }> = {}\n for (const d of defs) {\n if (fieldsetFilter) {\n const fieldsets = Array.isArray(d.configJson?.fieldsets)\n ? d.configJson.fieldsets\n .filter((entry: unknown): entry is string => typeof entry === 'string')\n .map((entry: string) => entry.trim())\n .filter((entry: string) => entry.length > 0)\n : []\n const rawFieldset = typeof d.configJson?.fieldset === 'string' ? d.configJson.fieldset.trim() : ''\n const normalizedFieldset = rawFieldset.length ? rawFieldset : null\n const matches = fieldsets.length > 0\n ? fieldsets.some((entry: string) => fieldsetFilter.has(entry))\n : fieldsetFilter.has(normalizedFieldset)\n if (!matches) continue\n }\n const key = d.key\n const entityId = String(d.entityId)\n const current = byKey[key]\n const rankNew = order.get(entityId) ?? Number.MAX_SAFE_INTEGER\n if (!current) {\n byKey[key] = { kind: d.kind, multi: Boolean((d as any).configJson?.multi), entityId }\n continue\n }\n const rankOld = order.get(current.entityId) ?? Number.MAX_SAFE_INTEGER\n if (rankNew < rankOld) {\n byKey[key] = { kind: d.kind, multi: Boolean((d as any).configJson?.multi), entityId }\n }\n }\n\n const coerce = (kind: string, v: unknown) => {\n if (v == null) return v as undefined\n switch (kind) {\n case 'integer': return Number.parseInt(String(v), 10)\n case 'float': return Number.parseFloat(String(v))\n case 'boolean': return parseBooleanToken(String(v)) === true\n case 'date':\n case 'datetime': return String(v)\n default: return String(v)\n }\n }\n\n for (const [rawKey, rawVal] of entries) {\n const isIn = rawKey.endsWith('In')\n const key = isIn ? rawKey.replace(/^cf_/, '').replace(/In$/, '') : rawKey.replace(/^cf_/, '')\n const def = byKey[key]\n const fieldId = `cf:${key}`\n if (!def) continue\n if (isIn) {\n const list = Array.isArray(rawVal)\n ? (rawVal as unknown[])\n : String(rawVal)\n .split(',')\n .map((s) => s.trim())\n .filter(Boolean)\n if (list.length) out[fieldId] = { $in: list.map((x) => coerce(def.kind, x)) as (string[] | number[] | boolean[]) }\n } else {\n out[fieldId] = coerce(def.kind, rawVal)\n }\n }\n\n return out\n}\n\nexport function splitCustomFieldPayload(raw: unknown): SplitCustomFieldPayload {\n const base: Record<string, unknown> = {}\n const custom: Record<string, unknown> = {}\n if (!raw || typeof raw !== 'object') return { base, custom }\n for (const [key, value] of Object.entries(raw as Record<string, unknown>)) {\n if (key === 'customFields') {\n if (Array.isArray(value)) {\n value.forEach((entry) => {\n if (!entry || typeof entry !== 'object') return\n const entryKey = typeof (entry as any).key === 'string' ? (entry as any).key.trim() : ''\n if (!entryKey) return\n custom[entryKey] = (entry as any).value\n })\n continue\n }\n if (value && typeof value === 'object') {\n for (const [ck, cv] of Object.entries(value as Record<string, unknown>)) {\n const normalizedKey = typeof ck === 'string' ? ck.trim() : ''\n if (!normalizedKey) continue\n custom[normalizedKey] = cv\n }\n continue\n }\n }\n if (key === 'customValues' && value && typeof value === 'object' && !Array.isArray(value)) {\n for (const [ck, cv] of Object.entries(value as Record<string, unknown>)) {\n custom[String(ck)] = cv\n }\n continue\n }\n if (key.startsWith('cf_')) {\n custom[key.slice(3)] = value\n continue\n }\n if (key.startsWith('cf:')) {\n custom[key.slice(3)] = value\n continue\n }\n base[key] = value\n }\n return { base, custom }\n}\n\nexport function extractCustomFieldValuesFromPayload(raw: Record<string, unknown>): Record<string, unknown> {\n return splitCustomFieldPayload(raw).custom\n}\n\ntype LoadCustomFieldDefinitionIndexOptions = {\n em: EntityManager\n entityIds: string | string[]\n tenantId?: string | null | undefined\n organizationIds?: Array<string | null | undefined> | null\n fieldset?: string | string[] | null\n}\n\ntype CustomFieldDefIndexCache = {\n get(key: string): Promise<unknown> | unknown\n set(key: string, value: unknown, opts?: { ttl?: number; tags?: string[] }): Promise<unknown> | unknown\n deleteByTags?(tags: string[]): Promise<number> | number\n}\n\nconst CF_DEF_INDEX_CACHE_KEY_PREFIX = 'crud:cf-def-index'\n// Phase 2 default-off: integration runs observed `/api/customers/people`\n// returning 500 with this cache path active, and the readiness-probe\n// timeout blocked artifact upload so the direct stack trace was lost.\n// Until cross-request safety of the SQLite-cache JSON round-trip is\n// re-verified, ship with the layer disabled. Set\n// `OM_CF_DEF_CACHE_TTL_MS=300000` (or any positive integer) to opt in.\nconst CF_DEF_INDEX_DEFAULT_TTL_MS = 0\n\nfunction resolveCfDefIndexCacheTtlMs(): number {\n const raw = process.env.OM_CF_DEF_CACHE_TTL_MS\n if (raw === undefined) return CF_DEF_INDEX_DEFAULT_TTL_MS\n const parsed = Number(raw)\n if (!Number.isFinite(parsed) || parsed < 0) return CF_DEF_INDEX_DEFAULT_TTL_MS\n return parsed\n}\n\nfunction buildCfDefIndexCacheKey(opts: {\n tenantId: string | null\n entityIds: string[]\n organizationIds: string[]\n fieldsetKey: string | null\n}): string {\n const tenant = opts.tenantId ?? 'global'\n const entities = opts.entityIds.slice().sort().join('|')\n const orgs = opts.organizationIds.length ? opts.organizationIds.slice().sort().join('|') : 'none'\n const fieldset = opts.fieldsetKey ?? 'all'\n return `${CF_DEF_INDEX_CACHE_KEY_PREFIX}:${tenant}:${entities}:${orgs}:${fieldset}`\n}\n\nfunction buildCfDefIndexCacheTags(opts: {\n tenantId: string | null\n entityIds: string[]\n}): string[] {\n const tenant = opts.tenantId ?? 'global'\n const tagBase = `entities:definitions:${tenant}`\n const tags = new Set<string>([tagBase])\n for (const entityId of opts.entityIds) {\n tags.add(`${tagBase}:entity:${entityId}`)\n }\n return Array.from(tags)\n}\n\nfunction normalizeFieldsetKey(value: string | string[] | null | undefined): string | null {\n if (!value) return null\n if (Array.isArray(value)) {\n const cleaned = value\n .map((entry) => (typeof entry === 'string' ? entry.trim() : ''))\n .filter((entry) => entry.length > 0)\n if (!cleaned.length) return null\n return cleaned.sort().join(',')\n }\n if (typeof value !== 'string') return null\n const trimmed = value.trim()\n return trimmed.length > 0 ? trimmed : null\n}\n\nfunction serializableIndexFromMap(index: CustomFieldDefinitionIndex): Array<[string, CustomFieldDefinitionSummary[]]> {\n return Array.from(index.entries())\n}\n\nfunction indexMapFromSerializable(value: unknown): CustomFieldDefinitionIndex | null {\n if (!Array.isArray(value)) return null\n const map: CustomFieldDefinitionIndex = new Map()\n for (const entry of value) {\n if (!Array.isArray(entry) || entry.length !== 2) return null\n const [key, summaries] = entry as [unknown, unknown]\n if (typeof key !== 'string' || !Array.isArray(summaries)) return null\n map.set(key, summaries as CustomFieldDefinitionSummary[])\n }\n return map\n}\n\n// Per-request micro-cache. Two CRUD calls within one HTTP request (rare but\n// possible via interceptors) share the same Map keyed by ctx-like objects.\nconst requestScopedCfDefIndexCache = new WeakMap<object, Map<string, CustomFieldDefinitionIndex>>()\n\nexport type CustomFieldDefinitionIndexCacheKey = string\n\nexport function getRequestScopedCfDefIndexCache(scope: object): Map<string, CustomFieldDefinitionIndex> {\n let bucket = requestScopedCfDefIndexCache.get(scope)\n if (!bucket) {\n bucket = new Map()\n requestScopedCfDefIndexCache.set(scope, bucket)\n }\n return bucket\n}\n\nasync function loadCustomFieldDefinitionIndexFresh(\n opts: LoadCustomFieldDefinitionIndexOptions & { entityIds: string[]; orgCandidates: string[] }\n): Promise<CustomFieldDefinitionIndex> {\n const { em, entityIds, orgCandidates } = opts\n const tenantId = opts.tenantId ?? null\n const scopeClauses: Record<string, unknown>[] = [\n tenantId\n ? { $or: [{ tenantId: tenantId as any }, { tenantId: null }] }\n : { tenantId: null },\n ]\n if (orgCandidates.length) {\n scopeClauses.push({\n $or: [{ organizationId: { $in: orgCandidates as any } }, { organizationId: null }],\n })\n } else {\n scopeClauses.push({ organizationId: null })\n }\n const where: Record<string, unknown> = {\n entityId: { $in: entityIds as any },\n deletedAt: null,\n isActive: true,\n $and: scopeClauses,\n }\n const defs = await em.find(CustomFieldDef, where as any)\n const rows: CustomFieldDefinitionRow[] = defs.map((def) => ({\n key: def.key,\n entityId: String((def as any).entityId),\n kind: typeof def.kind === 'string' ? def.kind : null,\n configJson: (def as any).configJson,\n organizationId: def.organizationId ?? null,\n tenantId: def.tenantId ?? null,\n deletedAt: (def as any).deletedAt ?? null,\n updatedAt: (def as any).updatedAt ?? null,\n }))\n return buildCustomFieldDefinitionIndexFromRows(rows, {\n organizationIds: orgCandidates,\n fieldset: opts.fieldset,\n })\n}\n\nexport async function loadCustomFieldDefinitionIndex(opts: LoadCustomFieldDefinitionIndexOptions & {\n cache?: CustomFieldDefIndexCache | null\n requestScope?: object | null\n}): Promise<CustomFieldDefinitionIndex> {\n const list = Array.isArray(opts.entityIds) ? opts.entityIds : [opts.entityIds]\n const entityIds = list\n .map((id) => (typeof id === 'string' ? id.trim() : String(id ?? '')))\n .filter((id) => id.length > 0)\n if (!entityIds.length) return new Map()\n const tenantId = opts.tenantId ?? null\n const orgCandidates = Array.isArray(opts.organizationIds)\n ? opts.organizationIds\n .map((id) => (typeof id === 'string' ? id.trim() : id))\n .filter((id): id is string => typeof id === 'string' && id.length > 0)\n : []\n\n const fieldsetKey = normalizeFieldsetKey(opts.fieldset)\n const ttlMs = resolveCfDefIndexCacheTtlMs()\n const cacheKey = buildCfDefIndexCacheKey({\n tenantId,\n entityIds,\n organizationIds: orgCandidates,\n fieldsetKey,\n })\n\n const requestBucket = opts.requestScope\n ? getRequestScopedCfDefIndexCache(opts.requestScope)\n : null\n if (requestBucket) {\n const cached = requestBucket.get(cacheKey)\n if (cached) return cached\n }\n\n const sharedCache = ttlMs > 0 ? opts.cache ?? null : null\n if (sharedCache && typeof sharedCache.get === 'function') {\n try {\n const cached = await sharedCache.get(cacheKey)\n const restored = indexMapFromSerializable(cached)\n if (restored) {\n if (requestBucket) requestBucket.set(cacheKey, restored)\n return restored\n }\n } catch (err) {\n console.warn('[crud:cf-def-cache] read failed', err)\n }\n }\n\n const index = await loadCustomFieldDefinitionIndexFresh({\n ...opts,\n entityIds,\n orgCandidates,\n })\n\n if (sharedCache && typeof sharedCache.set === 'function') {\n try {\n await sharedCache.set(cacheKey, serializableIndexFromMap(index), {\n ttl: ttlMs,\n tags: buildCfDefIndexCacheTags({ tenantId, entityIds }),\n })\n } catch (err) {\n console.warn('[crud:cf-def-cache] write failed', err)\n }\n }\n if (requestBucket) requestBucket.set(cacheKey, index)\n return index\n}\n\nexport type ApplyCustomFieldsNormalizationOptions = {\n /**\n * When true, removes raw `cf_*` and `cf:*` keys from the record once they\n * have been extracted into `customValues` / `customFields`. Produces a single\n * canonical response shape (issue #1769). Defaults to `false` to preserve the\n * existing wire format for callers that read `cf_*` from the top level.\n */\n stripPrefixedKeys?: boolean\n}\n\nexport function applyCustomFieldsNormalization(\n record: Record<string, unknown>,\n decorated: CustomFieldDisplayPayload,\n options: ApplyCustomFieldsNormalizationOptions = {},\n): Record<string, unknown> {\n const stripPrefixedKeys = options.stripPrefixedKeys === true\n const base: Record<string, unknown> = {}\n for (const [key, value] of Object.entries(record)) {\n if (stripPrefixedKeys && (key.startsWith('cf_') || key.startsWith('cf:'))) continue\n base[key] = value\n }\n base.customValues = decorated.customValues\n base.customFields = decorated.customFields\n return base\n}\n\nexport function decorateRecordWithCustomFields(\n record: Record<string, unknown>,\n definitions: CustomFieldDefinitionIndex,\n context: {\n organizationId?: string | null\n tenantId?: string | null\n } = {},\n): CustomFieldDisplayPayload {\n const rawEntries = extractAllCustomFieldEntries(record)\n if (!Object.keys(rawEntries).length) {\n return { customValues: null, customFields: [] }\n }\n const values: Record<string, unknown> = {}\n const entries: Array<{ entry: CustomFieldDisplayEntry; priority: number; updatedAt: number }> = []\n const organizationId = context.organizationId ?? null\n const tenantId = context.tenantId ?? null\n\n Object.entries(rawEntries).forEach(([prefixedKey, value]) => {\n const bareKey = prefixedKey.replace(/^cf_/, '')\n const normalizedKey = normalizeDefinitionKey(bareKey)\n if (!normalizedKey) return\n const defsForKey = definitions.get(normalizedKey) ?? []\n const resolvedDef = selectDefinitionForRecord(defsForKey, organizationId, tenantId)\n // Skip custom field values without active definitions to prevent orphaned fields\n if (!resolvedDef) return\n values[bareKey] = value\n const entry: CustomFieldDisplayEntry = {\n key: bareKey,\n label: resolvedDef.label ?? bareKey,\n value,\n kind: resolvedDef.kind ?? null,\n multi: resolvedDef.multi ?? Array.isArray(value),\n }\n entries.push({\n entry,\n priority: resolvedDef.priority ?? Number.MAX_SAFE_INTEGER,\n updatedAt: resolvedDef.updatedAt ?? 0,\n })\n })\n\n const ordered = entries\n .sort((a, b) => {\n const priorityDiff = a.priority - b.priority\n if (priorityDiff !== 0) return priorityDiff\n const updatedDiff = b.updatedAt - a.updatedAt\n if (updatedDiff !== 0) return updatedDiff\n return a.entry.key.localeCompare(b.entry.key)\n })\n .map((item) => item.entry)\n\n return {\n customValues: Object.keys(values).length ? values : null,\n customFields: ordered,\n }\n}\n\nexport async function loadCustomFieldValues(opts: {\n em: EntityManager\n entityId: EntityId\n recordIds: string[]\n tenantIdByRecord?: Record<string, string | null | undefined>\n organizationIdByRecord?: Record<string, string | null | undefined>\n tenantFallbacks?: (string | null | undefined)[]\n encryptionService?: TenantDataEncryptionService | null\n}): Promise<Record<string, Record<string, unknown>>> {\n const { em, entityId, recordIds } = opts\n if (!Array.isArray(recordIds) || recordIds.length === 0) return {}\n\n const normalizedRecordIds = recordIds.map((id) => String(id))\n let encryptionService: TenantDataEncryptionService | null | undefined\n const encryptionCache = new Map<string | null, string | null>()\n const getEncryptionService = () => {\n if (encryptionService !== undefined) return encryptionService\n encryptionService = resolveTenantEncryptionService(em, opts.encryptionService)\n return encryptionService\n }\n const tenantCandidates = new Set<string | null>()\n tenantCandidates.add(null)\n if (opts.tenantIdByRecord) {\n for (const val of Object.values(opts.tenantIdByRecord)) {\n tenantCandidates.add(val ? String(val) : null)\n }\n }\n if (opts.tenantFallbacks) {\n for (const val of opts.tenantFallbacks) tenantCandidates.add(val ? String(val) : null)\n }\n const fallbackTenant = (opts.tenantFallbacks || []).find((t) => t != null) ?? null\n\n const tenantList = Array.from(tenantCandidates)\n const tenantNonNull = tenantList.filter((t): t is string => t !== null)\n const tenantFilter = tenantNonNull.length\n ? { tenantId: { $in: [...tenantNonNull, null] as any } }\n : { tenantId: null }\n const cfRows = await em.find(CustomFieldValue, {\n entityId: entityId as any,\n recordId: { $in: normalizedRecordIds as any },\n deletedAt: null,\n ...(tenantList.length ? tenantFilter : {}),\n })\n\n if (!cfRows.length) return {}\n\n const allKeys = Array.from(new Set(cfRows.map((row) => String(row.fieldKey))))\n const organizationCandidates = new Set<string | null>()\n organizationCandidates.add(null)\n if (opts.organizationIdByRecord) {\n for (const val of Object.values(opts.organizationIdByRecord)) {\n organizationCandidates.add(val ? String(val) : null)\n }\n }\n for (const row of cfRows) {\n organizationCandidates.add(row.organizationId ? String(row.organizationId) : null)\n }\n const orgList = Array.from(organizationCandidates)\n\n const defs = allKeys.length\n ? await em.find(CustomFieldDef, {\n entityId: entityId as any,\n key: { $in: allKeys as any },\n deletedAt: null,\n isActive: true,\n ...(tenantList.length ? { tenantId: tenantFilter.tenantId } : {}),\n organizationId: { $in: orgList as any },\n })\n : []\n\n const defsByKey = new Map<string, CustomFieldDef[]>()\n for (const def of defs) {\n const list = defsByKey.get(def.key) || []\n list.push(def)\n defsByKey.set(def.key, list)\n }\n\n const pickDefinition = (fieldKey: string, organizationId: string | null, tenantId: string | null) => {\n const candidates = defsByKey.get(fieldKey)\n if (!candidates || candidates.length === 0) return null\n const active = candidates.filter((opt) => opt.isActive !== false && !opt.deletedAt)\n const list = active.length ? active : candidates\n if (organizationId && tenantId) {\n const exact = list.find((opt) => opt.organizationId === organizationId && opt.tenantId === tenantId)\n if (exact) return exact\n }\n if (organizationId) {\n const orgMatch = list.find((opt) => opt.organizationId === organizationId && (!tenantId || opt.tenantId == null || opt.tenantId === tenantId))\n if (orgMatch) return orgMatch\n }\n if (tenantId) {\n const tenantMatch = list.find((opt) => opt.organizationId == null && opt.tenantId === tenantId)\n if (tenantMatch) return tenantMatch\n }\n const global = list.find((opt) => opt.organizationId == null && opt.tenantId == null)\n return global ?? list[0]\n }\n\n const valueFromRow = (row: CustomFieldValue): unknown => {\n if (row.valueMultiline !== null && row.valueMultiline !== undefined) return row.valueMultiline\n if (row.valueText !== null && row.valueText !== undefined) return row.valueText\n if (row.valueInt !== null && row.valueInt !== undefined) return row.valueInt\n if (row.valueFloat !== null && row.valueFloat !== undefined) return row.valueFloat\n if (row.valueBool !== null && row.valueBool !== undefined) return row.valueBool\n return null\n }\n\n type Bucket = { orgId: string | null; tenantId: string | null; values: unknown[]; def?: CustomFieldDef | null; encrypted?: boolean }\n const buckets = new Map<string, Bucket>()\n\n const rowInfos = cfRows.map((row) => {\n const recordId = String(row.recordId)\n const key = String(row.fieldKey)\n const bucketKey = `${recordId}::${key}`\n const orgId = row.organizationId ? String(row.organizationId) : null\n const tenantId = row.tenantId ? String(row.tenantId) : null\n const resolvedOrgId = orgId ?? (opts.organizationIdByRecord?.[recordId] ?? null)\n const resolvedTenantId = tenantId ?? (opts.tenantIdByRecord?.[recordId] ?? fallbackTenant)\n const def = pickDefinition(key, resolvedOrgId, resolvedTenantId)\n const encrypted = Boolean(def?.configJson && (def as any).configJson?.encrypted)\n const value = valueFromRow(row)\n return { bucketKey, resolvedOrgId, resolvedTenantId, tenantId, def, encrypted, value }\n })\n\n // Decrypt every encrypted value concurrently so a list of N rows \u00D7 M encrypted\n // fields costs the slowest single decryption rather than the sum (issue #2229).\n // The shared encryptionCache keeps DEK lookups deduped per tenant.\n const decryptedValues = await Promise.all(\n rowInfos.map((info) =>\n info.encrypted\n ? decryptCustomFieldValue(\n info.value,\n info.resolvedTenantId ?? info.tenantId ?? null,\n getEncryptionService(),\n encryptionCache,\n { kind: info.def?.kind ?? null },\n )\n : info.value,\n ),\n )\n\n rowInfos.forEach((info, index) => {\n const decrypted = decryptedValues[index]\n const existing = buckets.get(info.bucketKey)\n if (existing) {\n if (existing.orgId == null && info.resolvedOrgId) existing.orgId = info.resolvedOrgId\n if (existing.tenantId == null && info.resolvedTenantId) existing.tenantId = info.resolvedTenantId\n if (existing.def == null && info.def) existing.def = info.def\n existing.encrypted = existing.encrypted || info.encrypted\n existing.values.push(decrypted)\n } else {\n buckets.set(info.bucketKey, { orgId: info.resolvedOrgId, tenantId: info.resolvedTenantId, values: [decrypted], def: info.def ?? null, encrypted: info.encrypted })\n }\n })\n\n const result: Record<string, Record<string, unknown>> = {}\n for (const [compoundKey, bucket] of buckets.entries()) {\n const [recordId, fieldKey] = compoundKey.split('::')\n if (!result[recordId]) result[recordId] = {}\n const prefixed = `cf_${fieldKey}`\n const def = bucket.def ?? pickDefinition(fieldKey, bucket.orgId ?? (opts.organizationIdByRecord?.[recordId] ?? null), bucket.tenantId ?? (opts.tenantIdByRecord?.[recordId] ?? null))\n if (def && def.configJson && typeof def.configJson === 'object' && (def.configJson as any).multi) {\n const cleaned = bucket.values.filter((v) => v !== undefined && v !== null)\n result[recordId][prefixed] = cleaned\n } else if (bucket.values.length > 1) {\n const cleaned = bucket.values.filter((v) => v !== undefined)\n result[recordId][prefixed] = cleaned\n } else {\n result[recordId][prefixed] = bucket.values[0] ?? null\n }\n }\n\n return result\n}\n\nexport function summarizeCustomFields(record: Record<string, unknown>): CustomFieldSnapshot {\n const entries = extractAllCustomFieldEntries(record)\n const values = Object.fromEntries(\n Object.entries(entries).map(([prefixedKey, value]) => [\n prefixedKey.replace(/^cf_/, ''),\n value,\n ]),\n )\n const customValues = Object.keys(values).length ? values : null\n const customFields = Object.entries(values).map(([key, value]) => ({\n key,\n label: key,\n value,\n kind: null,\n multi: Array.isArray(value),\n }))\n return { entries, customValues, customFields }\n}\n"],
|
|
5
|
-
"mappings": "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",
|
|
4
|
+
"sourcesContent": ["import type { CustomFieldSet, EntityId } from '@open-mercato/shared/modules/entities'\nimport type { EntityManager } from '@mikro-orm/core'\nimport { CustomFieldDef, CustomFieldValue } from '@open-mercato/core/modules/entities/data/entities'\nimport type { WhereValue } from '@open-mercato/shared/lib/query/types'\nimport type { TenantDataEncryptionService } from '../encryption/tenantDataEncryptionService'\nimport { decryptCustomFieldValue, resolveTenantEncryptionService } from '../encryption/customFieldValues'\nimport { parseBooleanToken } from '../boolean'\nimport { extractCustomFieldEntries } from './custom-fields-client'\nimport {\n buildCustomFieldDefinitionIndexFromRows,\n normalizeDefinitionKey,\n normalizeFieldsetFilter,\n selectDefinitionForRecord,\n type CustomFieldDefinitionIndex,\n type CustomFieldDefinitionRow,\n type CustomFieldDefinitionSummary,\n} from './custom-field-definition-index'\n\nexport type { CustomFieldDefinitionSummary, CustomFieldDefinitionIndex } from './custom-field-definition-index'\n\nexport type CustomFieldSelectors = {\n keys: string[]\n selectors: string[] // e.g. ['cf:priority', 'cf:severity']\n outputKeys: string[] // e.g. ['cf_priority', 'cf_severity']\n}\n\nexport type SplitCustomFieldPayload = {\n base: Record<string, unknown>\n custom: Record<string, unknown>\n}\n\nexport type CustomFieldDisplayEntry = {\n key: string\n label: string | null\n value: unknown\n kind: string | null\n multi: boolean\n}\n\nexport type CustomFieldDisplayPayload = {\n customValues: Record<string, unknown> | null\n customFields: CustomFieldDisplayEntry[]\n}\n\nexport type CustomFieldSnapshot = {\n entries: Record<string, unknown>\n customValues: Record<string, unknown> | null\n customFields: CustomFieldDisplayEntry[]\n}\n\nexport function buildCustomFieldSelectorsForEntity(entityId: EntityId, sets: CustomFieldSet[]): CustomFieldSelectors {\n const keys = Array.from(new Set(\n (sets || [])\n .filter((s) => s.entity === entityId)\n .flatMap((s) => (s.fields || []).map((f) => f.key))\n ))\n const selectors = keys.map((k) => `cf:${k}`)\n const outputKeys = keys.map((k) => `cf_${k}`)\n return { keys, selectors, outputKeys }\n}\n\nexport function normalizeCustomFieldValue(val: unknown): unknown {\n if (Array.isArray(val)) return val\n if (typeof val === 'string') {\n const s = val.trim()\n // Parse Postgres array-like '{a,b,c}' to string[] when present\n if (s.startsWith('{') && s.endsWith('}')) {\n const inner = s.slice(1, -1).trim()\n if (!inner) return []\n return inner.split(/[\\s,]+/).map((x) => x.trim()).filter(Boolean)\n }\n return s\n }\n return val as any\n}\n\n// Extracts cf_* fields from a record that may contain both 'cf:<key>' and/or 'cf_<key>'\nexport function extractCustomFieldsFromItem(item: Record<string, unknown>, keys: string[]): Record<string, unknown> {\n const out: Record<string, unknown> = {}\n for (const key of keys) {\n const colon = item[`cf:${key}` as keyof typeof item]\n const snake = item[`cf_${key}` as keyof typeof item]\n const value = colon !== undefined ? colon : snake\n if (value !== undefined) out[`cf_${key}`] = normalizeCustomFieldValue(value)\n }\n return out\n}\n\nexport function extractAllCustomFieldEntries(item: Record<string, unknown>): Record<string, unknown> {\n return extractCustomFieldEntries(item)\n}\n\nexport async function buildCustomFieldFiltersFromQuery(opts: {\n entityId?: EntityId\n entityIds?: EntityId[]\n query: Record<string, unknown>\n em: EntityManager\n tenantId: string | null | undefined\n fieldset?: string | string[] | null\n}): Promise<Record<string, WhereValue>> {\n const out: Record<string, WhereValue> = {}\n const entries = Object.entries(opts.query).filter(([k]) => k.startsWith('cf_'))\n if (!entries.length) return out\n\n const entityIdList = Array.isArray(opts.entityIds) && opts.entityIds.length\n ? opts.entityIds\n : opts.entityId\n ? [opts.entityId]\n : []\n if (!entityIdList.length) return out\n\n // Tenant-only scope: allow global (null) or tenant match; ignore organization here\n const defs = await opts.em.find(CustomFieldDef, {\n entityId: { $in: entityIdList as any },\n isActive: true,\n $and: [\n { $or: [ { tenantId: opts.tenantId as any }, { tenantId: null } ] },\n ],\n })\n const fieldsetFilter = normalizeFieldsetFilter(opts.fieldset)\n const order = new Map<string, number>()\n entityIdList.map(String).forEach((id, index) => order.set(id, index))\n const byKey: Record<string, { kind: string; multi?: boolean; entityId: string }> = {}\n for (const d of defs) {\n if (fieldsetFilter) {\n const fieldsets = Array.isArray(d.configJson?.fieldsets)\n ? d.configJson.fieldsets\n .filter((entry: unknown): entry is string => typeof entry === 'string')\n .map((entry: string) => entry.trim())\n .filter((entry: string) => entry.length > 0)\n : []\n const rawFieldset = typeof d.configJson?.fieldset === 'string' ? d.configJson.fieldset.trim() : ''\n const normalizedFieldset = rawFieldset.length ? rawFieldset : null\n const matches = fieldsets.length > 0\n ? fieldsets.some((entry: string) => fieldsetFilter.has(entry))\n : fieldsetFilter.has(normalizedFieldset)\n if (!matches) continue\n }\n const key = d.key\n const entityId = String(d.entityId)\n const current = byKey[key]\n const rankNew = order.get(entityId) ?? Number.MAX_SAFE_INTEGER\n if (!current) {\n byKey[key] = { kind: d.kind, multi: Boolean((d as any).configJson?.multi), entityId }\n continue\n }\n const rankOld = order.get(current.entityId) ?? Number.MAX_SAFE_INTEGER\n if (rankNew < rankOld) {\n byKey[key] = { kind: d.kind, multi: Boolean((d as any).configJson?.multi), entityId }\n }\n }\n\n const coerce = (kind: string, v: unknown) => {\n if (v == null) return v as undefined\n switch (kind) {\n case 'integer': return Number.parseInt(String(v), 10)\n case 'float': return Number.parseFloat(String(v))\n case 'boolean': return parseBooleanToken(String(v)) === true\n case 'date':\n case 'datetime': return String(v)\n default: return String(v)\n }\n }\n\n for (const [rawKey, rawVal] of entries) {\n const isIn = rawKey.endsWith('In')\n const key = isIn ? rawKey.replace(/^cf_/, '').replace(/In$/, '') : rawKey.replace(/^cf_/, '')\n const def = byKey[key]\n const fieldId = `cf:${key}`\n if (!def) continue\n if (isIn) {\n const list = Array.isArray(rawVal)\n ? (rawVal as unknown[])\n : String(rawVal)\n .split(',')\n .map((s) => s.trim())\n .filter(Boolean)\n if (list.length) out[fieldId] = { $in: list.map((x) => coerce(def.kind, x)) as (string[] | number[] | boolean[]) }\n } else {\n out[fieldId] = coerce(def.kind, rawVal)\n }\n }\n\n return out\n}\n\nexport function splitCustomFieldPayload(raw: unknown): SplitCustomFieldPayload {\n const base: Record<string, unknown> = {}\n const custom: Record<string, unknown> = {}\n if (!raw || typeof raw !== 'object') return { base, custom }\n for (const [key, value] of Object.entries(raw as Record<string, unknown>)) {\n if (key === 'customFields') {\n if (Array.isArray(value)) {\n value.forEach((entry) => {\n if (!entry || typeof entry !== 'object') return\n const entryKey = typeof (entry as any).key === 'string' ? (entry as any).key.trim() : ''\n if (!entryKey) return\n custom[entryKey] = (entry as any).value\n })\n continue\n }\n if (value && typeof value === 'object') {\n for (const [ck, cv] of Object.entries(value as Record<string, unknown>)) {\n const normalizedKey = typeof ck === 'string' ? ck.trim() : ''\n if (!normalizedKey) continue\n custom[normalizedKey] = cv\n }\n continue\n }\n }\n if (key === 'customValues' && value && typeof value === 'object' && !Array.isArray(value)) {\n for (const [ck, cv] of Object.entries(value as Record<string, unknown>)) {\n custom[String(ck)] = cv\n }\n continue\n }\n if (key.startsWith('cf_')) {\n custom[key.slice(3)] = value\n continue\n }\n if (key.startsWith('cf:')) {\n custom[key.slice(3)] = value\n continue\n }\n base[key] = value\n }\n return { base, custom }\n}\n\nexport function extractCustomFieldValuesFromPayload(raw: Record<string, unknown>): Record<string, unknown> {\n return splitCustomFieldPayload(raw).custom\n}\n\ntype LoadCustomFieldDefinitionIndexOptions = {\n em: EntityManager\n entityIds: string | string[]\n tenantId?: string | null | undefined\n organizationIds?: Array<string | null | undefined> | null\n fieldset?: string | string[] | null\n}\n\ntype CustomFieldDefIndexCache = {\n get(key: string): Promise<unknown> | unknown\n set(key: string, value: unknown, opts?: { ttl?: number; tags?: string[] }): Promise<unknown> | unknown\n deleteByTags?(tags: string[]): Promise<number> | number\n}\n\nconst CF_DEF_INDEX_CACHE_KEY_PREFIX = 'crud:cf-def-index'\n// Phase 2 default-off: integration runs observed `/api/customers/people`\n// returning 500 with this cache path active, and the readiness-probe\n// timeout blocked artifact upload so the direct stack trace was lost.\n// Until cross-request safety of the SQLite-cache JSON round-trip is\n// re-verified, ship with the layer disabled. Set\n// `OM_CF_DEF_CACHE_TTL_MS=300000` (or any positive integer) to opt in.\nconst CF_DEF_INDEX_DEFAULT_TTL_MS = 0\n\nfunction resolveCfDefIndexCacheTtlMs(): number {\n const raw = process.env.OM_CF_DEF_CACHE_TTL_MS\n if (raw === undefined) return CF_DEF_INDEX_DEFAULT_TTL_MS\n const parsed = Number(raw)\n if (!Number.isFinite(parsed) || parsed < 0) return CF_DEF_INDEX_DEFAULT_TTL_MS\n return parsed\n}\n\nfunction buildCfDefIndexCacheKey(opts: {\n tenantId: string | null\n entityIds: string[]\n organizationIds: string[]\n fieldsetKey: string | null\n}): string {\n const tenant = opts.tenantId ?? 'global'\n const entities = opts.entityIds.slice().sort((a, b) => (a < b ? -1 : a > b ? 1 : 0)).join('|')\n const orgs = opts.organizationIds.length ? opts.organizationIds.slice().sort((a, b) => (a < b ? -1 : a > b ? 1 : 0)).join('|') : 'none'\n const fieldset = opts.fieldsetKey ?? 'all'\n return `${CF_DEF_INDEX_CACHE_KEY_PREFIX}:${tenant}:${entities}:${orgs}:${fieldset}`\n}\n\nfunction buildCfDefIndexCacheTags(opts: {\n tenantId: string | null\n entityIds: string[]\n}): string[] {\n const tenant = opts.tenantId ?? 'global'\n const tagBase = `entities:definitions:${tenant}`\n const tags = new Set<string>([tagBase])\n for (const entityId of opts.entityIds) {\n tags.add(`${tagBase}:entity:${entityId}`)\n }\n return Array.from(tags)\n}\n\nfunction normalizeFieldsetKey(value: string | string[] | null | undefined): string | null {\n if (!value) return null\n if (Array.isArray(value)) {\n const cleaned = value\n .map((entry) => (typeof entry === 'string' ? entry.trim() : ''))\n .filter((entry) => entry.length > 0)\n if (!cleaned.length) return null\n return cleaned.sort((a, b) => (a < b ? -1 : a > b ? 1 : 0)).join(',')\n }\n if (typeof value !== 'string') return null\n const trimmed = value.trim()\n return trimmed.length > 0 ? trimmed : null\n}\n\nfunction serializableIndexFromMap(index: CustomFieldDefinitionIndex): Array<[string, CustomFieldDefinitionSummary[]]> {\n return Array.from(index.entries())\n}\n\nfunction indexMapFromSerializable(value: unknown): CustomFieldDefinitionIndex | null {\n if (!Array.isArray(value)) return null\n const map: CustomFieldDefinitionIndex = new Map()\n for (const entry of value) {\n if (!Array.isArray(entry) || entry.length !== 2) return null\n const [key, summaries] = entry as [unknown, unknown]\n if (typeof key !== 'string' || !Array.isArray(summaries)) return null\n map.set(key, summaries as CustomFieldDefinitionSummary[])\n }\n return map\n}\n\n// Per-request micro-cache. Two CRUD calls within one HTTP request (rare but\n// possible via interceptors) share the same Map keyed by ctx-like objects.\nconst requestScopedCfDefIndexCache = new WeakMap<object, Map<string, CustomFieldDefinitionIndex>>()\n\nexport type CustomFieldDefinitionIndexCacheKey = string\n\nexport function getRequestScopedCfDefIndexCache(scope: object): Map<string, CustomFieldDefinitionIndex> {\n let bucket = requestScopedCfDefIndexCache.get(scope)\n if (!bucket) {\n bucket = new Map()\n requestScopedCfDefIndexCache.set(scope, bucket)\n }\n return bucket\n}\n\nasync function loadCustomFieldDefinitionIndexFresh(\n opts: LoadCustomFieldDefinitionIndexOptions & { entityIds: string[]; orgCandidates: string[] }\n): Promise<CustomFieldDefinitionIndex> {\n const { em, entityIds, orgCandidates } = opts\n const tenantId = opts.tenantId ?? null\n const scopeClauses: Record<string, unknown>[] = [\n tenantId\n ? { $or: [{ tenantId: tenantId as any }, { tenantId: null }] }\n : { tenantId: null },\n ]\n if (orgCandidates.length) {\n scopeClauses.push({\n $or: [{ organizationId: { $in: orgCandidates as any } }, { organizationId: null }],\n })\n } else {\n scopeClauses.push({ organizationId: null })\n }\n const where: Record<string, unknown> = {\n entityId: { $in: entityIds as any },\n deletedAt: null,\n isActive: true,\n $and: scopeClauses,\n }\n const defs = await em.find(CustomFieldDef, where as any)\n const rows: CustomFieldDefinitionRow[] = defs.map((def) => ({\n key: def.key,\n entityId: String((def as any).entityId),\n kind: typeof def.kind === 'string' ? def.kind : null,\n configJson: (def as any).configJson,\n organizationId: def.organizationId ?? null,\n tenantId: def.tenantId ?? null,\n deletedAt: (def as any).deletedAt ?? null,\n updatedAt: (def as any).updatedAt ?? null,\n }))\n return buildCustomFieldDefinitionIndexFromRows(rows, {\n organizationIds: orgCandidates,\n fieldset: opts.fieldset,\n })\n}\n\nexport async function loadCustomFieldDefinitionIndex(opts: LoadCustomFieldDefinitionIndexOptions & {\n cache?: CustomFieldDefIndexCache | null\n requestScope?: object | null\n}): Promise<CustomFieldDefinitionIndex> {\n const list = Array.isArray(opts.entityIds) ? opts.entityIds : [opts.entityIds]\n const entityIds = list\n .map((id) => (typeof id === 'string' ? id.trim() : String(id ?? '')))\n .filter((id) => id.length > 0)\n if (!entityIds.length) return new Map()\n const tenantId = opts.tenantId ?? null\n const orgCandidates = Array.isArray(opts.organizationIds)\n ? opts.organizationIds\n .map((id) => (typeof id === 'string' ? id.trim() : id))\n .filter((id): id is string => typeof id === 'string' && id.length > 0)\n : []\n\n const fieldsetKey = normalizeFieldsetKey(opts.fieldset)\n const ttlMs = resolveCfDefIndexCacheTtlMs()\n const cacheKey = buildCfDefIndexCacheKey({\n tenantId,\n entityIds,\n organizationIds: orgCandidates,\n fieldsetKey,\n })\n\n const requestBucket = opts.requestScope\n ? getRequestScopedCfDefIndexCache(opts.requestScope)\n : null\n if (requestBucket) {\n const cached = requestBucket.get(cacheKey)\n if (cached) return cached\n }\n\n const sharedCache = ttlMs > 0 ? opts.cache ?? null : null\n if (sharedCache && typeof sharedCache.get === 'function') {\n try {\n const cached = await sharedCache.get(cacheKey)\n const restored = indexMapFromSerializable(cached)\n if (restored) {\n if (requestBucket) requestBucket.set(cacheKey, restored)\n return restored\n }\n } catch (err) {\n console.warn('[crud:cf-def-cache] read failed', err)\n }\n }\n\n const index = await loadCustomFieldDefinitionIndexFresh({\n ...opts,\n entityIds,\n orgCandidates,\n })\n\n if (sharedCache && typeof sharedCache.set === 'function') {\n try {\n await sharedCache.set(cacheKey, serializableIndexFromMap(index), {\n ttl: ttlMs,\n tags: buildCfDefIndexCacheTags({ tenantId, entityIds }),\n })\n } catch (err) {\n console.warn('[crud:cf-def-cache] write failed', err)\n }\n }\n if (requestBucket) requestBucket.set(cacheKey, index)\n return index\n}\n\nexport type ApplyCustomFieldsNormalizationOptions = {\n /**\n * When true, removes raw `cf_*` and `cf:*` keys from the record once they\n * have been extracted into `customValues` / `customFields`. Produces a single\n * canonical response shape (issue #1769). Defaults to `false` to preserve the\n * existing wire format for callers that read `cf_*` from the top level.\n */\n stripPrefixedKeys?: boolean\n}\n\nexport function applyCustomFieldsNormalization(\n record: Record<string, unknown>,\n decorated: CustomFieldDisplayPayload,\n options: ApplyCustomFieldsNormalizationOptions = {},\n): Record<string, unknown> {\n const stripPrefixedKeys = options.stripPrefixedKeys === true\n const base: Record<string, unknown> = {}\n for (const [key, value] of Object.entries(record)) {\n if (stripPrefixedKeys && (key.startsWith('cf_') || key.startsWith('cf:'))) continue\n base[key] = value\n }\n base.customValues = decorated.customValues\n base.customFields = decorated.customFields\n return base\n}\n\nexport function decorateRecordWithCustomFields(\n record: Record<string, unknown>,\n definitions: CustomFieldDefinitionIndex,\n context: {\n organizationId?: string | null\n tenantId?: string | null\n } = {},\n): CustomFieldDisplayPayload {\n const rawEntries = extractAllCustomFieldEntries(record)\n if (!Object.keys(rawEntries).length) {\n return { customValues: null, customFields: [] }\n }\n const values: Record<string, unknown> = {}\n const entries: Array<{ entry: CustomFieldDisplayEntry; priority: number; updatedAt: number }> = []\n const organizationId = context.organizationId ?? null\n const tenantId = context.tenantId ?? null\n\n Object.entries(rawEntries).forEach(([prefixedKey, value]) => {\n const bareKey = prefixedKey.replace(/^cf_/, '')\n const normalizedKey = normalizeDefinitionKey(bareKey)\n if (!normalizedKey) return\n const defsForKey = definitions.get(normalizedKey) ?? []\n const resolvedDef = selectDefinitionForRecord(defsForKey, organizationId, tenantId)\n // Skip custom field values without active definitions to prevent orphaned fields\n if (!resolvedDef) return\n values[bareKey] = value\n const entry: CustomFieldDisplayEntry = {\n key: bareKey,\n label: resolvedDef.label ?? bareKey,\n value,\n kind: resolvedDef.kind ?? null,\n multi: resolvedDef.multi ?? Array.isArray(value),\n }\n entries.push({\n entry,\n priority: resolvedDef.priority ?? Number.MAX_SAFE_INTEGER,\n updatedAt: resolvedDef.updatedAt ?? 0,\n })\n })\n\n const ordered = entries\n .sort((a, b) => {\n const priorityDiff = a.priority - b.priority\n if (priorityDiff !== 0) return priorityDiff\n const updatedDiff = b.updatedAt - a.updatedAt\n if (updatedDiff !== 0) return updatedDiff\n return a.entry.key.localeCompare(b.entry.key)\n })\n .map((item) => item.entry)\n\n return {\n customValues: Object.keys(values).length ? values : null,\n customFields: ordered,\n }\n}\n\nexport async function loadCustomFieldValues(opts: {\n em: EntityManager\n entityId: EntityId\n recordIds: string[]\n tenantIdByRecord?: Record<string, string | null | undefined>\n organizationIdByRecord?: Record<string, string | null | undefined>\n tenantFallbacks?: (string | null | undefined)[]\n encryptionService?: TenantDataEncryptionService | null\n}): Promise<Record<string, Record<string, unknown>>> {\n const { em, entityId, recordIds } = opts\n if (!Array.isArray(recordIds) || recordIds.length === 0) return {}\n\n const normalizedRecordIds = recordIds.map((id) => String(id))\n let encryptionService: TenantDataEncryptionService | null | undefined\n const encryptionCache = new Map<string | null, string | null>()\n const getEncryptionService = () => {\n if (encryptionService !== undefined) return encryptionService\n encryptionService = resolveTenantEncryptionService(em, opts.encryptionService)\n return encryptionService\n }\n const tenantCandidates = new Set<string | null>()\n tenantCandidates.add(null)\n if (opts.tenantIdByRecord) {\n for (const val of Object.values(opts.tenantIdByRecord)) {\n tenantCandidates.add(val ? String(val) : null)\n }\n }\n if (opts.tenantFallbacks) {\n for (const val of opts.tenantFallbacks) tenantCandidates.add(val ? String(val) : null)\n }\n const fallbackTenant = (opts.tenantFallbacks || []).find((t) => t != null) ?? null\n\n const tenantList = Array.from(tenantCandidates)\n const tenantNonNull = tenantList.filter((t): t is string => t !== null)\n const tenantFilter = tenantNonNull.length\n ? { tenantId: { $in: [...tenantNonNull, null] as any } }\n : { tenantId: null }\n const cfRows = await em.find(CustomFieldValue, {\n entityId: entityId as any,\n recordId: { $in: normalizedRecordIds as any },\n deletedAt: null,\n ...(tenantList.length ? tenantFilter : {}),\n })\n\n if (!cfRows.length) return {}\n\n const allKeys = Array.from(new Set(cfRows.map((row) => String(row.fieldKey))))\n const organizationCandidates = new Set<string | null>()\n organizationCandidates.add(null)\n if (opts.organizationIdByRecord) {\n for (const val of Object.values(opts.organizationIdByRecord)) {\n organizationCandidates.add(val ? String(val) : null)\n }\n }\n for (const row of cfRows) {\n organizationCandidates.add(row.organizationId ? String(row.organizationId) : null)\n }\n const orgList = Array.from(organizationCandidates)\n\n const defs = allKeys.length\n ? await em.find(CustomFieldDef, {\n entityId: entityId as any,\n key: { $in: allKeys as any },\n deletedAt: null,\n isActive: true,\n ...(tenantList.length ? { tenantId: tenantFilter.tenantId } : {}),\n organizationId: { $in: orgList as any },\n })\n : []\n\n const defsByKey = new Map<string, CustomFieldDef[]>()\n for (const def of defs) {\n const list = defsByKey.get(def.key) || []\n list.push(def)\n defsByKey.set(def.key, list)\n }\n\n const pickDefinition = (fieldKey: string, organizationId: string | null, tenantId: string | null) => {\n const candidates = defsByKey.get(fieldKey)\n if (!candidates || candidates.length === 0) return null\n const active = candidates.filter((opt) => opt.isActive !== false && !opt.deletedAt)\n const list = active.length ? active : candidates\n if (organizationId && tenantId) {\n const exact = list.find((opt) => opt.organizationId === organizationId && opt.tenantId === tenantId)\n if (exact) return exact\n }\n if (organizationId) {\n const orgMatch = list.find((opt) => opt.organizationId === organizationId && (!tenantId || opt.tenantId == null || opt.tenantId === tenantId))\n if (orgMatch) return orgMatch\n }\n if (tenantId) {\n const tenantMatch = list.find((opt) => opt.organizationId == null && opt.tenantId === tenantId)\n if (tenantMatch) return tenantMatch\n }\n const global = list.find((opt) => opt.organizationId == null && opt.tenantId == null)\n return global ?? list[0]\n }\n\n const valueFromRow = (row: CustomFieldValue): unknown => {\n if (row.valueMultiline !== null && row.valueMultiline !== undefined) return row.valueMultiline\n if (row.valueText !== null && row.valueText !== undefined) return row.valueText\n if (row.valueInt !== null && row.valueInt !== undefined) return row.valueInt\n if (row.valueFloat !== null && row.valueFloat !== undefined) return row.valueFloat\n if (row.valueBool !== null && row.valueBool !== undefined) return row.valueBool\n return null\n }\n\n type Bucket = { orgId: string | null; tenantId: string | null; values: unknown[]; def?: CustomFieldDef | null; encrypted?: boolean }\n const buckets = new Map<string, Bucket>()\n\n const rowInfos = cfRows.map((row) => {\n const recordId = String(row.recordId)\n const key = String(row.fieldKey)\n const bucketKey = `${recordId}::${key}`\n const orgId = row.organizationId ? String(row.organizationId) : null\n const tenantId = row.tenantId ? String(row.tenantId) : null\n const resolvedOrgId = orgId ?? (opts.organizationIdByRecord?.[recordId] ?? null)\n const resolvedTenantId = tenantId ?? (opts.tenantIdByRecord?.[recordId] ?? fallbackTenant)\n const def = pickDefinition(key, resolvedOrgId, resolvedTenantId)\n const encrypted = Boolean(def?.configJson && (def as any).configJson?.encrypted)\n const value = valueFromRow(row)\n return { bucketKey, resolvedOrgId, resolvedTenantId, tenantId, def, encrypted, value }\n })\n\n // Decrypt every encrypted value concurrently so a list of N rows \u00D7 M encrypted\n // fields costs the slowest single decryption rather than the sum (issue #2229).\n // The shared encryptionCache keeps DEK lookups deduped per tenant.\n const decryptedValues = await Promise.all(\n rowInfos.map((info) =>\n info.encrypted\n ? decryptCustomFieldValue(\n info.value,\n info.resolvedTenantId ?? info.tenantId ?? null,\n getEncryptionService(),\n encryptionCache,\n { kind: info.def?.kind ?? null },\n )\n : info.value,\n ),\n )\n\n rowInfos.forEach((info, index) => {\n const decrypted = decryptedValues[index]\n const existing = buckets.get(info.bucketKey)\n if (existing) {\n if (existing.orgId == null && info.resolvedOrgId) existing.orgId = info.resolvedOrgId\n if (existing.tenantId == null && info.resolvedTenantId) existing.tenantId = info.resolvedTenantId\n if (existing.def == null && info.def) existing.def = info.def\n existing.encrypted = existing.encrypted || info.encrypted\n existing.values.push(decrypted)\n } else {\n buckets.set(info.bucketKey, { orgId: info.resolvedOrgId, tenantId: info.resolvedTenantId, values: [decrypted], def: info.def ?? null, encrypted: info.encrypted })\n }\n })\n\n const result: Record<string, Record<string, unknown>> = {}\n for (const [compoundKey, bucket] of buckets.entries()) {\n const [recordId, fieldKey] = compoundKey.split('::')\n if (!result[recordId]) result[recordId] = {}\n const prefixed = `cf_${fieldKey}`\n const def = bucket.def ?? pickDefinition(fieldKey, bucket.orgId ?? (opts.organizationIdByRecord?.[recordId] ?? null), bucket.tenantId ?? (opts.tenantIdByRecord?.[recordId] ?? null))\n if (def && def.configJson && typeof def.configJson === 'object' && (def.configJson as any).multi) {\n const cleaned = bucket.values.filter((v) => v !== undefined && v !== null)\n result[recordId][prefixed] = cleaned\n } else if (bucket.values.length > 1) {\n const cleaned = bucket.values.filter((v) => v !== undefined)\n result[recordId][prefixed] = cleaned\n } else {\n result[recordId][prefixed] = bucket.values[0] ?? null\n }\n }\n\n return result\n}\n\nexport function summarizeCustomFields(record: Record<string, unknown>): CustomFieldSnapshot {\n const entries = extractAllCustomFieldEntries(record)\n const values = Object.fromEntries(\n Object.entries(entries).map(([prefixedKey, value]) => [\n prefixedKey.replace(/^cf_/, ''),\n value,\n ]),\n )\n const customValues = Object.keys(values).length ? values : null\n const customFields = Object.entries(values).map(([key, value]) => ({\n key,\n label: key,\n value,\n kind: null,\n multi: Array.isArray(value),\n }))\n return { entries, customValues, customFields }\n}\n"],
|
|
5
|
+
"mappings": "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",
|
|
6
6
|
"names": []
|
|
7
7
|
}
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"version": 3,
|
|
3
3
|
"sources": ["../../../src/lib/crud/mutation-guard.ts"],
|
|
4
|
-
"sourcesContent": ["import type { AwilixContainer } from 'awilix'\n\nexport type CrudMutationGuardValidationSuccess = {\n ok: true\n shouldRunAfterSuccess: boolean\n metadata?: Record<string, unknown> | null\n}\n\nexport type CrudMutationGuardValidationFailure = {\n ok: false\n status: number\n body: Record<string, unknown>\n}\n\nexport type CrudMutationGuardValidationResult =\n | CrudMutationGuardValidationSuccess\n | CrudMutationGuardValidationFailure\n\nexport type CrudMutationGuardValidateInput = {\n tenantId: string\n organizationId?: string | null\n userId: string\n resourceKind: string\n resourceId: string\n operation: 'create' | 'update' | 'delete' | 'custom'\n requestMethod: string\n requestHeaders: Headers\n mutationPayload?: Record<string, unknown> | null\n}\n\nexport type CrudMutationGuardAfterSuccessInput = {\n tenantId: string\n organizationId?: string | null\n userId: string\n resourceKind: string\n resourceId: string\n operation: 'create' | 'update' | 'delete' | 'custom'\n requestMethod: string\n requestHeaders: Headers\n metadata?: Record<string, unknown> | null\n}\n\ntype CrudMutationGuardServiceLike = {\n validateMutation: (input: CrudMutationGuardValidateInput) => Promise<CrudMutationGuardValidationResult>\n afterMutationSuccess: (input: CrudMutationGuardAfterSuccessInput) => Promise<void>\n}\n\nfunction resolveCrudMutationGuardService(container: AwilixContainer): CrudMutationGuardServiceLike | null {\n try {\n const service = container.resolve<CrudMutationGuardServiceLike>('crudMutationGuardService')\n if (!service) return null\n if (typeof service.validateMutation !== 'function') return null\n if (typeof service.afterMutationSuccess !== 'function') return null\n return service\n } catch {\n return null\n }\n}\n\n/**\n * @deprecated Use `runMutationGuards()` from `@open-mercato/shared/lib/crud/mutation-guard-registry`
|
|
5
|
-
"mappings": "AA+CA,SAAS,gCAAgC,WAAiE;AACxG,MAAI;AACF,UAAM,UAAU,UAAU,QAAsC,0BAA0B;AAC1F,QAAI,CAAC,QAAS,QAAO;AACrB,QAAI,OAAO,QAAQ,qBAAqB,WAAY,QAAO;AAC3D,QAAI,OAAO,QAAQ,yBAAyB,WAAY,QAAO;AAC/D,WAAO;AAAA,EACT,QAAQ;AACN,WAAO;AAAA,EACT;AACF;
|
|
4
|
+
"sourcesContent": ["import type { AwilixContainer } from 'awilix'\n\nexport type CrudMutationGuardValidationSuccess = {\n ok: true\n shouldRunAfterSuccess: boolean\n metadata?: Record<string, unknown> | null\n}\n\nexport type CrudMutationGuardValidationFailure = {\n ok: false\n status: number\n body: Record<string, unknown>\n}\n\nexport type CrudMutationGuardValidationResult =\n | CrudMutationGuardValidationSuccess\n | CrudMutationGuardValidationFailure\n\nexport type CrudMutationGuardValidateInput = {\n tenantId: string\n organizationId?: string | null\n userId: string\n resourceKind: string\n resourceId: string\n operation: 'create' | 'update' | 'delete' | 'custom'\n requestMethod: string\n requestHeaders: Headers\n mutationPayload?: Record<string, unknown> | null\n}\n\nexport type CrudMutationGuardAfterSuccessInput = {\n tenantId: string\n organizationId?: string | null\n userId: string\n resourceKind: string\n resourceId: string\n operation: 'create' | 'update' | 'delete' | 'custom'\n requestMethod: string\n requestHeaders: Headers\n metadata?: Record<string, unknown> | null\n}\n\ntype CrudMutationGuardServiceLike = {\n validateMutation: (input: CrudMutationGuardValidateInput) => Promise<CrudMutationGuardValidationResult>\n afterMutationSuccess: (input: CrudMutationGuardAfterSuccessInput) => Promise<void>\n}\n\nfunction resolveCrudMutationGuardService(container: AwilixContainer): CrudMutationGuardServiceLike | null {\n try {\n const service = container.resolve<CrudMutationGuardServiceLike>('crudMutationGuardService')\n if (!service) return null\n if (typeof service.validateMutation !== 'function') return null\n if (typeof service.afterMutationSuccess !== 'function') return null\n return service\n } catch {\n return null\n }\n}\n\n/**\n * @deprecated Resolves ONLY the single DI-registered `crudMutationGuardService`,\n * so it silently bypasses every guard in the global mutation-guard store\n * (`getAllMutationGuardInstances()`). Use the full registry instead:\n * `runRouteMutationGuards()` from `@open-mercato/shared/lib/crud/route-mutation-guard`\n * for custom write routes, or `runMutationGuards()` from\n * `@open-mercato/shared/lib/crud/mutation-guard-registry` directly. The legacy\n * service is still honored on the modern path via `bridgeLegacyGuard()`. This\n * function will be removed in a future release.\n */\nexport async function validateCrudMutationGuard(\n container: AwilixContainer,\n input: CrudMutationGuardValidateInput,\n): Promise<CrudMutationGuardValidationResult | null> {\n const service = resolveCrudMutationGuardService(container)\n if (!service) return null\n return service.validateMutation(input)\n}\n\n/**\n * @deprecated Runs ONLY the single DI-registered `crudMutationGuardService`'s\n * after-success hook, skipping the registry guards' `afterSuccess` callbacks.\n * Use the `runAfterSuccess()` returned by `runRouteMutationGuards()` from\n * `@open-mercato/shared/lib/crud/route-mutation-guard`, or the\n * `afterSuccessCallbacks` returned by `runMutationGuards()` from\n * `@open-mercato/shared/lib/crud/mutation-guard-registry`. This function will be\n * removed in a future release.\n */\nexport async function runCrudMutationGuardAfterSuccess(\n container: AwilixContainer,\n input: CrudMutationGuardAfterSuccessInput,\n): Promise<void> {\n const service = resolveCrudMutationGuardService(container)\n if (!service) return\n try {\n await service.afterMutationSuccess(input)\n } catch (error) {\n console.error('[crud-mutation-guard] after-success hook failed', {\n resourceKind: input.resourceKind,\n resourceId: input.resourceId,\n operation: input.operation,\n requestMethod: input.requestMethod,\n error: error instanceof Error ? error.message : String(error),\n })\n }\n}\n"],
|
|
5
|
+
"mappings": "AA+CA,SAAS,gCAAgC,WAAiE;AACxG,MAAI;AACF,UAAM,UAAU,UAAU,QAAsC,0BAA0B;AAC1F,QAAI,CAAC,QAAS,QAAO;AACrB,QAAI,OAAO,QAAQ,qBAAqB,WAAY,QAAO;AAC3D,QAAI,OAAO,QAAQ,yBAAyB,WAAY,QAAO;AAC/D,WAAO;AAAA,EACT,QAAQ;AACN,WAAO;AAAA,EACT;AACF;AAYA,eAAsB,0BACpB,WACA,OACmD;AACnD,QAAM,UAAU,gCAAgC,SAAS;AACzD,MAAI,CAAC,QAAS,QAAO;AACrB,SAAO,QAAQ,iBAAiB,KAAK;AACvC;AAWA,eAAsB,iCACpB,WACA,OACe;AACf,QAAM,UAAU,gCAAgC,SAAS;AACzD,MAAI,CAAC,QAAS;AACd,MAAI;AACF,UAAM,QAAQ,qBAAqB,KAAK;AAAA,EAC1C,SAAS,OAAO;AACd,YAAQ,MAAM,mDAAmD;AAAA,MAC/D,cAAc,MAAM;AAAA,MACpB,YAAY,MAAM;AAAA,MAClB,WAAW,MAAM;AAAA,MACjB,eAAe,MAAM;AAAA,MACrB,OAAO,iBAAiB,QAAQ,MAAM,UAAU,OAAO,KAAK;AAAA,IAC9D,CAAC;AAAA,EACH;AACF;",
|
|
6
6
|
"names": []
|
|
7
7
|
}
|
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
import {
|
|
2
|
+
bridgeLegacyGuard,
|
|
3
|
+
runMutationGuards
|
|
4
|
+
} from "./mutation-guard-registry.js";
|
|
5
|
+
import { getAllMutationGuardInstances } from "./mutation-guard-store.js";
|
|
6
|
+
function toRegistryMutationOperation(operation) {
|
|
7
|
+
if (operation === "create" || operation === "delete") return operation;
|
|
8
|
+
return "update";
|
|
9
|
+
}
|
|
10
|
+
async function resolveRouteUserFeatures(container, auth) {
|
|
11
|
+
if (auth.userFeatures) return auth.userFeatures;
|
|
12
|
+
try {
|
|
13
|
+
const rbac = container.resolve("rbacService");
|
|
14
|
+
if (rbac?.getGrantedFeatures) {
|
|
15
|
+
return await rbac.getGrantedFeatures(auth.userId, {
|
|
16
|
+
tenantId: auth.tenantId,
|
|
17
|
+
organizationId: auth.organizationId ?? null
|
|
18
|
+
});
|
|
19
|
+
}
|
|
20
|
+
} catch {
|
|
21
|
+
}
|
|
22
|
+
return [];
|
|
23
|
+
}
|
|
24
|
+
async function runRouteMutationGuards(params) {
|
|
25
|
+
const { container, req, auth, input } = params;
|
|
26
|
+
const operation = toRegistryMutationOperation(input.operation);
|
|
27
|
+
const allGuards = [...getAllMutationGuardInstances()];
|
|
28
|
+
const legacyGuard = bridgeLegacyGuard(container);
|
|
29
|
+
if (legacyGuard) allGuards.push(legacyGuard);
|
|
30
|
+
const userFeatures = await resolveRouteUserFeatures(container, auth);
|
|
31
|
+
const guardResult = await runMutationGuards(
|
|
32
|
+
allGuards,
|
|
33
|
+
{
|
|
34
|
+
tenantId: auth.tenantId,
|
|
35
|
+
organizationId: auth.organizationId ?? null,
|
|
36
|
+
userId: auth.userId,
|
|
37
|
+
resourceKind: input.resourceKind,
|
|
38
|
+
resourceId: input.resourceId ?? null,
|
|
39
|
+
operation,
|
|
40
|
+
requestMethod: req.method,
|
|
41
|
+
requestHeaders: req.headers,
|
|
42
|
+
mutationPayload: input.mutationPayload ?? null
|
|
43
|
+
},
|
|
44
|
+
{ userFeatures }
|
|
45
|
+
);
|
|
46
|
+
if (!guardResult.ok) {
|
|
47
|
+
const errorStatus = guardResult.errorStatus ?? 422;
|
|
48
|
+
const errorBody = guardResult.errorBody ?? { error: "Operation blocked by guard" };
|
|
49
|
+
return {
|
|
50
|
+
ok: false,
|
|
51
|
+
errorStatus,
|
|
52
|
+
errorBody,
|
|
53
|
+
response: Response.json(errorBody, { status: errorStatus })
|
|
54
|
+
};
|
|
55
|
+
}
|
|
56
|
+
return {
|
|
57
|
+
ok: true,
|
|
58
|
+
modifiedPayload: guardResult.modifiedPayload,
|
|
59
|
+
runAfterSuccess: async () => {
|
|
60
|
+
for (const { guard, metadata } of guardResult.afterSuccessCallbacks) {
|
|
61
|
+
try {
|
|
62
|
+
await guard.afterSuccess({
|
|
63
|
+
tenantId: auth.tenantId,
|
|
64
|
+
organizationId: auth.organizationId ?? null,
|
|
65
|
+
userId: auth.userId,
|
|
66
|
+
resourceKind: input.resourceKind,
|
|
67
|
+
resourceId: input.resourceId ?? "",
|
|
68
|
+
operation,
|
|
69
|
+
requestMethod: req.method,
|
|
70
|
+
requestHeaders: req.headers,
|
|
71
|
+
metadata
|
|
72
|
+
});
|
|
73
|
+
} catch (error) {
|
|
74
|
+
console.error(`[mutation-guard] afterSuccess failed for guard ${guard.id}`, error);
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
};
|
|
79
|
+
}
|
|
80
|
+
export {
|
|
81
|
+
runRouteMutationGuards,
|
|
82
|
+
toRegistryMutationOperation
|
|
83
|
+
};
|
|
84
|
+
//# sourceMappingURL=route-mutation-guard.js.map
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
{
|
|
2
|
+
"version": 3,
|
|
3
|
+
"sources": ["../../../src/lib/crud/route-mutation-guard.ts"],
|
|
4
|
+
"sourcesContent": ["import type { AwilixContainer } from 'awilix'\nimport {\n bridgeLegacyGuard,\n runMutationGuards,\n type MutationGuard,\n} from './mutation-guard-registry'\nimport { getAllMutationGuardInstances } from './mutation-guard-store'\n\n/**\n * Shared registry-based mutation-guard wrapper for custom write routes that do\n * not use `makeCrudRoute`.\n *\n * This mirrors `collectAndRunGuards()` in `factory.ts`: it runs **every** guard\n * collected from the global mutation-guard store (`getAllMutationGuardInstances()`)\n * plus the bridged legacy DI service (`bridgeLegacyGuard()`), so a custom route\n * enforces the same guard set as every `makeCrudRoute` write.\n *\n * Prefer this over the deprecated `validateCrudMutationGuard()` /\n * `runCrudMutationGuardAfterSuccess()` pair, which resolve only the single\n * DI-registered `crudMutationGuardService` and silently skip registry guards.\n */\n\nexport type RouteMutationGuardOperation = 'create' | 'update' | 'delete' | 'custom'\n\nexport type RouteMutationGuardAuth = {\n userId: string\n tenantId: string\n organizationId?: string | null\n /**\n * Pre-resolved granted features for the caller. When omitted, the wrapper\n * resolves them via `rbacService.getGrantedFeatures` (same source the CRUD\n * factory uses). Feature-gated registry guards only run when their required\n * features are present, so supplying the wrong set silently skips guards.\n */\n userFeatures?: string[]\n}\n\nexport type RouteMutationGuardInput = {\n resourceKind: string\n resourceId?: string | null\n /**\n * The route's logical operation. `'custom'` (state-changing action endpoints)\n * is mapped to the closest registry operation, `'update'`, because\n * `runMutationGuards` only understands `create | update | delete`.\n */\n operation?: RouteMutationGuardOperation\n mutationPayload?: Record<string, unknown> | null\n}\n\nexport type RouteMutationGuardBlocked = {\n ok: false\n errorStatus: number\n errorBody: Record<string, unknown>\n /** Ready-to-return JSON response built from `errorBody` / `errorStatus`. */\n response: Response\n}\n\nexport type RouteMutationGuardPassed = {\n ok: true\n /** Merged payload when a guard transformed it; `undefined` when unchanged. */\n modifiedPayload?: Record<string, unknown>\n /**\n * Runs every guard's `afterSuccess` callback that requested it. Callback\n * failures are caught and logged so a committed write still succeeds \u2014 call\n * this only after the mutation has committed.\n */\n runAfterSuccess: () => Promise<void>\n}\n\nexport type RouteMutationGuardResult = RouteMutationGuardBlocked | RouteMutationGuardPassed\n\ntype RbacServiceLike = {\n getGrantedFeatures: (\n userId: string,\n opts: { tenantId: string | null; organizationId: string | null },\n ) => Promise<string[]>\n}\n\n/**\n * Map a route-level operation to the registry operation set. State-changing\n * action endpoints (`'custom'`) and `'update'` both map to `'update'` per the\n * `packages/core/AGENTS.md` \u2192 API Routes guidance.\n */\nexport function toRegistryMutationOperation(\n operation: RouteMutationGuardOperation | undefined,\n): 'create' | 'update' | 'delete' {\n if (operation === 'create' || operation === 'delete') return operation\n return 'update'\n}\n\nasync function resolveRouteUserFeatures(\n container: AwilixContainer,\n auth: RouteMutationGuardAuth,\n): Promise<string[]> {\n if (auth.userFeatures) return auth.userFeatures\n try {\n const rbac = container.resolve('rbacService') as RbacServiceLike | undefined\n if (rbac?.getGrantedFeatures) {\n return await rbac.getGrantedFeatures(auth.userId, {\n tenantId: auth.tenantId,\n organizationId: auth.organizationId ?? null,\n })\n }\n } catch {\n // rbacService not available \u2014 guards without feature requirements still run.\n }\n return []\n}\n\n/**\n * Collect every registered mutation guard plus the bridged legacy DI service,\n * run them against the route's mutation, and return either a blocked result\n * (with a ready response) or a passed result carrying the merged payload and an\n * after-success runner.\n */\nexport async function runRouteMutationGuards(params: {\n container: AwilixContainer\n req: Request\n auth: RouteMutationGuardAuth\n input: RouteMutationGuardInput\n}): Promise<RouteMutationGuardResult> {\n const { container, req, auth, input } = params\n const operation = toRegistryMutationOperation(input.operation)\n\n const allGuards: MutationGuard[] = [...getAllMutationGuardInstances()]\n const legacyGuard = bridgeLegacyGuard(container)\n if (legacyGuard) allGuards.push(legacyGuard)\n\n const userFeatures = await resolveRouteUserFeatures(container, auth)\n\n const guardResult = await runMutationGuards(\n allGuards,\n {\n tenantId: auth.tenantId,\n organizationId: auth.organizationId ?? null,\n userId: auth.userId,\n resourceKind: input.resourceKind,\n resourceId: input.resourceId ?? null,\n operation,\n requestMethod: req.method,\n requestHeaders: req.headers,\n mutationPayload: input.mutationPayload ?? null,\n },\n { userFeatures },\n )\n\n if (!guardResult.ok) {\n const errorStatus = guardResult.errorStatus ?? 422\n const errorBody = guardResult.errorBody ?? { error: 'Operation blocked by guard' }\n return {\n ok: false,\n errorStatus,\n errorBody,\n response: Response.json(errorBody, { status: errorStatus }),\n }\n }\n\n return {\n ok: true,\n modifiedPayload: guardResult.modifiedPayload,\n runAfterSuccess: async () => {\n for (const { guard, metadata } of guardResult.afterSuccessCallbacks) {\n try {\n await guard.afterSuccess!({\n tenantId: auth.tenantId,\n organizationId: auth.organizationId ?? null,\n userId: auth.userId,\n resourceKind: input.resourceKind,\n resourceId: input.resourceId ?? '',\n operation,\n requestMethod: req.method,\n requestHeaders: req.headers,\n metadata,\n })\n } catch (error) {\n console.error(`[mutation-guard] afterSuccess failed for guard ${guard.id}`, error)\n }\n }\n },\n }\n}\n"],
|
|
5
|
+
"mappings": "AACA;AAAA,EACE;AAAA,EACA;AAAA,OAEK;AACP,SAAS,oCAAoC;AA6EtC,SAAS,4BACd,WACgC;AAChC,MAAI,cAAc,YAAY,cAAc,SAAU,QAAO;AAC7D,SAAO;AACT;AAEA,eAAe,yBACb,WACA,MACmB;AACnB,MAAI,KAAK,aAAc,QAAO,KAAK;AACnC,MAAI;AACF,UAAM,OAAO,UAAU,QAAQ,aAAa;AAC5C,QAAI,MAAM,oBAAoB;AAC5B,aAAO,MAAM,KAAK,mBAAmB,KAAK,QAAQ;AAAA,QAChD,UAAU,KAAK;AAAA,QACf,gBAAgB,KAAK,kBAAkB;AAAA,MACzC,CAAC;AAAA,IACH;AAAA,EACF,QAAQ;AAAA,EAER;AACA,SAAO,CAAC;AACV;AAQA,eAAsB,uBAAuB,QAKP;AACpC,QAAM,EAAE,WAAW,KAAK,MAAM,MAAM,IAAI;AACxC,QAAM,YAAY,4BAA4B,MAAM,SAAS;AAE7D,QAAM,YAA6B,CAAC,GAAG,6BAA6B,CAAC;AACrE,QAAM,cAAc,kBAAkB,SAAS;AAC/C,MAAI,YAAa,WAAU,KAAK,WAAW;AAE3C,QAAM,eAAe,MAAM,yBAAyB,WAAW,IAAI;AAEnE,QAAM,cAAc,MAAM;AAAA,IACxB;AAAA,IACA;AAAA,MACE,UAAU,KAAK;AAAA,MACf,gBAAgB,KAAK,kBAAkB;AAAA,MACvC,QAAQ,KAAK;AAAA,MACb,cAAc,MAAM;AAAA,MACpB,YAAY,MAAM,cAAc;AAAA,MAChC;AAAA,MACA,eAAe,IAAI;AAAA,MACnB,gBAAgB,IAAI;AAAA,MACpB,iBAAiB,MAAM,mBAAmB;AAAA,IAC5C;AAAA,IACA,EAAE,aAAa;AAAA,EACjB;AAEA,MAAI,CAAC,YAAY,IAAI;AACnB,UAAM,cAAc,YAAY,eAAe;AAC/C,UAAM,YAAY,YAAY,aAAa,EAAE,OAAO,6BAA6B;AACjF,WAAO;AAAA,MACL,IAAI;AAAA,MACJ;AAAA,MACA;AAAA,MACA,UAAU,SAAS,KAAK,WAAW,EAAE,QAAQ,YAAY,CAAC;AAAA,IAC5D;AAAA,EACF;AAEA,SAAO;AAAA,IACL,IAAI;AAAA,IACJ,iBAAiB,YAAY;AAAA,IAC7B,iBAAiB,YAAY;AAC3B,iBAAW,EAAE,OAAO,SAAS,KAAK,YAAY,uBAAuB;AACnE,YAAI;AACF,gBAAM,MAAM,aAAc;AAAA,YACxB,UAAU,KAAK;AAAA,YACf,gBAAgB,KAAK,kBAAkB;AAAA,YACvC,QAAQ,KAAK;AAAA,YACb,cAAc,MAAM;AAAA,YACpB,YAAY,MAAM,cAAc;AAAA,YAChC;AAAA,YACA,eAAe,IAAI;AAAA,YACnB,gBAAgB,IAAI;AAAA,YACpB;AAAA,UACF,CAAC;AAAA,QACH,SAAS,OAAO;AACd,kBAAQ,MAAM,kDAAkD,MAAM,EAAE,IAAI,KAAK;AAAA,QACnF;AAAA,MACF;AAAA,IACF;AAAA,EACF;AACF;",
|
|
6
|
+
"names": []
|
|
7
|
+
}
|
package/dist/lib/version.js
CHANGED
package/dist/lib/version.js.map
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"version": 3,
|
|
3
3
|
"sources": ["../../src/lib/version.ts"],
|
|
4
|
-
"sourcesContent": ["// Build-time generated version\nexport const APP_VERSION = '0.6.6-develop.
|
|
4
|
+
"sourcesContent": ["// Build-time generated version\nexport const APP_VERSION = '0.6.6-develop.6343.1.8120f84f30'\nexport const appVersion = APP_VERSION\n"],
|
|
5
5
|
"mappings": "AACO,MAAM,cAAc;AACpB,MAAM,aAAa;",
|
|
6
6
|
"names": []
|
|
7
7
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@open-mercato/shared",
|
|
3
|
-
"version": "0.6.6-develop.
|
|
3
|
+
"version": "0.6.6-develop.6343.1.8120f84f30",
|
|
4
4
|
"license": "MIT",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "./dist/index.js",
|
|
@@ -93,7 +93,7 @@
|
|
|
93
93
|
"@mikro-orm/core": "^7.1.4",
|
|
94
94
|
"@mikro-orm/decorators": "^7.1.4",
|
|
95
95
|
"@mikro-orm/postgresql": "^7.1.4",
|
|
96
|
-
"@open-mercato/cache": "0.6.6-develop.
|
|
96
|
+
"@open-mercato/cache": "0.6.6-develop.6343.1.8120f84f30",
|
|
97
97
|
"dotenv": "^17.4.2",
|
|
98
98
|
"rate-limiter-flexible": "^11.2.0",
|
|
99
99
|
"re2js": "2.8.3",
|
|
@@ -76,7 +76,7 @@ function walkFiles(dirPath: string): string[] {
|
|
|
76
76
|
function listGeneratedCacheFiles(appRoot: string): string[] {
|
|
77
77
|
return walkFiles(getGeneratedDir(appRoot))
|
|
78
78
|
.filter((filePath) => filePath.endsWith('.mjs'))
|
|
79
|
-
.sort()
|
|
79
|
+
.sort((a, b) => (a < b ? -1 : a > b ? 1 : 0))
|
|
80
80
|
}
|
|
81
81
|
|
|
82
82
|
function findStaleGeneratedCacheFiles(appRoot: string): string[] {
|
|
@@ -0,0 +1,260 @@
|
|
|
1
|
+
import type { AwilixContainer } from 'awilix'
|
|
2
|
+
import { registerMutationGuards } from '../mutation-guard-store'
|
|
3
|
+
import type { MutationGuard } from '../mutation-guard-registry'
|
|
4
|
+
import { runRouteMutationGuards, toRegistryMutationOperation } from '../route-mutation-guard'
|
|
5
|
+
|
|
6
|
+
type Registrations = Record<string, unknown>
|
|
7
|
+
|
|
8
|
+
function makeContainer(registrations: Registrations = {}): AwilixContainer {
|
|
9
|
+
return {
|
|
10
|
+
resolve: (name: string) => {
|
|
11
|
+
if (Object.prototype.hasOwnProperty.call(registrations, name)) return registrations[name]
|
|
12
|
+
throw new Error(`[test] no registration for ${name}`)
|
|
13
|
+
},
|
|
14
|
+
} as unknown as AwilixContainer
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
function makeRequest(method = 'POST'): Request {
|
|
18
|
+
return new Request('https://example.test/api/things', { method })
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
function makeGuard(overrides: Partial<MutationGuard> & { id: string }): MutationGuard {
|
|
22
|
+
return {
|
|
23
|
+
targetEntity: 'things.thing',
|
|
24
|
+
operations: ['create', 'update', 'delete'],
|
|
25
|
+
priority: 50,
|
|
26
|
+
validate: jest.fn().mockResolvedValue({ ok: true }),
|
|
27
|
+
...overrides,
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
function registerStoreGuards(guards: MutationGuard[]) {
|
|
32
|
+
registerMutationGuards([{ moduleId: 'things', guards }])
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
const baseAuth = {
|
|
36
|
+
userId: 'user-1',
|
|
37
|
+
tenantId: 'tenant-1',
|
|
38
|
+
organizationId: 'org-1',
|
|
39
|
+
userFeatures: [] as string[],
|
|
40
|
+
}
|
|
41
|
+
const baseInput = {
|
|
42
|
+
resourceKind: 'things.thing',
|
|
43
|
+
resourceId: 'thing-1',
|
|
44
|
+
operation: 'update' as const,
|
|
45
|
+
mutationPayload: { title: 'Updated' },
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
afterEach(() => {
|
|
49
|
+
registerMutationGuards([])
|
|
50
|
+
jest.restoreAllMocks()
|
|
51
|
+
})
|
|
52
|
+
|
|
53
|
+
describe('toRegistryMutationOperation', () => {
|
|
54
|
+
it('passes create and delete through and maps update/custom/undefined to update', () => {
|
|
55
|
+
expect(toRegistryMutationOperation('create')).toBe('create')
|
|
56
|
+
expect(toRegistryMutationOperation('delete')).toBe('delete')
|
|
57
|
+
expect(toRegistryMutationOperation('update')).toBe('update')
|
|
58
|
+
expect(toRegistryMutationOperation('custom')).toBe('update')
|
|
59
|
+
expect(toRegistryMutationOperation(undefined)).toBe('update')
|
|
60
|
+
})
|
|
61
|
+
})
|
|
62
|
+
|
|
63
|
+
describe('runRouteMutationGuards', () => {
|
|
64
|
+
it('runs a registry-store guard the legacy bridge path would have skipped', async () => {
|
|
65
|
+
const guard = makeGuard({ id: 'store-guard' })
|
|
66
|
+
registerStoreGuards([guard])
|
|
67
|
+
|
|
68
|
+
const result = await runRouteMutationGuards({
|
|
69
|
+
container: makeContainer(),
|
|
70
|
+
req: makeRequest(),
|
|
71
|
+
auth: baseAuth,
|
|
72
|
+
input: baseInput,
|
|
73
|
+
})
|
|
74
|
+
|
|
75
|
+
expect(result.ok).toBe(true)
|
|
76
|
+
expect(guard.validate).toHaveBeenCalledTimes(1)
|
|
77
|
+
})
|
|
78
|
+
|
|
79
|
+
it('blocks and returns a ready response when a store guard rejects', async () => {
|
|
80
|
+
const guard = makeGuard({
|
|
81
|
+
id: 'blocking-guard',
|
|
82
|
+
validate: jest.fn().mockResolvedValue({ ok: false, status: 422, body: { error: 'nope' } }),
|
|
83
|
+
})
|
|
84
|
+
registerStoreGuards([guard])
|
|
85
|
+
|
|
86
|
+
const result = await runRouteMutationGuards({
|
|
87
|
+
container: makeContainer(),
|
|
88
|
+
req: makeRequest(),
|
|
89
|
+
auth: baseAuth,
|
|
90
|
+
input: baseInput,
|
|
91
|
+
})
|
|
92
|
+
|
|
93
|
+
expect(result.ok).toBe(false)
|
|
94
|
+
if (result.ok) throw new Error('expected blocked result')
|
|
95
|
+
expect(result.errorStatus).toBe(422)
|
|
96
|
+
expect(result.errorBody).toEqual({ error: 'nope' })
|
|
97
|
+
expect(result.response.status).toBe(422)
|
|
98
|
+
await expect(result.response.json()).resolves.toEqual({ error: 'nope' })
|
|
99
|
+
})
|
|
100
|
+
|
|
101
|
+
it('threads modifiedPayload from a store guard', async () => {
|
|
102
|
+
const guard = makeGuard({
|
|
103
|
+
id: 'transform-guard',
|
|
104
|
+
validate: jest.fn().mockResolvedValue({ ok: true, modifiedPayload: { extra: true } }),
|
|
105
|
+
})
|
|
106
|
+
registerStoreGuards([guard])
|
|
107
|
+
|
|
108
|
+
const result = await runRouteMutationGuards({
|
|
109
|
+
container: makeContainer(),
|
|
110
|
+
req: makeRequest(),
|
|
111
|
+
auth: baseAuth,
|
|
112
|
+
input: baseInput,
|
|
113
|
+
})
|
|
114
|
+
|
|
115
|
+
expect(result.ok).toBe(true)
|
|
116
|
+
if (!result.ok) throw new Error('expected passed result')
|
|
117
|
+
expect(result.modifiedPayload).toEqual({ title: 'Updated', extra: true })
|
|
118
|
+
})
|
|
119
|
+
|
|
120
|
+
it('runs afterSuccess callbacks via runAfterSuccess()', async () => {
|
|
121
|
+
const afterSuccess = jest.fn().mockResolvedValue(undefined)
|
|
122
|
+
const guard = makeGuard({
|
|
123
|
+
id: 'after-guard',
|
|
124
|
+
validate: jest.fn().mockResolvedValue({ ok: true, shouldRunAfterSuccess: true, metadata: { ping: 1 } }),
|
|
125
|
+
afterSuccess,
|
|
126
|
+
})
|
|
127
|
+
registerStoreGuards([guard])
|
|
128
|
+
|
|
129
|
+
const result = await runRouteMutationGuards({
|
|
130
|
+
container: makeContainer(),
|
|
131
|
+
req: makeRequest(),
|
|
132
|
+
auth: baseAuth,
|
|
133
|
+
input: baseInput,
|
|
134
|
+
})
|
|
135
|
+
|
|
136
|
+
expect(result.ok).toBe(true)
|
|
137
|
+
if (!result.ok) throw new Error('expected passed result')
|
|
138
|
+
expect(afterSuccess).not.toHaveBeenCalled()
|
|
139
|
+
|
|
140
|
+
await result.runAfterSuccess()
|
|
141
|
+
|
|
142
|
+
expect(afterSuccess).toHaveBeenCalledTimes(1)
|
|
143
|
+
expect(afterSuccess).toHaveBeenCalledWith(
|
|
144
|
+
expect.objectContaining({ resourceKind: 'things.thing', resourceId: 'thing-1', metadata: { ping: 1 } }),
|
|
145
|
+
)
|
|
146
|
+
})
|
|
147
|
+
|
|
148
|
+
it('swallows afterSuccess callback errors so a committed write still succeeds', async () => {
|
|
149
|
+
const consoleError = jest.spyOn(console, 'error').mockImplementation(() => {})
|
|
150
|
+
const guard = makeGuard({
|
|
151
|
+
id: 'throwing-after-guard',
|
|
152
|
+
validate: jest.fn().mockResolvedValue({ ok: true, shouldRunAfterSuccess: true }),
|
|
153
|
+
afterSuccess: jest.fn().mockRejectedValue(new Error('after boom')),
|
|
154
|
+
})
|
|
155
|
+
registerStoreGuards([guard])
|
|
156
|
+
|
|
157
|
+
const result = await runRouteMutationGuards({
|
|
158
|
+
container: makeContainer(),
|
|
159
|
+
req: makeRequest(),
|
|
160
|
+
auth: baseAuth,
|
|
161
|
+
input: baseInput,
|
|
162
|
+
})
|
|
163
|
+
|
|
164
|
+
expect(result.ok).toBe(true)
|
|
165
|
+
if (!result.ok) throw new Error('expected passed result')
|
|
166
|
+
await expect(result.runAfterSuccess()).resolves.toBeUndefined()
|
|
167
|
+
expect(consoleError).toHaveBeenCalled()
|
|
168
|
+
})
|
|
169
|
+
|
|
170
|
+
it('skips feature-gated guards when the caller lacks the feature', async () => {
|
|
171
|
+
const guard = makeGuard({
|
|
172
|
+
id: 'gated-guard',
|
|
173
|
+
features: ['premium.locks'],
|
|
174
|
+
validate: jest.fn().mockResolvedValue({ ok: false, message: 'should not run' }),
|
|
175
|
+
})
|
|
176
|
+
registerStoreGuards([guard])
|
|
177
|
+
|
|
178
|
+
const passes = await runRouteMutationGuards({
|
|
179
|
+
container: makeContainer(),
|
|
180
|
+
req: makeRequest(),
|
|
181
|
+
auth: { ...baseAuth, userFeatures: [] },
|
|
182
|
+
input: baseInput,
|
|
183
|
+
})
|
|
184
|
+
expect(passes.ok).toBe(true)
|
|
185
|
+
expect(guard.validate).not.toHaveBeenCalled()
|
|
186
|
+
|
|
187
|
+
const blocks = await runRouteMutationGuards({
|
|
188
|
+
container: makeContainer(),
|
|
189
|
+
req: makeRequest(),
|
|
190
|
+
auth: { ...baseAuth, userFeatures: ['premium.locks'] },
|
|
191
|
+
input: baseInput,
|
|
192
|
+
})
|
|
193
|
+
expect(blocks.ok).toBe(false)
|
|
194
|
+
})
|
|
195
|
+
|
|
196
|
+
it('resolves userFeatures from rbacService when not pre-supplied', async () => {
|
|
197
|
+
const getGrantedFeatures = jest.fn().mockResolvedValue(['premium.locks'])
|
|
198
|
+
const guard = makeGuard({
|
|
199
|
+
id: 'gated-guard',
|
|
200
|
+
features: ['premium.locks'],
|
|
201
|
+
validate: jest.fn().mockResolvedValue({ ok: false, message: 'blocked via rbac-resolved features' }),
|
|
202
|
+
})
|
|
203
|
+
registerStoreGuards([guard])
|
|
204
|
+
|
|
205
|
+
const result = await runRouteMutationGuards({
|
|
206
|
+
container: makeContainer({ rbacService: { getGrantedFeatures } }),
|
|
207
|
+
req: makeRequest(),
|
|
208
|
+
auth: { userId: 'user-1', tenantId: 'tenant-1', organizationId: 'org-1' },
|
|
209
|
+
input: baseInput,
|
|
210
|
+
})
|
|
211
|
+
|
|
212
|
+
expect(getGrantedFeatures).toHaveBeenCalledWith('user-1', { tenantId: 'tenant-1', organizationId: 'org-1' })
|
|
213
|
+
expect(result.ok).toBe(false)
|
|
214
|
+
})
|
|
215
|
+
|
|
216
|
+
it('maps a custom operation to update so update-scoped guards run', async () => {
|
|
217
|
+
const updateGuard = makeGuard({
|
|
218
|
+
id: 'update-only-guard',
|
|
219
|
+
operations: ['update'],
|
|
220
|
+
validate: jest.fn().mockResolvedValue({ ok: true }),
|
|
221
|
+
})
|
|
222
|
+
const createGuard = makeGuard({
|
|
223
|
+
id: 'create-only-guard',
|
|
224
|
+
operations: ['create'],
|
|
225
|
+
validate: jest.fn().mockResolvedValue({ ok: true }),
|
|
226
|
+
})
|
|
227
|
+
registerStoreGuards([updateGuard, createGuard])
|
|
228
|
+
|
|
229
|
+
const result = await runRouteMutationGuards({
|
|
230
|
+
container: makeContainer(),
|
|
231
|
+
req: makeRequest(),
|
|
232
|
+
auth: baseAuth,
|
|
233
|
+
input: { ...baseInput, operation: 'custom' },
|
|
234
|
+
})
|
|
235
|
+
|
|
236
|
+
expect(result.ok).toBe(true)
|
|
237
|
+
expect(updateGuard.validate).toHaveBeenCalledTimes(1)
|
|
238
|
+
expect(createGuard.validate).not.toHaveBeenCalled()
|
|
239
|
+
})
|
|
240
|
+
|
|
241
|
+
it('includes the bridged legacy DI guard', async () => {
|
|
242
|
+
const legacyService = {
|
|
243
|
+
validateMutation: jest.fn().mockResolvedValue({ ok: false, status: 409, body: { error: 'locked' } }),
|
|
244
|
+
afterMutationSuccess: jest.fn().mockResolvedValue(undefined),
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
const result = await runRouteMutationGuards({
|
|
248
|
+
container: makeContainer({ crudMutationGuardService: legacyService }),
|
|
249
|
+
req: makeRequest('PUT'),
|
|
250
|
+
auth: baseAuth,
|
|
251
|
+
input: baseInput,
|
|
252
|
+
})
|
|
253
|
+
|
|
254
|
+
expect(legacyService.validateMutation).toHaveBeenCalledTimes(1)
|
|
255
|
+
expect(result.ok).toBe(false)
|
|
256
|
+
if (result.ok) throw new Error('expected blocked result')
|
|
257
|
+
expect(result.errorStatus).toBe(409)
|
|
258
|
+
expect(result.errorBody).toEqual({ error: 'locked' })
|
|
259
|
+
})
|
|
260
|
+
})
|
|
@@ -269,8 +269,8 @@ function buildCfDefIndexCacheKey(opts: {
|
|
|
269
269
|
fieldsetKey: string | null
|
|
270
270
|
}): string {
|
|
271
271
|
const tenant = opts.tenantId ?? 'global'
|
|
272
|
-
const entities = opts.entityIds.slice().sort().join('|')
|
|
273
|
-
const orgs = opts.organizationIds.length ? opts.organizationIds.slice().sort().join('|') : 'none'
|
|
272
|
+
const entities = opts.entityIds.slice().sort((a, b) => (a < b ? -1 : a > b ? 1 : 0)).join('|')
|
|
273
|
+
const orgs = opts.organizationIds.length ? opts.organizationIds.slice().sort((a, b) => (a < b ? -1 : a > b ? 1 : 0)).join('|') : 'none'
|
|
274
274
|
const fieldset = opts.fieldsetKey ?? 'all'
|
|
275
275
|
return `${CF_DEF_INDEX_CACHE_KEY_PREFIX}:${tenant}:${entities}:${orgs}:${fieldset}`
|
|
276
276
|
}
|
|
@@ -295,7 +295,7 @@ function normalizeFieldsetKey(value: string | string[] | null | undefined): stri
|
|
|
295
295
|
.map((entry) => (typeof entry === 'string' ? entry.trim() : ''))
|
|
296
296
|
.filter((entry) => entry.length > 0)
|
|
297
297
|
if (!cleaned.length) return null
|
|
298
|
-
return cleaned.sort().join(',')
|
|
298
|
+
return cleaned.sort((a, b) => (a < b ? -1 : a > b ? 1 : 0)).join(',')
|
|
299
299
|
}
|
|
300
300
|
if (typeof value !== 'string') return null
|
|
301
301
|
const trimmed = value.trim()
|
|
@@ -58,8 +58,14 @@ function resolveCrudMutationGuardService(container: AwilixContainer): CrudMutati
|
|
|
58
58
|
}
|
|
59
59
|
|
|
60
60
|
/**
|
|
61
|
-
* @deprecated
|
|
62
|
-
*
|
|
61
|
+
* @deprecated Resolves ONLY the single DI-registered `crudMutationGuardService`,
|
|
62
|
+
* so it silently bypasses every guard in the global mutation-guard store
|
|
63
|
+
* (`getAllMutationGuardInstances()`). Use the full registry instead:
|
|
64
|
+
* `runRouteMutationGuards()` from `@open-mercato/shared/lib/crud/route-mutation-guard`
|
|
65
|
+
* for custom write routes, or `runMutationGuards()` from
|
|
66
|
+
* `@open-mercato/shared/lib/crud/mutation-guard-registry` directly. The legacy
|
|
67
|
+
* service is still honored on the modern path via `bridgeLegacyGuard()`. This
|
|
68
|
+
* function will be removed in a future release.
|
|
63
69
|
*/
|
|
64
70
|
export async function validateCrudMutationGuard(
|
|
65
71
|
container: AwilixContainer,
|
|
@@ -71,8 +77,13 @@ export async function validateCrudMutationGuard(
|
|
|
71
77
|
}
|
|
72
78
|
|
|
73
79
|
/**
|
|
74
|
-
* @deprecated
|
|
75
|
-
*
|
|
80
|
+
* @deprecated Runs ONLY the single DI-registered `crudMutationGuardService`'s
|
|
81
|
+
* after-success hook, skipping the registry guards' `afterSuccess` callbacks.
|
|
82
|
+
* Use the `runAfterSuccess()` returned by `runRouteMutationGuards()` from
|
|
83
|
+
* `@open-mercato/shared/lib/crud/route-mutation-guard`, or the
|
|
84
|
+
* `afterSuccessCallbacks` returned by `runMutationGuards()` from
|
|
85
|
+
* `@open-mercato/shared/lib/crud/mutation-guard-registry`. This function will be
|
|
86
|
+
* removed in a future release.
|
|
76
87
|
*/
|
|
77
88
|
export async function runCrudMutationGuardAfterSuccess(
|
|
78
89
|
container: AwilixContainer,
|
|
@@ -0,0 +1,181 @@
|
|
|
1
|
+
import type { AwilixContainer } from 'awilix'
|
|
2
|
+
import {
|
|
3
|
+
bridgeLegacyGuard,
|
|
4
|
+
runMutationGuards,
|
|
5
|
+
type MutationGuard,
|
|
6
|
+
} from './mutation-guard-registry'
|
|
7
|
+
import { getAllMutationGuardInstances } from './mutation-guard-store'
|
|
8
|
+
|
|
9
|
+
/**
|
|
10
|
+
* Shared registry-based mutation-guard wrapper for custom write routes that do
|
|
11
|
+
* not use `makeCrudRoute`.
|
|
12
|
+
*
|
|
13
|
+
* This mirrors `collectAndRunGuards()` in `factory.ts`: it runs **every** guard
|
|
14
|
+
* collected from the global mutation-guard store (`getAllMutationGuardInstances()`)
|
|
15
|
+
* plus the bridged legacy DI service (`bridgeLegacyGuard()`), so a custom route
|
|
16
|
+
* enforces the same guard set as every `makeCrudRoute` write.
|
|
17
|
+
*
|
|
18
|
+
* Prefer this over the deprecated `validateCrudMutationGuard()` /
|
|
19
|
+
* `runCrudMutationGuardAfterSuccess()` pair, which resolve only the single
|
|
20
|
+
* DI-registered `crudMutationGuardService` and silently skip registry guards.
|
|
21
|
+
*/
|
|
22
|
+
|
|
23
|
+
export type RouteMutationGuardOperation = 'create' | 'update' | 'delete' | 'custom'
|
|
24
|
+
|
|
25
|
+
export type RouteMutationGuardAuth = {
|
|
26
|
+
userId: string
|
|
27
|
+
tenantId: string
|
|
28
|
+
organizationId?: string | null
|
|
29
|
+
/**
|
|
30
|
+
* Pre-resolved granted features for the caller. When omitted, the wrapper
|
|
31
|
+
* resolves them via `rbacService.getGrantedFeatures` (same source the CRUD
|
|
32
|
+
* factory uses). Feature-gated registry guards only run when their required
|
|
33
|
+
* features are present, so supplying the wrong set silently skips guards.
|
|
34
|
+
*/
|
|
35
|
+
userFeatures?: string[]
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
export type RouteMutationGuardInput = {
|
|
39
|
+
resourceKind: string
|
|
40
|
+
resourceId?: string | null
|
|
41
|
+
/**
|
|
42
|
+
* The route's logical operation. `'custom'` (state-changing action endpoints)
|
|
43
|
+
* is mapped to the closest registry operation, `'update'`, because
|
|
44
|
+
* `runMutationGuards` only understands `create | update | delete`.
|
|
45
|
+
*/
|
|
46
|
+
operation?: RouteMutationGuardOperation
|
|
47
|
+
mutationPayload?: Record<string, unknown> | null
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
export type RouteMutationGuardBlocked = {
|
|
51
|
+
ok: false
|
|
52
|
+
errorStatus: number
|
|
53
|
+
errorBody: Record<string, unknown>
|
|
54
|
+
/** Ready-to-return JSON response built from `errorBody` / `errorStatus`. */
|
|
55
|
+
response: Response
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
export type RouteMutationGuardPassed = {
|
|
59
|
+
ok: true
|
|
60
|
+
/** Merged payload when a guard transformed it; `undefined` when unchanged. */
|
|
61
|
+
modifiedPayload?: Record<string, unknown>
|
|
62
|
+
/**
|
|
63
|
+
* Runs every guard's `afterSuccess` callback that requested it. Callback
|
|
64
|
+
* failures are caught and logged so a committed write still succeeds — call
|
|
65
|
+
* this only after the mutation has committed.
|
|
66
|
+
*/
|
|
67
|
+
runAfterSuccess: () => Promise<void>
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
export type RouteMutationGuardResult = RouteMutationGuardBlocked | RouteMutationGuardPassed
|
|
71
|
+
|
|
72
|
+
type RbacServiceLike = {
|
|
73
|
+
getGrantedFeatures: (
|
|
74
|
+
userId: string,
|
|
75
|
+
opts: { tenantId: string | null; organizationId: string | null },
|
|
76
|
+
) => Promise<string[]>
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
/**
|
|
80
|
+
* Map a route-level operation to the registry operation set. State-changing
|
|
81
|
+
* action endpoints (`'custom'`) and `'update'` both map to `'update'` per the
|
|
82
|
+
* `packages/core/AGENTS.md` → API Routes guidance.
|
|
83
|
+
*/
|
|
84
|
+
export function toRegistryMutationOperation(
|
|
85
|
+
operation: RouteMutationGuardOperation | undefined,
|
|
86
|
+
): 'create' | 'update' | 'delete' {
|
|
87
|
+
if (operation === 'create' || operation === 'delete') return operation
|
|
88
|
+
return 'update'
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
async function resolveRouteUserFeatures(
|
|
92
|
+
container: AwilixContainer,
|
|
93
|
+
auth: RouteMutationGuardAuth,
|
|
94
|
+
): Promise<string[]> {
|
|
95
|
+
if (auth.userFeatures) return auth.userFeatures
|
|
96
|
+
try {
|
|
97
|
+
const rbac = container.resolve('rbacService') as RbacServiceLike | undefined
|
|
98
|
+
if (rbac?.getGrantedFeatures) {
|
|
99
|
+
return await rbac.getGrantedFeatures(auth.userId, {
|
|
100
|
+
tenantId: auth.tenantId,
|
|
101
|
+
organizationId: auth.organizationId ?? null,
|
|
102
|
+
})
|
|
103
|
+
}
|
|
104
|
+
} catch {
|
|
105
|
+
// rbacService not available — guards without feature requirements still run.
|
|
106
|
+
}
|
|
107
|
+
return []
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
/**
|
|
111
|
+
* Collect every registered mutation guard plus the bridged legacy DI service,
|
|
112
|
+
* run them against the route's mutation, and return either a blocked result
|
|
113
|
+
* (with a ready response) or a passed result carrying the merged payload and an
|
|
114
|
+
* after-success runner.
|
|
115
|
+
*/
|
|
116
|
+
export async function runRouteMutationGuards(params: {
|
|
117
|
+
container: AwilixContainer
|
|
118
|
+
req: Request
|
|
119
|
+
auth: RouteMutationGuardAuth
|
|
120
|
+
input: RouteMutationGuardInput
|
|
121
|
+
}): Promise<RouteMutationGuardResult> {
|
|
122
|
+
const { container, req, auth, input } = params
|
|
123
|
+
const operation = toRegistryMutationOperation(input.operation)
|
|
124
|
+
|
|
125
|
+
const allGuards: MutationGuard[] = [...getAllMutationGuardInstances()]
|
|
126
|
+
const legacyGuard = bridgeLegacyGuard(container)
|
|
127
|
+
if (legacyGuard) allGuards.push(legacyGuard)
|
|
128
|
+
|
|
129
|
+
const userFeatures = await resolveRouteUserFeatures(container, auth)
|
|
130
|
+
|
|
131
|
+
const guardResult = await runMutationGuards(
|
|
132
|
+
allGuards,
|
|
133
|
+
{
|
|
134
|
+
tenantId: auth.tenantId,
|
|
135
|
+
organizationId: auth.organizationId ?? null,
|
|
136
|
+
userId: auth.userId,
|
|
137
|
+
resourceKind: input.resourceKind,
|
|
138
|
+
resourceId: input.resourceId ?? null,
|
|
139
|
+
operation,
|
|
140
|
+
requestMethod: req.method,
|
|
141
|
+
requestHeaders: req.headers,
|
|
142
|
+
mutationPayload: input.mutationPayload ?? null,
|
|
143
|
+
},
|
|
144
|
+
{ userFeatures },
|
|
145
|
+
)
|
|
146
|
+
|
|
147
|
+
if (!guardResult.ok) {
|
|
148
|
+
const errorStatus = guardResult.errorStatus ?? 422
|
|
149
|
+
const errorBody = guardResult.errorBody ?? { error: 'Operation blocked by guard' }
|
|
150
|
+
return {
|
|
151
|
+
ok: false,
|
|
152
|
+
errorStatus,
|
|
153
|
+
errorBody,
|
|
154
|
+
response: Response.json(errorBody, { status: errorStatus }),
|
|
155
|
+
}
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
return {
|
|
159
|
+
ok: true,
|
|
160
|
+
modifiedPayload: guardResult.modifiedPayload,
|
|
161
|
+
runAfterSuccess: async () => {
|
|
162
|
+
for (const { guard, metadata } of guardResult.afterSuccessCallbacks) {
|
|
163
|
+
try {
|
|
164
|
+
await guard.afterSuccess!({
|
|
165
|
+
tenantId: auth.tenantId,
|
|
166
|
+
organizationId: auth.organizationId ?? null,
|
|
167
|
+
userId: auth.userId,
|
|
168
|
+
resourceKind: input.resourceKind,
|
|
169
|
+
resourceId: input.resourceId ?? '',
|
|
170
|
+
operation,
|
|
171
|
+
requestMethod: req.method,
|
|
172
|
+
requestHeaders: req.headers,
|
|
173
|
+
metadata,
|
|
174
|
+
})
|
|
175
|
+
} catch (error) {
|
|
176
|
+
console.error(`[mutation-guard] afterSuccess failed for guard ${guard.id}`, error)
|
|
177
|
+
}
|
|
178
|
+
}
|
|
179
|
+
},
|
|
180
|
+
}
|
|
181
|
+
}
|