@open-mercato/core 0.6.7-develop.6623.1.01d02d34c3 → 0.6.7-develop.6628.1.f9e4ecf70f

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.
Files changed (29) hide show
  1. package/dist/modules/audit_logs/api/audit-logs/access/route.js +20 -12
  2. package/dist/modules/audit_logs/api/audit-logs/access/route.js.map +2 -2
  3. package/dist/modules/audit_logs/api/audit-logs/actions/export/route.js +25 -17
  4. package/dist/modules/audit_logs/api/audit-logs/actions/export/route.js.map +2 -2
  5. package/dist/modules/audit_logs/api/audit-logs/actions/route.js +9 -1
  6. package/dist/modules/audit_logs/api/audit-logs/actions/route.js.map +2 -2
  7. package/dist/modules/customers/api/dictionaries/[kind]/route.js +7 -2
  8. package/dist/modules/customers/api/dictionaries/[kind]/route.js.map +2 -2
  9. package/dist/modules/customers/components/formConfig.js +22 -9
  10. package/dist/modules/customers/components/formConfig.js.map +2 -2
  11. package/dist/modules/customers/lib/dictionaries.js +2 -0
  12. package/dist/modules/customers/lib/dictionaries.js.map +2 -2
  13. package/dist/modules/dictionaries/components/DictionaryEntrySelect.js +22 -5
  14. package/dist/modules/dictionaries/components/DictionaryEntrySelect.js.map +2 -2
  15. package/dist/modules/entities/backend/entities/user/[entityId]/page.js +12 -1
  16. package/dist/modules/entities/backend/entities/user/[entityId]/page.js.map +2 -2
  17. package/package.json +7 -7
  18. package/src/modules/audit_logs/api/audit-logs/access/route.ts +20 -12
  19. package/src/modules/audit_logs/api/audit-logs/actions/export/route.ts +25 -17
  20. package/src/modules/audit_logs/api/audit-logs/actions/route.ts +9 -1
  21. package/src/modules/customers/api/dictionaries/[kind]/route.ts +6 -1
  22. package/src/modules/customers/components/formConfig.tsx +26 -8
  23. package/src/modules/customers/lib/dictionaries.ts +1 -0
  24. package/src/modules/dictionaries/components/DictionaryEntrySelect.tsx +25 -4
  25. package/src/modules/entities/backend/entities/user/[entityId]/page.tsx +19 -2
  26. package/src/modules/entities/i18n/de.json +1 -0
  27. package/src/modules/entities/i18n/en.json +1 -0
  28. package/src/modules/entities/i18n/es.json +1 -0
  29. package/src/modules/entities/i18n/pl.json +1 -0
@@ -90,18 +90,26 @@ async function GET(req) {
90
90
  if (canViewTenant && actorQuery) {
91
91
  actorUserId = actorQuery;
92
92
  }
93
- const list = await accessLogs.list({
94
- tenantId: auth.tenantId ?? void 0,
95
- organizationId: organizationId ?? void 0,
96
- actorUserId,
97
- resourceKind: resourceKind ?? void 0,
98
- accessType: accessType ?? void 0,
99
- page,
100
- pageSize,
101
- limit: url.searchParams.get("limit") ? parseNumber(url.searchParams.get("limit"), { min: 1, max: 200, fallback: pageSize }) : void 0,
102
- before,
103
- after
104
- });
93
+ let list;
94
+ try {
95
+ list = await accessLogs.list({
96
+ tenantId: auth.tenantId ?? void 0,
97
+ organizationId: organizationId ?? void 0,
98
+ actorUserId,
99
+ resourceKind: resourceKind ?? void 0,
100
+ accessType: accessType ?? void 0,
101
+ page,
102
+ pageSize,
103
+ limit: url.searchParams.get("limit") ? parseNumber(url.searchParams.get("limit"), { min: 1, max: 200, fallback: pageSize }) : void 0,
104
+ before,
105
+ after
106
+ });
107
+ } catch (err) {
108
+ if (err instanceof z.ZodError) {
109
+ return NextResponse.json({ error: "Validation failed", details: err.issues }, { status: 400 });
110
+ }
111
+ throw err;
112
+ }
105
113
  const displayMaps = await loadAuditLogDisplayMaps(em, {
106
114
  userIds: list.items.map((entry) => entry.actorUserId).filter((value) => !!value),
107
115
  tenantIds: list.items.map((entry) => entry.tenantId).filter((value) => !!value),
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "version": 3,
3
3
  "sources": ["../../../../../../src/modules/audit_logs/api/audit-logs/access/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 { AccessLogService } from '@open-mercato/core/modules/audit_logs/services/accessLogService'\nimport type { EntityManager } from '@mikro-orm/postgresql'\nimport { loadAuditLogDisplayMaps } from '../display'\nimport { z } from 'zod'\nimport type { OpenApiRouteDoc } from '@open-mercato/shared/lib/openapi'\n\nexport const metadata = {\n GET: { requireAuth: true, requireFeatures: ['audit_logs.view_self'] },\n}\n\nconst auditAccessQuerySchema = z.object({\n organizationId: z.string().uuid().describe('Limit results to a specific organization').optional(),\n actorUserId: z.string().uuid().describe('Filter by actor user id (tenant administrators only)').optional(),\n resourceKind: z.string().describe('Restrict to a resource kind such as `order` or `product`').optional(),\n accessType: z.string().describe('Access type filter, e.g. `read` or `export`').optional(),\n page: z.string().describe('Page number (default 1)').optional(),\n pageSize: z.string().describe('Page size (default 50)').optional(),\n limit: z.string().describe('Explicit maximum number of records when paginating manually').optional(),\n before: z.string().describe('Return logs created before this ISO-8601 timestamp').optional(),\n after: z.string().describe('Return logs created after this ISO-8601 timestamp').optional(),\n})\n\nconst auditAccessItemSchema = z.object({\n id: z.string(),\n resourceKind: z.string(),\n resourceId: z.string(),\n accessType: z.string(),\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 fields: z.array(z.string()),\n context: z.record(z.string(), z.unknown()).nullable(),\n createdAt: z.string(),\n})\n\nconst auditAccessResponseSchema = z.object({\n items: z.array(auditAccessItemSchema),\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 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\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 accessLogs = (container.resolve('accessLogService') as AccessLogService)\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')\n const accessType = url.searchParams.get('accessType')\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 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 let actorUserId = auth.sub\n if (canViewTenant && actorQuery) {\n actorUserId = actorQuery\n }\n\n const list = await accessLogs.list({\n tenantId: auth.tenantId ?? undefined,\n organizationId: organizationId ?? undefined,\n actorUserId,\n resourceKind: resourceKind ?? undefined,\n accessType: accessType ?? undefined,\n page,\n pageSize,\n limit: url.searchParams.get('limit') ? parseNumber(url.searchParams.get('limit'), { min: 1, max: 200, fallback: pageSize }) : undefined,\n before,\n after,\n })\n\n const displayMaps = await loadAuditLogDisplayMaps(em, {\n userIds: list.items.map((entry) => entry.actorUserId).filter((value): value is string => !!value),\n tenantIds: list.items.map((entry) => entry.tenantId).filter((value): value is string => !!value),\n organizationIds: list.items.map((entry) => entry.organizationId).filter((value): value is string => !!value),\n })\n\n const items = list.items.map((entry) => ({\n id: entry.id,\n resourceKind: entry.resourceKind,\n resourceId: entry.resourceId,\n accessType: entry.accessType,\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 fields: entry.fieldsJson ?? [],\n context: entry.contextJson,\n createdAt: entry.createdAt?.toISOString?.() ?? entry.createdAt,\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 access audit logs',\n description: 'Retrieve read-only audit log entries detailing resource access within the current tenant or organization.',\n methods: {\n GET: {\n summary: 'Retrieve access logs',\n description:\n 'Fetches paginated access audit logs scoped to the authenticated user. Tenant administrators can optionally expand the search to other actors or organizations.',\n query: auditAccessQuerySchema,\n responses: [\n { status: 200, description: 'Access logs returned successfully', schema: auditAccessResponseSchema },\n ],\n errors: [\n { status: 400, description: 'Invalid filters supplied', schema: errorSchema },\n { status: 401, description: 'Authentication required', schema: errorSchema },\n ],\n },\n },\n}\n"],
5
- "mappings": "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",
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 { AccessLogService } from '@open-mercato/core/modules/audit_logs/services/accessLogService'\nimport type { EntityManager } from '@mikro-orm/postgresql'\nimport { loadAuditLogDisplayMaps } from '../display'\nimport { z } from 'zod'\nimport type { OpenApiRouteDoc } from '@open-mercato/shared/lib/openapi'\n\nexport const metadata = {\n GET: { requireAuth: true, requireFeatures: ['audit_logs.view_self'] },\n}\n\nconst auditAccessQuerySchema = z.object({\n organizationId: z.string().uuid().describe('Limit results to a specific organization').optional(),\n actorUserId: z.string().uuid().describe('Filter by actor user id (tenant administrators only)').optional(),\n resourceKind: z.string().describe('Restrict to a resource kind such as `order` or `product`').optional(),\n accessType: z.string().describe('Access type filter, e.g. `read` or `export`').optional(),\n page: z.string().describe('Page number (default 1)').optional(),\n pageSize: z.string().describe('Page size (default 50)').optional(),\n limit: z.string().describe('Explicit maximum number of records when paginating manually').optional(),\n before: z.string().describe('Return logs created before this ISO-8601 timestamp').optional(),\n after: z.string().describe('Return logs created after this ISO-8601 timestamp').optional(),\n})\n\nconst auditAccessItemSchema = z.object({\n id: z.string(),\n resourceKind: z.string(),\n resourceId: z.string(),\n accessType: z.string(),\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 fields: z.array(z.string()),\n context: z.record(z.string(), z.unknown()).nullable(),\n createdAt: z.string(),\n})\n\nconst auditAccessResponseSchema = z.object({\n items: z.array(auditAccessItemSchema),\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 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\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 accessLogs = (container.resolve('accessLogService') as AccessLogService)\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')\n const accessType = url.searchParams.get('accessType')\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 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 let actorUserId = auth.sub\n if (canViewTenant && actorQuery) {\n actorUserId = actorQuery\n }\n\n let list: Awaited<ReturnType<AccessLogService['list']>>\n try {\n list = await accessLogs.list({\n tenantId: auth.tenantId ?? undefined,\n organizationId: organizationId ?? undefined,\n actorUserId,\n resourceKind: resourceKind ?? undefined,\n accessType: accessType ?? undefined,\n page,\n pageSize,\n limit: url.searchParams.get('limit') ? parseNumber(url.searchParams.get('limit'), { min: 1, max: 200, fallback: pageSize }) : undefined,\n before,\n after,\n })\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 displayMaps = await loadAuditLogDisplayMaps(em, {\n userIds: list.items.map((entry) => entry.actorUserId).filter((value): value is string => !!value),\n tenantIds: list.items.map((entry) => entry.tenantId).filter((value): value is string => !!value),\n organizationIds: list.items.map((entry) => entry.organizationId).filter((value): value is string => !!value),\n })\n\n const items = list.items.map((entry) => ({\n id: entry.id,\n resourceKind: entry.resourceKind,\n resourceId: entry.resourceId,\n accessType: entry.accessType,\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 fields: entry.fieldsJson ?? [],\n context: entry.contextJson,\n createdAt: entry.createdAt?.toISOString?.() ?? entry.createdAt,\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 access audit logs',\n description: 'Retrieve read-only audit log entries detailing resource access within the current tenant or organization.',\n methods: {\n GET: {\n summary: 'Retrieve access logs',\n description:\n 'Fetches paginated access audit logs scoped to the authenticated user. Tenant administrators can optionally expand the search to other actors or organizations.',\n query: auditAccessQuerySchema,\n responses: [\n { status: 200, description: 'Access logs returned successfully', schema: auditAccessResponseSchema },\n ],\n errors: [\n { status: 400, description: 'Invalid filters supplied', schema: errorSchema },\n { status: 401, description: 'Authentication required', schema: errorSchema },\n ],\n },\n },\n}\n"],
5
+ "mappings": "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",
6
6
  "names": []
7
7
  }
@@ -111,23 +111,31 @@ async function GET(req) {
111
111
  actorUserIds = parsedActorUserIds;
112
112
  }
113
113
  }
114
- const entriesResult = await actionLogs.list({
115
- tenantId: auth.tenantId ?? void 0,
116
- organizationId: organizationId ?? void 0,
117
- actorUserId,
118
- actorUserIds,
119
- resourceKind,
120
- resourceId,
121
- actionTypes,
122
- fieldNames,
123
- includeRelated,
124
- undoableOnly,
125
- sortField,
126
- sortDir,
127
- limit,
128
- before,
129
- after
130
- });
114
+ let entriesResult;
115
+ try {
116
+ entriesResult = await actionLogs.list({
117
+ tenantId: auth.tenantId ?? void 0,
118
+ organizationId: organizationId ?? void 0,
119
+ actorUserId,
120
+ actorUserIds,
121
+ resourceKind,
122
+ resourceId,
123
+ actionTypes,
124
+ fieldNames,
125
+ includeRelated,
126
+ undoableOnly,
127
+ sortField,
128
+ sortDir,
129
+ limit,
130
+ before,
131
+ after
132
+ });
133
+ } catch (err) {
134
+ if (err instanceof z.ZodError) {
135
+ return NextResponse.json({ error: "Validation failed", details: err.issues }, { status: 400 });
136
+ }
137
+ throw err;
138
+ }
131
139
  const entries = entriesResult.items;
132
140
  const displayMaps = await loadAuditLogDisplayMaps(em, {
133
141
  userIds: entries.map((entry) => entry.actorUserId).filter((value) => Boolean(value)),
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "version": 3,
3
3
  "sources": ["../../../../../../../src/modules/audit_logs/api/audit-logs/actions/export/route.ts"],
4
- "sourcesContent": ["import { NextResponse } from 'next/server'\nimport { z } from 'zod'\nimport type { EntityManager } from '@mikro-orm/postgresql'\nimport { createRequestContainer } from '@open-mercato/shared/lib/di/container'\nimport { getAuthFromRequest } from '@open-mercato/shared/lib/auth/server'\nimport { defaultExportFilename, serializeExport, type PreparedExport } from '@open-mercato/shared/lib/crud/exporters'\nimport type { OpenApiRouteDoc } from '@open-mercato/shared/lib/openapi'\nimport { parseBooleanToken } from '@open-mercato/shared/lib/boolean'\nimport { resolveFeatureCheckContext } from '@open-mercato/core/modules/directory/utils/organizationScope'\nimport type { RbacService } from '@open-mercato/core/modules/auth/services/rbacService'\nimport { extractChangeRows } from '@open-mercato/core/modules/audit_logs/lib/changeRows'\nimport {\n ACTION_LOG_FILTER_TYPES,\n deriveActionLogActionType,\n deriveActionLogSource,\n} from '@open-mercato/core/modules/audit_logs/lib/projections'\nimport { ActionLogService } from '@open-mercato/core/modules/audit_logs/services/actionLogService'\nimport { loadAuditLogDisplayMaps } from '../../display'\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 exportQuerySchema = 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 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 export (default 1000, capped at 1000)').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 responseSchema = z.object({\n file: z.literal('csv'),\n})\n\nconst errorSchema = z.object({\n error: z.string(),\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 parseLimit(param: string | null): number {\n if (!param) return 1000\n const value = Number(param)\n if (!Number.isFinite(value)) return 1000\n return Math.min(Math.max(Math.trunc(value), 1), 1000)\n}\n\nfunction parseDate(value: string | null): Date | undefined {\n if (!value) return undefined\n const timestamp = Date.parse(value)\n if (Number.isNaN(timestamp)) return undefined\n return new Date(timestamp)\n}\n\nfunction formatValue(value: unknown): string {\n if (value == null) return ''\n if (value instanceof Date) return value.toISOString()\n if (Array.isArray(value)) return value.map((entry) => formatValue(entry)).filter(Boolean).join(', ')\n if (typeof value === 'object') return JSON.stringify(value)\n return String(value)\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\n const container = await createRequestContainer()\n const { organizationId: defaultOrganizationId, scope } = await resolveFeatureCheckContext({ container, auth, request: req })\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 undoableOnly = parseBooleanToken(url.searchParams.get('undoableOnly')) === true\n const limit = parseLimit(url.searchParams.get('limit'))\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 let actorUserId: string | undefined = canViewTenant ? undefined : auth.sub\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 entriesResult = await actionLogs.list({\n tenantId: auth.tenantId ?? undefined,\n organizationId: organizationId ?? undefined,\n actorUserId,\n actorUserIds,\n resourceKind,\n resourceId,\n actionTypes,\n fieldNames,\n includeRelated,\n undoableOnly,\n sortField,\n sortDir,\n limit,\n before,\n after,\n })\n const entries = entriesResult.items\n\n const displayMaps = await loadAuditLogDisplayMaps(em, {\n userIds: entries.map((entry: any) => entry.actorUserId).filter((value: any): value is string => Boolean(value)),\n tenantIds: entries.map((entry: any) => entry.tenantId).filter((value: any): value is string => Boolean(value)),\n organizationIds: entries.map((entry: any) => entry.organizationId).filter((value: any): value is string => Boolean(value)),\n })\n\n const rows = entries.flatMap((entry: any) => {\n const actionType = deriveActionLogActionType(entry)\n const actionLabel = actionType === 'system'\n ? entry.actionLabel ?? 'System'\n : actionType.charAt(0).toUpperCase() + actionType.slice(1)\n const baseRow = {\n when: entry.createdAt?.toISOString?.() ?? '',\n user: entry.actorUserId ? displayMaps.users[entry.actorUserId] ?? entry.actorUserId : 'System',\n action: actionLabel,\n source: deriveActionLogSource(entry.contextJson, entry.actorUserId).toUpperCase(),\n }\n const changes = extractChangeRows(entry.changesJson, entry.snapshotBefore)\n\n if (changes.length === 0) {\n return [{\n ...baseRow,\n field: '',\n oldValue: '',\n newValue: '',\n }]\n }\n\n return changes.map((change) => {\n return {\n ...baseRow,\n field: change.field,\n oldValue: formatValue(change.from),\n newValue: formatValue(change.to),\n }\n })\n })\n\n const prepared: PreparedExport = {\n columns: [\n { field: 'when', header: 'When' },\n { field: 'user', header: 'User' },\n { field: 'action', header: 'Action' },\n { field: 'field', header: 'Field' },\n { field: 'oldValue', header: 'Old Value' },\n { field: 'newValue', header: 'New Value' },\n { field: 'source', header: 'Source' },\n ],\n rows,\n }\n\n const serialized = serializeExport(prepared, 'csv')\n const filename = defaultExportFilename('changelog-export', 'csv')\n\n return new Response(serialized.body, {\n headers: {\n 'content-type': serialized.contentType,\n 'content-disposition': `attachment; filename=\"${filename}\"`,\n },\n })\n}\n\nexport const openApi: OpenApiRouteDoc = {\n summary: 'Export action audit logs',\n description: 'Exports filtered action audit log entries for the current tenant as CSV.',\n methods: {\n GET: {\n summary: 'Export action logs as CSV',\n description:\n 'Returns a CSV attachment containing filtered action audit log entries. Tenant administrators can widen the scope to other actors or organizations.',\n query: exportQuerySchema,\n responses: [\n { status: 200, description: 'CSV export generated successfully', schema: responseSchema },\n ],\n errors: [\n { status: 400, description: 'Invalid filter values', schema: errorSchema },\n { status: 401, description: 'Authentication required', schema: errorSchema },\n ],\n },\n },\n}\n"],
5
- "mappings": "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",
4
+ "sourcesContent": ["import { NextResponse } from 'next/server'\nimport { z } from 'zod'\nimport type { EntityManager } from '@mikro-orm/postgresql'\nimport { createRequestContainer } from '@open-mercato/shared/lib/di/container'\nimport { getAuthFromRequest } from '@open-mercato/shared/lib/auth/server'\nimport { defaultExportFilename, serializeExport, type PreparedExport } from '@open-mercato/shared/lib/crud/exporters'\nimport type { OpenApiRouteDoc } from '@open-mercato/shared/lib/openapi'\nimport { parseBooleanToken } from '@open-mercato/shared/lib/boolean'\nimport { resolveFeatureCheckContext } from '@open-mercato/core/modules/directory/utils/organizationScope'\nimport type { RbacService } from '@open-mercato/core/modules/auth/services/rbacService'\nimport { extractChangeRows } from '@open-mercato/core/modules/audit_logs/lib/changeRows'\nimport {\n ACTION_LOG_FILTER_TYPES,\n deriveActionLogActionType,\n deriveActionLogSource,\n} from '@open-mercato/core/modules/audit_logs/lib/projections'\nimport { ActionLogService } from '@open-mercato/core/modules/audit_logs/services/actionLogService'\nimport { loadAuditLogDisplayMaps } from '../../display'\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 exportQuerySchema = 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 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 export (default 1000, capped at 1000)').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 responseSchema = z.object({\n file: z.literal('csv'),\n})\n\nconst errorSchema = z.object({\n error: z.string(),\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 parseLimit(param: string | null): number {\n if (!param) return 1000\n const value = Number(param)\n if (!Number.isFinite(value)) return 1000\n return Math.min(Math.max(Math.trunc(value), 1), 1000)\n}\n\nfunction parseDate(value: string | null): Date | undefined {\n if (!value) return undefined\n const timestamp = Date.parse(value)\n if (Number.isNaN(timestamp)) return undefined\n return new Date(timestamp)\n}\n\nfunction formatValue(value: unknown): string {\n if (value == null) return ''\n if (value instanceof Date) return value.toISOString()\n if (Array.isArray(value)) return value.map((entry) => formatValue(entry)).filter(Boolean).join(', ')\n if (typeof value === 'object') return JSON.stringify(value)\n return String(value)\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\n const container = await createRequestContainer()\n const { organizationId: defaultOrganizationId, scope } = await resolveFeatureCheckContext({ container, auth, request: req })\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 undoableOnly = parseBooleanToken(url.searchParams.get('undoableOnly')) === true\n const limit = parseLimit(url.searchParams.get('limit'))\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 let actorUserId: string | undefined = canViewTenant ? undefined : auth.sub\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 let entriesResult: Awaited<ReturnType<ActionLogService['list']>>\n try {\n entriesResult = await actionLogs.list({\n tenantId: auth.tenantId ?? undefined,\n organizationId: organizationId ?? undefined,\n actorUserId,\n actorUserIds,\n resourceKind,\n resourceId,\n actionTypes,\n fieldNames,\n includeRelated,\n undoableOnly,\n sortField,\n sortDir,\n limit,\n before,\n after,\n })\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 const entries = entriesResult.items\n\n const displayMaps = await loadAuditLogDisplayMaps(em, {\n userIds: entries.map((entry: any) => entry.actorUserId).filter((value: any): value is string => Boolean(value)),\n tenantIds: entries.map((entry: any) => entry.tenantId).filter((value: any): value is string => Boolean(value)),\n organizationIds: entries.map((entry: any) => entry.organizationId).filter((value: any): value is string => Boolean(value)),\n })\n\n const rows = entries.flatMap((entry: any) => {\n const actionType = deriveActionLogActionType(entry)\n const actionLabel = actionType === 'system'\n ? entry.actionLabel ?? 'System'\n : actionType.charAt(0).toUpperCase() + actionType.slice(1)\n const baseRow = {\n when: entry.createdAt?.toISOString?.() ?? '',\n user: entry.actorUserId ? displayMaps.users[entry.actorUserId] ?? entry.actorUserId : 'System',\n action: actionLabel,\n source: deriveActionLogSource(entry.contextJson, entry.actorUserId).toUpperCase(),\n }\n const changes = extractChangeRows(entry.changesJson, entry.snapshotBefore)\n\n if (changes.length === 0) {\n return [{\n ...baseRow,\n field: '',\n oldValue: '',\n newValue: '',\n }]\n }\n\n return changes.map((change) => {\n return {\n ...baseRow,\n field: change.field,\n oldValue: formatValue(change.from),\n newValue: formatValue(change.to),\n }\n })\n })\n\n const prepared: PreparedExport = {\n columns: [\n { field: 'when', header: 'When' },\n { field: 'user', header: 'User' },\n { field: 'action', header: 'Action' },\n { field: 'field', header: 'Field' },\n { field: 'oldValue', header: 'Old Value' },\n { field: 'newValue', header: 'New Value' },\n { field: 'source', header: 'Source' },\n ],\n rows,\n }\n\n const serialized = serializeExport(prepared, 'csv')\n const filename = defaultExportFilename('changelog-export', 'csv')\n\n return new Response(serialized.body, {\n headers: {\n 'content-type': serialized.contentType,\n 'content-disposition': `attachment; filename=\"${filename}\"`,\n },\n })\n}\n\nexport const openApi: OpenApiRouteDoc = {\n summary: 'Export action audit logs',\n description: 'Exports filtered action audit log entries for the current tenant as CSV.',\n methods: {\n GET: {\n summary: 'Export action logs as CSV',\n description:\n 'Returns a CSV attachment containing filtered action audit log entries. Tenant administrators can widen the scope to other actors or organizations.',\n query: exportQuerySchema,\n responses: [\n { status: 200, description: 'CSV export generated successfully', schema: responseSchema },\n ],\n errors: [\n { status: 400, description: 'Invalid filter values', schema: errorSchema },\n { status: 401, description: 'Authentication required', schema: errorSchema },\n ],\n },\n },\n}\n"],
5
+ "mappings": "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",
6
6
  "names": []
7
7
  }
@@ -169,7 +169,15 @@ async function GET(req) {
169
169
  after
170
170
  };
171
171
  void includeTotal;
172
- const list = await actionLogs.list(listQuery);
172
+ let list;
173
+ try {
174
+ list = await actionLogs.list(listQuery);
175
+ } catch (err) {
176
+ if (err instanceof z.ZodError) {
177
+ return NextResponse.json({ error: "Validation failed", details: err.issues }, { status: 400 });
178
+ }
179
+ throw err;
180
+ }
173
181
  const displayMaps = await loadAuditLogDisplayMaps(em, {
174
182
  userIds: list.items.map((entry) => entry.actorUserId).filter((value) => !!value),
175
183
  tenantIds: list.items.map((entry) => entry.tenantId).filter((value) => !!value),
@@ -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 { 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'\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\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 let actorUserId: string | undefined = canViewTenant ? undefined : auth.sub\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 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 const list = await actionLogs.list(listQuery)\n\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 })\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 ],\n },\n },\n}\n"],
5
- "mappings": "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",
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 { 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'\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\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 let actorUserId: string | undefined = canViewTenant ? undefined : auth.sub\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 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 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 })\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 ],\n },\n },\n}\n"],
5
+ "mappings": "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",
6
6
  "names": []
7
7
  }
@@ -19,6 +19,7 @@ import {
19
19
  import { findWithDecryption, findOneWithDecryption } from "@open-mercato/shared/lib/encryption/find";
20
20
  import { readJsonSafe } from "@open-mercato/shared/lib/http/readJsonSafe";
21
21
  import { createLogger } from "@open-mercato/shared/lib/logger";
22
+ import { CUSTOMER_DICTIONARY_ORGANIZATION_REQUIRED_CODE } from "../../../lib/dictionaries.js";
22
23
  const logger = createLogger("customers");
23
24
  const colorSchema = z.string().trim().regex(/^#([0-9A-Fa-f]{6})$/, "Invalid color hex");
24
25
  const iconSchema = z.string().trim().min(1).max(48);
@@ -46,7 +47,10 @@ async function GET(req, ctx) {
46
47
  });
47
48
  const { kind, mappedKind } = mapDictionaryKind(ctx.params?.kind);
48
49
  if (!organizationId) {
49
- throw new CrudHttpError(400, { error: translate("customers.errors.organization_required", "Organization context is required") });
50
+ throw new CrudHttpError(400, {
51
+ error: translate("customers.errors.organization_required", "Organization context is required"),
52
+ code: CUSTOMER_DICTIONARY_ORGANIZATION_REQUIRED_CODE
53
+ });
50
54
  }
51
55
  const settings = await loadCustomerSettings(em, { tenantId, organizationId });
52
56
  const sortMode = resolveDictionaryEntrySortMode(settings?.dictionarySortModes?.[kind]);
@@ -252,7 +256,8 @@ const dictionaryListResponseSchema = z.object({
252
256
  items: z.array(dictionaryEntrySchema)
253
257
  });
254
258
  const dictionaryErrorSchema = z.object({
255
- error: z.string()
259
+ error: z.string(),
260
+ code: z.string().optional()
256
261
  });
257
262
  const openApi = {
258
263
  tag: "Customers",
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "version": 3,
3
3
  "sources": ["../../../../../../src/modules/customers/api/dictionaries/%5Bkind%5D/route.ts"],
4
- "sourcesContent": ["import { NextResponse } from 'next/server'\nimport { resolveTranslations } from '@open-mercato/shared/lib/i18n/server'\nimport { CrudHttpError, isCrudHttpError } from '@open-mercato/shared/lib/crud/errors'\nimport type { CommandBus } from '@open-mercato/shared/lib/commands'\nimport type { CommandExecuteResult } from '@open-mercato/shared/lib/commands/types'\nimport { serializeOperationMetadata } from '@open-mercato/shared/lib/commands/operationMetadata'\nimport { CustomerDictionaryEntry } from '../../../data/entities'\nimport { mapDictionaryKind, resolveDictionaryActorId, resolveDictionaryRouteContext } from '../context'\nimport { createDictionaryCacheKey, createDictionaryCacheTags, invalidateDictionaryCache, DICTIONARY_CACHE_TTL_MS } from '../cache'\nimport { loadCustomerSettings } from '../../../commands/settings'\nimport {\n resolveDictionaryEntrySortMode,\n sortDictionaryEntries,\n} from '@open-mercato/core/modules/dictionaries/lib/entrySort'\nimport { z } from 'zod'\nimport type { OpenApiRouteDoc } from '@open-mercato/shared/lib/openapi'\nimport { loadRoleTypeUsageMap, resolveRoleTypeUsageKey } from '../../../lib/roleTypeUsage'\nimport {\n runCrudMutationGuardAfterSuccess,\n validateCrudMutationGuard,\n} from '@open-mercato/shared/lib/crud/mutation-guard'\nimport { findWithDecryption, findOneWithDecryption } from '@open-mercato/shared/lib/encryption/find'\nimport { readJsonSafe } from '@open-mercato/shared/lib/http/readJsonSafe'\nimport { createLogger } from '@open-mercato/shared/lib/logger'\n\nconst logger = createLogger('customers')\n\nconst colorSchema = z.string().trim().regex(/^#([0-9A-Fa-f]{6})$/, 'Invalid color hex')\nconst iconSchema = z.string().trim().min(1).max(48)\n\nconst postSchema = z.object({\n value: z.string().trim().min(1).max(150),\n label: z.string().trim().max(150).optional(),\n color: colorSchema.or(z.null()).optional(),\n icon: iconSchema.or(z.null()).optional(),\n})\n\nconst querySchema = z.object({\n organizationId: z.string().uuid().optional(),\n})\n\nexport const metadata = {\n GET: { requireAuth: true, requireFeatures: ['customers.people.view'] },\n POST: { requireAuth: true, requireFeatures: ['customers.settings.manage'] },\n}\n\nexport async function GET(req: Request, ctx: { params?: { kind?: string } }) {\n try {\n const url = new URL(req.url)\n const query = querySchema.parse({\n organizationId: url.searchParams.get('organizationId') ?? undefined,\n })\n const { translate, em, organizationId, readableOrganizationIds, tenantId, cache } = await resolveDictionaryRouteContext(req, {\n selectedId: query.organizationId ?? undefined,\n })\n const { kind, mappedKind } = mapDictionaryKind(ctx.params?.kind)\n if (!organizationId) {\n throw new CrudHttpError(400, { error: translate('customers.errors.organization_required', 'Organization context is required') })\n }\n const settings = await loadCustomerSettings(em, { tenantId, organizationId })\n const sortMode = resolveDictionaryEntrySortMode(settings?.dictionarySortModes?.[kind])\n const scopedOrganizationIds = readableOrganizationIds.length > 0 ? readableOrganizationIds : [organizationId]\n const canUseCache = Boolean(cache) && mappedKind !== 'person_company_role'\n\n let cacheKey: string | null = null\n if (canUseCache && cache) {\n cacheKey = createDictionaryCacheKey({\n tenantId,\n organizationId,\n mappedKind,\n sortMode,\n readableOrganizationIds: scopedOrganizationIds,\n })\n const cached = await cache.get(cacheKey)\n if (cached) {\n return NextResponse.json(cached)\n }\n }\n\n const entries = await findWithDecryption(\n em,\n CustomerDictionaryEntry,\n { tenantId, kind: mappedKind, organizationId: { $in: scopedOrganizationIds } } as any,\n { orderBy: { label: 'asc' } },\n { tenantId, organizationId },\n )\n\n const inheritedPriority = new Map(scopedOrganizationIds.map((id, index) => [id, index]))\n const sortByLabel = (left: CustomerDictionaryEntry, right: CustomerDictionaryEntry) =>\n left.label.localeCompare(right.label, undefined, { sensitivity: 'base' })\n\n const localEntries = entries\n .filter((entry) => entry.organizationId === organizationId)\n .sort(sortByLabel)\n const inheritedEntries = entries\n .filter((entry) => entry.organizationId !== organizationId)\n .sort((left, right) => {\n const leftPriority = inheritedPriority.get(left.organizationId) ?? Number.MAX_SAFE_INTEGER\n const rightPriority = inheritedPriority.get(right.organizationId) ?? Number.MAX_SAFE_INTEGER\n if (leftPriority !== rightPriority) return leftPriority - rightPriority\n return sortByLabel(left, right)\n })\n\n const preferredEntries = new Map<string, CustomerDictionaryEntry>()\n for (const entry of [...localEntries, ...inheritedEntries]) {\n const normalizedValue = entry.normalizedValue?.trim() || entry.value.trim().toLowerCase()\n if (!normalizedValue || preferredEntries.has(normalizedValue)) continue\n preferredEntries.set(normalizedValue, entry)\n }\n\n const preferredEntryList = Array.from(preferredEntries.values())\n const usageByEntryKey =\n mappedKind === 'person_company_role'\n ? await loadRoleTypeUsageMap(em, {\n tenantId,\n entries: preferredEntryList.map((entry) => ({\n organizationId: entry.organizationId,\n value: entry.value,\n })),\n })\n : new Map()\n\n const items = sortDictionaryEntries(\n preferredEntryList\n .map((entry) => ({\n id: entry.id,\n value: entry.value,\n label: entry.label,\n color: entry.color,\n icon: entry.icon,\n organizationId: entry.organizationId,\n isInherited: entry.organizationId !== organizationId,\n createdAt: entry.createdAt,\n updatedAt: entry.updatedAt,\n ...(mappedKind === 'person_company_role'\n ? {\n usageCount:\n usageByEntryKey.get(resolveRoleTypeUsageKey(entry.organizationId, entry.value))?.total ?? 0,\n }\n : {}),\n })),\n sortMode,\n )\n\n const responseBody = {\n sortMode,\n items,\n }\n\n if (canUseCache && cache && cacheKey) {\n const tags = createDictionaryCacheTags({\n tenantId,\n mappedKind,\n organizationIds: scopedOrganizationIds,\n })\n try {\n await cache.set(cacheKey, responseBody, {\n ttl: DICTIONARY_CACHE_TTL_MS,\n tags,\n })\n } catch (err) {\n logger.warn('Failed to set cache entry', { component: 'dictionaries.cache', err })\n }\n }\n\n return NextResponse.json(responseBody)\n } catch (err) {\n if (isCrudHttpError(err)) {\n return NextResponse.json(err.body, { status: err.status })\n }\n const { translate } = await resolveTranslations()\n logger.error('customers.dictionaries.list failed', { err })\n return NextResponse.json({ error: translate('customers.errors.lookup_failed', 'Failed to load dictionary entries') }, { status: 400 })\n }\n}\n\nexport async function POST(req: Request, ctx: { params?: { kind?: string } }) {\n try {\n const context = await resolveDictionaryRouteContext(req)\n if (!context.organizationId) {\n throw new CrudHttpError(400, { error: context.translate('customers.errors.organization_required', 'Organization context is required') })\n }\n const { mappedKind } = mapDictionaryKind(ctx.params?.kind)\n const body = postSchema.parse(await readJsonSafe(req, {}))\n const guardUserId = resolveDictionaryActorId(context.auth)\n const guardResult = await validateCrudMutationGuard(context.container, {\n tenantId: context.tenantId,\n organizationId: context.organizationId,\n userId: guardUserId,\n resourceKind: 'customers.dictionary_entry',\n resourceId: '',\n operation: 'create',\n requestMethod: req.method,\n requestHeaders: req.headers,\n mutationPayload: body,\n })\n if (guardResult && !guardResult.ok) {\n return NextResponse.json(guardResult.body, { status: guardResult.status })\n }\n const commandBus = (context.container.resolve('commandBus') as CommandBus)\n const { result, logEntry } =\n (await commandBus.execute('customers.dictionaryEntries.create', {\n input: {\n tenantId: context.tenantId,\n organizationId: context.organizationId,\n kind: mappedKind,\n value: body.value,\n label: body.label,\n color: body.color,\n icon: body.icon,\n },\n ctx: context.ctx,\n })) as CommandExecuteResult<{ entryId: string; mode: 'created' | 'updated' | 'unchanged' }>\n const entry = await findOneWithDecryption(context.em.fork(), CustomerDictionaryEntry, result.entryId, {}, { tenantId: context.tenantId, organizationId: context.organizationId })\n if (!entry) {\n throw new CrudHttpError(400, { error: context.translate('customers.errors.lookup_failed', 'Failed to save dictionary entry') })\n }\n\n await invalidateDictionaryCache(context.cache, {\n tenantId: context.tenantId,\n mappedKind,\n organizationIds: [entry.organizationId],\n })\n\n if (guardResult?.ok && guardResult.shouldRunAfterSuccess) {\n await runCrudMutationGuardAfterSuccess(context.container, {\n tenantId: context.tenantId,\n organizationId: context.organizationId,\n userId: guardUserId,\n resourceKind: 'customers.dictionary_entry',\n resourceId: entry.id,\n operation: 'create',\n requestMethod: req.method,\n requestHeaders: req.headers,\n metadata: guardResult.metadata ?? null,\n })\n }\n\n const response = NextResponse.json(\n {\n id: entry.id,\n value: entry.value,\n label: entry.label,\n color: entry.color,\n icon: entry.icon,\n organizationId: entry.organizationId,\n isInherited: false,\n },\n { status: result.mode === 'created' ? 201 : 200 }\n )\n if (logEntry?.undoToken && logEntry?.id && logEntry?.commandId) {\n response.headers.set(\n 'x-om-operation',\n serializeOperationMetadata({\n id: logEntry.id,\n undoToken: logEntry.undoToken,\n commandId: logEntry.commandId,\n actionLabel: logEntry.actionLabel ?? null,\n resourceKind: logEntry.resourceKind ?? 'customers.dictionary_entry',\n resourceId: entry.id,\n executedAt: logEntry.createdAt instanceof Date ? logEntry.createdAt.toISOString() : undefined,\n })\n )\n }\n return response\n } catch (err) {\n if (isCrudHttpError(err)) {\n return NextResponse.json(err.body, { status: err.status })\n }\n const { translate } = await resolveTranslations()\n logger.error('customers.dictionaries.create failed', { err })\n return NextResponse.json({ error: translate('customers.errors.lookup_failed', 'Failed to save dictionary entry') }, { status: 400 })\n }\n}\n\nconst dictionaryEntrySchema = z.object({\n id: z.string().uuid(),\n value: z.string(),\n label: z.string().nullable().optional(),\n color: z.string().nullable().optional(),\n icon: z.string().nullable().optional(),\n organizationId: z.string().uuid().nullable().optional(),\n isInherited: z.boolean().optional(),\n createdAt: z.date().or(z.string()).optional(),\n updatedAt: z.date().or(z.string()).optional(),\n})\n\nconst dictionaryListResponseSchema = z.object({\n sortMode: z.string().optional(),\n items: z.array(dictionaryEntrySchema),\n})\n\nconst dictionaryErrorSchema = z.object({\n error: z.string(),\n})\n\nexport const openApi: OpenApiRouteDoc = {\n tag: 'Customers',\n summary: 'Customer dictionary entries',\n methods: {\n GET: {\n summary: 'List dictionary entries',\n description: 'Returns dictionary entries for the requested kind within the currently selected organization.',\n responses: [\n { status: 200, description: 'Dictionary entries', schema: dictionaryListResponseSchema },\n { status: 401, description: 'Unauthorized', schema: dictionaryErrorSchema },\n { status: 400, description: 'Failed to resolve dictionary context', schema: dictionaryErrorSchema },\n ],\n },\n POST: {\n summary: 'Create or override dictionary entry',\n description: 'Creates a dictionary entry (or updates the existing entry for the same value) within the current organization scope.',\n requestBody: {\n contentType: 'application/json',\n schema: postSchema,\n },\n responses: [\n { status: 201, description: 'Dictionary entry created', schema: dictionaryEntrySchema },\n { status: 200, description: 'Dictionary entry updated', schema: dictionaryEntrySchema },\n ],\n errors: [\n { status: 400, description: 'Invalid payload', schema: dictionaryErrorSchema },\n { status: 401, description: 'Unauthorized', schema: dictionaryErrorSchema },\n { status: 409, description: 'Duplicate value conflict', schema: dictionaryErrorSchema },\n ],\n },\n },\n}\n"],
5
- "mappings": "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",
4
+ "sourcesContent": ["import { NextResponse } from 'next/server'\nimport { resolveTranslations } from '@open-mercato/shared/lib/i18n/server'\nimport { CrudHttpError, isCrudHttpError } from '@open-mercato/shared/lib/crud/errors'\nimport type { CommandBus } from '@open-mercato/shared/lib/commands'\nimport type { CommandExecuteResult } from '@open-mercato/shared/lib/commands/types'\nimport { serializeOperationMetadata } from '@open-mercato/shared/lib/commands/operationMetadata'\nimport { CustomerDictionaryEntry } from '../../../data/entities'\nimport { mapDictionaryKind, resolveDictionaryActorId, resolveDictionaryRouteContext } from '../context'\nimport { createDictionaryCacheKey, createDictionaryCacheTags, invalidateDictionaryCache, DICTIONARY_CACHE_TTL_MS } from '../cache'\nimport { loadCustomerSettings } from '../../../commands/settings'\nimport {\n resolveDictionaryEntrySortMode,\n sortDictionaryEntries,\n} from '@open-mercato/core/modules/dictionaries/lib/entrySort'\nimport { z } from 'zod'\nimport type { OpenApiRouteDoc } from '@open-mercato/shared/lib/openapi'\nimport { loadRoleTypeUsageMap, resolveRoleTypeUsageKey } from '../../../lib/roleTypeUsage'\nimport {\n runCrudMutationGuardAfterSuccess,\n validateCrudMutationGuard,\n} from '@open-mercato/shared/lib/crud/mutation-guard'\nimport { findWithDecryption, findOneWithDecryption } from '@open-mercato/shared/lib/encryption/find'\nimport { readJsonSafe } from '@open-mercato/shared/lib/http/readJsonSafe'\nimport { createLogger } from '@open-mercato/shared/lib/logger'\nimport { CUSTOMER_DICTIONARY_ORGANIZATION_REQUIRED_CODE } from '../../../lib/dictionaries'\n\nconst logger = createLogger('customers')\n\nconst colorSchema = z.string().trim().regex(/^#([0-9A-Fa-f]{6})$/, 'Invalid color hex')\nconst iconSchema = z.string().trim().min(1).max(48)\n\nconst postSchema = z.object({\n value: z.string().trim().min(1).max(150),\n label: z.string().trim().max(150).optional(),\n color: colorSchema.or(z.null()).optional(),\n icon: iconSchema.or(z.null()).optional(),\n})\n\nconst querySchema = z.object({\n organizationId: z.string().uuid().optional(),\n})\n\nexport const metadata = {\n GET: { requireAuth: true, requireFeatures: ['customers.people.view'] },\n POST: { requireAuth: true, requireFeatures: ['customers.settings.manage'] },\n}\n\nexport async function GET(req: Request, ctx: { params?: { kind?: string } }) {\n try {\n const url = new URL(req.url)\n const query = querySchema.parse({\n organizationId: url.searchParams.get('organizationId') ?? undefined,\n })\n const { translate, em, organizationId, readableOrganizationIds, tenantId, cache } = await resolveDictionaryRouteContext(req, {\n selectedId: query.organizationId ?? undefined,\n })\n const { kind, mappedKind } = mapDictionaryKind(ctx.params?.kind)\n if (!organizationId) {\n throw new CrudHttpError(400, {\n error: translate('customers.errors.organization_required', 'Organization context is required'),\n code: CUSTOMER_DICTIONARY_ORGANIZATION_REQUIRED_CODE,\n })\n }\n const settings = await loadCustomerSettings(em, { tenantId, organizationId })\n const sortMode = resolveDictionaryEntrySortMode(settings?.dictionarySortModes?.[kind])\n const scopedOrganizationIds = readableOrganizationIds.length > 0 ? readableOrganizationIds : [organizationId]\n const canUseCache = Boolean(cache) && mappedKind !== 'person_company_role'\n\n let cacheKey: string | null = null\n if (canUseCache && cache) {\n cacheKey = createDictionaryCacheKey({\n tenantId,\n organizationId,\n mappedKind,\n sortMode,\n readableOrganizationIds: scopedOrganizationIds,\n })\n const cached = await cache.get(cacheKey)\n if (cached) {\n return NextResponse.json(cached)\n }\n }\n\n const entries = await findWithDecryption(\n em,\n CustomerDictionaryEntry,\n { tenantId, kind: mappedKind, organizationId: { $in: scopedOrganizationIds } } as any,\n { orderBy: { label: 'asc' } },\n { tenantId, organizationId },\n )\n\n const inheritedPriority = new Map(scopedOrganizationIds.map((id, index) => [id, index]))\n const sortByLabel = (left: CustomerDictionaryEntry, right: CustomerDictionaryEntry) =>\n left.label.localeCompare(right.label, undefined, { sensitivity: 'base' })\n\n const localEntries = entries\n .filter((entry) => entry.organizationId === organizationId)\n .sort(sortByLabel)\n const inheritedEntries = entries\n .filter((entry) => entry.organizationId !== organizationId)\n .sort((left, right) => {\n const leftPriority = inheritedPriority.get(left.organizationId) ?? Number.MAX_SAFE_INTEGER\n const rightPriority = inheritedPriority.get(right.organizationId) ?? Number.MAX_SAFE_INTEGER\n if (leftPriority !== rightPriority) return leftPriority - rightPriority\n return sortByLabel(left, right)\n })\n\n const preferredEntries = new Map<string, CustomerDictionaryEntry>()\n for (const entry of [...localEntries, ...inheritedEntries]) {\n const normalizedValue = entry.normalizedValue?.trim() || entry.value.trim().toLowerCase()\n if (!normalizedValue || preferredEntries.has(normalizedValue)) continue\n preferredEntries.set(normalizedValue, entry)\n }\n\n const preferredEntryList = Array.from(preferredEntries.values())\n const usageByEntryKey =\n mappedKind === 'person_company_role'\n ? await loadRoleTypeUsageMap(em, {\n tenantId,\n entries: preferredEntryList.map((entry) => ({\n organizationId: entry.organizationId,\n value: entry.value,\n })),\n })\n : new Map()\n\n const items = sortDictionaryEntries(\n preferredEntryList\n .map((entry) => ({\n id: entry.id,\n value: entry.value,\n label: entry.label,\n color: entry.color,\n icon: entry.icon,\n organizationId: entry.organizationId,\n isInherited: entry.organizationId !== organizationId,\n createdAt: entry.createdAt,\n updatedAt: entry.updatedAt,\n ...(mappedKind === 'person_company_role'\n ? {\n usageCount:\n usageByEntryKey.get(resolveRoleTypeUsageKey(entry.organizationId, entry.value))?.total ?? 0,\n }\n : {}),\n })),\n sortMode,\n )\n\n const responseBody = {\n sortMode,\n items,\n }\n\n if (canUseCache && cache && cacheKey) {\n const tags = createDictionaryCacheTags({\n tenantId,\n mappedKind,\n organizationIds: scopedOrganizationIds,\n })\n try {\n await cache.set(cacheKey, responseBody, {\n ttl: DICTIONARY_CACHE_TTL_MS,\n tags,\n })\n } catch (err) {\n logger.warn('Failed to set cache entry', { component: 'dictionaries.cache', err })\n }\n }\n\n return NextResponse.json(responseBody)\n } catch (err) {\n if (isCrudHttpError(err)) {\n return NextResponse.json(err.body, { status: err.status })\n }\n const { translate } = await resolveTranslations()\n logger.error('customers.dictionaries.list failed', { err })\n return NextResponse.json({ error: translate('customers.errors.lookup_failed', 'Failed to load dictionary entries') }, { status: 400 })\n }\n}\n\nexport async function POST(req: Request, ctx: { params?: { kind?: string } }) {\n try {\n const context = await resolveDictionaryRouteContext(req)\n if (!context.organizationId) {\n throw new CrudHttpError(400, { error: context.translate('customers.errors.organization_required', 'Organization context is required') })\n }\n const { mappedKind } = mapDictionaryKind(ctx.params?.kind)\n const body = postSchema.parse(await readJsonSafe(req, {}))\n const guardUserId = resolveDictionaryActorId(context.auth)\n const guardResult = await validateCrudMutationGuard(context.container, {\n tenantId: context.tenantId,\n organizationId: context.organizationId,\n userId: guardUserId,\n resourceKind: 'customers.dictionary_entry',\n resourceId: '',\n operation: 'create',\n requestMethod: req.method,\n requestHeaders: req.headers,\n mutationPayload: body,\n })\n if (guardResult && !guardResult.ok) {\n return NextResponse.json(guardResult.body, { status: guardResult.status })\n }\n const commandBus = (context.container.resolve('commandBus') as CommandBus)\n const { result, logEntry } =\n (await commandBus.execute('customers.dictionaryEntries.create', {\n input: {\n tenantId: context.tenantId,\n organizationId: context.organizationId,\n kind: mappedKind,\n value: body.value,\n label: body.label,\n color: body.color,\n icon: body.icon,\n },\n ctx: context.ctx,\n })) as CommandExecuteResult<{ entryId: string; mode: 'created' | 'updated' | 'unchanged' }>\n const entry = await findOneWithDecryption(context.em.fork(), CustomerDictionaryEntry, result.entryId, {}, { tenantId: context.tenantId, organizationId: context.organizationId })\n if (!entry) {\n throw new CrudHttpError(400, { error: context.translate('customers.errors.lookup_failed', 'Failed to save dictionary entry') })\n }\n\n await invalidateDictionaryCache(context.cache, {\n tenantId: context.tenantId,\n mappedKind,\n organizationIds: [entry.organizationId],\n })\n\n if (guardResult?.ok && guardResult.shouldRunAfterSuccess) {\n await runCrudMutationGuardAfterSuccess(context.container, {\n tenantId: context.tenantId,\n organizationId: context.organizationId,\n userId: guardUserId,\n resourceKind: 'customers.dictionary_entry',\n resourceId: entry.id,\n operation: 'create',\n requestMethod: req.method,\n requestHeaders: req.headers,\n metadata: guardResult.metadata ?? null,\n })\n }\n\n const response = NextResponse.json(\n {\n id: entry.id,\n value: entry.value,\n label: entry.label,\n color: entry.color,\n icon: entry.icon,\n organizationId: entry.organizationId,\n isInherited: false,\n },\n { status: result.mode === 'created' ? 201 : 200 }\n )\n if (logEntry?.undoToken && logEntry?.id && logEntry?.commandId) {\n response.headers.set(\n 'x-om-operation',\n serializeOperationMetadata({\n id: logEntry.id,\n undoToken: logEntry.undoToken,\n commandId: logEntry.commandId,\n actionLabel: logEntry.actionLabel ?? null,\n resourceKind: logEntry.resourceKind ?? 'customers.dictionary_entry',\n resourceId: entry.id,\n executedAt: logEntry.createdAt instanceof Date ? logEntry.createdAt.toISOString() : undefined,\n })\n )\n }\n return response\n } catch (err) {\n if (isCrudHttpError(err)) {\n return NextResponse.json(err.body, { status: err.status })\n }\n const { translate } = await resolveTranslations()\n logger.error('customers.dictionaries.create failed', { err })\n return NextResponse.json({ error: translate('customers.errors.lookup_failed', 'Failed to save dictionary entry') }, { status: 400 })\n }\n}\n\nconst dictionaryEntrySchema = z.object({\n id: z.string().uuid(),\n value: z.string(),\n label: z.string().nullable().optional(),\n color: z.string().nullable().optional(),\n icon: z.string().nullable().optional(),\n organizationId: z.string().uuid().nullable().optional(),\n isInherited: z.boolean().optional(),\n createdAt: z.date().or(z.string()).optional(),\n updatedAt: z.date().or(z.string()).optional(),\n})\n\nconst dictionaryListResponseSchema = z.object({\n sortMode: z.string().optional(),\n items: z.array(dictionaryEntrySchema),\n})\n\nconst dictionaryErrorSchema = z.object({\n error: z.string(),\n code: z.string().optional(),\n})\n\nexport const openApi: OpenApiRouteDoc = {\n tag: 'Customers',\n summary: 'Customer dictionary entries',\n methods: {\n GET: {\n summary: 'List dictionary entries',\n description: 'Returns dictionary entries for the requested kind within the currently selected organization.',\n responses: [\n { status: 200, description: 'Dictionary entries', schema: dictionaryListResponseSchema },\n { status: 401, description: 'Unauthorized', schema: dictionaryErrorSchema },\n { status: 400, description: 'Failed to resolve dictionary context', schema: dictionaryErrorSchema },\n ],\n },\n POST: {\n summary: 'Create or override dictionary entry',\n description: 'Creates a dictionary entry (or updates the existing entry for the same value) within the current organization scope.',\n requestBody: {\n contentType: 'application/json',\n schema: postSchema,\n },\n responses: [\n { status: 201, description: 'Dictionary entry created', schema: dictionaryEntrySchema },\n { status: 200, description: 'Dictionary entry updated', schema: dictionaryEntrySchema },\n ],\n errors: [\n { status: 400, description: 'Invalid payload', schema: dictionaryErrorSchema },\n { status: 401, description: 'Unauthorized', schema: dictionaryErrorSchema },\n { status: 409, description: 'Duplicate value conflict', schema: dictionaryErrorSchema },\n ],\n },\n },\n}\n"],
5
+ "mappings": "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",
6
6
  "names": []
7
7
  }
@@ -33,7 +33,8 @@ import { collectCustomFieldValues } from "@open-mercato/ui/backend/utils/customF
33
33
  import { PhoneNumberField } from "@open-mercato/ui/backend/inputs/PhoneNumberField";
34
34
  import { isValidPhoneNumber } from "@open-mercato/shared/lib/phone";
35
35
  import {
36
- DictionaryEntrySelect
36
+ DictionaryEntrySelect,
37
+ DictionaryOptionsUnavailableError
37
38
  } from "@open-mercato/core/modules/dictionaries/components/DictionaryEntrySelect";
38
39
  import { RolesSection } from "./detail/RolesSection.js";
39
40
  import { useQueryClient } from "@tanstack/react-query";
@@ -46,6 +47,7 @@ import {
46
47
  } from "./detail/hooks/useCustomerDictionary.js";
47
48
  import {
48
49
  CUSTOMER_DICTIONARIES_MANAGE_HREF,
50
+ CUSTOMER_DICTIONARY_ORGANIZATION_REQUIRED_CODE,
49
51
  getCustomerDictionaryManageHref
50
52
  } from "../lib/dictionaries.js";
51
53
  import { normalizeCustomFieldSubmitValue } from "./detail/customFieldUtils.js";
@@ -114,14 +116,25 @@ function DictionarySelectField({
114
116
  [translate]
115
117
  );
116
118
  const fetchOptions = React.useCallback(async () => {
117
- const data = await ensureCustomerDictionary(queryClient, kind, scopeVersion);
118
- return data.entries.map((entry) => ({
119
- value: entry.value,
120
- label: entry.label,
121
- color: entry.color ?? null,
122
- icon: entry.icon ?? null
123
- }));
124
- }, [kind, queryClient, scopeVersion]);
119
+ try {
120
+ const data = await ensureCustomerDictionary(queryClient, kind, scopeVersion);
121
+ return data.entries.map((entry) => ({
122
+ value: entry.value,
123
+ label: entry.label,
124
+ color: entry.color ?? null,
125
+ icon: entry.icon ?? null
126
+ }));
127
+ } catch (err) {
128
+ const responseError = err;
129
+ if (responseError?.status === 400 && responseError.code === CUSTOMER_DICTIONARY_ORGANIZATION_REQUIRED_CODE) {
130
+ const serverMessage = err instanceof Error ? err.message.trim() : "";
131
+ throw new DictionaryOptionsUnavailableError(
132
+ serverMessage.length ? serverMessage : translate("customers.errors.organization_required", "Organization context is required")
133
+ );
134
+ }
135
+ throw err;
136
+ }
137
+ }, [kind, queryClient, scopeVersion, translate]);
125
138
  const createOption = React.useCallback(
126
139
  async (input) => {
127
140
  const call = await apiCall(