@open-mercato/enterprise 0.6.1-develop.3069.d40b4417a9 → 0.6.1-develop.3090.06ab462170
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/dist/modules/record_locks/lib/recordLockService.js +55 -13
- package/dist/modules/record_locks/lib/recordLockService.js.map +2 -2
- package/dist/modules/record_locks/subscribers/conflict-detected-notification.js +20 -2
- package/dist/modules/record_locks/subscribers/conflict-detected-notification.js.map +2 -2
- package/package.json +5 -5
- package/src/modules/record_locks/lib/recordLockService.ts +59 -13
- package/src/modules/record_locks/subscribers/conflict-detected-notification.ts +25 -3
|
@@ -2,6 +2,8 @@ import { randomUUID } from "crypto";
|
|
|
2
2
|
import { UniqueConstraintViolationException } from "@mikro-orm/core";
|
|
3
3
|
import { sql } from "kysely";
|
|
4
4
|
import { ActionLog } from "@open-mercato/core/modules/audit_logs/data/entities";
|
|
5
|
+
import { findOneWithDecryption } from "@open-mercato/shared/lib/encryption/find";
|
|
6
|
+
import { parseDecryptedFieldValue } from "@open-mercato/shared/lib/encryption/tenantDataEncryptionService";
|
|
5
7
|
import { emitRecordLocksEvent } from "../events.js";
|
|
6
8
|
import {
|
|
7
9
|
RecordLock,
|
|
@@ -111,6 +113,25 @@ const MISSING_CONFLICT_VALUE = /* @__PURE__ */ Symbol("record_lock_conflict_miss
|
|
|
111
113
|
function isRecordValue(value) {
|
|
112
114
|
return Boolean(value && typeof value === "object" && !Array.isArray(value));
|
|
113
115
|
}
|
|
116
|
+
function parseDecryptedJsonLike(value) {
|
|
117
|
+
return typeof value === "string" ? parseDecryptedFieldValue(value) : value;
|
|
118
|
+
}
|
|
119
|
+
function readJsonRecordValue(value) {
|
|
120
|
+
const parsed = parseDecryptedJsonLike(value);
|
|
121
|
+
return isRecordValue(parsed) ? parsed : null;
|
|
122
|
+
}
|
|
123
|
+
function readActionLogChangesJson(log) {
|
|
124
|
+
return readJsonRecordValue(log?.changesJson ?? null);
|
|
125
|
+
}
|
|
126
|
+
function normalizeActionLogPayload(log) {
|
|
127
|
+
if (!log) return null;
|
|
128
|
+
log.changesJson = readJsonRecordValue(log.changesJson);
|
|
129
|
+
log.snapshotBefore = parseDecryptedJsonLike(log.snapshotBefore);
|
|
130
|
+
log.snapshotAfter = parseDecryptedJsonLike(log.snapshotAfter);
|
|
131
|
+
log.contextJson = readJsonRecordValue(log.contextJson);
|
|
132
|
+
log.commandPayload = parseDecryptedJsonLike(log.commandPayload);
|
|
133
|
+
return log;
|
|
134
|
+
}
|
|
114
135
|
function toIsoDate(value) {
|
|
115
136
|
if (value instanceof Date) {
|
|
116
137
|
if (Number.isNaN(value.getTime())) return null;
|
|
@@ -1064,7 +1085,13 @@ class RecordLockService {
|
|
|
1064
1085
|
if (input.organizationId !== void 0) {
|
|
1065
1086
|
where.organizationId = normalizeScopeOrganization(input.organizationId);
|
|
1066
1087
|
}
|
|
1067
|
-
return
|
|
1088
|
+
return normalizeActionLogPayload(await findOneWithDecryption(
|
|
1089
|
+
this.em,
|
|
1090
|
+
ActionLog,
|
|
1091
|
+
where,
|
|
1092
|
+
{ orderBy: { createdAt: "desc" } },
|
|
1093
|
+
{ tenantId: input.tenantId, organizationId: normalizeScopeOrganization(input.organizationId) }
|
|
1094
|
+
));
|
|
1068
1095
|
}
|
|
1069
1096
|
async findLatestActionLogWithScopeFallback(input) {
|
|
1070
1097
|
const scoped = await this.findLatestActionLog(input);
|
|
@@ -1087,25 +1114,33 @@ class RecordLockService {
|
|
|
1087
1114
|
if (input.organizationId !== void 0) {
|
|
1088
1115
|
where.organizationId = normalizeScopeOrganization(input.organizationId);
|
|
1089
1116
|
}
|
|
1090
|
-
return
|
|
1117
|
+
return normalizeActionLogPayload(await findOneWithDecryption(
|
|
1118
|
+
this.em,
|
|
1119
|
+
ActionLog,
|
|
1120
|
+
where,
|
|
1121
|
+
{ orderBy: { createdAt: "desc" } },
|
|
1122
|
+
{ tenantId: input.tenantId, organizationId: normalizeScopeOrganization(input.organizationId) }
|
|
1123
|
+
));
|
|
1091
1124
|
}
|
|
1092
1125
|
summarizeChangedFieldsFromActionLog(log) {
|
|
1093
1126
|
if (!log) return "";
|
|
1094
|
-
|
|
1095
|
-
|
|
1127
|
+
const changesJson = readActionLogChangesJson(log);
|
|
1128
|
+
if (changesJson) {
|
|
1129
|
+
const fromChanges = Object.keys(changesJson).filter((field) => !shouldSkipConflictField(field)).slice(0, 12).map(formatChangedFieldLabel).join(", ");
|
|
1096
1130
|
if (fromChanges) return fromChanges;
|
|
1097
1131
|
}
|
|
1098
|
-
const before =
|
|
1099
|
-
const after =
|
|
1132
|
+
const before = readJsonRecordValue(log.snapshotBefore);
|
|
1133
|
+
const after = readJsonRecordValue(log.snapshotAfter);
|
|
1100
1134
|
if (!before || !after) return "";
|
|
1101
1135
|
const diffPaths = /* @__PURE__ */ new Set();
|
|
1102
1136
|
this.collectSnapshotDiffPaths(before, after, null, diffPaths, /* @__PURE__ */ new Set());
|
|
1103
1137
|
return Array.from(diffPaths).filter((field) => !shouldSkipConflictField(field)).sort((left, right) => left.localeCompare(right)).slice(0, 12).map(formatChangedFieldLabel).join(", ");
|
|
1104
1138
|
}
|
|
1105
1139
|
buildIncomingChangeRowsFromActionLog(log) {
|
|
1106
|
-
|
|
1140
|
+
const changesJson = readActionLogChangesJson(log);
|
|
1141
|
+
if (!changesJson) return [];
|
|
1107
1142
|
const rows = [];
|
|
1108
|
-
for (const [rawField, rawChange] of Object.entries(
|
|
1143
|
+
for (const [rawField, rawChange] of Object.entries(changesJson)) {
|
|
1109
1144
|
if (rows.length >= 12) break;
|
|
1110
1145
|
if (shouldSkipConflictField(rawField)) continue;
|
|
1111
1146
|
const change = isRecordValue(rawChange) ? rawChange : {};
|
|
@@ -1275,8 +1310,15 @@ class RecordLockService {
|
|
|
1275
1310
|
if (!logId) return null;
|
|
1276
1311
|
let resolved = this.actionLogService ? await this.actionLogService.findById(logId) : null;
|
|
1277
1312
|
if (!resolved) {
|
|
1278
|
-
resolved = await
|
|
1313
|
+
resolved = await findOneWithDecryption(
|
|
1314
|
+
this.em,
|
|
1315
|
+
ActionLog,
|
|
1316
|
+
{ id: logId, deletedAt: null },
|
|
1317
|
+
void 0,
|
|
1318
|
+
{ tenantId: scope.tenantId, organizationId: normalizeScopeOrganization(scope.organizationId) }
|
|
1319
|
+
);
|
|
1279
1320
|
}
|
|
1321
|
+
resolved = normalizeActionLogPayload(resolved);
|
|
1280
1322
|
if (!resolved || resolved.deletedAt) return null;
|
|
1281
1323
|
if (resolved.tenantId !== scope.tenantId) return null;
|
|
1282
1324
|
if (scope.organizationId !== void 0) {
|
|
@@ -1318,12 +1360,12 @@ class RecordLockService {
|
|
|
1318
1360
|
};
|
|
1319
1361
|
const baseLog = await this.findActionLogById(conflict.baseActionLogId, scope);
|
|
1320
1362
|
const incomingLog = await this.findActionLogById(conflict.incomingActionLogId, scope);
|
|
1321
|
-
const baseSnapshot =
|
|
1322
|
-
const incomingBeforeSnapshot =
|
|
1323
|
-
const incomingAfterSnapshot =
|
|
1363
|
+
const baseSnapshot = readJsonRecordValue(baseLog?.snapshotAfter ?? null);
|
|
1364
|
+
const incomingBeforeSnapshot = readJsonRecordValue(incomingLog?.snapshotBefore ?? null);
|
|
1365
|
+
const incomingAfterSnapshot = readJsonRecordValue(incomingLog?.snapshotAfter ?? null);
|
|
1324
1366
|
const fallbackBaseSnapshot = baseSnapshot ?? incomingBeforeSnapshot;
|
|
1325
1367
|
const changeMap = /* @__PURE__ */ new Map();
|
|
1326
|
-
const incomingChanges =
|
|
1368
|
+
const incomingChanges = readActionLogChangesJson(incomingLog);
|
|
1327
1369
|
if (incomingChanges) {
|
|
1328
1370
|
for (const [fieldPathRaw, rawChange] of Object.entries(incomingChanges)) {
|
|
1329
1371
|
const fieldPath = fieldPathRaw.trim();
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"version": 3,
|
|
3
3
|
"sources": ["../../../../src/modules/record_locks/lib/recordLockService.ts"],
|
|
4
|
-
"sourcesContent": ["import { randomUUID } from 'crypto'\nimport { UniqueConstraintViolationException, type FilterQuery } from '@mikro-orm/core'\nimport type { EntityManager } from '@mikro-orm/postgresql'\nimport { type Kysely, sql } from 'kysely'\nimport type { ModuleConfigService } from '@open-mercato/core/modules/configs/lib/module-config-service'\nimport { ActionLog } from '@open-mercato/core/modules/audit_logs/data/entities'\nimport type { ActionLogService } from '@open-mercato/core/modules/audit_logs/services/actionLogService'\nimport type { RbacService } from '@open-mercato/core/modules/auth/services/rbacService'\nimport { emitRecordLocksEvent } from '../events'\nimport {\n RecordLock,\n RecordLockConflict,\n type RecordLockStatus,\n type RecordLockReleaseReason,\n type RecordLockConflictResolution,\n type RecordLockConflictStatus,\n} from '../data/entities'\nimport {\n recordLockMutationHeaderSchema,\n type RecordLockMutationHeaders,\n type RecordLockReleaseInput as RecordLockReleasePayloadInput,\n type RecordLockSettingsInput,\n} from '../data/validators'\nimport {\n DEFAULT_RECORD_LOCK_SETTINGS,\n RECORD_LOCKS_MODULE_ID,\n RECORD_LOCKS_SETTINGS_NAME,\n isRecordLockingEnabledForResource,\n normalizeRecordLockSettings,\n type RecordLockSettings,\n type RecordLockStrategy,\n} from './config'\n\nconst ACTIVE_LOCK_STATUS: RecordLockStatus = 'active'\nconst ACTIVE_SCOPE_UNIQUE_CONSTRAINTS = new Set([\n 'record_locks_active_scope_org_unique',\n 'record_locks_active_scope_tenant_unique',\n 'record_locks_active_scope_user_org_unique',\n 'record_locks_active_scope_user_tenant_unique',\n])\nconst LOCK_CONTENTION_EVENT_TTL_MS = 15_000\nconst PARTICIPANT_REJOIN_AFTER_SAVE_SUPPRESS_MS = 20_000\nconst LOCK_CONTENTION_EVENT_MAX_ENTRIES = 2_000\nconst lockContentionEventThrottle = new Map<string, number>()\nconst LOCK_CLEANUP_INTERVAL_MS = 5 * 60 * 1000\nconst LOCK_RETENTION_MS = 3 * 24 * 60 * 60 * 1000\nconst RESOLVED_CONFLICT_RETENTION_MS = 7 * 24 * 60 * 60 * 1000\nconst PENDING_CONFLICT_RETENTION_MS = 24 * 60 * 60 * 1000\nconst LOCK_CLEANUP_STATE_TTL_MS = 24 * 60 * 60 * 1000\nconst LOCK_CLEANUP_STATE_MAX_ENTRIES = 2_000\nconst lockCleanupStateByTenant = new Map<string, { lastRunAt: number; inFlight: boolean; lastSeenAt: number }>()\n\nexport type RecordLockScope = {\n tenantId: string\n organizationId?: string | null\n userId: string\n}\n\nexport type RecordLockResource = {\n resourceKind: string\n resourceId: string\n}\n\nexport type RecordLockAcquireInput = RecordLockScope & RecordLockResource & {\n lockedByIp?: string | null\n}\n\nexport type RecordLockHeartbeatInput = RecordLockScope & RecordLockResource & {\n token: string\n}\n\nexport type RecordLockReleaseInput = RecordLockScope & RecordLockResource & {\n token?: string\n reason?: Exclude<RecordLockReleaseReason, 'expired' | 'force'>\n} & Pick<RecordLockReleasePayloadInput, 'conflictId' | 'resolution'>\n\nexport type RecordLockForceReleaseInput = RecordLockScope & RecordLockResource & {\n reason?: string | null\n}\n\nexport type RecordLockMutationValidationInput = RecordLockScope & RecordLockResource & {\n method: 'PUT' | 'DELETE'\n headers: Partial<RecordLockMutationHeaders>\n mutationPayload?: Record<string, unknown> | null\n}\n\nexport type RecordLockConflictChange = {\n field: string\n displayValue: unknown\n baseValue: unknown\n incomingValue: unknown\n mineValue: unknown\n}\n\nexport type RecordLockConflictPayload = {\n id: string\n resourceKind: string\n resourceId: string\n baseActionLogId: string | null\n incomingActionLogId: string | null\n allowIncomingOverride: boolean\n canOverrideIncoming: boolean\n resolutionOptions: Array<'accept_mine'>\n changes: RecordLockConflictChange[]\n}\n\nexport type RecordLockView = {\n id: string\n resourceKind: string\n resourceId: string\n token: string | null\n strategy: RecordLockStrategy\n status: RecordLockStatus\n lockedByUserId: string\n lockedByIp: string | null\n baseActionLogId: string | null\n lockedAt: string\n lastHeartbeatAt: string\n expiresAt: string\n participants: RecordLockParticipantView[]\n activeParticipantCount: number\n}\n\nexport type RecordLockParticipantView = {\n userId: string\n lockedByIp: string | null\n lockedAt: string\n lastHeartbeatAt: string\n expiresAt: string\n}\n\nexport type RecordLockAcquireResult = {\n ok: true\n enabled: boolean\n resourceEnabled: boolean\n strategy: RecordLockStrategy\n allowForceUnlock: boolean\n heartbeatSeconds: number\n acquired: boolean\n latestActionLogId: string | null\n lock: RecordLockView | null\n}\n\nexport type RecordLockAcquireFailure = RecordLockValidationFailure & {\n allowForceUnlock: boolean\n}\n\nexport type RecordLockHeartbeatResult = {\n ok: true\n expiresAt: string | null\n}\n\nexport type RecordLockReleaseResult = {\n ok: true\n released: boolean\n conflictResolved: boolean\n}\n\nexport type RecordLockForceReleaseResult = {\n ok: true\n released: boolean\n lock: RecordLockView | null\n}\n\nexport type RecordLockValidationSuccess = {\n ok: true\n enabled: boolean\n resourceEnabled: boolean\n strategy: RecordLockStrategy\n shouldReleaseOnSuccess: boolean\n lock: RecordLockView | null\n latestActionLogId: string | null\n}\n\nexport type RecordLockValidationFailure = {\n ok: false\n status: 409 | 423\n error: string\n code: 'record_lock_conflict' | 'record_locked'\n lock: RecordLockView | null\n conflict?: RecordLockConflictPayload\n}\n\nexport type RecordLockValidationResult = RecordLockValidationSuccess | RecordLockValidationFailure\n\nexport type RecordLockServiceDeps = {\n em: EntityManager\n moduleConfigService?: ModuleConfigService | null\n actionLogService?: ActionLogService | null\n rbacService?: RbacService | null\n}\n\nexport type ParsedRecordLockHeaders = Partial<RecordLockMutationHeaders>\n\nfunction normalizeDate(value: Date): string {\n return value.toISOString()\n}\n\nfunction trimToNull(value: string | null | undefined): string | null {\n if (typeof value !== 'string') return null\n const trimmed = value.trim()\n return trimmed.length ? trimmed : null\n}\n\nfunction normalizeScopeOrganization(value: string | null | undefined): string | null {\n const trimmed = trimToNull(value)\n return trimmed ?? null\n}\n\nfunction shouldEmitLockContentionEvent(input: {\n tenantId: string\n organizationId?: string | null\n resourceKind: string\n resourceId: string\n lockedByUserId: string\n attemptedByUserId: string\n}): boolean {\n if (process.env.NODE_ENV === 'test') return true\n const now = Date.now()\n const key = [\n input.tenantId,\n normalizeScopeOrganization(input.organizationId) ?? 'global',\n input.resourceKind,\n input.resourceId,\n input.lockedByUserId,\n input.attemptedByUserId,\n ].join('|')\n\n const lastEmittedAt = lockContentionEventThrottle.get(key)\n if (typeof lastEmittedAt === 'number' && now - lastEmittedAt < LOCK_CONTENTION_EVENT_TTL_MS) {\n return false\n }\n\n lockContentionEventThrottle.set(key, now)\n\n for (const [cachedKey, cachedAt] of lockContentionEventThrottle.entries()) {\n if (now - cachedAt > LOCK_CONTENTION_EVENT_TTL_MS) {\n lockContentionEventThrottle.delete(cachedKey)\n }\n }\n if (lockContentionEventThrottle.size > LOCK_CONTENTION_EVENT_MAX_ENTRIES) {\n const oldest = Array.from(lockContentionEventThrottle.entries())\n .sort((left, right) => left[1] - right[1])\n .slice(0, lockContentionEventThrottle.size - LOCK_CONTENTION_EVENT_MAX_ENTRIES)\n for (const [staleKey] of oldest) lockContentionEventThrottle.delete(staleKey)\n }\n\n return true\n}\n\nfunction isActiveLockScopeUniqueViolation(error: unknown): boolean {\n if (error instanceof UniqueConstraintViolationException) {\n const errorWithConstraint = error as unknown as { constraint?: unknown }\n const constraint = typeof errorWithConstraint.constraint === 'string'\n ? errorWithConstraint.constraint\n : null\n if (constraint && ACTIVE_SCOPE_UNIQUE_CONSTRAINTS.has(constraint)) return true\n }\n if (!error || typeof error !== 'object') return false\n const code = (error as { code?: unknown }).code\n if (code !== '23505') return false\n const message = typeof (error as { message?: unknown }).message === 'string'\n ? (error as { message: string }).message.toLowerCase()\n : ''\n for (const constraint of ACTIVE_SCOPE_UNIQUE_CONSTRAINTS) {\n if (message.includes(constraint)) return true\n }\n return false\n}\n\nfunction getKysely(em: EntityManager): Kysely<any> {\n return (em as unknown as { getKysely: () => Kysely<any> }).getKysely()\n}\n\nconst SKIPPED_CONFLICT_FIELDS = new Set([\n 'updatedAt',\n 'updated_at',\n 'createdAt',\n 'created_at',\n 'deletedAt',\n 'deleted_at',\n])\n\nfunction shouldSkipConflictField(path: string): boolean {\n if (!path.trim().length) return true\n if (SKIPPED_CONFLICT_FIELDS.has(path)) return true\n const segments = path.split('.').filter((segment) => segment.length > 0)\n if (!segments.length) return true\n return SKIPPED_CONFLICT_FIELDS.has(segments[segments.length - 1] ?? '')\n}\n\nconst MISSING_CONFLICT_VALUE = Symbol('record_lock_conflict_missing_value')\n\nfunction isRecordValue(value: unknown): value is Record<string, unknown> {\n return Boolean(value && typeof value === 'object' && !Array.isArray(value))\n}\n\nfunction toIsoDate(value: unknown): string | null {\n if (value instanceof Date) {\n if (Number.isNaN(value.getTime())) return null\n return value.toISOString()\n }\n if (typeof value === 'string') {\n const parsed = new Date(value)\n return Number.isNaN(parsed.getTime()) ? null : parsed.toISOString()\n }\n return null\n}\n\nfunction valuesEqual(a: unknown, b: unknown, seen?: Set<unknown>): boolean {\n if (Object.is(a, b)) return true\n\n if (a instanceof Date || b instanceof Date) {\n const left = toIsoDate(a)\n const right = toIsoDate(b)\n return left !== null && right !== null && left === right\n }\n\n if (Array.isArray(a) && Array.isArray(b)) {\n if (a.length !== b.length) return false\n for (let index = 0; index < a.length; index += 1) {\n if (!valuesEqual(a[index], b[index], seen)) return false\n }\n return true\n }\n\n if (isRecordValue(a) && isRecordValue(b)) {\n if (!seen) seen = new Set()\n if (seen.has(a) || seen.has(b)) return false\n seen.add(a)\n seen.add(b)\n const aKeys = Object.keys(a)\n const bKeys = Object.keys(b)\n if (aKeys.length !== bKeys.length) return false\n for (const key of aKeys) {\n if (!Object.prototype.hasOwnProperty.call(b, key)) return false\n if (!valuesEqual(a[key], b[key], seen)) return false\n }\n return true\n }\n\n return false\n}\n\nfunction readPathValue(source: unknown, path: string): unknown | typeof MISSING_CONFLICT_VALUE {\n if (!path.trim().length || !isRecordValue(source)) return MISSING_CONFLICT_VALUE\n if (Object.prototype.hasOwnProperty.call(source, path)) return source[path]\n\n const segments = path.split('.').filter((segment) => segment.length > 0)\n if (!segments.length) return MISSING_CONFLICT_VALUE\n\n let current: unknown = source\n for (const segment of segments) {\n if (!isRecordValue(current)) return MISSING_CONFLICT_VALUE\n if (!Object.prototype.hasOwnProperty.call(current, segment)) return MISSING_CONFLICT_VALUE\n current = current[segment]\n }\n return current\n}\n\nfunction buildPathVariants(path: string): string[] {\n const trimmed = path.trim()\n if (!trimmed.length) return []\n\n const segments = trimmed.split('.').filter((segment) => segment.length > 0)\n if (segments.length <= 1) return [trimmed]\n\n const variants = new Set<string>([trimmed])\n for (let index = 1; index < segments.length; index += 1) {\n variants.add(segments.slice(index).join('.'))\n }\n return Array.from(variants)\n}\n\nfunction readPathValueLoose(source: unknown, path: string): unknown | typeof MISSING_CONFLICT_VALUE {\n const variants = buildPathVariants(path)\n for (const variant of variants) {\n const value = readPathValue(source, variant)\n if (value !== MISSING_CONFLICT_VALUE) return value\n }\n return MISSING_CONFLICT_VALUE\n}\n\nfunction normalizeConflictValue(value: unknown): unknown {\n return value === undefined ? null : value\n}\n\nfunction formatNotificationValue(value: unknown): string {\n if (value === null || value === undefined) return '-'\n if (typeof value === 'string') return value\n if (typeof value === 'number' || typeof value === 'boolean') return String(value)\n if (value instanceof Date) return value.toISOString()\n try {\n return JSON.stringify(value)\n } catch {\n return String(value)\n }\n}\n\nfunction formatChangedFieldLabel(rawField: string): string {\n const trimmedField = rawField.trim()\n const withoutNamespace = trimmedField.includes('::') ? (trimmedField.split('::').pop() ?? trimmedField) : trimmedField\n const withoutPrefix = withoutNamespace.includes('.') ? (withoutNamespace.split('.').pop() ?? withoutNamespace) : withoutNamespace\n const words = withoutPrefix\n .replace(/([a-z0-9])([A-Z])/g, '$1 $2')\n .replace(/[._-]+/g, ' ')\n .replace(/\\s+/g, ' ')\n .trim()\n .split(' ')\n .filter(Boolean)\n\n if (!words.length) return trimmedField\n return words\n .map((word) => word.charAt(0).toUpperCase() + word.slice(1))\n .join(' ')\n}\n\nexport function readRecordLockHeaders(headers: Headers): ParsedRecordLockHeaders {\n const raw = {\n resourceKind: trimToNull(headers.get('x-om-record-lock-kind')) ?? undefined,\n resourceId: trimToNull(headers.get('x-om-record-lock-resource-id')) ?? undefined,\n token: trimToNull(headers.get('x-om-record-lock-token')) ?? undefined,\n baseLogId: trimToNull(headers.get('x-om-record-lock-base-log-id')) ?? undefined,\n resolution: trimToNull(headers.get('x-om-record-lock-resolution')) ?? undefined,\n conflictId: trimToNull(headers.get('x-om-record-lock-conflict-id')) ?? undefined,\n }\n\n const parsed = recordLockMutationHeaderSchema.partial().safeParse(raw)\n if (!parsed.success) return {}\n return parsed.data\n}\n\nexport class RecordLockService {\n private readonly em: EntityManager\n\n private readonly moduleConfigService: ModuleConfigService | null\n\n private readonly actionLogService: ActionLogService | null\n\n private readonly rbacService: RbacService | null\n\n constructor(deps: RecordLockServiceDeps) {\n this.em = deps.em\n this.moduleConfigService = deps.moduleConfigService ?? null\n this.actionLogService = deps.actionLogService ?? null\n this.rbacService = deps.rbacService ?? null\n }\n\n async getSettings(): Promise<RecordLockSettings> {\n if (!this.moduleConfigService) return DEFAULT_RECORD_LOCK_SETTINGS\n\n const value = await this.moduleConfigService.getValue<RecordLockSettings>(\n RECORD_LOCKS_MODULE_ID,\n RECORD_LOCKS_SETTINGS_NAME,\n { defaultValue: DEFAULT_RECORD_LOCK_SETTINGS },\n )\n\n return normalizeRecordLockSettings(value ?? DEFAULT_RECORD_LOCK_SETTINGS)\n }\n\n async saveSettings(input: RecordLockSettingsInput): Promise<RecordLockSettings> {\n const settings = normalizeRecordLockSettings(input)\n if (!this.moduleConfigService) return settings\n\n await this.moduleConfigService.setValue(RECORD_LOCKS_MODULE_ID, RECORD_LOCKS_SETTINGS_NAME, settings)\n return settings // NOSONAR \u2014 both paths return settings by design; the branch controls persistence\n }\n\n async acquire(input: RecordLockAcquireInput): Promise<RecordLockAcquireResult | RecordLockAcquireFailure> {\n this.scheduleCleanup(input.tenantId)\n const settings = await this.getSettings()\n const latest = await this.findLatestActionLogWithScopeFallback(input)\n const resourceEnabled = isRecordLockingEnabledForResource(settings, input.resourceKind)\n\n if (!resourceEnabled) {\n return {\n ok: true,\n enabled: settings.enabled,\n resourceEnabled: false,\n strategy: settings.strategy,\n allowForceUnlock: settings.allowForceUnlock,\n heartbeatSeconds: settings.heartbeatSeconds,\n acquired: false,\n latestActionLogId: latest?.id ?? null,\n lock: null,\n }\n }\n\n const now = new Date()\n let activeLocks = await this.findActiveLocks(input, now)\n const ownedActiveLock = activeLocks.find((lock) => lock.lockedByUserId === input.userId) ?? null\n const competingActiveLock = activeLocks.find((lock) => lock.lockedByUserId !== input.userId) ?? null\n\n if (settings.strategy === 'pessimistic' && !ownedActiveLock && competingActiveLock) {\n const lockView = this.toLockView(competingActiveLock, false, activeLocks)\n if (shouldEmitLockContentionEvent({\n tenantId: competingActiveLock.tenantId,\n organizationId: competingActiveLock.organizationId,\n resourceKind: competingActiveLock.resourceKind,\n resourceId: competingActiveLock.resourceId,\n lockedByUserId: competingActiveLock.lockedByUserId,\n attemptedByUserId: input.userId,\n })) {\n await emitRecordLocksEvent('record_locks.lock.contended', {\n lockId: competingActiveLock.id,\n resourceKind: competingActiveLock.resourceKind,\n resourceId: competingActiveLock.resourceId,\n tenantId: competingActiveLock.tenantId,\n organizationId: competingActiveLock.organizationId,\n lockedByUserId: competingActiveLock.lockedByUserId,\n attemptedByUserId: input.userId,\n })\n }\n return {\n ok: false,\n status: 423,\n error: 'Record is currently locked by another user',\n code: 'record_locked',\n allowForceUnlock: settings.allowForceUnlock,\n lock: lockView,\n }\n }\n\n if (ownedActiveLock) {\n ownedActiveLock.strategy = settings.strategy\n ownedActiveLock.lockedByIp = input.lockedByIp ?? ownedActiveLock.lockedByIp ?? null\n ownedActiveLock.lastHeartbeatAt = now\n ownedActiveLock.expiresAt = new Date(now.getTime() + settings.timeoutSeconds * 1000)\n await this.em.flush()\n\n activeLocks = await this.findActiveLocks(input, now)\n return {\n ok: true,\n enabled: settings.enabled,\n resourceEnabled: true,\n strategy: settings.strategy,\n allowForceUnlock: settings.allowForceUnlock,\n heartbeatSeconds: settings.heartbeatSeconds,\n acquired: false,\n latestActionLogId: latest?.id ?? null,\n lock: this.toLockView(ownedActiveLock, true, activeLocks),\n }\n }\n\n const lock = this.em.create(RecordLock, {\n resourceKind: input.resourceKind,\n resourceId: input.resourceId,\n token: randomUUID(),\n strategy: settings.strategy,\n status: ACTIVE_LOCK_STATUS,\n lockedByUserId: input.userId,\n lockedByIp: input.lockedByIp ?? null,\n baseActionLogId: latest?.id ?? null,\n lockedAt: now,\n lastHeartbeatAt: now,\n expiresAt: new Date(now.getTime() + settings.timeoutSeconds * 1000),\n tenantId: input.tenantId,\n organizationId: normalizeScopeOrganization(input.organizationId),\n })\n\n this.em.persist(lock)\n let createdNewLock = true\n try {\n await this.em.flush()\n } catch (error) {\n if (!isActiveLockScopeUniqueViolation(error)) throw error\n const clear = (this.em as { clear?: () => void }).clear\n if (typeof clear === 'function') clear.call(this.em)\n const locksAfterCollision = await this.findActiveLocks(input, now)\n const competingAfterCollision = locksAfterCollision.find((item) => item.lockedByUserId !== input.userId) ?? null\n if (settings.strategy === 'pessimistic' && competingAfterCollision) {\n if (shouldEmitLockContentionEvent({\n tenantId: competingAfterCollision.tenantId,\n organizationId: competingAfterCollision.organizationId,\n resourceKind: competingAfterCollision.resourceKind,\n resourceId: competingAfterCollision.resourceId,\n lockedByUserId: competingAfterCollision.lockedByUserId,\n attemptedByUserId: input.userId,\n })) {\n await emitRecordLocksEvent('record_locks.lock.contended', {\n lockId: competingAfterCollision.id,\n resourceKind: competingAfterCollision.resourceKind,\n resourceId: competingAfterCollision.resourceId,\n tenantId: competingAfterCollision.tenantId,\n organizationId: competingAfterCollision.organizationId,\n lockedByUserId: competingAfterCollision.lockedByUserId,\n attemptedByUserId: input.userId,\n })\n }\n return {\n ok: false,\n status: 423,\n error: 'Record is currently locked by another user',\n code: 'record_locked',\n allowForceUnlock: settings.allowForceUnlock,\n lock: this.toLockView(competingAfterCollision, false, locksAfterCollision),\n }\n }\n const existingOwned = await this.findOwnedActiveLock(input)\n if (!existingOwned) throw error\n existingOwned.strategy = settings.strategy\n existingOwned.lockedByIp = input.lockedByIp ?? existingOwned.lockedByIp ?? null\n existingOwned.lastHeartbeatAt = now\n existingOwned.expiresAt = new Date(now.getTime() + settings.timeoutSeconds * 1000)\n await this.em.flush()\n createdNewLock = false\n }\n\n const activeAfterAcquire = await this.findActiveLocks(input, now)\n const ownedAfterAcquire = activeAfterAcquire.find((item) => item.lockedByUserId === input.userId)\n ?? await this.findOwnedActiveLock(input)\n ?? lock\n ?? null\n if (!ownedAfterAcquire) {\n const fallbackLock = activeAfterAcquire[0] ?? null\n return {\n ok: true,\n enabled: settings.enabled,\n resourceEnabled: true,\n strategy: settings.strategy,\n allowForceUnlock: settings.allowForceUnlock,\n heartbeatSeconds: settings.heartbeatSeconds,\n acquired: false,\n latestActionLogId: latest?.id ?? null,\n lock: fallbackLock ? this.toLockView(fallbackLock, false, activeAfterAcquire) : null,\n }\n }\n\n if (createdNewLock) {\n await emitRecordLocksEvent('record_locks.lock.acquired', {\n lockId: ownedAfterAcquire.id,\n resourceKind: ownedAfterAcquire.resourceKind,\n resourceId: ownedAfterAcquire.resourceId,\n tenantId: ownedAfterAcquire.tenantId,\n organizationId: ownedAfterAcquire.organizationId,\n lockedByUserId: ownedAfterAcquire.lockedByUserId,\n strategy: ownedAfterAcquire.strategy,\n baseActionLogId: ownedAfterAcquire.baseActionLogId,\n activeParticipantCount: activeAfterAcquire.length,\n })\n\n const recipientUserIds = activeAfterAcquire\n .filter((item) => item.lockedByUserId !== input.userId)\n .map((item) => item.lockedByUserId)\n const shouldSuppressJoinNotification = await this.hasRecentSavedRelease({\n tenantId: input.tenantId,\n organizationId: input.organizationId,\n userId: input.userId,\n resourceKind: input.resourceKind,\n resourceId: input.resourceId,\n now,\n })\n\n if (!shouldSuppressJoinNotification) {\n await emitRecordLocksEvent('record_locks.participant.joined', {\n lockId: ownedAfterAcquire.id,\n resourceKind: ownedAfterAcquire.resourceKind,\n resourceId: ownedAfterAcquire.resourceId,\n tenantId: ownedAfterAcquire.tenantId,\n organizationId: ownedAfterAcquire.organizationId,\n joinedUserId: input.userId,\n joinedIp: ownedAfterAcquire.lockedByIp ?? null,\n recipientUserIds,\n activeParticipantCount: activeAfterAcquire.length,\n })\n }\n }\n\n return {\n ok: true,\n enabled: settings.enabled,\n resourceEnabled: true,\n strategy: settings.strategy,\n allowForceUnlock: settings.allowForceUnlock,\n heartbeatSeconds: settings.heartbeatSeconds,\n acquired: createdNewLock,\n latestActionLogId: latest?.id ?? null,\n lock: this.toLockView(ownedAfterAcquire, true, activeAfterAcquire),\n }\n }\n\n async heartbeat(input: RecordLockHeartbeatInput): Promise<RecordLockHeartbeatResult> {\n const settings = await this.getSettings()\n const resourceEnabled = isRecordLockingEnabledForResource(settings, input.resourceKind)\n if (!resourceEnabled) return { ok: true, expiresAt: null }\n\n const lock = await this.findOwnedLockByToken(input)\n if (!lock) return { ok: true, expiresAt: null }\n\n const now = new Date()\n if (lock.expiresAt <= now) {\n this.markLockReleased(lock, {\n status: 'expired',\n reason: 'expired',\n releasedByUserId: lock.lockedByUserId,\n now,\n })\n await this.em.flush()\n return { ok: true, expiresAt: null }\n }\n\n lock.lastHeartbeatAt = now\n lock.expiresAt = new Date(now.getTime() + settings.timeoutSeconds * 1000)\n await this.em.flush()\n return { ok: true, expiresAt: normalizeDate(lock.expiresAt) }\n }\n\n async release(input: RecordLockReleaseInput): Promise<RecordLockReleaseResult> {\n const settings = await this.getSettings()\n const resourceEnabled = isRecordLockingEnabledForResource(settings, input.resourceKind)\n if (!resourceEnabled) return { ok: true, released: false, conflictResolved: false }\n\n let conflictResolved = false\n if (input.reason === 'conflict_resolved' && input.conflictId && input.resolution === 'accept_incoming') {\n const conflict = await this.findConflictById(input.conflictId, input)\n if (conflict && conflict.status === 'pending' && conflict.conflictActorUserId === input.userId) {\n await this.resolveConflict(conflict, input.resolution, input.userId)\n conflictResolved = true\n }\n }\n\n const lock = input.token\n ? await this.findOwnedLockByToken(input)\n : await this.findOwnedActiveLock(input)\n if (!lock) return { ok: true, released: false, conflictResolved }\n\n const now = new Date()\n this.markLockReleased(lock, {\n status: 'released',\n reason: input.reason ?? 'cancelled',\n releasedByUserId: input.userId,\n now,\n })\n await this.em.flush()\n\n await emitRecordLocksEvent('record_locks.lock.released', {\n lockId: lock.id,\n resourceKind: lock.resourceKind,\n resourceId: lock.resourceId,\n tenantId: lock.tenantId,\n organizationId: lock.organizationId,\n lockedByUserId: lock.lockedByUserId,\n releasedByUserId: input.userId,\n reason: lock.releaseReason,\n })\n\n if (lock.releaseReason === 'unmount') {\n const remainingActiveLocks = await this.findActiveLocks(input, now)\n const recipientUserIds = remainingActiveLocks\n .map((activeLock) => activeLock.lockedByUserId)\n .filter((userId) => userId !== lock.lockedByUserId)\n\n if (recipientUserIds.length) {\n await emitRecordLocksEvent('record_locks.participant.left', {\n lockId: lock.id,\n resourceKind: lock.resourceKind,\n resourceId: lock.resourceId,\n tenantId: lock.tenantId,\n organizationId: lock.organizationId,\n leftUserId: lock.lockedByUserId,\n reason: 'unmount',\n recipientUserIds,\n activeParticipantCount: remainingActiveLocks.length,\n })\n }\n }\n\n return { ok: true, released: true, conflictResolved }\n }\n\n async forceRelease(input: RecordLockForceReleaseInput): Promise<RecordLockForceReleaseResult> {\n const settings = await this.getSettings()\n const resourceEnabled = isRecordLockingEnabledForResource(settings, input.resourceKind)\n const canForceRelease = await this.canUserForceRelease(input, settings)\n if (!resourceEnabled || !settings.allowForceUnlock || !canForceRelease) {\n return { ok: true, released: false, lock: null }\n }\n\n const now = new Date()\n const activeLocks = this.sortLocksByJoinOrder(await this.findActiveLocks(input, now))\n const lock = activeLocks[0] ?? null\n if (!lock) return { ok: true, released: false, lock: null }\n\n this.markLockReleased(lock, {\n status: 'force_released',\n reason: 'force',\n releasedByUserId: input.userId,\n now,\n })\n await this.em.flush()\n\n await emitRecordLocksEvent('record_locks.lock.force_released', {\n lockId: lock.id,\n resourceKind: lock.resourceKind,\n resourceId: lock.resourceId,\n tenantId: lock.tenantId,\n organizationId: lock.organizationId,\n lockedByUserId: lock.lockedByUserId,\n releasedByUserId: input.userId,\n reason: input.reason ?? null,\n })\n\n const remainingLocks = this.sortLocksByJoinOrder(activeLocks.filter((item) => item.id !== lock.id))\n const nextInQueue = remainingLocks[0] ?? null\n return { ok: true, released: true, lock: nextInQueue ? this.toLockView(nextInQueue, false, remainingLocks) : null }\n }\n\n async validateMutation(input: RecordLockMutationValidationInput): Promise<RecordLockValidationResult> {\n this.scheduleCleanup(input.tenantId)\n const settings = await this.getSettings()\n const resourceEnabled = isRecordLockingEnabledForResource(settings, input.resourceKind)\n const canOverrideIncoming = await this.canUserOverrideIncoming(input, settings)\n\n if (!resourceEnabled) {\n return {\n ok: true,\n enabled: settings.enabled,\n resourceEnabled: false,\n strategy: settings.strategy,\n shouldReleaseOnSuccess: false,\n lock: null,\n latestActionLogId: null,\n }\n }\n\n const parsedHeaders = this.normalizeMutationHeaders(input.headers)\n const keepMineResolution = parsedHeaders.resolution === 'accept_mine' || parsedHeaders.resolution === 'merged'\n ? parsedHeaders.resolution\n : null\n const hasKeepMineIntent = keepMineResolution !== null\n const now = new Date()\n const activeLocks = await this.findActiveLocks(input, now)\n const ownedActiveLock = activeLocks.find((lock) => lock.lockedByUserId === input.userId) ?? null\n const competingLock = activeLocks.find((lock) => lock.lockedByUserId !== input.userId) ?? null\n const latest = await this.findLatestActionLogWithScopeFallback(input)\n const shouldReleaseOnSuccess = Boolean(\n ownedActiveLock\n && (!parsedHeaders.token || ownedActiveLock.token === parsedHeaders.token),\n )\n\n if (settings.strategy === 'pessimistic') {\n if (competingLock && !ownedActiveLock) {\n return {\n ok: false,\n status: 423,\n error: 'Record is currently locked by another user',\n code: 'record_locked',\n lock: this.toLockView(competingLock, false, activeLocks),\n }\n }\n\n if (ownedActiveLock) {\n if (parsedHeaders.token && ownedActiveLock.token !== parsedHeaders.token) {\n return {\n ok: false,\n status: 423,\n error: 'Valid lock token is required for this mutation',\n code: 'record_locked',\n lock: this.toLockView(ownedActiveLock, false, activeLocks),\n }\n }\n }\n\n return {\n ok: true,\n enabled: settings.enabled,\n resourceEnabled: true,\n strategy: settings.strategy,\n shouldReleaseOnSuccess,\n lock: ownedActiveLock ? this.toLockView(ownedActiveLock, false, activeLocks) : null,\n latestActionLogId: latest?.id ?? null,\n }\n }\n\n const existingConflict = parsedHeaders.conflictId\n ? await this.findConflictById(parsedHeaders.conflictId, input)\n : null\n\n if (existingConflict) {\n const canResolveExistingConflict = existingConflict.status === 'pending'\n && existingConflict.conflictActorUserId === input.userId\n\n if (parsedHeaders.resolution === 'accept_mine' || parsedHeaders.resolution === 'merged') {\n const isAlreadyResolvedByRequester = existingConflict.conflictActorUserId === input.userId\n && existingConflict.status !== 'pending'\n && existingConflict.resolution === parsedHeaders.resolution\n\n if (!canResolveExistingConflict && !isAlreadyResolvedByRequester) {\n return {\n ok: false,\n status: 409,\n error: 'Record conflict requires resolution before saving',\n code: 'record_lock_conflict',\n lock: ownedActiveLock ? this.toLockView(ownedActiveLock, false, activeLocks) : null,\n conflict: await this.toConflictPayload(existingConflict, input.mutationPayload ?? null, settings.allowIncomingOverride, canOverrideIncoming),\n }\n }\n if (!canOverrideIncoming) {\n return {\n ok: false,\n status: 409,\n error: 'Record conflict requires resolution before saving',\n code: 'record_lock_conflict',\n lock: ownedActiveLock ? this.toLockView(ownedActiveLock, false, activeLocks) : null,\n conflict: await this.toConflictPayload(existingConflict, input.mutationPayload ?? null, settings.allowIncomingOverride, canOverrideIncoming),\n }\n }\n if (canResolveExistingConflict) {\n await this.resolveConflict(existingConflict, parsedHeaders.resolution, input.userId)\n }\n } else {\n return {\n ok: false,\n status: 409,\n error: 'Record conflict requires resolution before saving',\n code: 'record_lock_conflict',\n lock: ownedActiveLock ? this.toLockView(ownedActiveLock, false, activeLocks) : null,\n conflict: await this.toConflictPayload(existingConflict, input.mutationPayload ?? null, settings.allowIncomingOverride, canOverrideIncoming),\n }\n }\n }\n\n if (!existingConflict) {\n const baseActionLogId = parsedHeaders.baseLogId\n ?? (ownedActiveLock ? ownedActiveLock.baseActionLogId : null)\n\n const hasConflictingBaseLog = Boolean(\n latest?.id\n && baseActionLogId\n && latest.id !== baseActionLogId\n )\n const hasConflictingWriteAfterLockStart = Boolean(\n latest?.id\n && !baseActionLogId\n && ownedActiveLock\n && latest.createdAt instanceof Date\n && ownedActiveLock.lockedAt instanceof Date\n && latest.createdAt.getTime() > ownedActiveLock.lockedAt.getTime()\n && latest.actorUserId !== input.userId\n )\n const isConflictingWrite = hasConflictingBaseLog || hasConflictingWriteAfterLockStart\n\n if (isConflictingWrite) {\n if (keepMineResolution && canOverrideIncoming) {\n const autoResolvedConflict = await this.createConflict({\n scope: input,\n baseActionLogId,\n incomingActionLogId: latest?.id ?? null,\n conflictActorUserId: input.userId,\n incomingActorUserId: latest?.actorUserId ?? null,\n })\n await this.resolveConflict(autoResolvedConflict, keepMineResolution, input.userId)\n\n return {\n ok: true,\n enabled: settings.enabled,\n resourceEnabled: true,\n strategy: settings.strategy,\n shouldReleaseOnSuccess,\n lock: ownedActiveLock ? this.toLockView(ownedActiveLock, false, activeLocks) : null,\n latestActionLogId: latest?.id ?? null,\n }\n }\n\n const conflict = await this.createConflict({\n scope: input,\n baseActionLogId,\n incomingActionLogId: latest?.id ?? null,\n conflictActorUserId: input.userId,\n incomingActorUserId: latest?.actorUserId ?? null,\n })\n\n return {\n ok: false,\n status: 409,\n error: 'Record conflict detected',\n code: 'record_lock_conflict',\n lock: ownedActiveLock ? this.toLockView(ownedActiveLock, false, activeLocks) : null,\n conflict: await this.toConflictPayload(conflict, input.mutationPayload ?? null, settings.allowIncomingOverride, canOverrideIncoming),\n }\n }\n }\n\n return {\n ok: true,\n enabled: settings.enabled,\n resourceEnabled: true,\n strategy: settings.strategy,\n shouldReleaseOnSuccess,\n lock: ownedActiveLock ? this.toLockView(ownedActiveLock, false, activeLocks) : null,\n latestActionLogId: latest?.id ?? null,\n }\n }\n\n async releaseAfterMutation(input: RecordLockReleaseInput): Promise<void> {\n const releaseResult = await this.release({\n ...input,\n reason: input.reason ?? 'saved',\n })\n if (!releaseResult.released) return\n }\n\n async emitIncomingChangesNotificationAfterMutation(input: {\n tenantId: string\n organizationId?: string | null\n userId: string\n resourceKind: string\n resourceId: string\n method: 'PUT' | 'DELETE'\n }): Promise<void> {\n if (input.method !== 'PUT') return\n const settings = await this.getSettings()\n if (!settings.notifyOnConflict || !isRecordLockingEnabledForResource(settings, input.resourceKind)) return\n\n const now = new Date()\n let activeLocks = await this.findActiveLocks(input, now)\n if (!activeLocks.length) {\n const fallbackWhere: FilterQuery<RecordLock> = {\n tenantId: input.tenantId,\n deletedAt: null,\n resourceKind: input.resourceKind,\n resourceId: input.resourceId,\n status: ACTIVE_LOCK_STATUS,\n }\n const fallbackLocks = await this.em.find(RecordLock, fallbackWhere, { orderBy: { updatedAt: 'desc' } })\n activeLocks = Array.isArray(fallbackLocks) ? fallbackLocks : []\n }\n\n const recipientUserIds = new Set<string>()\n for (const lock of activeLocks) {\n if (lock.lockedByUserId !== input.userId) {\n recipientUserIds.add(lock.lockedByUserId)\n }\n }\n if (!recipientUserIds.size) {\n const fallbackWindowMs = Math.max((settings.timeoutSeconds ?? 300) * 1000, 60_000)\n const fallbackSince = new Date(now.getTime() - fallbackWindowMs)\n const recentLocks = await this.em.find(RecordLock, {\n tenantId: input.tenantId,\n deletedAt: null,\n resourceKind: input.resourceKind,\n resourceId: input.resourceId,\n updatedAt: { $gte: fallbackSince },\n }, { orderBy: { updatedAt: 'desc' }, limit: 50 })\n\n for (const lock of (Array.isArray(recentLocks) ? recentLocks : [])) {\n if (lock.lockedByUserId !== input.userId) {\n recipientUserIds.add(lock.lockedByUserId)\n }\n }\n }\n if (!recipientUserIds.size) return\n\n let latest = await this.findLatestActionLog(input)\n if (!latest) {\n latest = await this.findLatestActionLog({\n tenantId: input.tenantId,\n resourceKind: input.resourceKind,\n resourceId: input.resourceId,\n })\n }\n let actorLog = latest?.actorUserId === input.userId\n ? latest\n : await this.findLatestActionLogByActor(input, input.userId)\n if (!actorLog) {\n actorLog = await this.findLatestActionLogByActor({\n tenantId: input.tenantId,\n resourceKind: input.resourceKind,\n resourceId: input.resourceId,\n }, input.userId)\n }\n const incomingLog = actorLog ?? latest\n\n const changedFields = incomingLog\n ? this.summarizeChangedFieldsFromActionLog(incomingLog)\n : ''\n const changedRows = this.buildIncomingChangeRowsFromActionLog(incomingLog)\n const changedRowsJson = changedRows.length ? JSON.stringify(changedRows) : ''\n\n await emitRecordLocksEvent('record_locks.incoming_changes.available', {\n resourceKind: input.resourceKind,\n resourceId: input.resourceId,\n tenantId: input.tenantId,\n organizationId: normalizeScopeOrganization(input.organizationId),\n incomingActorUserId: input.userId,\n incomingActionLogId: incomingLog?.id ?? null,\n recipientUserIds: Array.from(recipientUserIds),\n changedFields: changedFields || '-',\n changedRowsJson,\n })\n }\n\n async emitRecordDeletedNotificationAfterMutation(input: {\n tenantId: string\n organizationId?: string | null\n userId: string\n resourceKind: string\n resourceId: string\n method: 'PUT' | 'DELETE'\n }): Promise<void> {\n if (input.method !== 'DELETE') return\n const settings = await this.getSettings()\n if (!isRecordLockingEnabledForResource(settings, input.resourceKind)) return\n\n const now = new Date()\n let activeLocks = await this.findActiveLocks(input, now)\n if (!activeLocks.length) {\n const fallbackWhere: FilterQuery<RecordLock> = {\n tenantId: input.tenantId,\n deletedAt: null,\n resourceKind: input.resourceKind,\n resourceId: input.resourceId,\n status: ACTIVE_LOCK_STATUS,\n }\n const fallbackLocks = await this.em.find(RecordLock, fallbackWhere, { orderBy: { updatedAt: 'desc' } })\n activeLocks = Array.isArray(fallbackLocks) ? fallbackLocks : []\n }\n\n const recipientUserIds = new Set<string>()\n for (const lock of activeLocks) {\n if (lock.lockedByUserId !== input.userId) {\n recipientUserIds.add(lock.lockedByUserId)\n }\n }\n\n if (!recipientUserIds.size) {\n const fallbackWindowMs = Math.max((settings.timeoutSeconds ?? 300) * 1000, 60_000)\n const fallbackSince = new Date(now.getTime() - fallbackWindowMs)\n const recentLocks = await this.em.find(RecordLock, {\n tenantId: input.tenantId,\n deletedAt: null,\n resourceKind: input.resourceKind,\n resourceId: input.resourceId,\n updatedAt: { $gte: fallbackSince },\n }, { orderBy: { updatedAt: 'desc' }, limit: 50 })\n\n for (const lock of (Array.isArray(recentLocks) ? recentLocks : [])) {\n if (lock.lockedByUserId !== input.userId) {\n recipientUserIds.add(lock.lockedByUserId)\n }\n }\n }\n if (!recipientUserIds.size) return\n\n await emitRecordLocksEvent('record_locks.record.deleted', {\n resourceKind: input.resourceKind,\n resourceId: input.resourceId,\n tenantId: input.tenantId,\n organizationId: normalizeScopeOrganization(input.organizationId),\n deletedByUserId: input.userId,\n recipientUserIds: Array.from(recipientUserIds),\n })\n }\n\n async resolveConflictById(input: {\n conflictId: string\n tenantId: string\n organizationId?: string | null\n userId: string\n resolution: 'accept_incoming' | 'accept_mine' | 'merged'\n }): Promise<boolean> {\n const settings = await this.getSettings()\n const canOverrideIncoming = await this.canUserOverrideIncoming(input, settings)\n const conflict = await this.em.findOne(RecordLockConflict, {\n id: input.conflictId,\n tenantId: input.tenantId,\n organizationId: normalizeScopeOrganization(input.organizationId),\n deletedAt: null,\n })\n if (!conflict || conflict.status !== 'pending' || conflict.conflictActorUserId !== input.userId) {\n return false\n }\n if ((input.resolution === 'accept_mine' || input.resolution === 'merged') && !canOverrideIncoming) {\n return false\n }\n await this.resolveConflict(conflict, input.resolution, input.userId)\n return true\n }\n\n private async canUserOverrideIncoming(\n input: Pick<RecordLockScope, 'tenantId' | 'organizationId' | 'userId'>,\n settings: RecordLockSettings,\n ): Promise<boolean> {\n if (!settings.allowIncomingOverride) return false\n if (!this.rbacService) return false\n\n try {\n return await this.rbacService.userHasAllFeatures(\n input.userId,\n ['record_locks.override_incoming'],\n {\n tenantId: input.tenantId,\n organizationId: normalizeScopeOrganization(input.organizationId),\n },\n )\n } catch {\n return false\n }\n }\n\n private async canUserForceRelease(\n input: Pick<RecordLockScope, 'tenantId' | 'organizationId' | 'userId'>,\n settings: RecordLockSettings,\n ): Promise<boolean> {\n if (!settings.allowForceUnlock) return false\n if (!this.rbacService) return false\n\n try {\n return await this.rbacService.userHasAllFeatures(\n input.userId,\n ['record_locks.force_release'],\n {\n tenantId: input.tenantId,\n organizationId: normalizeScopeOrganization(input.organizationId),\n },\n )\n } catch {\n return false\n }\n }\n\n private pruneLockCleanupState(now: number): void {\n for (const [tenantId, state] of lockCleanupStateByTenant.entries()) {\n if (!state.inFlight && now - state.lastSeenAt > LOCK_CLEANUP_STATE_TTL_MS) {\n lockCleanupStateByTenant.delete(tenantId)\n }\n }\n\n if (lockCleanupStateByTenant.size <= LOCK_CLEANUP_STATE_MAX_ENTRIES) return\n\n const removable = Array.from(lockCleanupStateByTenant.entries())\n .filter(([, state]) => !state.inFlight)\n .sort((left, right) => left[1].lastSeenAt - right[1].lastSeenAt)\n const overflow = lockCleanupStateByTenant.size - LOCK_CLEANUP_STATE_MAX_ENTRIES\n for (const [tenantId] of removable.slice(0, Math.max(0, overflow))) {\n lockCleanupStateByTenant.delete(tenantId)\n }\n }\n\n private scheduleCleanup(tenantId: string): void {\n const now = Date.now()\n this.pruneLockCleanupState(now)\n const state = lockCleanupStateByTenant.get(tenantId) ?? { lastRunAt: 0, inFlight: false, lastSeenAt: now }\n state.lastSeenAt = now\n lockCleanupStateByTenant.set(tenantId, state)\n if (state.inFlight) return\n if (now - state.lastRunAt < LOCK_CLEANUP_INTERVAL_MS) return\n\n state.inFlight = true\n state.lastRunAt = now\n lockCleanupStateByTenant.set(tenantId, state)\n\n void this.cleanupHistoricalRecords(tenantId).finally(() => {\n const current = lockCleanupStateByTenant.get(tenantId)\n if (!current) return\n current.inFlight = false\n lockCleanupStateByTenant.set(tenantId, current)\n })\n }\n\n private async cleanupHistoricalRecords(tenantId: string): Promise<void> {\n try {\n const db = getKysely(this.em)\n const now = Date.now()\n const lockCutoff = new Date(now - LOCK_RETENTION_MS)\n const resolvedConflictCutoff = new Date(now - RESOLVED_CONFLICT_RETENTION_MS)\n const pendingConflictCutoff = new Date(now - PENDING_CONFLICT_RETENTION_MS)\n const deletedAt = new Date(now)\n\n await db\n .updateTable('record_locks' as any)\n .set({\n deleted_at: deletedAt,\n updated_at: deletedAt,\n } as any)\n .where('tenant_id' as any, '=', tenantId)\n .where('deleted_at' as any, 'is', null as any)\n .where('status' as any, '!=', ACTIVE_LOCK_STATUS)\n .where('updated_at' as any, '<', lockCutoff)\n .execute()\n\n await db\n .updateTable('record_lock_conflicts' as any)\n .set({\n deleted_at: deletedAt,\n updated_at: deletedAt,\n } as any)\n .where('tenant_id' as any, '=', tenantId)\n .where('deleted_at' as any, 'is', null as any)\n .where((eb: any) => eb.or([\n eb.and([\n eb('status' as any, '=', 'pending'),\n eb('created_at' as any, '<', pendingConflictCutoff),\n ]),\n eb.and([\n eb('status' as any, '!=', 'pending'),\n eb('updated_at' as any, '<', resolvedConflictCutoff),\n ]),\n ]))\n .execute()\n } catch {\n // Best-effort cleanup must never fail lock workflows.\n }\n }\n\n private normalizeMutationHeaders(headers: Partial<RecordLockMutationHeaders>): Partial<RecordLockMutationHeaders> {\n const parsed = recordLockMutationHeaderSchema.partial().safeParse(headers)\n if (!parsed.success) return {}\n return parsed.data\n }\n\n private buildScopeWhere(scope: Pick<RecordLockScope, 'tenantId' | 'organizationId'>): {\n tenantId: string\n deletedAt: null\n organizationId?: string | null\n } {\n const where: {\n tenantId: string\n deletedAt: null\n organizationId?: string | null\n } = {\n tenantId: scope.tenantId,\n deletedAt: null,\n }\n\n if (scope.organizationId !== undefined) {\n where.organizationId = normalizeScopeOrganization(scope.organizationId)\n }\n\n return where\n }\n\n private async findActiveLocks(\n input: Pick<RecordLockScope, 'tenantId' | 'organizationId'> & RecordLockResource,\n now: Date,\n ): Promise<RecordLock[]> {\n const legacyFinder = (this as unknown as {\n findActiveLock?: (args: Pick<RecordLockScope, 'tenantId' | 'organizationId'> & RecordLockResource, at: Date) => Promise<RecordLock | null>\n }).findActiveLock\n if (typeof legacyFinder === 'function') {\n const legacyResult = await legacyFinder(input, now)\n return legacyResult ? [legacyResult] : []\n }\n\n const where: FilterQuery<RecordLock> = {\n ...this.buildScopeWhere(input),\n resourceKind: input.resourceKind,\n resourceId: input.resourceId,\n status: ACTIVE_LOCK_STATUS,\n }\n\n const locks = await this.em.find(RecordLock, where, { orderBy: { updatedAt: 'desc' } })\n if (!Array.isArray(locks) || !locks.length) return []\n\n let dirty = false\n const active: RecordLock[] = []\n const expiredLocks: RecordLock[] = []\n\n for (const lock of locks) {\n if (lock.expiresAt <= now) {\n this.markLockReleased(lock, {\n status: 'expired',\n reason: 'expired',\n releasedByUserId: lock.lockedByUserId,\n now,\n })\n dirty = true\n expiredLocks.push(lock)\n continue\n }\n\n active.push(lock)\n }\n\n if (dirty) await this.em.flush()\n if (expiredLocks.length) {\n const recipientUserIds = active.map((lock) => lock.lockedByUserId)\n for (const expiredLock of expiredLocks) {\n await emitRecordLocksEvent('record_locks.participant.left', {\n lockId: expiredLock.id,\n resourceKind: expiredLock.resourceKind,\n resourceId: expiredLock.resourceId,\n tenantId: expiredLock.tenantId,\n organizationId: expiredLock.organizationId,\n leftUserId: expiredLock.lockedByUserId,\n reason: 'expired',\n recipientUserIds,\n activeParticipantCount: active.length,\n })\n }\n }\n return active\n }\n\n private async findOwnedLockByToken(\n input: Pick<RecordLockScope, 'tenantId' | 'organizationId' | 'userId'> & RecordLockResource & { token?: string },\n ): Promise<RecordLock | null> {\n if (!input.token) return null\n\n const where: FilterQuery<RecordLock> = {\n ...this.buildScopeWhere(input),\n resourceKind: input.resourceKind,\n resourceId: input.resourceId,\n token: input.token,\n lockedByUserId: input.userId,\n status: ACTIVE_LOCK_STATUS,\n }\n\n return this.em.findOne(RecordLock, where)\n }\n\n private async findOwnedActiveLock(\n input: Pick<RecordLockScope, 'tenantId' | 'organizationId' | 'userId'> & RecordLockResource,\n ): Promise<RecordLock | null> {\n const where: FilterQuery<RecordLock> = {\n ...this.buildScopeWhere(input),\n resourceKind: input.resourceKind,\n resourceId: input.resourceId,\n lockedByUserId: input.userId,\n status: ACTIVE_LOCK_STATUS,\n }\n return this.em.findOne(RecordLock, where)\n }\n\n private async hasRecentSavedRelease(input: {\n tenantId: string\n organizationId?: string | null\n userId: string\n resourceKind: string\n resourceId: string\n now: Date\n }): Promise<boolean> {\n const cutoff = new Date(input.now.getTime() - PARTICIPANT_REJOIN_AFTER_SAVE_SUPPRESS_MS)\n const where: FilterQuery<RecordLock> = {\n tenantId: input.tenantId,\n resourceKind: input.resourceKind,\n resourceId: input.resourceId,\n lockedByUserId: input.userId,\n status: 'released',\n releaseReason: 'saved',\n deletedAt: null,\n releasedAt: { $gte: cutoff },\n }\n if (input.organizationId !== undefined) {\n where.organizationId = normalizeScopeOrganization(input.organizationId)\n }\n\n const scoped = await this.em.findOne(RecordLock, where, { orderBy: { releasedAt: 'desc' } })\n if (scoped) return true\n if (input.organizationId === undefined) return false\n\n return Boolean(await this.em.findOne(RecordLock, {\n tenantId: input.tenantId,\n resourceKind: input.resourceKind,\n resourceId: input.resourceId,\n lockedByUserId: input.userId,\n status: 'released',\n releaseReason: 'saved',\n deletedAt: null,\n releasedAt: { $gte: cutoff },\n }, { orderBy: { releasedAt: 'desc' } }))\n }\n\n private markLockReleased(\n lock: RecordLock,\n params: {\n status: RecordLockStatus\n reason: RecordLockReleaseReason\n releasedByUserId: string\n now: Date\n },\n ) {\n lock.status = params.status\n lock.releaseReason = params.reason\n lock.releasedByUserId = params.releasedByUserId\n lock.releasedAt = params.now\n lock.updatedAt = params.now\n }\n\n private async findLatestActionLog(\n input: Pick<RecordLockScope, 'tenantId' | 'organizationId'> & RecordLockResource,\n ): Promise<ActionLog | null> {\n const where: FilterQuery<ActionLog> = {\n tenantId: input.tenantId,\n resourceKind: input.resourceKind,\n resourceId: input.resourceId,\n deletedAt: null,\n }\n\n if (input.organizationId !== undefined) {\n where.organizationId = normalizeScopeOrganization(input.organizationId)\n }\n\n return this.em.findOne(ActionLog, where, { orderBy: { createdAt: 'desc' } })\n }\n\n private async findLatestActionLogWithScopeFallback(\n input: Pick<RecordLockScope, 'tenantId' | 'organizationId'> & RecordLockResource,\n ): Promise<ActionLog | null> {\n const scoped = await this.findLatestActionLog(input)\n if (scoped) return scoped\n if (input.organizationId !== null) return null\n\n return this.findLatestActionLog({\n tenantId: input.tenantId,\n resourceKind: input.resourceKind,\n resourceId: input.resourceId,\n })\n }\n\n private async findLatestActionLogByActor(\n input: Pick<RecordLockScope, 'tenantId' | 'organizationId'> & RecordLockResource,\n actorUserId: string,\n ): Promise<ActionLog | null> {\n const where: FilterQuery<ActionLog> = {\n tenantId: input.tenantId,\n resourceKind: input.resourceKind,\n resourceId: input.resourceId,\n actorUserId,\n deletedAt: null,\n }\n\n if (input.organizationId !== undefined) {\n where.organizationId = normalizeScopeOrganization(input.organizationId)\n }\n\n return this.em.findOne(ActionLog, where, { orderBy: { createdAt: 'desc' } })\n }\n\n private summarizeChangedFieldsFromActionLog(log: ActionLog | null): string {\n if (!log) return ''\n\n if (isRecordValue(log.changesJson)) {\n const fromChanges = Object.keys(log.changesJson)\n .filter((field) => !shouldSkipConflictField(field))\n .slice(0, 12)\n .map(formatChangedFieldLabel)\n .join(', ')\n if (fromChanges) return fromChanges\n }\n\n const before = isRecordValue(log.snapshotBefore) ? log.snapshotBefore : null\n const after = isRecordValue(log.snapshotAfter) ? log.snapshotAfter : null\n if (!before || !after) return ''\n\n const diffPaths = new Set<string>()\n this.collectSnapshotDiffPaths(before, after, null, diffPaths, new Set<unknown>())\n\n return Array.from(diffPaths)\n .filter((field) => !shouldSkipConflictField(field))\n .sort((left, right) => left.localeCompare(right))\n .slice(0, 12)\n .map(formatChangedFieldLabel)\n .join(', ')\n }\n\n private buildIncomingChangeRowsFromActionLog(log: ActionLog | null): Array<{\n field: string\n incoming: string\n current: string\n }> {\n if (!log || !isRecordValue(log.changesJson)) return []\n\n const rows: Array<{ field: string; incoming: string; current: string }> = []\n for (const [rawField, rawChange] of Object.entries(log.changesJson)) {\n if (rows.length >= 12) break\n if (shouldSkipConflictField(rawField)) continue\n\n const change = isRecordValue(rawChange) ? rawChange : {}\n const incoming = Object.prototype.hasOwnProperty.call(change, 'to')\n ? formatNotificationValue(change.to)\n : '-'\n const current = Object.prototype.hasOwnProperty.call(change, 'from')\n ? formatNotificationValue(change.from)\n : '-'\n\n rows.push({\n field: formatChangedFieldLabel(rawField),\n incoming,\n current,\n })\n }\n\n return rows\n }\n\n private toParticipantView(lock: RecordLock): RecordLockParticipantView {\n return {\n userId: lock.lockedByUserId,\n lockedByIp: lock.lockedByIp ?? null,\n lockedAt: normalizeDate(lock.lockedAt),\n lastHeartbeatAt: normalizeDate(lock.lastHeartbeatAt),\n expiresAt: normalizeDate(lock.expiresAt),\n }\n }\n\n private sortLocksByJoinOrder(locks: RecordLock[]): RecordLock[] {\n return [...locks].sort((left, right) => {\n const leftLockedAt = left.lockedAt instanceof Date ? left.lockedAt.getTime() : 0\n const rightLockedAt = right.lockedAt instanceof Date ? right.lockedAt.getTime() : 0\n if (leftLockedAt !== rightLockedAt) return leftLockedAt - rightLockedAt\n\n const leftCreatedAt = left.createdAt instanceof Date ? left.createdAt.getTime() : 0\n const rightCreatedAt = right.createdAt instanceof Date ? right.createdAt.getTime() : 0\n if (leftCreatedAt !== rightCreatedAt) return leftCreatedAt - rightCreatedAt\n\n return left.id.localeCompare(right.id)\n })\n }\n\n private toLockView(lock: RecordLock, includeToken: boolean, activeLocks: RecordLock[] = [lock]): RecordLockView {\n const participants = activeLocks.map((item) => this.toParticipantView(item))\n return {\n id: lock.id,\n resourceKind: lock.resourceKind,\n resourceId: lock.resourceId,\n token: includeToken ? lock.token : null,\n strategy: lock.strategy,\n status: lock.status,\n lockedByUserId: lock.lockedByUserId,\n lockedByIp: lock.lockedByIp ?? null,\n baseActionLogId: lock.baseActionLogId,\n lockedAt: normalizeDate(lock.lockedAt),\n lastHeartbeatAt: normalizeDate(lock.lastHeartbeatAt),\n expiresAt: normalizeDate(lock.expiresAt),\n participants,\n activeParticipantCount: participants.length,\n }\n }\n\n private async createConflict(input: {\n scope: Pick<RecordLockScope, 'tenantId' | 'organizationId'> & RecordLockResource\n baseActionLogId: string | null\n incomingActionLogId: string | null\n conflictActorUserId: string\n incomingActorUserId: string | null\n }): Promise<RecordLockConflict> {\n const dedupeKey = [\n 'record_locks',\n 'conflict',\n input.scope.tenantId,\n normalizeScopeOrganization(input.scope.organizationId) ?? 'global',\n input.scope.resourceKind,\n input.scope.resourceId,\n input.conflictActorUserId,\n input.baseActionLogId ?? 'none',\n input.incomingActionLogId ?? 'none',\n ].join(':')\n\n const result = await this.em.transactional(async (tx) => {\n try {\n const db = getKysely(tx as EntityManager)\n await sql`select pg_advisory_xact_lock(hashtext(${dedupeKey}))`.execute(db)\n } catch {\n // Best-effort lock; fallback to find-first behavior below.\n }\n\n const existing = await this.findPendingConflictByFingerprint(tx as EntityManager, input)\n if (existing) {\n return { conflict: existing, created: false as const }\n }\n\n const conflict = tx.create(RecordLockConflict, {\n resourceKind: input.scope.resourceKind,\n resourceId: input.scope.resourceId,\n status: 'pending',\n resolution: null,\n baseActionLogId: input.baseActionLogId,\n incomingActionLogId: input.incomingActionLogId,\n conflictActorUserId: input.conflictActorUserId,\n incomingActorUserId: input.incomingActorUserId,\n tenantId: input.scope.tenantId,\n organizationId: normalizeScopeOrganization(input.scope.organizationId),\n })\n\n tx.persist(conflict)\n await tx.flush()\n return { conflict, created: true as const }\n })\n\n if (result.created) {\n await emitRecordLocksEvent('record_locks.conflict.detected', {\n conflictId: result.conflict.id,\n resourceKind: result.conflict.resourceKind,\n resourceId: result.conflict.resourceId,\n tenantId: result.conflict.tenantId,\n organizationId: result.conflict.organizationId,\n conflictActorUserId: result.conflict.conflictActorUserId,\n incomingActorUserId: result.conflict.incomingActorUserId,\n baseActionLogId: result.conflict.baseActionLogId,\n incomingActionLogId: result.conflict.incomingActionLogId,\n })\n }\n\n return result.conflict\n }\n\n private findPendingConflictByFingerprint(\n em: EntityManager,\n input: {\n scope: Pick<RecordLockScope, 'tenantId' | 'organizationId'> & RecordLockResource\n baseActionLogId: string | null\n incomingActionLogId: string | null\n conflictActorUserId: string\n },\n ): Promise<RecordLockConflict | null> {\n return em.findOne(RecordLockConflict, {\n tenantId: input.scope.tenantId,\n organizationId: normalizeScopeOrganization(input.scope.organizationId),\n resourceKind: input.scope.resourceKind,\n resourceId: input.scope.resourceId,\n conflictActorUserId: input.conflictActorUserId,\n status: 'pending',\n baseActionLogId: input.baseActionLogId,\n incomingActionLogId: input.incomingActionLogId,\n deletedAt: null,\n }, { orderBy: { createdAt: 'desc' } })\n }\n\n private async resolveConflict(\n conflict: RecordLockConflict,\n resolution: 'accept_incoming' | Extract<RecordLockMutationHeaders['resolution'], 'accept_mine' | 'merged'>,\n resolvedByUserId: string,\n ): Promise<void> {\n const now = new Date()\n\n const resolutionMap: Record<'accept_incoming' | Extract<RecordLockMutationHeaders['resolution'], 'accept_mine' | 'merged'>, {\n status: RecordLockConflictStatus\n resolution: RecordLockConflictResolution\n }> = {\n accept_incoming: { status: 'resolved_accept_incoming', resolution: 'accept_incoming' },\n accept_mine: { status: 'resolved_accept_mine', resolution: 'accept_mine' },\n merged: { status: 'resolved_merged', resolution: 'merged' },\n }\n\n const target = resolutionMap[resolution]\n conflict.status = target.status\n conflict.resolution = target.resolution\n conflict.resolvedByUserId = resolvedByUserId\n conflict.resolvedAt = now\n conflict.updatedAt = now\n await this.em.flush()\n\n await emitRecordLocksEvent('record_locks.conflict.resolved', {\n conflictId: conflict.id,\n resourceKind: conflict.resourceKind,\n resourceId: conflict.resourceId,\n tenantId: conflict.tenantId,\n organizationId: conflict.organizationId,\n conflictActorUserId: conflict.conflictActorUserId,\n incomingActorUserId: conflict.incomingActorUserId,\n resolution: conflict.resolution,\n resolvedByUserId,\n })\n }\n\n private async findConflictById(\n conflictId: string,\n scope: Pick<RecordLockScope, 'tenantId' | 'organizationId'> & RecordLockResource,\n ): Promise<RecordLockConflict | null> {\n const where: FilterQuery<RecordLockConflict> = {\n id: conflictId,\n tenantId: scope.tenantId,\n resourceKind: scope.resourceKind,\n resourceId: scope.resourceId,\n deletedAt: null,\n }\n\n if (scope.organizationId !== undefined) {\n where.organizationId = normalizeScopeOrganization(scope.organizationId)\n }\n\n const scoped = await this.em.findOne(RecordLockConflict, where)\n if (scoped || scope.organizationId === undefined) return scoped\n\n return this.em.findOne(RecordLockConflict, {\n id: conflictId,\n tenantId: scope.tenantId,\n resourceKind: scope.resourceKind,\n resourceId: scope.resourceId,\n deletedAt: null,\n })\n }\n\n private async findActionLogById(\n logId: string | null,\n scope: Pick<RecordLockScope, 'tenantId' | 'organizationId'> & RecordLockResource,\n ): Promise<ActionLog | null> {\n if (!logId) return null\n\n let resolved = this.actionLogService\n ? await this.actionLogService.findById(logId)\n : null\n if (!resolved) {\n resolved = await this.em.findOne(ActionLog, { id: logId, deletedAt: null })\n }\n if (!resolved || resolved.deletedAt) return null\n\n if (resolved.tenantId !== scope.tenantId) return null\n\n if (scope.organizationId !== undefined) {\n const expectedOrganizationId = normalizeScopeOrganization(scope.organizationId)\n if (normalizeScopeOrganization(resolved.organizationId) !== expectedOrganizationId) return null\n }\n\n if (resolved.resourceKind !== scope.resourceKind || resolved.resourceId !== scope.resourceId) {\n return null\n }\n\n return resolved\n }\n\n private collectSnapshotDiffPaths(\n before: unknown,\n after: unknown,\n pathPrefix: string | null,\n output: Set<string>,\n seen: Set<unknown>,\n ): void {\n if (valuesEqual(before, after)) return\n\n const beforeRecord = isRecordValue(before) ? before : null\n const afterRecord = isRecordValue(after) ? after : null\n\n if (!beforeRecord || !afterRecord) {\n if (pathPrefix) output.add(pathPrefix)\n return\n }\n\n if (seen.has(beforeRecord) || seen.has(afterRecord)) {\n if (pathPrefix) output.add(pathPrefix)\n return\n }\n\n seen.add(beforeRecord)\n seen.add(afterRecord)\n\n const keys = new Set([...Object.keys(beforeRecord), ...Object.keys(afterRecord)])\n for (const key of keys) {\n if (SKIPPED_CONFLICT_FIELDS.has(key)) continue\n const nextPath = pathPrefix ? `${pathPrefix}.${key}` : key\n this.collectSnapshotDiffPaths(beforeRecord[key], afterRecord[key], nextPath, output, seen)\n }\n }\n\n private async buildConflictChanges(\n conflict: RecordLockConflict,\n mutationPayload: Record<string, unknown> | null,\n ): Promise<RecordLockConflictChange[]> {\n const scope = {\n tenantId: conflict.tenantId,\n organizationId: conflict.organizationId,\n resourceKind: conflict.resourceKind,\n resourceId: conflict.resourceId,\n }\n\n const baseLog = await this.findActionLogById(conflict.baseActionLogId, scope)\n const incomingLog = await this.findActionLogById(conflict.incomingActionLogId, scope)\n\n const baseSnapshot = isRecordValue(baseLog?.snapshotAfter) ? baseLog.snapshotAfter : null\n const incomingBeforeSnapshot = isRecordValue(incomingLog?.snapshotBefore) ? incomingLog.snapshotBefore : null\n const incomingAfterSnapshot = isRecordValue(incomingLog?.snapshotAfter) ? incomingLog.snapshotAfter : null\n const fallbackBaseSnapshot = baseSnapshot ?? incomingBeforeSnapshot\n\n const changeMap = new Map<string, { baseValue: unknown; incomingValue: unknown }>()\n\n const incomingChanges = isRecordValue(incomingLog?.changesJson) ? incomingLog.changesJson : null\n if (incomingChanges) {\n for (const [fieldPathRaw, rawChange] of Object.entries(incomingChanges)) {\n const fieldPath = fieldPathRaw.trim()\n if (shouldSkipConflictField(fieldPath)) continue\n\n const changeRecord = isRecordValue(rawChange) ? rawChange : null\n const fromValue = changeRecord && Object.prototype.hasOwnProperty.call(changeRecord, 'from')\n ? changeRecord.from\n : readPathValueLoose(fallbackBaseSnapshot, fieldPath)\n const toValue = changeRecord && Object.prototype.hasOwnProperty.call(changeRecord, 'to')\n ? changeRecord.to\n : readPathValueLoose(incomingAfterSnapshot, fieldPath)\n\n changeMap.set(fieldPath, {\n baseValue: fromValue === MISSING_CONFLICT_VALUE ? null : normalizeConflictValue(fromValue),\n incomingValue: toValue === MISSING_CONFLICT_VALUE ? null : normalizeConflictValue(toValue),\n })\n }\n }\n\n if (!changeMap.size && fallbackBaseSnapshot && incomingAfterSnapshot) {\n const diffPaths = new Set<string>()\n this.collectSnapshotDiffPaths(\n fallbackBaseSnapshot,\n incomingAfterSnapshot,\n null,\n diffPaths,\n new Set<unknown>(),\n )\n\n for (const fieldPath of diffPaths) {\n if (shouldSkipConflictField(fieldPath)) continue\n const fromValue = readPathValueLoose(fallbackBaseSnapshot, fieldPath)\n const toValue = readPathValueLoose(incomingAfterSnapshot, fieldPath)\n changeMap.set(fieldPath, {\n baseValue: fromValue === MISSING_CONFLICT_VALUE ? null : normalizeConflictValue(fromValue),\n incomingValue: toValue === MISSING_CONFLICT_VALUE ? null : normalizeConflictValue(toValue),\n })\n }\n }\n\n if (!changeMap.size && mutationPayload && incomingAfterSnapshot) {\n for (const fieldPath of Object.keys(mutationPayload)) {\n if (shouldSkipConflictField(fieldPath)) continue\n const mineValue = readPathValueLoose(mutationPayload, fieldPath)\n const incomingValue = readPathValueLoose(incomingAfterSnapshot, fieldPath)\n if (mineValue === MISSING_CONFLICT_VALUE || incomingValue === MISSING_CONFLICT_VALUE) continue\n if (valuesEqual(mineValue, incomingValue)) continue\n const baseValue = readPathValueLoose(fallbackBaseSnapshot, fieldPath)\n changeMap.set(fieldPath, {\n baseValue: baseValue === MISSING_CONFLICT_VALUE ? null : normalizeConflictValue(baseValue),\n incomingValue: normalizeConflictValue(incomingValue),\n })\n }\n }\n\n if (!changeMap.size) return []\n\n const allFields = Array.from(changeMap.keys())\n const preferredFields = mutationPayload\n ? allFields.filter((fieldPath) => {\n const mineValue = readPathValueLoose(mutationPayload, fieldPath)\n if (mineValue === MISSING_CONFLICT_VALUE) return false\n const incomingValue = changeMap.get(fieldPath)?.incomingValue\n return !valuesEqual(mineValue, incomingValue)\n })\n : []\n const selectedFields = (preferredFields.length ? preferredFields : allFields)\n .filter((fieldPath) => !shouldSkipConflictField(fieldPath))\n .sort((left, right) => left.localeCompare(right))\n .slice(0, 25)\n\n return selectedFields.map((fieldPath) => {\n const entry = changeMap.get(fieldPath) ?? { baseValue: null, incomingValue: null }\n const mineValueRaw = mutationPayload ? readPathValueLoose(mutationPayload, fieldPath) : MISSING_CONFLICT_VALUE\n const mineValue = mineValueRaw === MISSING_CONFLICT_VALUE\n ? entry.baseValue\n : normalizeConflictValue(mineValueRaw)\n\n return {\n field: fieldPath,\n displayValue: normalizeConflictValue(entry.baseValue),\n baseValue: normalizeConflictValue(entry.baseValue),\n incomingValue: normalizeConflictValue(entry.incomingValue),\n mineValue: normalizeConflictValue(mineValue),\n }\n })\n }\n\n private async toConflictPayload(\n conflict: RecordLockConflict,\n mutationPayload: Record<string, unknown> | null,\n allowIncomingOverride: boolean,\n canOverrideIncoming: boolean,\n ): Promise<RecordLockConflictPayload> {\n const changes = await this.buildConflictChanges(conflict, mutationPayload)\n return {\n id: conflict.id,\n resourceKind: conflict.resourceKind,\n resourceId: conflict.resourceId,\n baseActionLogId: conflict.baseActionLogId,\n incomingActionLogId: conflict.incomingActionLogId,\n allowIncomingOverride,\n canOverrideIncoming,\n resolutionOptions: canOverrideIncoming ? ['accept_mine'] : [],\n changes,\n }\n }\n}\n\nexport function createRecordLockService(deps: RecordLockServiceDeps): RecordLockService {\n return new RecordLockService(deps)\n}\n"],
|
|
5
|
-
"mappings": "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",
|
|
4
|
+
"sourcesContent": ["import { randomUUID } from 'crypto'\nimport { UniqueConstraintViolationException, type FilterQuery } from '@mikro-orm/core'\nimport type { EntityManager } from '@mikro-orm/postgresql'\nimport { type Kysely, sql } from 'kysely'\nimport type { ModuleConfigService } from '@open-mercato/core/modules/configs/lib/module-config-service'\nimport { ActionLog } from '@open-mercato/core/modules/audit_logs/data/entities'\nimport type { ActionLogService } from '@open-mercato/core/modules/audit_logs/services/actionLogService'\nimport type { RbacService } from '@open-mercato/core/modules/auth/services/rbacService'\nimport { findOneWithDecryption } from '@open-mercato/shared/lib/encryption/find'\nimport { parseDecryptedFieldValue } from '@open-mercato/shared/lib/encryption/tenantDataEncryptionService'\nimport { emitRecordLocksEvent } from '../events'\nimport {\n RecordLock,\n RecordLockConflict,\n type RecordLockStatus,\n type RecordLockReleaseReason,\n type RecordLockConflictResolution,\n type RecordLockConflictStatus,\n} from '../data/entities'\nimport {\n recordLockMutationHeaderSchema,\n type RecordLockMutationHeaders,\n type RecordLockReleaseInput as RecordLockReleasePayloadInput,\n type RecordLockSettingsInput,\n} from '../data/validators'\nimport {\n DEFAULT_RECORD_LOCK_SETTINGS,\n RECORD_LOCKS_MODULE_ID,\n RECORD_LOCKS_SETTINGS_NAME,\n isRecordLockingEnabledForResource,\n normalizeRecordLockSettings,\n type RecordLockSettings,\n type RecordLockStrategy,\n} from './config'\n\nconst ACTIVE_LOCK_STATUS: RecordLockStatus = 'active'\nconst ACTIVE_SCOPE_UNIQUE_CONSTRAINTS = new Set([\n 'record_locks_active_scope_org_unique',\n 'record_locks_active_scope_tenant_unique',\n 'record_locks_active_scope_user_org_unique',\n 'record_locks_active_scope_user_tenant_unique',\n])\nconst LOCK_CONTENTION_EVENT_TTL_MS = 15_000\nconst PARTICIPANT_REJOIN_AFTER_SAVE_SUPPRESS_MS = 20_000\nconst LOCK_CONTENTION_EVENT_MAX_ENTRIES = 2_000\nconst lockContentionEventThrottle = new Map<string, number>()\nconst LOCK_CLEANUP_INTERVAL_MS = 5 * 60 * 1000\nconst LOCK_RETENTION_MS = 3 * 24 * 60 * 60 * 1000\nconst RESOLVED_CONFLICT_RETENTION_MS = 7 * 24 * 60 * 60 * 1000\nconst PENDING_CONFLICT_RETENTION_MS = 24 * 60 * 60 * 1000\nconst LOCK_CLEANUP_STATE_TTL_MS = 24 * 60 * 60 * 1000\nconst LOCK_CLEANUP_STATE_MAX_ENTRIES = 2_000\nconst lockCleanupStateByTenant = new Map<string, { lastRunAt: number; inFlight: boolean; lastSeenAt: number }>()\n\nexport type RecordLockScope = {\n tenantId: string\n organizationId?: string | null\n userId: string\n}\n\nexport type RecordLockResource = {\n resourceKind: string\n resourceId: string\n}\n\nexport type RecordLockAcquireInput = RecordLockScope & RecordLockResource & {\n lockedByIp?: string | null\n}\n\nexport type RecordLockHeartbeatInput = RecordLockScope & RecordLockResource & {\n token: string\n}\n\nexport type RecordLockReleaseInput = RecordLockScope & RecordLockResource & {\n token?: string\n reason?: Exclude<RecordLockReleaseReason, 'expired' | 'force'>\n} & Pick<RecordLockReleasePayloadInput, 'conflictId' | 'resolution'>\n\nexport type RecordLockForceReleaseInput = RecordLockScope & RecordLockResource & {\n reason?: string | null\n}\n\nexport type RecordLockMutationValidationInput = RecordLockScope & RecordLockResource & {\n method: 'PUT' | 'DELETE'\n headers: Partial<RecordLockMutationHeaders>\n mutationPayload?: Record<string, unknown> | null\n}\n\nexport type RecordLockConflictChange = {\n field: string\n displayValue: unknown\n baseValue: unknown\n incomingValue: unknown\n mineValue: unknown\n}\n\nexport type RecordLockConflictPayload = {\n id: string\n resourceKind: string\n resourceId: string\n baseActionLogId: string | null\n incomingActionLogId: string | null\n allowIncomingOverride: boolean\n canOverrideIncoming: boolean\n resolutionOptions: Array<'accept_mine'>\n changes: RecordLockConflictChange[]\n}\n\nexport type RecordLockView = {\n id: string\n resourceKind: string\n resourceId: string\n token: string | null\n strategy: RecordLockStrategy\n status: RecordLockStatus\n lockedByUserId: string\n lockedByIp: string | null\n baseActionLogId: string | null\n lockedAt: string\n lastHeartbeatAt: string\n expiresAt: string\n participants: RecordLockParticipantView[]\n activeParticipantCount: number\n}\n\nexport type RecordLockParticipantView = {\n userId: string\n lockedByIp: string | null\n lockedAt: string\n lastHeartbeatAt: string\n expiresAt: string\n}\n\nexport type RecordLockAcquireResult = {\n ok: true\n enabled: boolean\n resourceEnabled: boolean\n strategy: RecordLockStrategy\n allowForceUnlock: boolean\n heartbeatSeconds: number\n acquired: boolean\n latestActionLogId: string | null\n lock: RecordLockView | null\n}\n\nexport type RecordLockAcquireFailure = RecordLockValidationFailure & {\n allowForceUnlock: boolean\n}\n\nexport type RecordLockHeartbeatResult = {\n ok: true\n expiresAt: string | null\n}\n\nexport type RecordLockReleaseResult = {\n ok: true\n released: boolean\n conflictResolved: boolean\n}\n\nexport type RecordLockForceReleaseResult = {\n ok: true\n released: boolean\n lock: RecordLockView | null\n}\n\nexport type RecordLockValidationSuccess = {\n ok: true\n enabled: boolean\n resourceEnabled: boolean\n strategy: RecordLockStrategy\n shouldReleaseOnSuccess: boolean\n lock: RecordLockView | null\n latestActionLogId: string | null\n}\n\nexport type RecordLockValidationFailure = {\n ok: false\n status: 409 | 423\n error: string\n code: 'record_lock_conflict' | 'record_locked'\n lock: RecordLockView | null\n conflict?: RecordLockConflictPayload\n}\n\nexport type RecordLockValidationResult = RecordLockValidationSuccess | RecordLockValidationFailure\n\nexport type RecordLockServiceDeps = {\n em: EntityManager\n moduleConfigService?: ModuleConfigService | null\n actionLogService?: ActionLogService | null\n rbacService?: RbacService | null\n}\n\nexport type ParsedRecordLockHeaders = Partial<RecordLockMutationHeaders>\n\nfunction normalizeDate(value: Date): string {\n return value.toISOString()\n}\n\nfunction trimToNull(value: string | null | undefined): string | null {\n if (typeof value !== 'string') return null\n const trimmed = value.trim()\n return trimmed.length ? trimmed : null\n}\n\nfunction normalizeScopeOrganization(value: string | null | undefined): string | null {\n const trimmed = trimToNull(value)\n return trimmed ?? null\n}\n\nfunction shouldEmitLockContentionEvent(input: {\n tenantId: string\n organizationId?: string | null\n resourceKind: string\n resourceId: string\n lockedByUserId: string\n attemptedByUserId: string\n}): boolean {\n if (process.env.NODE_ENV === 'test') return true\n const now = Date.now()\n const key = [\n input.tenantId,\n normalizeScopeOrganization(input.organizationId) ?? 'global',\n input.resourceKind,\n input.resourceId,\n input.lockedByUserId,\n input.attemptedByUserId,\n ].join('|')\n\n const lastEmittedAt = lockContentionEventThrottle.get(key)\n if (typeof lastEmittedAt === 'number' && now - lastEmittedAt < LOCK_CONTENTION_EVENT_TTL_MS) {\n return false\n }\n\n lockContentionEventThrottle.set(key, now)\n\n for (const [cachedKey, cachedAt] of lockContentionEventThrottle.entries()) {\n if (now - cachedAt > LOCK_CONTENTION_EVENT_TTL_MS) {\n lockContentionEventThrottle.delete(cachedKey)\n }\n }\n if (lockContentionEventThrottle.size > LOCK_CONTENTION_EVENT_MAX_ENTRIES) {\n const oldest = Array.from(lockContentionEventThrottle.entries())\n .sort((left, right) => left[1] - right[1])\n .slice(0, lockContentionEventThrottle.size - LOCK_CONTENTION_EVENT_MAX_ENTRIES)\n for (const [staleKey] of oldest) lockContentionEventThrottle.delete(staleKey)\n }\n\n return true\n}\n\nfunction isActiveLockScopeUniqueViolation(error: unknown): boolean {\n if (error instanceof UniqueConstraintViolationException) {\n const errorWithConstraint = error as unknown as { constraint?: unknown }\n const constraint = typeof errorWithConstraint.constraint === 'string'\n ? errorWithConstraint.constraint\n : null\n if (constraint && ACTIVE_SCOPE_UNIQUE_CONSTRAINTS.has(constraint)) return true\n }\n if (!error || typeof error !== 'object') return false\n const code = (error as { code?: unknown }).code\n if (code !== '23505') return false\n const message = typeof (error as { message?: unknown }).message === 'string'\n ? (error as { message: string }).message.toLowerCase()\n : ''\n for (const constraint of ACTIVE_SCOPE_UNIQUE_CONSTRAINTS) {\n if (message.includes(constraint)) return true\n }\n return false\n}\n\nfunction getKysely(em: EntityManager): Kysely<any> {\n return (em as unknown as { getKysely: () => Kysely<any> }).getKysely()\n}\n\nconst SKIPPED_CONFLICT_FIELDS = new Set([\n 'updatedAt',\n 'updated_at',\n 'createdAt',\n 'created_at',\n 'deletedAt',\n 'deleted_at',\n])\n\nfunction shouldSkipConflictField(path: string): boolean {\n if (!path.trim().length) return true\n if (SKIPPED_CONFLICT_FIELDS.has(path)) return true\n const segments = path.split('.').filter((segment) => segment.length > 0)\n if (!segments.length) return true\n return SKIPPED_CONFLICT_FIELDS.has(segments[segments.length - 1] ?? '')\n}\n\nconst MISSING_CONFLICT_VALUE = Symbol('record_lock_conflict_missing_value')\n\nfunction isRecordValue(value: unknown): value is Record<string, unknown> {\n return Boolean(value && typeof value === 'object' && !Array.isArray(value))\n}\n\nfunction parseDecryptedJsonLike(value: unknown): unknown {\n return typeof value === 'string' ? parseDecryptedFieldValue(value) : value\n}\n\nfunction readJsonRecordValue(value: unknown): Record<string, unknown> | null {\n const parsed = parseDecryptedJsonLike(value)\n return isRecordValue(parsed) ? parsed : null\n}\n\nfunction readActionLogChangesJson(log: ActionLog | null): Record<string, unknown> | null {\n return readJsonRecordValue(log?.changesJson ?? null)\n}\n\nfunction normalizeActionLogPayload(log: ActionLog | null): ActionLog | null {\n if (!log) return null\n log.changesJson = readJsonRecordValue(log.changesJson)\n log.snapshotBefore = parseDecryptedJsonLike(log.snapshotBefore)\n log.snapshotAfter = parseDecryptedJsonLike(log.snapshotAfter)\n log.contextJson = readJsonRecordValue(log.contextJson)\n log.commandPayload = parseDecryptedJsonLike(log.commandPayload)\n return log\n}\n\nfunction toIsoDate(value: unknown): string | null {\n if (value instanceof Date) {\n if (Number.isNaN(value.getTime())) return null\n return value.toISOString()\n }\n if (typeof value === 'string') {\n const parsed = new Date(value)\n return Number.isNaN(parsed.getTime()) ? null : parsed.toISOString()\n }\n return null\n}\n\nfunction valuesEqual(a: unknown, b: unknown, seen?: Set<unknown>): boolean {\n if (Object.is(a, b)) return true\n\n if (a instanceof Date || b instanceof Date) {\n const left = toIsoDate(a)\n const right = toIsoDate(b)\n return left !== null && right !== null && left === right\n }\n\n if (Array.isArray(a) && Array.isArray(b)) {\n if (a.length !== b.length) return false\n for (let index = 0; index < a.length; index += 1) {\n if (!valuesEqual(a[index], b[index], seen)) return false\n }\n return true\n }\n\n if (isRecordValue(a) && isRecordValue(b)) {\n if (!seen) seen = new Set()\n if (seen.has(a) || seen.has(b)) return false\n seen.add(a)\n seen.add(b)\n const aKeys = Object.keys(a)\n const bKeys = Object.keys(b)\n if (aKeys.length !== bKeys.length) return false\n for (const key of aKeys) {\n if (!Object.prototype.hasOwnProperty.call(b, key)) return false\n if (!valuesEqual(a[key], b[key], seen)) return false\n }\n return true\n }\n\n return false\n}\n\nfunction readPathValue(source: unknown, path: string): unknown | typeof MISSING_CONFLICT_VALUE {\n if (!path.trim().length || !isRecordValue(source)) return MISSING_CONFLICT_VALUE\n if (Object.prototype.hasOwnProperty.call(source, path)) return source[path]\n\n const segments = path.split('.').filter((segment) => segment.length > 0)\n if (!segments.length) return MISSING_CONFLICT_VALUE\n\n let current: unknown = source\n for (const segment of segments) {\n if (!isRecordValue(current)) return MISSING_CONFLICT_VALUE\n if (!Object.prototype.hasOwnProperty.call(current, segment)) return MISSING_CONFLICT_VALUE\n current = current[segment]\n }\n return current\n}\n\nfunction buildPathVariants(path: string): string[] {\n const trimmed = path.trim()\n if (!trimmed.length) return []\n\n const segments = trimmed.split('.').filter((segment) => segment.length > 0)\n if (segments.length <= 1) return [trimmed]\n\n const variants = new Set<string>([trimmed])\n for (let index = 1; index < segments.length; index += 1) {\n variants.add(segments.slice(index).join('.'))\n }\n return Array.from(variants)\n}\n\nfunction readPathValueLoose(source: unknown, path: string): unknown | typeof MISSING_CONFLICT_VALUE {\n const variants = buildPathVariants(path)\n for (const variant of variants) {\n const value = readPathValue(source, variant)\n if (value !== MISSING_CONFLICT_VALUE) return value\n }\n return MISSING_CONFLICT_VALUE\n}\n\nfunction normalizeConflictValue(value: unknown): unknown {\n return value === undefined ? null : value\n}\n\nfunction formatNotificationValue(value: unknown): string {\n if (value === null || value === undefined) return '-'\n if (typeof value === 'string') return value\n if (typeof value === 'number' || typeof value === 'boolean') return String(value)\n if (value instanceof Date) return value.toISOString()\n try {\n return JSON.stringify(value)\n } catch {\n return String(value)\n }\n}\n\nfunction formatChangedFieldLabel(rawField: string): string {\n const trimmedField = rawField.trim()\n const withoutNamespace = trimmedField.includes('::') ? (trimmedField.split('::').pop() ?? trimmedField) : trimmedField\n const withoutPrefix = withoutNamespace.includes('.') ? (withoutNamespace.split('.').pop() ?? withoutNamespace) : withoutNamespace\n const words = withoutPrefix\n .replace(/([a-z0-9])([A-Z])/g, '$1 $2')\n .replace(/[._-]+/g, ' ')\n .replace(/\\s+/g, ' ')\n .trim()\n .split(' ')\n .filter(Boolean)\n\n if (!words.length) return trimmedField\n return words\n .map((word) => word.charAt(0).toUpperCase() + word.slice(1))\n .join(' ')\n}\n\nexport function readRecordLockHeaders(headers: Headers): ParsedRecordLockHeaders {\n const raw = {\n resourceKind: trimToNull(headers.get('x-om-record-lock-kind')) ?? undefined,\n resourceId: trimToNull(headers.get('x-om-record-lock-resource-id')) ?? undefined,\n token: trimToNull(headers.get('x-om-record-lock-token')) ?? undefined,\n baseLogId: trimToNull(headers.get('x-om-record-lock-base-log-id')) ?? undefined,\n resolution: trimToNull(headers.get('x-om-record-lock-resolution')) ?? undefined,\n conflictId: trimToNull(headers.get('x-om-record-lock-conflict-id')) ?? undefined,\n }\n\n const parsed = recordLockMutationHeaderSchema.partial().safeParse(raw)\n if (!parsed.success) return {}\n return parsed.data\n}\n\nexport class RecordLockService {\n private readonly em: EntityManager\n\n private readonly moduleConfigService: ModuleConfigService | null\n\n private readonly actionLogService: ActionLogService | null\n\n private readonly rbacService: RbacService | null\n\n constructor(deps: RecordLockServiceDeps) {\n this.em = deps.em\n this.moduleConfigService = deps.moduleConfigService ?? null\n this.actionLogService = deps.actionLogService ?? null\n this.rbacService = deps.rbacService ?? null\n }\n\n async getSettings(): Promise<RecordLockSettings> {\n if (!this.moduleConfigService) return DEFAULT_RECORD_LOCK_SETTINGS\n\n const value = await this.moduleConfigService.getValue<RecordLockSettings>(\n RECORD_LOCKS_MODULE_ID,\n RECORD_LOCKS_SETTINGS_NAME,\n { defaultValue: DEFAULT_RECORD_LOCK_SETTINGS },\n )\n\n return normalizeRecordLockSettings(value ?? DEFAULT_RECORD_LOCK_SETTINGS)\n }\n\n async saveSettings(input: RecordLockSettingsInput): Promise<RecordLockSettings> {\n const settings = normalizeRecordLockSettings(input)\n if (!this.moduleConfigService) return settings\n\n await this.moduleConfigService.setValue(RECORD_LOCKS_MODULE_ID, RECORD_LOCKS_SETTINGS_NAME, settings)\n return settings // NOSONAR \u2014 both paths return settings by design; the branch controls persistence\n }\n\n async acquire(input: RecordLockAcquireInput): Promise<RecordLockAcquireResult | RecordLockAcquireFailure> {\n this.scheduleCleanup(input.tenantId)\n const settings = await this.getSettings()\n const latest = await this.findLatestActionLogWithScopeFallback(input)\n const resourceEnabled = isRecordLockingEnabledForResource(settings, input.resourceKind)\n\n if (!resourceEnabled) {\n return {\n ok: true,\n enabled: settings.enabled,\n resourceEnabled: false,\n strategy: settings.strategy,\n allowForceUnlock: settings.allowForceUnlock,\n heartbeatSeconds: settings.heartbeatSeconds,\n acquired: false,\n latestActionLogId: latest?.id ?? null,\n lock: null,\n }\n }\n\n const now = new Date()\n let activeLocks = await this.findActiveLocks(input, now)\n const ownedActiveLock = activeLocks.find((lock) => lock.lockedByUserId === input.userId) ?? null\n const competingActiveLock = activeLocks.find((lock) => lock.lockedByUserId !== input.userId) ?? null\n\n if (settings.strategy === 'pessimistic' && !ownedActiveLock && competingActiveLock) {\n const lockView = this.toLockView(competingActiveLock, false, activeLocks)\n if (shouldEmitLockContentionEvent({\n tenantId: competingActiveLock.tenantId,\n organizationId: competingActiveLock.organizationId,\n resourceKind: competingActiveLock.resourceKind,\n resourceId: competingActiveLock.resourceId,\n lockedByUserId: competingActiveLock.lockedByUserId,\n attemptedByUserId: input.userId,\n })) {\n await emitRecordLocksEvent('record_locks.lock.contended', {\n lockId: competingActiveLock.id,\n resourceKind: competingActiveLock.resourceKind,\n resourceId: competingActiveLock.resourceId,\n tenantId: competingActiveLock.tenantId,\n organizationId: competingActiveLock.organizationId,\n lockedByUserId: competingActiveLock.lockedByUserId,\n attemptedByUserId: input.userId,\n })\n }\n return {\n ok: false,\n status: 423,\n error: 'Record is currently locked by another user',\n code: 'record_locked',\n allowForceUnlock: settings.allowForceUnlock,\n lock: lockView,\n }\n }\n\n if (ownedActiveLock) {\n ownedActiveLock.strategy = settings.strategy\n ownedActiveLock.lockedByIp = input.lockedByIp ?? ownedActiveLock.lockedByIp ?? null\n ownedActiveLock.lastHeartbeatAt = now\n ownedActiveLock.expiresAt = new Date(now.getTime() + settings.timeoutSeconds * 1000)\n await this.em.flush()\n\n activeLocks = await this.findActiveLocks(input, now)\n return {\n ok: true,\n enabled: settings.enabled,\n resourceEnabled: true,\n strategy: settings.strategy,\n allowForceUnlock: settings.allowForceUnlock,\n heartbeatSeconds: settings.heartbeatSeconds,\n acquired: false,\n latestActionLogId: latest?.id ?? null,\n lock: this.toLockView(ownedActiveLock, true, activeLocks),\n }\n }\n\n const lock = this.em.create(RecordLock, {\n resourceKind: input.resourceKind,\n resourceId: input.resourceId,\n token: randomUUID(),\n strategy: settings.strategy,\n status: ACTIVE_LOCK_STATUS,\n lockedByUserId: input.userId,\n lockedByIp: input.lockedByIp ?? null,\n baseActionLogId: latest?.id ?? null,\n lockedAt: now,\n lastHeartbeatAt: now,\n expiresAt: new Date(now.getTime() + settings.timeoutSeconds * 1000),\n tenantId: input.tenantId,\n organizationId: normalizeScopeOrganization(input.organizationId),\n })\n\n this.em.persist(lock)\n let createdNewLock = true\n try {\n await this.em.flush()\n } catch (error) {\n if (!isActiveLockScopeUniqueViolation(error)) throw error\n const clear = (this.em as { clear?: () => void }).clear\n if (typeof clear === 'function') clear.call(this.em)\n const locksAfterCollision = await this.findActiveLocks(input, now)\n const competingAfterCollision = locksAfterCollision.find((item) => item.lockedByUserId !== input.userId) ?? null\n if (settings.strategy === 'pessimistic' && competingAfterCollision) {\n if (shouldEmitLockContentionEvent({\n tenantId: competingAfterCollision.tenantId,\n organizationId: competingAfterCollision.organizationId,\n resourceKind: competingAfterCollision.resourceKind,\n resourceId: competingAfterCollision.resourceId,\n lockedByUserId: competingAfterCollision.lockedByUserId,\n attemptedByUserId: input.userId,\n })) {\n await emitRecordLocksEvent('record_locks.lock.contended', {\n lockId: competingAfterCollision.id,\n resourceKind: competingAfterCollision.resourceKind,\n resourceId: competingAfterCollision.resourceId,\n tenantId: competingAfterCollision.tenantId,\n organizationId: competingAfterCollision.organizationId,\n lockedByUserId: competingAfterCollision.lockedByUserId,\n attemptedByUserId: input.userId,\n })\n }\n return {\n ok: false,\n status: 423,\n error: 'Record is currently locked by another user',\n code: 'record_locked',\n allowForceUnlock: settings.allowForceUnlock,\n lock: this.toLockView(competingAfterCollision, false, locksAfterCollision),\n }\n }\n const existingOwned = await this.findOwnedActiveLock(input)\n if (!existingOwned) throw error\n existingOwned.strategy = settings.strategy\n existingOwned.lockedByIp = input.lockedByIp ?? existingOwned.lockedByIp ?? null\n existingOwned.lastHeartbeatAt = now\n existingOwned.expiresAt = new Date(now.getTime() + settings.timeoutSeconds * 1000)\n await this.em.flush()\n createdNewLock = false\n }\n\n const activeAfterAcquire = await this.findActiveLocks(input, now)\n const ownedAfterAcquire = activeAfterAcquire.find((item) => item.lockedByUserId === input.userId)\n ?? await this.findOwnedActiveLock(input)\n ?? lock\n ?? null\n if (!ownedAfterAcquire) {\n const fallbackLock = activeAfterAcquire[0] ?? null\n return {\n ok: true,\n enabled: settings.enabled,\n resourceEnabled: true,\n strategy: settings.strategy,\n allowForceUnlock: settings.allowForceUnlock,\n heartbeatSeconds: settings.heartbeatSeconds,\n acquired: false,\n latestActionLogId: latest?.id ?? null,\n lock: fallbackLock ? this.toLockView(fallbackLock, false, activeAfterAcquire) : null,\n }\n }\n\n if (createdNewLock) {\n await emitRecordLocksEvent('record_locks.lock.acquired', {\n lockId: ownedAfterAcquire.id,\n resourceKind: ownedAfterAcquire.resourceKind,\n resourceId: ownedAfterAcquire.resourceId,\n tenantId: ownedAfterAcquire.tenantId,\n organizationId: ownedAfterAcquire.organizationId,\n lockedByUserId: ownedAfterAcquire.lockedByUserId,\n strategy: ownedAfterAcquire.strategy,\n baseActionLogId: ownedAfterAcquire.baseActionLogId,\n activeParticipantCount: activeAfterAcquire.length,\n })\n\n const recipientUserIds = activeAfterAcquire\n .filter((item) => item.lockedByUserId !== input.userId)\n .map((item) => item.lockedByUserId)\n const shouldSuppressJoinNotification = await this.hasRecentSavedRelease({\n tenantId: input.tenantId,\n organizationId: input.organizationId,\n userId: input.userId,\n resourceKind: input.resourceKind,\n resourceId: input.resourceId,\n now,\n })\n\n if (!shouldSuppressJoinNotification) {\n await emitRecordLocksEvent('record_locks.participant.joined', {\n lockId: ownedAfterAcquire.id,\n resourceKind: ownedAfterAcquire.resourceKind,\n resourceId: ownedAfterAcquire.resourceId,\n tenantId: ownedAfterAcquire.tenantId,\n organizationId: ownedAfterAcquire.organizationId,\n joinedUserId: input.userId,\n joinedIp: ownedAfterAcquire.lockedByIp ?? null,\n recipientUserIds,\n activeParticipantCount: activeAfterAcquire.length,\n })\n }\n }\n\n return {\n ok: true,\n enabled: settings.enabled,\n resourceEnabled: true,\n strategy: settings.strategy,\n allowForceUnlock: settings.allowForceUnlock,\n heartbeatSeconds: settings.heartbeatSeconds,\n acquired: createdNewLock,\n latestActionLogId: latest?.id ?? null,\n lock: this.toLockView(ownedAfterAcquire, true, activeAfterAcquire),\n }\n }\n\n async heartbeat(input: RecordLockHeartbeatInput): Promise<RecordLockHeartbeatResult> {\n const settings = await this.getSettings()\n const resourceEnabled = isRecordLockingEnabledForResource(settings, input.resourceKind)\n if (!resourceEnabled) return { ok: true, expiresAt: null }\n\n const lock = await this.findOwnedLockByToken(input)\n if (!lock) return { ok: true, expiresAt: null }\n\n const now = new Date()\n if (lock.expiresAt <= now) {\n this.markLockReleased(lock, {\n status: 'expired',\n reason: 'expired',\n releasedByUserId: lock.lockedByUserId,\n now,\n })\n await this.em.flush()\n return { ok: true, expiresAt: null }\n }\n\n lock.lastHeartbeatAt = now\n lock.expiresAt = new Date(now.getTime() + settings.timeoutSeconds * 1000)\n await this.em.flush()\n return { ok: true, expiresAt: normalizeDate(lock.expiresAt) }\n }\n\n async release(input: RecordLockReleaseInput): Promise<RecordLockReleaseResult> {\n const settings = await this.getSettings()\n const resourceEnabled = isRecordLockingEnabledForResource(settings, input.resourceKind)\n if (!resourceEnabled) return { ok: true, released: false, conflictResolved: false }\n\n let conflictResolved = false\n if (input.reason === 'conflict_resolved' && input.conflictId && input.resolution === 'accept_incoming') {\n const conflict = await this.findConflictById(input.conflictId, input)\n if (conflict && conflict.status === 'pending' && conflict.conflictActorUserId === input.userId) {\n await this.resolveConflict(conflict, input.resolution, input.userId)\n conflictResolved = true\n }\n }\n\n const lock = input.token\n ? await this.findOwnedLockByToken(input)\n : await this.findOwnedActiveLock(input)\n if (!lock) return { ok: true, released: false, conflictResolved }\n\n const now = new Date()\n this.markLockReleased(lock, {\n status: 'released',\n reason: input.reason ?? 'cancelled',\n releasedByUserId: input.userId,\n now,\n })\n await this.em.flush()\n\n await emitRecordLocksEvent('record_locks.lock.released', {\n lockId: lock.id,\n resourceKind: lock.resourceKind,\n resourceId: lock.resourceId,\n tenantId: lock.tenantId,\n organizationId: lock.organizationId,\n lockedByUserId: lock.lockedByUserId,\n releasedByUserId: input.userId,\n reason: lock.releaseReason,\n })\n\n if (lock.releaseReason === 'unmount') {\n const remainingActiveLocks = await this.findActiveLocks(input, now)\n const recipientUserIds = remainingActiveLocks\n .map((activeLock) => activeLock.lockedByUserId)\n .filter((userId) => userId !== lock.lockedByUserId)\n\n if (recipientUserIds.length) {\n await emitRecordLocksEvent('record_locks.participant.left', {\n lockId: lock.id,\n resourceKind: lock.resourceKind,\n resourceId: lock.resourceId,\n tenantId: lock.tenantId,\n organizationId: lock.organizationId,\n leftUserId: lock.lockedByUserId,\n reason: 'unmount',\n recipientUserIds,\n activeParticipantCount: remainingActiveLocks.length,\n })\n }\n }\n\n return { ok: true, released: true, conflictResolved }\n }\n\n async forceRelease(input: RecordLockForceReleaseInput): Promise<RecordLockForceReleaseResult> {\n const settings = await this.getSettings()\n const resourceEnabled = isRecordLockingEnabledForResource(settings, input.resourceKind)\n const canForceRelease = await this.canUserForceRelease(input, settings)\n if (!resourceEnabled || !settings.allowForceUnlock || !canForceRelease) {\n return { ok: true, released: false, lock: null }\n }\n\n const now = new Date()\n const activeLocks = this.sortLocksByJoinOrder(await this.findActiveLocks(input, now))\n const lock = activeLocks[0] ?? null\n if (!lock) return { ok: true, released: false, lock: null }\n\n this.markLockReleased(lock, {\n status: 'force_released',\n reason: 'force',\n releasedByUserId: input.userId,\n now,\n })\n await this.em.flush()\n\n await emitRecordLocksEvent('record_locks.lock.force_released', {\n lockId: lock.id,\n resourceKind: lock.resourceKind,\n resourceId: lock.resourceId,\n tenantId: lock.tenantId,\n organizationId: lock.organizationId,\n lockedByUserId: lock.lockedByUserId,\n releasedByUserId: input.userId,\n reason: input.reason ?? null,\n })\n\n const remainingLocks = this.sortLocksByJoinOrder(activeLocks.filter((item) => item.id !== lock.id))\n const nextInQueue = remainingLocks[0] ?? null\n return { ok: true, released: true, lock: nextInQueue ? this.toLockView(nextInQueue, false, remainingLocks) : null }\n }\n\n async validateMutation(input: RecordLockMutationValidationInput): Promise<RecordLockValidationResult> {\n this.scheduleCleanup(input.tenantId)\n const settings = await this.getSettings()\n const resourceEnabled = isRecordLockingEnabledForResource(settings, input.resourceKind)\n const canOverrideIncoming = await this.canUserOverrideIncoming(input, settings)\n\n if (!resourceEnabled) {\n return {\n ok: true,\n enabled: settings.enabled,\n resourceEnabled: false,\n strategy: settings.strategy,\n shouldReleaseOnSuccess: false,\n lock: null,\n latestActionLogId: null,\n }\n }\n\n const parsedHeaders = this.normalizeMutationHeaders(input.headers)\n const keepMineResolution = parsedHeaders.resolution === 'accept_mine' || parsedHeaders.resolution === 'merged'\n ? parsedHeaders.resolution\n : null\n const hasKeepMineIntent = keepMineResolution !== null\n const now = new Date()\n const activeLocks = await this.findActiveLocks(input, now)\n const ownedActiveLock = activeLocks.find((lock) => lock.lockedByUserId === input.userId) ?? null\n const competingLock = activeLocks.find((lock) => lock.lockedByUserId !== input.userId) ?? null\n const latest = await this.findLatestActionLogWithScopeFallback(input)\n const shouldReleaseOnSuccess = Boolean(\n ownedActiveLock\n && (!parsedHeaders.token || ownedActiveLock.token === parsedHeaders.token),\n )\n\n if (settings.strategy === 'pessimistic') {\n if (competingLock && !ownedActiveLock) {\n return {\n ok: false,\n status: 423,\n error: 'Record is currently locked by another user',\n code: 'record_locked',\n lock: this.toLockView(competingLock, false, activeLocks),\n }\n }\n\n if (ownedActiveLock) {\n if (parsedHeaders.token && ownedActiveLock.token !== parsedHeaders.token) {\n return {\n ok: false,\n status: 423,\n error: 'Valid lock token is required for this mutation',\n code: 'record_locked',\n lock: this.toLockView(ownedActiveLock, false, activeLocks),\n }\n }\n }\n\n return {\n ok: true,\n enabled: settings.enabled,\n resourceEnabled: true,\n strategy: settings.strategy,\n shouldReleaseOnSuccess,\n lock: ownedActiveLock ? this.toLockView(ownedActiveLock, false, activeLocks) : null,\n latestActionLogId: latest?.id ?? null,\n }\n }\n\n const existingConflict = parsedHeaders.conflictId\n ? await this.findConflictById(parsedHeaders.conflictId, input)\n : null\n\n if (existingConflict) {\n const canResolveExistingConflict = existingConflict.status === 'pending'\n && existingConflict.conflictActorUserId === input.userId\n\n if (parsedHeaders.resolution === 'accept_mine' || parsedHeaders.resolution === 'merged') {\n const isAlreadyResolvedByRequester = existingConflict.conflictActorUserId === input.userId\n && existingConflict.status !== 'pending'\n && existingConflict.resolution === parsedHeaders.resolution\n\n if (!canResolveExistingConflict && !isAlreadyResolvedByRequester) {\n return {\n ok: false,\n status: 409,\n error: 'Record conflict requires resolution before saving',\n code: 'record_lock_conflict',\n lock: ownedActiveLock ? this.toLockView(ownedActiveLock, false, activeLocks) : null,\n conflict: await this.toConflictPayload(existingConflict, input.mutationPayload ?? null, settings.allowIncomingOverride, canOverrideIncoming),\n }\n }\n if (!canOverrideIncoming) {\n return {\n ok: false,\n status: 409,\n error: 'Record conflict requires resolution before saving',\n code: 'record_lock_conflict',\n lock: ownedActiveLock ? this.toLockView(ownedActiveLock, false, activeLocks) : null,\n conflict: await this.toConflictPayload(existingConflict, input.mutationPayload ?? null, settings.allowIncomingOverride, canOverrideIncoming),\n }\n }\n if (canResolveExistingConflict) {\n await this.resolveConflict(existingConflict, parsedHeaders.resolution, input.userId)\n }\n } else {\n return {\n ok: false,\n status: 409,\n error: 'Record conflict requires resolution before saving',\n code: 'record_lock_conflict',\n lock: ownedActiveLock ? this.toLockView(ownedActiveLock, false, activeLocks) : null,\n conflict: await this.toConflictPayload(existingConflict, input.mutationPayload ?? null, settings.allowIncomingOverride, canOverrideIncoming),\n }\n }\n }\n\n if (!existingConflict) {\n const baseActionLogId = parsedHeaders.baseLogId\n ?? (ownedActiveLock ? ownedActiveLock.baseActionLogId : null)\n\n const hasConflictingBaseLog = Boolean(\n latest?.id\n && baseActionLogId\n && latest.id !== baseActionLogId\n )\n const hasConflictingWriteAfterLockStart = Boolean(\n latest?.id\n && !baseActionLogId\n && ownedActiveLock\n && latest.createdAt instanceof Date\n && ownedActiveLock.lockedAt instanceof Date\n && latest.createdAt.getTime() > ownedActiveLock.lockedAt.getTime()\n && latest.actorUserId !== input.userId\n )\n const isConflictingWrite = hasConflictingBaseLog || hasConflictingWriteAfterLockStart\n\n if (isConflictingWrite) {\n if (keepMineResolution && canOverrideIncoming) {\n const autoResolvedConflict = await this.createConflict({\n scope: input,\n baseActionLogId,\n incomingActionLogId: latest?.id ?? null,\n conflictActorUserId: input.userId,\n incomingActorUserId: latest?.actorUserId ?? null,\n })\n await this.resolveConflict(autoResolvedConflict, keepMineResolution, input.userId)\n\n return {\n ok: true,\n enabled: settings.enabled,\n resourceEnabled: true,\n strategy: settings.strategy,\n shouldReleaseOnSuccess,\n lock: ownedActiveLock ? this.toLockView(ownedActiveLock, false, activeLocks) : null,\n latestActionLogId: latest?.id ?? null,\n }\n }\n\n const conflict = await this.createConflict({\n scope: input,\n baseActionLogId,\n incomingActionLogId: latest?.id ?? null,\n conflictActorUserId: input.userId,\n incomingActorUserId: latest?.actorUserId ?? null,\n })\n\n return {\n ok: false,\n status: 409,\n error: 'Record conflict detected',\n code: 'record_lock_conflict',\n lock: ownedActiveLock ? this.toLockView(ownedActiveLock, false, activeLocks) : null,\n conflict: await this.toConflictPayload(conflict, input.mutationPayload ?? null, settings.allowIncomingOverride, canOverrideIncoming),\n }\n }\n }\n\n return {\n ok: true,\n enabled: settings.enabled,\n resourceEnabled: true,\n strategy: settings.strategy,\n shouldReleaseOnSuccess,\n lock: ownedActiveLock ? this.toLockView(ownedActiveLock, false, activeLocks) : null,\n latestActionLogId: latest?.id ?? null,\n }\n }\n\n async releaseAfterMutation(input: RecordLockReleaseInput): Promise<void> {\n const releaseResult = await this.release({\n ...input,\n reason: input.reason ?? 'saved',\n })\n if (!releaseResult.released) return\n }\n\n async emitIncomingChangesNotificationAfterMutation(input: {\n tenantId: string\n organizationId?: string | null\n userId: string\n resourceKind: string\n resourceId: string\n method: 'PUT' | 'DELETE'\n }): Promise<void> {\n if (input.method !== 'PUT') return\n const settings = await this.getSettings()\n if (!settings.notifyOnConflict || !isRecordLockingEnabledForResource(settings, input.resourceKind)) return\n\n const now = new Date()\n let activeLocks = await this.findActiveLocks(input, now)\n if (!activeLocks.length) {\n const fallbackWhere: FilterQuery<RecordLock> = {\n tenantId: input.tenantId,\n deletedAt: null,\n resourceKind: input.resourceKind,\n resourceId: input.resourceId,\n status: ACTIVE_LOCK_STATUS,\n }\n const fallbackLocks = await this.em.find(RecordLock, fallbackWhere, { orderBy: { updatedAt: 'desc' } })\n activeLocks = Array.isArray(fallbackLocks) ? fallbackLocks : []\n }\n\n const recipientUserIds = new Set<string>()\n for (const lock of activeLocks) {\n if (lock.lockedByUserId !== input.userId) {\n recipientUserIds.add(lock.lockedByUserId)\n }\n }\n if (!recipientUserIds.size) {\n const fallbackWindowMs = Math.max((settings.timeoutSeconds ?? 300) * 1000, 60_000)\n const fallbackSince = new Date(now.getTime() - fallbackWindowMs)\n const recentLocks = await this.em.find(RecordLock, {\n tenantId: input.tenantId,\n deletedAt: null,\n resourceKind: input.resourceKind,\n resourceId: input.resourceId,\n updatedAt: { $gte: fallbackSince },\n }, { orderBy: { updatedAt: 'desc' }, limit: 50 })\n\n for (const lock of (Array.isArray(recentLocks) ? recentLocks : [])) {\n if (lock.lockedByUserId !== input.userId) {\n recipientUserIds.add(lock.lockedByUserId)\n }\n }\n }\n if (!recipientUserIds.size) return\n\n let latest = await this.findLatestActionLog(input)\n if (!latest) {\n latest = await this.findLatestActionLog({\n tenantId: input.tenantId,\n resourceKind: input.resourceKind,\n resourceId: input.resourceId,\n })\n }\n let actorLog = latest?.actorUserId === input.userId\n ? latest\n : await this.findLatestActionLogByActor(input, input.userId)\n if (!actorLog) {\n actorLog = await this.findLatestActionLogByActor({\n tenantId: input.tenantId,\n resourceKind: input.resourceKind,\n resourceId: input.resourceId,\n }, input.userId)\n }\n const incomingLog = actorLog ?? latest\n\n const changedFields = incomingLog\n ? this.summarizeChangedFieldsFromActionLog(incomingLog)\n : ''\n const changedRows = this.buildIncomingChangeRowsFromActionLog(incomingLog)\n const changedRowsJson = changedRows.length ? JSON.stringify(changedRows) : ''\n\n await emitRecordLocksEvent('record_locks.incoming_changes.available', {\n resourceKind: input.resourceKind,\n resourceId: input.resourceId,\n tenantId: input.tenantId,\n organizationId: normalizeScopeOrganization(input.organizationId),\n incomingActorUserId: input.userId,\n incomingActionLogId: incomingLog?.id ?? null,\n recipientUserIds: Array.from(recipientUserIds),\n changedFields: changedFields || '-',\n changedRowsJson,\n })\n }\n\n async emitRecordDeletedNotificationAfterMutation(input: {\n tenantId: string\n organizationId?: string | null\n userId: string\n resourceKind: string\n resourceId: string\n method: 'PUT' | 'DELETE'\n }): Promise<void> {\n if (input.method !== 'DELETE') return\n const settings = await this.getSettings()\n if (!isRecordLockingEnabledForResource(settings, input.resourceKind)) return\n\n const now = new Date()\n let activeLocks = await this.findActiveLocks(input, now)\n if (!activeLocks.length) {\n const fallbackWhere: FilterQuery<RecordLock> = {\n tenantId: input.tenantId,\n deletedAt: null,\n resourceKind: input.resourceKind,\n resourceId: input.resourceId,\n status: ACTIVE_LOCK_STATUS,\n }\n const fallbackLocks = await this.em.find(RecordLock, fallbackWhere, { orderBy: { updatedAt: 'desc' } })\n activeLocks = Array.isArray(fallbackLocks) ? fallbackLocks : []\n }\n\n const recipientUserIds = new Set<string>()\n for (const lock of activeLocks) {\n if (lock.lockedByUserId !== input.userId) {\n recipientUserIds.add(lock.lockedByUserId)\n }\n }\n\n if (!recipientUserIds.size) {\n const fallbackWindowMs = Math.max((settings.timeoutSeconds ?? 300) * 1000, 60_000)\n const fallbackSince = new Date(now.getTime() - fallbackWindowMs)\n const recentLocks = await this.em.find(RecordLock, {\n tenantId: input.tenantId,\n deletedAt: null,\n resourceKind: input.resourceKind,\n resourceId: input.resourceId,\n updatedAt: { $gte: fallbackSince },\n }, { orderBy: { updatedAt: 'desc' }, limit: 50 })\n\n for (const lock of (Array.isArray(recentLocks) ? recentLocks : [])) {\n if (lock.lockedByUserId !== input.userId) {\n recipientUserIds.add(lock.lockedByUserId)\n }\n }\n }\n if (!recipientUserIds.size) return\n\n await emitRecordLocksEvent('record_locks.record.deleted', {\n resourceKind: input.resourceKind,\n resourceId: input.resourceId,\n tenantId: input.tenantId,\n organizationId: normalizeScopeOrganization(input.organizationId),\n deletedByUserId: input.userId,\n recipientUserIds: Array.from(recipientUserIds),\n })\n }\n\n async resolveConflictById(input: {\n conflictId: string\n tenantId: string\n organizationId?: string | null\n userId: string\n resolution: 'accept_incoming' | 'accept_mine' | 'merged'\n }): Promise<boolean> {\n const settings = await this.getSettings()\n const canOverrideIncoming = await this.canUserOverrideIncoming(input, settings)\n const conflict = await this.em.findOne(RecordLockConflict, {\n id: input.conflictId,\n tenantId: input.tenantId,\n organizationId: normalizeScopeOrganization(input.organizationId),\n deletedAt: null,\n })\n if (!conflict || conflict.status !== 'pending' || conflict.conflictActorUserId !== input.userId) {\n return false\n }\n if ((input.resolution === 'accept_mine' || input.resolution === 'merged') && !canOverrideIncoming) {\n return false\n }\n await this.resolveConflict(conflict, input.resolution, input.userId)\n return true\n }\n\n private async canUserOverrideIncoming(\n input: Pick<RecordLockScope, 'tenantId' | 'organizationId' | 'userId'>,\n settings: RecordLockSettings,\n ): Promise<boolean> {\n if (!settings.allowIncomingOverride) return false\n if (!this.rbacService) return false\n\n try {\n return await this.rbacService.userHasAllFeatures(\n input.userId,\n ['record_locks.override_incoming'],\n {\n tenantId: input.tenantId,\n organizationId: normalizeScopeOrganization(input.organizationId),\n },\n )\n } catch {\n return false\n }\n }\n\n private async canUserForceRelease(\n input: Pick<RecordLockScope, 'tenantId' | 'organizationId' | 'userId'>,\n settings: RecordLockSettings,\n ): Promise<boolean> {\n if (!settings.allowForceUnlock) return false\n if (!this.rbacService) return false\n\n try {\n return await this.rbacService.userHasAllFeatures(\n input.userId,\n ['record_locks.force_release'],\n {\n tenantId: input.tenantId,\n organizationId: normalizeScopeOrganization(input.organizationId),\n },\n )\n } catch {\n return false\n }\n }\n\n private pruneLockCleanupState(now: number): void {\n for (const [tenantId, state] of lockCleanupStateByTenant.entries()) {\n if (!state.inFlight && now - state.lastSeenAt > LOCK_CLEANUP_STATE_TTL_MS) {\n lockCleanupStateByTenant.delete(tenantId)\n }\n }\n\n if (lockCleanupStateByTenant.size <= LOCK_CLEANUP_STATE_MAX_ENTRIES) return\n\n const removable = Array.from(lockCleanupStateByTenant.entries())\n .filter(([, state]) => !state.inFlight)\n .sort((left, right) => left[1].lastSeenAt - right[1].lastSeenAt)\n const overflow = lockCleanupStateByTenant.size - LOCK_CLEANUP_STATE_MAX_ENTRIES\n for (const [tenantId] of removable.slice(0, Math.max(0, overflow))) {\n lockCleanupStateByTenant.delete(tenantId)\n }\n }\n\n private scheduleCleanup(tenantId: string): void {\n const now = Date.now()\n this.pruneLockCleanupState(now)\n const state = lockCleanupStateByTenant.get(tenantId) ?? { lastRunAt: 0, inFlight: false, lastSeenAt: now }\n state.lastSeenAt = now\n lockCleanupStateByTenant.set(tenantId, state)\n if (state.inFlight) return\n if (now - state.lastRunAt < LOCK_CLEANUP_INTERVAL_MS) return\n\n state.inFlight = true\n state.lastRunAt = now\n lockCleanupStateByTenant.set(tenantId, state)\n\n void this.cleanupHistoricalRecords(tenantId).finally(() => {\n const current = lockCleanupStateByTenant.get(tenantId)\n if (!current) return\n current.inFlight = false\n lockCleanupStateByTenant.set(tenantId, current)\n })\n }\n\n private async cleanupHistoricalRecords(tenantId: string): Promise<void> {\n try {\n const db = getKysely(this.em)\n const now = Date.now()\n const lockCutoff = new Date(now - LOCK_RETENTION_MS)\n const resolvedConflictCutoff = new Date(now - RESOLVED_CONFLICT_RETENTION_MS)\n const pendingConflictCutoff = new Date(now - PENDING_CONFLICT_RETENTION_MS)\n const deletedAt = new Date(now)\n\n await db\n .updateTable('record_locks' as any)\n .set({\n deleted_at: deletedAt,\n updated_at: deletedAt,\n } as any)\n .where('tenant_id' as any, '=', tenantId)\n .where('deleted_at' as any, 'is', null as any)\n .where('status' as any, '!=', ACTIVE_LOCK_STATUS)\n .where('updated_at' as any, '<', lockCutoff)\n .execute()\n\n await db\n .updateTable('record_lock_conflicts' as any)\n .set({\n deleted_at: deletedAt,\n updated_at: deletedAt,\n } as any)\n .where('tenant_id' as any, '=', tenantId)\n .where('deleted_at' as any, 'is', null as any)\n .where((eb: any) => eb.or([\n eb.and([\n eb('status' as any, '=', 'pending'),\n eb('created_at' as any, '<', pendingConflictCutoff),\n ]),\n eb.and([\n eb('status' as any, '!=', 'pending'),\n eb('updated_at' as any, '<', resolvedConflictCutoff),\n ]),\n ]))\n .execute()\n } catch {\n // Best-effort cleanup must never fail lock workflows.\n }\n }\n\n private normalizeMutationHeaders(headers: Partial<RecordLockMutationHeaders>): Partial<RecordLockMutationHeaders> {\n const parsed = recordLockMutationHeaderSchema.partial().safeParse(headers)\n if (!parsed.success) return {}\n return parsed.data\n }\n\n private buildScopeWhere(scope: Pick<RecordLockScope, 'tenantId' | 'organizationId'>): {\n tenantId: string\n deletedAt: null\n organizationId?: string | null\n } {\n const where: {\n tenantId: string\n deletedAt: null\n organizationId?: string | null\n } = {\n tenantId: scope.tenantId,\n deletedAt: null,\n }\n\n if (scope.organizationId !== undefined) {\n where.organizationId = normalizeScopeOrganization(scope.organizationId)\n }\n\n return where\n }\n\n private async findActiveLocks(\n input: Pick<RecordLockScope, 'tenantId' | 'organizationId'> & RecordLockResource,\n now: Date,\n ): Promise<RecordLock[]> {\n const legacyFinder = (this as unknown as {\n findActiveLock?: (args: Pick<RecordLockScope, 'tenantId' | 'organizationId'> & RecordLockResource, at: Date) => Promise<RecordLock | null>\n }).findActiveLock\n if (typeof legacyFinder === 'function') {\n const legacyResult = await legacyFinder(input, now)\n return legacyResult ? [legacyResult] : []\n }\n\n const where: FilterQuery<RecordLock> = {\n ...this.buildScopeWhere(input),\n resourceKind: input.resourceKind,\n resourceId: input.resourceId,\n status: ACTIVE_LOCK_STATUS,\n }\n\n const locks = await this.em.find(RecordLock, where, { orderBy: { updatedAt: 'desc' } })\n if (!Array.isArray(locks) || !locks.length) return []\n\n let dirty = false\n const active: RecordLock[] = []\n const expiredLocks: RecordLock[] = []\n\n for (const lock of locks) {\n if (lock.expiresAt <= now) {\n this.markLockReleased(lock, {\n status: 'expired',\n reason: 'expired',\n releasedByUserId: lock.lockedByUserId,\n now,\n })\n dirty = true\n expiredLocks.push(lock)\n continue\n }\n\n active.push(lock)\n }\n\n if (dirty) await this.em.flush()\n if (expiredLocks.length) {\n const recipientUserIds = active.map((lock) => lock.lockedByUserId)\n for (const expiredLock of expiredLocks) {\n await emitRecordLocksEvent('record_locks.participant.left', {\n lockId: expiredLock.id,\n resourceKind: expiredLock.resourceKind,\n resourceId: expiredLock.resourceId,\n tenantId: expiredLock.tenantId,\n organizationId: expiredLock.organizationId,\n leftUserId: expiredLock.lockedByUserId,\n reason: 'expired',\n recipientUserIds,\n activeParticipantCount: active.length,\n })\n }\n }\n return active\n }\n\n private async findOwnedLockByToken(\n input: Pick<RecordLockScope, 'tenantId' | 'organizationId' | 'userId'> & RecordLockResource & { token?: string },\n ): Promise<RecordLock | null> {\n if (!input.token) return null\n\n const where: FilterQuery<RecordLock> = {\n ...this.buildScopeWhere(input),\n resourceKind: input.resourceKind,\n resourceId: input.resourceId,\n token: input.token,\n lockedByUserId: input.userId,\n status: ACTIVE_LOCK_STATUS,\n }\n\n return this.em.findOne(RecordLock, where)\n }\n\n private async findOwnedActiveLock(\n input: Pick<RecordLockScope, 'tenantId' | 'organizationId' | 'userId'> & RecordLockResource,\n ): Promise<RecordLock | null> {\n const where: FilterQuery<RecordLock> = {\n ...this.buildScopeWhere(input),\n resourceKind: input.resourceKind,\n resourceId: input.resourceId,\n lockedByUserId: input.userId,\n status: ACTIVE_LOCK_STATUS,\n }\n return this.em.findOne(RecordLock, where)\n }\n\n private async hasRecentSavedRelease(input: {\n tenantId: string\n organizationId?: string | null\n userId: string\n resourceKind: string\n resourceId: string\n now: Date\n }): Promise<boolean> {\n const cutoff = new Date(input.now.getTime() - PARTICIPANT_REJOIN_AFTER_SAVE_SUPPRESS_MS)\n const where: FilterQuery<RecordLock> = {\n tenantId: input.tenantId,\n resourceKind: input.resourceKind,\n resourceId: input.resourceId,\n lockedByUserId: input.userId,\n status: 'released',\n releaseReason: 'saved',\n deletedAt: null,\n releasedAt: { $gte: cutoff },\n }\n if (input.organizationId !== undefined) {\n where.organizationId = normalizeScopeOrganization(input.organizationId)\n }\n\n const scoped = await this.em.findOne(RecordLock, where, { orderBy: { releasedAt: 'desc' } })\n if (scoped) return true\n if (input.organizationId === undefined) return false\n\n return Boolean(await this.em.findOne(RecordLock, {\n tenantId: input.tenantId,\n resourceKind: input.resourceKind,\n resourceId: input.resourceId,\n lockedByUserId: input.userId,\n status: 'released',\n releaseReason: 'saved',\n deletedAt: null,\n releasedAt: { $gte: cutoff },\n }, { orderBy: { releasedAt: 'desc' } }))\n }\n\n private markLockReleased(\n lock: RecordLock,\n params: {\n status: RecordLockStatus\n reason: RecordLockReleaseReason\n releasedByUserId: string\n now: Date\n },\n ) {\n lock.status = params.status\n lock.releaseReason = params.reason\n lock.releasedByUserId = params.releasedByUserId\n lock.releasedAt = params.now\n lock.updatedAt = params.now\n }\n\n private async findLatestActionLog(\n input: Pick<RecordLockScope, 'tenantId' | 'organizationId'> & RecordLockResource,\n ): Promise<ActionLog | null> {\n const where: FilterQuery<ActionLog> = {\n tenantId: input.tenantId,\n resourceKind: input.resourceKind,\n resourceId: input.resourceId,\n deletedAt: null,\n }\n\n if (input.organizationId !== undefined) {\n where.organizationId = normalizeScopeOrganization(input.organizationId)\n }\n\n return normalizeActionLogPayload(await findOneWithDecryption(\n this.em,\n ActionLog,\n where,\n { orderBy: { createdAt: 'desc' } },\n { tenantId: input.tenantId, organizationId: normalizeScopeOrganization(input.organizationId) },\n ))\n }\n\n private async findLatestActionLogWithScopeFallback(\n input: Pick<RecordLockScope, 'tenantId' | 'organizationId'> & RecordLockResource,\n ): Promise<ActionLog | null> {\n const scoped = await this.findLatestActionLog(input)\n if (scoped) return scoped\n if (input.organizationId !== null) return null\n\n return this.findLatestActionLog({\n tenantId: input.tenantId,\n resourceKind: input.resourceKind,\n resourceId: input.resourceId,\n })\n }\n\n private async findLatestActionLogByActor(\n input: Pick<RecordLockScope, 'tenantId' | 'organizationId'> & RecordLockResource,\n actorUserId: string,\n ): Promise<ActionLog | null> {\n const where: FilterQuery<ActionLog> = {\n tenantId: input.tenantId,\n resourceKind: input.resourceKind,\n resourceId: input.resourceId,\n actorUserId,\n deletedAt: null,\n }\n\n if (input.organizationId !== undefined) {\n where.organizationId = normalizeScopeOrganization(input.organizationId)\n }\n\n return normalizeActionLogPayload(await findOneWithDecryption(\n this.em,\n ActionLog,\n where,\n { orderBy: { createdAt: 'desc' } },\n { tenantId: input.tenantId, organizationId: normalizeScopeOrganization(input.organizationId) },\n ))\n }\n\n private summarizeChangedFieldsFromActionLog(log: ActionLog | null): string {\n if (!log) return ''\n\n const changesJson = readActionLogChangesJson(log)\n if (changesJson) {\n const fromChanges = Object.keys(changesJson)\n .filter((field) => !shouldSkipConflictField(field))\n .slice(0, 12)\n .map(formatChangedFieldLabel)\n .join(', ')\n if (fromChanges) return fromChanges\n }\n\n const before = readJsonRecordValue(log.snapshotBefore)\n const after = readJsonRecordValue(log.snapshotAfter)\n if (!before || !after) return ''\n\n const diffPaths = new Set<string>()\n this.collectSnapshotDiffPaths(before, after, null, diffPaths, new Set<unknown>())\n\n return Array.from(diffPaths)\n .filter((field) => !shouldSkipConflictField(field))\n .sort((left, right) => left.localeCompare(right))\n .slice(0, 12)\n .map(formatChangedFieldLabel)\n .join(', ')\n }\n\n private buildIncomingChangeRowsFromActionLog(log: ActionLog | null): Array<{\n field: string\n incoming: string\n current: string\n }> {\n const changesJson = readActionLogChangesJson(log)\n if (!changesJson) return []\n\n const rows: Array<{ field: string; incoming: string; current: string }> = []\n for (const [rawField, rawChange] of Object.entries(changesJson)) {\n if (rows.length >= 12) break\n if (shouldSkipConflictField(rawField)) continue\n\n const change = isRecordValue(rawChange) ? rawChange : {}\n const incoming = Object.prototype.hasOwnProperty.call(change, 'to')\n ? formatNotificationValue(change.to)\n : '-'\n const current = Object.prototype.hasOwnProperty.call(change, 'from')\n ? formatNotificationValue(change.from)\n : '-'\n\n rows.push({\n field: formatChangedFieldLabel(rawField),\n incoming,\n current,\n })\n }\n\n return rows\n }\n\n private toParticipantView(lock: RecordLock): RecordLockParticipantView {\n return {\n userId: lock.lockedByUserId,\n lockedByIp: lock.lockedByIp ?? null,\n lockedAt: normalizeDate(lock.lockedAt),\n lastHeartbeatAt: normalizeDate(lock.lastHeartbeatAt),\n expiresAt: normalizeDate(lock.expiresAt),\n }\n }\n\n private sortLocksByJoinOrder(locks: RecordLock[]): RecordLock[] {\n return [...locks].sort((left, right) => {\n const leftLockedAt = left.lockedAt instanceof Date ? left.lockedAt.getTime() : 0\n const rightLockedAt = right.lockedAt instanceof Date ? right.lockedAt.getTime() : 0\n if (leftLockedAt !== rightLockedAt) return leftLockedAt - rightLockedAt\n\n const leftCreatedAt = left.createdAt instanceof Date ? left.createdAt.getTime() : 0\n const rightCreatedAt = right.createdAt instanceof Date ? right.createdAt.getTime() : 0\n if (leftCreatedAt !== rightCreatedAt) return leftCreatedAt - rightCreatedAt\n\n return left.id.localeCompare(right.id)\n })\n }\n\n private toLockView(lock: RecordLock, includeToken: boolean, activeLocks: RecordLock[] = [lock]): RecordLockView {\n const participants = activeLocks.map((item) => this.toParticipantView(item))\n return {\n id: lock.id,\n resourceKind: lock.resourceKind,\n resourceId: lock.resourceId,\n token: includeToken ? lock.token : null,\n strategy: lock.strategy,\n status: lock.status,\n lockedByUserId: lock.lockedByUserId,\n lockedByIp: lock.lockedByIp ?? null,\n baseActionLogId: lock.baseActionLogId,\n lockedAt: normalizeDate(lock.lockedAt),\n lastHeartbeatAt: normalizeDate(lock.lastHeartbeatAt),\n expiresAt: normalizeDate(lock.expiresAt),\n participants,\n activeParticipantCount: participants.length,\n }\n }\n\n private async createConflict(input: {\n scope: Pick<RecordLockScope, 'tenantId' | 'organizationId'> & RecordLockResource\n baseActionLogId: string | null\n incomingActionLogId: string | null\n conflictActorUserId: string\n incomingActorUserId: string | null\n }): Promise<RecordLockConflict> {\n const dedupeKey = [\n 'record_locks',\n 'conflict',\n input.scope.tenantId,\n normalizeScopeOrganization(input.scope.organizationId) ?? 'global',\n input.scope.resourceKind,\n input.scope.resourceId,\n input.conflictActorUserId,\n input.baseActionLogId ?? 'none',\n input.incomingActionLogId ?? 'none',\n ].join(':')\n\n const result = await this.em.transactional(async (tx) => {\n try {\n const db = getKysely(tx as EntityManager)\n await sql`select pg_advisory_xact_lock(hashtext(${dedupeKey}))`.execute(db)\n } catch {\n // Best-effort lock; fallback to find-first behavior below.\n }\n\n const existing = await this.findPendingConflictByFingerprint(tx as EntityManager, input)\n if (existing) {\n return { conflict: existing, created: false as const }\n }\n\n const conflict = tx.create(RecordLockConflict, {\n resourceKind: input.scope.resourceKind,\n resourceId: input.scope.resourceId,\n status: 'pending',\n resolution: null,\n baseActionLogId: input.baseActionLogId,\n incomingActionLogId: input.incomingActionLogId,\n conflictActorUserId: input.conflictActorUserId,\n incomingActorUserId: input.incomingActorUserId,\n tenantId: input.scope.tenantId,\n organizationId: normalizeScopeOrganization(input.scope.organizationId),\n })\n\n tx.persist(conflict)\n await tx.flush()\n return { conflict, created: true as const }\n })\n\n if (result.created) {\n await emitRecordLocksEvent('record_locks.conflict.detected', {\n conflictId: result.conflict.id,\n resourceKind: result.conflict.resourceKind,\n resourceId: result.conflict.resourceId,\n tenantId: result.conflict.tenantId,\n organizationId: result.conflict.organizationId,\n conflictActorUserId: result.conflict.conflictActorUserId,\n incomingActorUserId: result.conflict.incomingActorUserId,\n baseActionLogId: result.conflict.baseActionLogId,\n incomingActionLogId: result.conflict.incomingActionLogId,\n })\n }\n\n return result.conflict\n }\n\n private findPendingConflictByFingerprint(\n em: EntityManager,\n input: {\n scope: Pick<RecordLockScope, 'tenantId' | 'organizationId'> & RecordLockResource\n baseActionLogId: string | null\n incomingActionLogId: string | null\n conflictActorUserId: string\n },\n ): Promise<RecordLockConflict | null> {\n return em.findOne(RecordLockConflict, {\n tenantId: input.scope.tenantId,\n organizationId: normalizeScopeOrganization(input.scope.organizationId),\n resourceKind: input.scope.resourceKind,\n resourceId: input.scope.resourceId,\n conflictActorUserId: input.conflictActorUserId,\n status: 'pending',\n baseActionLogId: input.baseActionLogId,\n incomingActionLogId: input.incomingActionLogId,\n deletedAt: null,\n }, { orderBy: { createdAt: 'desc' } })\n }\n\n private async resolveConflict(\n conflict: RecordLockConflict,\n resolution: 'accept_incoming' | Extract<RecordLockMutationHeaders['resolution'], 'accept_mine' | 'merged'>,\n resolvedByUserId: string,\n ): Promise<void> {\n const now = new Date()\n\n const resolutionMap: Record<'accept_incoming' | Extract<RecordLockMutationHeaders['resolution'], 'accept_mine' | 'merged'>, {\n status: RecordLockConflictStatus\n resolution: RecordLockConflictResolution\n }> = {\n accept_incoming: { status: 'resolved_accept_incoming', resolution: 'accept_incoming' },\n accept_mine: { status: 'resolved_accept_mine', resolution: 'accept_mine' },\n merged: { status: 'resolved_merged', resolution: 'merged' },\n }\n\n const target = resolutionMap[resolution]\n conflict.status = target.status\n conflict.resolution = target.resolution\n conflict.resolvedByUserId = resolvedByUserId\n conflict.resolvedAt = now\n conflict.updatedAt = now\n await this.em.flush()\n\n await emitRecordLocksEvent('record_locks.conflict.resolved', {\n conflictId: conflict.id,\n resourceKind: conflict.resourceKind,\n resourceId: conflict.resourceId,\n tenantId: conflict.tenantId,\n organizationId: conflict.organizationId,\n conflictActorUserId: conflict.conflictActorUserId,\n incomingActorUserId: conflict.incomingActorUserId,\n resolution: conflict.resolution,\n resolvedByUserId,\n })\n }\n\n private async findConflictById(\n conflictId: string,\n scope: Pick<RecordLockScope, 'tenantId' | 'organizationId'> & RecordLockResource,\n ): Promise<RecordLockConflict | null> {\n const where: FilterQuery<RecordLockConflict> = {\n id: conflictId,\n tenantId: scope.tenantId,\n resourceKind: scope.resourceKind,\n resourceId: scope.resourceId,\n deletedAt: null,\n }\n\n if (scope.organizationId !== undefined) {\n where.organizationId = normalizeScopeOrganization(scope.organizationId)\n }\n\n const scoped = await this.em.findOne(RecordLockConflict, where)\n if (scoped || scope.organizationId === undefined) return scoped\n\n return this.em.findOne(RecordLockConflict, {\n id: conflictId,\n tenantId: scope.tenantId,\n resourceKind: scope.resourceKind,\n resourceId: scope.resourceId,\n deletedAt: null,\n })\n }\n\n private async findActionLogById(\n logId: string | null,\n scope: Pick<RecordLockScope, 'tenantId' | 'organizationId'> & RecordLockResource,\n ): Promise<ActionLog | null> {\n if (!logId) return null\n\n let resolved = this.actionLogService\n ? await this.actionLogService.findById(logId)\n : null\n if (!resolved) {\n resolved = await findOneWithDecryption(\n this.em,\n ActionLog,\n { id: logId, deletedAt: null },\n undefined,\n { tenantId: scope.tenantId, organizationId: normalizeScopeOrganization(scope.organizationId) },\n )\n }\n resolved = normalizeActionLogPayload(resolved)\n if (!resolved || resolved.deletedAt) return null\n\n if (resolved.tenantId !== scope.tenantId) return null\n\n if (scope.organizationId !== undefined) {\n const expectedOrganizationId = normalizeScopeOrganization(scope.organizationId)\n if (normalizeScopeOrganization(resolved.organizationId) !== expectedOrganizationId) return null\n }\n\n if (resolved.resourceKind !== scope.resourceKind || resolved.resourceId !== scope.resourceId) {\n return null\n }\n\n return resolved\n }\n\n private collectSnapshotDiffPaths(\n before: unknown,\n after: unknown,\n pathPrefix: string | null,\n output: Set<string>,\n seen: Set<unknown>,\n ): void {\n if (valuesEqual(before, after)) return\n\n const beforeRecord = isRecordValue(before) ? before : null\n const afterRecord = isRecordValue(after) ? after : null\n\n if (!beforeRecord || !afterRecord) {\n if (pathPrefix) output.add(pathPrefix)\n return\n }\n\n if (seen.has(beforeRecord) || seen.has(afterRecord)) {\n if (pathPrefix) output.add(pathPrefix)\n return\n }\n\n seen.add(beforeRecord)\n seen.add(afterRecord)\n\n const keys = new Set([...Object.keys(beforeRecord), ...Object.keys(afterRecord)])\n for (const key of keys) {\n if (SKIPPED_CONFLICT_FIELDS.has(key)) continue\n const nextPath = pathPrefix ? `${pathPrefix}.${key}` : key\n this.collectSnapshotDiffPaths(beforeRecord[key], afterRecord[key], nextPath, output, seen)\n }\n }\n\n private async buildConflictChanges(\n conflict: RecordLockConflict,\n mutationPayload: Record<string, unknown> | null,\n ): Promise<RecordLockConflictChange[]> {\n const scope = {\n tenantId: conflict.tenantId,\n organizationId: conflict.organizationId,\n resourceKind: conflict.resourceKind,\n resourceId: conflict.resourceId,\n }\n\n const baseLog = await this.findActionLogById(conflict.baseActionLogId, scope)\n const incomingLog = await this.findActionLogById(conflict.incomingActionLogId, scope)\n\n const baseSnapshot = readJsonRecordValue(baseLog?.snapshotAfter ?? null)\n const incomingBeforeSnapshot = readJsonRecordValue(incomingLog?.snapshotBefore ?? null)\n const incomingAfterSnapshot = readJsonRecordValue(incomingLog?.snapshotAfter ?? null)\n const fallbackBaseSnapshot = baseSnapshot ?? incomingBeforeSnapshot\n\n const changeMap = new Map<string, { baseValue: unknown; incomingValue: unknown }>()\n\n const incomingChanges = readActionLogChangesJson(incomingLog)\n if (incomingChanges) {\n for (const [fieldPathRaw, rawChange] of Object.entries(incomingChanges)) {\n const fieldPath = fieldPathRaw.trim()\n if (shouldSkipConflictField(fieldPath)) continue\n\n const changeRecord = isRecordValue(rawChange) ? rawChange : null\n const fromValue = changeRecord && Object.prototype.hasOwnProperty.call(changeRecord, 'from')\n ? changeRecord.from\n : readPathValueLoose(fallbackBaseSnapshot, fieldPath)\n const toValue = changeRecord && Object.prototype.hasOwnProperty.call(changeRecord, 'to')\n ? changeRecord.to\n : readPathValueLoose(incomingAfterSnapshot, fieldPath)\n\n changeMap.set(fieldPath, {\n baseValue: fromValue === MISSING_CONFLICT_VALUE ? null : normalizeConflictValue(fromValue),\n incomingValue: toValue === MISSING_CONFLICT_VALUE ? null : normalizeConflictValue(toValue),\n })\n }\n }\n\n if (!changeMap.size && fallbackBaseSnapshot && incomingAfterSnapshot) {\n const diffPaths = new Set<string>()\n this.collectSnapshotDiffPaths(\n fallbackBaseSnapshot,\n incomingAfterSnapshot,\n null,\n diffPaths,\n new Set<unknown>(),\n )\n\n for (const fieldPath of diffPaths) {\n if (shouldSkipConflictField(fieldPath)) continue\n const fromValue = readPathValueLoose(fallbackBaseSnapshot, fieldPath)\n const toValue = readPathValueLoose(incomingAfterSnapshot, fieldPath)\n changeMap.set(fieldPath, {\n baseValue: fromValue === MISSING_CONFLICT_VALUE ? null : normalizeConflictValue(fromValue),\n incomingValue: toValue === MISSING_CONFLICT_VALUE ? null : normalizeConflictValue(toValue),\n })\n }\n }\n\n if (!changeMap.size && mutationPayload && incomingAfterSnapshot) {\n for (const fieldPath of Object.keys(mutationPayload)) {\n if (shouldSkipConflictField(fieldPath)) continue\n const mineValue = readPathValueLoose(mutationPayload, fieldPath)\n const incomingValue = readPathValueLoose(incomingAfterSnapshot, fieldPath)\n if (mineValue === MISSING_CONFLICT_VALUE || incomingValue === MISSING_CONFLICT_VALUE) continue\n if (valuesEqual(mineValue, incomingValue)) continue\n const baseValue = readPathValueLoose(fallbackBaseSnapshot, fieldPath)\n changeMap.set(fieldPath, {\n baseValue: baseValue === MISSING_CONFLICT_VALUE ? null : normalizeConflictValue(baseValue),\n incomingValue: normalizeConflictValue(incomingValue),\n })\n }\n }\n\n if (!changeMap.size) return []\n\n const allFields = Array.from(changeMap.keys())\n const preferredFields = mutationPayload\n ? allFields.filter((fieldPath) => {\n const mineValue = readPathValueLoose(mutationPayload, fieldPath)\n if (mineValue === MISSING_CONFLICT_VALUE) return false\n const incomingValue = changeMap.get(fieldPath)?.incomingValue\n return !valuesEqual(mineValue, incomingValue)\n })\n : []\n const selectedFields = (preferredFields.length ? preferredFields : allFields)\n .filter((fieldPath) => !shouldSkipConflictField(fieldPath))\n .sort((left, right) => left.localeCompare(right))\n .slice(0, 25)\n\n return selectedFields.map((fieldPath) => {\n const entry = changeMap.get(fieldPath) ?? { baseValue: null, incomingValue: null }\n const mineValueRaw = mutationPayload ? readPathValueLoose(mutationPayload, fieldPath) : MISSING_CONFLICT_VALUE\n const mineValue = mineValueRaw === MISSING_CONFLICT_VALUE\n ? entry.baseValue\n : normalizeConflictValue(mineValueRaw)\n\n return {\n field: fieldPath,\n displayValue: normalizeConflictValue(entry.baseValue),\n baseValue: normalizeConflictValue(entry.baseValue),\n incomingValue: normalizeConflictValue(entry.incomingValue),\n mineValue: normalizeConflictValue(mineValue),\n }\n })\n }\n\n private async toConflictPayload(\n conflict: RecordLockConflict,\n mutationPayload: Record<string, unknown> | null,\n allowIncomingOverride: boolean,\n canOverrideIncoming: boolean,\n ): Promise<RecordLockConflictPayload> {\n const changes = await this.buildConflictChanges(conflict, mutationPayload)\n return {\n id: conflict.id,\n resourceKind: conflict.resourceKind,\n resourceId: conflict.resourceId,\n baseActionLogId: conflict.baseActionLogId,\n incomingActionLogId: conflict.incomingActionLogId,\n allowIncomingOverride,\n canOverrideIncoming,\n resolutionOptions: canOverrideIncoming ? ['accept_mine'] : [],\n changes,\n }\n }\n}\n\nexport function createRecordLockService(deps: RecordLockServiceDeps): RecordLockService {\n return new RecordLockService(deps)\n}\n"],
|
|
5
|
+
"mappings": "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",
|
|
6
6
|
"names": ["lock"]
|
|
7
7
|
}
|
|
@@ -1,6 +1,8 @@
|
|
|
1
1
|
import { buildNotificationFromType } from "@open-mercato/core/modules/notifications/lib/notificationBuilder";
|
|
2
2
|
import { resolveNotificationService } from "@open-mercato/core/modules/notifications/lib/notificationService";
|
|
3
3
|
import { ActionLog } from "@open-mercato/core/modules/audit_logs/data/entities";
|
|
4
|
+
import { findOneWithDecryption } from "@open-mercato/shared/lib/encryption/find";
|
|
5
|
+
import { parseDecryptedFieldValue } from "@open-mercato/shared/lib/encryption/tenantDataEncryptionService";
|
|
4
6
|
import {
|
|
5
7
|
isConflictNotificationEnabled,
|
|
6
8
|
resolveRecordLockNotificationType,
|
|
@@ -19,6 +21,15 @@ function formatChangedFieldLabel(rawField) {
|
|
|
19
21
|
if (!words.length) return trimmedField;
|
|
20
22
|
return words.map((word) => word.charAt(0).toUpperCase() + word.slice(1)).join(" ");
|
|
21
23
|
}
|
|
24
|
+
function readActionLogChanges(log) {
|
|
25
|
+
const rawChanges = log?.changesJson;
|
|
26
|
+
if (rawChanges && typeof rawChanges === "object" && !Array.isArray(rawChanges)) {
|
|
27
|
+
return rawChanges;
|
|
28
|
+
}
|
|
29
|
+
if (typeof rawChanges !== "string") return null;
|
|
30
|
+
const parsed = parseDecryptedFieldValue(rawChanges);
|
|
31
|
+
return parsed && typeof parsed === "object" && !Array.isArray(parsed) ? parsed : null;
|
|
32
|
+
}
|
|
22
33
|
async function handle(payload, ctx) {
|
|
23
34
|
if (!payload.conflictActorUserId) return;
|
|
24
35
|
const notificationsEnabled = await isConflictNotificationEnabled(ctx, {
|
|
@@ -28,8 +39,15 @@ async function handle(payload, ctx) {
|
|
|
28
39
|
if (!notificationsEnabled) return;
|
|
29
40
|
try {
|
|
30
41
|
const em = ctx.resolve("em").fork();
|
|
31
|
-
const incomingLog = payload.incomingActionLogId ? await
|
|
32
|
-
|
|
42
|
+
const incomingLog = payload.incomingActionLogId ? await findOneWithDecryption(
|
|
43
|
+
em,
|
|
44
|
+
ActionLog,
|
|
45
|
+
{ id: payload.incomingActionLogId, deletedAt: null },
|
|
46
|
+
void 0,
|
|
47
|
+
{ tenantId: payload.tenantId, organizationId: payload.organizationId ?? null }
|
|
48
|
+
) : null;
|
|
49
|
+
const changesJson = readActionLogChanges(incomingLog);
|
|
50
|
+
const changedFields = changesJson ? Object.keys(changesJson).slice(0, 12).map(formatChangedFieldLabel).join(", ") : "";
|
|
33
51
|
const notificationService = resolveNotificationService(ctx);
|
|
34
52
|
const typeDef = resolveRecordLockNotificationType("record_locks.conflict.detected");
|
|
35
53
|
if (!typeDef) return;
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"version": 3,
|
|
3
3
|
"sources": ["../../../../src/modules/record_locks/subscribers/conflict-detected-notification.ts"],
|
|
4
|
-
"sourcesContent": ["import { buildNotificationFromType } from '@open-mercato/core/modules/notifications/lib/notificationBuilder'\nimport { resolveNotificationService } from '@open-mercato/core/modules/notifications/lib/notificationService'\nimport type { EntityManager } from '@mikro-orm/postgresql'\nimport { ActionLog } from '@open-mercato/core/modules/audit_logs/data/entities'\nimport {\n isConflictNotificationEnabled,\n resolveRecordLockNotificationType,\n resolveRecordResourceLink,\n} from '../lib/notificationHelpers'\n\nexport const metadata = {\n event: 'record_locks.conflict.detected',\n persistent: true,\n id: 'record_locks:conflict-detected-notification',\n}\n\ntype Payload = {\n conflictId: string\n resourceKind: string\n resourceId: string\n tenantId: string\n organizationId?: string | null\n conflictActorUserId: string\n incomingActorUserId?: string | null\n baseActionLogId?: string | null\n incomingActionLogId?: string | null\n}\n\ntype ResolverContext = {\n resolve: <T = unknown>(name: string) => T\n}\n\nfunction formatChangedFieldLabel(rawField: string): string {\n const trimmedField = rawField.trim()\n const withoutNamespace = trimmedField.includes('::') ? (trimmedField.split('::').pop() ?? trimmedField) : trimmedField\n const withoutPrefix = withoutNamespace.includes('.') ? (withoutNamespace.split('.').pop() ?? withoutNamespace) : withoutNamespace\n const words = withoutPrefix\n .replace(/([a-z0-9])([A-Z])/g, '$1 $2')\n .replace(/[._-]+/g, ' ')\n .replace(/\\s+/g, ' ')\n .trim()\n .split(' ')\n .filter(Boolean)\n\n if (!words.length) return trimmedField\n return words\n .map((word) => word.charAt(0).toUpperCase() + word.slice(1))\n .join(' ')\n}\n\nexport default async function handle(payload: Payload, ctx: ResolverContext) {\n if (!payload.conflictActorUserId) return\n\n const notificationsEnabled = await isConflictNotificationEnabled(ctx, {\n tenantId: payload.tenantId,\n organizationId: payload.organizationId ?? null,\n })\n if (!notificationsEnabled) return\n\n try {\n const em = (ctx.resolve('em') as EntityManager).fork()\n const incomingLog = payload.incomingActionLogId\n ? await em
|
|
5
|
-
"mappings": "AAAA,SAAS,iCAAiC;AAC1C,SAAS,kCAAkC;AAE3C,SAAS,iBAAiB;AAC1B;AAAA,EACE;AAAA,EACA;AAAA,EACA;AAAA,OACK;AAEA,MAAM,WAAW;AAAA,EACtB,OAAO;AAAA,EACP,YAAY;AAAA,EACZ,IAAI;AACN;AAkBA,SAAS,wBAAwB,UAA0B;AACzD,QAAM,eAAe,SAAS,KAAK;AACnC,QAAM,mBAAmB,aAAa,SAAS,IAAI,IAAK,aAAa,MAAM,IAAI,EAAE,IAAI,KAAK,eAAgB;AAC1G,QAAM,gBAAgB,iBAAiB,SAAS,GAAG,IAAK,iBAAiB,MAAM,GAAG,EAAE,IAAI,KAAK,mBAAoB;AACjH,QAAM,QAAQ,cACX,QAAQ,sBAAsB,OAAO,EACrC,QAAQ,WAAW,GAAG,EACtB,QAAQ,QAAQ,GAAG,EACnB,KAAK,EACL,MAAM,GAAG,EACT,OAAO,OAAO;AAEjB,MAAI,CAAC,MAAM,OAAQ,QAAO;AAC1B,SAAO,MACJ,IAAI,CAAC,SAAS,KAAK,OAAO,CAAC,EAAE,YAAY,IAAI,KAAK,MAAM,CAAC,CAAC,EAC1D,KAAK,GAAG;AACb;AAEA,eAAO,OAA8B,SAAkB,KAAsB;AAC3E,MAAI,CAAC,QAAQ,oBAAqB;AAElC,QAAM,uBAAuB,MAAM,8BAA8B,KAAK;AAAA,IACpE,UAAU,QAAQ;AAAA,IAClB,gBAAgB,QAAQ,kBAAkB;AAAA,EAC5C,CAAC;AACD,MAAI,CAAC,qBAAsB;AAE3B,MAAI;AACF,UAAM,KAAM,IAAI,QAAQ,IAAI,EAAoB,KAAK;AACrD,UAAM,cAAc,QAAQ,sBACxB,MAAM,
|
|
4
|
+
"sourcesContent": ["import { buildNotificationFromType } from '@open-mercato/core/modules/notifications/lib/notificationBuilder'\nimport { resolveNotificationService } from '@open-mercato/core/modules/notifications/lib/notificationService'\nimport type { EntityManager } from '@mikro-orm/postgresql'\nimport { ActionLog } from '@open-mercato/core/modules/audit_logs/data/entities'\nimport { findOneWithDecryption } from '@open-mercato/shared/lib/encryption/find'\nimport { parseDecryptedFieldValue } from '@open-mercato/shared/lib/encryption/tenantDataEncryptionService'\nimport {\n isConflictNotificationEnabled,\n resolveRecordLockNotificationType,\n resolveRecordResourceLink,\n} from '../lib/notificationHelpers'\n\nexport const metadata = {\n event: 'record_locks.conflict.detected',\n persistent: true,\n id: 'record_locks:conflict-detected-notification',\n}\n\ntype Payload = {\n conflictId: string\n resourceKind: string\n resourceId: string\n tenantId: string\n organizationId?: string | null\n conflictActorUserId: string\n incomingActorUserId?: string | null\n baseActionLogId?: string | null\n incomingActionLogId?: string | null\n}\n\ntype ResolverContext = {\n resolve: <T = unknown>(name: string) => T\n}\n\nfunction formatChangedFieldLabel(rawField: string): string {\n const trimmedField = rawField.trim()\n const withoutNamespace = trimmedField.includes('::') ? (trimmedField.split('::').pop() ?? trimmedField) : trimmedField\n const withoutPrefix = withoutNamespace.includes('.') ? (withoutNamespace.split('.').pop() ?? withoutNamespace) : withoutNamespace\n const words = withoutPrefix\n .replace(/([a-z0-9])([A-Z])/g, '$1 $2')\n .replace(/[._-]+/g, ' ')\n .replace(/\\s+/g, ' ')\n .trim()\n .split(' ')\n .filter(Boolean)\n\n if (!words.length) return trimmedField\n return words\n .map((word) => word.charAt(0).toUpperCase() + word.slice(1))\n .join(' ')\n}\n\nfunction readActionLogChanges(log: ActionLog | null): Record<string, unknown> | null {\n const rawChanges = log?.changesJson as unknown\n if (rawChanges && typeof rawChanges === 'object' && !Array.isArray(rawChanges)) {\n return rawChanges as Record<string, unknown>\n }\n if (typeof rawChanges !== 'string') return null\n\n const parsed = parseDecryptedFieldValue(rawChanges)\n return parsed && typeof parsed === 'object' && !Array.isArray(parsed)\n ? parsed as Record<string, unknown>\n : null\n}\n\nexport default async function handle(payload: Payload, ctx: ResolverContext) {\n if (!payload.conflictActorUserId) return\n\n const notificationsEnabled = await isConflictNotificationEnabled(ctx, {\n tenantId: payload.tenantId,\n organizationId: payload.organizationId ?? null,\n })\n if (!notificationsEnabled) return\n\n try {\n const em = (ctx.resolve('em') as EntityManager).fork()\n const incomingLog = payload.incomingActionLogId\n ? await findOneWithDecryption(\n em,\n ActionLog,\n { id: payload.incomingActionLogId, deletedAt: null },\n undefined,\n { tenantId: payload.tenantId, organizationId: payload.organizationId ?? null },\n )\n : null\n const changesJson = readActionLogChanges(incomingLog)\n const changedFields = changesJson\n ? Object.keys(changesJson).slice(0, 12).map(formatChangedFieldLabel).join(', ')\n : ''\n\n const notificationService = resolveNotificationService(ctx)\n const typeDef = resolveRecordLockNotificationType('record_locks.conflict.detected')\n if (!typeDef) return\n\n const notificationInput = buildNotificationFromType(typeDef, {\n recipientUserId: payload.conflictActorUserId,\n bodyVariables: {\n resourceKind: payload.resourceKind,\n changedFields: changedFields || '-',\n },\n sourceEntityType: 'record_locks:conflict',\n sourceEntityId: payload.conflictId,\n linkHref: resolveRecordResourceLink(payload.resourceKind, payload.resourceId),\n groupKey: `record_locks.conflict.detected:${payload.conflictId}`,\n })\n\n await notificationService.create(notificationInput, {\n tenantId: payload.tenantId,\n organizationId: payload.organizationId ?? null,\n })\n } catch (error) {\n console.error('[record_locks:conflict-detected-notification] Failed to create notification:', error)\n }\n}\n"],
|
|
5
|
+
"mappings": "AAAA,SAAS,iCAAiC;AAC1C,SAAS,kCAAkC;AAE3C,SAAS,iBAAiB;AAC1B,SAAS,6BAA6B;AACtC,SAAS,gCAAgC;AACzC;AAAA,EACE;AAAA,EACA;AAAA,EACA;AAAA,OACK;AAEA,MAAM,WAAW;AAAA,EACtB,OAAO;AAAA,EACP,YAAY;AAAA,EACZ,IAAI;AACN;AAkBA,SAAS,wBAAwB,UAA0B;AACzD,QAAM,eAAe,SAAS,KAAK;AACnC,QAAM,mBAAmB,aAAa,SAAS,IAAI,IAAK,aAAa,MAAM,IAAI,EAAE,IAAI,KAAK,eAAgB;AAC1G,QAAM,gBAAgB,iBAAiB,SAAS,GAAG,IAAK,iBAAiB,MAAM,GAAG,EAAE,IAAI,KAAK,mBAAoB;AACjH,QAAM,QAAQ,cACX,QAAQ,sBAAsB,OAAO,EACrC,QAAQ,WAAW,GAAG,EACtB,QAAQ,QAAQ,GAAG,EACnB,KAAK,EACL,MAAM,GAAG,EACT,OAAO,OAAO;AAEjB,MAAI,CAAC,MAAM,OAAQ,QAAO;AAC1B,SAAO,MACJ,IAAI,CAAC,SAAS,KAAK,OAAO,CAAC,EAAE,YAAY,IAAI,KAAK,MAAM,CAAC,CAAC,EAC1D,KAAK,GAAG;AACb;AAEA,SAAS,qBAAqB,KAAuD;AACnF,QAAM,aAAa,KAAK;AACxB,MAAI,cAAc,OAAO,eAAe,YAAY,CAAC,MAAM,QAAQ,UAAU,GAAG;AAC9E,WAAO;AAAA,EACT;AACA,MAAI,OAAO,eAAe,SAAU,QAAO;AAE3C,QAAM,SAAS,yBAAyB,UAAU;AAClD,SAAO,UAAU,OAAO,WAAW,YAAY,CAAC,MAAM,QAAQ,MAAM,IAChE,SACA;AACN;AAEA,eAAO,OAA8B,SAAkB,KAAsB;AAC3E,MAAI,CAAC,QAAQ,oBAAqB;AAElC,QAAM,uBAAuB,MAAM,8BAA8B,KAAK;AAAA,IACpE,UAAU,QAAQ;AAAA,IAClB,gBAAgB,QAAQ,kBAAkB;AAAA,EAC5C,CAAC;AACD,MAAI,CAAC,qBAAsB;AAE3B,MAAI;AACF,UAAM,KAAM,IAAI,QAAQ,IAAI,EAAoB,KAAK;AACrD,UAAM,cAAc,QAAQ,sBACxB,MAAM;AAAA,MACJ;AAAA,MACA;AAAA,MACA,EAAE,IAAI,QAAQ,qBAAqB,WAAW,KAAK;AAAA,MACnD;AAAA,MACA,EAAE,UAAU,QAAQ,UAAU,gBAAgB,QAAQ,kBAAkB,KAAK;AAAA,IAC/E,IACA;AACJ,UAAM,cAAc,qBAAqB,WAAW;AACpD,UAAM,gBAAgB,cAClB,OAAO,KAAK,WAAW,EAAE,MAAM,GAAG,EAAE,EAAE,IAAI,uBAAuB,EAAE,KAAK,IAAI,IAC5E;AAEJ,UAAM,sBAAsB,2BAA2B,GAAG;AAC1D,UAAM,UAAU,kCAAkC,gCAAgC;AAClF,QAAI,CAAC,QAAS;AAEd,UAAM,oBAAoB,0BAA0B,SAAS;AAAA,MAC3D,iBAAiB,QAAQ;AAAA,MACzB,eAAe;AAAA,QACb,cAAc,QAAQ;AAAA,QACtB,eAAe,iBAAiB;AAAA,MAClC;AAAA,MACA,kBAAkB;AAAA,MAClB,gBAAgB,QAAQ;AAAA,MACxB,UAAU,0BAA0B,QAAQ,cAAc,QAAQ,UAAU;AAAA,MAC5E,UAAU,kCAAkC,QAAQ,UAAU;AAAA,IAChE,CAAC;AAED,UAAM,oBAAoB,OAAO,mBAAmB;AAAA,MAClD,UAAU,QAAQ;AAAA,MAClB,gBAAgB,QAAQ,kBAAkB;AAAA,IAC5C,CAAC;AAAA,EACH,SAAS,OAAO;AACd,YAAQ,MAAM,gFAAgF,KAAK;AAAA,EACrG;AACF;",
|
|
6
6
|
"names": []
|
|
7
7
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@open-mercato/enterprise",
|
|
3
|
-
"version": "0.6.1-develop.
|
|
3
|
+
"version": "0.6.1-develop.3090.06ab462170",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"main": "./dist/index.js",
|
|
6
6
|
"scripts": {
|
|
@@ -64,8 +64,8 @@
|
|
|
64
64
|
}
|
|
65
65
|
},
|
|
66
66
|
"dependencies": {
|
|
67
|
-
"@open-mercato/core": "0.6.1-develop.
|
|
68
|
-
"@open-mercato/ui": "0.6.1-develop.
|
|
67
|
+
"@open-mercato/core": "0.6.1-develop.3090.06ab462170",
|
|
68
|
+
"@open-mercato/ui": "0.6.1-develop.3090.06ab462170",
|
|
69
69
|
"@simplewebauthn/browser": "^13.3.0",
|
|
70
70
|
"@simplewebauthn/server": "^13.3.0",
|
|
71
71
|
"@simplewebauthn/types": "^12.0.0",
|
|
@@ -75,10 +75,10 @@
|
|
|
75
75
|
"qrcode": "^1.5.4"
|
|
76
76
|
},
|
|
77
77
|
"peerDependencies": {
|
|
78
|
-
"@open-mercato/shared": "0.6.1-develop.
|
|
78
|
+
"@open-mercato/shared": "0.6.1-develop.3090.06ab462170"
|
|
79
79
|
},
|
|
80
80
|
"devDependencies": {
|
|
81
|
-
"@open-mercato/shared": "0.6.1-develop.
|
|
81
|
+
"@open-mercato/shared": "0.6.1-develop.3090.06ab462170",
|
|
82
82
|
"@types/jest": "^30.0.0",
|
|
83
83
|
"jest": "^30.3.0",
|
|
84
84
|
"ts-jest": "^29.4.9"
|
|
@@ -6,6 +6,8 @@ import type { ModuleConfigService } from '@open-mercato/core/modules/configs/lib
|
|
|
6
6
|
import { ActionLog } from '@open-mercato/core/modules/audit_logs/data/entities'
|
|
7
7
|
import type { ActionLogService } from '@open-mercato/core/modules/audit_logs/services/actionLogService'
|
|
8
8
|
import type { RbacService } from '@open-mercato/core/modules/auth/services/rbacService'
|
|
9
|
+
import { findOneWithDecryption } from '@open-mercato/shared/lib/encryption/find'
|
|
10
|
+
import { parseDecryptedFieldValue } from '@open-mercato/shared/lib/encryption/tenantDataEncryptionService'
|
|
9
11
|
import { emitRecordLocksEvent } from '../events'
|
|
10
12
|
import {
|
|
11
13
|
RecordLock,
|
|
@@ -295,6 +297,29 @@ function isRecordValue(value: unknown): value is Record<string, unknown> {
|
|
|
295
297
|
return Boolean(value && typeof value === 'object' && !Array.isArray(value))
|
|
296
298
|
}
|
|
297
299
|
|
|
300
|
+
function parseDecryptedJsonLike(value: unknown): unknown {
|
|
301
|
+
return typeof value === 'string' ? parseDecryptedFieldValue(value) : value
|
|
302
|
+
}
|
|
303
|
+
|
|
304
|
+
function readJsonRecordValue(value: unknown): Record<string, unknown> | null {
|
|
305
|
+
const parsed = parseDecryptedJsonLike(value)
|
|
306
|
+
return isRecordValue(parsed) ? parsed : null
|
|
307
|
+
}
|
|
308
|
+
|
|
309
|
+
function readActionLogChangesJson(log: ActionLog | null): Record<string, unknown> | null {
|
|
310
|
+
return readJsonRecordValue(log?.changesJson ?? null)
|
|
311
|
+
}
|
|
312
|
+
|
|
313
|
+
function normalizeActionLogPayload(log: ActionLog | null): ActionLog | null {
|
|
314
|
+
if (!log) return null
|
|
315
|
+
log.changesJson = readJsonRecordValue(log.changesJson)
|
|
316
|
+
log.snapshotBefore = parseDecryptedJsonLike(log.snapshotBefore)
|
|
317
|
+
log.snapshotAfter = parseDecryptedJsonLike(log.snapshotAfter)
|
|
318
|
+
log.contextJson = readJsonRecordValue(log.contextJson)
|
|
319
|
+
log.commandPayload = parseDecryptedJsonLike(log.commandPayload)
|
|
320
|
+
return log
|
|
321
|
+
}
|
|
322
|
+
|
|
298
323
|
function toIsoDate(value: unknown): string | null {
|
|
299
324
|
if (value instanceof Date) {
|
|
300
325
|
if (Number.isNaN(value.getTime())) return null
|
|
@@ -1491,7 +1516,13 @@ export class RecordLockService {
|
|
|
1491
1516
|
where.organizationId = normalizeScopeOrganization(input.organizationId)
|
|
1492
1517
|
}
|
|
1493
1518
|
|
|
1494
|
-
return
|
|
1519
|
+
return normalizeActionLogPayload(await findOneWithDecryption(
|
|
1520
|
+
this.em,
|
|
1521
|
+
ActionLog,
|
|
1522
|
+
where,
|
|
1523
|
+
{ orderBy: { createdAt: 'desc' } },
|
|
1524
|
+
{ tenantId: input.tenantId, organizationId: normalizeScopeOrganization(input.organizationId) },
|
|
1525
|
+
))
|
|
1495
1526
|
}
|
|
1496
1527
|
|
|
1497
1528
|
private async findLatestActionLogWithScopeFallback(
|
|
@@ -1524,14 +1555,21 @@ export class RecordLockService {
|
|
|
1524
1555
|
where.organizationId = normalizeScopeOrganization(input.organizationId)
|
|
1525
1556
|
}
|
|
1526
1557
|
|
|
1527
|
-
return
|
|
1558
|
+
return normalizeActionLogPayload(await findOneWithDecryption(
|
|
1559
|
+
this.em,
|
|
1560
|
+
ActionLog,
|
|
1561
|
+
where,
|
|
1562
|
+
{ orderBy: { createdAt: 'desc' } },
|
|
1563
|
+
{ tenantId: input.tenantId, organizationId: normalizeScopeOrganization(input.organizationId) },
|
|
1564
|
+
))
|
|
1528
1565
|
}
|
|
1529
1566
|
|
|
1530
1567
|
private summarizeChangedFieldsFromActionLog(log: ActionLog | null): string {
|
|
1531
1568
|
if (!log) return ''
|
|
1532
1569
|
|
|
1533
|
-
|
|
1534
|
-
|
|
1570
|
+
const changesJson = readActionLogChangesJson(log)
|
|
1571
|
+
if (changesJson) {
|
|
1572
|
+
const fromChanges = Object.keys(changesJson)
|
|
1535
1573
|
.filter((field) => !shouldSkipConflictField(field))
|
|
1536
1574
|
.slice(0, 12)
|
|
1537
1575
|
.map(formatChangedFieldLabel)
|
|
@@ -1539,8 +1577,8 @@ export class RecordLockService {
|
|
|
1539
1577
|
if (fromChanges) return fromChanges
|
|
1540
1578
|
}
|
|
1541
1579
|
|
|
1542
|
-
const before =
|
|
1543
|
-
const after =
|
|
1580
|
+
const before = readJsonRecordValue(log.snapshotBefore)
|
|
1581
|
+
const after = readJsonRecordValue(log.snapshotAfter)
|
|
1544
1582
|
if (!before || !after) return ''
|
|
1545
1583
|
|
|
1546
1584
|
const diffPaths = new Set<string>()
|
|
@@ -1559,10 +1597,11 @@ export class RecordLockService {
|
|
|
1559
1597
|
incoming: string
|
|
1560
1598
|
current: string
|
|
1561
1599
|
}> {
|
|
1562
|
-
|
|
1600
|
+
const changesJson = readActionLogChangesJson(log)
|
|
1601
|
+
if (!changesJson) return []
|
|
1563
1602
|
|
|
1564
1603
|
const rows: Array<{ field: string; incoming: string; current: string }> = []
|
|
1565
|
-
for (const [rawField, rawChange] of Object.entries(
|
|
1604
|
+
for (const [rawField, rawChange] of Object.entries(changesJson)) {
|
|
1566
1605
|
if (rows.length >= 12) break
|
|
1567
1606
|
if (shouldSkipConflictField(rawField)) continue
|
|
1568
1607
|
|
|
@@ -1792,8 +1831,15 @@ export class RecordLockService {
|
|
|
1792
1831
|
? await this.actionLogService.findById(logId)
|
|
1793
1832
|
: null
|
|
1794
1833
|
if (!resolved) {
|
|
1795
|
-
resolved = await
|
|
1834
|
+
resolved = await findOneWithDecryption(
|
|
1835
|
+
this.em,
|
|
1836
|
+
ActionLog,
|
|
1837
|
+
{ id: logId, deletedAt: null },
|
|
1838
|
+
undefined,
|
|
1839
|
+
{ tenantId: scope.tenantId, organizationId: normalizeScopeOrganization(scope.organizationId) },
|
|
1840
|
+
)
|
|
1796
1841
|
}
|
|
1842
|
+
resolved = normalizeActionLogPayload(resolved)
|
|
1797
1843
|
if (!resolved || resolved.deletedAt) return null
|
|
1798
1844
|
|
|
1799
1845
|
if (resolved.tenantId !== scope.tenantId) return null
|
|
@@ -1857,14 +1903,14 @@ export class RecordLockService {
|
|
|
1857
1903
|
const baseLog = await this.findActionLogById(conflict.baseActionLogId, scope)
|
|
1858
1904
|
const incomingLog = await this.findActionLogById(conflict.incomingActionLogId, scope)
|
|
1859
1905
|
|
|
1860
|
-
const baseSnapshot =
|
|
1861
|
-
const incomingBeforeSnapshot =
|
|
1862
|
-
const incomingAfterSnapshot =
|
|
1906
|
+
const baseSnapshot = readJsonRecordValue(baseLog?.snapshotAfter ?? null)
|
|
1907
|
+
const incomingBeforeSnapshot = readJsonRecordValue(incomingLog?.snapshotBefore ?? null)
|
|
1908
|
+
const incomingAfterSnapshot = readJsonRecordValue(incomingLog?.snapshotAfter ?? null)
|
|
1863
1909
|
const fallbackBaseSnapshot = baseSnapshot ?? incomingBeforeSnapshot
|
|
1864
1910
|
|
|
1865
1911
|
const changeMap = new Map<string, { baseValue: unknown; incomingValue: unknown }>()
|
|
1866
1912
|
|
|
1867
|
-
const incomingChanges =
|
|
1913
|
+
const incomingChanges = readActionLogChangesJson(incomingLog)
|
|
1868
1914
|
if (incomingChanges) {
|
|
1869
1915
|
for (const [fieldPathRaw, rawChange] of Object.entries(incomingChanges)) {
|
|
1870
1916
|
const fieldPath = fieldPathRaw.trim()
|
|
@@ -2,6 +2,8 @@ import { buildNotificationFromType } from '@open-mercato/core/modules/notificati
|
|
|
2
2
|
import { resolveNotificationService } from '@open-mercato/core/modules/notifications/lib/notificationService'
|
|
3
3
|
import type { EntityManager } from '@mikro-orm/postgresql'
|
|
4
4
|
import { ActionLog } from '@open-mercato/core/modules/audit_logs/data/entities'
|
|
5
|
+
import { findOneWithDecryption } from '@open-mercato/shared/lib/encryption/find'
|
|
6
|
+
import { parseDecryptedFieldValue } from '@open-mercato/shared/lib/encryption/tenantDataEncryptionService'
|
|
5
7
|
import {
|
|
6
8
|
isConflictNotificationEnabled,
|
|
7
9
|
resolveRecordLockNotificationType,
|
|
@@ -48,6 +50,19 @@ function formatChangedFieldLabel(rawField: string): string {
|
|
|
48
50
|
.join(' ')
|
|
49
51
|
}
|
|
50
52
|
|
|
53
|
+
function readActionLogChanges(log: ActionLog | null): Record<string, unknown> | null {
|
|
54
|
+
const rawChanges = log?.changesJson as unknown
|
|
55
|
+
if (rawChanges && typeof rawChanges === 'object' && !Array.isArray(rawChanges)) {
|
|
56
|
+
return rawChanges as Record<string, unknown>
|
|
57
|
+
}
|
|
58
|
+
if (typeof rawChanges !== 'string') return null
|
|
59
|
+
|
|
60
|
+
const parsed = parseDecryptedFieldValue(rawChanges)
|
|
61
|
+
return parsed && typeof parsed === 'object' && !Array.isArray(parsed)
|
|
62
|
+
? parsed as Record<string, unknown>
|
|
63
|
+
: null
|
|
64
|
+
}
|
|
65
|
+
|
|
51
66
|
export default async function handle(payload: Payload, ctx: ResolverContext) {
|
|
52
67
|
if (!payload.conflictActorUserId) return
|
|
53
68
|
|
|
@@ -60,10 +75,17 @@ export default async function handle(payload: Payload, ctx: ResolverContext) {
|
|
|
60
75
|
try {
|
|
61
76
|
const em = (ctx.resolve('em') as EntityManager).fork()
|
|
62
77
|
const incomingLog = payload.incomingActionLogId
|
|
63
|
-
? await
|
|
78
|
+
? await findOneWithDecryption(
|
|
79
|
+
em,
|
|
80
|
+
ActionLog,
|
|
81
|
+
{ id: payload.incomingActionLogId, deletedAt: null },
|
|
82
|
+
undefined,
|
|
83
|
+
{ tenantId: payload.tenantId, organizationId: payload.organizationId ?? null },
|
|
84
|
+
)
|
|
64
85
|
: null
|
|
65
|
-
const
|
|
66
|
-
|
|
86
|
+
const changesJson = readActionLogChanges(incomingLog)
|
|
87
|
+
const changedFields = changesJson
|
|
88
|
+
? Object.keys(changesJson).slice(0, 12).map(formatChangedFieldLabel).join(', ')
|
|
67
89
|
: ''
|
|
68
90
|
|
|
69
91
|
const notificationService = resolveNotificationService(ctx)
|