@open-mercato/core 0.10.1-develop.7888.1.96c8c2bd5f → 0.10.1-develop.7900.1.69c0120c72
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.turbo/turbo-build.log +1 -1
- package/dist/modules/audit_logs/api/audit-logs/actions/route.js +12 -26
- package/dist/modules/audit_logs/api/audit-logs/actions/route.js.map +2 -2
- package/dist/modules/audit_logs/services/actionLogService.js +10 -4
- package/dist/modules/audit_logs/services/actionLogService.js.map +2 -2
- package/dist/modules/customers/backend/customers/deals/page.js +5 -2
- package/dist/modules/customers/backend/customers/deals/page.js.map +2 -2
- package/dist/modules/customers/backend/customers/deals/pipeline/page.js +9 -6
- package/dist/modules/customers/backend/customers/deals/pipeline/page.js.map +2 -2
- package/dist/modules/customers/components/detail/CompanyKpiBar.js +3 -2
- package/dist/modules/customers/components/detail/CompanyKpiBar.js.map +2 -2
- package/dist/modules/customers/lib/duplicateDeal.js +10 -1
- package/dist/modules/customers/lib/duplicateDeal.js.map +2 -2
- package/dist/modules/query_index/lib/coverage.js +36 -19
- package/dist/modules/query_index/lib/coverage.js.map +2 -2
- package/package.json +7 -7
- package/src/modules/audit_logs/api/audit-logs/actions/route.ts +18 -28
- package/src/modules/audit_logs/services/actionLogService.ts +20 -18
- package/src/modules/business_rules/i18n/es.json +1 -1
- package/src/modules/customers/backend/customers/deals/page.tsx +4 -2
- package/src/modules/customers/backend/customers/deals/pipeline/page.tsx +8 -6
- package/src/modules/customers/components/detail/CompanyKpiBar.tsx +2 -1
- package/src/modules/customers/i18n/de.json +6 -0
- package/src/modules/customers/i18n/en.json +6 -0
- package/src/modules/customers/i18n/es.json +6 -0
- package/src/modules/customers/i18n/ko.json +6 -0
- package/src/modules/customers/i18n/pl.json +6 -0
- package/src/modules/customers/lib/duplicateDeal.ts +9 -0
- package/src/modules/query_index/lib/coverage.ts +54 -31
package/.turbo/turbo-build.log
CHANGED
|
@@ -25,10 +25,10 @@ const auditActionQuerySchema = z.object({
|
|
|
25
25
|
includeRelated: z.enum(["true", "false"]).default("false").describe("When `true`, also returns changes to child entities linked via parentResourceKind/parentResourceId").optional(),
|
|
26
26
|
includeTotal: z.enum(["true", "false"]).default("false").describe("When `true`, the response includes the filtered total count.").optional(),
|
|
27
27
|
undoableOnly: z.enum(["true", "false"]).default("false").describe("When `true`, only undoable actions are returned").optional(),
|
|
28
|
-
limit: z.string().describe("Maximum number of records to return (default 50, max 1000)").optional(),
|
|
29
|
-
offset: z.string().describe("Zero-based record offset for pagination (legacy \u2014 prefer page/pageSize)").optional(),
|
|
30
|
-
page: z.string().describe("Page number (default 1)").optional(),
|
|
31
|
-
pageSize: z.string().describe("Page size (default 50, max 200)").optional(),
|
|
28
|
+
limit: z.string().describe("Maximum number of records to return (default 50, max 1000). Ignored when `pageSize` is sent.").optional(),
|
|
29
|
+
offset: z.string().describe("Zero-based record offset for pagination (legacy \u2014 prefer page/pageSize). Takes precedence over `page` when both are sent.").optional(),
|
|
30
|
+
page: z.string().describe("Page number (default 1). Ignored when `offset` is sent.").optional(),
|
|
31
|
+
pageSize: z.string().describe("Page size (default 50, max 200). Takes precedence over `limit` when both are sent.").optional(),
|
|
32
32
|
sortField: z.enum(SORT_FIELDS).describe("Sort field: `createdAt`, `user`, `action`, `field`, or `source`.").optional(),
|
|
33
33
|
sortDir: z.enum(SORT_DIRECTIONS).describe("Sort direction: `asc` or `desc`.").optional(),
|
|
34
34
|
before: z.string().describe("Return actions created before this ISO-8601 timestamp").optional(),
|
|
@@ -74,17 +74,11 @@ function parseDate(value) {
|
|
|
74
74
|
if (Number.isNaN(ts)) return void 0;
|
|
75
75
|
return new Date(ts);
|
|
76
76
|
}
|
|
77
|
-
function
|
|
78
|
-
if (
|
|
77
|
+
function parseOptionalInteger(param, { min, max }) {
|
|
78
|
+
if (param === null || param.trim() === "") return void 0;
|
|
79
79
|
const value = Number(param);
|
|
80
|
-
if (!Number.isFinite(value)) return
|
|
81
|
-
return Math.min(Math.max(Math.trunc(value),
|
|
82
|
-
}
|
|
83
|
-
function parseOffset(param) {
|
|
84
|
-
if (!param) return 0;
|
|
85
|
-
const value = Number(param);
|
|
86
|
-
if (!Number.isFinite(value)) return 0;
|
|
87
|
-
return Math.max(Math.trunc(value), 0);
|
|
80
|
+
if (!Number.isFinite(value)) return void 0;
|
|
81
|
+
return Math.min(Math.max(Math.trunc(value), min), max);
|
|
88
82
|
}
|
|
89
83
|
function splitCsv(value) {
|
|
90
84
|
if (!value) return [];
|
|
@@ -95,14 +89,6 @@ function parseActionTypes(param) {
|
|
|
95
89
|
(value) => ACTION_TYPE_TOKENS.includes(value)
|
|
96
90
|
);
|
|
97
91
|
}
|
|
98
|
-
function parseNumber(param, { min, max, fallback }) {
|
|
99
|
-
if (!param) return fallback;
|
|
100
|
-
const value = Number(param);
|
|
101
|
-
if (!Number.isFinite(value)) return fallback;
|
|
102
|
-
const normalized = Math.trunc(value);
|
|
103
|
-
if (Number.isNaN(normalized)) return fallback;
|
|
104
|
-
return Math.min(Math.max(normalized, min), max);
|
|
105
|
-
}
|
|
106
92
|
async function GET(req) {
|
|
107
93
|
const auth = await getAuthFromRequest(req);
|
|
108
94
|
if (!auth) return NextResponse.json({ error: "Unauthorized" }, { status: 401 });
|
|
@@ -130,10 +116,10 @@ async function GET(req) {
|
|
|
130
116
|
const includeRelated = parseBooleanToken(url.searchParams.get("includeRelated")) === true;
|
|
131
117
|
const includeTotal = parseBooleanToken(url.searchParams.get("includeTotal")) === true;
|
|
132
118
|
const undoableOnly = parseBooleanToken(url.searchParams.get("undoableOnly")) === true;
|
|
133
|
-
const limit =
|
|
134
|
-
const offset =
|
|
135
|
-
const page =
|
|
136
|
-
const pageSize =
|
|
119
|
+
const limit = parseOptionalInteger(url.searchParams.get("limit"), { min: 1, max: 1e3 });
|
|
120
|
+
const offset = parseOptionalInteger(url.searchParams.get("offset"), { min: 0, max: Number.MAX_SAFE_INTEGER });
|
|
121
|
+
const page = parseOptionalInteger(url.searchParams.get("page"), { min: 1, max: 1e6 });
|
|
122
|
+
const pageSize = parseOptionalInteger(url.searchParams.get("pageSize"), { min: 1, max: 200 });
|
|
137
123
|
const sortField = SORT_FIELDS.find((value) => value === url.searchParams.get("sortField")) ?? "createdAt";
|
|
138
124
|
const sortDir = SORT_DIRECTIONS.find((value) => value === url.searchParams.get("sortDir")) ?? "desc";
|
|
139
125
|
const before = parseDate(url.searchParams.get("before"));
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"version": 3,
|
|
3
3
|
"sources": ["../../../../../../src/modules/audit_logs/api/audit-logs/actions/route.ts"],
|
|
4
|
-
"sourcesContent": ["import { NextResponse } from 'next/server'\nimport { createRequestContainer } from '@open-mercato/shared/lib/di/container'\nimport { getAuthFromRequest } from '@open-mercato/shared/lib/auth/server'\nimport { resolveFeatureCheckContext } from '@open-mercato/core/modules/directory/utils/organizationScope'\nimport type { RbacService } from '@open-mercato/core/modules/auth/services/rbacService'\nimport { ActionLogService } from '@open-mercato/core/modules/audit_logs/services/actionLogService'\nimport type { EntityManager } from '@mikro-orm/postgresql'\nimport { loadAuditLogDisplayMaps } from '../display'\nimport { requireResolvedTenantScope } from '../readScope'\nimport { z } from 'zod'\nimport type { OpenApiRouteDoc } from '@open-mercato/shared/lib/openapi'\nimport { parseBooleanToken } from '@open-mercato/shared/lib/boolean'\nimport { ACTION_LOG_FILTER_TYPES } from '@open-mercato/core/modules/audit_logs/lib/projections'\nimport { resolveTranslations } from '@open-mercato/shared/lib/i18n/server'\nimport { resolveCanonicalAuthSubject } from '@open-mercato/core/modules/audit_logs/lib/actorSubject'\n\nexport const metadata = {\n GET: { requireAuth: true, requireFeatures: ['audit_logs.view_self'] },\n}\n\nconst ACTION_TYPE_TOKENS = ACTION_LOG_FILTER_TYPES\nconst SORT_FIELDS = ['createdAt', 'user', 'action', 'field', 'source'] as const\nconst SORT_DIRECTIONS = ['asc', 'desc'] as const\n\nconst auditActionQuerySchema = z.object({\n organizationId: z.string().uuid().describe('Limit results to a specific organization').optional(),\n actorUserId: z\n .string()\n .describe('Filter logs created by specific actor IDs (tenant administrators only). Accepts a single UUID or a comma-separated UUID list.')\n .optional(),\n resourceKind: z.string().describe('Filter by resource kind (e.g., \"order\", \"product\")').optional(),\n resourceId: z.string().describe('Filter by resource ID (UUID of the specific record)').optional(),\n actionType: z\n .string()\n .describe('Filter by action type (`create`, `edit`, `delete`, `assign`). Accepts a single value or a comma-separated list.')\n .optional(),\n fieldName: z\n .string()\n .describe('Filter to entries where the given field changed. Accepts a single field name or a comma-separated list.')\n .optional(),\n includeRelated: z\n .enum(['true', 'false'])\n .default('false')\n .describe('When `true`, also returns changes to child entities linked via parentResourceKind/parentResourceId')\n .optional(),\n includeTotal: z\n .enum(['true', 'false'])\n .default('false')\n .describe('When `true`, the response includes the filtered total count.')\n .optional(),\n undoableOnly: z\n .enum(['true', 'false'])\n .default('false')\n .describe('When `true`, only undoable actions are returned')\n .optional(),\n limit: z.string().describe('Maximum number of records to return (default 50, max 1000)').optional(),\n offset: z.string().describe('Zero-based record offset for pagination (legacy \u2014 prefer page/pageSize)').optional(),\n page: z.string().describe('Page number (default 1)').optional(),\n pageSize: z.string().describe('Page size (default 50, max 200)').optional(),\n sortField: z\n .enum(SORT_FIELDS)\n .describe('Sort field: `createdAt`, `user`, `action`, `field`, or `source`.')\n .optional(),\n sortDir: z\n .enum(SORT_DIRECTIONS)\n .describe('Sort direction: `asc` or `desc`.')\n .optional(),\n before: z.string().describe('Return actions created before this ISO-8601 timestamp').optional(),\n after: z.string().describe('Return actions created after this ISO-8601 timestamp').optional(),\n})\n\nconst auditActionItemSchema = z.object({\n id: z.string(),\n commandId: z.string(),\n actionLabel: z.string().nullable(),\n executionState: z.enum(['done', 'undone', 'failed', 'redone']),\n actorUserId: z.string().uuid().nullable(),\n actorUserName: z.string().nullable(),\n tenantId: z.string().uuid().nullable(),\n tenantName: z.string().nullable(),\n organizationId: z.string().uuid().nullable(),\n organizationName: z.string().nullable(),\n resourceKind: z.string().nullable(),\n resourceId: z.string().nullable(),\n parentResourceKind: z.string().nullable().optional(),\n parentResourceId: z.string().nullable().optional(),\n undoToken: z.string().nullable(),\n createdAt: z.string(),\n updatedAt: z.string(),\n snapshotBefore: z.unknown().nullable(),\n snapshotAfter: z.unknown().nullable(),\n changes: z.record(z.string(), z.unknown()).nullable(),\n context: z.record(z.string(), z.unknown()).nullable(),\n})\n\nconst auditActionResponseSchema = z.object({\n items: z.array(auditActionItemSchema),\n canViewTenant: z.boolean(),\n page: z.number().int(),\n pageSize: z.number().int(),\n total: z.number().int(),\n totalPages: z.number().int(),\n})\n\nconst errorSchema = z.object({\n error: z.string(),\n})\n\nfunction parseDate(value: string | null): Date | undefined {\n if (!value) return undefined\n const ts = Date.parse(value)\n if (Number.isNaN(ts)) return undefined\n return new Date(ts)\n}\n\nfunction parseLimit(param: string | null): number {\n if (!param) return 50\n const value = Number(param)\n if (!Number.isFinite(value)) return 50\n return Math.min(Math.max(Math.trunc(value), 1), 1000)\n}\n\nfunction parseOffset(param: string | null): number {\n if (!param) return 0\n const value = Number(param)\n if (!Number.isFinite(value)) return 0\n return Math.max(Math.trunc(value), 0)\n}\n\nfunction splitCsv(value: string | null): string[] {\n if (!value) return []\n return value\n .split(',')\n .map((entry) => entry.trim())\n .filter(Boolean)\n}\n\nfunction parseActionTypes(param: string | null) {\n return splitCsv(param).filter((value): value is (typeof ACTION_TYPE_TOKENS)[number] =>\n ACTION_TYPE_TOKENS.includes(value as (typeof ACTION_TYPE_TOKENS)[number]),\n )\n}\n\nfunction parseNumber(param: string | null, { min, max, fallback }: { min: number; max: number; fallback: number }) {\n if (!param) return fallback\n const value = Number(param)\n if (!Number.isFinite(value)) return fallback\n const normalized = Math.trunc(value)\n if (Number.isNaN(normalized)) return fallback\n return Math.min(Math.max(normalized, min), max)\n}\n\nexport async function GET(req: Request) {\n const auth = await getAuthFromRequest(req)\n if (!auth) return NextResponse.json({ error: 'Unauthorized' }, { status: 401 })\n const canonicalAuthSubject = resolveCanonicalAuthSubject(auth)\n if (!canonicalAuthSubject) return NextResponse.json({ error: 'Unauthorized' }, { status: 401 })\n\n const tenantScopeGuard = requireResolvedTenantScope(auth)\n if (tenantScopeGuard) return tenantScopeGuard\n\n const container = await createRequestContainer()\n const { organizationId: defaultOrganizationId, scope } = await resolveFeatureCheckContext({ container, auth, request: req })\n\n const rbac = (container.resolve('rbacService') as RbacService)\n const actionLogs = (container.resolve('actionLogService') as ActionLogService)\n const em = (container.resolve('em') as EntityManager)\n\n const canViewTenant = await rbac.userHasAllFeatures(\n auth.sub,\n ['audit_logs.view_tenant'],\n { tenantId: auth.tenantId ?? null, organizationId: defaultOrganizationId ?? null },\n )\n\n const url = new URL(req.url)\n const queryOrgId = url.searchParams.get('organizationId')\n const actorQuery = url.searchParams.get('actorUserId')\n const resourceKind = url.searchParams.get('resourceKind') ?? undefined\n const resourceId = url.searchParams.get('resourceId') ?? undefined\n const actionTypes = parseActionTypes(url.searchParams.get('actionType'))\n const fieldNames = splitCsv(url.searchParams.get('fieldName'))\n const includeRelated = parseBooleanToken(url.searchParams.get('includeRelated')) === true\n const includeTotal = parseBooleanToken(url.searchParams.get('includeTotal')) === true\n const undoableOnly = parseBooleanToken(url.searchParams.get('undoableOnly')) === true\n const limit = parseLimit(url.searchParams.get('limit'))\n const offset = parseOffset(url.searchParams.get('offset'))\n const page = parseNumber(url.searchParams.get('page'), { min: 1, max: 1000000, fallback: 1 })\n const pageSize = parseNumber(url.searchParams.get('pageSize'), { min: 1, max: 200, fallback: 50 })\n const sortField = SORT_FIELDS.find((value) => value === url.searchParams.get('sortField')) ?? 'createdAt'\n const sortDir = SORT_DIRECTIONS.find((value) => value === url.searchParams.get('sortDir')) ?? 'desc'\n const before = parseDate(url.searchParams.get('before'))\n const after = parseDate(url.searchParams.get('after'))\n\n let organizationId = defaultOrganizationId\n if (queryOrgId) {\n if (scope.allowedIds === null || scope.allowedIds.includes(queryOrgId)) {\n organizationId = queryOrgId\n }\n }\n\n const actorSubject = canViewTenant ? undefined : canonicalAuthSubject.subject\n let actorUserId: string | undefined\n let actorUserIds: string[] | undefined\n if (canViewTenant && actorQuery) {\n const parsedActorUserIds = splitCsv(actorQuery)\n if (parsedActorUserIds.length === 1) {\n actorUserId = parsedActorUserIds[0]\n } else if (parsedActorUserIds.length > 1) {\n actorUserId = undefined\n actorUserIds = parsedActorUserIds\n }\n }\n\n const listQuery = {\n tenantId: auth.tenantId ?? undefined,\n organizationId: organizationId ?? undefined,\n actorSubject,\n actorUserId,\n actorUserIds,\n resourceKind,\n resourceId,\n actionTypes,\n fieldNames,\n includeRelated,\n undoableOnly,\n sortField,\n sortDir,\n limit,\n offset,\n page,\n pageSize,\n before,\n after,\n }\n\n // includeTotal flag is retained for backward compatibility but page-based pagination\n // always returns total/totalPages from the list query's findAndCount.\n void includeTotal\n let list: Awaited<ReturnType<ActionLogService['list']>>\n try {\n list = await actionLogs.list(listQuery)\n } catch (err) {\n if (err instanceof z.ZodError) {\n return NextResponse.json({ error: 'Validation failed', details: err.issues }, { status: 400 })\n }\n throw err\n }\n\n const { translate } = await resolveTranslations()\n const displayMaps = await loadAuditLogDisplayMaps(em, {\n userIds: list.items.map((entry: any) => entry.actorUserId).filter((value: any): value is string => !!value),\n tenantIds: list.items.map((entry: any) => entry.tenantId).filter((value: any): value is string => !!value),\n organizationIds: list.items.map((entry: any) => entry.organizationId).filter((value: any): value is string => !!value),\n }, { translate })\n\n const items = list.items.map((entry: any) => ({\n id: entry.id,\n commandId: entry.commandId,\n actionLabel: entry.actionLabel,\n executionState: entry.executionState,\n actorUserId: entry.actorUserId,\n actorUserName: entry.actorUserId ? displayMaps.users[entry.actorUserId] ?? null : null,\n tenantId: entry.tenantId,\n tenantName: entry.tenantId ? displayMaps.tenants[entry.tenantId] ?? null : null,\n organizationId: entry.organizationId,\n organizationName: entry.organizationId ? displayMaps.organizations[entry.organizationId] ?? null : null,\n resourceKind: entry.resourceKind,\n resourceId: entry.resourceId,\n parentResourceKind: entry.parentResourceKind,\n parentResourceId: entry.parentResourceId,\n undoToken: entry.undoToken,\n createdAt: entry.createdAt?.toISOString?.() ?? entry.createdAt,\n updatedAt: entry.updatedAt?.toISOString?.() ?? entry.updatedAt,\n snapshotBefore: entry.snapshotBefore,\n snapshotAfter: entry.snapshotAfter,\n changes: entry.changesJson,\n context: entry.contextJson,\n }))\n\n return NextResponse.json({\n items,\n canViewTenant,\n page: list.page,\n pageSize: list.pageSize,\n total: list.total,\n totalPages: list.totalPages,\n })\n}\n\nexport const openApi: OpenApiRouteDoc = {\n summary: 'List action audit logs',\n description: 'Retrieve recent state-changing actions with undo/redo metadata for the current tenant.',\n methods: {\n GET: {\n summary: 'Fetch action logs',\n description:\n 'Returns recent action audit log entries. Tenant administrators can widen the scope to other actors or organizations, and callers can optionally restrict results to undoable actions.',\n query: auditActionQuerySchema,\n responses: [\n { status: 200, description: 'Action logs retrieved successfully', schema: auditActionResponseSchema },\n ],\n errors: [\n { status: 400, description: 'Invalid filter values', schema: errorSchema },\n { status: 401, description: 'Authentication required', schema: errorSchema },\n { status: 403, description: 'Caller has no resolved tenant scope and is not a superadmin', schema: errorSchema },\n ],\n },\n },\n}\n"],
|
|
5
|
-
"mappings": "AAAA,SAAS,oBAAoB;AAC7B,SAAS,8BAA8B;AACvC,SAAS,0BAA0B;AACnC,SAAS,kCAAkC;AAI3C,SAAS,+BAA+B;AACxC,SAAS,kCAAkC;AAC3C,SAAS,SAAS;AAElB,SAAS,yBAAyB;AAClC,SAAS,+BAA+B;AACxC,SAAS,2BAA2B;AACpC,SAAS,mCAAmC;AAErC,MAAM,WAAW;AAAA,EACtB,KAAK,EAAE,aAAa,MAAM,iBAAiB,CAAC,sBAAsB,EAAE;AACtE;AAEA,MAAM,qBAAqB;AAC3B,MAAM,cAAc,CAAC,aAAa,QAAQ,UAAU,SAAS,QAAQ;AACrE,MAAM,kBAAkB,CAAC,OAAO,MAAM;AAEtC,MAAM,yBAAyB,EAAE,OAAO;AAAA,EACtC,gBAAgB,EAAE,OAAO,EAAE,KAAK,EAAE,SAAS,0CAA0C,EAAE,SAAS;AAAA,EAChG,aAAa,EACV,OAAO,EACP,SAAS,+HAA+H,EACxI,SAAS;AAAA,EACZ,cAAc,EAAE,OAAO,EAAE,SAAS,oDAAoD,EAAE,SAAS;AAAA,EACjG,YAAY,EAAE,OAAO,EAAE,SAAS,qDAAqD,EAAE,SAAS;AAAA,EAChG,YAAY,EACT,OAAO,EACP,SAAS,iHAAiH,EAC1H,SAAS;AAAA,EACZ,WAAW,EACR,OAAO,EACP,SAAS,yGAAyG,EAClH,SAAS;AAAA,EACZ,gBAAgB,EACb,KAAK,CAAC,QAAQ,OAAO,CAAC,EACtB,QAAQ,OAAO,EACf,SAAS,oGAAoG,EAC7G,SAAS;AAAA,EACZ,cAAc,EACX,KAAK,CAAC,QAAQ,OAAO,CAAC,EACtB,QAAQ,OAAO,EACf,SAAS,8DAA8D,EACvE,SAAS;AAAA,EACZ,cAAc,EACX,KAAK,CAAC,QAAQ,OAAO,CAAC,EACtB,QAAQ,OAAO,EACf,SAAS,iDAAiD,EAC1D,SAAS;AAAA,EACZ,OAAO,
|
|
4
|
+
"sourcesContent": ["import { NextResponse } from 'next/server'\nimport { createRequestContainer } from '@open-mercato/shared/lib/di/container'\nimport { getAuthFromRequest } from '@open-mercato/shared/lib/auth/server'\nimport { resolveFeatureCheckContext } from '@open-mercato/core/modules/directory/utils/organizationScope'\nimport type { RbacService } from '@open-mercato/core/modules/auth/services/rbacService'\nimport { ActionLogService } from '@open-mercato/core/modules/audit_logs/services/actionLogService'\nimport type { EntityManager } from '@mikro-orm/postgresql'\nimport { loadAuditLogDisplayMaps } from '../display'\nimport { requireResolvedTenantScope } from '../readScope'\nimport { z } from 'zod'\nimport type { OpenApiRouteDoc } from '@open-mercato/shared/lib/openapi'\nimport { parseBooleanToken } from '@open-mercato/shared/lib/boolean'\nimport { ACTION_LOG_FILTER_TYPES } from '@open-mercato/core/modules/audit_logs/lib/projections'\nimport { resolveTranslations } from '@open-mercato/shared/lib/i18n/server'\nimport { resolveCanonicalAuthSubject } from '@open-mercato/core/modules/audit_logs/lib/actorSubject'\n\nexport const metadata = {\n GET: { requireAuth: true, requireFeatures: ['audit_logs.view_self'] },\n}\n\nconst ACTION_TYPE_TOKENS = ACTION_LOG_FILTER_TYPES\nconst SORT_FIELDS = ['createdAt', 'user', 'action', 'field', 'source'] as const\nconst SORT_DIRECTIONS = ['asc', 'desc'] as const\n\nconst auditActionQuerySchema = z.object({\n organizationId: z.string().uuid().describe('Limit results to a specific organization').optional(),\n actorUserId: z\n .string()\n .describe('Filter logs created by specific actor IDs (tenant administrators only). Accepts a single UUID or a comma-separated UUID list.')\n .optional(),\n resourceKind: z.string().describe('Filter by resource kind (e.g., \"order\", \"product\")').optional(),\n resourceId: z.string().describe('Filter by resource ID (UUID of the specific record)').optional(),\n actionType: z\n .string()\n .describe('Filter by action type (`create`, `edit`, `delete`, `assign`). Accepts a single value or a comma-separated list.')\n .optional(),\n fieldName: z\n .string()\n .describe('Filter to entries where the given field changed. Accepts a single field name or a comma-separated list.')\n .optional(),\n includeRelated: z\n .enum(['true', 'false'])\n .default('false')\n .describe('When `true`, also returns changes to child entities linked via parentResourceKind/parentResourceId')\n .optional(),\n includeTotal: z\n .enum(['true', 'false'])\n .default('false')\n .describe('When `true`, the response includes the filtered total count.')\n .optional(),\n undoableOnly: z\n .enum(['true', 'false'])\n .default('false')\n .describe('When `true`, only undoable actions are returned')\n .optional(),\n limit: z\n .string()\n .describe('Maximum number of records to return (default 50, max 1000). Ignored when `pageSize` is sent.')\n .optional(),\n offset: z\n .string()\n .describe('Zero-based record offset for pagination (legacy \u2014 prefer page/pageSize). Takes precedence over `page` when both are sent.')\n .optional(),\n page: z.string().describe('Page number (default 1). Ignored when `offset` is sent.').optional(),\n pageSize: z.string().describe('Page size (default 50, max 200). Takes precedence over `limit` when both are sent.').optional(),\n sortField: z\n .enum(SORT_FIELDS)\n .describe('Sort field: `createdAt`, `user`, `action`, `field`, or `source`.')\n .optional(),\n sortDir: z\n .enum(SORT_DIRECTIONS)\n .describe('Sort direction: `asc` or `desc`.')\n .optional(),\n before: z.string().describe('Return actions created before this ISO-8601 timestamp').optional(),\n after: z.string().describe('Return actions created after this ISO-8601 timestamp').optional(),\n})\n\nconst auditActionItemSchema = z.object({\n id: z.string(),\n commandId: z.string(),\n actionLabel: z.string().nullable(),\n executionState: z.enum(['done', 'undone', 'failed', 'redone']),\n actorUserId: z.string().uuid().nullable(),\n actorUserName: z.string().nullable(),\n tenantId: z.string().uuid().nullable(),\n tenantName: z.string().nullable(),\n organizationId: z.string().uuid().nullable(),\n organizationName: z.string().nullable(),\n resourceKind: z.string().nullable(),\n resourceId: z.string().nullable(),\n parentResourceKind: z.string().nullable().optional(),\n parentResourceId: z.string().nullable().optional(),\n undoToken: z.string().nullable(),\n createdAt: z.string(),\n updatedAt: z.string(),\n snapshotBefore: z.unknown().nullable(),\n snapshotAfter: z.unknown().nullable(),\n changes: z.record(z.string(), z.unknown()).nullable(),\n context: z.record(z.string(), z.unknown()).nullable(),\n})\n\nconst auditActionResponseSchema = z.object({\n items: z.array(auditActionItemSchema),\n canViewTenant: z.boolean(),\n page: z.number().int(),\n pageSize: z.number().int(),\n total: z.number().int(),\n totalPages: z.number().int(),\n})\n\nconst errorSchema = z.object({\n error: z.string(),\n})\n\nfunction parseDate(value: string | null): Date | undefined {\n if (!value) return undefined\n const ts = Date.parse(value)\n if (Number.isNaN(ts)) return undefined\n return new Date(ts)\n}\n\nfunction parseOptionalInteger(param: string | null, { min, max }: { min: number; max: number }): number | undefined {\n if (param === null || param.trim() === '') return undefined\n const value = Number(param)\n if (!Number.isFinite(value)) return undefined\n return Math.min(Math.max(Math.trunc(value), min), max)\n}\n\nfunction splitCsv(value: string | null): string[] {\n if (!value) return []\n return value\n .split(',')\n .map((entry) => entry.trim())\n .filter(Boolean)\n}\n\nfunction parseActionTypes(param: string | null) {\n return splitCsv(param).filter((value): value is (typeof ACTION_TYPE_TOKENS)[number] =>\n ACTION_TYPE_TOKENS.includes(value as (typeof ACTION_TYPE_TOKENS)[number]),\n )\n}\n\nexport async function GET(req: Request) {\n const auth = await getAuthFromRequest(req)\n if (!auth) return NextResponse.json({ error: 'Unauthorized' }, { status: 401 })\n const canonicalAuthSubject = resolveCanonicalAuthSubject(auth)\n if (!canonicalAuthSubject) return NextResponse.json({ error: 'Unauthorized' }, { status: 401 })\n\n const tenantScopeGuard = requireResolvedTenantScope(auth)\n if (tenantScopeGuard) return tenantScopeGuard\n\n const container = await createRequestContainer()\n const { organizationId: defaultOrganizationId, scope } = await resolveFeatureCheckContext({ container, auth, request: req })\n\n const rbac = (container.resolve('rbacService') as RbacService)\n const actionLogs = (container.resolve('actionLogService') as ActionLogService)\n const em = (container.resolve('em') as EntityManager)\n\n const canViewTenant = await rbac.userHasAllFeatures(\n auth.sub,\n ['audit_logs.view_tenant'],\n { tenantId: auth.tenantId ?? null, organizationId: defaultOrganizationId ?? null },\n )\n\n const url = new URL(req.url)\n const queryOrgId = url.searchParams.get('organizationId')\n const actorQuery = url.searchParams.get('actorUserId')\n const resourceKind = url.searchParams.get('resourceKind') ?? undefined\n const resourceId = url.searchParams.get('resourceId') ?? undefined\n const actionTypes = parseActionTypes(url.searchParams.get('actionType'))\n const fieldNames = splitCsv(url.searchParams.get('fieldName'))\n const includeRelated = parseBooleanToken(url.searchParams.get('includeRelated')) === true\n const includeTotal = parseBooleanToken(url.searchParams.get('includeTotal')) === true\n const undoableOnly = parseBooleanToken(url.searchParams.get('undoableOnly')) === true\n const limit = parseOptionalInteger(url.searchParams.get('limit'), { min: 1, max: 1000 })\n const offset = parseOptionalInteger(url.searchParams.get('offset'), { min: 0, max: Number.MAX_SAFE_INTEGER })\n const page = parseOptionalInteger(url.searchParams.get('page'), { min: 1, max: 1000000 })\n const pageSize = parseOptionalInteger(url.searchParams.get('pageSize'), { min: 1, max: 200 })\n const sortField = SORT_FIELDS.find((value) => value === url.searchParams.get('sortField')) ?? 'createdAt'\n const sortDir = SORT_DIRECTIONS.find((value) => value === url.searchParams.get('sortDir')) ?? 'desc'\n const before = parseDate(url.searchParams.get('before'))\n const after = parseDate(url.searchParams.get('after'))\n\n let organizationId = defaultOrganizationId\n if (queryOrgId) {\n if (scope.allowedIds === null || scope.allowedIds.includes(queryOrgId)) {\n organizationId = queryOrgId\n }\n }\n\n const actorSubject = canViewTenant ? undefined : canonicalAuthSubject.subject\n let actorUserId: string | undefined\n let actorUserIds: string[] | undefined\n if (canViewTenant && actorQuery) {\n const parsedActorUserIds = splitCsv(actorQuery)\n if (parsedActorUserIds.length === 1) {\n actorUserId = parsedActorUserIds[0]\n } else if (parsedActorUserIds.length > 1) {\n actorUserId = undefined\n actorUserIds = parsedActorUserIds\n }\n }\n\n const listQuery = {\n tenantId: auth.tenantId ?? undefined,\n organizationId: organizationId ?? undefined,\n actorSubject,\n actorUserId,\n actorUserIds,\n resourceKind,\n resourceId,\n actionTypes,\n fieldNames,\n includeRelated,\n undoableOnly,\n sortField,\n sortDir,\n limit,\n offset,\n page,\n pageSize,\n before,\n after,\n }\n\n // includeTotal flag is retained for backward compatibility but page-based pagination\n // always returns total/totalPages from the list query's findAndCount.\n void includeTotal\n let list: Awaited<ReturnType<ActionLogService['list']>>\n try {\n list = await actionLogs.list(listQuery)\n } catch (err) {\n if (err instanceof z.ZodError) {\n return NextResponse.json({ error: 'Validation failed', details: err.issues }, { status: 400 })\n }\n throw err\n }\n\n const { translate } = await resolveTranslations()\n const displayMaps = await loadAuditLogDisplayMaps(em, {\n userIds: list.items.map((entry: any) => entry.actorUserId).filter((value: any): value is string => !!value),\n tenantIds: list.items.map((entry: any) => entry.tenantId).filter((value: any): value is string => !!value),\n organizationIds: list.items.map((entry: any) => entry.organizationId).filter((value: any): value is string => !!value),\n }, { translate })\n\n const items = list.items.map((entry: any) => ({\n id: entry.id,\n commandId: entry.commandId,\n actionLabel: entry.actionLabel,\n executionState: entry.executionState,\n actorUserId: entry.actorUserId,\n actorUserName: entry.actorUserId ? displayMaps.users[entry.actorUserId] ?? null : null,\n tenantId: entry.tenantId,\n tenantName: entry.tenantId ? displayMaps.tenants[entry.tenantId] ?? null : null,\n organizationId: entry.organizationId,\n organizationName: entry.organizationId ? displayMaps.organizations[entry.organizationId] ?? null : null,\n resourceKind: entry.resourceKind,\n resourceId: entry.resourceId,\n parentResourceKind: entry.parentResourceKind,\n parentResourceId: entry.parentResourceId,\n undoToken: entry.undoToken,\n createdAt: entry.createdAt?.toISOString?.() ?? entry.createdAt,\n updatedAt: entry.updatedAt?.toISOString?.() ?? entry.updatedAt,\n snapshotBefore: entry.snapshotBefore,\n snapshotAfter: entry.snapshotAfter,\n changes: entry.changesJson,\n context: entry.contextJson,\n }))\n\n return NextResponse.json({\n items,\n canViewTenant,\n page: list.page,\n pageSize: list.pageSize,\n total: list.total,\n totalPages: list.totalPages,\n })\n}\n\nexport const openApi: OpenApiRouteDoc = {\n summary: 'List action audit logs',\n description: 'Retrieve recent state-changing actions with undo/redo metadata for the current tenant.',\n methods: {\n GET: {\n summary: 'Fetch action logs',\n description:\n 'Returns recent action audit log entries. Tenant administrators can widen the scope to other actors or organizations, and callers can optionally restrict results to undoable actions.',\n query: auditActionQuerySchema,\n responses: [\n { status: 200, description: 'Action logs retrieved successfully', schema: auditActionResponseSchema },\n ],\n errors: [\n { status: 400, description: 'Invalid filter values', schema: errorSchema },\n { status: 401, description: 'Authentication required', schema: errorSchema },\n { status: 403, description: 'Caller has no resolved tenant scope and is not a superadmin', schema: errorSchema },\n ],\n },\n },\n}\n"],
|
|
5
|
+
"mappings": "AAAA,SAAS,oBAAoB;AAC7B,SAAS,8BAA8B;AACvC,SAAS,0BAA0B;AACnC,SAAS,kCAAkC;AAI3C,SAAS,+BAA+B;AACxC,SAAS,kCAAkC;AAC3C,SAAS,SAAS;AAElB,SAAS,yBAAyB;AAClC,SAAS,+BAA+B;AACxC,SAAS,2BAA2B;AACpC,SAAS,mCAAmC;AAErC,MAAM,WAAW;AAAA,EACtB,KAAK,EAAE,aAAa,MAAM,iBAAiB,CAAC,sBAAsB,EAAE;AACtE;AAEA,MAAM,qBAAqB;AAC3B,MAAM,cAAc,CAAC,aAAa,QAAQ,UAAU,SAAS,QAAQ;AACrE,MAAM,kBAAkB,CAAC,OAAO,MAAM;AAEtC,MAAM,yBAAyB,EAAE,OAAO;AAAA,EACtC,gBAAgB,EAAE,OAAO,EAAE,KAAK,EAAE,SAAS,0CAA0C,EAAE,SAAS;AAAA,EAChG,aAAa,EACV,OAAO,EACP,SAAS,+HAA+H,EACxI,SAAS;AAAA,EACZ,cAAc,EAAE,OAAO,EAAE,SAAS,oDAAoD,EAAE,SAAS;AAAA,EACjG,YAAY,EAAE,OAAO,EAAE,SAAS,qDAAqD,EAAE,SAAS;AAAA,EAChG,YAAY,EACT,OAAO,EACP,SAAS,iHAAiH,EAC1H,SAAS;AAAA,EACZ,WAAW,EACR,OAAO,EACP,SAAS,yGAAyG,EAClH,SAAS;AAAA,EACZ,gBAAgB,EACb,KAAK,CAAC,QAAQ,OAAO,CAAC,EACtB,QAAQ,OAAO,EACf,SAAS,oGAAoG,EAC7G,SAAS;AAAA,EACZ,cAAc,EACX,KAAK,CAAC,QAAQ,OAAO,CAAC,EACtB,QAAQ,OAAO,EACf,SAAS,8DAA8D,EACvE,SAAS;AAAA,EACZ,cAAc,EACX,KAAK,CAAC,QAAQ,OAAO,CAAC,EACtB,QAAQ,OAAO,EACf,SAAS,iDAAiD,EAC1D,SAAS;AAAA,EACZ,OAAO,EACJ,OAAO,EACP,SAAS,8FAA8F,EACvG,SAAS;AAAA,EACZ,QAAQ,EACL,OAAO,EACP,SAAS,gIAA2H,EACpI,SAAS;AAAA,EACZ,MAAM,EAAE,OAAO,EAAE,SAAS,yDAAyD,EAAE,SAAS;AAAA,EAC9F,UAAU,EAAE,OAAO,EAAE,SAAS,oFAAoF,EAAE,SAAS;AAAA,EAC7H,WAAW,EACR,KAAK,WAAW,EAChB,SAAS,kEAAkE,EAC3E,SAAS;AAAA,EACZ,SAAS,EACN,KAAK,eAAe,EACpB,SAAS,kCAAkC,EAC3C,SAAS;AAAA,EACZ,QAAQ,EAAE,OAAO,EAAE,SAAS,uDAAuD,EAAE,SAAS;AAAA,EAC9F,OAAO,EAAE,OAAO,EAAE,SAAS,sDAAsD,EAAE,SAAS;AAC9F,CAAC;AAED,MAAM,wBAAwB,EAAE,OAAO;AAAA,EACrC,IAAI,EAAE,OAAO;AAAA,EACb,WAAW,EAAE,OAAO;AAAA,EACpB,aAAa,EAAE,OAAO,EAAE,SAAS;AAAA,EACjC,gBAAgB,EAAE,KAAK,CAAC,QAAQ,UAAU,UAAU,QAAQ,CAAC;AAAA,EAC7D,aAAa,EAAE,OAAO,EAAE,KAAK,EAAE,SAAS;AAAA,EACxC,eAAe,EAAE,OAAO,EAAE,SAAS;AAAA,EACnC,UAAU,EAAE,OAAO,EAAE,KAAK,EAAE,SAAS;AAAA,EACrC,YAAY,EAAE,OAAO,EAAE,SAAS;AAAA,EAChC,gBAAgB,EAAE,OAAO,EAAE,KAAK,EAAE,SAAS;AAAA,EAC3C,kBAAkB,EAAE,OAAO,EAAE,SAAS;AAAA,EACtC,cAAc,EAAE,OAAO,EAAE,SAAS;AAAA,EAClC,YAAY,EAAE,OAAO,EAAE,SAAS;AAAA,EAChC,oBAAoB,EAAE,OAAO,EAAE,SAAS,EAAE,SAAS;AAAA,EACnD,kBAAkB,EAAE,OAAO,EAAE,SAAS,EAAE,SAAS;AAAA,EACjD,WAAW,EAAE,OAAO,EAAE,SAAS;AAAA,EAC/B,WAAW,EAAE,OAAO;AAAA,EACpB,WAAW,EAAE,OAAO;AAAA,EACpB,gBAAgB,EAAE,QAAQ,EAAE,SAAS;AAAA,EACrC,eAAe,EAAE,QAAQ,EAAE,SAAS;AAAA,EACpC,SAAS,EAAE,OAAO,EAAE,OAAO,GAAG,EAAE,QAAQ,CAAC,EAAE,SAAS;AAAA,EACpD,SAAS,EAAE,OAAO,EAAE,OAAO,GAAG,EAAE,QAAQ,CAAC,EAAE,SAAS;AACtD,CAAC;AAED,MAAM,4BAA4B,EAAE,OAAO;AAAA,EACzC,OAAO,EAAE,MAAM,qBAAqB;AAAA,EACpC,eAAe,EAAE,QAAQ;AAAA,EACzB,MAAM,EAAE,OAAO,EAAE,IAAI;AAAA,EACrB,UAAU,EAAE,OAAO,EAAE,IAAI;AAAA,EACzB,OAAO,EAAE,OAAO,EAAE,IAAI;AAAA,EACtB,YAAY,EAAE,OAAO,EAAE,IAAI;AAC7B,CAAC;AAED,MAAM,cAAc,EAAE,OAAO;AAAA,EAC3B,OAAO,EAAE,OAAO;AAClB,CAAC;AAED,SAAS,UAAU,OAAwC;AACzD,MAAI,CAAC,MAAO,QAAO;AACnB,QAAM,KAAK,KAAK,MAAM,KAAK;AAC3B,MAAI,OAAO,MAAM,EAAE,EAAG,QAAO;AAC7B,SAAO,IAAI,KAAK,EAAE;AACpB;AAEA,SAAS,qBAAqB,OAAsB,EAAE,KAAK,IAAI,GAAqD;AAClH,MAAI,UAAU,QAAQ,MAAM,KAAK,MAAM,GAAI,QAAO;AAClD,QAAM,QAAQ,OAAO,KAAK;AAC1B,MAAI,CAAC,OAAO,SAAS,KAAK,EAAG,QAAO;AACpC,SAAO,KAAK,IAAI,KAAK,IAAI,KAAK,MAAM,KAAK,GAAG,GAAG,GAAG,GAAG;AACvD;AAEA,SAAS,SAAS,OAAgC;AAChD,MAAI,CAAC,MAAO,QAAO,CAAC;AACpB,SAAO,MACJ,MAAM,GAAG,EACT,IAAI,CAAC,UAAU,MAAM,KAAK,CAAC,EAC3B,OAAO,OAAO;AACnB;AAEA,SAAS,iBAAiB,OAAsB;AAC9C,SAAO,SAAS,KAAK,EAAE;AAAA,IAAO,CAAC,UAC7B,mBAAmB,SAAS,KAA4C;AAAA,EAC1E;AACF;AAEA,eAAsB,IAAI,KAAc;AACtC,QAAM,OAAO,MAAM,mBAAmB,GAAG;AACzC,MAAI,CAAC,KAAM,QAAO,aAAa,KAAK,EAAE,OAAO,eAAe,GAAG,EAAE,QAAQ,IAAI,CAAC;AAC9E,QAAM,uBAAuB,4BAA4B,IAAI;AAC7D,MAAI,CAAC,qBAAsB,QAAO,aAAa,KAAK,EAAE,OAAO,eAAe,GAAG,EAAE,QAAQ,IAAI,CAAC;AAE9F,QAAM,mBAAmB,2BAA2B,IAAI;AACxD,MAAI,iBAAkB,QAAO;AAE7B,QAAM,YAAY,MAAM,uBAAuB;AAC/C,QAAM,EAAE,gBAAgB,uBAAuB,MAAM,IAAI,MAAM,2BAA2B,EAAE,WAAW,MAAM,SAAS,IAAI,CAAC;AAE3H,QAAM,OAAQ,UAAU,QAAQ,aAAa;AAC7C,QAAM,aAAc,UAAU,QAAQ,kBAAkB;AACxD,QAAM,KAAM,UAAU,QAAQ,IAAI;AAElC,QAAM,gBAAgB,MAAM,KAAK;AAAA,IAC/B,KAAK;AAAA,IACL,CAAC,wBAAwB;AAAA,IACzB,EAAE,UAAU,KAAK,YAAY,MAAM,gBAAgB,yBAAyB,KAAK;AAAA,EACnF;AAEA,QAAM,MAAM,IAAI,IAAI,IAAI,GAAG;AAC3B,QAAM,aAAa,IAAI,aAAa,IAAI,gBAAgB;AACxD,QAAM,aAAa,IAAI,aAAa,IAAI,aAAa;AACrD,QAAM,eAAe,IAAI,aAAa,IAAI,cAAc,KAAK;AAC7D,QAAM,aAAa,IAAI,aAAa,IAAI,YAAY,KAAK;AACzD,QAAM,cAAc,iBAAiB,IAAI,aAAa,IAAI,YAAY,CAAC;AACvE,QAAM,aAAa,SAAS,IAAI,aAAa,IAAI,WAAW,CAAC;AAC7D,QAAM,iBAAiB,kBAAkB,IAAI,aAAa,IAAI,gBAAgB,CAAC,MAAM;AACrF,QAAM,eAAe,kBAAkB,IAAI,aAAa,IAAI,cAAc,CAAC,MAAM;AACjF,QAAM,eAAe,kBAAkB,IAAI,aAAa,IAAI,cAAc,CAAC,MAAM;AACjF,QAAM,QAAQ,qBAAqB,IAAI,aAAa,IAAI,OAAO,GAAG,EAAE,KAAK,GAAG,KAAK,IAAK,CAAC;AACvF,QAAM,SAAS,qBAAqB,IAAI,aAAa,IAAI,QAAQ,GAAG,EAAE,KAAK,GAAG,KAAK,OAAO,iBAAiB,CAAC;AAC5G,QAAM,OAAO,qBAAqB,IAAI,aAAa,IAAI,MAAM,GAAG,EAAE,KAAK,GAAG,KAAK,IAAQ,CAAC;AACxF,QAAM,WAAW,qBAAqB,IAAI,aAAa,IAAI,UAAU,GAAG,EAAE,KAAK,GAAG,KAAK,IAAI,CAAC;AAC5F,QAAM,YAAY,YAAY,KAAK,CAAC,UAAU,UAAU,IAAI,aAAa,IAAI,WAAW,CAAC,KAAK;AAC9F,QAAM,UAAU,gBAAgB,KAAK,CAAC,UAAU,UAAU,IAAI,aAAa,IAAI,SAAS,CAAC,KAAK;AAC9F,QAAM,SAAS,UAAU,IAAI,aAAa,IAAI,QAAQ,CAAC;AACvD,QAAM,QAAQ,UAAU,IAAI,aAAa,IAAI,OAAO,CAAC;AAErD,MAAI,iBAAiB;AACrB,MAAI,YAAY;AACd,QAAI,MAAM,eAAe,QAAQ,MAAM,WAAW,SAAS,UAAU,GAAG;AACtE,uBAAiB;AAAA,IACnB;AAAA,EACF;AAEA,QAAM,eAAe,gBAAgB,SAAY,qBAAqB;AACtE,MAAI;AACJ,MAAI;AACJ,MAAI,iBAAiB,YAAY;AAC/B,UAAM,qBAAqB,SAAS,UAAU;AAC9C,QAAI,mBAAmB,WAAW,GAAG;AACnC,oBAAc,mBAAmB,CAAC;AAAA,IACpC,WAAW,mBAAmB,SAAS,GAAG;AACxC,oBAAc;AACd,qBAAe;AAAA,IACjB;AAAA,EACF;AAEA,QAAM,YAAY;AAAA,IAChB,UAAU,KAAK,YAAY;AAAA,IAC3B,gBAAgB,kBAAkB;AAAA,IAClC;AAAA,IACA;AAAA,IACA;AAAA,IACA;AAAA,IACA;AAAA,IACA;AAAA,IACA;AAAA,IACA;AAAA,IACA;AAAA,IACA;AAAA,IACA;AAAA,IACA;AAAA,IACA;AAAA,IACA;AAAA,IACA;AAAA,IACA;AAAA,IACA;AAAA,EACF;AAIA,OAAK;AACL,MAAI;AACJ,MAAI;AACF,WAAO,MAAM,WAAW,KAAK,SAAS;AAAA,EACxC,SAAS,KAAK;AACZ,QAAI,eAAe,EAAE,UAAU;AAC7B,aAAO,aAAa,KAAK,EAAE,OAAO,qBAAqB,SAAS,IAAI,OAAO,GAAG,EAAE,QAAQ,IAAI,CAAC;AAAA,IAC/F;AACA,UAAM;AAAA,EACR;AAEA,QAAM,EAAE,UAAU,IAAI,MAAM,oBAAoB;AAChD,QAAM,cAAc,MAAM,wBAAwB,IAAI;AAAA,IACpD,SAAS,KAAK,MAAM,IAAI,CAAC,UAAe,MAAM,WAAW,EAAE,OAAO,CAAC,UAAgC,CAAC,CAAC,KAAK;AAAA,IAC1G,WAAW,KAAK,MAAM,IAAI,CAAC,UAAe,MAAM,QAAQ,EAAE,OAAO,CAAC,UAAgC,CAAC,CAAC,KAAK;AAAA,IACzG,iBAAiB,KAAK,MAAM,IAAI,CAAC,UAAe,MAAM,cAAc,EAAE,OAAO,CAAC,UAAgC,CAAC,CAAC,KAAK;AAAA,EACvH,GAAG,EAAE,UAAU,CAAC;AAEhB,QAAM,QAAQ,KAAK,MAAM,IAAI,CAAC,WAAgB;AAAA,IAC5C,IAAI,MAAM;AAAA,IACV,WAAW,MAAM;AAAA,IACjB,aAAa,MAAM;AAAA,IACnB,gBAAgB,MAAM;AAAA,IACtB,aAAa,MAAM;AAAA,IACnB,eAAe,MAAM,cAAc,YAAY,MAAM,MAAM,WAAW,KAAK,OAAO;AAAA,IAClF,UAAU,MAAM;AAAA,IAChB,YAAY,MAAM,WAAW,YAAY,QAAQ,MAAM,QAAQ,KAAK,OAAO;AAAA,IAC3E,gBAAgB,MAAM;AAAA,IACtB,kBAAkB,MAAM,iBAAiB,YAAY,cAAc,MAAM,cAAc,KAAK,OAAO;AAAA,IACnG,cAAc,MAAM;AAAA,IACpB,YAAY,MAAM;AAAA,IAClB,oBAAoB,MAAM;AAAA,IAC1B,kBAAkB,MAAM;AAAA,IACxB,WAAW,MAAM;AAAA,IACjB,WAAW,MAAM,WAAW,cAAc,KAAK,MAAM;AAAA,IACrD,WAAW,MAAM,WAAW,cAAc,KAAK,MAAM;AAAA,IACrD,gBAAgB,MAAM;AAAA,IACtB,eAAe,MAAM;AAAA,IACrB,SAAS,MAAM;AAAA,IACf,SAAS,MAAM;AAAA,EACjB,EAAE;AAEF,SAAO,aAAa,KAAK;AAAA,IACvB;AAAA,IACA;AAAA,IACA,MAAM,KAAK;AAAA,IACX,UAAU,KAAK;AAAA,IACf,OAAO,KAAK;AAAA,IACZ,YAAY,KAAK;AAAA,EACnB,CAAC;AACH;AAEO,MAAM,UAA2B;AAAA,EACtC,SAAS;AAAA,EACT,aAAa;AAAA,EACb,SAAS;AAAA,IACP,KAAK;AAAA,MACH,SAAS;AAAA,MACT,aACE;AAAA,MACF,OAAO;AAAA,MACP,WAAW;AAAA,QACT,EAAE,QAAQ,KAAK,aAAa,sCAAsC,QAAQ,0BAA0B;AAAA,MACtG;AAAA,MACA,QAAQ;AAAA,QACN,EAAE,QAAQ,KAAK,aAAa,yBAAyB,QAAQ,YAAY;AAAA,QACzE,EAAE,QAAQ,KAAK,aAAa,2BAA2B,QAAQ,YAAY;AAAA,QAC3E,EAAE,QAAQ,KAAK,aAAa,+DAA+D,QAAQ,YAAY;AAAA,MACjH;AAAA,IACF;AAAA,EACF;AACF;",
|
|
6
6
|
"names": []
|
|
7
7
|
}
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { raw } from "@mikro-orm/core";
|
|
2
2
|
import { sql } from "kysely";
|
|
3
|
+
import { z } from "zod";
|
|
3
4
|
import { ActionLog } from "@open-mercato/core/modules/audit_logs/data/entities";
|
|
4
5
|
import {
|
|
5
6
|
actionLogCreateSchema,
|
|
@@ -23,6 +24,11 @@ import {
|
|
|
23
24
|
canonicalizeActorSubject
|
|
24
25
|
} from "@open-mercato/core/modules/audit_logs/lib/actorSubject";
|
|
25
26
|
const logger = createLogger("audit_logs").child({ component: "action-log-service" });
|
|
27
|
+
const DEFAULT_LIST_PAGE_SIZE = 50;
|
|
28
|
+
const actionLogListQuerySchema = actionLogListSchema.extend({
|
|
29
|
+
page: z.number().int().positive().optional(),
|
|
30
|
+
pageSize: z.number().int().positive().max(200).optional()
|
|
31
|
+
});
|
|
26
32
|
let validationWarningLogged = false;
|
|
27
33
|
let runtimeValidationAvailable = null;
|
|
28
34
|
let decryptionWarningLogged = false;
|
|
@@ -306,7 +312,7 @@ class ActionLogService {
|
|
|
306
312
|
};
|
|
307
313
|
}
|
|
308
314
|
parseListQuery(query) {
|
|
309
|
-
return
|
|
315
|
+
return actionLogListQuerySchema.parse({
|
|
310
316
|
...query
|
|
311
317
|
});
|
|
312
318
|
}
|
|
@@ -326,9 +332,9 @@ class ActionLogService {
|
|
|
326
332
|
return Array.from(new Set(values)).filter((value) => ACTION_LOG_FILTER_TYPES.includes(value));
|
|
327
333
|
}
|
|
328
334
|
resolvePagination(parsed) {
|
|
329
|
-
const pageSize =
|
|
330
|
-
const page =
|
|
331
|
-
const offset =
|
|
335
|
+
const pageSize = parsed.pageSize ?? parsed.limit ?? DEFAULT_LIST_PAGE_SIZE;
|
|
336
|
+
const page = parsed.page ?? 1;
|
|
337
|
+
const offset = parsed.offset ?? (page - 1) * pageSize;
|
|
332
338
|
return { page, pageSize, offset, limit: pageSize };
|
|
333
339
|
}
|
|
334
340
|
async loadEntries(parsed, options) {
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"version": 3,
|
|
3
3
|
"sources": ["../../../../src/modules/audit_logs/services/actionLogService.ts"],
|
|
4
|
-
"sourcesContent": ["import { raw, type FilterQuery } from '@mikro-orm/core'\nimport type { EntityManager } from '@mikro-orm/postgresql'\nimport { sql } from 'kysely'\nimport { ActionLog } from '@open-mercato/core/modules/audit_logs/data/entities'\nimport {\n actionLogCreateSchema,\n actionLogListSchema,\n uuid,\n type ActionLogCreateInput,\n type ActionLogListQuery,\n} from '@open-mercato/core/modules/audit_logs/data/validators'\nimport { isRecord } from '@open-mercato/core/modules/audit_logs/lib/changeRows'\nimport {\n ACTION_LOG_FILTER_TYPES,\n type ActionLogFilterType,\n deriveActionLogProjection,\n} from '@open-mercato/core/modules/audit_logs/lib/projections'\nimport { decryptWithAesGcm } from '@open-mercato/shared/lib/encryption/aes'\nimport {\n TenantDataEncryptionService,\n parseDecryptedFieldValue,\n} from '@open-mercato/shared/lib/encryption/tenantDataEncryptionService'\nimport { toOptionalString } from '@open-mercato/shared/lib/string/coerce'\nimport { createLogger } from '@open-mercato/shared/lib/logger'\nimport {\n ACTION_LOG_ACTOR_SUBJECT_CONTEXT_KEY,\n actionLogBelongsToSubject,\n canonicalizeActorSubject,\n} from '@open-mercato/core/modules/audit_logs/lib/actorSubject'\n\nconst logger = createLogger('audit_logs').child({ component: 'action-log-service' })\n\nlet validationWarningLogged = false\nlet runtimeValidationAvailable: boolean | null = null\nlet decryptionWarningLogged = false\n\nconst isZodRuntimeMissing = (err: unknown) => err instanceof TypeError && typeof err.message === 'string' && err.message.includes('_zod')\n\nconst SORT_FIELDS = {\n createdAt: 'action_logs.created_at',\n} as const\nconst UUID_REGEX = /^[0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[1-5][0-9a-fA-F]{3}-[89abAB][0-9a-fA-F]{3}-[0-9a-fA-F]{12}$/\n// Mirrors what `uuid` (`data/validators.ts`) accepts \u2014 versions 1-8 plus the nil and\n// max UUIDs \u2014 and is used only when the zod runtime is unavailable, so the actor\n// sanitizer can never reject a value `actionLogCreateSchema` would have accepted.\nexport const SCHEMA_UUID_REGEX = /^(?:[0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[1-8][0-9a-fA-F]{3}-[89abAB][0-9a-fA-F]{3}-[0-9a-fA-F]{12}|00000000-0000-0000-0000-000000000000|ffffffff-ffff-ffff-ffff-ffffffffffff)$/\nconst SYSTEM_ACTOR_PREFIX = 'system:'\n// `context.systemActor` names the automated principal behind an entry; `context.source`\n// (read by `deriveActionLogSource`) names the channel it arrived through. They are\n// siblings on purpose and never contradict each other: an entry carrying `systemActor`\n// has a null actor column, which already derives the `system` source.\nconst SYSTEM_ACTOR_CONTEXT_KEY = 'systemActor'\nconst SYSTEM_ACTOR_REFERENCE_MAX_LENGTH = 255\nconst REPLAY_QUERY_BATCH_SIZE = 100\nexport const REPLAY_ENCRYPTED_SCAN_MAX_PAGES = 10\n\ntype ApiKeyReplayEntryKind = 'canonical' | 'legacy' | 'invalid'\n\nfunction toNullableUuid(value: unknown): string | null {\n return typeof value === 'string' && UUID_REGEX.test(value) ? value : null\n}\n\nfunction isSchemaUuid(value: string): boolean {\n if (runtimeValidationAvailable === false) return SCHEMA_UUID_REGEX.test(value)\n\n try {\n return uuid.safeParse(value).success\n } catch (err) {\n if (isZodRuntimeMissing(err)) runtimeValidationAvailable = false\n return SCHEMA_UUID_REGEX.test(value)\n }\n}\n\nfunction readActorCandidate(value: unknown): string | null {\n if (typeof value !== 'string') return null\n const trimmed = value.trim()\n return trimmed.length > 0 ? trimmed : null\n}\n\nfunction normalizeActorUserId(value: unknown): string | null {\n const canonical = canonicalizeActorSubject(value)\n return canonical && isSchemaUuid(canonical.storageId) ? canonical.storageId : null\n}\n\nfunction toSystemActorReference(candidate: string): string | null {\n if (!candidate.startsWith(SYSTEM_ACTOR_PREFIX)) return null\n if (candidate.length === SYSTEM_ACTOR_PREFIX.length) return null\n return candidate.slice(0, SYSTEM_ACTOR_REFERENCE_MAX_LENGTH)\n}\n\ntype ActionLogProjectionBackfillOptions = {\n batchSize?: number\n force?: boolean\n logger?: (message: string) => void\n organizationId?: string | null\n tenantId?: string | null\n}\n\nexport type ActionLogProjectionBackfillResult = {\n errors: number\n scanned: number\n skipped: number\n updated: number\n}\n\ntype BackfillRow = {\n action_label: string | null\n action_type: string | null\n actor_user_id: string | null\n changed_fields: string[] | null\n changes_json: Record<string, unknown> | null\n command_id: string\n context_json: Record<string, unknown> | null\n created_at: Date\n id: string\n organization_id: string | null\n primary_changed_field: string | null\n snapshot_before: unknown | null\n source_key: string | null\n tenant_id: string | null\n}\n\nfunction readString(record: Record<string, unknown>, ...keys: string[]): string | null {\n for (const key of keys) {\n const value = record[key]\n if (typeof value === 'string' && value.length > 0) return value\n }\n\n return null\n}\n\nfunction readRecord(record: Record<string, unknown>, ...keys: string[]): Record<string, unknown> | null {\n for (const key of keys) {\n const value = record[key]\n if (isRecord(value)) return value\n }\n\n return null\n}\n\nfunction readValue(record: Record<string, unknown>, ...keys: string[]): unknown {\n for (const key of keys) {\n if (Object.prototype.hasOwnProperty.call(record, key)) return record[key]\n }\n\n return undefined\n}\n\nfunction readStringArray(record: Record<string, unknown>, ...keys: string[]): string[] | null {\n for (const key of keys) {\n const value = record[key]\n if (Array.isArray(value)) {\n return value.filter((entry): entry is string => typeof entry === 'string')\n }\n }\n\n return null\n}\n\nfunction stringArraysEqual(left: string[] | null, right: string[]): boolean {\n if (!Array.isArray(left)) return false\n if (left.length !== right.length) return false\n\n return left.every((value, index) => value === right[index])\n}\n\nexport class ActionLogService {\n constructor(\n private readonly em: EntityManager,\n private readonly tenantEncryptionService?: TenantDataEncryptionService,\n ) {}\n\n private async decryptEntryPayload<T extends Record<string, unknown>>(entry: T): Promise<T> {\n if (!this.tenantEncryptionService?.isEnabled()) return entry\n\n try {\n const tenantId = readString(entry, 'tenantId', 'tenant_id')\n const organizationId = readString(entry, 'organizationId', 'organization_id')\n const dek = await this.tenantEncryptionService.getDek(tenantId)\n const deepDecrypt = (value: unknown): unknown => {\n if (!dek) return value\n if (typeof value === 'string' && value.split(':').length === 4 && value.endsWith(':v1')) {\n const decrypted = decryptWithAesGcm(value, dek.key)\n if (decrypted === null) return value\n return parseDecryptedFieldValue(decrypted)\n }\n if (Array.isArray(value)) return value.map((item) => deepDecrypt(item))\n if (value && typeof value === 'object') {\n const copy: Record<string, unknown> = {}\n for (const [key, item] of Object.entries(value as Record<string, unknown>)) {\n copy[key] = deepDecrypt(item)\n }\n return copy\n }\n return value\n }\n\n const decrypted = await this.tenantEncryptionService.decryptEntityPayload(\n 'audit_logs:action_log',\n entry,\n tenantId,\n organizationId,\n { em: this.em },\n )\n\n const merged = {\n ...entry,\n ...decrypted,\n } as Record<string, unknown>\n\n // Audit log jsonb columns are encrypted as JSON-stringified payloads. After the\n // service-level `decryptEntityPayload` call (which now returns raw strings \u2014\n // see issue #1810 follow-up), reattach the structured shape via\n // `parseDecryptedFieldValue` before running the recursive `deepDecrypt` walk\n // so nested encrypted strings inside payload objects/arrays are handled.\n const restoreJson = (value: unknown): unknown =>\n typeof value === 'string' ? parseDecryptedFieldValue(value) : value\n\n merged.changesJson = deepDecrypt(restoreJson(merged.changesJson ?? merged.changes_json ?? entry.changesJson ?? entry.changes_json))\n merged.changes_json = merged.changesJson\n merged.snapshotBefore = deepDecrypt(restoreJson(merged.snapshotBefore ?? merged.snapshot_before ?? entry.snapshotBefore ?? entry.snapshot_before))\n merged.snapshot_before = merged.snapshotBefore\n merged.snapshotAfter = deepDecrypt(restoreJson(merged.snapshotAfter ?? merged.snapshot_after ?? entry.snapshotAfter ?? entry.snapshot_after))\n merged.snapshot_after = merged.snapshotAfter\n merged.commandPayload = deepDecrypt(restoreJson(merged.commandPayload ?? merged.command_payload ?? entry.commandPayload ?? entry.command_payload))\n merged.command_payload = merged.commandPayload\n merged.contextJson = deepDecrypt(restoreJson(merged.contextJson ?? merged.context_json ?? entry.contextJson ?? entry.context_json))\n merged.context_json = merged.contextJson\n\n return merged as T\n } catch (err) {\n if (!decryptionWarningLogged) {\n decryptionWarningLogged = true\n logger.warn('Failed to decrypt action log entry', { err })\n }\n return entry\n }\n }\n\n private async decryptEntries(entries: ActionLog | ActionLog[] | null | undefined): Promise<void> {\n if (!entries) return\n\n const list = Array.isArray(entries) ? entries : [entries]\n for (const entry of list) {\n Object.assign(entry as unknown as Record<string, unknown>, await this.decryptEntryPayload(entry as unknown as Record<string, unknown>))\n }\n }\n\n private async decryptDetachedEntry(entry: ActionLog): Promise<ActionLog> {\n const detached = { ...(entry as unknown as Record<string, unknown>) }\n return await this.decryptEntryPayload(detached) as unknown as ActionLog\n }\n\n async log(input: ActionLogCreateInput, transactionalEm?: EntityManager): Promise<ActionLog | null> {\n const data = this.parseCreateInput(input)\n const operationEm = transactionalEm ?? this.em.fork()\n const log = this.createLogEntity(operationEm, data)\n await operationEm.persist(log).flush()\n if (transactionalEm) return this.decryptDetachedEntry(log)\n await this.decryptEntries(log)\n return log\n }\n\n private parseCreateInput(input: ActionLogCreateInput): ActionLogCreateInput {\n const sanitized = this.sanitizeActor(input)\n let data: ActionLogCreateInput\n const schema = actionLogCreateSchema as typeof actionLogCreateSchema & { _zod?: unknown }\n const canValidate = Boolean(schema && typeof schema.parse === 'function')\n const shouldValidate = canValidate && runtimeValidationAvailable !== false\n\n if (shouldValidate) {\n try {\n data = schema.parse(sanitized)\n runtimeValidationAvailable = true\n } catch (err) {\n if (!isZodRuntimeMissing(err) && !validationWarningLogged) {\n validationWarningLogged = true\n logger.warn('Falling back to permissive action log payload parser', { err })\n }\n if (isZodRuntimeMissing(err)) runtimeValidationAvailable = false\n data = this.normalizeInput(sanitized)\n }\n } else {\n data = this.normalizeInput(sanitized)\n }\n\n return data\n }\n\n private sanitizeActor(input: ActionLogCreateInput): ActionLogCreateInput {\n if (!input) return input\n const candidate = readActorCandidate(input.actorUserId)\n const actor = canonicalizeActorSubject(candidate)\n const actorUserId = actor?.storageId ?? null\n const systemActorReference = candidate && !actorUserId ? toSystemActorReference(candidate) : null\n\n if (actor) {\n const context = isRecord(input.context) ? { ...input.context } : {}\n context[ACTION_LOG_ACTOR_SUBJECT_CONTEXT_KEY] = actor.subject\n return { ...input, actorUserId, context }\n }\n\n if (!systemActorReference) return { ...input, actorUserId: null }\n\n const context = isRecord(input.context) ? { ...input.context } : {}\n if (context[SYSTEM_ACTOR_CONTEXT_KEY] === undefined) {\n context[SYSTEM_ACTOR_CONTEXT_KEY] = systemActorReference\n }\n\n return { ...input, actorUserId, context }\n }\n\n private createLogEntity(fork: EntityManager, data: ActionLogCreateInput): ActionLog {\n const projection = deriveActionLogProjection({\n actorUserId: data.actorUserId ?? null,\n actionLabel: data.actionLabel ?? null,\n changes: isRecord(data.changes) ? data.changes : null,\n commandId: data.commandId,\n context: isRecord(data.context) ? data.context : null,\n snapshotBefore: data.snapshotBefore,\n })\n\n return fork.create(ActionLog, {\n tenantId: data.tenantId ?? null,\n organizationId: data.organizationId ?? null,\n actorUserId: data.actorUserId ?? null,\n onBehalfOfUserId: data.onBehalfOfUserId ?? null,\n commandId: data.commandId,\n actionLabel: data.actionLabel ?? null,\n actionType: projection.actionType,\n resourceKind: data.resourceKind ?? null,\n resourceId: data.resourceId ?? null,\n parentResourceKind: data.parentResourceKind ?? null,\n parentResourceId: data.parentResourceId ?? null,\n relatedResourceKind: toOptionalString(data.relatedResourceKind) ?? null,\n relatedResourceId: toOptionalString(data.relatedResourceId) ?? null,\n executionState: data.executionState ?? 'done',\n undoToken: data.undoToken ?? null,\n commandPayload: data.commandPayload ?? null,\n snapshotBefore: data.snapshotBefore ?? null,\n snapshotAfter: data.snapshotAfter ?? null,\n changesJson: isRecord(data.changes) ? data.changes : null,\n changedFields: projection.changedFields,\n primaryChangedField: projection.primaryChangedField,\n contextJson: isRecord(data.context) ? data.context : null,\n sourceKey: projection.sourceKey,\n createdAt: new Date(),\n updatedAt: new Date(),\n })\n }\n\n private normalizeInput(input: Partial<ActionLogCreateInput> | null | undefined): ActionLogCreateInput {\n if (!input) {\n return {\n tenantId: null,\n organizationId: null,\n actorUserId: null,\n onBehalfOfUserId: null,\n commandId: 'unknown',\n actionLabel: undefined,\n resourceKind: undefined,\n resourceId: undefined,\n relatedResourceKind: null,\n relatedResourceId: null,\n executionState: 'done',\n undoToken: undefined,\n commandPayload: undefined,\n snapshotBefore: undefined,\n snapshotAfter: undefined,\n changes: undefined,\n context: undefined,\n }\n }\n\n const normalizeRecordLike = (value: unknown): ActionLogCreateInput['changes'] => {\n if (value === null) return null\n if (Array.isArray(value)) return value\n if (typeof value === 'object') return value as Record<string, unknown>\n return undefined\n }\n\n const normalizeContext = (value: unknown) => (\n typeof value === 'object' && value !== null && !Array.isArray(value)\n ? value as Record<string, unknown>\n : undefined\n )\n\n return {\n tenantId: toNullableUuid(input.tenantId),\n organizationId: toNullableUuid(input.organizationId),\n actorUserId: normalizeActorUserId(input.actorUserId),\n onBehalfOfUserId: toNullableUuid(input.onBehalfOfUserId),\n commandId: typeof input.commandId === 'string' && input.commandId.length > 0 ? input.commandId : 'unknown',\n actionLabel: toOptionalString(input.actionLabel) ?? undefined,\n resourceKind: toOptionalString(input.resourceKind) ?? undefined,\n resourceId: toOptionalString(input.resourceId) ?? undefined,\n parentResourceKind: toOptionalString(input.parentResourceKind) ?? null,\n parentResourceId: toOptionalString(input.parentResourceId) ?? null,\n relatedResourceKind: toOptionalString(input.relatedResourceKind) ?? null,\n relatedResourceId: toOptionalString(input.relatedResourceId) ?? null,\n executionState: input.executionState === 'undone' || input.executionState === 'failed' ? input.executionState : 'done',\n undoToken: toOptionalString(input.undoToken) ?? undefined,\n commandPayload: input.commandPayload,\n snapshotBefore: input.snapshotBefore,\n snapshotAfter: input.snapshotAfter,\n changes: normalizeRecordLike(input.changes),\n context: normalizeContext(input.context),\n }\n }\n\n private parseListQuery(query: Partial<ActionLogListQuery>) {\n return actionLogListSchema.parse({\n ...query,\n })\n }\n\n private resolveActorUserIds(parsed: ActionLogListQuery): string[] {\n const values = [...(parsed.actorUserIds ?? [])]\n if (parsed.actorUserId) values.push(parsed.actorUserId)\n\n return Array.from(new Set(values.map((value) => value.trim()).filter(Boolean)))\n }\n\n private resolveFieldNames(parsed: ActionLogListQuery): string[] {\n const values = [...(parsed.fieldNames ?? [])]\n if (parsed.fieldName) values.push(parsed.fieldName)\n\n return Array.from(new Set(values.map((value) => value.trim()).filter(Boolean)))\n }\n\n private resolveActionTypes(parsed: ActionLogListQuery): ActionLogFilterType[] {\n const values = [...(parsed.actionTypes ?? [])]\n if (parsed.actionType) values.push(parsed.actionType)\n\n return Array.from(new Set(values))\n .filter((value): value is ActionLogFilterType => ACTION_LOG_FILTER_TYPES.includes(value as ActionLogFilterType))\n }\n\n private resolvePagination(parsed: ActionLogListQuery): { page: number; pageSize: number; offset: number; limit: number } {\n const pageSize =\n typeof parsed.pageSize === 'number' && parsed.pageSize > 0\n ? parsed.pageSize\n : typeof parsed.limit === 'number' && parsed.limit > 0\n ? parsed.limit\n : 50\n const page = typeof parsed.page === 'number' && parsed.page > 0 ? parsed.page : 1\n const offset =\n typeof parsed.offset === 'number' && parsed.offset >= 0\n ? parsed.offset\n : (page - 1) * pageSize\n return { page, pageSize, offset, limit: pageSize }\n }\n\n private async loadEntries(parsed: ActionLogListQuery, options?: { paginate?: boolean }) {\n let query = (this.buildListQuery(parsed) as any).select('action_logs.id as id')\n\n if (options?.paginate !== false) {\n const { limit, offset } = this.resolvePagination(parsed)\n query = query.limit(limit).offset(offset)\n }\n\n const rows = await query.execute()\n const ids = rows.map((row: any) => row.id).filter(Boolean)\n if (ids.length === 0) return []\n\n const results = await this.em.find(ActionLog, {\n id: { $in: ids } as any,\n deletedAt: null,\n })\n await this.decryptEntries(results)\n\n const byId = new Map(results.map((entry: any) => [entry.id, entry]))\n const entries = ids\n .map((id: any) => byId.get(id))\n .filter((entry: any): entry is ActionLog => Boolean(entry))\n const apiKeyActor = parsed.actorSubject ? canonicalizeActorSubject(parsed.actorSubject) : null\n if (apiKeyActor?.kind !== 'api_key') return entries\n return entries.filter((entry: ActionLog) => (\n this.classifyApiKeyReplayEntry(entry, apiKeyActor.subject) !== 'invalid'\n ))\n }\n\n private buildListQuery(parsed: ActionLogListQuery): any {\n let query = (this.em.getKysely<any>() as any)\n .selectFrom('action_logs')\n .selectAll()\n .where('action_logs.deleted_at', 'is', null) as any\n\n if (parsed.tenantId) query = query.where('action_logs.tenant_id', '=', parsed.tenantId)\n if (parsed.organizationId) query = query.where('action_logs.organization_id', '=', parsed.organizationId)\n\n if (parsed.actorSubject) {\n const actor = canonicalizeActorSubject(parsed.actorSubject)\n if (!actor) return query.where(sql<boolean>`false`)\n query = query.where('action_logs.actor_user_id', '=', actor.storageId)\n if (actor.kind === 'api_key') {\n query = query.where(sql<boolean>`(\n action_logs.context_json is null\n or jsonb_typeof(action_logs.context_json) = 'string'\n or (\n jsonb_typeof(action_logs.context_json) = 'object'\n and (\n not jsonb_exists(action_logs.context_json, ${ACTION_LOG_ACTOR_SUBJECT_CONTEXT_KEY})\n or action_logs.context_json ->> ${ACTION_LOG_ACTOR_SUBJECT_CONTEXT_KEY} = ${actor.subject}\n )\n )\n )`)\n }\n }\n\n const actorUserIds = this.resolveActorUserIds(parsed)\n if (actorUserIds.length === 1) query = query.where('action_logs.actor_user_id', '=', actorUserIds[0])\n if (actorUserIds.length > 1) query = query.where('action_logs.actor_user_id', 'in', actorUserIds)\n\n if (parsed.includeRelated && parsed.resourceKind && parsed.resourceId) {\n query = query.where((eb: any) =>\n eb.or([\n eb.and([\n eb('action_logs.resource_kind', '=', parsed.resourceKind),\n eb('action_logs.resource_id', '=', parsed.resourceId),\n ]),\n eb.and([\n eb('action_logs.parent_resource_kind', '=', parsed.resourceKind),\n eb('action_logs.parent_resource_id', '=', parsed.resourceId),\n ]),\n eb.and([\n eb('action_logs.related_resource_kind', '=', parsed.resourceKind),\n eb('action_logs.related_resource_id', '=', parsed.resourceId),\n ]),\n ])\n )\n } else {\n if (parsed.resourceKind) query = query.where('action_logs.resource_kind', '=', parsed.resourceKind)\n if (parsed.resourceId) query = query.where('action_logs.resource_id', '=', parsed.resourceId)\n }\n\n if (parsed.undoableOnly) query = query.where('action_logs.undo_token', 'is not', null)\n if (parsed.before) query = query.where('action_logs.created_at', '<', parsed.before)\n if (parsed.after) query = query.where('action_logs.created_at', '>', parsed.after)\n\n const fieldNames = this.resolveFieldNames(parsed)\n if (fieldNames.length === 1) query = query.where('action_logs.primary_changed_field', '=', fieldNames[0])\n if (fieldNames.length > 1) query = query.where('action_logs.primary_changed_field', 'in', fieldNames)\n\n const actionTypes = this.resolveActionTypes(parsed)\n if (actionTypes.length === 1) query = query.where('action_logs.action_type', '=', actionTypes[0])\n if (actionTypes.length > 1) query = query.where('action_logs.action_type', 'in', actionTypes)\n\n if (parsed.sortField === 'user') {\n query = query.leftJoin('users as audit_actor', 'audit_actor.id', 'action_logs.actor_user_id')\n }\n\n const sortDir = parsed.sortDir === 'asc' ? 'asc' : 'desc'\n switch (parsed.sortField) {\n case 'user':\n query = query.orderBy(sql`coalesce(nullif(audit_actor.name, ''), audit_actor.email, '')`, sortDir)\n break\n case 'action':\n query = query.orderBy(sql`coalesce(action_logs.action_type, '')`, sortDir)\n break\n case 'field':\n query = query.orderBy(sql`coalesce(action_logs.primary_changed_field, '')`, sortDir)\n break\n case 'source':\n query = query.orderBy(sql`coalesce(action_logs.source_key, '')`, sortDir)\n break\n case 'createdAt':\n default:\n query = query.orderBy(SORT_FIELDS.createdAt, sortDir)\n query = query.orderBy('action_logs.id', sortDir)\n return query\n }\n\n query = query.orderBy('action_logs.created_at', 'desc')\n query = query.orderBy('action_logs.id', 'desc')\n return query\n }\n\n async count(query: Partial<ActionLogListQuery>) {\n const parsed = this.parseListQuery(query)\n const row = await (this.buildListQuery(parsed) as any)\n .clearSelect()\n .clearOrderBy()\n .select(sql<string>`count(*)`.as('count'))\n .executeTakeFirst()\n\n if (!row) return 0\n const rawCount = row.count ?? 0\n return typeof rawCount === 'number' ? rawCount : Number.parseInt(rawCount, 10) || 0\n }\n\n async list(query: Partial<ActionLogListQuery>) {\n const parsed = this.parseListQuery(query)\n const { page, pageSize } = this.resolvePagination(parsed)\n const [items, total] = await Promise.all([\n this.loadEntries(parsed),\n this.count(parsed),\n ])\n const totalPages = Math.max(1, Math.ceil((total || 0) / (pageSize || 1)))\n return { items, total, page, pageSize, totalPages }\n }\n\n private classifyApiKeyReplayEntry(entry: ActionLog, actorSubject: string): ApiKeyReplayEntryKind {\n if (entry.contextJson === null || entry.contextJson === undefined) return 'legacy'\n if (!isRecord(entry.contextJson)) return 'invalid'\n if (!Object.prototype.hasOwnProperty.call(entry.contextJson, ACTION_LOG_ACTOR_SUBJECT_CONTEXT_KEY)) {\n return 'legacy'\n }\n return actionLogBelongsToSubject(entry, actorSubject) ? 'canonical' : 'invalid'\n }\n\n private async findLatestReplayEntry(\n actorUserId: string,\n filter: Record<string, unknown>,\n orderField: 'createdAt' | 'updatedAt',\n ): Promise<ActionLog | null> {\n const actor = canonicalizeActorSubject(actorUserId)\n if (!actor) return null\n const where: Record<string, unknown> = {\n actorUserId: actor.storageId,\n ...filter,\n }\n const orderBy = orderField === 'createdAt'\n ? { createdAt: 'desc' as const, id: 'desc' as const }\n : { updatedAt: 'desc' as const, id: 'desc' as const }\n\n if (actor.kind === 'user') {\n const entry = await this.em.findOne(ActionLog, where as FilterQuery<ActionLog>, { orderBy })\n await this.decryptEntries(entry)\n return entry\n }\n\n if (this.tenantEncryptionService?.isEnabled()) {\n let legacyEntry: ActionLog | null = null\n let cursor: { id: string; orderedAt: Date } | null = null\n for (let pageIndex = 0; ; pageIndex += 1) {\n if (pageIndex >= REPLAY_ENCRYPTED_SCAN_MAX_PAGES) {\n logger.warn('Stopped encrypted API-key replay scan at the page limit', {\n maxRows: REPLAY_ENCRYPTED_SCAN_MAX_PAGES * REPLAY_QUERY_BATCH_SIZE,\n hasLegacyFallback: legacyEntry !== null,\n })\n return legacyEntry\n }\n const pageWhere: Record<string, unknown> = cursor\n ? {\n $and: [\n where,\n {\n $or: [\n { [orderField]: { $lt: cursor.orderedAt } },\n { [orderField]: cursor.orderedAt, id: { $lt: cursor.id } },\n ],\n },\n ],\n }\n : where\n const entries: ActionLog[] = await this.em.find(ActionLog, pageWhere as FilterQuery<ActionLog>, {\n limit: REPLAY_QUERY_BATCH_SIZE,\n orderBy,\n })\n await this.decryptEntries(entries)\n for (const entry of entries) {\n const entryKind = this.classifyApiKeyReplayEntry(entry, actor.subject)\n if (entryKind === 'canonical') return entry\n if (entryKind === 'legacy' && !legacyEntry) legacyEntry = entry\n }\n if (entries.length < REPLAY_QUERY_BATCH_SIZE) return legacyEntry\n const lastEntry: ActionLog | undefined = entries.at(-1)\n if (!lastEntry) return legacyEntry\n cursor = {\n id: String(lastEntry.id),\n orderedAt: orderField === 'createdAt' ? lastEntry.createdAt : lastEntry.updatedAt,\n }\n }\n }\n\n const canonicalEntry = await this.em.findOne(ActionLog, {\n ...where,\n [raw(`\"context_json\" ->> '${ACTION_LOG_ACTOR_SUBJECT_CONTEXT_KEY}'`)]: actor.subject,\n } as FilterQuery<ActionLog>, { orderBy })\n if (canonicalEntry) {\n await this.decryptEntries(canonicalEntry)\n return this.classifyApiKeyReplayEntry(canonicalEntry, actor.subject) === 'canonical'\n ? canonicalEntry\n : null\n }\n\n const legacyEntry = await this.em.findOne(ActionLog, {\n ...where,\n [raw(`(\"context_json\" is null or (jsonb_typeof(\"context_json\") = 'object' and not jsonb_exists(\"context_json\", '${ACTION_LOG_ACTOR_SUBJECT_CONTEXT_KEY}')))`)]: true,\n } as FilterQuery<ActionLog>, { orderBy })\n await this.decryptEntries(legacyEntry)\n return legacyEntry && this.classifyApiKeyReplayEntry(legacyEntry, actor.subject) === 'legacy'\n ? legacyEntry\n : null\n }\n\n async latestUndoableForActor(actorUserId: string, scope: { tenantId?: string | null; organizationId?: string | null }) {\n const where: Record<string, unknown> = {\n undoToken: { $ne: null },\n executionState: 'done',\n deletedAt: null,\n }\n if (scope.tenantId) where.tenantId = scope.tenantId\n if (scope.organizationId) where.organizationId = scope.organizationId\n\n return this.findLatestReplayEntry(actorUserId, where, 'createdAt')\n }\n\n async claimForUndo(id: string, transactionalEm?: EntityManager): Promise<boolean> {\n const operationEm = transactionalEm ?? this.em\n const affected = await operationEm.nativeUpdate(\n ActionLog,\n { id, executionState: 'done', deletedAt: null },\n { executionState: 'undoing' },\n )\n return affected === 1\n }\n\n async releaseUndoClaim(id: string, transactionalEm?: EntityManager): Promise<boolean> {\n const operationEm = transactionalEm ?? this.em\n const affected = await operationEm.nativeUpdate(\n ActionLog,\n { id, executionState: 'undoing', deletedAt: null },\n { executionState: 'done' },\n )\n return affected === 1\n }\n\n async markUndone(id: string, traceInput?: ActionLogCreateInput, transactionalEm?: EntityManager) {\n const operationEm = transactionalEm ?? this.em.fork()\n const log = await operationEm.findOne(ActionLog, { id, deletedAt: null })\n if (!log) return null\n\n log.executionState = 'undone'\n log.undoToken = null\n\n const traceLog = traceInput ? this.createLogEntity(operationEm, this.parseCreateInput(traceInput)) : null\n if (traceLog) {\n operationEm.persist(traceLog)\n }\n\n await operationEm.flush()\n if (transactionalEm) return this.decryptDetachedEntry(log)\n await this.decryptEntries(log)\n if (traceLog) await this.decryptEntries(traceLog)\n\n return log\n }\n\n async findByUndoToken(undoToken: string) {\n const entry = await this.em.findOne(ActionLog, { undoToken, deletedAt: null })\n await this.decryptEntries(entry)\n return entry\n }\n\n async findById(id: string) {\n const entry = await this.em.findOne(ActionLog, { id, deletedAt: null })\n await this.decryptEntries(entry)\n return entry\n }\n\n async latestUndoableForResource(params: {\n actorUserId: string\n tenantId?: string | null\n organizationId?: string | null\n resourceKind?: string | null\n resourceId?: string | null\n }) {\n const where: Record<string, unknown> = {\n undoToken: { $ne: null },\n executionState: 'done',\n deletedAt: null,\n }\n if (params.tenantId) where.tenantId = params.tenantId\n if (params.organizationId) where.organizationId = params.organizationId\n if (params.resourceKind) where.resourceKind = params.resourceKind\n if (params.resourceId) where.resourceId = params.resourceId\n\n return this.findLatestReplayEntry(params.actorUserId, where, 'createdAt')\n }\n\n async latestUndoneForActor(actorUserId: string, scope: { tenantId?: string | null; organizationId?: string | null }) {\n const where: Record<string, unknown> = {\n executionState: 'undone',\n deletedAt: null,\n }\n if (scope.tenantId) where.tenantId = scope.tenantId\n if (scope.organizationId) where.organizationId = scope.organizationId\n\n return this.findLatestReplayEntry(actorUserId, where, 'updatedAt')\n }\n\n async markRedone(id: string) {\n const operationEm = this.em.fork()\n const log = await operationEm.findOne(ActionLog, { id, deletedAt: null })\n if (!log) return null\n\n log.executionState = 'redone'\n log.undoToken = null\n await operationEm.flush()\n return log\n }\n\n /**\n * Compare-and-set the source of a redo from `undone` to its final `redone`\n * state. When supplied, `transactionalEm` is the same EntityManager used by\n * the replayed command and its new action log, so a later failure restores\n * the source row to `undone` automatically.\n */\n async claimForRedo(id: string, transactionalEm?: EntityManager): Promise<boolean> {\n const operationEm = transactionalEm ?? this.em\n const affected = await operationEm.nativeUpdate(\n ActionLog,\n { id, executionState: 'undone', deletedAt: null },\n { executionState: 'redone', undoToken: null },\n )\n return affected === 1\n }\n\n async backfillProjections(options: ActionLogProjectionBackfillOptions = {}): Promise<ActionLogProjectionBackfillResult> {\n const batchSize = Math.min(Math.max(Math.trunc(options.batchSize ?? 250), 1), 1000)\n const logger = options.logger ?? (() => {})\n const result: ActionLogProjectionBackfillResult = {\n errors: 0,\n scanned: 0,\n skipped: 0,\n updated: 0,\n }\n\n let cursorCreatedAt: Date | null = null\n let cursorId: string | null = null\n\n while (true) {\n const rowsQuery = (this.em.getKysely<any>() as any)\n .selectFrom('action_logs')\n .select([\n 'action_logs.id',\n 'action_logs.tenant_id',\n 'action_logs.organization_id',\n 'action_logs.actor_user_id',\n 'action_logs.command_id',\n 'action_logs.action_label',\n 'action_logs.snapshot_before',\n 'action_logs.changes_json',\n 'action_logs.context_json',\n 'action_logs.action_type',\n 'action_logs.source_key',\n 'action_logs.changed_fields',\n 'action_logs.primary_changed_field',\n 'action_logs.created_at',\n ])\n .where('action_logs.deleted_at', 'is', null) as any\n\n if (options.tenantId) rowsQuery.where('action_logs.tenant_id', '=', options.tenantId)\n if (options.organizationId) rowsQuery.where('action_logs.organization_id', '=', options.organizationId)\n\n if (!options.force) {\n rowsQuery.where((eb: any) =>\n eb.or([\n eb('action_logs.action_type', 'is', null),\n eb('action_logs.source_key', 'is', null),\n eb('action_logs.changed_fields', 'is', null),\n ])\n )\n }\n\n if (cursorCreatedAt && cursorId) {\n rowsQuery.where((eb: any) =>\n eb.or([\n eb('action_logs.created_at', '>', cursorCreatedAt),\n eb.and([\n eb('action_logs.created_at', '=', cursorCreatedAt),\n eb('action_logs.id', '>', cursorId),\n ]),\n ])\n )\n }\n\n const rows = await rowsQuery\n .orderBy('created_at', 'asc')\n .orderBy('id', 'asc')\n .limit(batchSize)\n\n if (rows.length === 0) break\n\n for (const row of rows) {\n result.scanned += 1\n\n try {\n const decrypted = await this.decryptEntryPayload(row as unknown as Record<string, unknown>)\n const projection = deriveActionLogProjection({\n actorUserId: readString(decrypted, 'actorUserId', 'actor_user_id'),\n actionLabel: readString(decrypted, 'actionLabel', 'action_label'),\n changes: readRecord(decrypted, 'changesJson', 'changes_json'),\n commandId: readString(decrypted, 'commandId', 'command_id') ?? 'unknown',\n context: readRecord(decrypted, 'contextJson', 'context_json'),\n snapshotBefore: readValue(decrypted, 'snapshotBefore', 'snapshot_before'),\n })\n\n const needsUpdate = options.force === true\n || row.action_type !== projection.actionType\n || row.source_key !== projection.sourceKey\n || row.primary_changed_field !== projection.primaryChangedField\n || !stringArraysEqual(row.changed_fields, projection.changedFields)\n\n if (!needsUpdate) {\n result.skipped += 1\n continue\n }\n\n await (this.em.getKysely<any>() as any)\n .updateTable('action_logs')\n .set({\n action_type: projection.actionType,\n changed_fields: projection.changedFields,\n primary_changed_field: projection.primaryChangedField,\n source_key: projection.sourceKey,\n })\n .where('id', '=', row.id)\n .execute()\n\n result.updated += 1\n } catch (err) {\n result.errors += 1\n logger(`[backfill] Failed for action log ${row.id}: ${err instanceof Error ? err.message : String(err)}`)\n }\n }\n\n const lastRow = rows[rows.length - 1]\n cursorCreatedAt = lastRow.created_at\n cursorId = lastRow.id\n\n logger(\n `[backfill] Processed ${result.scanned} action logs (updated: ${result.updated}, skipped: ${result.skipped}, errors: ${result.errors})`,\n )\n\n if (rows.length < batchSize) break\n }\n\n return result\n }\n}\n"],
|
|
5
|
-
"mappings": "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",
|
|
4
|
+
"sourcesContent": ["import { raw, type FilterQuery } from '@mikro-orm/core'\nimport type { EntityManager } from '@mikro-orm/postgresql'\nimport { sql } from 'kysely'\nimport { z } from 'zod'\nimport { ActionLog } from '@open-mercato/core/modules/audit_logs/data/entities'\nimport {\n actionLogCreateSchema,\n actionLogListSchema,\n uuid,\n type ActionLogCreateInput,\n type ActionLogListQuery,\n} from '@open-mercato/core/modules/audit_logs/data/validators'\nimport { isRecord } from '@open-mercato/core/modules/audit_logs/lib/changeRows'\nimport {\n ACTION_LOG_FILTER_TYPES,\n type ActionLogFilterType,\n deriveActionLogProjection,\n} from '@open-mercato/core/modules/audit_logs/lib/projections'\nimport { decryptWithAesGcm } from '@open-mercato/shared/lib/encryption/aes'\nimport {\n TenantDataEncryptionService,\n parseDecryptedFieldValue,\n} from '@open-mercato/shared/lib/encryption/tenantDataEncryptionService'\nimport { toOptionalString } from '@open-mercato/shared/lib/string/coerce'\nimport { createLogger } from '@open-mercato/shared/lib/logger'\nimport {\n ACTION_LOG_ACTOR_SUBJECT_CONTEXT_KEY,\n actionLogBelongsToSubject,\n canonicalizeActorSubject,\n} from '@open-mercato/core/modules/audit_logs/lib/actorSubject'\n\nconst logger = createLogger('audit_logs').child({ component: 'action-log-service' })\n\nconst DEFAULT_LIST_PAGE_SIZE = 50\n\nconst actionLogListQuerySchema = actionLogListSchema.extend({\n page: z.number().int().positive().optional(),\n pageSize: z.number().int().positive().max(200).optional(),\n})\n\ntype ParsedActionLogListQuery = z.infer<typeof actionLogListQuerySchema>\n\nlet validationWarningLogged = false\nlet runtimeValidationAvailable: boolean | null = null\nlet decryptionWarningLogged = false\n\nconst isZodRuntimeMissing = (err: unknown) => err instanceof TypeError && typeof err.message === 'string' && err.message.includes('_zod')\n\nconst SORT_FIELDS = {\n createdAt: 'action_logs.created_at',\n} as const\nconst UUID_REGEX = /^[0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[1-5][0-9a-fA-F]{3}-[89abAB][0-9a-fA-F]{3}-[0-9a-fA-F]{12}$/\n// Mirrors what `uuid` (`data/validators.ts`) accepts \u2014 versions 1-8 plus the nil and\n// max UUIDs \u2014 and is used only when the zod runtime is unavailable, so the actor\n// sanitizer can never reject a value `actionLogCreateSchema` would have accepted.\nexport const SCHEMA_UUID_REGEX = /^(?:[0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[1-8][0-9a-fA-F]{3}-[89abAB][0-9a-fA-F]{3}-[0-9a-fA-F]{12}|00000000-0000-0000-0000-000000000000|ffffffff-ffff-ffff-ffff-ffffffffffff)$/\nconst SYSTEM_ACTOR_PREFIX = 'system:'\n// `context.systemActor` names the automated principal behind an entry; `context.source`\n// (read by `deriveActionLogSource`) names the channel it arrived through. They are\n// siblings on purpose and never contradict each other: an entry carrying `systemActor`\n// has a null actor column, which already derives the `system` source.\nconst SYSTEM_ACTOR_CONTEXT_KEY = 'systemActor'\nconst SYSTEM_ACTOR_REFERENCE_MAX_LENGTH = 255\nconst REPLAY_QUERY_BATCH_SIZE = 100\nexport const REPLAY_ENCRYPTED_SCAN_MAX_PAGES = 10\n\ntype ApiKeyReplayEntryKind = 'canonical' | 'legacy' | 'invalid'\n\nfunction toNullableUuid(value: unknown): string | null {\n return typeof value === 'string' && UUID_REGEX.test(value) ? value : null\n}\n\nfunction isSchemaUuid(value: string): boolean {\n if (runtimeValidationAvailable === false) return SCHEMA_UUID_REGEX.test(value)\n\n try {\n return uuid.safeParse(value).success\n } catch (err) {\n if (isZodRuntimeMissing(err)) runtimeValidationAvailable = false\n return SCHEMA_UUID_REGEX.test(value)\n }\n}\n\nfunction readActorCandidate(value: unknown): string | null {\n if (typeof value !== 'string') return null\n const trimmed = value.trim()\n return trimmed.length > 0 ? trimmed : null\n}\n\nfunction normalizeActorUserId(value: unknown): string | null {\n const canonical = canonicalizeActorSubject(value)\n return canonical && isSchemaUuid(canonical.storageId) ? canonical.storageId : null\n}\n\nfunction toSystemActorReference(candidate: string): string | null {\n if (!candidate.startsWith(SYSTEM_ACTOR_PREFIX)) return null\n if (candidate.length === SYSTEM_ACTOR_PREFIX.length) return null\n return candidate.slice(0, SYSTEM_ACTOR_REFERENCE_MAX_LENGTH)\n}\n\ntype ActionLogProjectionBackfillOptions = {\n batchSize?: number\n force?: boolean\n logger?: (message: string) => void\n organizationId?: string | null\n tenantId?: string | null\n}\n\nexport type ActionLogProjectionBackfillResult = {\n errors: number\n scanned: number\n skipped: number\n updated: number\n}\n\ntype BackfillRow = {\n action_label: string | null\n action_type: string | null\n actor_user_id: string | null\n changed_fields: string[] | null\n changes_json: Record<string, unknown> | null\n command_id: string\n context_json: Record<string, unknown> | null\n created_at: Date\n id: string\n organization_id: string | null\n primary_changed_field: string | null\n snapshot_before: unknown | null\n source_key: string | null\n tenant_id: string | null\n}\n\nfunction readString(record: Record<string, unknown>, ...keys: string[]): string | null {\n for (const key of keys) {\n const value = record[key]\n if (typeof value === 'string' && value.length > 0) return value\n }\n\n return null\n}\n\nfunction readRecord(record: Record<string, unknown>, ...keys: string[]): Record<string, unknown> | null {\n for (const key of keys) {\n const value = record[key]\n if (isRecord(value)) return value\n }\n\n return null\n}\n\nfunction readValue(record: Record<string, unknown>, ...keys: string[]): unknown {\n for (const key of keys) {\n if (Object.prototype.hasOwnProperty.call(record, key)) return record[key]\n }\n\n return undefined\n}\n\nfunction readStringArray(record: Record<string, unknown>, ...keys: string[]): string[] | null {\n for (const key of keys) {\n const value = record[key]\n if (Array.isArray(value)) {\n return value.filter((entry): entry is string => typeof entry === 'string')\n }\n }\n\n return null\n}\n\nfunction stringArraysEqual(left: string[] | null, right: string[]): boolean {\n if (!Array.isArray(left)) return false\n if (left.length !== right.length) return false\n\n return left.every((value, index) => value === right[index])\n}\n\nexport class ActionLogService {\n constructor(\n private readonly em: EntityManager,\n private readonly tenantEncryptionService?: TenantDataEncryptionService,\n ) {}\n\n private async decryptEntryPayload<T extends Record<string, unknown>>(entry: T): Promise<T> {\n if (!this.tenantEncryptionService?.isEnabled()) return entry\n\n try {\n const tenantId = readString(entry, 'tenantId', 'tenant_id')\n const organizationId = readString(entry, 'organizationId', 'organization_id')\n const dek = await this.tenantEncryptionService.getDek(tenantId)\n const deepDecrypt = (value: unknown): unknown => {\n if (!dek) return value\n if (typeof value === 'string' && value.split(':').length === 4 && value.endsWith(':v1')) {\n const decrypted = decryptWithAesGcm(value, dek.key)\n if (decrypted === null) return value\n return parseDecryptedFieldValue(decrypted)\n }\n if (Array.isArray(value)) return value.map((item) => deepDecrypt(item))\n if (value && typeof value === 'object') {\n const copy: Record<string, unknown> = {}\n for (const [key, item] of Object.entries(value as Record<string, unknown>)) {\n copy[key] = deepDecrypt(item)\n }\n return copy\n }\n return value\n }\n\n const decrypted = await this.tenantEncryptionService.decryptEntityPayload(\n 'audit_logs:action_log',\n entry,\n tenantId,\n organizationId,\n { em: this.em },\n )\n\n const merged = {\n ...entry,\n ...decrypted,\n } as Record<string, unknown>\n\n // Audit log jsonb columns are encrypted as JSON-stringified payloads. After the\n // service-level `decryptEntityPayload` call (which now returns raw strings \u2014\n // see issue #1810 follow-up), reattach the structured shape via\n // `parseDecryptedFieldValue` before running the recursive `deepDecrypt` walk\n // so nested encrypted strings inside payload objects/arrays are handled.\n const restoreJson = (value: unknown): unknown =>\n typeof value === 'string' ? parseDecryptedFieldValue(value) : value\n\n merged.changesJson = deepDecrypt(restoreJson(merged.changesJson ?? merged.changes_json ?? entry.changesJson ?? entry.changes_json))\n merged.changes_json = merged.changesJson\n merged.snapshotBefore = deepDecrypt(restoreJson(merged.snapshotBefore ?? merged.snapshot_before ?? entry.snapshotBefore ?? entry.snapshot_before))\n merged.snapshot_before = merged.snapshotBefore\n merged.snapshotAfter = deepDecrypt(restoreJson(merged.snapshotAfter ?? merged.snapshot_after ?? entry.snapshotAfter ?? entry.snapshot_after))\n merged.snapshot_after = merged.snapshotAfter\n merged.commandPayload = deepDecrypt(restoreJson(merged.commandPayload ?? merged.command_payload ?? entry.commandPayload ?? entry.command_payload))\n merged.command_payload = merged.commandPayload\n merged.contextJson = deepDecrypt(restoreJson(merged.contextJson ?? merged.context_json ?? entry.contextJson ?? entry.context_json))\n merged.context_json = merged.contextJson\n\n return merged as T\n } catch (err) {\n if (!decryptionWarningLogged) {\n decryptionWarningLogged = true\n logger.warn('Failed to decrypt action log entry', { err })\n }\n return entry\n }\n }\n\n private async decryptEntries(entries: ActionLog | ActionLog[] | null | undefined): Promise<void> {\n if (!entries) return\n\n const list = Array.isArray(entries) ? entries : [entries]\n for (const entry of list) {\n Object.assign(entry as unknown as Record<string, unknown>, await this.decryptEntryPayload(entry as unknown as Record<string, unknown>))\n }\n }\n\n private async decryptDetachedEntry(entry: ActionLog): Promise<ActionLog> {\n const detached = { ...(entry as unknown as Record<string, unknown>) }\n return await this.decryptEntryPayload(detached) as unknown as ActionLog\n }\n\n async log(input: ActionLogCreateInput, transactionalEm?: EntityManager): Promise<ActionLog | null> {\n const data = this.parseCreateInput(input)\n const operationEm = transactionalEm ?? this.em.fork()\n const log = this.createLogEntity(operationEm, data)\n await operationEm.persist(log).flush()\n if (transactionalEm) return this.decryptDetachedEntry(log)\n await this.decryptEntries(log)\n return log\n }\n\n private parseCreateInput(input: ActionLogCreateInput): ActionLogCreateInput {\n const sanitized = this.sanitizeActor(input)\n let data: ActionLogCreateInput\n const schema = actionLogCreateSchema as typeof actionLogCreateSchema & { _zod?: unknown }\n const canValidate = Boolean(schema && typeof schema.parse === 'function')\n const shouldValidate = canValidate && runtimeValidationAvailable !== false\n\n if (shouldValidate) {\n try {\n data = schema.parse(sanitized)\n runtimeValidationAvailable = true\n } catch (err) {\n if (!isZodRuntimeMissing(err) && !validationWarningLogged) {\n validationWarningLogged = true\n logger.warn('Falling back to permissive action log payload parser', { err })\n }\n if (isZodRuntimeMissing(err)) runtimeValidationAvailable = false\n data = this.normalizeInput(sanitized)\n }\n } else {\n data = this.normalizeInput(sanitized)\n }\n\n return data\n }\n\n private sanitizeActor(input: ActionLogCreateInput): ActionLogCreateInput {\n if (!input) return input\n const candidate = readActorCandidate(input.actorUserId)\n const actor = canonicalizeActorSubject(candidate)\n const actorUserId = actor?.storageId ?? null\n const systemActorReference = candidate && !actorUserId ? toSystemActorReference(candidate) : null\n\n if (actor) {\n const context = isRecord(input.context) ? { ...input.context } : {}\n context[ACTION_LOG_ACTOR_SUBJECT_CONTEXT_KEY] = actor.subject\n return { ...input, actorUserId, context }\n }\n\n if (!systemActorReference) return { ...input, actorUserId: null }\n\n const context = isRecord(input.context) ? { ...input.context } : {}\n if (context[SYSTEM_ACTOR_CONTEXT_KEY] === undefined) {\n context[SYSTEM_ACTOR_CONTEXT_KEY] = systemActorReference\n }\n\n return { ...input, actorUserId, context }\n }\n\n private createLogEntity(fork: EntityManager, data: ActionLogCreateInput): ActionLog {\n const projection = deriveActionLogProjection({\n actorUserId: data.actorUserId ?? null,\n actionLabel: data.actionLabel ?? null,\n changes: isRecord(data.changes) ? data.changes : null,\n commandId: data.commandId,\n context: isRecord(data.context) ? data.context : null,\n snapshotBefore: data.snapshotBefore,\n })\n\n return fork.create(ActionLog, {\n tenantId: data.tenantId ?? null,\n organizationId: data.organizationId ?? null,\n actorUserId: data.actorUserId ?? null,\n onBehalfOfUserId: data.onBehalfOfUserId ?? null,\n commandId: data.commandId,\n actionLabel: data.actionLabel ?? null,\n actionType: projection.actionType,\n resourceKind: data.resourceKind ?? null,\n resourceId: data.resourceId ?? null,\n parentResourceKind: data.parentResourceKind ?? null,\n parentResourceId: data.parentResourceId ?? null,\n relatedResourceKind: toOptionalString(data.relatedResourceKind) ?? null,\n relatedResourceId: toOptionalString(data.relatedResourceId) ?? null,\n executionState: data.executionState ?? 'done',\n undoToken: data.undoToken ?? null,\n commandPayload: data.commandPayload ?? null,\n snapshotBefore: data.snapshotBefore ?? null,\n snapshotAfter: data.snapshotAfter ?? null,\n changesJson: isRecord(data.changes) ? data.changes : null,\n changedFields: projection.changedFields,\n primaryChangedField: projection.primaryChangedField,\n contextJson: isRecord(data.context) ? data.context : null,\n sourceKey: projection.sourceKey,\n createdAt: new Date(),\n updatedAt: new Date(),\n })\n }\n\n private normalizeInput(input: Partial<ActionLogCreateInput> | null | undefined): ActionLogCreateInput {\n if (!input) {\n return {\n tenantId: null,\n organizationId: null,\n actorUserId: null,\n onBehalfOfUserId: null,\n commandId: 'unknown',\n actionLabel: undefined,\n resourceKind: undefined,\n resourceId: undefined,\n relatedResourceKind: null,\n relatedResourceId: null,\n executionState: 'done',\n undoToken: undefined,\n commandPayload: undefined,\n snapshotBefore: undefined,\n snapshotAfter: undefined,\n changes: undefined,\n context: undefined,\n }\n }\n\n const normalizeRecordLike = (value: unknown): ActionLogCreateInput['changes'] => {\n if (value === null) return null\n if (Array.isArray(value)) return value\n if (typeof value === 'object') return value as Record<string, unknown>\n return undefined\n }\n\n const normalizeContext = (value: unknown) => (\n typeof value === 'object' && value !== null && !Array.isArray(value)\n ? value as Record<string, unknown>\n : undefined\n )\n\n return {\n tenantId: toNullableUuid(input.tenantId),\n organizationId: toNullableUuid(input.organizationId),\n actorUserId: normalizeActorUserId(input.actorUserId),\n onBehalfOfUserId: toNullableUuid(input.onBehalfOfUserId),\n commandId: typeof input.commandId === 'string' && input.commandId.length > 0 ? input.commandId : 'unknown',\n actionLabel: toOptionalString(input.actionLabel) ?? undefined,\n resourceKind: toOptionalString(input.resourceKind) ?? undefined,\n resourceId: toOptionalString(input.resourceId) ?? undefined,\n parentResourceKind: toOptionalString(input.parentResourceKind) ?? null,\n parentResourceId: toOptionalString(input.parentResourceId) ?? null,\n relatedResourceKind: toOptionalString(input.relatedResourceKind) ?? null,\n relatedResourceId: toOptionalString(input.relatedResourceId) ?? null,\n executionState: input.executionState === 'undone' || input.executionState === 'failed' ? input.executionState : 'done',\n undoToken: toOptionalString(input.undoToken) ?? undefined,\n commandPayload: input.commandPayload,\n snapshotBefore: input.snapshotBefore,\n snapshotAfter: input.snapshotAfter,\n changes: normalizeRecordLike(input.changes),\n context: normalizeContext(input.context),\n }\n }\n\n private parseListQuery(query: Partial<ActionLogListQuery>) {\n return actionLogListQuerySchema.parse({\n ...query,\n })\n }\n\n private resolveActorUserIds(parsed: ParsedActionLogListQuery): string[] {\n const values = [...(parsed.actorUserIds ?? [])]\n if (parsed.actorUserId) values.push(parsed.actorUserId)\n\n return Array.from(new Set(values.map((value) => value.trim()).filter(Boolean)))\n }\n\n private resolveFieldNames(parsed: ParsedActionLogListQuery): string[] {\n const values = [...(parsed.fieldNames ?? [])]\n if (parsed.fieldName) values.push(parsed.fieldName)\n\n return Array.from(new Set(values.map((value) => value.trim()).filter(Boolean)))\n }\n\n private resolveActionTypes(parsed: ParsedActionLogListQuery): ActionLogFilterType[] {\n const values = [...(parsed.actionTypes ?? [])]\n if (parsed.actionType) values.push(parsed.actionType)\n\n return Array.from(new Set(values))\n .filter((value): value is ActionLogFilterType => ACTION_LOG_FILTER_TYPES.includes(value as ActionLogFilterType))\n }\n\n private resolvePagination(parsed: ParsedActionLogListQuery): { page: number; pageSize: number; offset: number; limit: number } {\n const pageSize = parsed.pageSize ?? parsed.limit ?? DEFAULT_LIST_PAGE_SIZE\n const page = parsed.page ?? 1\n const offset = parsed.offset ?? (page - 1) * pageSize\n return { page, pageSize, offset, limit: pageSize }\n }\n\n private async loadEntries(parsed: ParsedActionLogListQuery, options?: { paginate?: boolean }) {\n let query = (this.buildListQuery(parsed) as any).select('action_logs.id as id')\n\n if (options?.paginate !== false) {\n const { limit, offset } = this.resolvePagination(parsed)\n query = query.limit(limit).offset(offset)\n }\n\n const rows = await query.execute()\n const ids = rows.map((row: any) => row.id).filter(Boolean)\n if (ids.length === 0) return []\n\n const results = await this.em.find(ActionLog, {\n id: { $in: ids } as any,\n deletedAt: null,\n })\n await this.decryptEntries(results)\n\n const byId = new Map(results.map((entry: any) => [entry.id, entry]))\n const entries = ids\n .map((id: any) => byId.get(id))\n .filter((entry: any): entry is ActionLog => Boolean(entry))\n const apiKeyActor = parsed.actorSubject ? canonicalizeActorSubject(parsed.actorSubject) : null\n if (apiKeyActor?.kind !== 'api_key') return entries\n return entries.filter((entry: ActionLog) => (\n this.classifyApiKeyReplayEntry(entry, apiKeyActor.subject) !== 'invalid'\n ))\n }\n\n private buildListQuery(parsed: ParsedActionLogListQuery): any {\n let query = (this.em.getKysely<any>() as any)\n .selectFrom('action_logs')\n .selectAll()\n .where('action_logs.deleted_at', 'is', null) as any\n\n if (parsed.tenantId) query = query.where('action_logs.tenant_id', '=', parsed.tenantId)\n if (parsed.organizationId) query = query.where('action_logs.organization_id', '=', parsed.organizationId)\n\n if (parsed.actorSubject) {\n const actor = canonicalizeActorSubject(parsed.actorSubject)\n if (!actor) return query.where(sql<boolean>`false`)\n query = query.where('action_logs.actor_user_id', '=', actor.storageId)\n if (actor.kind === 'api_key') {\n query = query.where(sql<boolean>`(\n action_logs.context_json is null\n or jsonb_typeof(action_logs.context_json) = 'string'\n or (\n jsonb_typeof(action_logs.context_json) = 'object'\n and (\n not jsonb_exists(action_logs.context_json, ${ACTION_LOG_ACTOR_SUBJECT_CONTEXT_KEY})\n or action_logs.context_json ->> ${ACTION_LOG_ACTOR_SUBJECT_CONTEXT_KEY} = ${actor.subject}\n )\n )\n )`)\n }\n }\n\n const actorUserIds = this.resolveActorUserIds(parsed)\n if (actorUserIds.length === 1) query = query.where('action_logs.actor_user_id', '=', actorUserIds[0])\n if (actorUserIds.length > 1) query = query.where('action_logs.actor_user_id', 'in', actorUserIds)\n\n if (parsed.includeRelated && parsed.resourceKind && parsed.resourceId) {\n query = query.where((eb: any) =>\n eb.or([\n eb.and([\n eb('action_logs.resource_kind', '=', parsed.resourceKind),\n eb('action_logs.resource_id', '=', parsed.resourceId),\n ]),\n eb.and([\n eb('action_logs.parent_resource_kind', '=', parsed.resourceKind),\n eb('action_logs.parent_resource_id', '=', parsed.resourceId),\n ]),\n eb.and([\n eb('action_logs.related_resource_kind', '=', parsed.resourceKind),\n eb('action_logs.related_resource_id', '=', parsed.resourceId),\n ]),\n ])\n )\n } else {\n if (parsed.resourceKind) query = query.where('action_logs.resource_kind', '=', parsed.resourceKind)\n if (parsed.resourceId) query = query.where('action_logs.resource_id', '=', parsed.resourceId)\n }\n\n if (parsed.undoableOnly) query = query.where('action_logs.undo_token', 'is not', null)\n if (parsed.before) query = query.where('action_logs.created_at', '<', parsed.before)\n if (parsed.after) query = query.where('action_logs.created_at', '>', parsed.after)\n\n const fieldNames = this.resolveFieldNames(parsed)\n if (fieldNames.length === 1) query = query.where('action_logs.primary_changed_field', '=', fieldNames[0])\n if (fieldNames.length > 1) query = query.where('action_logs.primary_changed_field', 'in', fieldNames)\n\n const actionTypes = this.resolveActionTypes(parsed)\n if (actionTypes.length === 1) query = query.where('action_logs.action_type', '=', actionTypes[0])\n if (actionTypes.length > 1) query = query.where('action_logs.action_type', 'in', actionTypes)\n\n if (parsed.sortField === 'user') {\n query = query.leftJoin('users as audit_actor', 'audit_actor.id', 'action_logs.actor_user_id')\n }\n\n const sortDir = parsed.sortDir === 'asc' ? 'asc' : 'desc'\n switch (parsed.sortField) {\n case 'user':\n query = query.orderBy(sql`coalesce(nullif(audit_actor.name, ''), audit_actor.email, '')`, sortDir)\n break\n case 'action':\n query = query.orderBy(sql`coalesce(action_logs.action_type, '')`, sortDir)\n break\n case 'field':\n query = query.orderBy(sql`coalesce(action_logs.primary_changed_field, '')`, sortDir)\n break\n case 'source':\n query = query.orderBy(sql`coalesce(action_logs.source_key, '')`, sortDir)\n break\n case 'createdAt':\n default:\n query = query.orderBy(SORT_FIELDS.createdAt, sortDir)\n query = query.orderBy('action_logs.id', sortDir)\n return query\n }\n\n query = query.orderBy('action_logs.created_at', 'desc')\n query = query.orderBy('action_logs.id', 'desc')\n return query\n }\n\n async count(query: Partial<ActionLogListQuery>) {\n const parsed = this.parseListQuery(query)\n const row = await (this.buildListQuery(parsed) as any)\n .clearSelect()\n .clearOrderBy()\n .select(sql<string>`count(*)`.as('count'))\n .executeTakeFirst()\n\n if (!row) return 0\n const rawCount = row.count ?? 0\n return typeof rawCount === 'number' ? rawCount : Number.parseInt(rawCount, 10) || 0\n }\n\n async list(query: Partial<ActionLogListQuery>) {\n const parsed = this.parseListQuery(query)\n const { page, pageSize } = this.resolvePagination(parsed)\n const [items, total] = await Promise.all([\n this.loadEntries(parsed),\n this.count(parsed),\n ])\n const totalPages = Math.max(1, Math.ceil((total || 0) / (pageSize || 1)))\n return { items, total, page, pageSize, totalPages }\n }\n\n private classifyApiKeyReplayEntry(entry: ActionLog, actorSubject: string): ApiKeyReplayEntryKind {\n if (entry.contextJson === null || entry.contextJson === undefined) return 'legacy'\n if (!isRecord(entry.contextJson)) return 'invalid'\n if (!Object.prototype.hasOwnProperty.call(entry.contextJson, ACTION_LOG_ACTOR_SUBJECT_CONTEXT_KEY)) {\n return 'legacy'\n }\n return actionLogBelongsToSubject(entry, actorSubject) ? 'canonical' : 'invalid'\n }\n\n private async findLatestReplayEntry(\n actorUserId: string,\n filter: Record<string, unknown>,\n orderField: 'createdAt' | 'updatedAt',\n ): Promise<ActionLog | null> {\n const actor = canonicalizeActorSubject(actorUserId)\n if (!actor) return null\n const where: Record<string, unknown> = {\n actorUserId: actor.storageId,\n ...filter,\n }\n const orderBy = orderField === 'createdAt'\n ? { createdAt: 'desc' as const, id: 'desc' as const }\n : { updatedAt: 'desc' as const, id: 'desc' as const }\n\n if (actor.kind === 'user') {\n const entry = await this.em.findOne(ActionLog, where as FilterQuery<ActionLog>, { orderBy })\n await this.decryptEntries(entry)\n return entry\n }\n\n if (this.tenantEncryptionService?.isEnabled()) {\n let legacyEntry: ActionLog | null = null\n let cursor: { id: string; orderedAt: Date } | null = null\n for (let pageIndex = 0; ; pageIndex += 1) {\n if (pageIndex >= REPLAY_ENCRYPTED_SCAN_MAX_PAGES) {\n logger.warn('Stopped encrypted API-key replay scan at the page limit', {\n maxRows: REPLAY_ENCRYPTED_SCAN_MAX_PAGES * REPLAY_QUERY_BATCH_SIZE,\n hasLegacyFallback: legacyEntry !== null,\n })\n return legacyEntry\n }\n const pageWhere: Record<string, unknown> = cursor\n ? {\n $and: [\n where,\n {\n $or: [\n { [orderField]: { $lt: cursor.orderedAt } },\n { [orderField]: cursor.orderedAt, id: { $lt: cursor.id } },\n ],\n },\n ],\n }\n : where\n const entries: ActionLog[] = await this.em.find(ActionLog, pageWhere as FilterQuery<ActionLog>, {\n limit: REPLAY_QUERY_BATCH_SIZE,\n orderBy,\n })\n await this.decryptEntries(entries)\n for (const entry of entries) {\n const entryKind = this.classifyApiKeyReplayEntry(entry, actor.subject)\n if (entryKind === 'canonical') return entry\n if (entryKind === 'legacy' && !legacyEntry) legacyEntry = entry\n }\n if (entries.length < REPLAY_QUERY_BATCH_SIZE) return legacyEntry\n const lastEntry: ActionLog | undefined = entries.at(-1)\n if (!lastEntry) return legacyEntry\n cursor = {\n id: String(lastEntry.id),\n orderedAt: orderField === 'createdAt' ? lastEntry.createdAt : lastEntry.updatedAt,\n }\n }\n }\n\n const canonicalEntry = await this.em.findOne(ActionLog, {\n ...where,\n [raw(`\"context_json\" ->> '${ACTION_LOG_ACTOR_SUBJECT_CONTEXT_KEY}'`)]: actor.subject,\n } as FilterQuery<ActionLog>, { orderBy })\n if (canonicalEntry) {\n await this.decryptEntries(canonicalEntry)\n return this.classifyApiKeyReplayEntry(canonicalEntry, actor.subject) === 'canonical'\n ? canonicalEntry\n : null\n }\n\n const legacyEntry = await this.em.findOne(ActionLog, {\n ...where,\n [raw(`(\"context_json\" is null or (jsonb_typeof(\"context_json\") = 'object' and not jsonb_exists(\"context_json\", '${ACTION_LOG_ACTOR_SUBJECT_CONTEXT_KEY}')))`)]: true,\n } as FilterQuery<ActionLog>, { orderBy })\n await this.decryptEntries(legacyEntry)\n return legacyEntry && this.classifyApiKeyReplayEntry(legacyEntry, actor.subject) === 'legacy'\n ? legacyEntry\n : null\n }\n\n async latestUndoableForActor(actorUserId: string, scope: { tenantId?: string | null; organizationId?: string | null }) {\n const where: Record<string, unknown> = {\n undoToken: { $ne: null },\n executionState: 'done',\n deletedAt: null,\n }\n if (scope.tenantId) where.tenantId = scope.tenantId\n if (scope.organizationId) where.organizationId = scope.organizationId\n\n return this.findLatestReplayEntry(actorUserId, where, 'createdAt')\n }\n\n async claimForUndo(id: string, transactionalEm?: EntityManager): Promise<boolean> {\n const operationEm = transactionalEm ?? this.em\n const affected = await operationEm.nativeUpdate(\n ActionLog,\n { id, executionState: 'done', deletedAt: null },\n { executionState: 'undoing' },\n )\n return affected === 1\n }\n\n async releaseUndoClaim(id: string, transactionalEm?: EntityManager): Promise<boolean> {\n const operationEm = transactionalEm ?? this.em\n const affected = await operationEm.nativeUpdate(\n ActionLog,\n { id, executionState: 'undoing', deletedAt: null },\n { executionState: 'done' },\n )\n return affected === 1\n }\n\n async markUndone(id: string, traceInput?: ActionLogCreateInput, transactionalEm?: EntityManager) {\n const operationEm = transactionalEm ?? this.em.fork()\n const log = await operationEm.findOne(ActionLog, { id, deletedAt: null })\n if (!log) return null\n\n log.executionState = 'undone'\n log.undoToken = null\n\n const traceLog = traceInput ? this.createLogEntity(operationEm, this.parseCreateInput(traceInput)) : null\n if (traceLog) {\n operationEm.persist(traceLog)\n }\n\n await operationEm.flush()\n if (transactionalEm) return this.decryptDetachedEntry(log)\n await this.decryptEntries(log)\n if (traceLog) await this.decryptEntries(traceLog)\n\n return log\n }\n\n async findByUndoToken(undoToken: string) {\n const entry = await this.em.findOne(ActionLog, { undoToken, deletedAt: null })\n await this.decryptEntries(entry)\n return entry\n }\n\n async findById(id: string) {\n const entry = await this.em.findOne(ActionLog, { id, deletedAt: null })\n await this.decryptEntries(entry)\n return entry\n }\n\n async latestUndoableForResource(params: {\n actorUserId: string\n tenantId?: string | null\n organizationId?: string | null\n resourceKind?: string | null\n resourceId?: string | null\n }) {\n const where: Record<string, unknown> = {\n undoToken: { $ne: null },\n executionState: 'done',\n deletedAt: null,\n }\n if (params.tenantId) where.tenantId = params.tenantId\n if (params.organizationId) where.organizationId = params.organizationId\n if (params.resourceKind) where.resourceKind = params.resourceKind\n if (params.resourceId) where.resourceId = params.resourceId\n\n return this.findLatestReplayEntry(params.actorUserId, where, 'createdAt')\n }\n\n async latestUndoneForActor(actorUserId: string, scope: { tenantId?: string | null; organizationId?: string | null }) {\n const where: Record<string, unknown> = {\n executionState: 'undone',\n deletedAt: null,\n }\n if (scope.tenantId) where.tenantId = scope.tenantId\n if (scope.organizationId) where.organizationId = scope.organizationId\n\n return this.findLatestReplayEntry(actorUserId, where, 'updatedAt')\n }\n\n async markRedone(id: string) {\n const operationEm = this.em.fork()\n const log = await operationEm.findOne(ActionLog, { id, deletedAt: null })\n if (!log) return null\n\n log.executionState = 'redone'\n log.undoToken = null\n await operationEm.flush()\n return log\n }\n\n /**\n * Compare-and-set the source of a redo from `undone` to its final `redone`\n * state. When supplied, `transactionalEm` is the same EntityManager used by\n * the replayed command and its new action log, so a later failure restores\n * the source row to `undone` automatically.\n */\n async claimForRedo(id: string, transactionalEm?: EntityManager): Promise<boolean> {\n const operationEm = transactionalEm ?? this.em\n const affected = await operationEm.nativeUpdate(\n ActionLog,\n { id, executionState: 'undone', deletedAt: null },\n { executionState: 'redone', undoToken: null },\n )\n return affected === 1\n }\n\n async backfillProjections(options: ActionLogProjectionBackfillOptions = {}): Promise<ActionLogProjectionBackfillResult> {\n const batchSize = Math.min(Math.max(Math.trunc(options.batchSize ?? 250), 1), 1000)\n const logger = options.logger ?? (() => {})\n const result: ActionLogProjectionBackfillResult = {\n errors: 0,\n scanned: 0,\n skipped: 0,\n updated: 0,\n }\n\n let cursorCreatedAt: Date | null = null\n let cursorId: string | null = null\n\n while (true) {\n const rowsQuery = (this.em.getKysely<any>() as any)\n .selectFrom('action_logs')\n .select([\n 'action_logs.id',\n 'action_logs.tenant_id',\n 'action_logs.organization_id',\n 'action_logs.actor_user_id',\n 'action_logs.command_id',\n 'action_logs.action_label',\n 'action_logs.snapshot_before',\n 'action_logs.changes_json',\n 'action_logs.context_json',\n 'action_logs.action_type',\n 'action_logs.source_key',\n 'action_logs.changed_fields',\n 'action_logs.primary_changed_field',\n 'action_logs.created_at',\n ])\n .where('action_logs.deleted_at', 'is', null) as any\n\n if (options.tenantId) rowsQuery.where('action_logs.tenant_id', '=', options.tenantId)\n if (options.organizationId) rowsQuery.where('action_logs.organization_id', '=', options.organizationId)\n\n if (!options.force) {\n rowsQuery.where((eb: any) =>\n eb.or([\n eb('action_logs.action_type', 'is', null),\n eb('action_logs.source_key', 'is', null),\n eb('action_logs.changed_fields', 'is', null),\n ])\n )\n }\n\n if (cursorCreatedAt && cursorId) {\n rowsQuery.where((eb: any) =>\n eb.or([\n eb('action_logs.created_at', '>', cursorCreatedAt),\n eb.and([\n eb('action_logs.created_at', '=', cursorCreatedAt),\n eb('action_logs.id', '>', cursorId),\n ]),\n ])\n )\n }\n\n const rows = await rowsQuery\n .orderBy('created_at', 'asc')\n .orderBy('id', 'asc')\n .limit(batchSize)\n\n if (rows.length === 0) break\n\n for (const row of rows) {\n result.scanned += 1\n\n try {\n const decrypted = await this.decryptEntryPayload(row as unknown as Record<string, unknown>)\n const projection = deriveActionLogProjection({\n actorUserId: readString(decrypted, 'actorUserId', 'actor_user_id'),\n actionLabel: readString(decrypted, 'actionLabel', 'action_label'),\n changes: readRecord(decrypted, 'changesJson', 'changes_json'),\n commandId: readString(decrypted, 'commandId', 'command_id') ?? 'unknown',\n context: readRecord(decrypted, 'contextJson', 'context_json'),\n snapshotBefore: readValue(decrypted, 'snapshotBefore', 'snapshot_before'),\n })\n\n const needsUpdate = options.force === true\n || row.action_type !== projection.actionType\n || row.source_key !== projection.sourceKey\n || row.primary_changed_field !== projection.primaryChangedField\n || !stringArraysEqual(row.changed_fields, projection.changedFields)\n\n if (!needsUpdate) {\n result.skipped += 1\n continue\n }\n\n await (this.em.getKysely<any>() as any)\n .updateTable('action_logs')\n .set({\n action_type: projection.actionType,\n changed_fields: projection.changedFields,\n primary_changed_field: projection.primaryChangedField,\n source_key: projection.sourceKey,\n })\n .where('id', '=', row.id)\n .execute()\n\n result.updated += 1\n } catch (err) {\n result.errors += 1\n logger(`[backfill] Failed for action log ${row.id}: ${err instanceof Error ? err.message : String(err)}`)\n }\n }\n\n const lastRow = rows[rows.length - 1]\n cursorCreatedAt = lastRow.created_at\n cursorId = lastRow.id\n\n logger(\n `[backfill] Processed ${result.scanned} action logs (updated: ${result.updated}, skipped: ${result.skipped}, errors: ${result.errors})`,\n )\n\n if (rows.length < batchSize) break\n }\n\n return result\n }\n}\n"],
|
|
5
|
+
"mappings": "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",
|
|
6
6
|
"names": ["decrypted", "context", "legacyEntry", "logger"]
|
|
7
7
|
}
|
|
@@ -67,7 +67,7 @@ import { createLogger } from "@open-mercato/shared/lib/logger";
|
|
|
67
67
|
import { UserCircle2 } from "lucide-react";
|
|
68
68
|
import { ReassignOwnerDialog } from "./components/ReassignOwnerDialog.js";
|
|
69
69
|
import { requestBulkOwnerReassignment, buildReassignOwnerSuccess } from "../../../lib/reassignDealOwner.js";
|
|
70
|
-
import { buildDealDuplicatePayload } from "../../../lib/duplicateDeal.js";
|
|
70
|
+
import { buildDealDuplicatePayload, formatDealDuplicateTitle } from "../../../lib/duplicateDeal.js";
|
|
71
71
|
const logger = createLogger("customers");
|
|
72
72
|
function makeDealsPresets() {
|
|
73
73
|
return [
|
|
@@ -451,7 +451,10 @@ function CustomersDealsPage() {
|
|
|
451
451
|
if (pendingDuplicateId) return;
|
|
452
452
|
const payload = buildDealDuplicatePayload(
|
|
453
453
|
row,
|
|
454
|
-
|
|
454
|
+
formatDealDuplicateTitle(
|
|
455
|
+
row.title,
|
|
456
|
+
(title) => t("customers.deals.list.duplicateTitle", "{title} (copy)", { title })
|
|
457
|
+
)
|
|
455
458
|
);
|
|
456
459
|
setPendingDuplicateId(row.id);
|
|
457
460
|
try {
|