@open-mercato/core 0.7.1-develop.7130.1.fef2396fd8 → 0.7.1-develop.7131.1.20abf9e885
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/modules/audit_logs/api/audit-logs/actions/redo/route.js +5 -0
- package/dist/modules/audit_logs/api/audit-logs/actions/redo/route.js.map +2 -2
- package/dist/modules/auth/api/profile/route.js +5 -0
- package/dist/modules/auth/api/profile/route.js.map +2 -2
- package/dist/modules/customers/api/activities/route.js +13 -0
- package/dist/modules/customers/api/activities/route.js.map +2 -2
- package/dist/modules/customers/api/dictionaries/[kind]/[id]/route.js +9 -0
- package/dist/modules/customers/api/dictionaries/[kind]/[id]/route.js.map +2 -2
- package/dist/modules/customers/api/dictionaries/[kind]/route.js +5 -0
- package/dist/modules/customers/api/dictionaries/[kind]/route.js.map +2 -2
- package/dist/modules/customers/api/dictionaries/kind-settings/route.js +5 -0
- package/dist/modules/customers/api/dictionaries/kind-settings/route.js.map +2 -2
- package/dist/modules/customers/api/interactions/cancel/route.js +5 -0
- package/dist/modules/customers/api/interactions/cancel/route.js.map +2 -2
- package/dist/modules/customers/api/interactions/complete/route.js +5 -0
- package/dist/modules/customers/api/interactions/complete/route.js.map +2 -2
- package/dist/modules/customers/api/labels/assign/route.js +5 -0
- package/dist/modules/customers/api/labels/assign/route.js.map +2 -2
- package/dist/modules/customers/api/labels/route.js +5 -0
- package/dist/modules/customers/api/labels/route.js.map +2 -2
- package/dist/modules/customers/api/labels/unassign/route.js +5 -0
- package/dist/modules/customers/api/labels/unassign/route.js.map +2 -2
- package/dist/modules/customers/api/people/[id]/companies/[linkId]/route.js +9 -0
- package/dist/modules/customers/api/people/[id]/companies/[linkId]/route.js.map +2 -2
- package/dist/modules/customers/api/people/[id]/companies/route.js +5 -0
- package/dist/modules/customers/api/people/[id]/companies/route.js.map +2 -2
- package/dist/modules/customers/api/pipeline-stages/reorder/route.js +5 -0
- package/dist/modules/customers/api/pipeline-stages/reorder/route.js.map +2 -2
- package/dist/modules/customers/api/pipeline-stages/route.js +13 -0
- package/dist/modules/customers/api/pipeline-stages/route.js.map +2 -2
- package/dist/modules/customers/api/pipelines/route.js +13 -0
- package/dist/modules/customers/api/pipelines/route.js.map +2 -2
- package/dist/modules/customers/api/settings/address-format/route.js +5 -0
- package/dist/modules/customers/api/settings/address-format/route.js.map +2 -2
- package/dist/modules/customers/api/settings/dictionary-sort-modes/route.js +5 -0
- package/dist/modules/customers/api/settings/dictionary-sort-modes/route.js.map +2 -2
- package/dist/modules/customers/api/settings/stuck-threshold/route.js +5 -0
- package/dist/modules/customers/api/settings/stuck-threshold/route.js.map +2 -2
- package/dist/modules/customers/api/tags/assign/route.js +5 -0
- package/dist/modules/customers/api/tags/assign/route.js.map +2 -2
- package/dist/modules/customers/api/tags/unassign/route.js +5 -0
- package/dist/modules/customers/api/tags/unassign/route.js.map +2 -2
- package/dist/modules/customers/api/todos/route.js +13 -0
- package/dist/modules/customers/api/todos/route.js.map +2 -2
- package/dist/modules/dictionaries/api/[dictionaryId]/entries/[entryId]/route.js +9 -0
- package/dist/modules/dictionaries/api/[dictionaryId]/entries/[entryId]/route.js.map +2 -2
- package/dist/modules/dictionaries/api/[dictionaryId]/entries/reorder/route.js +5 -0
- package/dist/modules/dictionaries/api/[dictionaryId]/entries/reorder/route.js.map +2 -2
- package/dist/modules/dictionaries/api/[dictionaryId]/entries/route.js +5 -0
- package/dist/modules/dictionaries/api/[dictionaryId]/entries/route.js.map +2 -2
- package/dist/modules/dictionaries/api/[dictionaryId]/entries/set-default/route.js +5 -0
- package/dist/modules/dictionaries/api/[dictionaryId]/entries/set-default/route.js.map +2 -2
- package/dist/modules/directory/api/organization-branding/route.js +5 -0
- package/dist/modules/directory/api/organization-branding/route.js.map +2 -2
- package/dist/modules/eudr/api/plots/import/route.js.map +2 -2
- package/dist/modules/eudr/api/product-mappings/suggestions/apply/route.js.map +2 -2
- package/dist/modules/feature_toggles/api/overrides/route.js +5 -0
- package/dist/modules/feature_toggles/api/overrides/route.js.map +2 -2
- package/dist/modules/messages/api/[id]/actions/[actionId]/route.js +5 -0
- package/dist/modules/messages/api/[id]/actions/[actionId]/route.js.map +2 -2
- package/dist/modules/messages/api/[id]/conversation/archive/route.js +5 -0
- package/dist/modules/messages/api/[id]/conversation/archive/route.js.map +2 -2
- package/dist/modules/messages/api/[id]/conversation/read/route.js +5 -0
- package/dist/modules/messages/api/[id]/conversation/read/route.js.map +2 -2
- package/dist/modules/messages/api/[id]/conversation/route.js +5 -0
- package/dist/modules/messages/api/[id]/conversation/route.js.map +2 -2
- package/dist/modules/messages/api/[id]/forward/route.js +5 -0
- package/dist/modules/messages/api/[id]/forward/route.js.map +2 -2
- package/dist/modules/messages/api/[id]/reply/route.js +5 -0
- package/dist/modules/messages/api/[id]/reply/route.js.map +2 -2
- package/dist/modules/messages/api/[id]/route.js +9 -0
- package/dist/modules/messages/api/[id]/route.js.map +2 -2
- package/dist/modules/messages/api/token/[token]/route.js +5 -0
- package/dist/modules/messages/api/token/[token]/route.js.map +2 -2
- package/dist/modules/resources/api/resources/tags/assign/route.js +5 -0
- package/dist/modules/resources/api/resources/tags/assign/route.js.map +2 -2
- package/dist/modules/resources/api/resources/tags/unassign/route.js +5 -0
- package/dist/modules/resources/api/resources/tags/unassign/route.js.map +2 -2
- package/dist/modules/sales/api/quotes/accept/route.js +5 -0
- package/dist/modules/sales/api/quotes/accept/route.js.map +2 -2
- package/dist/modules/sales/api/quotes/convert/route.js +5 -0
- package/dist/modules/sales/api/quotes/convert/route.js.map +2 -2
- package/dist/modules/sales/api/settings/document-numbers/route.js +5 -0
- package/dist/modules/sales/api/settings/document-numbers/route.js.map +2 -2
- package/dist/modules/sales/api/settings/order-editing/route.js +5 -0
- package/dist/modules/sales/api/settings/order-editing/route.js.map +2 -2
- package/dist/modules/staff/api/leave-requests/accept/route.js +5 -0
- package/dist/modules/staff/api/leave-requests/accept/route.js.map +2 -2
- package/dist/modules/staff/api/leave-requests/reject/route.js +5 -0
- package/dist/modules/staff/api/leave-requests/reject/route.js.map +2 -2
- package/dist/modules/staff/api/team-members/self/route.js +5 -0
- package/dist/modules/staff/api/team-members/self/route.js.map +2 -2
- package/dist/modules/staff/api/team-members/tags/assign/route.js +5 -0
- package/dist/modules/staff/api/team-members/tags/assign/route.js.map +2 -2
- package/dist/modules/staff/api/team-members/tags/unassign/route.js +5 -0
- package/dist/modules/staff/api/team-members/tags/unassign/route.js.map +2 -2
- package/dist/modules/staff/api/timesheets/time-entries/start-timer/route.js +5 -0
- package/dist/modules/staff/api/timesheets/time-entries/start-timer/route.js.map +2 -2
- package/dist/modules/translations/api/[entityType]/[entityId]/route.js +9 -0
- package/dist/modules/translations/api/[entityType]/[entityId]/route.js.map +2 -2
- package/dist/modules/warranty_claims/api/ai/assess/route.js +5 -0
- package/dist/modules/warranty_claims/api/ai/assess/route.js.map +2 -2
- package/dist/modules/warranty_claims/api/assign/route.js +5 -0
- package/dist/modules/warranty_claims/api/assign/route.js.map +2 -2
- package/dist/modules/warranty_claims/api/credit-memo/route.js +5 -0
- package/dist/modules/warranty_claims/api/credit-memo/route.js.map +2 -2
- package/dist/modules/warranty_claims/api/events/route.js +5 -0
- package/dist/modules/warranty_claims/api/events/route.js.map +2 -2
- package/dist/modules/warranty_claims/api/portal/claims/[id]/submit/route.js +5 -0
- package/dist/modules/warranty_claims/api/portal/claims/[id]/submit/route.js.map +2 -2
- package/dist/modules/warranty_claims/api/portal/claims/[id]/withdraw/route.js +5 -0
- package/dist/modules/warranty_claims/api/portal/claims/[id]/withdraw/route.js.map +2 -2
- package/dist/modules/warranty_claims/api/portal/claims/route.js +5 -0
- package/dist/modules/warranty_claims/api/portal/claims/route.js.map +2 -2
- package/dist/modules/warranty_claims/api/portal/events/route.js +5 -0
- package/dist/modules/warranty_claims/api/portal/events/route.js.map +2 -2
- package/dist/modules/warranty_claims/api/receiving/route.js +5 -0
- package/dist/modules/warranty_claims/api/receiving/route.js.map +2 -2
- package/dist/modules/warranty_claims/api/replacement-order/route.js +5 -0
- package/dist/modules/warranty_claims/api/replacement-order/route.js.map +2 -2
- package/dist/modules/warranty_claims/api/return-label/route.js +5 -0
- package/dist/modules/warranty_claims/api/return-label/route.js.map +2 -2
- package/dist/modules/warranty_claims/api/sales-return/route.js +5 -0
- package/dist/modules/warranty_claims/api/sales-return/route.js.map +2 -2
- package/dist/modules/warranty_claims/api/settings-general/route.js +5 -0
- package/dist/modules/warranty_claims/api/settings-general/route.js.map +2 -2
- package/dist/modules/warranty_claims/api/submit/route.js +5 -0
- package/dist/modules/warranty_claims/api/submit/route.js.map +2 -2
- package/dist/modules/warranty_claims/api/transition/route.js +5 -0
- package/dist/modules/warranty_claims/api/transition/route.js.map +2 -2
- package/dist/modules/warranty_claims/api/vendor-recovery/route.js +5 -0
- package/dist/modules/warranty_claims/api/vendor-recovery/route.js.map +2 -2
- package/dist/modules/wms/api/sales-orders/[salesOrderId]/warehouse-assignment/route.js +5 -0
- package/dist/modules/wms/api/sales-orders/[salesOrderId]/warehouse-assignment/route.js.map +2 -2
- package/package.json +7 -7
- package/src/modules/audit_logs/api/audit-logs/actions/redo/route.ts +5 -0
- package/src/modules/auth/api/profile/route.ts +5 -0
- package/src/modules/customers/api/activities/route.ts +13 -0
- package/src/modules/customers/api/dictionaries/[kind]/[id]/route.ts +9 -0
- package/src/modules/customers/api/dictionaries/[kind]/route.ts +5 -0
- package/src/modules/customers/api/dictionaries/kind-settings/route.ts +5 -0
- package/src/modules/customers/api/interactions/cancel/route.ts +5 -0
- package/src/modules/customers/api/interactions/complete/route.ts +5 -0
- package/src/modules/customers/api/labels/assign/route.ts +5 -0
- package/src/modules/customers/api/labels/route.ts +5 -0
- package/src/modules/customers/api/labels/unassign/route.ts +5 -0
- package/src/modules/customers/api/people/[id]/companies/[linkId]/route.ts +9 -0
- package/src/modules/customers/api/people/[id]/companies/route.ts +5 -0
- package/src/modules/customers/api/pipeline-stages/reorder/route.ts +5 -0
- package/src/modules/customers/api/pipeline-stages/route.ts +13 -0
- package/src/modules/customers/api/pipelines/route.ts +13 -0
- package/src/modules/customers/api/settings/address-format/route.ts +5 -0
- package/src/modules/customers/api/settings/dictionary-sort-modes/route.ts +5 -0
- package/src/modules/customers/api/settings/stuck-threshold/route.ts +5 -0
- package/src/modules/customers/api/tags/assign/route.ts +5 -0
- package/src/modules/customers/api/tags/unassign/route.ts +5 -0
- package/src/modules/customers/api/todos/route.ts +13 -0
- package/src/modules/dictionaries/api/[dictionaryId]/entries/[entryId]/route.ts +9 -0
- package/src/modules/dictionaries/api/[dictionaryId]/entries/reorder/route.ts +5 -0
- package/src/modules/dictionaries/api/[dictionaryId]/entries/route.ts +5 -0
- package/src/modules/dictionaries/api/[dictionaryId]/entries/set-default/route.ts +5 -0
- package/src/modules/directory/api/organization-branding/route.ts +5 -0
- package/src/modules/eudr/api/plots/import/route.ts +7 -0
- package/src/modules/eudr/api/product-mappings/suggestions/apply/route.ts +7 -0
- package/src/modules/feature_toggles/api/overrides/route.ts +5 -0
- package/src/modules/messages/api/[id]/actions/[actionId]/route.ts +5 -0
- package/src/modules/messages/api/[id]/conversation/archive/route.ts +5 -0
- package/src/modules/messages/api/[id]/conversation/read/route.ts +5 -0
- package/src/modules/messages/api/[id]/conversation/route.ts +5 -0
- package/src/modules/messages/api/[id]/forward/route.ts +5 -0
- package/src/modules/messages/api/[id]/reply/route.ts +5 -0
- package/src/modules/messages/api/[id]/route.ts +9 -0
- package/src/modules/messages/api/token/[token]/route.ts +5 -0
- package/src/modules/resources/api/resources/tags/assign/route.ts +5 -0
- package/src/modules/resources/api/resources/tags/unassign/route.ts +5 -0
- package/src/modules/sales/api/quotes/accept/route.ts +5 -0
- package/src/modules/sales/api/quotes/convert/route.ts +5 -0
- package/src/modules/sales/api/settings/document-numbers/route.ts +5 -0
- package/src/modules/sales/api/settings/order-editing/route.ts +5 -0
- package/src/modules/staff/api/leave-requests/accept/route.ts +5 -0
- package/src/modules/staff/api/leave-requests/reject/route.ts +5 -0
- package/src/modules/staff/api/team-members/self/route.ts +5 -0
- package/src/modules/staff/api/team-members/tags/assign/route.ts +5 -0
- package/src/modules/staff/api/team-members/tags/unassign/route.ts +5 -0
- package/src/modules/staff/api/timesheets/time-entries/start-timer/route.ts +5 -0
- package/src/modules/translations/api/[entityType]/[entityId]/route.ts +9 -0
- package/src/modules/warranty_claims/api/ai/assess/route.ts +5 -0
- package/src/modules/warranty_claims/api/assign/route.ts +5 -0
- package/src/modules/warranty_claims/api/credit-memo/route.ts +5 -0
- package/src/modules/warranty_claims/api/events/route.ts +5 -0
- package/src/modules/warranty_claims/api/portal/claims/[id]/submit/route.ts +5 -0
- package/src/modules/warranty_claims/api/portal/claims/[id]/withdraw/route.ts +5 -0
- package/src/modules/warranty_claims/api/portal/claims/route.ts +5 -0
- package/src/modules/warranty_claims/api/portal/events/route.ts +5 -0
- package/src/modules/warranty_claims/api/receiving/route.ts +5 -0
- package/src/modules/warranty_claims/api/replacement-order/route.ts +5 -0
- package/src/modules/warranty_claims/api/return-label/route.ts +5 -0
- package/src/modules/warranty_claims/api/sales-return/route.ts +5 -0
- package/src/modules/warranty_claims/api/settings-general/route.ts +5 -0
- package/src/modules/warranty_claims/api/submit/route.ts +5 -0
- package/src/modules/warranty_claims/api/transition/route.ts +5 -0
- package/src/modules/warranty_claims/api/vendor-recovery/route.ts +5 -0
- package/src/modules/wms/api/sales-orders/[salesOrderId]/warehouse-assignment/route.ts +5 -0
|
@@ -35,6 +35,7 @@ import {
|
|
|
35
35
|
sortDictionaryEntries
|
|
36
36
|
} from "@open-mercato/core/modules/dictionaries/lib/entrySort";
|
|
37
37
|
import { createLogger } from "@open-mercato/shared/lib/logger";
|
|
38
|
+
import { getCommandInterceptorHttpRejection } from "@open-mercato/shared/lib/commands/errors";
|
|
38
39
|
const logger = createLogger("dictionaries").child({ component: "entries-api" });
|
|
39
40
|
const paramsSchema = z.object({ dictionaryId: z.string().uuid() });
|
|
40
41
|
const DICTIONARY_ENTRY_RESOURCE = "dictionaries.entry";
|
|
@@ -243,6 +244,10 @@ async function POST(req, ctx) {
|
|
|
243
244
|
if (isCrudHttpError(err)) {
|
|
244
245
|
return NextResponse.json(err.body, { status: err.status });
|
|
245
246
|
}
|
|
247
|
+
const interceptorRejection = getCommandInterceptorHttpRejection(err);
|
|
248
|
+
if (interceptorRejection) {
|
|
249
|
+
return NextResponse.json(interceptorRejection.body, { status: interceptorRejection.status });
|
|
250
|
+
}
|
|
246
251
|
if (err instanceof z.ZodError) {
|
|
247
252
|
return NextResponse.json({ error: "Invalid input", details: err.issues }, { status: 400 });
|
|
248
253
|
}
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"version": 3,
|
|
3
3
|
"sources": ["../../../../../../src/modules/dictionaries/api/%5BdictionaryId%5D/entries/route.ts"],
|
|
4
|
-
"sourcesContent": ["import { NextResponse } from 'next/server'\nimport { z } from 'zod'\nimport { runWithCacheTenant } from '@open-mercato/cache'\nimport { Dictionary, DictionaryEntry } from '@open-mercato/core/modules/dictionaries/data/entities'\nimport { resolveDictionariesRouteContext, resolveDictionaryActorId } from '@open-mercato/core/modules/dictionaries/api/context'\nimport {\n createDictionaryEntrySchema,\n listDictionaryEntriesQuerySchema,\n DICTIONARY_ENTRIES_MAX_LIMIT,\n} from '@open-mercato/core/modules/dictionaries/data/validators'\nimport { CrudHttpError, isCrudHttpError } from '@open-mercato/shared/lib/crud/errors'\nimport {\n runCrudMutationGuardAfterSuccess,\n validateCrudMutationGuard,\n} from '@open-mercato/shared/lib/crud/mutation-guard'\nimport type { CommandBus } from '@open-mercato/shared/lib/commands'\nimport { serializeOperationMetadata } from '@open-mercato/shared/lib/commands/operationMetadata'\nimport type { OpenApiMethodDoc, OpenApiRouteDoc } from '@open-mercato/shared/lib/openapi'\nimport {\n createDictionaryEntrySchema as createEntryDocSchema,\n dictionaryEntriesQuerySchema,\n dictionaryEntryListResponseSchema,\n dictionaryEntryResponseSchema,\n dictionaryIdParamsSchema,\n dictionariesErrorSchema,\n dictionariesTag,\n} from '../../openapi'\nimport { findOneWithDecryption, findWithDecryption } from '@open-mercato/shared/lib/encryption/find'\nimport {\n buildCollectionTags,\n buildRecordTag,\n isCrudCacheEnabled,\n resolveCrudCache,\n} from '@open-mercato/shared/lib/crud/cache'\nimport {\n resolveDictionaryEntrySortMode,\n sortDictionaryEntries,\n} from '@open-mercato/core/modules/dictionaries/lib/entrySort'\nimport { createLogger } from '@open-mercato/shared/lib/logger'\n\nconst logger = createLogger('dictionaries').child({ component: 'entries-api' })\n\nconst paramsSchema = z.object({ dictionaryId: z.string().uuid() })\n\nconst DICTIONARY_ENTRY_RESOURCE = 'dictionaries.entry'\nconst DICTIONARY_DEFINITION_RESOURCE = 'dictionaries.dictionary'\nconst DICTIONARY_ENTRIES_TTL_MS = 5 * 60_000\n\nfunction buildEntriesCacheKey(params: {\n dictionaryId: string\n organizationId: string | null\n sortMode: string\n limit: number\n offset: number\n}): string {\n return `dictionaries:entries:${params.dictionaryId}:org=${params.organizationId ?? 'null'}:sort=${params.sortMode}:limit=${params.limit}:offset=${params.offset}`\n}\n\nasync function loadDictionary(\n context: Awaited<ReturnType<typeof resolveDictionariesRouteContext>>,\n id: string,\n options: { allowInherited?: boolean } = {},\n) {\n const { allowInherited = false } = options\n if (!allowInherited && !context.organizationId) {\n throw new CrudHttpError(400, { error: context.translate('dictionaries.errors.organization_required', 'Organization context is required') })\n }\n const baseFilter = {\n id,\n tenantId: context.tenantId,\n deletedAt: null,\n }\n const filter = allowInherited\n ? {\n ...baseFilter,\n ...(context.readableOrganizationIds.length\n ? { organizationId: { $in: context.readableOrganizationIds } }\n : {}),\n }\n : {\n ...baseFilter,\n organizationId: context.organizationId,\n }\n const dictionary = await context.em.findOne(Dictionary, filter)\n if (!dictionary) {\n throw new CrudHttpError(404, { error: context.translate('dictionaries.errors.not_found', 'Dictionary not found') })\n }\n return dictionary\n}\n\nexport const metadata = {\n GET: { requireAuth: true, requireFeatures: ['dictionaries.view'] },\n POST: { requireAuth: true, requireFeatures: ['dictionaries.manage'] },\n}\n\nexport async function GET(req: Request, ctx: { params?: { dictionaryId?: string } }) {\n try {\n const context = await resolveDictionariesRouteContext(req)\n if (!context.auth) {\n return NextResponse.json({ error: 'Unauthorized' }, { status: 401 })\n }\n const { dictionaryId } = paramsSchema.parse({ dictionaryId: ctx.params?.dictionaryId })\n const requestUrl = new URL(req.url)\n const { limit, offset } = listDictionaryEntriesQuerySchema.parse({\n limit: requestUrl.searchParams.get('limit') ?? undefined,\n offset: requestUrl.searchParams.get('offset') ?? undefined,\n })\n const dictionary = await loadDictionary(context, dictionaryId, { allowInherited: true })\n const dictionaryTenantId = dictionary.tenantId\n const dictionaryOrgId = dictionary.organizationId ?? null\n const sortMode = resolveDictionaryEntrySortMode(dictionary.entrySortMode)\n\n // Dictionary CF selects hit this on every CrudForm open, so cache the\n // decrypted+sorted options payload. The entry writes flow through the\n // command bus (resourceKind dictionaries.entry), which already flushes the\n // matching collection tag post-commit \u2014 no new invalidation wiring needed.\n // Only the default page is cached: keying the cache on caller-supplied\n // pagination would let any reader mint unbounded distinct cache entries.\n const isDefaultPage = limit === DICTIONARY_ENTRIES_MAX_LIMIT && offset === 0\n const cache = isCrudCacheEnabled() && isDefaultPage ? resolveCrudCache(context.container) : null\n const cacheKey = cache\n ? buildEntriesCacheKey({ dictionaryId, organizationId: dictionaryOrgId, sortMode, limit, offset })\n : null\n\n if (cache && cacheKey) {\n const cached = await runWithCacheTenant(dictionaryTenantId, () => cache.get(cacheKey))\n if (cached) {\n return NextResponse.json(cached)\n }\n }\n\n const entriesFilter = {\n dictionary,\n organizationId: dictionary.organizationId,\n tenantId: dictionary.tenantId,\n }\n const total = await context.em.count(DictionaryEntry, entriesFilter)\n // Bound the decrypt+sort work per request: page membership is decided by a\n // stable database ordering, and the dictionary's sort mode orders the page.\n const entries = await findWithDecryption(\n context.em,\n DictionaryEntry,\n entriesFilter,\n { orderBy: { position: 'asc', id: 'asc' }, limit, offset },\n { tenantId: dictionary.tenantId, organizationId: dictionary.organizationId },\n )\n const sortedEntries = sortDictionaryEntries(entries, sortMode)\n\n const payload = {\n items: sortedEntries.map((entry) => ({\n id: entry.id,\n value: entry.value,\n label: entry.label,\n color: entry.color,\n icon: entry.icon,\n position: entry.position ?? 0,\n isDefault: entry.isDefault ?? false,\n createdAt: entry.createdAt,\n updatedAt: entry.updatedAt,\n })),\n total,\n limit,\n offset,\n hasMore: offset + sortedEntries.length < total,\n // Page membership comes from the database ordering, while the page itself\n // is ordered in memory. A client assembling several pages has to re-apply\n // the same comparator, so it needs the mode that produced this ordering.\n sortMode,\n }\n\n if (cache && cacheKey) {\n try {\n await runWithCacheTenant(dictionaryTenantId, () =>\n cache.set(cacheKey, payload, {\n ttl: DICTIONARY_ENTRIES_TTL_MS,\n tags: [\n ...buildCollectionTags(DICTIONARY_ENTRY_RESOURCE, dictionaryTenantId, [dictionaryOrgId]),\n buildRecordTag(DICTIONARY_DEFINITION_RESOURCE, dictionaryTenantId, dictionaryId),\n ],\n }),\n )\n } catch {}\n }\n\n return NextResponse.json(payload)\n } catch (err) {\n if (isCrudHttpError(err)) {\n return NextResponse.json(err.body, { status: err.status })\n }\n if (err instanceof z.ZodError) {\n return NextResponse.json({ error: 'Invalid input', details: err.issues }, { status: 400 })\n }\n logger.error('Failed to load dictionary entries', { err })\n return NextResponse.json({ error: 'Failed to load dictionary entries' }, { status: 500 })\n }\n}\n\nexport async function POST(req: Request, ctx: { params?: { dictionaryId?: string } }) {\n try {\n const context = await resolveDictionariesRouteContext(req)\n if (!context.auth) {\n return NextResponse.json({ error: 'Unauthorized' }, { status: 401 })\n }\n const { dictionaryId } = paramsSchema.parse({ dictionaryId: ctx.params?.dictionaryId })\n const payload = createDictionaryEntrySchema.parse(await req.json().catch(() => ({})))\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: DICTIONARY_ENTRY_RESOURCE,\n resourceId: dictionaryId,\n operation: 'create',\n requestMethod: req.method,\n requestHeaders: req.headers,\n mutationPayload: payload,\n })\n if (guardResult && !guardResult.ok) {\n return NextResponse.json(guardResult.body, { status: guardResult.status })\n }\n // These nested routes do not use makeCrudRoute, so we invoke the command bus directly.\n const commandBus = (context.container.resolve('commandBus') as CommandBus)\n const { result, logEntry } = await commandBus.execute('dictionaries.entries.create', {\n input: { ...payload, dictionaryId },\n ctx: context.ctx,\n })\n const createResult = (result ?? {}) as { entryId?: string | null }\n const createdEntryId = typeof createResult.entryId === 'string' ? createResult.entryId : null\n if (!createdEntryId) {\n throw new CrudHttpError(500, { error: context.translate('dictionaries.errors.entry_create_failed', 'Failed to create dictionary entry') })\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: DICTIONARY_ENTRY_RESOURCE,\n resourceId: createdEntryId,\n operation: 'create',\n requestMethod: req.method,\n requestHeaders: req.headers,\n metadata: guardResult.metadata ?? null,\n })\n }\n const entry = await findOneWithDecryption(\n context.em.fork(),\n DictionaryEntry,\n createdEntryId,\n { populate: ['dictionary'] },\n { tenantId: context.auth.tenantId ?? null, organizationId: context.auth.orgId ?? null },\n )\n if (!entry) {\n throw new CrudHttpError(500, { error: context.translate('dictionaries.errors.entry_create_failed', 'Failed to create dictionary entry') })\n }\n const response = NextResponse.json({\n id: entry.id,\n value: entry.value,\n label: entry.label,\n color: entry.color,\n icon: entry.icon,\n position: entry.position ?? 0,\n isDefault: entry.isDefault ?? false,\n createdAt: entry.createdAt,\n updatedAt: entry.updatedAt,\n }, { status: 201 })\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 ?? 'dictionaries.entry',\n resourceId: createdEntryId,\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 if (err instanceof z.ZodError) {\n return NextResponse.json({ error: 'Invalid input', details: err.issues }, { status: 400 })\n }\n logger.error('Failed to create dictionary entry', { err })\n return NextResponse.json({ error: 'Failed to create dictionary entry' }, { status: 500 })\n }\n}\n\nconst dictionaryEntriesGetDoc: OpenApiMethodDoc = {\n summary: 'List dictionary entries',\n description: `Returns entries for the specified dictionary ordered by its configured entry sort mode. The number of entries returned per request is capped at ${DICTIONARY_ENTRIES_MAX_LIMIT}; use \\`limit\\` and \\`offset\\` with the \\`total\\`/\\`hasMore\\` response fields to page through larger dictionaries.`,\n tags: [dictionariesTag],\n query: dictionaryEntriesQuerySchema,\n responses: [\n { status: 200, description: 'Dictionary entries.', schema: dictionaryEntryListResponseSchema },\n ],\n errors: [\n { status: 400, description: 'Invalid parameters', schema: dictionariesErrorSchema },\n { status: 401, description: 'Authentication required', schema: dictionariesErrorSchema },\n { status: 404, description: 'Dictionary not found', schema: dictionariesErrorSchema },\n { status: 500, description: 'Failed to load dictionary entries', schema: dictionariesErrorSchema },\n ],\n}\n\nconst dictionaryEntriesPostDoc: OpenApiMethodDoc = {\n summary: 'Create dictionary entry',\n description: 'Creates a new entry in the specified dictionary.',\n tags: [dictionariesTag],\n requestBody: {\n contentType: 'application/json',\n schema: createEntryDocSchema,\n description: 'Entry value, label, and optional presentation metadata.',\n },\n responses: [\n { status: 201, description: 'Dictionary entry created.', schema: dictionaryEntryResponseSchema },\n ],\n errors: [\n { status: 400, description: 'Validation failed', schema: dictionariesErrorSchema },\n { status: 401, description: 'Authentication required', schema: dictionariesErrorSchema },\n { status: 404, description: 'Dictionary not found', schema: dictionariesErrorSchema },\n { status: 500, description: 'Failed to create dictionary entry', schema: dictionariesErrorSchema },\n ],\n}\n\nexport const openApi: OpenApiRouteDoc = {\n tag: dictionariesTag,\n summary: 'Dictionary entries collection',\n pathParams: dictionaryIdParamsSchema,\n methods: {\n GET: dictionaryEntriesGetDoc,\n POST: dictionaryEntriesPostDoc,\n },\n}\n"],
|
|
5
|
-
"mappings": "AAAA,SAAS,oBAAoB;AAC7B,SAAS,SAAS;AAClB,SAAS,0BAA0B;AACnC,SAAS,YAAY,uBAAuB;AAC5C,SAAS,iCAAiC,gCAAgC;AAC1E;AAAA,EACE;AAAA,EACA;AAAA,EACA;AAAA,OACK;AACP,SAAS,eAAe,uBAAuB;AAC/C;AAAA,EACE;AAAA,EACA;AAAA,OACK;AAEP,SAAS,kCAAkC;AAE3C;AAAA,EACE,+BAA+B;AAAA,EAC/B;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,OACK;AACP,SAAS,uBAAuB,0BAA0B;AAC1D;AAAA,EACE;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,OACK;AACP;AAAA,EACE;AAAA,EACA;AAAA,OACK;AACP,SAAS,oBAAoB;
|
|
4
|
+
"sourcesContent": ["import { NextResponse } from 'next/server'\nimport { z } from 'zod'\nimport { runWithCacheTenant } from '@open-mercato/cache'\nimport { Dictionary, DictionaryEntry } from '@open-mercato/core/modules/dictionaries/data/entities'\nimport { resolveDictionariesRouteContext, resolveDictionaryActorId } from '@open-mercato/core/modules/dictionaries/api/context'\nimport {\n createDictionaryEntrySchema,\n listDictionaryEntriesQuerySchema,\n DICTIONARY_ENTRIES_MAX_LIMIT,\n} from '@open-mercato/core/modules/dictionaries/data/validators'\nimport { CrudHttpError, isCrudHttpError } from '@open-mercato/shared/lib/crud/errors'\nimport {\n runCrudMutationGuardAfterSuccess,\n validateCrudMutationGuard,\n} from '@open-mercato/shared/lib/crud/mutation-guard'\nimport type { CommandBus } from '@open-mercato/shared/lib/commands'\nimport { serializeOperationMetadata } from '@open-mercato/shared/lib/commands/operationMetadata'\nimport type { OpenApiMethodDoc, OpenApiRouteDoc } from '@open-mercato/shared/lib/openapi'\nimport {\n createDictionaryEntrySchema as createEntryDocSchema,\n dictionaryEntriesQuerySchema,\n dictionaryEntryListResponseSchema,\n dictionaryEntryResponseSchema,\n dictionaryIdParamsSchema,\n dictionariesErrorSchema,\n dictionariesTag,\n} from '../../openapi'\nimport { findOneWithDecryption, findWithDecryption } from '@open-mercato/shared/lib/encryption/find'\nimport {\n buildCollectionTags,\n buildRecordTag,\n isCrudCacheEnabled,\n resolveCrudCache,\n} from '@open-mercato/shared/lib/crud/cache'\nimport {\n resolveDictionaryEntrySortMode,\n sortDictionaryEntries,\n} from '@open-mercato/core/modules/dictionaries/lib/entrySort'\nimport { createLogger } from '@open-mercato/shared/lib/logger'\nimport { getCommandInterceptorHttpRejection } from '@open-mercato/shared/lib/commands/errors'\n\nconst logger = createLogger('dictionaries').child({ component: 'entries-api' })\n\nconst paramsSchema = z.object({ dictionaryId: z.string().uuid() })\n\nconst DICTIONARY_ENTRY_RESOURCE = 'dictionaries.entry'\nconst DICTIONARY_DEFINITION_RESOURCE = 'dictionaries.dictionary'\nconst DICTIONARY_ENTRIES_TTL_MS = 5 * 60_000\n\nfunction buildEntriesCacheKey(params: {\n dictionaryId: string\n organizationId: string | null\n sortMode: string\n limit: number\n offset: number\n}): string {\n return `dictionaries:entries:${params.dictionaryId}:org=${params.organizationId ?? 'null'}:sort=${params.sortMode}:limit=${params.limit}:offset=${params.offset}`\n}\n\nasync function loadDictionary(\n context: Awaited<ReturnType<typeof resolveDictionariesRouteContext>>,\n id: string,\n options: { allowInherited?: boolean } = {},\n) {\n const { allowInherited = false } = options\n if (!allowInherited && !context.organizationId) {\n throw new CrudHttpError(400, { error: context.translate('dictionaries.errors.organization_required', 'Organization context is required') })\n }\n const baseFilter = {\n id,\n tenantId: context.tenantId,\n deletedAt: null,\n }\n const filter = allowInherited\n ? {\n ...baseFilter,\n ...(context.readableOrganizationIds.length\n ? { organizationId: { $in: context.readableOrganizationIds } }\n : {}),\n }\n : {\n ...baseFilter,\n organizationId: context.organizationId,\n }\n const dictionary = await context.em.findOne(Dictionary, filter)\n if (!dictionary) {\n throw new CrudHttpError(404, { error: context.translate('dictionaries.errors.not_found', 'Dictionary not found') })\n }\n return dictionary\n}\n\nexport const metadata = {\n GET: { requireAuth: true, requireFeatures: ['dictionaries.view'] },\n POST: { requireAuth: true, requireFeatures: ['dictionaries.manage'] },\n}\n\nexport async function GET(req: Request, ctx: { params?: { dictionaryId?: string } }) {\n try {\n const context = await resolveDictionariesRouteContext(req)\n if (!context.auth) {\n return NextResponse.json({ error: 'Unauthorized' }, { status: 401 })\n }\n const { dictionaryId } = paramsSchema.parse({ dictionaryId: ctx.params?.dictionaryId })\n const requestUrl = new URL(req.url)\n const { limit, offset } = listDictionaryEntriesQuerySchema.parse({\n limit: requestUrl.searchParams.get('limit') ?? undefined,\n offset: requestUrl.searchParams.get('offset') ?? undefined,\n })\n const dictionary = await loadDictionary(context, dictionaryId, { allowInherited: true })\n const dictionaryTenantId = dictionary.tenantId\n const dictionaryOrgId = dictionary.organizationId ?? null\n const sortMode = resolveDictionaryEntrySortMode(dictionary.entrySortMode)\n\n // Dictionary CF selects hit this on every CrudForm open, so cache the\n // decrypted+sorted options payload. The entry writes flow through the\n // command bus (resourceKind dictionaries.entry), which already flushes the\n // matching collection tag post-commit \u2014 no new invalidation wiring needed.\n // Only the default page is cached: keying the cache on caller-supplied\n // pagination would let any reader mint unbounded distinct cache entries.\n const isDefaultPage = limit === DICTIONARY_ENTRIES_MAX_LIMIT && offset === 0\n const cache = isCrudCacheEnabled() && isDefaultPage ? resolveCrudCache(context.container) : null\n const cacheKey = cache\n ? buildEntriesCacheKey({ dictionaryId, organizationId: dictionaryOrgId, sortMode, limit, offset })\n : null\n\n if (cache && cacheKey) {\n const cached = await runWithCacheTenant(dictionaryTenantId, () => cache.get(cacheKey))\n if (cached) {\n return NextResponse.json(cached)\n }\n }\n\n const entriesFilter = {\n dictionary,\n organizationId: dictionary.organizationId,\n tenantId: dictionary.tenantId,\n }\n const total = await context.em.count(DictionaryEntry, entriesFilter)\n // Bound the decrypt+sort work per request: page membership is decided by a\n // stable database ordering, and the dictionary's sort mode orders the page.\n const entries = await findWithDecryption(\n context.em,\n DictionaryEntry,\n entriesFilter,\n { orderBy: { position: 'asc', id: 'asc' }, limit, offset },\n { tenantId: dictionary.tenantId, organizationId: dictionary.organizationId },\n )\n const sortedEntries = sortDictionaryEntries(entries, sortMode)\n\n const payload = {\n items: sortedEntries.map((entry) => ({\n id: entry.id,\n value: entry.value,\n label: entry.label,\n color: entry.color,\n icon: entry.icon,\n position: entry.position ?? 0,\n isDefault: entry.isDefault ?? false,\n createdAt: entry.createdAt,\n updatedAt: entry.updatedAt,\n })),\n total,\n limit,\n offset,\n hasMore: offset + sortedEntries.length < total,\n // Page membership comes from the database ordering, while the page itself\n // is ordered in memory. A client assembling several pages has to re-apply\n // the same comparator, so it needs the mode that produced this ordering.\n sortMode,\n }\n\n if (cache && cacheKey) {\n try {\n await runWithCacheTenant(dictionaryTenantId, () =>\n cache.set(cacheKey, payload, {\n ttl: DICTIONARY_ENTRIES_TTL_MS,\n tags: [\n ...buildCollectionTags(DICTIONARY_ENTRY_RESOURCE, dictionaryTenantId, [dictionaryOrgId]),\n buildRecordTag(DICTIONARY_DEFINITION_RESOURCE, dictionaryTenantId, dictionaryId),\n ],\n }),\n )\n } catch {}\n }\n\n return NextResponse.json(payload)\n } catch (err) {\n if (isCrudHttpError(err)) {\n return NextResponse.json(err.body, { status: err.status })\n }\n if (err instanceof z.ZodError) {\n return NextResponse.json({ error: 'Invalid input', details: err.issues }, { status: 400 })\n }\n logger.error('Failed to load dictionary entries', { err })\n return NextResponse.json({ error: 'Failed to load dictionary entries' }, { status: 500 })\n }\n}\n\nexport async function POST(req: Request, ctx: { params?: { dictionaryId?: string } }) {\n try {\n const context = await resolveDictionariesRouteContext(req)\n if (!context.auth) {\n return NextResponse.json({ error: 'Unauthorized' }, { status: 401 })\n }\n const { dictionaryId } = paramsSchema.parse({ dictionaryId: ctx.params?.dictionaryId })\n const payload = createDictionaryEntrySchema.parse(await req.json().catch(() => ({})))\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: DICTIONARY_ENTRY_RESOURCE,\n resourceId: dictionaryId,\n operation: 'create',\n requestMethod: req.method,\n requestHeaders: req.headers,\n mutationPayload: payload,\n })\n if (guardResult && !guardResult.ok) {\n return NextResponse.json(guardResult.body, { status: guardResult.status })\n }\n // These nested routes do not use makeCrudRoute, so we invoke the command bus directly.\n const commandBus = (context.container.resolve('commandBus') as CommandBus)\n const { result, logEntry } = await commandBus.execute('dictionaries.entries.create', {\n input: { ...payload, dictionaryId },\n ctx: context.ctx,\n })\n const createResult = (result ?? {}) as { entryId?: string | null }\n const createdEntryId = typeof createResult.entryId === 'string' ? createResult.entryId : null\n if (!createdEntryId) {\n throw new CrudHttpError(500, { error: context.translate('dictionaries.errors.entry_create_failed', 'Failed to create dictionary entry') })\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: DICTIONARY_ENTRY_RESOURCE,\n resourceId: createdEntryId,\n operation: 'create',\n requestMethod: req.method,\n requestHeaders: req.headers,\n metadata: guardResult.metadata ?? null,\n })\n }\n const entry = await findOneWithDecryption(\n context.em.fork(),\n DictionaryEntry,\n createdEntryId,\n { populate: ['dictionary'] },\n { tenantId: context.auth.tenantId ?? null, organizationId: context.auth.orgId ?? null },\n )\n if (!entry) {\n throw new CrudHttpError(500, { error: context.translate('dictionaries.errors.entry_create_failed', 'Failed to create dictionary entry') })\n }\n const response = NextResponse.json({\n id: entry.id,\n value: entry.value,\n label: entry.label,\n color: entry.color,\n icon: entry.icon,\n position: entry.position ?? 0,\n isDefault: entry.isDefault ?? false,\n createdAt: entry.createdAt,\n updatedAt: entry.updatedAt,\n }, { status: 201 })\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 ?? 'dictionaries.entry',\n resourceId: createdEntryId,\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 interceptorRejection = getCommandInterceptorHttpRejection(err)\n if (interceptorRejection) {\n return NextResponse.json(interceptorRejection.body, { status: interceptorRejection.status })\n }\n if (err instanceof z.ZodError) {\n return NextResponse.json({ error: 'Invalid input', details: err.issues }, { status: 400 })\n }\n logger.error('Failed to create dictionary entry', { err })\n return NextResponse.json({ error: 'Failed to create dictionary entry' }, { status: 500 })\n }\n}\n\nconst dictionaryEntriesGetDoc: OpenApiMethodDoc = {\n summary: 'List dictionary entries',\n description: `Returns entries for the specified dictionary ordered by its configured entry sort mode. The number of entries returned per request is capped at ${DICTIONARY_ENTRIES_MAX_LIMIT}; use \\`limit\\` and \\`offset\\` with the \\`total\\`/\\`hasMore\\` response fields to page through larger dictionaries.`,\n tags: [dictionariesTag],\n query: dictionaryEntriesQuerySchema,\n responses: [\n { status: 200, description: 'Dictionary entries.', schema: dictionaryEntryListResponseSchema },\n ],\n errors: [\n { status: 400, description: 'Invalid parameters', schema: dictionariesErrorSchema },\n { status: 401, description: 'Authentication required', schema: dictionariesErrorSchema },\n { status: 404, description: 'Dictionary not found', schema: dictionariesErrorSchema },\n { status: 500, description: 'Failed to load dictionary entries', schema: dictionariesErrorSchema },\n ],\n}\n\nconst dictionaryEntriesPostDoc: OpenApiMethodDoc = {\n summary: 'Create dictionary entry',\n description: 'Creates a new entry in the specified dictionary.',\n tags: [dictionariesTag],\n requestBody: {\n contentType: 'application/json',\n schema: createEntryDocSchema,\n description: 'Entry value, label, and optional presentation metadata.',\n },\n responses: [\n { status: 201, description: 'Dictionary entry created.', schema: dictionaryEntryResponseSchema },\n ],\n errors: [\n { status: 400, description: 'Validation failed', schema: dictionariesErrorSchema },\n { status: 401, description: 'Authentication required', schema: dictionariesErrorSchema },\n { status: 404, description: 'Dictionary not found', schema: dictionariesErrorSchema },\n { status: 500, description: 'Failed to create dictionary entry', schema: dictionariesErrorSchema },\n ],\n}\n\nexport const openApi: OpenApiRouteDoc = {\n tag: dictionariesTag,\n summary: 'Dictionary entries collection',\n pathParams: dictionaryIdParamsSchema,\n methods: {\n GET: dictionaryEntriesGetDoc,\n POST: dictionaryEntriesPostDoc,\n },\n}\n"],
|
|
5
|
+
"mappings": "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",
|
|
6
6
|
"names": []
|
|
7
7
|
}
|
|
@@ -23,6 +23,7 @@ import {
|
|
|
23
23
|
} from "../../../openapi.js";
|
|
24
24
|
import { resolveDictionaryActorId } from "../../../context.js";
|
|
25
25
|
import { createLogger } from "@open-mercato/shared/lib/logger";
|
|
26
|
+
import { getCommandInterceptorHttpRejection } from "@open-mercato/shared/lib/commands/errors";
|
|
26
27
|
const logger = createLogger("dictionaries").child({ component: "entries-api" });
|
|
27
28
|
const paramsSchema = z.object({ dictionaryId: z.string().uuid() });
|
|
28
29
|
const metadata = {
|
|
@@ -117,6 +118,10 @@ async function POST(req, ctx) {
|
|
|
117
118
|
if (isCrudHttpError(err)) {
|
|
118
119
|
return NextResponse.json(err.body, { status: err.status });
|
|
119
120
|
}
|
|
121
|
+
const interceptorRejection = getCommandInterceptorHttpRejection(err);
|
|
122
|
+
if (interceptorRejection) {
|
|
123
|
+
return NextResponse.json(interceptorRejection.body, { status: interceptorRejection.status });
|
|
124
|
+
}
|
|
120
125
|
if (err instanceof z.ZodError) {
|
|
121
126
|
return NextResponse.json({ error: "Invalid input", details: err.issues }, { status: 400 });
|
|
122
127
|
}
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"version": 3,
|
|
3
3
|
"sources": ["../../../../../../../src/modules/dictionaries/api/%5BdictionaryId%5D/entries/set-default/route.ts"],
|
|
4
|
-
"sourcesContent": ["import { NextResponse } from 'next/server'\nimport { z } from 'zod'\nimport { Dictionary } from '@open-mercato/core/modules/dictionaries/data/entities'\nimport { resolveDictionariesRouteContext } from '@open-mercato/core/modules/dictionaries/api/context'\nimport {\n setDefaultDictionaryEntryCommandSchema,\n setDefaultDictionaryEntrySchema,\n type SetDefaultDictionaryEntryCommandInput,\n} from '@open-mercato/core/modules/dictionaries/data/validators'\nimport { CrudHttpError, isCrudHttpError } from '@open-mercato/shared/lib/crud/errors'\nimport type { CommandBus } from '@open-mercato/shared/lib/commands'\nimport type { OpenApiMethodDoc, OpenApiRouteDoc } from '@open-mercato/shared/lib/openapi'\nimport { serializeOperationMetadata } from '@open-mercato/shared/lib/commands/operationMetadata'\nimport { findOneWithDecryption } from '@open-mercato/shared/lib/encryption/find'\nimport { readJsonSafe } from '@open-mercato/shared/lib/http/readJsonSafe'\nimport {\n runCrudMutationGuardAfterSuccess,\n validateCrudMutationGuard,\n} from '@open-mercato/shared/lib/crud/mutation-guard'\nimport {\n dictionaryIdParamsSchema,\n dictionariesErrorSchema,\n dictionariesOkSchema,\n dictionariesTag,\n setDefaultEntryRequestSchema,\n} from '../../../openapi'\nimport { resolveDictionaryActorId } from '../../../context'\nimport { createLogger } from '@open-mercato/shared/lib/logger'\n\nconst logger = createLogger('dictionaries').child({ component: 'entries-api' })\n\nconst paramsSchema = z.object({ dictionaryId: z.string().uuid() })\n\nexport const metadata = {\n POST: { requireAuth: true, requireFeatures: ['dictionaries.manage'] },\n}\n\nexport async function POST(req: Request, ctx: { params?: { dictionaryId?: string } }) {\n try {\n const context = await resolveDictionariesRouteContext(req)\n if (!context.auth) {\n return NextResponse.json({ error: 'Unauthorized' }, { status: 401 })\n }\n const { dictionaryId } = paramsSchema.parse({ dictionaryId: ctx.params?.dictionaryId })\n\n if (!context.organizationId) {\n throw new CrudHttpError(400, { error: context.translate('dictionaries.errors.organization_required', 'Organization context is required') })\n }\n const dictionaryEm = context.em.fork()\n const dictionary = await findOneWithDecryption(\n dictionaryEm,\n Dictionary,\n {\n id: dictionaryId,\n organizationId: context.organizationId,\n tenantId: context.tenantId,\n deletedAt: null,\n },\n undefined,\n { tenantId: context.tenantId, organizationId: context.organizationId },\n )\n if (!dictionary) {\n throw new CrudHttpError(404, { error: context.translate('dictionaries.errors.not_found', 'Dictionary not found') })\n }\n\n const payload = setDefaultDictionaryEntrySchema.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: 'dictionaries.dictionary',\n resourceId: dictionaryId,\n operation: 'custom',\n requestMethod: req.method,\n requestHeaders: req.headers,\n mutationPayload: payload,\n })\n if (guardResult && !guardResult.ok) {\n return NextResponse.json(guardResult.body, { status: guardResult.status })\n }\n\n const commandInput = setDefaultDictionaryEntryCommandSchema.parse({\n dictionaryId,\n tenantId: context.tenantId,\n organizationId: context.organizationId,\n entryId: payload.entryId,\n } satisfies SetDefaultDictionaryEntryCommandInput)\n\n const commandBus = context.container.resolve('commandBus') as CommandBus\n const { logEntry } = await commandBus.execute<SetDefaultDictionaryEntryCommandInput, { dictionaryId: string; entryId: string; clearedIds: string[] }>(\n 'dictionaries.entries.set_default',\n {\n input: commandInput,\n ctx: context.ctx,\n },\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: 'dictionaries.dictionary',\n resourceId: dictionaryId,\n operation: 'custom',\n requestMethod: req.method,\n requestHeaders: req.headers,\n metadata: guardResult.metadata ?? null,\n })\n }\n\n const response = NextResponse.json({ ok: true })\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 ?? 'dictionaries.dictionary',\n resourceId: logEntry.resourceId ?? dictionaryId,\n executedAt: logEntry.createdAt instanceof Date ? logEntry.createdAt.toISOString() : new Date().toISOString(),\n }),\n )\n }\n return response\n } catch (err) {\n if (isCrudHttpError(err)) {\n return NextResponse.json(err.body, { status: err.status })\n }\n if (err instanceof z.ZodError) {\n return NextResponse.json({ error: 'Invalid input', details: err.issues }, { status: 400 })\n }\n logger.error('Failed to set default entry', { err })\n return NextResponse.json({ error: 'Failed to set default entry' }, { status: 500 })\n }\n}\n\nconst setDefaultPostDoc: OpenApiMethodDoc = {\n summary: 'Set default dictionary entry',\n description: 'Marks the specified entry as the default for this dictionary, clearing any previous default.',\n tags: [dictionariesTag],\n requestBody: {\n contentType: 'application/json',\n schema: setDefaultEntryRequestSchema,\n description: 'ID of the entry to set as default.',\n },\n responses: [\n { status: 200, description: 'Default entry set.', schema: dictionariesOkSchema },\n ],\n errors: [\n { status: 400, description: 'Validation failed', schema: dictionariesErrorSchema },\n { status: 401, description: 'Authentication required', schema: dictionariesErrorSchema },\n { status: 404, description: 'Dictionary or entry not found', schema: dictionariesErrorSchema },\n { status: 500, description: 'Failed to set default entry', schema: dictionariesErrorSchema },\n ],\n}\n\nexport const openApi: OpenApiRouteDoc = {\n tag: dictionariesTag,\n summary: 'Set default dictionary entry',\n pathParams: dictionaryIdParamsSchema,\n methods: {\n POST: setDefaultPostDoc,\n },\n}\n"],
|
|
5
|
-
"mappings": "AAAA,SAAS,oBAAoB;AAC7B,SAAS,SAAS;AAClB,SAAS,kBAAkB;AAC3B,SAAS,uCAAuC;AAChD;AAAA,EACE;AAAA,EACA;AAAA,OAEK;AACP,SAAS,eAAe,uBAAuB;AAG/C,SAAS,kCAAkC;AAC3C,SAAS,6BAA6B;AACtC,SAAS,oBAAoB;AAC7B;AAAA,EACE;AAAA,EACA;AAAA,OACK;AACP;AAAA,EACE;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,OACK;AACP,SAAS,gCAAgC;AACzC,SAAS,oBAAoB;
|
|
4
|
+
"sourcesContent": ["import { NextResponse } from 'next/server'\nimport { z } from 'zod'\nimport { Dictionary } from '@open-mercato/core/modules/dictionaries/data/entities'\nimport { resolveDictionariesRouteContext } from '@open-mercato/core/modules/dictionaries/api/context'\nimport {\n setDefaultDictionaryEntryCommandSchema,\n setDefaultDictionaryEntrySchema,\n type SetDefaultDictionaryEntryCommandInput,\n} from '@open-mercato/core/modules/dictionaries/data/validators'\nimport { CrudHttpError, isCrudHttpError } from '@open-mercato/shared/lib/crud/errors'\nimport type { CommandBus } from '@open-mercato/shared/lib/commands'\nimport type { OpenApiMethodDoc, OpenApiRouteDoc } from '@open-mercato/shared/lib/openapi'\nimport { serializeOperationMetadata } from '@open-mercato/shared/lib/commands/operationMetadata'\nimport { findOneWithDecryption } from '@open-mercato/shared/lib/encryption/find'\nimport { readJsonSafe } from '@open-mercato/shared/lib/http/readJsonSafe'\nimport {\n runCrudMutationGuardAfterSuccess,\n validateCrudMutationGuard,\n} from '@open-mercato/shared/lib/crud/mutation-guard'\nimport {\n dictionaryIdParamsSchema,\n dictionariesErrorSchema,\n dictionariesOkSchema,\n dictionariesTag,\n setDefaultEntryRequestSchema,\n} from '../../../openapi'\nimport { resolveDictionaryActorId } from '../../../context'\nimport { createLogger } from '@open-mercato/shared/lib/logger'\nimport { getCommandInterceptorHttpRejection } from '@open-mercato/shared/lib/commands/errors'\n\nconst logger = createLogger('dictionaries').child({ component: 'entries-api' })\n\nconst paramsSchema = z.object({ dictionaryId: z.string().uuid() })\n\nexport const metadata = {\n POST: { requireAuth: true, requireFeatures: ['dictionaries.manage'] },\n}\n\nexport async function POST(req: Request, ctx: { params?: { dictionaryId?: string } }) {\n try {\n const context = await resolveDictionariesRouteContext(req)\n if (!context.auth) {\n return NextResponse.json({ error: 'Unauthorized' }, { status: 401 })\n }\n const { dictionaryId } = paramsSchema.parse({ dictionaryId: ctx.params?.dictionaryId })\n\n if (!context.organizationId) {\n throw new CrudHttpError(400, { error: context.translate('dictionaries.errors.organization_required', 'Organization context is required') })\n }\n const dictionaryEm = context.em.fork()\n const dictionary = await findOneWithDecryption(\n dictionaryEm,\n Dictionary,\n {\n id: dictionaryId,\n organizationId: context.organizationId,\n tenantId: context.tenantId,\n deletedAt: null,\n },\n undefined,\n { tenantId: context.tenantId, organizationId: context.organizationId },\n )\n if (!dictionary) {\n throw new CrudHttpError(404, { error: context.translate('dictionaries.errors.not_found', 'Dictionary not found') })\n }\n\n const payload = setDefaultDictionaryEntrySchema.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: 'dictionaries.dictionary',\n resourceId: dictionaryId,\n operation: 'custom',\n requestMethod: req.method,\n requestHeaders: req.headers,\n mutationPayload: payload,\n })\n if (guardResult && !guardResult.ok) {\n return NextResponse.json(guardResult.body, { status: guardResult.status })\n }\n\n const commandInput = setDefaultDictionaryEntryCommandSchema.parse({\n dictionaryId,\n tenantId: context.tenantId,\n organizationId: context.organizationId,\n entryId: payload.entryId,\n } satisfies SetDefaultDictionaryEntryCommandInput)\n\n const commandBus = context.container.resolve('commandBus') as CommandBus\n const { logEntry } = await commandBus.execute<SetDefaultDictionaryEntryCommandInput, { dictionaryId: string; entryId: string; clearedIds: string[] }>(\n 'dictionaries.entries.set_default',\n {\n input: commandInput,\n ctx: context.ctx,\n },\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: 'dictionaries.dictionary',\n resourceId: dictionaryId,\n operation: 'custom',\n requestMethod: req.method,\n requestHeaders: req.headers,\n metadata: guardResult.metadata ?? null,\n })\n }\n\n const response = NextResponse.json({ ok: true })\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 ?? 'dictionaries.dictionary',\n resourceId: logEntry.resourceId ?? dictionaryId,\n executedAt: logEntry.createdAt instanceof Date ? logEntry.createdAt.toISOString() : new Date().toISOString(),\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 interceptorRejection = getCommandInterceptorHttpRejection(err)\n if (interceptorRejection) {\n return NextResponse.json(interceptorRejection.body, { status: interceptorRejection.status })\n }\n if (err instanceof z.ZodError) {\n return NextResponse.json({ error: 'Invalid input', details: err.issues }, { status: 400 })\n }\n logger.error('Failed to set default entry', { err })\n return NextResponse.json({ error: 'Failed to set default entry' }, { status: 500 })\n }\n}\n\nconst setDefaultPostDoc: OpenApiMethodDoc = {\n summary: 'Set default dictionary entry',\n description: 'Marks the specified entry as the default for this dictionary, clearing any previous default.',\n tags: [dictionariesTag],\n requestBody: {\n contentType: 'application/json',\n schema: setDefaultEntryRequestSchema,\n description: 'ID of the entry to set as default.',\n },\n responses: [\n { status: 200, description: 'Default entry set.', schema: dictionariesOkSchema },\n ],\n errors: [\n { status: 400, description: 'Validation failed', schema: dictionariesErrorSchema },\n { status: 401, description: 'Authentication required', schema: dictionariesErrorSchema },\n { status: 404, description: 'Dictionary or entry not found', schema: dictionariesErrorSchema },\n { status: 500, description: 'Failed to set default entry', schema: dictionariesErrorSchema },\n ],\n}\n\nexport const openApi: OpenApiRouteDoc = {\n tag: dictionariesTag,\n summary: 'Set default dictionary entry',\n pathParams: dictionaryIdParamsSchema,\n methods: {\n POST: setDefaultPostDoc,\n },\n}\n"],
|
|
5
|
+
"mappings": "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",
|
|
6
6
|
"names": []
|
|
7
7
|
}
|
|
@@ -16,6 +16,7 @@ import { organizationUpdateSchema } from "@open-mercato/core/modules/directory/d
|
|
|
16
16
|
import { resolveOrganizationScopeForRequest } from "@open-mercato/core/modules/directory/utils/organizationScope";
|
|
17
17
|
import "@open-mercato/core/modules/directory/commands/organizations";
|
|
18
18
|
import { createLogger } from "@open-mercato/shared/lib/logger";
|
|
19
|
+
import { getCommandInterceptorHttpRejection } from "@open-mercato/shared/lib/commands/errors";
|
|
19
20
|
const logger = createLogger("directory").child({ component: "organization-branding" });
|
|
20
21
|
const metadata = {
|
|
21
22
|
GET: { requireAuth: true, requireFeatures: ["directory.organizations.view"] },
|
|
@@ -224,6 +225,10 @@ async function PUT(req) {
|
|
|
224
225
|
if (isCrudHttpError(err)) {
|
|
225
226
|
return NextResponse.json(err.body, { status: err.status });
|
|
226
227
|
}
|
|
228
|
+
const interceptorRejection = getCommandInterceptorHttpRejection(err);
|
|
229
|
+
if (interceptorRejection) {
|
|
230
|
+
return NextResponse.json(interceptorRejection.body, { status: interceptorRejection.status });
|
|
231
|
+
}
|
|
227
232
|
logger.error("Organization branding update failed", { err });
|
|
228
233
|
return NextResponse.json(
|
|
229
234
|
{ error: resolved.translate("directory.branding.errors.save", "Failed to update organization branding.") },
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"version": 3,
|
|
3
3
|
"sources": ["../../../../../src/modules/directory/api/organization-branding/route.ts"],
|
|
4
|
-
"sourcesContent": ["import { NextResponse } from 'next/server'\nimport { z } from 'zod'\nimport type { EntityManager } from '@mikro-orm/postgresql'\nimport { runWithCacheTenant } from '@open-mercato/cache'\nimport type { CommandBus, CommandRuntimeContext } from '@open-mercato/shared/lib/commands'\nimport type { OpenApiRouteDoc } from '@open-mercato/shared/lib/openapi'\nimport { getAuthFromRequest } from '@open-mercato/shared/lib/auth/server'\nimport { createRequestContainer } from '@open-mercato/shared/lib/di/container'\nimport { isCrudHttpError } from '@open-mercato/shared/lib/crud/errors'\nimport {\n runCrudMutationGuardAfterSuccess,\n validateCrudMutationGuard,\n} from '@open-mercato/shared/lib/crud/mutation-guard'\nimport { enforceCommandOptimisticLock } from '@open-mercato/shared/lib/crud/optimistic-lock-command'\nimport { findOneWithDecryption } from '@open-mercato/shared/lib/encryption/find'\nimport { resolveTranslations } from '@open-mercato/shared/lib/i18n/server'\nimport { Organization } from '@open-mercato/core/modules/directory/data/entities'\nimport { organizationUpdateSchema } from '@open-mercato/core/modules/directory/data/validators'\nimport { resolveOrganizationScopeForRequest } from '@open-mercato/core/modules/directory/utils/organizationScope'\nimport '@open-mercato/core/modules/directory/commands/organizations'\nimport { createLogger } from '@open-mercato/shared/lib/logger'\n\nconst logger = createLogger('directory').child({ component: 'organization-branding' })\n\nexport const metadata = {\n GET: { requireAuth: true, requireFeatures: ['directory.organizations.view'] },\n PUT: { requireAuth: true, requireFeatures: ['directory.organizations.manage'] },\n}\n\nconst brandingResponseSchema = z.object({\n organizationId: z.string().uuid(),\n organizationName: z.string(),\n tenantId: z.string().uuid(),\n logoUrl: z.string().nullable(),\n logoPreserveAspectRatio: z.boolean(),\n updatedAt: z.string().nullable(),\n})\n\nconst brandingUpdateSchema = z.object({\n logoUrl: organizationUpdateSchema.shape.logoUrl,\n logoPreserveAspectRatio: organizationUpdateSchema.shape.logoPreserveAspectRatio,\n})\n\nconst errorSchema = z.object({\n error: z.string(),\n})\n\ntype RequestContainer = Awaited<ReturnType<typeof createRequestContainer>>\n\nfunction buildCommandContext(\n container: RequestContainer,\n auth: NonNullable<Awaited<ReturnType<typeof getAuthFromRequest>>>,\n req: Request,\n organizationId: string,\n tenantId: string,\n): CommandRuntimeContext {\n return {\n container,\n auth,\n organizationScope: {\n selectedId: organizationId,\n filterIds: [organizationId],\n allowedIds: null,\n tenantId,\n },\n selectedOrganizationId: organizationId,\n organizationIds: [organizationId],\n request: req,\n }\n}\n\nasync function resolveCurrentOrganization(req: Request) {\n const { translate } = await resolveTranslations()\n const auth = await getAuthFromRequest(req)\n if (!auth?.sub) {\n return {\n response: NextResponse.json({ error: translate('api.errors.unauthorized', 'Unauthorized') }, { status: 401 }),\n }\n }\n\n const container = await createRequestContainer()\n const scope = await resolveOrganizationScopeForRequest({ container, auth, request: req })\n const organizationId = scope.selectedId ?? auth.orgId ?? null\n const tenantId = scope.tenantId ?? auth.tenantId ?? null\n if (!organizationId || !tenantId) {\n return {\n response: NextResponse.json(\n {\n error: translate(\n 'directory.branding.errors.organizationRequired',\n 'Select a single organization before changing sidebar branding.',\n ),\n },\n { status: 400 },\n ),\n }\n }\n\n const em = container.resolve('em') as EntityManager\n const organization = await findOneWithDecryption(\n em,\n Organization,\n { id: organizationId, tenant: tenantId, deletedAt: null },\n { populate: ['tenant'] },\n { tenantId, organizationId },\n )\n if (!organization) {\n return {\n response: NextResponse.json(\n { error: translate('directory.branding.errors.notFound', 'Organization not found') },\n { status: 404 },\n ),\n }\n }\n\n return { auth, container, organization, organizationId, tenantId, translate }\n}\n\nfunction toIsoOrNull(value: Date | string | null | undefined): string | null {\n if (value == null) return null\n if (value instanceof Date) {\n const ms = value.getTime()\n return Number.isFinite(ms) ? new Date(ms).toISOString() : null\n }\n const trimmed = String(value).trim()\n if (!trimmed) return null\n const parsed = new Date(trimmed)\n return Number.isFinite(parsed.getTime()) ? parsed.toISOString() : null\n}\n\nfunction toResponsePayload(organization: Organization, tenantId: string) {\n return {\n organizationId: String(organization.id),\n organizationName: organization.name,\n tenantId,\n logoUrl: organization.logoUrl ?? null,\n logoPreserveAspectRatio: !!organization.logoPreserveAspectRatio,\n updatedAt: toIsoOrNull(organization.updatedAt),\n }\n}\n\nasync function invalidateSidebarBrandingCache(container: RequestContainer, organizationId: string, tenantId: string) {\n try {\n const cache = container.resolve('cache') as {\n deleteByTags?: (tags: string[]) => Promise<unknown>\n } | null\n await runWithCacheTenant(tenantId, () =>\n cache?.deleteByTags?.([\n `nav:sidebar:organization:${organizationId}`,\n `nav:sidebar:tenant:${tenantId}`,\n ]),\n )\n } catch {\n // Cache invalidation is best-effort; the persisted branding is the source of truth.\n }\n}\n\nexport async function GET(req: Request) {\n const resolved = await resolveCurrentOrganization(req)\n if ('response' in resolved) return resolved.response\n\n return NextResponse.json(toResponsePayload(resolved.organization, resolved.tenantId))\n}\n\nexport async function PUT(req: Request) {\n const resolved = await resolveCurrentOrganization(req)\n if ('response' in resolved) return resolved.response\n\n let body: unknown\n try {\n body = await req.json()\n } catch {\n return NextResponse.json(\n { error: resolved.translate('directory.branding.errors.invalidLogoUrl', 'Enter a valid image URL.') },\n { status: 422 },\n )\n }\n if (!body || typeof body !== 'object' || !Object.prototype.hasOwnProperty.call(body, 'logoUrl')) {\n return NextResponse.json(\n { error: resolved.translate('directory.branding.errors.invalidLogoUrl', 'Enter a valid image URL.') },\n { status: 422 },\n )\n }\n\n const parsed = brandingUpdateSchema.safeParse(body)\n if (!parsed.success) {\n return NextResponse.json(\n {\n error: resolved.translate('directory.branding.errors.invalidLogoUrl', 'Enter a valid image URL.'),\n issues: parsed.error.issues,\n },\n { status: 422 },\n )\n }\n\n try {\n // Refuse a stale overwrite when two tabs edit the same organization branding.\n // Strictly additive \u2014 a no-op when the client omits the expected-version header.\n enforceCommandOptimisticLock({\n resourceKind: 'directory.organization',\n resourceId: resolved.organizationId,\n current: resolved.organization.updatedAt ?? null,\n request: req,\n })\n\n // Mutation-guard contract for custom write routes: validate before the write,\n // then run the after-success hook once the command persists.\n const guardResult = await validateCrudMutationGuard(resolved.container, {\n tenantId: resolved.tenantId,\n organizationId: resolved.organizationId,\n userId: resolved.auth.sub,\n resourceKind: 'directory.organization',\n resourceId: resolved.organizationId,\n operation: 'update',\n requestMethod: req.method,\n requestHeaders: req.headers,\n mutationPayload: {\n logoUrl: parsed.data.logoUrl ?? null,\n ...(parsed.data.logoPreserveAspectRatio !== undefined\n ? { logoPreserveAspectRatio: parsed.data.logoPreserveAspectRatio }\n : {}),\n },\n })\n if (guardResult && !guardResult.ok) {\n return NextResponse.json(guardResult.body, { status: guardResult.status })\n }\n\n const commandBus = resolved.container.resolve('commandBus') as CommandBus\n const ctx = buildCommandContext(\n resolved.container,\n resolved.auth,\n req,\n resolved.organizationId,\n resolved.tenantId,\n )\n const { result } = await commandBus.execute<Record<string, unknown>, Organization>(\n 'directory.organizations.update',\n {\n input: {\n id: resolved.organizationId,\n tenantId: resolved.tenantId,\n logoUrl: parsed.data.logoUrl ?? null,\n ...(parsed.data.logoPreserveAspectRatio !== undefined\n ? { logoPreserveAspectRatio: parsed.data.logoPreserveAspectRatio }\n : {}),\n },\n ctx,\n },\n )\n\n if (guardResult?.ok && guardResult.shouldRunAfterSuccess) {\n await runCrudMutationGuardAfterSuccess(resolved.container, {\n tenantId: resolved.tenantId,\n organizationId: resolved.organizationId,\n userId: resolved.auth.sub,\n resourceKind: 'directory.organization',\n resourceId: resolved.organizationId,\n operation: 'update',\n requestMethod: req.method,\n requestHeaders: req.headers,\n metadata: guardResult.metadata ?? null,\n })\n }\n\n await invalidateSidebarBrandingCache(resolved.container, resolved.organizationId, resolved.tenantId)\n return NextResponse.json(toResponsePayload(result, resolved.tenantId))\n } catch (err) {\n if (isCrudHttpError(err)) {\n return NextResponse.json(err.body, { status: err.status })\n }\n logger.error('Organization branding update failed', { err })\n return NextResponse.json(\n { error: resolved.translate('directory.branding.errors.save', 'Failed to update organization branding.') },\n { status: 400 },\n )\n }\n}\n\nexport const openApi: OpenApiRouteDoc = {\n tag: 'Directory',\n summary: 'Current organization branding',\n methods: {\n GET: {\n summary: 'Read sidebar branding for the selected organization',\n description: 'Returns the logo URL used by the backend sidebar for the currently selected organization.',\n responses: [\n { status: 200, description: 'Organization branding', schema: brandingResponseSchema },\n ],\n errors: [\n { status: 400, description: 'A concrete organization scope is required', schema: errorSchema },\n { status: 401, description: 'Unauthorized', schema: errorSchema },\n { status: 404, description: 'Organization not found', schema: errorSchema },\n ],\n },\n PUT: {\n summary: 'Update sidebar branding for the selected organization',\n description: 'Stores an external image URL or an internal attachment image URL as the selected organization logo.',\n requestBody: {\n contentType: 'application/json',\n schema: brandingUpdateSchema,\n },\n responses: [\n { status: 200, description: 'Updated organization branding', schema: brandingResponseSchema },\n ],\n errors: [\n { status: 400, description: 'Save failed', schema: errorSchema },\n { status: 401, description: 'Unauthorized', schema: errorSchema },\n { status: 409, description: 'Organization branding changed since it was loaded', schema: errorSchema },\n { status: 422, description: 'Invalid logo URL', schema: errorSchema },\n ],\n },\n },\n}\n"],
|
|
5
|
-
"mappings": "AAAA,SAAS,oBAAoB;AAC7B,SAAS,SAAS;AAElB,SAAS,0BAA0B;AAGnC,SAAS,0BAA0B;AACnC,SAAS,8BAA8B;AACvC,SAAS,uBAAuB;AAChC;AAAA,EACE;AAAA,EACA;AAAA,OACK;AACP,SAAS,oCAAoC;AAC7C,SAAS,6BAA6B;AACtC,SAAS,2BAA2B;AACpC,SAAS,oBAAoB;AAC7B,SAAS,gCAAgC;AACzC,SAAS,0CAA0C;AACnD,OAAO;AACP,SAAS,oBAAoB;
|
|
4
|
+
"sourcesContent": ["import { NextResponse } from 'next/server'\nimport { z } from 'zod'\nimport type { EntityManager } from '@mikro-orm/postgresql'\nimport { runWithCacheTenant } from '@open-mercato/cache'\nimport type { CommandBus, CommandRuntimeContext } from '@open-mercato/shared/lib/commands'\nimport type { OpenApiRouteDoc } from '@open-mercato/shared/lib/openapi'\nimport { getAuthFromRequest } from '@open-mercato/shared/lib/auth/server'\nimport { createRequestContainer } from '@open-mercato/shared/lib/di/container'\nimport { isCrudHttpError } from '@open-mercato/shared/lib/crud/errors'\nimport {\n runCrudMutationGuardAfterSuccess,\n validateCrudMutationGuard,\n} from '@open-mercato/shared/lib/crud/mutation-guard'\nimport { enforceCommandOptimisticLock } from '@open-mercato/shared/lib/crud/optimistic-lock-command'\nimport { findOneWithDecryption } from '@open-mercato/shared/lib/encryption/find'\nimport { resolveTranslations } from '@open-mercato/shared/lib/i18n/server'\nimport { Organization } from '@open-mercato/core/modules/directory/data/entities'\nimport { organizationUpdateSchema } from '@open-mercato/core/modules/directory/data/validators'\nimport { resolveOrganizationScopeForRequest } from '@open-mercato/core/modules/directory/utils/organizationScope'\nimport '@open-mercato/core/modules/directory/commands/organizations'\nimport { createLogger } from '@open-mercato/shared/lib/logger'\nimport { getCommandInterceptorHttpRejection } from '@open-mercato/shared/lib/commands/errors'\n\nconst logger = createLogger('directory').child({ component: 'organization-branding' })\n\nexport const metadata = {\n GET: { requireAuth: true, requireFeatures: ['directory.organizations.view'] },\n PUT: { requireAuth: true, requireFeatures: ['directory.organizations.manage'] },\n}\n\nconst brandingResponseSchema = z.object({\n organizationId: z.string().uuid(),\n organizationName: z.string(),\n tenantId: z.string().uuid(),\n logoUrl: z.string().nullable(),\n logoPreserveAspectRatio: z.boolean(),\n updatedAt: z.string().nullable(),\n})\n\nconst brandingUpdateSchema = z.object({\n logoUrl: organizationUpdateSchema.shape.logoUrl,\n logoPreserveAspectRatio: organizationUpdateSchema.shape.logoPreserveAspectRatio,\n})\n\nconst errorSchema = z.object({\n error: z.string(),\n})\n\ntype RequestContainer = Awaited<ReturnType<typeof createRequestContainer>>\n\nfunction buildCommandContext(\n container: RequestContainer,\n auth: NonNullable<Awaited<ReturnType<typeof getAuthFromRequest>>>,\n req: Request,\n organizationId: string,\n tenantId: string,\n): CommandRuntimeContext {\n return {\n container,\n auth,\n organizationScope: {\n selectedId: organizationId,\n filterIds: [organizationId],\n allowedIds: null,\n tenantId,\n },\n selectedOrganizationId: organizationId,\n organizationIds: [organizationId],\n request: req,\n }\n}\n\nasync function resolveCurrentOrganization(req: Request) {\n const { translate } = await resolveTranslations()\n const auth = await getAuthFromRequest(req)\n if (!auth?.sub) {\n return {\n response: NextResponse.json({ error: translate('api.errors.unauthorized', 'Unauthorized') }, { status: 401 }),\n }\n }\n\n const container = await createRequestContainer()\n const scope = await resolveOrganizationScopeForRequest({ container, auth, request: req })\n const organizationId = scope.selectedId ?? auth.orgId ?? null\n const tenantId = scope.tenantId ?? auth.tenantId ?? null\n if (!organizationId || !tenantId) {\n return {\n response: NextResponse.json(\n {\n error: translate(\n 'directory.branding.errors.organizationRequired',\n 'Select a single organization before changing sidebar branding.',\n ),\n },\n { status: 400 },\n ),\n }\n }\n\n const em = container.resolve('em') as EntityManager\n const organization = await findOneWithDecryption(\n em,\n Organization,\n { id: organizationId, tenant: tenantId, deletedAt: null },\n { populate: ['tenant'] },\n { tenantId, organizationId },\n )\n if (!organization) {\n return {\n response: NextResponse.json(\n { error: translate('directory.branding.errors.notFound', 'Organization not found') },\n { status: 404 },\n ),\n }\n }\n\n return { auth, container, organization, organizationId, tenantId, translate }\n}\n\nfunction toIsoOrNull(value: Date | string | null | undefined): string | null {\n if (value == null) return null\n if (value instanceof Date) {\n const ms = value.getTime()\n return Number.isFinite(ms) ? new Date(ms).toISOString() : null\n }\n const trimmed = String(value).trim()\n if (!trimmed) return null\n const parsed = new Date(trimmed)\n return Number.isFinite(parsed.getTime()) ? parsed.toISOString() : null\n}\n\nfunction toResponsePayload(organization: Organization, tenantId: string) {\n return {\n organizationId: String(organization.id),\n organizationName: organization.name,\n tenantId,\n logoUrl: organization.logoUrl ?? null,\n logoPreserveAspectRatio: !!organization.logoPreserveAspectRatio,\n updatedAt: toIsoOrNull(organization.updatedAt),\n }\n}\n\nasync function invalidateSidebarBrandingCache(container: RequestContainer, organizationId: string, tenantId: string) {\n try {\n const cache = container.resolve('cache') as {\n deleteByTags?: (tags: string[]) => Promise<unknown>\n } | null\n await runWithCacheTenant(tenantId, () =>\n cache?.deleteByTags?.([\n `nav:sidebar:organization:${organizationId}`,\n `nav:sidebar:tenant:${tenantId}`,\n ]),\n )\n } catch {\n // Cache invalidation is best-effort; the persisted branding is the source of truth.\n }\n}\n\nexport async function GET(req: Request) {\n const resolved = await resolveCurrentOrganization(req)\n if ('response' in resolved) return resolved.response\n\n return NextResponse.json(toResponsePayload(resolved.organization, resolved.tenantId))\n}\n\nexport async function PUT(req: Request) {\n const resolved = await resolveCurrentOrganization(req)\n if ('response' in resolved) return resolved.response\n\n let body: unknown\n try {\n body = await req.json()\n } catch {\n return NextResponse.json(\n { error: resolved.translate('directory.branding.errors.invalidLogoUrl', 'Enter a valid image URL.') },\n { status: 422 },\n )\n }\n if (!body || typeof body !== 'object' || !Object.prototype.hasOwnProperty.call(body, 'logoUrl')) {\n return NextResponse.json(\n { error: resolved.translate('directory.branding.errors.invalidLogoUrl', 'Enter a valid image URL.') },\n { status: 422 },\n )\n }\n\n const parsed = brandingUpdateSchema.safeParse(body)\n if (!parsed.success) {\n return NextResponse.json(\n {\n error: resolved.translate('directory.branding.errors.invalidLogoUrl', 'Enter a valid image URL.'),\n issues: parsed.error.issues,\n },\n { status: 422 },\n )\n }\n\n try {\n // Refuse a stale overwrite when two tabs edit the same organization branding.\n // Strictly additive \u2014 a no-op when the client omits the expected-version header.\n enforceCommandOptimisticLock({\n resourceKind: 'directory.organization',\n resourceId: resolved.organizationId,\n current: resolved.organization.updatedAt ?? null,\n request: req,\n })\n\n // Mutation-guard contract for custom write routes: validate before the write,\n // then run the after-success hook once the command persists.\n const guardResult = await validateCrudMutationGuard(resolved.container, {\n tenantId: resolved.tenantId,\n organizationId: resolved.organizationId,\n userId: resolved.auth.sub,\n resourceKind: 'directory.organization',\n resourceId: resolved.organizationId,\n operation: 'update',\n requestMethod: req.method,\n requestHeaders: req.headers,\n mutationPayload: {\n logoUrl: parsed.data.logoUrl ?? null,\n ...(parsed.data.logoPreserveAspectRatio !== undefined\n ? { logoPreserveAspectRatio: parsed.data.logoPreserveAspectRatio }\n : {}),\n },\n })\n if (guardResult && !guardResult.ok) {\n return NextResponse.json(guardResult.body, { status: guardResult.status })\n }\n\n const commandBus = resolved.container.resolve('commandBus') as CommandBus\n const ctx = buildCommandContext(\n resolved.container,\n resolved.auth,\n req,\n resolved.organizationId,\n resolved.tenantId,\n )\n const { result } = await commandBus.execute<Record<string, unknown>, Organization>(\n 'directory.organizations.update',\n {\n input: {\n id: resolved.organizationId,\n tenantId: resolved.tenantId,\n logoUrl: parsed.data.logoUrl ?? null,\n ...(parsed.data.logoPreserveAspectRatio !== undefined\n ? { logoPreserveAspectRatio: parsed.data.logoPreserveAspectRatio }\n : {}),\n },\n ctx,\n },\n )\n\n if (guardResult?.ok && guardResult.shouldRunAfterSuccess) {\n await runCrudMutationGuardAfterSuccess(resolved.container, {\n tenantId: resolved.tenantId,\n organizationId: resolved.organizationId,\n userId: resolved.auth.sub,\n resourceKind: 'directory.organization',\n resourceId: resolved.organizationId,\n operation: 'update',\n requestMethod: req.method,\n requestHeaders: req.headers,\n metadata: guardResult.metadata ?? null,\n })\n }\n\n await invalidateSidebarBrandingCache(resolved.container, resolved.organizationId, resolved.tenantId)\n return NextResponse.json(toResponsePayload(result, resolved.tenantId))\n } catch (err) {\n if (isCrudHttpError(err)) {\n return NextResponse.json(err.body, { status: err.status })\n }\n const interceptorRejection = getCommandInterceptorHttpRejection(err)\n if (interceptorRejection) {\n return NextResponse.json(interceptorRejection.body, { status: interceptorRejection.status })\n }\n logger.error('Organization branding update failed', { err })\n return NextResponse.json(\n { error: resolved.translate('directory.branding.errors.save', 'Failed to update organization branding.') },\n { status: 400 },\n )\n }\n}\n\nexport const openApi: OpenApiRouteDoc = {\n tag: 'Directory',\n summary: 'Current organization branding',\n methods: {\n GET: {\n summary: 'Read sidebar branding for the selected organization',\n description: 'Returns the logo URL used by the backend sidebar for the currently selected organization.',\n responses: [\n { status: 200, description: 'Organization branding', schema: brandingResponseSchema },\n ],\n errors: [\n { status: 400, description: 'A concrete organization scope is required', schema: errorSchema },\n { status: 401, description: 'Unauthorized', schema: errorSchema },\n { status: 404, description: 'Organization not found', schema: errorSchema },\n ],\n },\n PUT: {\n summary: 'Update sidebar branding for the selected organization',\n description: 'Stores an external image URL or an internal attachment image URL as the selected organization logo.',\n requestBody: {\n contentType: 'application/json',\n schema: brandingUpdateSchema,\n },\n responses: [\n { status: 200, description: 'Updated organization branding', schema: brandingResponseSchema },\n ],\n errors: [\n { status: 400, description: 'Save failed', schema: errorSchema },\n { status: 401, description: 'Unauthorized', schema: errorSchema },\n { status: 409, description: 'Organization branding changed since it was loaded', schema: errorSchema },\n { status: 422, description: 'Invalid logo URL', schema: errorSchema },\n ],\n },\n },\n}\n"],
|
|
5
|
+
"mappings": "AAAA,SAAS,oBAAoB;AAC7B,SAAS,SAAS;AAElB,SAAS,0BAA0B;AAGnC,SAAS,0BAA0B;AACnC,SAAS,8BAA8B;AACvC,SAAS,uBAAuB;AAChC;AAAA,EACE;AAAA,EACA;AAAA,OACK;AACP,SAAS,oCAAoC;AAC7C,SAAS,6BAA6B;AACtC,SAAS,2BAA2B;AACpC,SAAS,oBAAoB;AAC7B,SAAS,gCAAgC;AACzC,SAAS,0CAA0C;AACnD,OAAO;AACP,SAAS,oBAAoB;AAC7B,SAAS,0CAA0C;AAEnD,MAAM,SAAS,aAAa,WAAW,EAAE,MAAM,EAAE,WAAW,wBAAwB,CAAC;AAE9E,MAAM,WAAW;AAAA,EACtB,KAAK,EAAE,aAAa,MAAM,iBAAiB,CAAC,8BAA8B,EAAE;AAAA,EAC5E,KAAK,EAAE,aAAa,MAAM,iBAAiB,CAAC,gCAAgC,EAAE;AAChF;AAEA,MAAM,yBAAyB,EAAE,OAAO;AAAA,EACtC,gBAAgB,EAAE,OAAO,EAAE,KAAK;AAAA,EAChC,kBAAkB,EAAE,OAAO;AAAA,EAC3B,UAAU,EAAE,OAAO,EAAE,KAAK;AAAA,EAC1B,SAAS,EAAE,OAAO,EAAE,SAAS;AAAA,EAC7B,yBAAyB,EAAE,QAAQ;AAAA,EACnC,WAAW,EAAE,OAAO,EAAE,SAAS;AACjC,CAAC;AAED,MAAM,uBAAuB,EAAE,OAAO;AAAA,EACpC,SAAS,yBAAyB,MAAM;AAAA,EACxC,yBAAyB,yBAAyB,MAAM;AAC1D,CAAC;AAED,MAAM,cAAc,EAAE,OAAO;AAAA,EAC3B,OAAO,EAAE,OAAO;AAClB,CAAC;AAID,SAAS,oBACP,WACA,MACA,KACA,gBACA,UACuB;AACvB,SAAO;AAAA,IACL;AAAA,IACA;AAAA,IACA,mBAAmB;AAAA,MACjB,YAAY;AAAA,MACZ,WAAW,CAAC,cAAc;AAAA,MAC1B,YAAY;AAAA,MACZ;AAAA,IACF;AAAA,IACA,wBAAwB;AAAA,IACxB,iBAAiB,CAAC,cAAc;AAAA,IAChC,SAAS;AAAA,EACX;AACF;AAEA,eAAe,2BAA2B,KAAc;AACtD,QAAM,EAAE,UAAU,IAAI,MAAM,oBAAoB;AAChD,QAAM,OAAO,MAAM,mBAAmB,GAAG;AACzC,MAAI,CAAC,MAAM,KAAK;AACd,WAAO;AAAA,MACL,UAAU,aAAa,KAAK,EAAE,OAAO,UAAU,2BAA2B,cAAc,EAAE,GAAG,EAAE,QAAQ,IAAI,CAAC;AAAA,IAC9G;AAAA,EACF;AAEA,QAAM,YAAY,MAAM,uBAAuB;AAC/C,QAAM,QAAQ,MAAM,mCAAmC,EAAE,WAAW,MAAM,SAAS,IAAI,CAAC;AACxF,QAAM,iBAAiB,MAAM,cAAc,KAAK,SAAS;AACzD,QAAM,WAAW,MAAM,YAAY,KAAK,YAAY;AACpD,MAAI,CAAC,kBAAkB,CAAC,UAAU;AAChC,WAAO;AAAA,MACL,UAAU,aAAa;AAAA,QACrB;AAAA,UACE,OAAO;AAAA,YACL;AAAA,YACA;AAAA,UACF;AAAA,QACF;AAAA,QACA,EAAE,QAAQ,IAAI;AAAA,MAChB;AAAA,IACF;AAAA,EACF;AAEA,QAAM,KAAK,UAAU,QAAQ,IAAI;AACjC,QAAM,eAAe,MAAM;AAAA,IACzB;AAAA,IACA;AAAA,IACA,EAAE,IAAI,gBAAgB,QAAQ,UAAU,WAAW,KAAK;AAAA,IACxD,EAAE,UAAU,CAAC,QAAQ,EAAE;AAAA,IACvB,EAAE,UAAU,eAAe;AAAA,EAC7B;AACA,MAAI,CAAC,cAAc;AACjB,WAAO;AAAA,MACL,UAAU,aAAa;AAAA,QACrB,EAAE,OAAO,UAAU,sCAAsC,wBAAwB,EAAE;AAAA,QACnF,EAAE,QAAQ,IAAI;AAAA,MAChB;AAAA,IACF;AAAA,EACF;AAEA,SAAO,EAAE,MAAM,WAAW,cAAc,gBAAgB,UAAU,UAAU;AAC9E;AAEA,SAAS,YAAY,OAAwD;AAC3E,MAAI,SAAS,KAAM,QAAO;AAC1B,MAAI,iBAAiB,MAAM;AACzB,UAAM,KAAK,MAAM,QAAQ;AACzB,WAAO,OAAO,SAAS,EAAE,IAAI,IAAI,KAAK,EAAE,EAAE,YAAY,IAAI;AAAA,EAC5D;AACA,QAAM,UAAU,OAAO,KAAK,EAAE,KAAK;AACnC,MAAI,CAAC,QAAS,QAAO;AACrB,QAAM,SAAS,IAAI,KAAK,OAAO;AAC/B,SAAO,OAAO,SAAS,OAAO,QAAQ,CAAC,IAAI,OAAO,YAAY,IAAI;AACpE;AAEA,SAAS,kBAAkB,cAA4B,UAAkB;AACvE,SAAO;AAAA,IACL,gBAAgB,OAAO,aAAa,EAAE;AAAA,IACtC,kBAAkB,aAAa;AAAA,IAC/B;AAAA,IACA,SAAS,aAAa,WAAW;AAAA,IACjC,yBAAyB,CAAC,CAAC,aAAa;AAAA,IACxC,WAAW,YAAY,aAAa,SAAS;AAAA,EAC/C;AACF;AAEA,eAAe,+BAA+B,WAA6B,gBAAwB,UAAkB;AACnH,MAAI;AACF,UAAM,QAAQ,UAAU,QAAQ,OAAO;AAGvC,UAAM;AAAA,MAAmB;AAAA,MAAU,MACjC,OAAO,eAAe;AAAA,QACpB,4BAA4B,cAAc;AAAA,QAC1C,sBAAsB,QAAQ;AAAA,MAChC,CAAC;AAAA,IACH;AAAA,EACF,QAAQ;AAAA,EAER;AACF;AAEA,eAAsB,IAAI,KAAc;AACtC,QAAM,WAAW,MAAM,2BAA2B,GAAG;AACrD,MAAI,cAAc,SAAU,QAAO,SAAS;AAE5C,SAAO,aAAa,KAAK,kBAAkB,SAAS,cAAc,SAAS,QAAQ,CAAC;AACtF;AAEA,eAAsB,IAAI,KAAc;AACtC,QAAM,WAAW,MAAM,2BAA2B,GAAG;AACrD,MAAI,cAAc,SAAU,QAAO,SAAS;AAE5C,MAAI;AACJ,MAAI;AACF,WAAO,MAAM,IAAI,KAAK;AAAA,EACxB,QAAQ;AACN,WAAO,aAAa;AAAA,MAClB,EAAE,OAAO,SAAS,UAAU,4CAA4C,0BAA0B,EAAE;AAAA,MACpG,EAAE,QAAQ,IAAI;AAAA,IAChB;AAAA,EACF;AACA,MAAI,CAAC,QAAQ,OAAO,SAAS,YAAY,CAAC,OAAO,UAAU,eAAe,KAAK,MAAM,SAAS,GAAG;AAC/F,WAAO,aAAa;AAAA,MAClB,EAAE,OAAO,SAAS,UAAU,4CAA4C,0BAA0B,EAAE;AAAA,MACpG,EAAE,QAAQ,IAAI;AAAA,IAChB;AAAA,EACF;AAEA,QAAM,SAAS,qBAAqB,UAAU,IAAI;AAClD,MAAI,CAAC,OAAO,SAAS;AACnB,WAAO,aAAa;AAAA,MAClB;AAAA,QACE,OAAO,SAAS,UAAU,4CAA4C,0BAA0B;AAAA,QAChG,QAAQ,OAAO,MAAM;AAAA,MACvB;AAAA,MACA,EAAE,QAAQ,IAAI;AAAA,IAChB;AAAA,EACF;AAEA,MAAI;AAGF,iCAA6B;AAAA,MAC3B,cAAc;AAAA,MACd,YAAY,SAAS;AAAA,MACrB,SAAS,SAAS,aAAa,aAAa;AAAA,MAC5C,SAAS;AAAA,IACX,CAAC;AAID,UAAM,cAAc,MAAM,0BAA0B,SAAS,WAAW;AAAA,MACtE,UAAU,SAAS;AAAA,MACnB,gBAAgB,SAAS;AAAA,MACzB,QAAQ,SAAS,KAAK;AAAA,MACtB,cAAc;AAAA,MACd,YAAY,SAAS;AAAA,MACrB,WAAW;AAAA,MACX,eAAe,IAAI;AAAA,MACnB,gBAAgB,IAAI;AAAA,MACpB,iBAAiB;AAAA,QACf,SAAS,OAAO,KAAK,WAAW;AAAA,QAChC,GAAI,OAAO,KAAK,4BAA4B,SACxC,EAAE,yBAAyB,OAAO,KAAK,wBAAwB,IAC/D,CAAC;AAAA,MACP;AAAA,IACF,CAAC;AACD,QAAI,eAAe,CAAC,YAAY,IAAI;AAClC,aAAO,aAAa,KAAK,YAAY,MAAM,EAAE,QAAQ,YAAY,OAAO,CAAC;AAAA,IAC3E;AAEA,UAAM,aAAa,SAAS,UAAU,QAAQ,YAAY;AAC1D,UAAM,MAAM;AAAA,MACV,SAAS;AAAA,MACT,SAAS;AAAA,MACT;AAAA,MACA,SAAS;AAAA,MACT,SAAS;AAAA,IACX;AACA,UAAM,EAAE,OAAO,IAAI,MAAM,WAAW;AAAA,MAClC;AAAA,MACA;AAAA,QACE,OAAO;AAAA,UACL,IAAI,SAAS;AAAA,UACb,UAAU,SAAS;AAAA,UACnB,SAAS,OAAO,KAAK,WAAW;AAAA,UAChC,GAAI,OAAO,KAAK,4BAA4B,SACxC,EAAE,yBAAyB,OAAO,KAAK,wBAAwB,IAC/D,CAAC;AAAA,QACP;AAAA,QACA;AAAA,MACF;AAAA,IACF;AAEA,QAAI,aAAa,MAAM,YAAY,uBAAuB;AACxD,YAAM,iCAAiC,SAAS,WAAW;AAAA,QACzD,UAAU,SAAS;AAAA,QACnB,gBAAgB,SAAS;AAAA,QACzB,QAAQ,SAAS,KAAK;AAAA,QACtB,cAAc;AAAA,QACd,YAAY,SAAS;AAAA,QACrB,WAAW;AAAA,QACX,eAAe,IAAI;AAAA,QACnB,gBAAgB,IAAI;AAAA,QACpB,UAAU,YAAY,YAAY;AAAA,MACpC,CAAC;AAAA,IACH;AAEA,UAAM,+BAA+B,SAAS,WAAW,SAAS,gBAAgB,SAAS,QAAQ;AACnG,WAAO,aAAa,KAAK,kBAAkB,QAAQ,SAAS,QAAQ,CAAC;AAAA,EACvE,SAAS,KAAK;AACZ,QAAI,gBAAgB,GAAG,GAAG;AACxB,aAAO,aAAa,KAAK,IAAI,MAAM,EAAE,QAAQ,IAAI,OAAO,CAAC;AAAA,IAC3D;AACA,UAAM,uBAAuB,mCAAmC,GAAG;AACnE,QAAI,sBAAsB;AACxB,aAAO,aAAa,KAAK,qBAAqB,MAAM,EAAE,QAAQ,qBAAqB,OAAO,CAAC;AAAA,IAC7F;AACA,WAAO,MAAM,uCAAuC,EAAE,IAAI,CAAC;AAC3D,WAAO,aAAa;AAAA,MAClB,EAAE,OAAO,SAAS,UAAU,kCAAkC,yCAAyC,EAAE;AAAA,MACzG,EAAE,QAAQ,IAAI;AAAA,IAChB;AAAA,EACF;AACF;AAEO,MAAM,UAA2B;AAAA,EACtC,KAAK;AAAA,EACL,SAAS;AAAA,EACT,SAAS;AAAA,IACP,KAAK;AAAA,MACH,SAAS;AAAA,MACT,aAAa;AAAA,MACb,WAAW;AAAA,QACT,EAAE,QAAQ,KAAK,aAAa,yBAAyB,QAAQ,uBAAuB;AAAA,MACtF;AAAA,MACA,QAAQ;AAAA,QACN,EAAE,QAAQ,KAAK,aAAa,6CAA6C,QAAQ,YAAY;AAAA,QAC7F,EAAE,QAAQ,KAAK,aAAa,gBAAgB,QAAQ,YAAY;AAAA,QAChE,EAAE,QAAQ,KAAK,aAAa,0BAA0B,QAAQ,YAAY;AAAA,MAC5E;AAAA,IACF;AAAA,IACA,KAAK;AAAA,MACH,SAAS;AAAA,MACT,aAAa;AAAA,MACb,aAAa;AAAA,QACX,aAAa;AAAA,QACb,QAAQ;AAAA,MACV;AAAA,MACA,WAAW;AAAA,QACT,EAAE,QAAQ,KAAK,aAAa,iCAAiC,QAAQ,uBAAuB;AAAA,MAC9F;AAAA,MACA,QAAQ;AAAA,QACN,EAAE,QAAQ,KAAK,aAAa,eAAe,QAAQ,YAAY;AAAA,QAC/D,EAAE,QAAQ,KAAK,aAAa,gBAAgB,QAAQ,YAAY;AAAA,QAChE,EAAE,QAAQ,KAAK,aAAa,qDAAqD,QAAQ,YAAY;AAAA,QACrG,EAAE,QAAQ,KAAK,aAAa,oBAAoB,QAAQ,YAAY;AAAA,MACtE;AAAA,IACF;AAAA,EACF;AACF;",
|
|
6
6
|
"names": []
|
|
7
7
|
}
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"version": 3,
|
|
3
3
|
"sources": ["../../../../../../src/modules/eudr/api/plots/import/route.ts"],
|
|
4
|
-
"sourcesContent": ["import { z } from 'zod'\nimport type { OpenApiRouteDoc } from '@open-mercato/shared/lib/openapi'\nimport { createRequestContainer } from '@open-mercato/shared/lib/di/container'\nimport { getAuthFromRequest } from '@open-mercato/shared/lib/auth/server'\nimport { resolveOrganizationScopeForRequest } from '@open-mercato/core/modules/directory/utils/organizationScope'\nimport type { CommandBus, CommandRuntimeContext } from '@open-mercato/shared/lib/commands'\nimport { CrudHttpError, isCrudHttpError } from '@open-mercato/shared/lib/crud/errors'\nimport { createLogger } from '@open-mercato/shared/lib/logger'\nimport { resolveTranslations } from '@open-mercato/shared/lib/i18n/server'\nimport { getAllMutationGuardInstances } from '@open-mercato/shared/lib/crud/mutation-guard-store'\nimport {\n bridgeLegacyGuard,\n runMutationGuards,\n type MutationGuard,\n type MutationGuardInput,\n} from '@open-mercato/shared/lib/crud/mutation-guard-registry'\nimport type { QueryEngine } from '@open-mercato/shared/lib/query/types'\nimport { E } from '#generated/entities.ids.generated'\nimport { collectFeatures } from '../../../lib/geometry'\nimport { plotCreateSchema } from '../../../data/validators'\n\nconst logger = createLogger('eudr').child({ component: 'api/plots/import' })\n\nexport const metadata = {\n POST: { requireAuth: true, requireFeatures: ['eudr.plots.manage'] },\n}\n\nconst importSchema = z.object({\n supplierEntityId: z.string().uuid(),\n defaultCountry: z.string().regex(/^[A-Za-z]{2}$/).transform((value) => value.toUpperCase()).optional(),\n featureCollection: z.unknown(),\n})\n\ntype PlotImportInput = z.infer<typeof importSchema>\ntype PlotImportFailure = { index: number; name: string; errorKey: string }\ntype RequestContext = { ctx: CommandRuntimeContext; organizationId: string; tenantId: string }\ntype CommandCreateResult = { entityId?: string; id?: string }\n\nfunction isRecord(value: unknown): value is Record<string, unknown> {\n return value !== null && typeof value === 'object' && !Array.isArray(value)\n}\n\nfunction optionalString(value: unknown): string | null {\n return typeof value === 'string' && value.trim().length > 0 ? value.trim() : null\n}\n\nfunction optionalNumber(value: unknown): number | null {\n return typeof value === 'number' && Number.isFinite(value) ? value : null\n}\n\nfunction resolveUserFeatures(auth: unknown): string[] {\n const features = (auth as { features?: unknown })?.features\n if (!Array.isArray(features)) return []\n return features.filter((value): value is string => typeof value === 'string')\n}\n\nasync function runGuards(\n ctx: CommandRuntimeContext,\n input: MutationGuardInput,\n): Promise<{\n ok: boolean\n errorBody?: Record<string, unknown>\n errorStatus?: number\n modifiedPayload?: Record<string, unknown>\n afterSuccessCallbacks: Array<{ guard: MutationGuard; metadata: Record<string, unknown> | null }>\n}> {\n const legacyGuard = bridgeLegacyGuard(ctx.container)\n const guards = [...getAllMutationGuardInstances(), ...(legacyGuard ? [legacyGuard] : [])]\n return runMutationGuards(guards, input, {\n userFeatures: resolveUserFeatures(ctx.auth),\n })\n}\n\nasync function runGuardAfterSuccessCallbacks(\n callbacks: Array<{ guard: MutationGuard; metadata: Record<string, unknown> | null }>,\n input: {\n tenantId: string\n organizationId: string\n userId: string\n resourceKind: string\n resourceId: string\n operation: 'create'\n requestMethod: string\n requestHeaders: Headers\n },\n): Promise<void> {\n for (const callback of callbacks) {\n if (!callback.guard.afterSuccess) continue\n try {\n await callback.guard.afterSuccess({\n ...input,\n metadata: callback.metadata ?? null,\n })\n } catch (err) {\n logger.warn('Mutation guard afterSuccess callback failed', { err })\n }\n }\n}\n\nasync function resolveRequestContext(req: Request): Promise<RequestContext> {\n const container = await createRequestContainer()\n const auth = await getAuthFromRequest(req)\n const { translate } = await resolveTranslations()\n\n if (!auth || !auth.tenantId) {\n throw new CrudHttpError(401, { error: translate('eudr.errors.unauthorized', 'Unauthorized') })\n }\n\n const scope = await resolveOrganizationScopeForRequest({ container, auth, request: req })\n const organizationId = scope?.selectedId ?? auth.orgId ?? null\n if (!organizationId) {\n throw new CrudHttpError(400, {\n error: translate('eudr.errors.organization_required', 'Organization context is required'),\n })\n }\n\n return {\n tenantId: auth.tenantId,\n organizationId,\n ctx: {\n container,\n auth,\n organizationScope: scope,\n selectedOrganizationId: organizationId,\n organizationIds: scope?.filterIds ?? (auth.orgId ? [auth.orgId] : null),\n request: req,\n },\n }\n}\n\nfunction featureProperties(feature: unknown): Record<string, unknown> {\n if (!isRecord(feature)) return {}\n const properties = feature.properties\n return isRecord(properties) ? properties : {}\n}\n\nfunction featureGeometryType(feature: unknown): string | null {\n if (!isRecord(feature)) return null\n if (feature.type === 'Feature' && isRecord(feature.geometry) && typeof feature.geometry.type === 'string') {\n return feature.geometry.type\n }\n return typeof feature.type === 'string' ? feature.type : null\n}\n\nfunction resolveFeatureName(properties: Record<string, unknown>, index: number): string {\n return optionalString(properties.name)\n ?? optionalString(properties.ProductionPlace)\n ?? `Plot ${index + 1}`\n}\n\nfunction errorKeyFromError(error: unknown): string {\n if (isCrudHttpError(error)) {\n const bodyError = error.body?.error\n return typeof bodyError === 'string' && bodyError.length > 0 ? bodyError : 'eudr.errors.plot_import_failed'\n }\n if (error instanceof z.ZodError) {\n const firstMessage = error.issues[0]?.message\n return typeof firstMessage === 'string' && firstMessage.length > 0 ? firstMessage : 'eudr.errors.plot_import_failed'\n }\n return 'eudr.errors.plot_import_failed'\n}\n\nasync function loadSupplierDisplayName(\n queryEngine: QueryEngine,\n scope: { tenantId: string; organizationId: string },\n supplierEntityId: string,\n): Promise<string | null> {\n // Soft cross-module read (FK-id convention) via the query engine \u2014 no direct\n // customers entity import; degrades to null when the peer is absent so the\n // import still succeeds without a snapshot.\n try {\n const customersEntityId = (E as Record<string, Record<string, string>>).customers?.customer_entity\n if (!customersEntityId) return null\n const result = await queryEngine.query<Record<string, unknown>>(customersEntityId, {\n fields: ['id', 'display_name'],\n filters: { id: { $in: [supplierEntityId] } },\n page: { page: 1, pageSize: 1 },\n tenantId: scope.tenantId,\n organizationId: scope.organizationId,\n })\n const rawName = result.items[0]?.display_name ?? result.items[0]?.displayName\n return typeof rawName === 'string' && rawName.length ? rawName : null\n } catch {\n return null\n }\n}\n\nasync function createPlotFromFeature(args: {\n commandBus: CommandBus\n ctx: CommandRuntimeContext\n scope: { tenantId: string; organizationId: string }\n input: PlotImportInput\n feature: unknown\n index: number\n supplierDisplayName: string | null\n}): Promise<{ id: string | null }> {\n const properties = featureProperties(args.feature)\n const name = resolveFeatureName(properties, args.index)\n const originCountry = optionalString(properties.ProducerCountry) ?? args.input.defaultCountry ?? null\n if (!originCountry) throw new CrudHttpError(400, { error: 'eudr.errors.importCountryMissing' })\n\n const area = optionalNumber(properties.Area)\n const base = {\n supplierEntityId: args.input.supplierEntityId,\n name,\n originCountry,\n geometry: args.feature,\n producerName: optionalString(properties.ProducerName),\n areaHa: featureGeometryType(args.feature) === 'Point' && area !== null ? area : undefined,\n supplierSnapshot: args.supplierDisplayName ? { displayName: args.supplierDisplayName } : undefined,\n }\n const parsed = plotCreateSchema.parse(base)\n const { result } = await args.commandBus.execute<typeof parsed & { tenantId: string; organizationId: string }, CommandCreateResult>(\n 'eudr.plots.create',\n {\n input: {\n ...parsed,\n tenantId: args.scope.tenantId,\n organizationId: args.scope.organizationId,\n },\n ctx: args.ctx,\n metadata: {\n tenantId: args.scope.tenantId,\n organizationId: args.scope.organizationId,\n resourceKind: 'eudr.plot',\n context: { cacheAliases: ['eudr.plot'] },\n },\n },\n )\n return { id: result?.entityId ?? result?.id ?? null }\n}\n\nexport async function POST(req: Request) {\n try {\n const requestContext = await resolveRequestContext(req)\n const { translate } = await resolveTranslations()\n const payload = await req.json().catch(() => ({}))\n const input = importSchema.parse(payload)\n const guardResult = await runGuards(requestContext.ctx, {\n tenantId: requestContext.tenantId,\n organizationId: requestContext.organizationId,\n userId: requestContext.ctx.auth?.sub ?? '',\n resourceKind: 'eudr.plot',\n resourceId: null,\n operation: 'create',\n requestMethod: req.method,\n requestHeaders: req.headers,\n mutationPayload: {\n supplierEntityId: input.supplierEntityId,\n defaultCountry: input.defaultCountry ?? null,\n },\n })\n if (!guardResult.ok) {\n return Response.json(\n guardResult.errorBody ?? { error: translate('eudr.errors.operation_blocked', 'Operation blocked by guard') },\n { status: guardResult.errorStatus ?? 422 },\n )\n }\n const guardPatch = guardResult.modifiedPayload ?? null\n const effectiveSupplierEntityId = typeof guardPatch?.supplierEntityId === 'string' && guardPatch.supplierEntityId.length\n ? guardPatch.supplierEntityId\n : input.supplierEntityId\n const effectiveDefaultCountry = typeof guardPatch?.defaultCountry === 'string' && guardPatch.defaultCountry.length\n ? guardPatch.defaultCountry\n : input.defaultCountry\n\n const featureResult = collectFeatures(input.featureCollection)\n if (!featureResult.ok) {\n return Response.json({ errorKey: featureResult.errorKey }, { status: 400 })\n }\n\n const commandBus = requestContext.ctx.container.resolve('commandBus') as CommandBus\n const failed: PlotImportFailure[] = []\n const createdIds: string[] = []\n const effectiveInput: PlotImportInput = {\n ...input,\n supplierEntityId: effectiveSupplierEntityId,\n defaultCountry: effectiveDefaultCountry,\n }\n\n const importScope = { tenantId: requestContext.tenantId, organizationId: requestContext.organizationId }\n const supplierDisplayName = await loadSupplierDisplayName(\n requestContext.ctx.container.resolve('queryEngine') as QueryEngine,\n importScope,\n effectiveSupplierEntityId,\n )\n\n for (const [index, feature] of featureResult.features.entries()) {\n const name = resolveFeatureName(featureProperties(feature), index)\n try {\n const created = await createPlotFromFeature({\n commandBus,\n ctx: requestContext.ctx,\n scope: importScope,\n input: effectiveInput,\n feature,\n index,\n supplierDisplayName,\n })\n if (created.id) createdIds.push(created.id)\n } catch (error) {\n failed.push({ index, name, errorKey: errorKeyFromError(error) })\n }\n }\n\n if (guardResult.afterSuccessCallbacks.length) {\n for (const id of createdIds) {\n await runGuardAfterSuccessCallbacks(guardResult.afterSuccessCallbacks, {\n tenantId: requestContext.tenantId,\n organizationId: requestContext.organizationId,\n userId: requestContext.ctx.auth?.sub ?? '',\n resourceKind: 'eudr.plot',\n resourceId: id,\n operation: 'create',\n requestMethod: req.method,\n requestHeaders: req.headers,\n })\n }\n }\n\n return Response.json({ created: createdIds.length, failed })\n } catch (error) {\n if (isCrudHttpError(error)) {\n return Response.json(error.body, { status: error.status })\n }\n const { translate } = await resolveTranslations()\n if (error instanceof z.ZodError) {\n return Response.json(\n { error: translate('eudr.errors.plot_import_failed', 'Failed to import plots') },\n { status: 400 },\n )\n }\n logger.error('Plot import failed', { err: error })\n return Response.json(\n { error: translate('eudr.errors.plot_import_failed', 'Failed to import plots') },\n { status: 500 },\n )\n }\n}\n\nconst importResponseSchema = z.object({\n created: z.number(),\n failed: z.array(z.object({\n index: z.number(),\n name: z.string(),\n errorKey: z.string(),\n })),\n})\n\nexport const openApi: OpenApiRouteDoc = {\n tag: 'EUDR',\n summary: 'Import EUDR plots from GeoJSON',\n methods: {\n POST: {\n summary: 'Import plots',\n requestBody: {\n contentType: 'application/json',\n schema: importSchema,\n },\n responses: [\n { status: 200, description: 'Import result', schema: importResponseSchema },\n { status: 400, description: 'Invalid import payload', schema: z.object({ error: z.string().optional(), errorKey: z.string().optional() }) },\n { status: 401, description: 'Unauthorized', schema: z.object({ error: z.string() }) },\n { status: 403, description: 'Forbidden', schema: z.object({ error: z.string() }) },\n { status: 500, description: 'Plot import failed', schema: z.object({ error: z.string() }) },\n ],\n },\n },\n}\n"],
|
|
5
|
-
"mappings": "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
|
|
4
|
+
"sourcesContent": ["import { z } from 'zod'\nimport type { OpenApiRouteDoc } from '@open-mercato/shared/lib/openapi'\nimport { createRequestContainer } from '@open-mercato/shared/lib/di/container'\nimport { getAuthFromRequest } from '@open-mercato/shared/lib/auth/server'\nimport { resolveOrganizationScopeForRequest } from '@open-mercato/core/modules/directory/utils/organizationScope'\nimport type { CommandBus, CommandRuntimeContext } from '@open-mercato/shared/lib/commands'\nimport { CrudHttpError, isCrudHttpError } from '@open-mercato/shared/lib/crud/errors'\nimport { createLogger } from '@open-mercato/shared/lib/logger'\nimport { resolveTranslations } from '@open-mercato/shared/lib/i18n/server'\nimport { getAllMutationGuardInstances } from '@open-mercato/shared/lib/crud/mutation-guard-store'\nimport {\n bridgeLegacyGuard,\n runMutationGuards,\n type MutationGuard,\n type MutationGuardInput,\n} from '@open-mercato/shared/lib/crud/mutation-guard-registry'\nimport type { QueryEngine } from '@open-mercato/shared/lib/query/types'\nimport { E } from '#generated/entities.ids.generated'\nimport { collectFeatures } from '../../../lib/geometry'\nimport { plotCreateSchema } from '../../../data/validators'\n\nconst logger = createLogger('eudr').child({ component: 'api/plots/import' })\n\nexport const metadata = {\n POST: { requireAuth: true, requireFeatures: ['eudr.plots.manage'] },\n}\n\nconst importSchema = z.object({\n supplierEntityId: z.string().uuid(),\n defaultCountry: z.string().regex(/^[A-Za-z]{2}$/).transform((value) => value.toUpperCase()).optional(),\n featureCollection: z.unknown(),\n})\n\ntype PlotImportInput = z.infer<typeof importSchema>\ntype PlotImportFailure = { index: number; name: string; errorKey: string }\ntype RequestContext = { ctx: CommandRuntimeContext; organizationId: string; tenantId: string }\ntype CommandCreateResult = { entityId?: string; id?: string }\n\nfunction isRecord(value: unknown): value is Record<string, unknown> {\n return value !== null && typeof value === 'object' && !Array.isArray(value)\n}\n\nfunction optionalString(value: unknown): string | null {\n return typeof value === 'string' && value.trim().length > 0 ? value.trim() : null\n}\n\nfunction optionalNumber(value: unknown): number | null {\n return typeof value === 'number' && Number.isFinite(value) ? value : null\n}\n\nfunction resolveUserFeatures(auth: unknown): string[] {\n const features = (auth as { features?: unknown })?.features\n if (!Array.isArray(features)) return []\n return features.filter((value): value is string => typeof value === 'string')\n}\n\nasync function runGuards(\n ctx: CommandRuntimeContext,\n input: MutationGuardInput,\n): Promise<{\n ok: boolean\n errorBody?: Record<string, unknown>\n errorStatus?: number\n modifiedPayload?: Record<string, unknown>\n afterSuccessCallbacks: Array<{ guard: MutationGuard; metadata: Record<string, unknown> | null }>\n}> {\n const legacyGuard = bridgeLegacyGuard(ctx.container)\n const guards = [...getAllMutationGuardInstances(), ...(legacyGuard ? [legacyGuard] : [])]\n return runMutationGuards(guards, input, {\n userFeatures: resolveUserFeatures(ctx.auth),\n })\n}\n\nasync function runGuardAfterSuccessCallbacks(\n callbacks: Array<{ guard: MutationGuard; metadata: Record<string, unknown> | null }>,\n input: {\n tenantId: string\n organizationId: string\n userId: string\n resourceKind: string\n resourceId: string\n operation: 'create'\n requestMethod: string\n requestHeaders: Headers\n },\n): Promise<void> {\n for (const callback of callbacks) {\n if (!callback.guard.afterSuccess) continue\n try {\n await callback.guard.afterSuccess({\n ...input,\n metadata: callback.metadata ?? null,\n })\n } catch (err) {\n logger.warn('Mutation guard afterSuccess callback failed', { err })\n }\n }\n}\n\nasync function resolveRequestContext(req: Request): Promise<RequestContext> {\n const container = await createRequestContainer()\n const auth = await getAuthFromRequest(req)\n const { translate } = await resolveTranslations()\n\n if (!auth || !auth.tenantId) {\n throw new CrudHttpError(401, { error: translate('eudr.errors.unauthorized', 'Unauthorized') })\n }\n\n const scope = await resolveOrganizationScopeForRequest({ container, auth, request: req })\n const organizationId = scope?.selectedId ?? auth.orgId ?? null\n if (!organizationId) {\n throw new CrudHttpError(400, {\n error: translate('eudr.errors.organization_required', 'Organization context is required'),\n })\n }\n\n return {\n tenantId: auth.tenantId,\n organizationId,\n ctx: {\n container,\n auth,\n organizationScope: scope,\n selectedOrganizationId: organizationId,\n organizationIds: scope?.filterIds ?? (auth.orgId ? [auth.orgId] : null),\n request: req,\n },\n }\n}\n\nfunction featureProperties(feature: unknown): Record<string, unknown> {\n if (!isRecord(feature)) return {}\n const properties = feature.properties\n return isRecord(properties) ? properties : {}\n}\n\nfunction featureGeometryType(feature: unknown): string | null {\n if (!isRecord(feature)) return null\n if (feature.type === 'Feature' && isRecord(feature.geometry) && typeof feature.geometry.type === 'string') {\n return feature.geometry.type\n }\n return typeof feature.type === 'string' ? feature.type : null\n}\n\nfunction resolveFeatureName(properties: Record<string, unknown>, index: number): string {\n return optionalString(properties.name)\n ?? optionalString(properties.ProductionPlace)\n ?? `Plot ${index + 1}`\n}\n\nfunction errorKeyFromError(error: unknown): string {\n if (isCrudHttpError(error)) {\n const bodyError = error.body?.error\n return typeof bodyError === 'string' && bodyError.length > 0 ? bodyError : 'eudr.errors.plot_import_failed'\n }\n if (error instanceof z.ZodError) {\n const firstMessage = error.issues[0]?.message\n return typeof firstMessage === 'string' && firstMessage.length > 0 ? firstMessage : 'eudr.errors.plot_import_failed'\n }\n return 'eudr.errors.plot_import_failed'\n}\n\nasync function loadSupplierDisplayName(\n queryEngine: QueryEngine,\n scope: { tenantId: string; organizationId: string },\n supplierEntityId: string,\n): Promise<string | null> {\n // Soft cross-module read (FK-id convention) via the query engine \u2014 no direct\n // customers entity import; degrades to null when the peer is absent so the\n // import still succeeds without a snapshot.\n try {\n const customersEntityId = (E as Record<string, Record<string, string>>).customers?.customer_entity\n if (!customersEntityId) return null\n const result = await queryEngine.query<Record<string, unknown>>(customersEntityId, {\n fields: ['id', 'display_name'],\n filters: { id: { $in: [supplierEntityId] } },\n page: { page: 1, pageSize: 1 },\n tenantId: scope.tenantId,\n organizationId: scope.organizationId,\n })\n const rawName = result.items[0]?.display_name ?? result.items[0]?.displayName\n return typeof rawName === 'string' && rawName.length ? rawName : null\n } catch {\n return null\n }\n}\n\nasync function createPlotFromFeature(args: {\n commandBus: CommandBus\n ctx: CommandRuntimeContext\n scope: { tenantId: string; organizationId: string }\n input: PlotImportInput\n feature: unknown\n index: number\n supplierDisplayName: string | null\n}): Promise<{ id: string | null }> {\n const properties = featureProperties(args.feature)\n const name = resolveFeatureName(properties, args.index)\n const originCountry = optionalString(properties.ProducerCountry) ?? args.input.defaultCountry ?? null\n if (!originCountry) throw new CrudHttpError(400, { error: 'eudr.errors.importCountryMissing' })\n\n const area = optionalNumber(properties.Area)\n const base = {\n supplierEntityId: args.input.supplierEntityId,\n name,\n originCountry,\n geometry: args.feature,\n producerName: optionalString(properties.ProducerName),\n areaHa: featureGeometryType(args.feature) === 'Point' && area !== null ? area : undefined,\n supplierSnapshot: args.supplierDisplayName ? { displayName: args.supplierDisplayName } : undefined,\n }\n const parsed = plotCreateSchema.parse(base)\n const { result } = await args.commandBus.execute<typeof parsed & { tenantId: string; organizationId: string }, CommandCreateResult>(\n 'eudr.plots.create',\n {\n input: {\n ...parsed,\n tenantId: args.scope.tenantId,\n organizationId: args.scope.organizationId,\n },\n ctx: args.ctx,\n metadata: {\n tenantId: args.scope.tenantId,\n organizationId: args.scope.organizationId,\n resourceKind: 'eudr.plot',\n context: { cacheAliases: ['eudr.plot'] },\n },\n },\n )\n return { id: result?.entityId ?? result?.id ?? null }\n}\n\nexport async function POST(req: Request) {\n try {\n const requestContext = await resolveRequestContext(req)\n const { translate } = await resolveTranslations()\n const payload = await req.json().catch(() => ({}))\n const input = importSchema.parse(payload)\n const guardResult = await runGuards(requestContext.ctx, {\n tenantId: requestContext.tenantId,\n organizationId: requestContext.organizationId,\n userId: requestContext.ctx.auth?.sub ?? '',\n resourceKind: 'eudr.plot',\n resourceId: null,\n operation: 'create',\n requestMethod: req.method,\n requestHeaders: req.headers,\n mutationPayload: {\n supplierEntityId: input.supplierEntityId,\n defaultCountry: input.defaultCountry ?? null,\n },\n })\n if (!guardResult.ok) {\n return Response.json(\n guardResult.errorBody ?? { error: translate('eudr.errors.operation_blocked', 'Operation blocked by guard') },\n { status: guardResult.errorStatus ?? 422 },\n )\n }\n const guardPatch = guardResult.modifiedPayload ?? null\n const effectiveSupplierEntityId = typeof guardPatch?.supplierEntityId === 'string' && guardPatch.supplierEntityId.length\n ? guardPatch.supplierEntityId\n : input.supplierEntityId\n const effectiveDefaultCountry = typeof guardPatch?.defaultCountry === 'string' && guardPatch.defaultCountry.length\n ? guardPatch.defaultCountry\n : input.defaultCountry\n\n const featureResult = collectFeatures(input.featureCollection)\n if (!featureResult.ok) {\n return Response.json({ errorKey: featureResult.errorKey }, { status: 400 })\n }\n\n const commandBus = requestContext.ctx.container.resolve('commandBus') as CommandBus\n const failed: PlotImportFailure[] = []\n const createdIds: string[] = []\n const effectiveInput: PlotImportInput = {\n ...input,\n supplierEntityId: effectiveSupplierEntityId,\n defaultCountry: effectiveDefaultCountry,\n }\n\n const importScope = { tenantId: requestContext.tenantId, organizationId: requestContext.organizationId }\n const supplierDisplayName = await loadSupplierDisplayName(\n requestContext.ctx.container.resolve('queryEngine') as QueryEngine,\n importScope,\n effectiveSupplierEntityId,\n )\n\n for (const [index, feature] of featureResult.features.entries()) {\n const name = resolveFeatureName(featureProperties(feature), index)\n try {\n const created = await createPlotFromFeature({\n commandBus,\n ctx: requestContext.ctx,\n scope: importScope,\n input: effectiveInput,\n feature,\n index,\n supplierDisplayName,\n })\n if (created.id) createdIds.push(created.id)\n } catch (error) {\n failed.push({ index, name, errorKey: errorKeyFromError(error) })\n }\n }\n\n if (guardResult.afterSuccessCallbacks.length) {\n for (const id of createdIds) {\n await runGuardAfterSuccessCallbacks(guardResult.afterSuccessCallbacks, {\n tenantId: requestContext.tenantId,\n organizationId: requestContext.organizationId,\n userId: requestContext.ctx.auth?.sub ?? '',\n resourceKind: 'eudr.plot',\n resourceId: id,\n operation: 'create',\n requestMethod: req.method,\n requestHeaders: req.headers,\n })\n }\n }\n\n return Response.json({ created: createdIds.length, failed })\n } catch (error) {\n if (isCrudHttpError(error)) {\n return Response.json(error.body, { status: error.status })\n }\n // No command-interceptor mapping here (issue #5097): every `commandBus.execute`\n // in this route runs inside the per-item loop above, whose catch records the\n // failure in `failed[]` and continues, so an interceptor rejection can never\n // reach this handler. The batch contract \u2014 HTTP 200 with a per-item failure\n // list \u2014 is deliberately unchanged; the route is listed in\n // `ROUTES_WITH_BATCH_ITEM_ERROR_HANDLING` in\n // `packages/core/src/__tests__/command-interceptor-http-coverage.test.ts`.\n const { translate } = await resolveTranslations()\n if (error instanceof z.ZodError) {\n return Response.json(\n { error: translate('eudr.errors.plot_import_failed', 'Failed to import plots') },\n { status: 400 },\n )\n }\n logger.error('Plot import failed', { err: error })\n return Response.json(\n { error: translate('eudr.errors.plot_import_failed', 'Failed to import plots') },\n { status: 500 },\n )\n }\n}\n\nconst importResponseSchema = z.object({\n created: z.number(),\n failed: z.array(z.object({\n index: z.number(),\n name: z.string(),\n errorKey: z.string(),\n })),\n})\n\nexport const openApi: OpenApiRouteDoc = {\n tag: 'EUDR',\n summary: 'Import EUDR plots from GeoJSON',\n methods: {\n POST: {\n summary: 'Import plots',\n requestBody: {\n contentType: 'application/json',\n schema: importSchema,\n },\n responses: [\n { status: 200, description: 'Import result', schema: importResponseSchema },\n { status: 400, description: 'Invalid import payload', schema: z.object({ error: z.string().optional(), errorKey: z.string().optional() }) },\n { status: 401, description: 'Unauthorized', schema: z.object({ error: z.string() }) },\n { status: 403, description: 'Forbidden', schema: z.object({ error: z.string() }) },\n { status: 500, description: 'Plot import failed', schema: z.object({ error: z.string() }) },\n ],\n },\n },\n}\n"],
|
|
5
|
+
"mappings": "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",
|
|
6
6
|
"names": []
|
|
7
7
|
}
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"version": 3,
|
|
3
3
|
"sources": ["../../../../../../../src/modules/eudr/api/product-mappings/suggestions/apply/route.ts"],
|
|
4
|
-
"sourcesContent": ["import { z } from 'zod'\nimport type { OpenApiRouteDoc } from '@open-mercato/shared/lib/openapi'\nimport { createRequestContainer } from '@open-mercato/shared/lib/di/container'\nimport { getAuthFromRequest } from '@open-mercato/shared/lib/auth/server'\nimport { resolveOrganizationScopeForRequest } from '@open-mercato/core/modules/directory/utils/organizationScope'\nimport type { CommandBus, CommandRuntimeContext } from '@open-mercato/shared/lib/commands'\nimport { CrudHttpError, isCrudHttpError } from '@open-mercato/shared/lib/crud/errors'\nimport { createLogger } from '@open-mercato/shared/lib/logger'\nimport { resolveTranslations } from '@open-mercato/shared/lib/i18n/server'\nimport { getAllMutationGuardInstances } from '@open-mercato/shared/lib/crud/mutation-guard-store'\nimport {\n bridgeLegacyGuard,\n runMutationGuards,\n type MutationGuard,\n type MutationGuardInput,\n} from '@open-mercato/shared/lib/crud/mutation-guard-registry'\nimport type { QueryEngine } from '@open-mercato/shared/lib/query/types'\nimport { E } from '#generated/entities.ids.generated'\nimport {\n EUDR_COMMODITIES,\n productMappingCreateSchema,\n} from '../../../../data/validators'\nimport { suggestCommodityForHsCode } from '../../../../lib/reference-data'\n\nconst logger = createLogger('eudr').child({ component: 'api/product-mappings/suggestions/apply' })\n\nconst CATALOG_LOOKUP_PAGE_SIZE = 100\n\nexport const metadata = {\n POST: { requireAuth: true, requireFeatures: ['eudr.mappings.manage'] },\n}\n\nconst applyItemSchema = z.object({\n productId: z.string().uuid(),\n commodity: z.enum(EUDR_COMMODITIES),\n hsCode: z.string().max(20).optional().nullable(),\n})\n\nconst applySchema = z\n .object({\n items: z.array(applyItemSchema).max(100).optional(),\n productIds: z.array(z.string().uuid()).max(100).optional(),\n })\n .superRefine((value, context) => {\n if ((value.items?.length ?? 0) > 0 || (value.productIds?.length ?? 0) > 0) return\n context.addIssue({\n code: z.ZodIssueCode.custom,\n message: 'eudr.errors.mapping_suggestions_apply_failed',\n })\n })\n\ntype ApplyItem = z.infer<typeof applyItemSchema>\ntype ApplyFailure = { productId: string; errorKey: string }\ntype ProductSnapshot = { name?: string | null; sku?: string | null }\ntype CatalogProductInfo = { snapshot: ProductSnapshot | null; hsCode: string | null }\ntype CatalogProductRecord = Record<string, unknown> & {\n id?: unknown\n title?: unknown\n name?: unknown\n sku?: unknown\n hs_code?: unknown\n}\ntype RequestContext = {\n ctx: CommandRuntimeContext\n tenantId: string\n organizationId: string\n}\ntype CommandCreateResult = { entityId?: string; id?: string }\n\nfunction readString(value: unknown): string | null {\n return typeof value === 'string' && value.trim().length > 0 ? value.trim() : null\n}\n\nfunction resolveUserFeatures(auth: unknown): string[] {\n const features = (auth as { features?: unknown })?.features\n if (!Array.isArray(features)) return []\n return features.filter((value): value is string => typeof value === 'string')\n}\n\nasync function runGuards(\n ctx: CommandRuntimeContext,\n input: MutationGuardInput,\n): Promise<{\n ok: boolean\n errorBody?: Record<string, unknown>\n errorStatus?: number\n modifiedPayload?: Record<string, unknown>\n afterSuccessCallbacks: Array<{ guard: MutationGuard; metadata: Record<string, unknown> | null }>\n}> {\n const legacyGuard = bridgeLegacyGuard(ctx.container)\n const guards = [...getAllMutationGuardInstances(), ...(legacyGuard ? [legacyGuard] : [])]\n return runMutationGuards(guards, input, {\n userFeatures: resolveUserFeatures(ctx.auth),\n })\n}\n\nasync function runGuardAfterSuccessCallbacks(\n callbacks: Array<{ guard: MutationGuard; metadata: Record<string, unknown> | null }>,\n input: {\n tenantId: string\n organizationId: string\n userId: string\n resourceKind: string\n resourceId: string\n operation: 'create'\n requestMethod: string\n requestHeaders: Headers\n },\n): Promise<void> {\n for (const callback of callbacks) {\n if (!callback.guard.afterSuccess) continue\n try {\n await callback.guard.afterSuccess({\n ...input,\n metadata: callback.metadata ?? null,\n })\n } catch (err) {\n logger.warn('Mutation guard afterSuccess callback failed', { err })\n }\n }\n}\n\nasync function resolveRequestContext(req: Request): Promise<RequestContext> {\n const container = await createRequestContainer()\n const auth = await getAuthFromRequest(req)\n const { translate } = await resolveTranslations()\n\n if (!auth || !auth.tenantId) {\n throw new CrudHttpError(401, { error: translate('eudr.errors.unauthorized', 'Unauthorized') })\n }\n\n const scope = await resolveOrganizationScopeForRequest({ container, auth, request: req })\n const organizationId = scope?.selectedId ?? auth.orgId ?? null\n if (!organizationId) {\n throw new CrudHttpError(400, {\n error: translate('eudr.errors.organization_required', 'Organization context is required'),\n })\n }\n\n return {\n tenantId: auth.tenantId,\n organizationId,\n ctx: {\n container,\n auth,\n organizationScope: scope,\n selectedOrganizationId: organizationId,\n organizationIds: scope?.filterIds ?? (auth.orgId ? [auth.orgId] : null),\n request: req,\n },\n }\n}\n\nasync function loadCatalogProductInfo(ctx: RequestContext, productIds: string[]): Promise<Map<string, CatalogProductInfo>> {\n const info = new Map<string, CatalogProductInfo>()\n for (const productId of productIds) info.set(productId, { snapshot: null, hsCode: null })\n if (!productIds.length) return info\n\n try {\n const queryEngine = ctx.ctx.container.resolve('queryEngine') as QueryEngine\n // `items` and `productIds` are capped at 100 each, so their union can reach\n // 200 \u2014 chunk the lookup to keep every page within the pageSize <= 100 rule.\n const chunks: string[][] = []\n for (let index = 0; index < productIds.length; index += CATALOG_LOOKUP_PAGE_SIZE) {\n chunks.push(productIds.slice(index, index + CATALOG_LOOKUP_PAGE_SIZE))\n }\n const products: CatalogProductRecord[] = []\n for (const chunk of chunks) {\n const result = await queryEngine.query<CatalogProductRecord>(E.catalog.catalog_product, {\n fields: ['id', 'title', 'name', 'sku', 'hs_code'],\n filters: { id: { $in: chunk } },\n page: { page: 1, pageSize: chunk.length },\n tenantId: ctx.tenantId,\n organizationId: ctx.organizationId,\n })\n products.push(...result.items)\n }\n for (const product of products) {\n const id = readString(product.id)\n if (!id) continue\n info.set(id, {\n snapshot: {\n name: readString(product.title) ?? readString(product.name),\n sku: readString(product.sku),\n },\n hsCode: readString(product.hs_code),\n })\n }\n } catch {\n return info\n }\n\n return info\n}\n\nfunction resolveApplyItems(\n input: z.infer<typeof applySchema>,\n catalogInfo: Map<string, CatalogProductInfo>,\n): { items: ApplyItem[]; failed: ApplyFailure[] } {\n const items: ApplyItem[] = [...(input.items ?? [])]\n const failed: ApplyFailure[] = []\n const explicitIds = new Set(items.map((item) => item.productId))\n\n for (const productId of input.productIds ?? []) {\n if (explicitIds.has(productId)) continue\n explicitIds.add(productId)\n const hsCode = catalogInfo.get(productId)?.hsCode ?? null\n const suggestedCommodity = suggestCommodityForHsCode(hsCode)\n if (!hsCode || !suggestedCommodity) {\n failed.push({ productId, errorKey: 'eudr.errors.mapping_suggestions_apply_failed' })\n continue\n }\n items.push({ productId, commodity: suggestedCommodity, hsCode })\n }\n\n return { items, failed }\n}\n\nfunction errorKeyFromError(error: unknown): string {\n if (isCrudHttpError(error)) {\n const bodyError = error.body?.error\n return typeof bodyError === 'string' && bodyError.length > 0 ? bodyError : 'eudr.errors.mapping_suggestions_apply_failed'\n }\n if (error instanceof z.ZodError) {\n const firstMessage = error.issues[0]?.message\n return typeof firstMessage === 'string' && firstMessage.length > 0 ? firstMessage : 'eudr.errors.mapping_suggestions_apply_failed'\n }\n return 'eudr.errors.mapping_suggestions_apply_failed'\n}\n\nasync function createMapping(args: {\n commandBus: CommandBus\n ctx: RequestContext\n item: ApplyItem\n productSnapshot: ProductSnapshot | null\n}): Promise<string | null> {\n const parsed = productMappingCreateSchema.parse({\n productId: args.item.productId,\n commodity: args.item.commodity,\n hsCode: args.item.hsCode ?? null,\n productSnapshot: args.productSnapshot,\n })\n const { result } = await args.commandBus.execute<typeof parsed & { tenantId: string; organizationId: string }, CommandCreateResult>(\n 'eudr.product_mappings.create',\n {\n input: {\n ...parsed,\n tenantId: args.ctx.tenantId,\n organizationId: args.ctx.organizationId,\n },\n ctx: args.ctx.ctx,\n metadata: {\n tenantId: args.ctx.tenantId,\n organizationId: args.ctx.organizationId,\n resourceKind: 'eudr.product_mapping',\n context: { cacheAliases: ['eudr.product_mapping'] },\n },\n },\n )\n return result?.entityId ?? result?.id ?? null\n}\n\nexport async function POST(req: Request) {\n try {\n const requestContext = await resolveRequestContext(req)\n const { translate } = await resolveTranslations()\n const payload = await req.json().catch(() => ({}))\n const input = applySchema.parse(payload)\n const catalogInfo = await loadCatalogProductInfo(\n requestContext,\n Array.from(new Set([\n ...(input.items ?? []).map((item) => item.productId),\n ...(input.productIds ?? []),\n ])),\n )\n const resolved = resolveApplyItems(input, catalogInfo)\n const guardResult = await runGuards(requestContext.ctx, {\n tenantId: requestContext.tenantId,\n organizationId: requestContext.organizationId,\n userId: requestContext.ctx.auth?.sub ?? '',\n resourceKind: 'eudr.product_mapping',\n resourceId: null,\n operation: 'create',\n requestMethod: req.method,\n requestHeaders: req.headers,\n mutationPayload: { items: resolved.items },\n })\n if (!guardResult.ok) {\n return Response.json(\n guardResult.errorBody ?? { error: translate('eudr.errors.operation_blocked', 'Operation blocked by guard') },\n { status: guardResult.errorStatus ?? 422 },\n )\n }\n\n const commandBus = requestContext.ctx.container.resolve('commandBus') as CommandBus\n const failed: ApplyFailure[] = [...resolved.failed]\n const createdIds: string[] = []\n\n const guardItems = Array.isArray((guardResult.modifiedPayload as { items?: unknown } | undefined)?.items)\n ? ((guardResult.modifiedPayload as { items: unknown[] }).items.filter((entry): entry is typeof resolved.items[number] => (\n typeof (entry as { productId?: unknown })?.productId === 'string'\n && typeof (entry as { commodity?: unknown })?.commodity === 'string'\n )))\n : resolved.items\n\n for (const item of guardItems) {\n try {\n const id = await createMapping({\n commandBus,\n ctx: requestContext,\n item,\n productSnapshot: catalogInfo.get(item.productId)?.snapshot ?? null,\n })\n if (id) createdIds.push(id)\n } catch (error) {\n failed.push({ productId: item.productId, errorKey: errorKeyFromError(error) })\n }\n }\n\n if (guardResult.afterSuccessCallbacks.length) {\n for (const id of createdIds) {\n await runGuardAfterSuccessCallbacks(guardResult.afterSuccessCallbacks, {\n tenantId: requestContext.tenantId,\n organizationId: requestContext.organizationId,\n userId: requestContext.ctx.auth?.sub ?? '',\n resourceKind: 'eudr.product_mapping',\n resourceId: id,\n operation: 'create',\n requestMethod: req.method,\n requestHeaders: req.headers,\n })\n }\n }\n\n return Response.json({ created: createdIds.length, failed })\n } catch (error) {\n if (isCrudHttpError(error)) {\n return Response.json(error.body, { status: error.status })\n }\n const { translate } = await resolveTranslations()\n if (error instanceof z.ZodError) {\n return Response.json(\n { error: translate('eudr.errors.mapping_suggestions_apply_failed', 'Failed to apply EUDR mapping suggestions') },\n { status: 400 },\n )\n }\n logger.error('Product mapping suggestions apply failed', { err: error })\n return Response.json(\n { error: translate('eudr.errors.mapping_suggestions_apply_failed', 'Failed to apply EUDR mapping suggestions') },\n { status: 500 },\n )\n }\n}\n\nconst applyResponseSchema = z.object({\n created: z.number(),\n failed: z.array(z.object({\n productId: z.string().uuid(),\n errorKey: z.string(),\n })),\n})\n\nexport const openApi: OpenApiRouteDoc = {\n tag: 'EUDR',\n summary: 'Apply EUDR product mapping suggestions',\n methods: {\n POST: {\n summary: 'Apply mapping suggestions',\n requestBody: {\n contentType: 'application/json',\n schema: applySchema,\n },\n responses: [\n { status: 200, description: 'Apply result', schema: applyResponseSchema },\n { status: 400, description: 'Invalid apply payload', schema: z.object({ error: z.string() }) },\n { status: 401, description: 'Unauthorized', schema: z.object({ error: z.string() }) },\n { status: 403, description: 'Forbidden', schema: z.object({ error: z.string() }) },\n { status: 500, description: 'Mapping suggestion apply failed', schema: z.object({ error: z.string() }) },\n ],\n },\n },\n}\n"],
|
|
5
|
-
"mappings": "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
|
|
4
|
+
"sourcesContent": ["import { z } from 'zod'\nimport type { OpenApiRouteDoc } from '@open-mercato/shared/lib/openapi'\nimport { createRequestContainer } from '@open-mercato/shared/lib/di/container'\nimport { getAuthFromRequest } from '@open-mercato/shared/lib/auth/server'\nimport { resolveOrganizationScopeForRequest } from '@open-mercato/core/modules/directory/utils/organizationScope'\nimport type { CommandBus, CommandRuntimeContext } from '@open-mercato/shared/lib/commands'\nimport { CrudHttpError, isCrudHttpError } from '@open-mercato/shared/lib/crud/errors'\nimport { createLogger } from '@open-mercato/shared/lib/logger'\nimport { resolveTranslations } from '@open-mercato/shared/lib/i18n/server'\nimport { getAllMutationGuardInstances } from '@open-mercato/shared/lib/crud/mutation-guard-store'\nimport {\n bridgeLegacyGuard,\n runMutationGuards,\n type MutationGuard,\n type MutationGuardInput,\n} from '@open-mercato/shared/lib/crud/mutation-guard-registry'\nimport type { QueryEngine } from '@open-mercato/shared/lib/query/types'\nimport { E } from '#generated/entities.ids.generated'\nimport {\n EUDR_COMMODITIES,\n productMappingCreateSchema,\n} from '../../../../data/validators'\nimport { suggestCommodityForHsCode } from '../../../../lib/reference-data'\n\nconst logger = createLogger('eudr').child({ component: 'api/product-mappings/suggestions/apply' })\n\nconst CATALOG_LOOKUP_PAGE_SIZE = 100\n\nexport const metadata = {\n POST: { requireAuth: true, requireFeatures: ['eudr.mappings.manage'] },\n}\n\nconst applyItemSchema = z.object({\n productId: z.string().uuid(),\n commodity: z.enum(EUDR_COMMODITIES),\n hsCode: z.string().max(20).optional().nullable(),\n})\n\nconst applySchema = z\n .object({\n items: z.array(applyItemSchema).max(100).optional(),\n productIds: z.array(z.string().uuid()).max(100).optional(),\n })\n .superRefine((value, context) => {\n if ((value.items?.length ?? 0) > 0 || (value.productIds?.length ?? 0) > 0) return\n context.addIssue({\n code: z.ZodIssueCode.custom,\n message: 'eudr.errors.mapping_suggestions_apply_failed',\n })\n })\n\ntype ApplyItem = z.infer<typeof applyItemSchema>\ntype ApplyFailure = { productId: string; errorKey: string }\ntype ProductSnapshot = { name?: string | null; sku?: string | null }\ntype CatalogProductInfo = { snapshot: ProductSnapshot | null; hsCode: string | null }\ntype CatalogProductRecord = Record<string, unknown> & {\n id?: unknown\n title?: unknown\n name?: unknown\n sku?: unknown\n hs_code?: unknown\n}\ntype RequestContext = {\n ctx: CommandRuntimeContext\n tenantId: string\n organizationId: string\n}\ntype CommandCreateResult = { entityId?: string; id?: string }\n\nfunction readString(value: unknown): string | null {\n return typeof value === 'string' && value.trim().length > 0 ? value.trim() : null\n}\n\nfunction resolveUserFeatures(auth: unknown): string[] {\n const features = (auth as { features?: unknown })?.features\n if (!Array.isArray(features)) return []\n return features.filter((value): value is string => typeof value === 'string')\n}\n\nasync function runGuards(\n ctx: CommandRuntimeContext,\n input: MutationGuardInput,\n): Promise<{\n ok: boolean\n errorBody?: Record<string, unknown>\n errorStatus?: number\n modifiedPayload?: Record<string, unknown>\n afterSuccessCallbacks: Array<{ guard: MutationGuard; metadata: Record<string, unknown> | null }>\n}> {\n const legacyGuard = bridgeLegacyGuard(ctx.container)\n const guards = [...getAllMutationGuardInstances(), ...(legacyGuard ? [legacyGuard] : [])]\n return runMutationGuards(guards, input, {\n userFeatures: resolveUserFeatures(ctx.auth),\n })\n}\n\nasync function runGuardAfterSuccessCallbacks(\n callbacks: Array<{ guard: MutationGuard; metadata: Record<string, unknown> | null }>,\n input: {\n tenantId: string\n organizationId: string\n userId: string\n resourceKind: string\n resourceId: string\n operation: 'create'\n requestMethod: string\n requestHeaders: Headers\n },\n): Promise<void> {\n for (const callback of callbacks) {\n if (!callback.guard.afterSuccess) continue\n try {\n await callback.guard.afterSuccess({\n ...input,\n metadata: callback.metadata ?? null,\n })\n } catch (err) {\n logger.warn('Mutation guard afterSuccess callback failed', { err })\n }\n }\n}\n\nasync function resolveRequestContext(req: Request): Promise<RequestContext> {\n const container = await createRequestContainer()\n const auth = await getAuthFromRequest(req)\n const { translate } = await resolveTranslations()\n\n if (!auth || !auth.tenantId) {\n throw new CrudHttpError(401, { error: translate('eudr.errors.unauthorized', 'Unauthorized') })\n }\n\n const scope = await resolveOrganizationScopeForRequest({ container, auth, request: req })\n const organizationId = scope?.selectedId ?? auth.orgId ?? null\n if (!organizationId) {\n throw new CrudHttpError(400, {\n error: translate('eudr.errors.organization_required', 'Organization context is required'),\n })\n }\n\n return {\n tenantId: auth.tenantId,\n organizationId,\n ctx: {\n container,\n auth,\n organizationScope: scope,\n selectedOrganizationId: organizationId,\n organizationIds: scope?.filterIds ?? (auth.orgId ? [auth.orgId] : null),\n request: req,\n },\n }\n}\n\nasync function loadCatalogProductInfo(ctx: RequestContext, productIds: string[]): Promise<Map<string, CatalogProductInfo>> {\n const info = new Map<string, CatalogProductInfo>()\n for (const productId of productIds) info.set(productId, { snapshot: null, hsCode: null })\n if (!productIds.length) return info\n\n try {\n const queryEngine = ctx.ctx.container.resolve('queryEngine') as QueryEngine\n // `items` and `productIds` are capped at 100 each, so their union can reach\n // 200 \u2014 chunk the lookup to keep every page within the pageSize <= 100 rule.\n const chunks: string[][] = []\n for (let index = 0; index < productIds.length; index += CATALOG_LOOKUP_PAGE_SIZE) {\n chunks.push(productIds.slice(index, index + CATALOG_LOOKUP_PAGE_SIZE))\n }\n const products: CatalogProductRecord[] = []\n for (const chunk of chunks) {\n const result = await queryEngine.query<CatalogProductRecord>(E.catalog.catalog_product, {\n fields: ['id', 'title', 'name', 'sku', 'hs_code'],\n filters: { id: { $in: chunk } },\n page: { page: 1, pageSize: chunk.length },\n tenantId: ctx.tenantId,\n organizationId: ctx.organizationId,\n })\n products.push(...result.items)\n }\n for (const product of products) {\n const id = readString(product.id)\n if (!id) continue\n info.set(id, {\n snapshot: {\n name: readString(product.title) ?? readString(product.name),\n sku: readString(product.sku),\n },\n hsCode: readString(product.hs_code),\n })\n }\n } catch {\n return info\n }\n\n return info\n}\n\nfunction resolveApplyItems(\n input: z.infer<typeof applySchema>,\n catalogInfo: Map<string, CatalogProductInfo>,\n): { items: ApplyItem[]; failed: ApplyFailure[] } {\n const items: ApplyItem[] = [...(input.items ?? [])]\n const failed: ApplyFailure[] = []\n const explicitIds = new Set(items.map((item) => item.productId))\n\n for (const productId of input.productIds ?? []) {\n if (explicitIds.has(productId)) continue\n explicitIds.add(productId)\n const hsCode = catalogInfo.get(productId)?.hsCode ?? null\n const suggestedCommodity = suggestCommodityForHsCode(hsCode)\n if (!hsCode || !suggestedCommodity) {\n failed.push({ productId, errorKey: 'eudr.errors.mapping_suggestions_apply_failed' })\n continue\n }\n items.push({ productId, commodity: suggestedCommodity, hsCode })\n }\n\n return { items, failed }\n}\n\nfunction errorKeyFromError(error: unknown): string {\n if (isCrudHttpError(error)) {\n const bodyError = error.body?.error\n return typeof bodyError === 'string' && bodyError.length > 0 ? bodyError : 'eudr.errors.mapping_suggestions_apply_failed'\n }\n if (error instanceof z.ZodError) {\n const firstMessage = error.issues[0]?.message\n return typeof firstMessage === 'string' && firstMessage.length > 0 ? firstMessage : 'eudr.errors.mapping_suggestions_apply_failed'\n }\n return 'eudr.errors.mapping_suggestions_apply_failed'\n}\n\nasync function createMapping(args: {\n commandBus: CommandBus\n ctx: RequestContext\n item: ApplyItem\n productSnapshot: ProductSnapshot | null\n}): Promise<string | null> {\n const parsed = productMappingCreateSchema.parse({\n productId: args.item.productId,\n commodity: args.item.commodity,\n hsCode: args.item.hsCode ?? null,\n productSnapshot: args.productSnapshot,\n })\n const { result } = await args.commandBus.execute<typeof parsed & { tenantId: string; organizationId: string }, CommandCreateResult>(\n 'eudr.product_mappings.create',\n {\n input: {\n ...parsed,\n tenantId: args.ctx.tenantId,\n organizationId: args.ctx.organizationId,\n },\n ctx: args.ctx.ctx,\n metadata: {\n tenantId: args.ctx.tenantId,\n organizationId: args.ctx.organizationId,\n resourceKind: 'eudr.product_mapping',\n context: { cacheAliases: ['eudr.product_mapping'] },\n },\n },\n )\n return result?.entityId ?? result?.id ?? null\n}\n\nexport async function POST(req: Request) {\n try {\n const requestContext = await resolveRequestContext(req)\n const { translate } = await resolveTranslations()\n const payload = await req.json().catch(() => ({}))\n const input = applySchema.parse(payload)\n const catalogInfo = await loadCatalogProductInfo(\n requestContext,\n Array.from(new Set([\n ...(input.items ?? []).map((item) => item.productId),\n ...(input.productIds ?? []),\n ])),\n )\n const resolved = resolveApplyItems(input, catalogInfo)\n const guardResult = await runGuards(requestContext.ctx, {\n tenantId: requestContext.tenantId,\n organizationId: requestContext.organizationId,\n userId: requestContext.ctx.auth?.sub ?? '',\n resourceKind: 'eudr.product_mapping',\n resourceId: null,\n operation: 'create',\n requestMethod: req.method,\n requestHeaders: req.headers,\n mutationPayload: { items: resolved.items },\n })\n if (!guardResult.ok) {\n return Response.json(\n guardResult.errorBody ?? { error: translate('eudr.errors.operation_blocked', 'Operation blocked by guard') },\n { status: guardResult.errorStatus ?? 422 },\n )\n }\n\n const commandBus = requestContext.ctx.container.resolve('commandBus') as CommandBus\n const failed: ApplyFailure[] = [...resolved.failed]\n const createdIds: string[] = []\n\n const guardItems = Array.isArray((guardResult.modifiedPayload as { items?: unknown } | undefined)?.items)\n ? ((guardResult.modifiedPayload as { items: unknown[] }).items.filter((entry): entry is typeof resolved.items[number] => (\n typeof (entry as { productId?: unknown })?.productId === 'string'\n && typeof (entry as { commodity?: unknown })?.commodity === 'string'\n )))\n : resolved.items\n\n for (const item of guardItems) {\n try {\n const id = await createMapping({\n commandBus,\n ctx: requestContext,\n item,\n productSnapshot: catalogInfo.get(item.productId)?.snapshot ?? null,\n })\n if (id) createdIds.push(id)\n } catch (error) {\n failed.push({ productId: item.productId, errorKey: errorKeyFromError(error) })\n }\n }\n\n if (guardResult.afterSuccessCallbacks.length) {\n for (const id of createdIds) {\n await runGuardAfterSuccessCallbacks(guardResult.afterSuccessCallbacks, {\n tenantId: requestContext.tenantId,\n organizationId: requestContext.organizationId,\n userId: requestContext.ctx.auth?.sub ?? '',\n resourceKind: 'eudr.product_mapping',\n resourceId: id,\n operation: 'create',\n requestMethod: req.method,\n requestHeaders: req.headers,\n })\n }\n }\n\n return Response.json({ created: createdIds.length, failed })\n } catch (error) {\n if (isCrudHttpError(error)) {\n return Response.json(error.body, { status: error.status })\n }\n // No command-interceptor mapping here (issue #5097): every `commandBus.execute`\n // in this route runs inside the per-item loop above, whose catch records the\n // failure in `failed[]` and continues, so an interceptor rejection can never\n // reach this handler. The batch contract \u2014 HTTP 200 with a per-item failure\n // list \u2014 is deliberately unchanged; the route is listed in\n // `ROUTES_WITH_BATCH_ITEM_ERROR_HANDLING` in\n // `packages/core/src/__tests__/command-interceptor-http-coverage.test.ts`.\n const { translate } = await resolveTranslations()\n if (error instanceof z.ZodError) {\n return Response.json(\n { error: translate('eudr.errors.mapping_suggestions_apply_failed', 'Failed to apply EUDR mapping suggestions') },\n { status: 400 },\n )\n }\n logger.error('Product mapping suggestions apply failed', { err: error })\n return Response.json(\n { error: translate('eudr.errors.mapping_suggestions_apply_failed', 'Failed to apply EUDR mapping suggestions') },\n { status: 500 },\n )\n }\n}\n\nconst applyResponseSchema = z.object({\n created: z.number(),\n failed: z.array(z.object({\n productId: z.string().uuid(),\n errorKey: z.string(),\n })),\n})\n\nexport const openApi: OpenApiRouteDoc = {\n tag: 'EUDR',\n summary: 'Apply EUDR product mapping suggestions',\n methods: {\n POST: {\n summary: 'Apply mapping suggestions',\n requestBody: {\n contentType: 'application/json',\n schema: applySchema,\n },\n responses: [\n { status: 200, description: 'Apply result', schema: applyResponseSchema },\n { status: 400, description: 'Invalid apply payload', schema: z.object({ error: z.string() }) },\n { status: 401, description: 'Unauthorized', schema: z.object({ error: z.string() }) },\n { status: 403, description: 'Forbidden', schema: z.object({ error: z.string() }) },\n { status: 500, description: 'Mapping suggestion apply failed', schema: z.object({ error: z.string() }) },\n ],\n },\n },\n}\n"],
|
|
5
|
+
"mappings": "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",
|
|
6
6
|
"names": []
|
|
7
7
|
}
|
|
@@ -21,6 +21,7 @@ import {
|
|
|
21
21
|
changeOverrideStateResponseSchema
|
|
22
22
|
} from "../openapi.js";
|
|
23
23
|
import { createLogger } from "@open-mercato/shared/lib/logger";
|
|
24
|
+
import { getCommandInterceptorHttpRejection } from "@open-mercato/shared/lib/commands/errors";
|
|
24
25
|
const logger = createLogger("feature_toggles").child({ component: "overrides" });
|
|
25
26
|
async function GET(req) {
|
|
26
27
|
const { auth, ctx, scope } = await buildContext(req);
|
|
@@ -156,6 +157,10 @@ async function PUT(req) {
|
|
|
156
157
|
if (isCrudHttpError(error)) {
|
|
157
158
|
return NextResponse.json(error.body, { status: error.status });
|
|
158
159
|
}
|
|
160
|
+
const interceptorRejection = getCommandInterceptorHttpRejection(error);
|
|
161
|
+
if (interceptorRejection) {
|
|
162
|
+
return NextResponse.json(interceptorRejection.body, { status: interceptorRejection.status });
|
|
163
|
+
}
|
|
159
164
|
const message = error instanceof Error ? error.message : "Unknown error";
|
|
160
165
|
if (message === "NOT_FOUND") return NextResponse.json({ error: "Not found" }, { status: 404 });
|
|
161
166
|
if (message === "INVALID_STATE" || message === "INVALID_TOGGLE_ID" || message === "INVALID_TENANT_ID") {
|