@open-mercato/core 0.6.7-develop.6726.1.983ae8a07e → 0.6.7-develop.6744.1.0483b18565
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.turbo/turbo-build.log +1 -1
- package/dist/generated/entities/resources_resource/index.js +2 -0
- package/dist/generated/entities/resources_resource/index.js.map +2 -2
- package/dist/generated/entity-fields-registry.js +1 -0
- package/dist/generated/entity-fields-registry.js.map +2 -2
- package/dist/helpers/integration/salesFixtures.js +31 -2
- package/dist/helpers/integration/salesFixtures.js.map +2 -2
- package/dist/helpers/integration/salesUi.js +5 -0
- package/dist/helpers/integration/salesUi.js.map +2 -2
- package/dist/modules/communication_channels/data/enrichers.js +4 -4
- package/dist/modules/communication_channels/data/enrichers.js.map +2 -2
- package/dist/modules/customers/analytics.js +1 -0
- package/dist/modules/customers/analytics.js.map +2 -2
- package/dist/modules/customers/components/detail/DealForm.js +2 -1
- package/dist/modules/customers/components/detail/DealForm.js.map +2 -2
- package/dist/modules/customers/components/detail/DealsSection.js +1 -1
- package/dist/modules/customers/components/detail/DealsSection.js.map +2 -2
- package/dist/modules/customers/data/validators.js +3 -1
- package/dist/modules/customers/data/validators.js.map +2 -2
- package/dist/modules/dashboards/widgets/dashboard/pipeline-summary/config.js +52 -2
- package/dist/modules/dashboards/widgets/dashboard/pipeline-summary/config.js.map +2 -2
- package/dist/modules/dashboards/widgets/dashboard/pipeline-summary/widget.client.js +42 -26
- package/dist/modules/dashboards/widgets/dashboard/pipeline-summary/widget.client.js.map +2 -2
- package/dist/modules/dashboards/widgets/dashboard/pipeline-summary/widget.js +2 -2
- package/dist/modules/dashboards/widgets/dashboard/pipeline-summary/widget.js.map +2 -2
- package/dist/modules/dictionaries/api/[dictionaryId]/entries/route.js +34 -12
- package/dist/modules/dictionaries/api/[dictionaryId]/entries/route.js.map +2 -2
- package/dist/modules/dictionaries/api/openapi.js +15 -2
- package/dist/modules/dictionaries/api/openapi.js.map +2 -2
- package/dist/modules/dictionaries/components/hooks/useDictionaryEntries.js +30 -29
- package/dist/modules/dictionaries/components/hooks/useDictionaryEntries.js.map +2 -2
- package/dist/modules/dictionaries/data/validators.js +7 -0
- package/dist/modules/dictionaries/data/validators.js.map +2 -2
- package/dist/modules/dictionaries/lib/clientEntries.js +4 -3
- package/dist/modules/dictionaries/lib/clientEntries.js.map +2 -2
- package/dist/modules/dictionaries/lib/fetchAllEntries.js +43 -0
- package/dist/modules/dictionaries/lib/fetchAllEntries.js.map +7 -0
- package/dist/modules/entities/api/definitions.js +51 -3
- package/dist/modules/entities/api/definitions.js.map +2 -2
- package/dist/modules/entities/api/entities.js +21 -2
- package/dist/modules/entities/api/entities.js.map +2 -2
- package/dist/modules/entities/api/records.js +3 -24
- package/dist/modules/entities/api/records.js.map +2 -2
- package/dist/modules/entities/api/sidebar-entities.js +34 -12
- package/dist/modules/entities/api/sidebar-entities.js.map +2 -2
- package/dist/modules/entities/lib/entityAcl.js +46 -0
- package/dist/modules/entities/lib/entityAcl.js.map +2 -2
- package/dist/modules/messages/api/route.js +2 -5
- package/dist/modules/messages/api/route.js.map +2 -2
- package/dist/modules/messages/lib/participantScope.js +15 -0
- package/dist/modules/messages/lib/participantScope.js.map +7 -0
- package/dist/modules/planner/components/unavailabilityReasons.js +3 -3
- package/dist/modules/planner/components/unavailabilityReasons.js.map +2 -2
- package/dist/modules/progress/lib/progressService.js +2 -0
- package/dist/modules/progress/lib/progressService.js.map +2 -2
- package/dist/modules/progress/lib/progressServiceImpl.js +322 -104
- package/dist/modules/progress/lib/progressServiceImpl.js.map +2 -2
- package/dist/modules/resources/api/resources.js +2 -0
- package/dist/modules/resources/api/resources.js.map +2 -2
- package/dist/modules/resources/backend/resources/resources/[id]/page.js +3 -2
- package/dist/modules/resources/backend/resources/resources/[id]/page.js.map +2 -2
- package/dist/modules/resources/commands/resources.js +9 -0
- package/dist/modules/resources/commands/resources.js.map +2 -2
- package/dist/modules/resources/components/detail/dictionaries.js +3 -3
- package/dist/modules/resources/components/detail/dictionaries.js.map +2 -2
- package/dist/modules/resources/data/entities.js +3 -0
- package/dist/modules/resources/data/entities.js.map +2 -2
- package/dist/modules/resources/data/validators.js +6 -2
- package/dist/modules/resources/data/validators.js.map +2 -2
- package/dist/modules/resources/lib/seeds.js +16 -9
- package/dist/modules/resources/lib/seeds.js.map +2 -2
- package/dist/modules/resources/migrations/Migration20260608231000.js +13 -0
- package/dist/modules/resources/migrations/Migration20260608231000.js.map +7 -0
- package/dist/modules/sales/backend/sales/documents/create/page.js +11 -14
- package/dist/modules/sales/backend/sales/documents/create/page.js.map +2 -2
- package/dist/modules/sales/commands/documents.js +8 -0
- package/dist/modules/sales/commands/documents.js.map +2 -2
- package/dist/modules/sales/components/documents/ItemsSection.js +18 -9
- package/dist/modules/sales/components/documents/ItemsSection.js.map +2 -2
- package/dist/modules/sales/components/documents/LineItemDialog.js +12 -5
- package/dist/modules/sales/components/documents/LineItemDialog.js.map +2 -2
- package/dist/modules/sales/components/documents/SalesDocumentForm.js +48 -2
- package/dist/modules/sales/components/documents/SalesDocumentForm.js.map +2 -2
- package/dist/modules/sales/components/documents/SalesOrderDraftLines.js +167 -0
- package/dist/modules/sales/components/documents/SalesOrderDraftLines.js.map +7 -0
- package/dist/modules/sales/data/validators.js +5 -1
- package/dist/modules/sales/data/validators.js.map +2 -2
- package/dist/modules/staff/components/detail/dictionaries.js +3 -3
- package/dist/modules/staff/components/detail/dictionaries.js.map +2 -2
- package/dist/modules/workflows/widgets/injection/order-approval/widget.client.js +11 -4
- package/dist/modules/workflows/widgets/injection/order-approval/widget.client.js.map +2 -2
- package/generated/entities/resources_resource/index.ts +1 -0
- package/generated/entity-fields-registry.ts +1 -0
- package/package.json +7 -7
- package/src/helpers/integration/salesFixtures.ts +34 -2
- package/src/helpers/integration/salesUi.ts +8 -0
- package/src/modules/communication_channels/data/enrichers.ts +6 -26
- package/src/modules/customers/analytics.ts +1 -0
- package/src/modules/customers/components/detail/DealForm.tsx +5 -1
- package/src/modules/customers/components/detail/DealsSection.tsx +1 -1
- package/src/modules/customers/data/validators.ts +5 -1
- package/src/modules/dashboards/i18n/de.json +3 -0
- package/src/modules/dashboards/i18n/en.json +3 -0
- package/src/modules/dashboards/i18n/es.json +3 -0
- package/src/modules/dashboards/i18n/pl.json +3 -0
- package/src/modules/dashboards/widgets/dashboard/pipeline-summary/config.ts +86 -0
- package/src/modules/dashboards/widgets/dashboard/pipeline-summary/widget.client.tsx +33 -18
- package/src/modules/dashboards/widgets/dashboard/pipeline-summary/widget.ts +2 -2
- package/src/modules/dictionaries/api/[dictionaryId]/entries/route.ts +39 -11
- package/src/modules/dictionaries/api/openapi.ts +19 -0
- package/src/modules/dictionaries/components/hooks/useDictionaryEntries.ts +35 -36
- package/src/modules/dictionaries/data/validators.ts +14 -0
- package/src/modules/dictionaries/lib/clientEntries.ts +4 -3
- package/src/modules/dictionaries/lib/fetchAllEntries.ts +72 -0
- package/src/modules/entities/api/definitions.ts +51 -3
- package/src/modules/entities/api/entities.ts +30 -2
- package/src/modules/entities/api/records.ts +3 -27
- package/src/modules/entities/api/sidebar-entities.ts +43 -14
- package/src/modules/entities/lib/entityAcl.ts +58 -0
- package/src/modules/messages/api/route.ts +4 -5
- package/src/modules/messages/lib/participantScope.ts +64 -0
- package/src/modules/planner/components/unavailabilityReasons.ts +3 -3
- package/src/modules/progress/AGENTS.md +11 -1
- package/src/modules/progress/lib/progressService.ts +1 -0
- package/src/modules/progress/lib/progressServiceImpl.ts +385 -117
- package/src/modules/resources/api/resources.ts +2 -0
- package/src/modules/resources/backend/resources/resources/[id]/page.tsx +9 -3
- package/src/modules/resources/commands/resources.ts +10 -0
- package/src/modules/resources/components/detail/dictionaries.ts +3 -3
- package/src/modules/resources/data/entities.ts +3 -0
- package/src/modules/resources/data/validators.ts +5 -0
- package/src/modules/resources/lib/seeds.ts +16 -9
- package/src/modules/resources/migrations/.snapshot-open-mercato.json +17 -1
- package/src/modules/resources/migrations/Migration20260608231000.ts +13 -0
- package/src/modules/sales/backend/sales/documents/create/page.tsx +2 -4
- package/src/modules/sales/commands/documents.ts +8 -0
- package/src/modules/sales/components/documents/ItemsSection.tsx +19 -10
- package/src/modules/sales/components/documents/LineItemDialog.tsx +13 -5
- package/src/modules/sales/components/documents/SalesDocumentForm.tsx +48 -6
- package/src/modules/sales/components/documents/SalesOrderDraftLines.tsx +198 -0
- package/src/modules/sales/data/validators.ts +7 -1
- package/src/modules/sales/i18n/de.json +2 -0
- package/src/modules/sales/i18n/en.json +2 -0
- package/src/modules/sales/i18n/es.json +2 -0
- package/src/modules/sales/i18n/pl.json +2 -0
- package/src/modules/staff/components/detail/dictionaries.ts +3 -3
- package/src/modules/workflows/widgets/injection/order-approval/widget.client.tsx +12 -5
|
@@ -9,6 +9,7 @@ import { isSystemEntitySelectable } from "@open-mercato/shared/lib/entities/syst
|
|
|
9
9
|
import { SYSTEM_ENTITY_RECORDS_BLOCKED_CODE, isOrmBackedSystemEntityId } from "@open-mercato/shared/lib/data/engine";
|
|
10
10
|
import { enforceCommandOptimisticLockWithGuards } from "@open-mercato/shared/lib/crud/optimistic-lock-command";
|
|
11
11
|
import { isCrudHttpError } from "@open-mercato/shared/lib/crud/errors";
|
|
12
|
+
import { canReadEntityMetadata, getDeclaredCustomEntityRestriction } from "../lib/entityAcl.js";
|
|
12
13
|
import {
|
|
13
14
|
beginEntitiesMutationGuard,
|
|
14
15
|
ENTITY_DEFINITION_RESOURCE_KIND
|
|
@@ -24,6 +25,11 @@ async function GET(req) {
|
|
|
24
25
|
if (!auth || !auth.tenantId || !auth.orgId && !auth.isSuperAdmin) return NextResponse.json({ error: "Unauthorized" }, { status: 401 });
|
|
25
26
|
const { resolve } = await createRequestContainer();
|
|
26
27
|
const em = resolve("em");
|
|
28
|
+
const rbac = resolve("rbacService");
|
|
29
|
+
const acl = await rbac.loadAcl(auth.sub ?? "", {
|
|
30
|
+
tenantId: auth.tenantId,
|
|
31
|
+
organizationId: auth.orgId ?? null
|
|
32
|
+
});
|
|
27
33
|
const AllEntities = getEntityIds();
|
|
28
34
|
const generated = [];
|
|
29
35
|
for (const modId of Object.keys(AllEntities)) {
|
|
@@ -64,7 +70,20 @@ async function GET(req) {
|
|
|
64
70
|
const existing = byId.get(cu.entityId);
|
|
65
71
|
byId.set(cu.entityId, { ...existing, ...cu, source: existing?.source ?? cu.source });
|
|
66
72
|
}
|
|
67
|
-
const
|
|
73
|
+
const visibleEntityIds = new Set(
|
|
74
|
+
Array.from(byId.values()).filter((item) => {
|
|
75
|
+
const isOrmBacked = isOrmBackedSystemEntityId(em, item.entityId);
|
|
76
|
+
const declaredRestriction = isOrmBacked ? void 0 : getDeclaredCustomEntityRestriction(item.entityId);
|
|
77
|
+
return canReadEntityMetadata({
|
|
78
|
+
entityId: item.entityId,
|
|
79
|
+
isCustomEntity: !isOrmBacked && (item.source === "custom" || declaredRestriction !== void 0),
|
|
80
|
+
isRestricted: declaredRestriction ?? item.accessRestricted === true,
|
|
81
|
+
acl
|
|
82
|
+
});
|
|
83
|
+
}).map((item) => item.entityId)
|
|
84
|
+
);
|
|
85
|
+
if (!visibleEntityIds.size) return NextResponse.json({ items: [] });
|
|
86
|
+
const defsWhere = { isActive: true, entityId: { $in: Array.from(visibleEntityIds) } };
|
|
68
87
|
defsWhere.$and = [
|
|
69
88
|
//{ $or: [ { organizationId: auth.orgId ?? undefined as any }, { organizationId: null } ] }, // the entities and custom fields are defined per tenant
|
|
70
89
|
{ tenantId: auth.tenantId ?? void 0 }
|
|
@@ -81,7 +100,7 @@ async function GET(req) {
|
|
|
81
100
|
}
|
|
82
101
|
const counts = {};
|
|
83
102
|
for (const [eid, set] of keySets.entries()) counts[eid] = set.size;
|
|
84
|
-
const items = Array.from(byId.values()).map((
|
|
103
|
+
const items = Array.from(byId.values()).filter((item) => visibleEntityIds.has(item.entityId)).map((item) => ({ ...item, count: counts[item.entityId] || 0 }));
|
|
85
104
|
return NextResponse.json({ items });
|
|
86
105
|
}
|
|
87
106
|
async function POST(req) {
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"version": 3,
|
|
3
3
|
"sources": ["../../../../src/modules/entities/api/entities.ts"],
|
|
4
|
-
"sourcesContent": ["import { NextResponse } from 'next/server'\nimport { z } from 'zod'\nimport { createRequestContainer } from '@open-mercato/shared/lib/di/container'\nimport { getAuthFromRequest } from '@open-mercato/shared/lib/auth/server'\nimport { CustomEntity, CustomFieldDef } from '@open-mercato/core/modules/entities/data/entities'\nimport { getEntityIds } from '@open-mercato/shared/lib/encryption/entityIds'\nimport { upsertCustomEntitySchema } from '@open-mercato/core/modules/entities/data/validators'\nimport type { OpenApiRouteDoc } from '@open-mercato/shared/lib/openapi'\nimport { isSystemEntitySelectable } from '@open-mercato/shared/lib/entities/system-entities'\nimport { SYSTEM_ENTITY_RECORDS_BLOCKED_CODE, isOrmBackedSystemEntityId } from '@open-mercato/shared/lib/data/engine'\nimport { enforceCommandOptimisticLockWithGuards } from '@open-mercato/shared/lib/crud/optimistic-lock-command'\nimport { isCrudHttpError } from '@open-mercato/shared/lib/crud/errors'\nimport {\n beginEntitiesMutationGuard,\n ENTITY_DEFINITION_RESOURCE_KIND,\n} from './definitions.mutation-guard'\n\nconst CUSTOM_ENTITY_DEFINITION_RESOURCE_KIND = ENTITY_DEFINITION_RESOURCE_KIND\n\nexport const metadata = {\n GET: { requireAuth: true },\n POST: { requireAuth: true, requireFeatures: ['entities.definitions.manage'] },\n DELETE: { requireAuth: true, requireFeatures: ['entities.definitions.manage'] },\n}\n\nexport async function GET(req: Request) {\n const auth = await getAuthFromRequest(req)\n if (!auth || !auth.tenantId || (!auth.orgId && !auth.isSuperAdmin)) return NextResponse.json({ error: 'Unauthorized' }, { status: 401 })\n\n const { resolve } = await createRequestContainer()\n const em = resolve('em') as any\n\n // Generated entities from code\n const AllEntities = getEntityIds()\n const generated: { entityId: string; source: 'code'; label: string }[] = []\n for (const modId of Object.keys(AllEntities)) {\n const entities = (AllEntities as any)[modId] as Record<string, string>\n for (const k of Object.keys(entities)) {\n const id = entities[k]\n if (!isSystemEntitySelectable(id)) continue\n generated.push({ entityId: id, source: 'code', label: id })\n }\n }\n\n // Custom user-defined entities (global/org/tenant scoped)\n const where: any = { isActive: true }\n where.$and = [\n { $or: [ { organizationId: auth.orgId ?? undefined as any }, { organizationId: null } ] },\n { $or: [ { tenantId: auth.tenantId ?? undefined as any }, { tenantId: null } ] },\n ]\n const customs = await em.find(CustomEntity as any, where as any, { orderBy: { entityId: 'asc' } as any })\n // Resolve overlay precedence: prefer organization/tenant-specific over global\n const customByEntityId = new Map<string, any>()\n for (const c of customs as any[]) {\n const specificity = (c.organizationId ? 2 : 0) + (c.tenantId ? 1 : 0)\n const prev = customByEntityId.get(c.entityId)\n const prevSpec = prev ? ((prev.organizationId ? 2 : 0) + (prev.tenantId ? 1 : 0)) : -1\n if (!prev || specificity > prevSpec) customByEntityId.set(c.entityId, c)\n }\n\n const custom = Array.from(customByEntityId.values())\n .filter((c) => isSystemEntitySelectable(c.entityId))\n .map((c) => ({\n entityId: c.entityId,\n source: 'custom' as const,\n label: c.label,\n description: c.description ?? undefined,\n labelField: (c as any).labelField ?? undefined,\n defaultEditor: (c as any).defaultEditor ?? undefined,\n showInSidebar: (c as any).showInSidebar ?? false,\n accessRestricted: (c as any).accessRestricted ?? false,\n updatedAt: c.updatedAt instanceof Date ? c.updatedAt.toISOString() : (c.updatedAt ?? undefined),\n }))\n\n const byId = new Map<string, any>()\n for (const g of generated) byId.set(g.entityId, g)\n for (const cu of custom) {\n const existing = byId.get(cu.entityId)\n byId.set(cu.entityId, { ...existing, ...cu, source: existing?.source ?? cu.source })\n }\n\n // Count field definitions scoped to current tenant/org (same scoping as custom entities)\n const defsWhere: any = { isActive: true }\n defsWhere.$and = [\n //{ $or: [ { organizationId: auth.orgId ?? undefined as any }, { organizationId: null } ] }, // the entities and custom fields are defined per tenant\n { tenantId: auth.tenantId ?? undefined as any },\n ]\n const defs = await em.find(CustomFieldDef as any, defsWhere as any)\n // Count distinct field names (keys) per entityId\n const keySets = new Map<string, Set<string>>()\n for (const d of defs as any[]) {\n const eid = String(d.entityId)\n const k = String(d.key)\n if (!isSystemEntitySelectable(eid)) continue\n const set = keySets.get(eid) || new Set<string>()\n set.add(k)\n keySets.set(eid, set)\n }\n const counts: Record<string, number> = {}\n for (const [eid, set] of keySets.entries()) counts[eid] = set.size\n\n const items = Array.from(byId.values()).map((it: any) => ({ ...it, count: counts[it.entityId] || 0 }))\n return NextResponse.json({ items })\n}\n\nexport async function POST(req: Request) {\n const auth = await getAuthFromRequest(req)\n if (!auth || !auth.orgId) return NextResponse.json({ error: 'Unauthorized' }, { status: 401 })\n\n let body: any\n try { body = await req.json() } catch { return NextResponse.json({ error: 'Invalid JSON' }, { status: 400 }) }\n const parsed = upsertCustomEntitySchema.safeParse(body)\n if (!parsed.success) {\n return NextResponse.json({ error: 'Validation failed', details: parsed.error.flatten() }, { status: 400 })\n }\n const input = parsed.data\n // Distinguish \"caller did not send the field\" (apply tenant default-restricted\n // policy on create) from \"caller explicitly chose false\".\n const accessRestrictedExplicit = body != null && typeof body === 'object'\n && Object.prototype.hasOwnProperty.call(body, 'accessRestricted')\n\n const container = await createRequestContainer()\n const { resolve } = container\n const em = resolve('em') as any\n\n // A registration for a module-declared, table-backed system entity would flip\n // query-engine classification to doc storage for the whole entity type (#2939's\n // failure mode via another door) \u2014 refuse to create one.\n if (isOrmBackedSystemEntityId(em, input.entityId)) {\n return NextResponse.json(\n { error: 'System entities cannot be registered as custom entities', code: SYSTEM_ENTITY_RECORDS_BLOCKED_CODE, entityId: input.entityId },\n { status: 400 },\n )\n }\n\n const where: any = { entityId: input.entityId, organizationId: auth.orgId ?? null, tenantId: auth.tenantId ?? null }\n let ent = await em.findOne(CustomEntity, where)\n if (ent) {\n try {\n await enforceCommandOptimisticLockWithGuards(container, {\n resourceKind: CUSTOM_ENTITY_DEFINITION_RESOURCE_KIND,\n resourceId: ent.id,\n current: ent.updatedAt ?? null,\n request: req,\n })\n } catch (lockError) {\n if (isCrudHttpError(lockError)) {\n return NextResponse.json(lockError.body, { status: lockError.status })\n }\n throw lockError\n }\n }\n\n const guard = await beginEntitiesMutationGuard({\n container,\n auth,\n req,\n resourceKind: ENTITY_DEFINITION_RESOURCE_KIND,\n resourceId: ent ? ent.id : input.entityId,\n operation: ent ? 'update' : 'create',\n mutationPayload: input as unknown as Record<string, unknown>,\n })\n if (guard.blockedResponse) return guard.blockedResponse\n\n const isCreate = !ent\n if (!ent) ent = em.create(CustomEntity, { ...where, createdAt: new Date() })\n ent.label = input.label\n ent.description = input.description ?? null\n ent.isActive = input.isActive ?? true\n ent.labelField = input.labelField ?? ent.labelField ?? null\n ent.defaultEditor = input.defaultEditor ?? ent.defaultEditor ?? null\n ent.showInSidebar = input.showInSidebar ?? ent.showInSidebar ?? false\n // Never silently flip a security control on a partial upsert. `access_restricted`\n // changes ONLY when the caller explicitly sends the field. On create without it,\n // fall back to the tenant default-restricted policy; on update without it, keep\n // the entity's current value (a metadata-only update must not un-restrict).\n if (accessRestrictedExplicit) {\n ent.accessRestricted = input.accessRestricted === true\n } else if (isCreate) {\n let policyDefault = false\n try {\n const moduleConfigService = resolve('moduleConfigService') as {\n getValue: (m: string, n: string, o?: { defaultValue?: unknown; scope?: { tenantId?: string | null } }) => Promise<unknown>\n }\n policyDefault = (await moduleConfigService.getValue('entities', 'newEntitiesRestrictedByDefault', {\n defaultValue: false,\n scope: { tenantId: auth.tenantId ?? null },\n })) === true\n } catch {}\n ent.accessRestricted = policyDefault\n } else {\n ent.accessRestricted = ent.accessRestricted ?? false\n }\n ent.updatedAt = new Date()\n em.persist(ent)\n await em.flush()\n await guard.runAfterSuccess()\n // Invalidate sidebar/nav cache for tenant scope (also when tenantId is null)\n try {\n const cache = (await createRequestContainer()).resolve('cache') as any\n if (cache) {\n await cache.deleteByTags([`nav:entities:${auth.tenantId || 'null'}`])\n }\n } catch {}\n return NextResponse.json({ ok: true, item: { id: ent.id, entityId: ent.entityId, label: ent.label, description: ent.description ?? undefined } })\n}\n\nexport async function DELETE(req: Request) {\n const auth = await getAuthFromRequest(req)\n if (!auth || !auth.orgId) return NextResponse.json({ error: 'Unauthorized' }, { status: 401 })\n let body: any\n try { body = await req.json() } catch { return NextResponse.json({ error: 'Invalid JSON' }, { status: 400 }) }\n const entityId = body?.entityId\n if (!entityId) return NextResponse.json({ error: 'entityId is required' }, { status: 400 })\n\n const container = await createRequestContainer()\n const { resolve } = container\n const em = resolve('em') as any\n\n const where: any = { entityId, organizationId: auth.orgId ?? null, tenantId: auth.tenantId ?? null }\n const ent = await em.findOne(CustomEntity, where)\n if (!ent) return NextResponse.json({ error: 'Not found' }, { status: 404 })\n\n const guard = await beginEntitiesMutationGuard({\n container,\n auth,\n req,\n resourceKind: ENTITY_DEFINITION_RESOURCE_KIND,\n resourceId: ent.id,\n operation: 'delete',\n mutationPayload: { entityId },\n })\n if (guard.blockedResponse) return guard.blockedResponse\n\n ent.isActive = false\n ent.updatedAt = new Date()\n ent.deletedAt = ent.deletedAt ?? new Date()\n em.persist(ent)\n await em.flush()\n await guard.runAfterSuccess()\n // Invalidate sidebar/nav cache for tenant scope (also when tenantId is null)\n try {\n const cache = (await createRequestContainer()).resolve('cache') as any\n if (cache) {\n await cache.deleteByTags([`nav:entities:${auth.tenantId || 'null'}`])\n }\n } catch {}\n return NextResponse.json({ ok: true })\n}\n\nconst entitySummarySchema = z.object({\n entityId: z.string(),\n source: z.enum(['code', 'custom']),\n label: z.string(),\n description: z.string().optional(),\n labelField: z.string().optional(),\n defaultEditor: z.string().optional(),\n showInSidebar: z.boolean().optional(),\n accessRestricted: z.boolean().optional(),\n updatedAt: z.string().optional(),\n count: z.number(),\n})\n\nconst entityListResponseSchema = z.object({\n items: z.array(entitySummarySchema),\n})\n\nconst deleteEntityRequestSchema = z.object({\n entityId: z.string(),\n})\n\nconst upsertCustomEntityResponseSchema = z.object({\n ok: z.literal(true),\n item: z.object({\n id: z.string().uuid(),\n entityId: z.string(),\n label: z.string(),\n description: z.string().optional(),\n }),\n})\n\nexport const openApi: OpenApiRouteDoc = {\n tag: 'Entities',\n summary: 'Manage custom entities',\n methods: {\n GET: {\n summary: 'List available entities',\n description: 'Returns generated and custom entities scoped to the caller with field counts per entity.',\n responses: [\n {\n status: 200,\n description: 'List of entities',\n schema: entityListResponseSchema,\n },\n {\n status: 401,\n description: 'Missing authentication',\n schema: z.object({ error: z.string() }),\n },\n ],\n },\n POST: {\n summary: 'Upsert custom entity',\n description: 'Creates or updates a tenant/org scoped custom entity definition.',\n requestBody: {\n contentType: 'application/json',\n schema: upsertCustomEntitySchema,\n },\n responses: [\n {\n status: 200,\n description: 'Entity saved',\n schema: upsertCustomEntityResponseSchema,\n },\n {\n status: 400,\n description: 'Validation error',\n schema: z.object({\n error: z.string(),\n details: z.any().optional(),\n }),\n },\n {\n status: 401,\n description: 'Missing authentication',\n schema: z.object({ error: z.string() }),\n },\n ],\n },\n DELETE: {\n summary: 'Soft delete custom entity',\n description: 'Marks the specified custom entity inactive within the current scope.',\n requestBody: {\n contentType: 'application/json',\n schema: deleteEntityRequestSchema,\n },\n responses: [\n {\n status: 200,\n description: 'Entity deleted',\n schema: z.object({ ok: z.boolean() }),\n },\n {\n status: 400,\n description: 'Missing entity id',\n schema: z.object({ error: z.string() }),\n },\n {\n status: 401,\n description: 'Missing authentication',\n schema: z.object({ error: z.string() }),\n },\n {\n status: 404,\n description: 'Entity not found in scope',\n schema: z.object({ error: z.string() }),\n },\n ],\n },\n },\n}\n"],
|
|
5
|
-
"mappings": "AAAA,SAAS,oBAAoB;AAC7B,SAAS,SAAS;AAClB,SAAS,8BAA8B;AACvC,SAAS,0BAA0B;AACnC,SAAS,cAAc,sBAAsB;AAC7C,SAAS,oBAAoB;AAC7B,SAAS,gCAAgC;AAEzC,SAAS,gCAAgC;AACzC,SAAS,oCAAoC,iCAAiC;AAC9E,SAAS,8CAA8C;AACvD,SAAS,uBAAuB;
|
|
4
|
+
"sourcesContent": ["import { NextResponse } from 'next/server'\nimport { z } from 'zod'\nimport { createRequestContainer } from '@open-mercato/shared/lib/di/container'\nimport { getAuthFromRequest } from '@open-mercato/shared/lib/auth/server'\nimport { CustomEntity, CustomFieldDef } from '@open-mercato/core/modules/entities/data/entities'\nimport { getEntityIds } from '@open-mercato/shared/lib/encryption/entityIds'\nimport { upsertCustomEntitySchema } from '@open-mercato/core/modules/entities/data/validators'\nimport type { OpenApiRouteDoc } from '@open-mercato/shared/lib/openapi'\nimport { isSystemEntitySelectable } from '@open-mercato/shared/lib/entities/system-entities'\nimport { SYSTEM_ENTITY_RECORDS_BLOCKED_CODE, isOrmBackedSystemEntityId } from '@open-mercato/shared/lib/data/engine'\nimport { enforceCommandOptimisticLockWithGuards } from '@open-mercato/shared/lib/crud/optimistic-lock-command'\nimport { isCrudHttpError } from '@open-mercato/shared/lib/crud/errors'\nimport type { RbacService } from '@open-mercato/core/modules/auth/services/rbacService'\nimport { canReadEntityMetadata, getDeclaredCustomEntityRestriction } from '../lib/entityAcl'\nimport {\n beginEntitiesMutationGuard,\n ENTITY_DEFINITION_RESOURCE_KIND,\n} from './definitions.mutation-guard'\n\nconst CUSTOM_ENTITY_DEFINITION_RESOURCE_KIND = ENTITY_DEFINITION_RESOURCE_KIND\n\nexport const metadata = {\n GET: { requireAuth: true },\n POST: { requireAuth: true, requireFeatures: ['entities.definitions.manage'] },\n DELETE: { requireAuth: true, requireFeatures: ['entities.definitions.manage'] },\n}\n\nexport async function GET(req: Request) {\n const auth = await getAuthFromRequest(req)\n if (!auth || !auth.tenantId || (!auth.orgId && !auth.isSuperAdmin)) return NextResponse.json({ error: 'Unauthorized' }, { status: 401 })\n\n const { resolve } = await createRequestContainer()\n const em = resolve('em') as any\n const rbac = resolve('rbacService') as RbacService\n const acl = await rbac.loadAcl(auth.sub ?? '', {\n tenantId: auth.tenantId,\n organizationId: auth.orgId ?? null,\n })\n\n // Generated entities from code\n const AllEntities = getEntityIds()\n const generated: { entityId: string; source: 'code'; label: string }[] = []\n for (const modId of Object.keys(AllEntities)) {\n const entities = (AllEntities as any)[modId] as Record<string, string>\n for (const k of Object.keys(entities)) {\n const id = entities[k]\n if (!isSystemEntitySelectable(id)) continue\n generated.push({ entityId: id, source: 'code', label: id })\n }\n }\n\n // Custom user-defined entities (global/org/tenant scoped)\n const where: any = { isActive: true }\n where.$and = [\n { $or: [ { organizationId: auth.orgId ?? undefined as any }, { organizationId: null } ] },\n { $or: [ { tenantId: auth.tenantId ?? undefined as any }, { tenantId: null } ] },\n ]\n const customs = await em.find(CustomEntity as any, where as any, { orderBy: { entityId: 'asc' } as any })\n // Resolve overlay precedence: prefer organization/tenant-specific over global\n const customByEntityId = new Map<string, any>()\n for (const c of customs as any[]) {\n const specificity = (c.organizationId ? 2 : 0) + (c.tenantId ? 1 : 0)\n const prev = customByEntityId.get(c.entityId)\n const prevSpec = prev ? ((prev.organizationId ? 2 : 0) + (prev.tenantId ? 1 : 0)) : -1\n if (!prev || specificity > prevSpec) customByEntityId.set(c.entityId, c)\n }\n\n const custom = Array.from(customByEntityId.values())\n .filter((c) => isSystemEntitySelectable(c.entityId))\n .map((c) => ({\n entityId: c.entityId,\n source: 'custom' as const,\n label: c.label,\n description: c.description ?? undefined,\n labelField: (c as any).labelField ?? undefined,\n defaultEditor: (c as any).defaultEditor ?? undefined,\n showInSidebar: (c as any).showInSidebar ?? false,\n accessRestricted: (c as any).accessRestricted ?? false,\n updatedAt: c.updatedAt instanceof Date ? c.updatedAt.toISOString() : (c.updatedAt ?? undefined),\n }))\n\n const byId = new Map<string, any>()\n for (const g of generated) byId.set(g.entityId, g)\n for (const cu of custom) {\n const existing = byId.get(cu.entityId)\n byId.set(cu.entityId, { ...existing, ...cu, source: existing?.source ?? cu.source })\n }\n\n const visibleEntityIds = new Set(\n Array.from(byId.values())\n .filter((item: any) => {\n const isOrmBacked = isOrmBackedSystemEntityId(em, item.entityId)\n const declaredRestriction = isOrmBacked\n ? undefined\n : getDeclaredCustomEntityRestriction(item.entityId)\n return canReadEntityMetadata({\n entityId: item.entityId,\n isCustomEntity: !isOrmBacked\n && (item.source === 'custom' || declaredRestriction !== undefined),\n isRestricted: declaredRestriction ?? item.accessRestricted === true,\n acl,\n })\n })\n .map((item: any) => item.entityId),\n )\n if (!visibleEntityIds.size) return NextResponse.json({ items: [] })\n\n // Count field definitions scoped to current tenant/org (same scoping as custom entities)\n const defsWhere: any = { isActive: true, entityId: { $in: Array.from(visibleEntityIds) } }\n defsWhere.$and = [\n //{ $or: [ { organizationId: auth.orgId ?? undefined as any }, { organizationId: null } ] }, // the entities and custom fields are defined per tenant\n { tenantId: auth.tenantId ?? undefined as any },\n ]\n const defs = await em.find(CustomFieldDef as any, defsWhere as any)\n // Count distinct field names (keys) per entityId\n const keySets = new Map<string, Set<string>>()\n for (const d of defs as any[]) {\n const eid = String(d.entityId)\n const k = String(d.key)\n if (!isSystemEntitySelectable(eid)) continue\n const set = keySets.get(eid) || new Set<string>()\n set.add(k)\n keySets.set(eid, set)\n }\n const counts: Record<string, number> = {}\n for (const [eid, set] of keySets.entries()) counts[eid] = set.size\n\n const items = Array.from(byId.values())\n .filter((item: any) => visibleEntityIds.has(item.entityId))\n .map((item: any) => ({ ...item, count: counts[item.entityId] || 0 }))\n return NextResponse.json({ items })\n}\n\nexport async function POST(req: Request) {\n const auth = await getAuthFromRequest(req)\n if (!auth || !auth.orgId) return NextResponse.json({ error: 'Unauthorized' }, { status: 401 })\n\n let body: any\n try { body = await req.json() } catch { return NextResponse.json({ error: 'Invalid JSON' }, { status: 400 }) }\n const parsed = upsertCustomEntitySchema.safeParse(body)\n if (!parsed.success) {\n return NextResponse.json({ error: 'Validation failed', details: parsed.error.flatten() }, { status: 400 })\n }\n const input = parsed.data\n // Distinguish \"caller did not send the field\" (apply tenant default-restricted\n // policy on create) from \"caller explicitly chose false\".\n const accessRestrictedExplicit = body != null && typeof body === 'object'\n && Object.prototype.hasOwnProperty.call(body, 'accessRestricted')\n\n const container = await createRequestContainer()\n const { resolve } = container\n const em = resolve('em') as any\n\n // A registration for a module-declared, table-backed system entity would flip\n // query-engine classification to doc storage for the whole entity type (#2939's\n // failure mode via another door) \u2014 refuse to create one.\n if (isOrmBackedSystemEntityId(em, input.entityId)) {\n return NextResponse.json(\n { error: 'System entities cannot be registered as custom entities', code: SYSTEM_ENTITY_RECORDS_BLOCKED_CODE, entityId: input.entityId },\n { status: 400 },\n )\n }\n\n const where: any = { entityId: input.entityId, organizationId: auth.orgId ?? null, tenantId: auth.tenantId ?? null }\n let ent = await em.findOne(CustomEntity, where)\n if (ent) {\n try {\n await enforceCommandOptimisticLockWithGuards(container, {\n resourceKind: CUSTOM_ENTITY_DEFINITION_RESOURCE_KIND,\n resourceId: ent.id,\n current: ent.updatedAt ?? null,\n request: req,\n })\n } catch (lockError) {\n if (isCrudHttpError(lockError)) {\n return NextResponse.json(lockError.body, { status: lockError.status })\n }\n throw lockError\n }\n }\n\n const guard = await beginEntitiesMutationGuard({\n container,\n auth,\n req,\n resourceKind: ENTITY_DEFINITION_RESOURCE_KIND,\n resourceId: ent ? ent.id : input.entityId,\n operation: ent ? 'update' : 'create',\n mutationPayload: input as unknown as Record<string, unknown>,\n })\n if (guard.blockedResponse) return guard.blockedResponse\n\n const isCreate = !ent\n if (!ent) ent = em.create(CustomEntity, { ...where, createdAt: new Date() })\n ent.label = input.label\n ent.description = input.description ?? null\n ent.isActive = input.isActive ?? true\n ent.labelField = input.labelField ?? ent.labelField ?? null\n ent.defaultEditor = input.defaultEditor ?? ent.defaultEditor ?? null\n ent.showInSidebar = input.showInSidebar ?? ent.showInSidebar ?? false\n // Never silently flip a security control on a partial upsert. `access_restricted`\n // changes ONLY when the caller explicitly sends the field. On create without it,\n // fall back to the tenant default-restricted policy; on update without it, keep\n // the entity's current value (a metadata-only update must not un-restrict).\n if (accessRestrictedExplicit) {\n ent.accessRestricted = input.accessRestricted === true\n } else if (isCreate) {\n let policyDefault = false\n try {\n const moduleConfigService = resolve('moduleConfigService') as {\n getValue: (m: string, n: string, o?: { defaultValue?: unknown; scope?: { tenantId?: string | null } }) => Promise<unknown>\n }\n policyDefault = (await moduleConfigService.getValue('entities', 'newEntitiesRestrictedByDefault', {\n defaultValue: false,\n scope: { tenantId: auth.tenantId ?? null },\n })) === true\n } catch {}\n ent.accessRestricted = policyDefault\n } else {\n ent.accessRestricted = ent.accessRestricted ?? false\n }\n ent.updatedAt = new Date()\n em.persist(ent)\n await em.flush()\n await guard.runAfterSuccess()\n // Invalidate sidebar/nav cache for tenant scope (also when tenantId is null)\n try {\n const cache = (await createRequestContainer()).resolve('cache') as any\n if (cache) {\n await cache.deleteByTags([`nav:entities:${auth.tenantId || 'null'}`])\n }\n } catch {}\n return NextResponse.json({ ok: true, item: { id: ent.id, entityId: ent.entityId, label: ent.label, description: ent.description ?? undefined } })\n}\n\nexport async function DELETE(req: Request) {\n const auth = await getAuthFromRequest(req)\n if (!auth || !auth.orgId) return NextResponse.json({ error: 'Unauthorized' }, { status: 401 })\n let body: any\n try { body = await req.json() } catch { return NextResponse.json({ error: 'Invalid JSON' }, { status: 400 }) }\n const entityId = body?.entityId\n if (!entityId) return NextResponse.json({ error: 'entityId is required' }, { status: 400 })\n\n const container = await createRequestContainer()\n const { resolve } = container\n const em = resolve('em') as any\n\n const where: any = { entityId, organizationId: auth.orgId ?? null, tenantId: auth.tenantId ?? null }\n const ent = await em.findOne(CustomEntity, where)\n if (!ent) return NextResponse.json({ error: 'Not found' }, { status: 404 })\n\n const guard = await beginEntitiesMutationGuard({\n container,\n auth,\n req,\n resourceKind: ENTITY_DEFINITION_RESOURCE_KIND,\n resourceId: ent.id,\n operation: 'delete',\n mutationPayload: { entityId },\n })\n if (guard.blockedResponse) return guard.blockedResponse\n\n ent.isActive = false\n ent.updatedAt = new Date()\n ent.deletedAt = ent.deletedAt ?? new Date()\n em.persist(ent)\n await em.flush()\n await guard.runAfterSuccess()\n // Invalidate sidebar/nav cache for tenant scope (also when tenantId is null)\n try {\n const cache = (await createRequestContainer()).resolve('cache') as any\n if (cache) {\n await cache.deleteByTags([`nav:entities:${auth.tenantId || 'null'}`])\n }\n } catch {}\n return NextResponse.json({ ok: true })\n}\n\nconst entitySummarySchema = z.object({\n entityId: z.string(),\n source: z.enum(['code', 'custom']),\n label: z.string(),\n description: z.string().optional(),\n labelField: z.string().optional(),\n defaultEditor: z.string().optional(),\n showInSidebar: z.boolean().optional(),\n accessRestricted: z.boolean().optional(),\n updatedAt: z.string().optional(),\n count: z.number(),\n})\n\nconst entityListResponseSchema = z.object({\n items: z.array(entitySummarySchema),\n})\n\nconst deleteEntityRequestSchema = z.object({\n entityId: z.string(),\n})\n\nconst upsertCustomEntityResponseSchema = z.object({\n ok: z.literal(true),\n item: z.object({\n id: z.string().uuid(),\n entityId: z.string(),\n label: z.string(),\n description: z.string().optional(),\n }),\n})\n\nexport const openApi: OpenApiRouteDoc = {\n tag: 'Entities',\n summary: 'Manage custom entities',\n methods: {\n GET: {\n summary: 'List available entities',\n description: 'Returns generated and custom entities scoped to the caller with field counts per entity.',\n responses: [\n {\n status: 200,\n description: 'List of entities',\n schema: entityListResponseSchema,\n },\n {\n status: 401,\n description: 'Missing authentication',\n schema: z.object({ error: z.string() }),\n },\n ],\n },\n POST: {\n summary: 'Upsert custom entity',\n description: 'Creates or updates a tenant/org scoped custom entity definition.',\n requestBody: {\n contentType: 'application/json',\n schema: upsertCustomEntitySchema,\n },\n responses: [\n {\n status: 200,\n description: 'Entity saved',\n schema: upsertCustomEntityResponseSchema,\n },\n {\n status: 400,\n description: 'Validation error',\n schema: z.object({\n error: z.string(),\n details: z.any().optional(),\n }),\n },\n {\n status: 401,\n description: 'Missing authentication',\n schema: z.object({ error: z.string() }),\n },\n ],\n },\n DELETE: {\n summary: 'Soft delete custom entity',\n description: 'Marks the specified custom entity inactive within the current scope.',\n requestBody: {\n contentType: 'application/json',\n schema: deleteEntityRequestSchema,\n },\n responses: [\n {\n status: 200,\n description: 'Entity deleted',\n schema: z.object({ ok: z.boolean() }),\n },\n {\n status: 400,\n description: 'Missing entity id',\n schema: z.object({ error: z.string() }),\n },\n {\n status: 401,\n description: 'Missing authentication',\n schema: z.object({ error: z.string() }),\n },\n {\n status: 404,\n description: 'Entity not found in scope',\n schema: z.object({ error: z.string() }),\n },\n ],\n },\n },\n}\n"],
|
|
5
|
+
"mappings": "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",
|
|
6
6
|
"names": []
|
|
7
7
|
}
|
|
@@ -9,30 +9,9 @@ import { parseBooleanToken, parseBooleanWithDefault } from "@open-mercato/shared
|
|
|
9
9
|
import { parseCommaSeparatedList } from "@open-mercato/shared/lib/string";
|
|
10
10
|
import { enforceCommandOptimisticLockWithGuards } from "@open-mercato/shared/lib/crud/optimistic-lock-command";
|
|
11
11
|
import { isCrudHttpError } from "@open-mercato/shared/lib/crud/errors";
|
|
12
|
-
import {
|
|
13
|
-
import { assertEntityAclForRequest } from "../lib/entityAcl.js";
|
|
12
|
+
import { assertEntityAclForRequest, getDeclaredCustomEntityRestriction } from "../lib/entityAcl.js";
|
|
14
13
|
import { createLogger } from "@open-mercato/shared/lib/logger";
|
|
15
14
|
const logger = createLogger("entities").child({ component: "records" });
|
|
16
|
-
let declaredCustomEntityRestricted = null;
|
|
17
|
-
function loadDeclaredCustomEntities() {
|
|
18
|
-
if (declaredCustomEntityRestricted === null) {
|
|
19
|
-
try {
|
|
20
|
-
const mods = getModules();
|
|
21
|
-
const map = /* @__PURE__ */ new Map();
|
|
22
|
-
for (const mod of mods ?? []) {
|
|
23
|
-
for (const spec of mod?.customEntities ?? []) {
|
|
24
|
-
if (spec?.id) map.set(spec.id, spec.accessRestricted === true);
|
|
25
|
-
}
|
|
26
|
-
}
|
|
27
|
-
declaredCustomEntityRestricted = map;
|
|
28
|
-
} catch {
|
|
29
|
-
}
|
|
30
|
-
}
|
|
31
|
-
return declaredCustomEntityRestricted ?? /* @__PURE__ */ new Map();
|
|
32
|
-
}
|
|
33
|
-
function isDeclaredCustomEntity(entityId) {
|
|
34
|
-
return loadDeclaredCustomEntities().has(entityId);
|
|
35
|
-
}
|
|
36
15
|
async function findScopedCustomEntity(em, CustomEntity, entityId, scope) {
|
|
37
16
|
const { tenantId, organizationId } = scope;
|
|
38
17
|
const candidates = [
|
|
@@ -53,8 +32,8 @@ async function findScopedCustomEntity(em, CustomEntity, entityId, scope) {
|
|
|
53
32
|
const CUSTOM_ENTITY_RECORD_RESOURCE_KIND = "entities.record";
|
|
54
33
|
async function classifyRecordsEntity(em, entityId, scope) {
|
|
55
34
|
if (isOrmBackedSystemEntityId(em, entityId)) return { kind: "system", restricted: false };
|
|
56
|
-
const
|
|
57
|
-
if (
|
|
35
|
+
const declaredRestriction = getDeclaredCustomEntityRestriction(entityId);
|
|
36
|
+
if (declaredRestriction !== void 0) return { kind: "custom", restricted: declaredRestriction };
|
|
58
37
|
try {
|
|
59
38
|
const { CustomEntity } = await import("../data/entities.js");
|
|
60
39
|
const scoped = await findScopedCustomEntity(em, CustomEntity, entityId, scope);
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"version": 3,
|
|
3
3
|
"sources": ["../../../../src/modules/entities/api/records.ts"],
|
|
4
|
-
"sourcesContent": ["import { NextResponse } from 'next/server'\nimport { z } from 'zod'\nimport { createRequestContainer } from '@open-mercato/shared/lib/di/container'\nimport { getAuthFromRequest } from '@open-mercato/shared/lib/auth/server'\nimport type { QueryEngine, QueryOptions, Where, Sort } from '@open-mercato/shared/lib/query/types'\nimport { normalizeExportFormat, serializeExport, defaultExportFilename, ensureColumns } from '@open-mercato/shared/lib/crud/exporters'\nimport type { RbacService } from '@open-mercato/core/modules/auth/services/rbacService'\nimport { resolveOrganizationScope, getSelectedOrganizationFromRequest } from '@open-mercato/core/modules/directory/utils/organizationScope'\nimport { SYSTEM_ENTITY_RECORDS_BLOCKED_CODE, isOrmBackedSystemEntityId } from '@open-mercato/shared/lib/data/engine'\nimport { parseBooleanToken, parseBooleanWithDefault } from '@open-mercato/shared/lib/boolean'\nimport { parseCommaSeparatedList } from '@open-mercato/shared/lib/string'\nimport { setRecordCustomFields } from '../lib/helpers'\nimport { CustomFieldValue } from '../data/entities'\nimport type { OpenApiRouteDoc } from '@open-mercato/shared/lib/openapi'\nimport { enforceCommandOptimisticLockWithGuards } from '@open-mercato/shared/lib/crud/optimistic-lock-command'\nimport { isCrudHttpError } from '@open-mercato/shared/lib/crud/errors'\nimport { getModules } from '@open-mercato/shared/lib/i18n/server'\nimport { assertEntityAclForRequest } from '../lib/entityAcl'\nimport { createLogger } from '@open-mercato/shared/lib/logger'\n\nconst logger = createLogger('entities').child({ component: 'records' })\n\nlet declaredCustomEntityRestricted: Map<string, boolean> | null = null\nfunction loadDeclaredCustomEntities(): Map<string, boolean> {\n if (declaredCustomEntityRestricted === null) {\n try {\n const mods = getModules() as Array<{ customEntities?: Array<{ id?: string; accessRestricted?: boolean }> }>\n const map = new Map<string, boolean>()\n for (const mod of mods ?? []) {\n for (const spec of mod?.customEntities ?? []) {\n if (spec?.id) map.set(spec.id, spec.accessRestricted === true)\n }\n }\n // Cache even when empty so we don't rebuild on every request (and fall back\n // to the DB lookup unnecessarily). Only a thrown getModules() leaves it null\n // so a genuinely-uninitialized registry is retried.\n declaredCustomEntityRestricted = map\n } catch {}\n }\n return declaredCustomEntityRestricted ?? new Map<string, boolean>()\n}\nfunction isDeclaredCustomEntity(entityId: string): boolean {\n return loadDeclaredCustomEntities().has(entityId)\n}\n\ntype RecordsEntityScope = { tenantId: string | null; organizationId: string | null }\n\n// Resolve the CustomEntity registration that applies to THIS caller, most-specific\n// first (org+tenant \u2192 tenant-global \u2192 instance-global), mirroring the overlay\n// precedence used by the entity-definitions list. Scoping matters because the\n// row's `access_restricted` flag is a security control: an unscoped lookup could\n// read another tenant's row for a colliding entityId (e.g. `user:vendors`) and\n// mis-decide the restriction. Returns null when the caller's scope has no row.\nasync function findScopedCustomEntity(em: any, CustomEntity: any, entityId: string, scope: RecordsEntityScope) {\n const { tenantId, organizationId } = scope\n const candidates: Array<Record<string, unknown>> = [\n { entityId, organizationId, tenantId },\n { entityId, organizationId: null, tenantId },\n { entityId, organizationId: null, tenantId: null },\n ]\n const seen = new Set<string>()\n for (const where of candidates) {\n const key = JSON.stringify(where)\n if (seen.has(key)) continue\n seen.add(key)\n const row = await em.findOne(CustomEntity as any, where)\n if (row) return row\n }\n return null\n}\n\nconst CUSTOM_ENTITY_RECORD_RESOURCE_KIND = 'entities.record'\n\ntype RecordsEntityKind = 'system' | 'custom' | 'unknown'\n\n// `restricted` is meaningful only when `kind === 'custom'`; it drives the\n// per-entity ACL gate in `assertEntityAclForRequest`.\ntype RecordsEntityClassification = { kind: RecordsEntityKind; restricted: boolean }\n\n// This surface manages doc-storage records, which exist for CUSTOM entities only.\n// Module-declared ids backed by a registered ORM table are system entities \u2014 their\n// records live in their own module tables/APIs, and stray doc rows for them poisoned\n// read-path classification platform-wide (#2939) \u2014 so they are rejected outright. The\n// previous fallback that classified an entity by the mere presence of\n// `custom_entities_storage` rows is gone: within the allowed set, declaration (ce.ts)\n// or an active `custom_entities` registration is authoritative.\nasync function classifyRecordsEntity(em: any, entityId: string, scope: RecordsEntityScope): Promise<RecordsEntityClassification> {\n if (isOrmBackedSystemEntityId(em, entityId)) return { kind: 'system', restricted: false }\n const declared = loadDeclaredCustomEntities()\n if (declared.has(entityId)) return { kind: 'custom', restricted: declared.get(entityId) === true }\n try {\n const { CustomEntity } = await import('../data/entities')\n // Restriction is decided from the row that applies to THIS caller's scope so\n // a colliding entityId in another tenant can't flip the flag.\n const scoped = await findScopedCustomEntity(em, CustomEntity, entityId, scope)\n if (scoped) return { kind: 'custom', restricted: (scoped as any).accessRestricted === true }\n // No in-scope registration: preserve the historical custom-vs-unknown\n // classification (any registration row \u2014 active or soft-deleted \u2014 proves the\n // id is custom; records persist beyond soft delete, TC-ENTITIES-006). A row\n // outside the caller's scope never marks the entity restricted for them, and\n // the record query is itself tenant/org-scoped, so this cannot leak data.\n const anyRow = await em.findOne(CustomEntity as any, { entityId })\n if (anyRow) return { kind: 'custom', restricted: false }\n } catch {}\n return { kind: 'unknown', restricted: false }\n}\n\nfunction systemEntityRecordsRejection(entityId: string) {\n return NextResponse.json(\n { error: 'Records are available for custom entities only', code: SYSTEM_ENTITY_RECORDS_BLOCKED_CODE, entityId },\n { status: 400 },\n )\n}\n\nasync function readCustomEntityRecordUpdatedAt(\n em: any,\n input: { entityType: string; entityId: string; organizationId: string | null },\n): Promise<string | null> {\n try {\n const db = em.getKysely()\n let query = db\n .selectFrom('custom_entities_storage' as any)\n .select(['updated_at' as any])\n .where('entity_type' as any, '=', input.entityType)\n .where('entity_id' as any, '=', input.entityId)\n query = input.organizationId === null\n ? query.where('organization_id' as any, 'is', null as any)\n : query.where('organization_id' as any, '=', input.organizationId)\n const row = await query.executeTakeFirst()\n const value = (row as any)?.updated_at\n if (value instanceof Date) return value.toISOString()\n if (typeof value === 'string' && value.length > 0) return value\n return null\n } catch {\n return null\n }\n}\n\nexport const metadata = {\n GET: { requireAuth: true, requireFeatures: ['entities.records.view'] },\n POST: { requireAuth: true, requireFeatures: ['entities.records.manage'] },\n PUT: { requireAuth: true, requireFeatures: ['entities.records.manage'] },\n DELETE: { requireAuth: true, requireFeatures: ['entities.records.manage'] },\n}\n\nconst DEFAULT_EXPORT_PAGE_SIZE = 1000\n\nconst listRecordsQuerySchema = z\n .object({\n entityId: z.string().min(1),\n page: z.coerce.number().int().min(1).optional(),\n pageSize: z.coerce.number().int().min(1).max(100).optional(),\n sortField: z.string().optional(),\n sortDir: z.enum(['asc', 'desc']).optional(),\n search: z.string().optional(),\n searchFields: z.string().optional(),\n withDeleted: z.coerce.boolean().optional(),\n format: z.enum(['csv', 'json', 'xml', 'markdown']).optional(),\n exportScope: z.enum(['full']).optional(),\n export_scope: z.enum(['full']).optional(),\n all: z.coerce.boolean().optional(),\n full: z.coerce.boolean().optional(),\n })\n .passthrough()\n\nconst recordItemSchema = z.record(z.string(), z.any())\n\nconst listRecordsResponseSchema = z.object({\n items: z.array(recordItemSchema),\n total: z.number(),\n page: z.number(),\n pageSize: z.number(),\n totalPages: z.number(),\n})\n\nexport async function GET(req: Request) {\n const url = new URL(req.url)\n const entityId = url.searchParams.get('entityId') || ''\n if (!entityId) return NextResponse.json({ error: 'entityId is required' }, { status: 400 })\n\n const auth = await getAuthFromRequest(req)\n if (!auth || !auth.tenantId) return NextResponse.json({ error: 'Unauthorized' }, { status: 401 })\n\n const requestedExport = normalizeExportFormat(url.searchParams.get('format'))\n const exportScopeRaw = (url.searchParams.get('exportScope') || url.searchParams.get('export_scope') || '').toLowerCase()\n const exportFullRequested = requestedExport != null && (exportScopeRaw === 'full' || parseBooleanWithDefault(url.searchParams.get('full'), false))\n const exportAll = parseBooleanWithDefault(url.searchParams.get('all'), false)\n const noPagination = exportAll || requestedExport != null\n const page = noPagination ? 1 : Math.max(parseInt(url.searchParams.get('page') || '1', 10) || 1, 1)\n const basePageSize = Math.min(Math.max(parseInt(url.searchParams.get('pageSize') || '50', 10) || 50, 1), 100)\n const pageSize = noPagination ? Math.max(basePageSize, DEFAULT_EXPORT_PAGE_SIZE) : basePageSize\n const sortField = url.searchParams.get('sortField') || 'id'\n const sortDir = (url.searchParams.get('sortDir') || 'asc').toLowerCase() === 'desc' ? 'desc' : 'asc'\n const withDeleted = parseBooleanWithDefault(url.searchParams.get('withDeleted'), false)\n const searchTerm = (url.searchParams.get('search') || '').trim()\n const searchFields = parseCommaSeparatedList(url.searchParams.get('searchFields'))\n\n const qpEntries: Array<[string, string]> = []\n for (const [key, val] of url.searchParams.entries()) {\n if (['entityId','page','pageSize','sortField','sortDir','withDeleted','format','exportScope','export_scope','all','full','search','searchFields'].includes(key)) continue\n qpEntries.push([key, val])\n }\n\n try {\n const { resolve } = await createRequestContainer()\n const qe = resolve('queryEngine') as QueryEngine\n const em = resolve('em') as any\n const rbac = resolve('rbacService') as RbacService\n const scope = await resolveOrganizationScope({ em, rbac, auth, selectedId: getSelectedOrganizationFromRequest(req) })\n let organizationIds: string[] | null = scope.filterIds\n // Module-declared custom entities (ce.ts) carry frozen system-style ids and are never\n // registered in `custom_entities`, so classification checks the declared registry plus\n // active registrations. System (table-backed) ids are rejected above; for the allowed\n // set `isCustomEntity` drives mapRow's cf_ stripping so the edit form reads back values.\n const { kind: entityKind, restricted: isRestricted } = await classifyRecordsEntity(em, entityId, { tenantId: auth.tenantId ?? null, organizationId: scope.selectedId ?? auth.orgId ?? null })\n if (entityKind === 'system') return systemEntityRecordsRejection(entityId)\n const isCustomEntity = entityKind === 'custom'\n await assertEntityAclForRequest({ auth, entityId, action: 'view', isCustomEntity, isRestricted, rbac })\n if (organizationIds && organizationIds.length === 0) {\n return NextResponse.json({ items: [], total: 0, page, pageSize, totalPages: 0 })\n }\n const normalizeCustomEntityValue = (value: unknown) => {\n if (Array.isArray(value)) {\n return value.map((entry) => {\n if (typeof entry !== 'string') return entry\n const parsed = parseBooleanToken(entry)\n return parsed === null ? entry : parsed\n })\n }\n if (typeof value !== 'string') return value\n const parsed = parseBooleanToken(value)\n return parsed === null ? value : parsed\n }\n const mapRow = (row: any) => {\n if (!isCustomEntity || !row || typeof row !== 'object') return row\n const out: Record<string, unknown> = {}\n for (const [k, v] of Object.entries(row)) {\n if (k.startsWith('cf_')) out[k.replace(/^cf_/, '')] = normalizeCustomEntityValue(v)\n else out[k] = v\n }\n return out\n }\n const mapFullRow = (row: any) => {\n if (!row || typeof row !== 'object') return row\n return { ...(row as Record<string, unknown>) }\n }\n // Build filters with awareness of custom-entity mode\n const filtersObj: Where<any> = {}\n const buildFilter = (key: string, val: string, allowAnyKey: boolean) => {\n if (key.startsWith('cf_')) {\n if (key.endsWith('In')) {\n const base = key.slice(0, -2)\n const values = parseCommaSeparatedList(val)\n ;(filtersObj as any)[base] = { $in: values }\n } else {\n if (val.includes(',')) {\n const values = parseCommaSeparatedList(val)\n ;(filtersObj as any)[key] = { $in: values }\n } else {\n const parsed = parseBooleanToken(val)\n ;(filtersObj as any)[key] = parsed === null ? val : parsed\n }\n }\n } else if (allowAnyKey) {\n if (val.includes(',')) {\n const values = parseCommaSeparatedList(val)\n ;(filtersObj as any)[key] = { $in: values }\n } else {\n const parsed = parseBooleanToken(val)\n ;(filtersObj as any)[key] = parsed === null ? val : parsed\n }\n } else {\n if (['id', 'created_at', 'updated_at', 'deleted_at', 'name', 'title', 'email'].includes(key)) {\n ;(filtersObj as any)[key] = val\n }\n }\n }\n\n if (organizationIds && organizationIds.length) {\n (filtersObj as any).organization_id = { $in: organizationIds }\n }\n const qopts: QueryOptions = {\n tenantId: auth.tenantId!,\n includeCustomFields: true,\n page: { page, pageSize },\n sort: [{ field: sortField as any, dir: sortDir as any }] as Sort[],\n filters: filtersObj as any,\n withDeleted,\n }\n if (organizationIds && organizationIds.length) {\n qopts.organizationIds = organizationIds\n }\n // Allowed entities are doc-storage-backed by definition (system ids were rejected\n // above) \u2014 direct the engine to doc storage explicitly so reads stay deterministic\n // even before the first record exists.\n if (isCustomEntity) qopts.forceCustomEntityStorage = true\n for (const [k, v] of qpEntries) buildFilter(k, v, isCustomEntity)\n // Server-side full-result search: match the term against the requested fields\n // (defaults to `id`) before pagination so totals/exports stay consistent with\n // the active search instead of filtering only the current client page (#3229).\n if (searchTerm) {\n const fields = searchFields.length ? searchFields : ['id']\n const pattern = `%${searchTerm}%`\n const orClauses = fields.map((field) => ({ [field]: { $ilike: pattern } }))\n ;(filtersObj as any).$or = orClauses\n }\n const res = await qe.query(entityId as any, qopts)\n const rawItems = res.items || []\n const viewPageItems = rawItems.map(mapRow)\n const fullPageItems = rawItems.map(mapFullRow)\n\n // Expose `updated_at` on custom-entity records. The query engine returns only\n // the `doc` fields + `id`, dropping the base `updated_at` column \u2014 which made\n // optimistic locking impossible end-to-end (no version for the edit page to\n // round-trip as the lock header). Batch-read it from storage and merge it in.\n if (isCustomEntity && viewPageItems.length) {\n try {\n const recordIds = viewPageItems\n .map((it: any) => it?.id)\n .filter((v: any): v is string => typeof v === 'string' && v.length > 0)\n if (recordIds.length) {\n const db = em.getKysely()\n const rows = await db\n .selectFrom('custom_entities_storage' as any)\n .select(['entity_id' as any, 'updated_at' as any])\n .where('entity_type' as any, '=', entityId)\n .where('entity_id' as any, 'in', recordIds as any)\n .execute()\n const updatedById = new Map<string, string>()\n for (const row of rows as any[]) {\n const value = row?.updated_at\n const iso = value instanceof Date ? value.toISOString() : (typeof value === 'string' && value.length > 0 ? value : null)\n if (iso && row?.entity_id) updatedById.set(String(row.entity_id), iso)\n }\n for (const item of viewPageItems as any[]) {\n const iso = updatedById.get(String(item?.id))\n if (iso) {\n item.updated_at = iso\n item.updatedAt = iso\n }\n }\n }\n } catch { /* best-effort: locking simply will not engage if storage is unavailable */ }\n }\n\n const total = typeof res.total === 'number' ? res.total : rawItems.length\n const effectivePageSize = res.pageSize || pageSize\n const payload = {\n items: viewPageItems,\n total,\n page: res.page || page,\n pageSize: effectivePageSize,\n totalPages: Math.ceil(total / (effectivePageSize || 1)),\n }\n\n if (requestedExport) {\n let exportItems: any[] = exportFullRequested ? [...fullPageItems] : [...viewPageItems]\n if (total > exportItems.length) {\n let nextPage = 2\n while (exportItems.length < total) {\n const nextRes = await qe.query(entityId as any, {\n ...qopts,\n page: { page: nextPage, pageSize },\n })\n const nextRawItems = nextRes.items || []\n if (!nextRawItems.length) break\n const nextViewItems = nextRawItems.map(mapRow)\n const nextFullItems = nextRawItems.map(mapFullRow)\n const nextBatch = exportFullRequested ? nextFullItems : nextViewItems\n exportItems.push(...nextBatch)\n if (nextBatch.length < pageSize) break\n nextPage += 1\n }\n }\n const prepared = {\n columns: ensureColumns(exportItems),\n rows: exportItems,\n }\n const filenameBase = exportFullRequested ? `${entityId || 'records'}_full` : entityId || 'records'\n const serialized = serializeExport(prepared, requestedExport)\n const filename = defaultExportFilename(filenameBase, requestedExport)\n return new Response(serialized.body, {\n headers: {\n 'content-type': serialized.contentType,\n 'content-disposition': `attachment; filename=\"${filename}\"`,\n },\n })\n }\n\n return NextResponse.json(payload)\n } catch (e) {\n if (isCrudHttpError(e)) return NextResponse.json(e.body, { status: e.status })\n logger.error('Records GET failed', { err: e })\n return NextResponse.json({ error: 'Internal server error' }, { status: 500 })\n }\n}\n\nconst postBodySchema = z.object({\n entityId: z.string().min(1),\n recordId: z.string().min(1).optional(),\n values: z.record(z.string(), z.any()).default({}),\n})\n\nconst putBodySchema = z.object({\n entityId: z.string().min(1),\n recordId: z.string().min(1),\n values: z.record(z.string(), z.any()).default({}),\n})\n\nconst mutationResponseSchema = z.object({\n ok: z.literal(true),\n item: z\n .object({\n entityId: z.string(),\n recordId: z.string(),\n })\n .optional(),\n})\n\nexport async function POST(req: Request) {\n const auth = await getAuthFromRequest(req)\n if (!auth || !auth.tenantId) return NextResponse.json({ error: 'Unauthorized' }, { status: 401 })\n\n let json: unknown\n try { json = await req.json() } catch { return NextResponse.json({ error: 'Invalid JSON' }, { status: 400 }) }\n const parsed = postBodySchema.safeParse(json)\n if (!parsed.success) return NextResponse.json({ error: 'Validation failed', details: parsed.error.flatten() }, { status: 400 })\n const { entityId } = parsed.data\n let { recordId, values } = parsed.data as { recordId?: string; values: Record<string, any> }\n\n try {\n const { resolve } = await createRequestContainer()\n const de = resolve('dataEngine') as any\n const em = resolve('em') as any\n const rbac = resolve('rbacService') as RbacService\n const scope = await resolveOrganizationScope({ em, rbac, auth, selectedId: getSelectedOrganizationFromRequest(req) })\n const targetOrgId = scope.selectedId ?? auth.orgId\n if (!targetOrgId) return NextResponse.json({ error: 'Organization context is required' }, { status: 400 })\n const { kind: entityKind, restricted: isRestricted } = await classifyRecordsEntity(em, entityId, { tenantId: auth.tenantId ?? null, organizationId: scope.selectedId ?? auth.orgId ?? null })\n if (entityKind === 'system') return systemEntityRecordsRejection(entityId)\n const isCustomEntity = entityKind === 'custom'\n await assertEntityAclForRequest({ auth, entityId, action: 'manage', isCustomEntity, isRestricted, rbac })\n // Strip reserved record/system columns the edit form echoes back from the loaded record\n // (`id`, plus `updated_at`/`updatedAt` used for optimistic locking). They are not custom\n // fields; without this they validate as cf_id / cf_updated_at / cf_updatedAt and are\n // rejected as \"Unknown custom field\", which fails EVERY custom-entity edit-form save.\n for (const reservedKey of ['id', 'created_at', 'createdAt', 'updated_at', 'updatedAt', 'deleted_at', 'deletedAt']) {\n delete (values as any)[reservedKey]\n }\n const norm = normalizeValues(values)\n\n // Validate against custom field definitions\n try {\n const { validateCustomFieldValuesServer } = await import('../lib/validation')\n const check = await validateCustomFieldValuesServer(em, { entityId, organizationId: targetOrgId, tenantId: auth.tenantId!, values: norm, rejectUndeclaredKeys: true })\n if (!check.ok) return NextResponse.json({ error: 'Validation failed', fields: check.fieldErrors }, { status: 400 })\n } catch { /* ignore if helper missing */ }\n\n const normalizedRecordId = (() => {\n const raw = String(recordId || '').trim()\n if (!raw) return undefined\n const low = raw.toLowerCase()\n if (low === 'create' || low === 'new' || low === 'null' || low === 'undefined') return undefined\n // Enforce UUID only; any non-uuid is ignored so we generate one in the DE\n const uuid = /^[0-9a-f]{8}-[0-9a-f]{4}-[1-5][0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/i\n return uuid.test(raw) ? raw : undefined\n })()\n const { id } = await de.createCustomEntityRecord({\n entityId,\n recordId: normalizedRecordId,\n organizationId: targetOrgId,\n tenantId: auth.tenantId!,\n values: norm,\n })\n\n return NextResponse.json({ ok: true, item: { entityId, recordId: id } })\n } catch (e) {\n if (isCrudHttpError(e)) return NextResponse.json(e.body, { status: e.status })\n logger.error('Records POST failed', { err: e })\n return NextResponse.json({ error: 'Internal server error' }, { status: 500 })\n }\n}\n\n// Avoid zod here to prevent runtime import issues in some environments\nfunction parsePutBody(json: any): { ok: true; data: { entityId: string; recordId: string; values: Record<string, any> } } | { ok: false; error: string } {\n if (!json || typeof json !== 'object') return { ok: false, error: 'Invalid JSON' }\n const entityId = typeof json.entityId === 'string' && json.entityId.length ? json.entityId : ''\n const recordId = typeof json.recordId === 'string' && json.recordId.length ? json.recordId : ''\n const values = (json.values && typeof json.values === 'object') ? json.values as Record<string, any> : {}\n if (!entityId || !recordId) return { ok: false, error: 'entityId and recordId are required' }\n return { ok: true, data: { entityId, recordId, values } }\n}\n\nexport async function PUT(req: Request) {\n const auth = await getAuthFromRequest(req)\n if (!auth || !auth.tenantId) return NextResponse.json({ error: 'Unauthorized' }, { status: 401 })\n\n let json: any\n try { json = await req.json() } catch { return NextResponse.json({ error: 'Invalid JSON' }, { status: 400 }) }\n const parsed = parsePutBody(json)\n if (!parsed.ok) return NextResponse.json({ error: parsed.error }, { status: 400 })\n const { entityId, recordId, values } = parsed.data\n\n try {\n const container = await createRequestContainer()\n const { resolve } = container\n const de = resolve('dataEngine') as any\n const em = resolve('em') as any\n const rbac = resolve('rbacService') as RbacService\n const scope = await resolveOrganizationScope({ em, rbac, auth, selectedId: getSelectedOrganizationFromRequest(req) })\n const targetOrgId = scope.selectedId ?? auth.orgId\n if (!targetOrgId) return NextResponse.json({ error: 'Organization context is required' }, { status: 400 })\n const { kind: entityKind, restricted: isRestricted } = await classifyRecordsEntity(em, entityId, { tenantId: auth.tenantId ?? null, organizationId: scope.selectedId ?? auth.orgId ?? null })\n if (entityKind === 'system') return systemEntityRecordsRejection(entityId)\n const isCustomEntity = entityKind === 'custom'\n await assertEntityAclForRequest({ auth, entityId, action: 'manage', isCustomEntity, isRestricted, rbac })\n // Strip reserved record/system columns the edit form echoes back from the loaded record\n // (`id`, plus `updated_at`/`updatedAt` used for optimistic locking). They are not custom\n // fields; without this they validate as cf_id / cf_updated_at / cf_updatedAt and are\n // rejected as \"Unknown custom field\", which fails EVERY custom-entity edit-form save.\n for (const reservedKey of ['id', 'created_at', 'createdAt', 'updated_at', 'updatedAt', 'deleted_at', 'deletedAt']) {\n delete (values as any)[reservedKey]\n }\n const norm = normalizeValues(values)\n\n // Validate against custom field definitions\n try {\n const { validateCustomFieldValuesServer } = await import('../lib/validation')\n const check = await validateCustomFieldValuesServer(em, { entityId, organizationId: targetOrgId, tenantId: auth.tenantId!, values: norm, rejectUndeclaredKeys: true })\n if (!check.ok) return NextResponse.json({ error: 'Validation failed', fields: check.fieldErrors }, { status: 400 })\n } catch { /* ignore if helper missing */ }\n\n // Normalize recordId: if blank/sentinel/non-uuid => create instead of update\n const uuidRe = /^[0-9a-f]{8}-[0-9a-f]{4}-[1-5][0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/i\n const rid = String(recordId || '').trim()\n const low = rid.toLowerCase()\n const isSentinel = !rid || low === 'create' || low === 'new' || low === 'null' || low === 'undefined'\n const isUuid = uuidRe.test(rid)\n if (isSentinel || !isUuid) {\n const created = await de.createCustomEntityRecord({\n entityId,\n recordId: undefined,\n organizationId: targetOrgId,\n tenantId: auth.tenantId!,\n values: norm,\n })\n return NextResponse.json({ ok: true, item: { entityId, recordId: created.id } })\n }\n\n try {\n const currentUpdatedAt = await readCustomEntityRecordUpdatedAt(em, {\n entityType: entityId,\n entityId: rid,\n organizationId: targetOrgId,\n })\n await enforceCommandOptimisticLockWithGuards(container, {\n resourceKind: CUSTOM_ENTITY_RECORD_RESOURCE_KIND,\n resourceId: rid,\n current: currentUpdatedAt,\n request: req,\n })\n } catch (lockError) {\n if (isCrudHttpError(lockError)) {\n return NextResponse.json(lockError.body, { status: lockError.status })\n }\n throw lockError\n }\n\n await de.updateCustomEntityRecord({\n entityId,\n recordId: rid,\n organizationId: targetOrgId,\n tenantId: auth.tenantId!,\n values: norm,\n })\n return NextResponse.json({ ok: true, item: { entityId, recordId: rid } })\n } catch (e) {\n if (isCrudHttpError(e)) return NextResponse.json(e.body, { status: e.status })\n return NextResponse.json({ error: 'Internal server error' }, { status: 500 })\n }\n}\n\nconst deleteBodySchema = z.object({\n entityId: z.string().min(1),\n recordId: z.string().min(1),\n})\n\nexport async function DELETE(req: Request) {\n const auth = await getAuthFromRequest(req)\n if (!auth || !auth.tenantId) return NextResponse.json({ error: 'Unauthorized' }, { status: 401 })\n\n const url = new URL(req.url)\n const qpEntityId = url.searchParams.get('entityId')\n const qpRecordId = url.searchParams.get('recordId')\n let payload: any = qpEntityId && qpRecordId ? { entityId: qpEntityId, recordId: qpRecordId } : null\n if (!payload) {\n try { payload = await req.json() } catch { payload = null }\n }\n const parsed = deleteBodySchema.safeParse(payload)\n if (!parsed.success) return NextResponse.json({ error: 'Validation failed', details: parsed.error.flatten() }, { status: 400 })\n const { entityId, recordId } = parsed.data\n\n try {\n const container = await createRequestContainer()\n const { resolve } = container\n const de = resolve('dataEngine') as any\n const em = resolve('em') as any\n const rbac = resolve('rbacService') as RbacService\n const scope = await resolveOrganizationScope({ em, rbac, auth, selectedId: getSelectedOrganizationFromRequest(req) })\n const targetOrgId = scope.selectedId ?? auth.orgId\n if (!targetOrgId) return NextResponse.json({ error: 'Organization context is required' }, { status: 400 })\n const { kind: entityKind, restricted: isRestricted } = await classifyRecordsEntity(em, entityId, { tenantId: auth.tenantId ?? null, organizationId: scope.selectedId ?? auth.orgId ?? null })\n if (entityKind === 'system') return systemEntityRecordsRejection(entityId)\n const isCustomEntity = entityKind === 'custom'\n await assertEntityAclForRequest({ auth, entityId, action: 'manage', isCustomEntity, isRestricted, rbac })\n\n try {\n const currentUpdatedAt = await readCustomEntityRecordUpdatedAt(em, {\n entityType: entityId,\n entityId: recordId,\n organizationId: targetOrgId,\n })\n await enforceCommandOptimisticLockWithGuards(container, {\n resourceKind: CUSTOM_ENTITY_RECORD_RESOURCE_KIND,\n resourceId: recordId,\n current: currentUpdatedAt,\n request: req,\n })\n } catch (lockError) {\n if (isCrudHttpError(lockError)) {\n return NextResponse.json(lockError.body, { status: lockError.status })\n }\n throw lockError\n }\n\n await de.deleteCustomEntityRecord({ entityId, recordId, organizationId: targetOrgId, tenantId: auth.tenantId!, soft: true })\n return NextResponse.json({ ok: true })\n } catch (e) {\n if (isCrudHttpError(e)) return NextResponse.json(e.body, { status: e.status })\n return NextResponse.json({ error: 'Internal server error' }, { status: 500 })\n }\n}\n\nfunction normalizeValues(input: Record<string, any>): Record<string, any> {\n const out: Record<string, any> = {}\n for (const [k, v] of Object.entries(input || {})) {\n const key = k.startsWith('cf_') ? k.replace(/^cf_/, '') : k\n out[key] = v\n }\n return out\n}\n\nconst deleteResponseSchema = z.object({\n ok: z.literal(true),\n})\n\nconst errorSchema = z.object({\n error: z.string(),\n}).passthrough()\n\nexport const openApi: OpenApiRouteDoc = {\n tag: 'Entities',\n summary: 'CRUD operations on entity records',\n methods: {\n GET: {\n summary: 'List records',\n description:\n 'Returns paginated records for the supplied entity. Supports custom field filters, exports, and soft-delete toggles.',\n query: listRecordsQuerySchema,\n responses: [\n {\n status: 200,\n description: 'Paginated records',\n schema: listRecordsResponseSchema,\n },\n {\n status: 400,\n description: 'Missing entity id',\n schema: errorSchema,\n },\n {\n status: 401,\n description: 'Missing authentication',\n schema: errorSchema,\n },\n {\n status: 500,\n description: 'Unexpected failure',\n schema: errorSchema,\n },\n ],\n },\n POST: {\n summary: 'Create record',\n description:\n 'Creates a record for the given entity. When `recordId` is omitted or not a UUID the data engine will generate one automatically.',\n requestBody: {\n contentType: 'application/json',\n schema: postBodySchema,\n },\n responses: [\n {\n status: 200,\n description: 'Record created',\n schema: mutationResponseSchema,\n },\n {\n status: 400,\n description: 'Validation failure',\n schema: errorSchema,\n },\n {\n status: 401,\n description: 'Missing authentication',\n schema: errorSchema,\n },\n {\n status: 500,\n description: 'Unexpected failure',\n schema: errorSchema,\n },\n ],\n },\n PUT: {\n summary: 'Update record',\n description:\n 'Updates an existing record. If the provided recordId is not a UUID the record will be created instead to support optimistic flows.',\n requestBody: {\n contentType: 'application/json',\n schema: putBodySchema,\n },\n responses: [\n {\n status: 200,\n description: 'Record updated',\n schema: mutationResponseSchema,\n },\n {\n status: 400,\n description: 'Validation failure',\n schema: errorSchema,\n },\n {\n status: 401,\n description: 'Missing authentication',\n schema: errorSchema,\n },\n {\n status: 500,\n description: 'Unexpected failure',\n schema: errorSchema,\n },\n ],\n },\n DELETE: {\n summary: 'Delete record',\n description: 'Soft deletes the specified record within the current tenant/org scope.',\n requestBody: {\n contentType: 'application/json',\n schema: deleteBodySchema,\n },\n responses: [\n {\n status: 200,\n description: 'Record deleted',\n schema: deleteResponseSchema,\n },\n {\n status: 400,\n description: 'Missing entity id or record id',\n schema: errorSchema,\n },\n {\n status: 401,\n description: 'Missing authentication',\n schema: errorSchema,\n },\n {\n status: 404,\n description: 'Record not found',\n schema: errorSchema,\n },\n {\n status: 500,\n description: 'Unexpected failure',\n schema: errorSchema,\n },\n ],\n },\n },\n}\n"],
|
|
5
|
-
"mappings": "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",
|
|
4
|
+
"sourcesContent": ["import { NextResponse } from 'next/server'\nimport { z } from 'zod'\nimport { createRequestContainer } from '@open-mercato/shared/lib/di/container'\nimport { getAuthFromRequest } from '@open-mercato/shared/lib/auth/server'\nimport type { QueryEngine, QueryOptions, Where, Sort } from '@open-mercato/shared/lib/query/types'\nimport { normalizeExportFormat, serializeExport, defaultExportFilename, ensureColumns } from '@open-mercato/shared/lib/crud/exporters'\nimport type { RbacService } from '@open-mercato/core/modules/auth/services/rbacService'\nimport { resolveOrganizationScope, getSelectedOrganizationFromRequest } from '@open-mercato/core/modules/directory/utils/organizationScope'\nimport { SYSTEM_ENTITY_RECORDS_BLOCKED_CODE, isOrmBackedSystemEntityId } from '@open-mercato/shared/lib/data/engine'\nimport { parseBooleanToken, parseBooleanWithDefault } from '@open-mercato/shared/lib/boolean'\nimport { parseCommaSeparatedList } from '@open-mercato/shared/lib/string'\nimport { setRecordCustomFields } from '../lib/helpers'\nimport { CustomFieldValue } from '../data/entities'\nimport type { OpenApiRouteDoc } from '@open-mercato/shared/lib/openapi'\nimport { enforceCommandOptimisticLockWithGuards } from '@open-mercato/shared/lib/crud/optimistic-lock-command'\nimport { isCrudHttpError } from '@open-mercato/shared/lib/crud/errors'\nimport { assertEntityAclForRequest, getDeclaredCustomEntityRestriction } from '../lib/entityAcl'\nimport { createLogger } from '@open-mercato/shared/lib/logger'\n\nconst logger = createLogger('entities').child({ component: 'records' })\n\ntype RecordsEntityScope = { tenantId: string | null; organizationId: string | null }\n\n// Resolve the CustomEntity registration that applies to THIS caller, most-specific\n// first (org+tenant \u2192 tenant-global \u2192 instance-global), mirroring the overlay\n// precedence used by the entity-definitions list. Scoping matters because the\n// row's `access_restricted` flag is a security control: an unscoped lookup could\n// read another tenant's row for a colliding entityId (e.g. `user:vendors`) and\n// mis-decide the restriction. Returns null when the caller's scope has no row.\nasync function findScopedCustomEntity(em: any, CustomEntity: any, entityId: string, scope: RecordsEntityScope) {\n const { tenantId, organizationId } = scope\n const candidates: Array<Record<string, unknown>> = [\n { entityId, organizationId, tenantId },\n { entityId, organizationId: null, tenantId },\n { entityId, organizationId: null, tenantId: null },\n ]\n const seen = new Set<string>()\n for (const where of candidates) {\n const key = JSON.stringify(where)\n if (seen.has(key)) continue\n seen.add(key)\n const row = await em.findOne(CustomEntity as any, where)\n if (row) return row\n }\n return null\n}\n\nconst CUSTOM_ENTITY_RECORD_RESOURCE_KIND = 'entities.record'\n\ntype RecordsEntityKind = 'system' | 'custom' | 'unknown'\n\n// `restricted` is meaningful only when `kind === 'custom'`; it drives the\n// per-entity ACL gate in `assertEntityAclForRequest`.\ntype RecordsEntityClassification = { kind: RecordsEntityKind; restricted: boolean }\n\n// This surface manages doc-storage records, which exist for CUSTOM entities only.\n// Module-declared ids backed by a registered ORM table are system entities \u2014 their\n// records live in their own module tables/APIs, and stray doc rows for them poisoned\n// read-path classification platform-wide (#2939) \u2014 so they are rejected outright. The\n// previous fallback that classified an entity by the mere presence of\n// `custom_entities_storage` rows is gone: within the allowed set, declaration (ce.ts)\n// or an active `custom_entities` registration is authoritative.\nasync function classifyRecordsEntity(em: any, entityId: string, scope: RecordsEntityScope): Promise<RecordsEntityClassification> {\n if (isOrmBackedSystemEntityId(em, entityId)) return { kind: 'system', restricted: false }\n const declaredRestriction = getDeclaredCustomEntityRestriction(entityId)\n if (declaredRestriction !== undefined) return { kind: 'custom', restricted: declaredRestriction }\n try {\n const { CustomEntity } = await import('../data/entities')\n // Restriction is decided from the row that applies to THIS caller's scope so\n // a colliding entityId in another tenant can't flip the flag.\n const scoped = await findScopedCustomEntity(em, CustomEntity, entityId, scope)\n if (scoped) return { kind: 'custom', restricted: (scoped as any).accessRestricted === true }\n // No in-scope registration: preserve the historical custom-vs-unknown\n // classification (any registration row \u2014 active or soft-deleted \u2014 proves the\n // id is custom; records persist beyond soft delete, TC-ENTITIES-006). A row\n // outside the caller's scope never marks the entity restricted for them, and\n // the record query is itself tenant/org-scoped, so this cannot leak data.\n const anyRow = await em.findOne(CustomEntity as any, { entityId })\n if (anyRow) return { kind: 'custom', restricted: false }\n } catch {}\n return { kind: 'unknown', restricted: false }\n}\n\nfunction systemEntityRecordsRejection(entityId: string) {\n return NextResponse.json(\n { error: 'Records are available for custom entities only', code: SYSTEM_ENTITY_RECORDS_BLOCKED_CODE, entityId },\n { status: 400 },\n )\n}\n\nasync function readCustomEntityRecordUpdatedAt(\n em: any,\n input: { entityType: string; entityId: string; organizationId: string | null },\n): Promise<string | null> {\n try {\n const db = em.getKysely()\n let query = db\n .selectFrom('custom_entities_storage' as any)\n .select(['updated_at' as any])\n .where('entity_type' as any, '=', input.entityType)\n .where('entity_id' as any, '=', input.entityId)\n query = input.organizationId === null\n ? query.where('organization_id' as any, 'is', null as any)\n : query.where('organization_id' as any, '=', input.organizationId)\n const row = await query.executeTakeFirst()\n const value = (row as any)?.updated_at\n if (value instanceof Date) return value.toISOString()\n if (typeof value === 'string' && value.length > 0) return value\n return null\n } catch {\n return null\n }\n}\n\nexport const metadata = {\n GET: { requireAuth: true, requireFeatures: ['entities.records.view'] },\n POST: { requireAuth: true, requireFeatures: ['entities.records.manage'] },\n PUT: { requireAuth: true, requireFeatures: ['entities.records.manage'] },\n DELETE: { requireAuth: true, requireFeatures: ['entities.records.manage'] },\n}\n\nconst DEFAULT_EXPORT_PAGE_SIZE = 1000\n\nconst listRecordsQuerySchema = z\n .object({\n entityId: z.string().min(1),\n page: z.coerce.number().int().min(1).optional(),\n pageSize: z.coerce.number().int().min(1).max(100).optional(),\n sortField: z.string().optional(),\n sortDir: z.enum(['asc', 'desc']).optional(),\n search: z.string().optional(),\n searchFields: z.string().optional(),\n withDeleted: z.coerce.boolean().optional(),\n format: z.enum(['csv', 'json', 'xml', 'markdown']).optional(),\n exportScope: z.enum(['full']).optional(),\n export_scope: z.enum(['full']).optional(),\n all: z.coerce.boolean().optional(),\n full: z.coerce.boolean().optional(),\n })\n .passthrough()\n\nconst recordItemSchema = z.record(z.string(), z.any())\n\nconst listRecordsResponseSchema = z.object({\n items: z.array(recordItemSchema),\n total: z.number(),\n page: z.number(),\n pageSize: z.number(),\n totalPages: z.number(),\n})\n\nexport async function GET(req: Request) {\n const url = new URL(req.url)\n const entityId = url.searchParams.get('entityId') || ''\n if (!entityId) return NextResponse.json({ error: 'entityId is required' }, { status: 400 })\n\n const auth = await getAuthFromRequest(req)\n if (!auth || !auth.tenantId) return NextResponse.json({ error: 'Unauthorized' }, { status: 401 })\n\n const requestedExport = normalizeExportFormat(url.searchParams.get('format'))\n const exportScopeRaw = (url.searchParams.get('exportScope') || url.searchParams.get('export_scope') || '').toLowerCase()\n const exportFullRequested = requestedExport != null && (exportScopeRaw === 'full' || parseBooleanWithDefault(url.searchParams.get('full'), false))\n const exportAll = parseBooleanWithDefault(url.searchParams.get('all'), false)\n const noPagination = exportAll || requestedExport != null\n const page = noPagination ? 1 : Math.max(parseInt(url.searchParams.get('page') || '1', 10) || 1, 1)\n const basePageSize = Math.min(Math.max(parseInt(url.searchParams.get('pageSize') || '50', 10) || 50, 1), 100)\n const pageSize = noPagination ? Math.max(basePageSize, DEFAULT_EXPORT_PAGE_SIZE) : basePageSize\n const sortField = url.searchParams.get('sortField') || 'id'\n const sortDir = (url.searchParams.get('sortDir') || 'asc').toLowerCase() === 'desc' ? 'desc' : 'asc'\n const withDeleted = parseBooleanWithDefault(url.searchParams.get('withDeleted'), false)\n const searchTerm = (url.searchParams.get('search') || '').trim()\n const searchFields = parseCommaSeparatedList(url.searchParams.get('searchFields'))\n\n const qpEntries: Array<[string, string]> = []\n for (const [key, val] of url.searchParams.entries()) {\n if (['entityId','page','pageSize','sortField','sortDir','withDeleted','format','exportScope','export_scope','all','full','search','searchFields'].includes(key)) continue\n qpEntries.push([key, val])\n }\n\n try {\n const { resolve } = await createRequestContainer()\n const qe = resolve('queryEngine') as QueryEngine\n const em = resolve('em') as any\n const rbac = resolve('rbacService') as RbacService\n const scope = await resolveOrganizationScope({ em, rbac, auth, selectedId: getSelectedOrganizationFromRequest(req) })\n let organizationIds: string[] | null = scope.filterIds\n // Module-declared custom entities (ce.ts) carry frozen system-style ids and are never\n // registered in `custom_entities`, so classification checks the declared registry plus\n // active registrations. System (table-backed) ids are rejected above; for the allowed\n // set `isCustomEntity` drives mapRow's cf_ stripping so the edit form reads back values.\n const { kind: entityKind, restricted: isRestricted } = await classifyRecordsEntity(em, entityId, { tenantId: auth.tenantId ?? null, organizationId: scope.selectedId ?? auth.orgId ?? null })\n if (entityKind === 'system') return systemEntityRecordsRejection(entityId)\n const isCustomEntity = entityKind === 'custom'\n await assertEntityAclForRequest({ auth, entityId, action: 'view', isCustomEntity, isRestricted, rbac })\n if (organizationIds && organizationIds.length === 0) {\n return NextResponse.json({ items: [], total: 0, page, pageSize, totalPages: 0 })\n }\n const normalizeCustomEntityValue = (value: unknown) => {\n if (Array.isArray(value)) {\n return value.map((entry) => {\n if (typeof entry !== 'string') return entry\n const parsed = parseBooleanToken(entry)\n return parsed === null ? entry : parsed\n })\n }\n if (typeof value !== 'string') return value\n const parsed = parseBooleanToken(value)\n return parsed === null ? value : parsed\n }\n const mapRow = (row: any) => {\n if (!isCustomEntity || !row || typeof row !== 'object') return row\n const out: Record<string, unknown> = {}\n for (const [k, v] of Object.entries(row)) {\n if (k.startsWith('cf_')) out[k.replace(/^cf_/, '')] = normalizeCustomEntityValue(v)\n else out[k] = v\n }\n return out\n }\n const mapFullRow = (row: any) => {\n if (!row || typeof row !== 'object') return row\n return { ...(row as Record<string, unknown>) }\n }\n // Build filters with awareness of custom-entity mode\n const filtersObj: Where<any> = {}\n const buildFilter = (key: string, val: string, allowAnyKey: boolean) => {\n if (key.startsWith('cf_')) {\n if (key.endsWith('In')) {\n const base = key.slice(0, -2)\n const values = parseCommaSeparatedList(val)\n ;(filtersObj as any)[base] = { $in: values }\n } else {\n if (val.includes(',')) {\n const values = parseCommaSeparatedList(val)\n ;(filtersObj as any)[key] = { $in: values }\n } else {\n const parsed = parseBooleanToken(val)\n ;(filtersObj as any)[key] = parsed === null ? val : parsed\n }\n }\n } else if (allowAnyKey) {\n if (val.includes(',')) {\n const values = parseCommaSeparatedList(val)\n ;(filtersObj as any)[key] = { $in: values }\n } else {\n const parsed = parseBooleanToken(val)\n ;(filtersObj as any)[key] = parsed === null ? val : parsed\n }\n } else {\n if (['id', 'created_at', 'updated_at', 'deleted_at', 'name', 'title', 'email'].includes(key)) {\n ;(filtersObj as any)[key] = val\n }\n }\n }\n\n if (organizationIds && organizationIds.length) {\n (filtersObj as any).organization_id = { $in: organizationIds }\n }\n const qopts: QueryOptions = {\n tenantId: auth.tenantId!,\n includeCustomFields: true,\n page: { page, pageSize },\n sort: [{ field: sortField as any, dir: sortDir as any }] as Sort[],\n filters: filtersObj as any,\n withDeleted,\n }\n if (organizationIds && organizationIds.length) {\n qopts.organizationIds = organizationIds\n }\n // Allowed entities are doc-storage-backed by definition (system ids were rejected\n // above) \u2014 direct the engine to doc storage explicitly so reads stay deterministic\n // even before the first record exists.\n if (isCustomEntity) qopts.forceCustomEntityStorage = true\n for (const [k, v] of qpEntries) buildFilter(k, v, isCustomEntity)\n // Server-side full-result search: match the term against the requested fields\n // (defaults to `id`) before pagination so totals/exports stay consistent with\n // the active search instead of filtering only the current client page (#3229).\n if (searchTerm) {\n const fields = searchFields.length ? searchFields : ['id']\n const pattern = `%${searchTerm}%`\n const orClauses = fields.map((field) => ({ [field]: { $ilike: pattern } }))\n ;(filtersObj as any).$or = orClauses\n }\n const res = await qe.query(entityId as any, qopts)\n const rawItems = res.items || []\n const viewPageItems = rawItems.map(mapRow)\n const fullPageItems = rawItems.map(mapFullRow)\n\n // Expose `updated_at` on custom-entity records. The query engine returns only\n // the `doc` fields + `id`, dropping the base `updated_at` column \u2014 which made\n // optimistic locking impossible end-to-end (no version for the edit page to\n // round-trip as the lock header). Batch-read it from storage and merge it in.\n if (isCustomEntity && viewPageItems.length) {\n try {\n const recordIds = viewPageItems\n .map((it: any) => it?.id)\n .filter((v: any): v is string => typeof v === 'string' && v.length > 0)\n if (recordIds.length) {\n const db = em.getKysely()\n const rows = await db\n .selectFrom('custom_entities_storage' as any)\n .select(['entity_id' as any, 'updated_at' as any])\n .where('entity_type' as any, '=', entityId)\n .where('entity_id' as any, 'in', recordIds as any)\n .execute()\n const updatedById = new Map<string, string>()\n for (const row of rows as any[]) {\n const value = row?.updated_at\n const iso = value instanceof Date ? value.toISOString() : (typeof value === 'string' && value.length > 0 ? value : null)\n if (iso && row?.entity_id) updatedById.set(String(row.entity_id), iso)\n }\n for (const item of viewPageItems as any[]) {\n const iso = updatedById.get(String(item?.id))\n if (iso) {\n item.updated_at = iso\n item.updatedAt = iso\n }\n }\n }\n } catch { /* best-effort: locking simply will not engage if storage is unavailable */ }\n }\n\n const total = typeof res.total === 'number' ? res.total : rawItems.length\n const effectivePageSize = res.pageSize || pageSize\n const payload = {\n items: viewPageItems,\n total,\n page: res.page || page,\n pageSize: effectivePageSize,\n totalPages: Math.ceil(total / (effectivePageSize || 1)),\n }\n\n if (requestedExport) {\n let exportItems: any[] = exportFullRequested ? [...fullPageItems] : [...viewPageItems]\n if (total > exportItems.length) {\n let nextPage = 2\n while (exportItems.length < total) {\n const nextRes = await qe.query(entityId as any, {\n ...qopts,\n page: { page: nextPage, pageSize },\n })\n const nextRawItems = nextRes.items || []\n if (!nextRawItems.length) break\n const nextViewItems = nextRawItems.map(mapRow)\n const nextFullItems = nextRawItems.map(mapFullRow)\n const nextBatch = exportFullRequested ? nextFullItems : nextViewItems\n exportItems.push(...nextBatch)\n if (nextBatch.length < pageSize) break\n nextPage += 1\n }\n }\n const prepared = {\n columns: ensureColumns(exportItems),\n rows: exportItems,\n }\n const filenameBase = exportFullRequested ? `${entityId || 'records'}_full` : entityId || 'records'\n const serialized = serializeExport(prepared, requestedExport)\n const filename = defaultExportFilename(filenameBase, requestedExport)\n return new Response(serialized.body, {\n headers: {\n 'content-type': serialized.contentType,\n 'content-disposition': `attachment; filename=\"${filename}\"`,\n },\n })\n }\n\n return NextResponse.json(payload)\n } catch (e) {\n if (isCrudHttpError(e)) return NextResponse.json(e.body, { status: e.status })\n logger.error('Records GET failed', { err: e })\n return NextResponse.json({ error: 'Internal server error' }, { status: 500 })\n }\n}\n\nconst postBodySchema = z.object({\n entityId: z.string().min(1),\n recordId: z.string().min(1).optional(),\n values: z.record(z.string(), z.any()).default({}),\n})\n\nconst putBodySchema = z.object({\n entityId: z.string().min(1),\n recordId: z.string().min(1),\n values: z.record(z.string(), z.any()).default({}),\n})\n\nconst mutationResponseSchema = z.object({\n ok: z.literal(true),\n item: z\n .object({\n entityId: z.string(),\n recordId: z.string(),\n })\n .optional(),\n})\n\nexport async function POST(req: Request) {\n const auth = await getAuthFromRequest(req)\n if (!auth || !auth.tenantId) return NextResponse.json({ error: 'Unauthorized' }, { status: 401 })\n\n let json: unknown\n try { json = await req.json() } catch { return NextResponse.json({ error: 'Invalid JSON' }, { status: 400 }) }\n const parsed = postBodySchema.safeParse(json)\n if (!parsed.success) return NextResponse.json({ error: 'Validation failed', details: parsed.error.flatten() }, { status: 400 })\n const { entityId } = parsed.data\n let { recordId, values } = parsed.data as { recordId?: string; values: Record<string, any> }\n\n try {\n const { resolve } = await createRequestContainer()\n const de = resolve('dataEngine') as any\n const em = resolve('em') as any\n const rbac = resolve('rbacService') as RbacService\n const scope = await resolveOrganizationScope({ em, rbac, auth, selectedId: getSelectedOrganizationFromRequest(req) })\n const targetOrgId = scope.selectedId ?? auth.orgId\n if (!targetOrgId) return NextResponse.json({ error: 'Organization context is required' }, { status: 400 })\n const { kind: entityKind, restricted: isRestricted } = await classifyRecordsEntity(em, entityId, { tenantId: auth.tenantId ?? null, organizationId: scope.selectedId ?? auth.orgId ?? null })\n if (entityKind === 'system') return systemEntityRecordsRejection(entityId)\n const isCustomEntity = entityKind === 'custom'\n await assertEntityAclForRequest({ auth, entityId, action: 'manage', isCustomEntity, isRestricted, rbac })\n // Strip reserved record/system columns the edit form echoes back from the loaded record\n // (`id`, plus `updated_at`/`updatedAt` used for optimistic locking). They are not custom\n // fields; without this they validate as cf_id / cf_updated_at / cf_updatedAt and are\n // rejected as \"Unknown custom field\", which fails EVERY custom-entity edit-form save.\n for (const reservedKey of ['id', 'created_at', 'createdAt', 'updated_at', 'updatedAt', 'deleted_at', 'deletedAt']) {\n delete (values as any)[reservedKey]\n }\n const norm = normalizeValues(values)\n\n // Validate against custom field definitions\n try {\n const { validateCustomFieldValuesServer } = await import('../lib/validation')\n const check = await validateCustomFieldValuesServer(em, { entityId, organizationId: targetOrgId, tenantId: auth.tenantId!, values: norm, rejectUndeclaredKeys: true })\n if (!check.ok) return NextResponse.json({ error: 'Validation failed', fields: check.fieldErrors }, { status: 400 })\n } catch { /* ignore if helper missing */ }\n\n const normalizedRecordId = (() => {\n const raw = String(recordId || '').trim()\n if (!raw) return undefined\n const low = raw.toLowerCase()\n if (low === 'create' || low === 'new' || low === 'null' || low === 'undefined') return undefined\n // Enforce UUID only; any non-uuid is ignored so we generate one in the DE\n const uuid = /^[0-9a-f]{8}-[0-9a-f]{4}-[1-5][0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/i\n return uuid.test(raw) ? raw : undefined\n })()\n const { id } = await de.createCustomEntityRecord({\n entityId,\n recordId: normalizedRecordId,\n organizationId: targetOrgId,\n tenantId: auth.tenantId!,\n values: norm,\n })\n\n return NextResponse.json({ ok: true, item: { entityId, recordId: id } })\n } catch (e) {\n if (isCrudHttpError(e)) return NextResponse.json(e.body, { status: e.status })\n logger.error('Records POST failed', { err: e })\n return NextResponse.json({ error: 'Internal server error' }, { status: 500 })\n }\n}\n\n// Avoid zod here to prevent runtime import issues in some environments\nfunction parsePutBody(json: any): { ok: true; data: { entityId: string; recordId: string; values: Record<string, any> } } | { ok: false; error: string } {\n if (!json || typeof json !== 'object') return { ok: false, error: 'Invalid JSON' }\n const entityId = typeof json.entityId === 'string' && json.entityId.length ? json.entityId : ''\n const recordId = typeof json.recordId === 'string' && json.recordId.length ? json.recordId : ''\n const values = (json.values && typeof json.values === 'object') ? json.values as Record<string, any> : {}\n if (!entityId || !recordId) return { ok: false, error: 'entityId and recordId are required' }\n return { ok: true, data: { entityId, recordId, values } }\n}\n\nexport async function PUT(req: Request) {\n const auth = await getAuthFromRequest(req)\n if (!auth || !auth.tenantId) return NextResponse.json({ error: 'Unauthorized' }, { status: 401 })\n\n let json: any\n try { json = await req.json() } catch { return NextResponse.json({ error: 'Invalid JSON' }, { status: 400 }) }\n const parsed = parsePutBody(json)\n if (!parsed.ok) return NextResponse.json({ error: parsed.error }, { status: 400 })\n const { entityId, recordId, values } = parsed.data\n\n try {\n const container = await createRequestContainer()\n const { resolve } = container\n const de = resolve('dataEngine') as any\n const em = resolve('em') as any\n const rbac = resolve('rbacService') as RbacService\n const scope = await resolveOrganizationScope({ em, rbac, auth, selectedId: getSelectedOrganizationFromRequest(req) })\n const targetOrgId = scope.selectedId ?? auth.orgId\n if (!targetOrgId) return NextResponse.json({ error: 'Organization context is required' }, { status: 400 })\n const { kind: entityKind, restricted: isRestricted } = await classifyRecordsEntity(em, entityId, { tenantId: auth.tenantId ?? null, organizationId: scope.selectedId ?? auth.orgId ?? null })\n if (entityKind === 'system') return systemEntityRecordsRejection(entityId)\n const isCustomEntity = entityKind === 'custom'\n await assertEntityAclForRequest({ auth, entityId, action: 'manage', isCustomEntity, isRestricted, rbac })\n // Strip reserved record/system columns the edit form echoes back from the loaded record\n // (`id`, plus `updated_at`/`updatedAt` used for optimistic locking). They are not custom\n // fields; without this they validate as cf_id / cf_updated_at / cf_updatedAt and are\n // rejected as \"Unknown custom field\", which fails EVERY custom-entity edit-form save.\n for (const reservedKey of ['id', 'created_at', 'createdAt', 'updated_at', 'updatedAt', 'deleted_at', 'deletedAt']) {\n delete (values as any)[reservedKey]\n }\n const norm = normalizeValues(values)\n\n // Validate against custom field definitions\n try {\n const { validateCustomFieldValuesServer } = await import('../lib/validation')\n const check = await validateCustomFieldValuesServer(em, { entityId, organizationId: targetOrgId, tenantId: auth.tenantId!, values: norm, rejectUndeclaredKeys: true })\n if (!check.ok) return NextResponse.json({ error: 'Validation failed', fields: check.fieldErrors }, { status: 400 })\n } catch { /* ignore if helper missing */ }\n\n // Normalize recordId: if blank/sentinel/non-uuid => create instead of update\n const uuidRe = /^[0-9a-f]{8}-[0-9a-f]{4}-[1-5][0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/i\n const rid = String(recordId || '').trim()\n const low = rid.toLowerCase()\n const isSentinel = !rid || low === 'create' || low === 'new' || low === 'null' || low === 'undefined'\n const isUuid = uuidRe.test(rid)\n if (isSentinel || !isUuid) {\n const created = await de.createCustomEntityRecord({\n entityId,\n recordId: undefined,\n organizationId: targetOrgId,\n tenantId: auth.tenantId!,\n values: norm,\n })\n return NextResponse.json({ ok: true, item: { entityId, recordId: created.id } })\n }\n\n try {\n const currentUpdatedAt = await readCustomEntityRecordUpdatedAt(em, {\n entityType: entityId,\n entityId: rid,\n organizationId: targetOrgId,\n })\n await enforceCommandOptimisticLockWithGuards(container, {\n resourceKind: CUSTOM_ENTITY_RECORD_RESOURCE_KIND,\n resourceId: rid,\n current: currentUpdatedAt,\n request: req,\n })\n } catch (lockError) {\n if (isCrudHttpError(lockError)) {\n return NextResponse.json(lockError.body, { status: lockError.status })\n }\n throw lockError\n }\n\n await de.updateCustomEntityRecord({\n entityId,\n recordId: rid,\n organizationId: targetOrgId,\n tenantId: auth.tenantId!,\n values: norm,\n })\n return NextResponse.json({ ok: true, item: { entityId, recordId: rid } })\n } catch (e) {\n if (isCrudHttpError(e)) return NextResponse.json(e.body, { status: e.status })\n return NextResponse.json({ error: 'Internal server error' }, { status: 500 })\n }\n}\n\nconst deleteBodySchema = z.object({\n entityId: z.string().min(1),\n recordId: z.string().min(1),\n})\n\nexport async function DELETE(req: Request) {\n const auth = await getAuthFromRequest(req)\n if (!auth || !auth.tenantId) return NextResponse.json({ error: 'Unauthorized' }, { status: 401 })\n\n const url = new URL(req.url)\n const qpEntityId = url.searchParams.get('entityId')\n const qpRecordId = url.searchParams.get('recordId')\n let payload: any = qpEntityId && qpRecordId ? { entityId: qpEntityId, recordId: qpRecordId } : null\n if (!payload) {\n try { payload = await req.json() } catch { payload = null }\n }\n const parsed = deleteBodySchema.safeParse(payload)\n if (!parsed.success) return NextResponse.json({ error: 'Validation failed', details: parsed.error.flatten() }, { status: 400 })\n const { entityId, recordId } = parsed.data\n\n try {\n const container = await createRequestContainer()\n const { resolve } = container\n const de = resolve('dataEngine') as any\n const em = resolve('em') as any\n const rbac = resolve('rbacService') as RbacService\n const scope = await resolveOrganizationScope({ em, rbac, auth, selectedId: getSelectedOrganizationFromRequest(req) })\n const targetOrgId = scope.selectedId ?? auth.orgId\n if (!targetOrgId) return NextResponse.json({ error: 'Organization context is required' }, { status: 400 })\n const { kind: entityKind, restricted: isRestricted } = await classifyRecordsEntity(em, entityId, { tenantId: auth.tenantId ?? null, organizationId: scope.selectedId ?? auth.orgId ?? null })\n if (entityKind === 'system') return systemEntityRecordsRejection(entityId)\n const isCustomEntity = entityKind === 'custom'\n await assertEntityAclForRequest({ auth, entityId, action: 'manage', isCustomEntity, isRestricted, rbac })\n\n try {\n const currentUpdatedAt = await readCustomEntityRecordUpdatedAt(em, {\n entityType: entityId,\n entityId: recordId,\n organizationId: targetOrgId,\n })\n await enforceCommandOptimisticLockWithGuards(container, {\n resourceKind: CUSTOM_ENTITY_RECORD_RESOURCE_KIND,\n resourceId: recordId,\n current: currentUpdatedAt,\n request: req,\n })\n } catch (lockError) {\n if (isCrudHttpError(lockError)) {\n return NextResponse.json(lockError.body, { status: lockError.status })\n }\n throw lockError\n }\n\n await de.deleteCustomEntityRecord({ entityId, recordId, organizationId: targetOrgId, tenantId: auth.tenantId!, soft: true })\n return NextResponse.json({ ok: true })\n } catch (e) {\n if (isCrudHttpError(e)) return NextResponse.json(e.body, { status: e.status })\n return NextResponse.json({ error: 'Internal server error' }, { status: 500 })\n }\n}\n\nfunction normalizeValues(input: Record<string, any>): Record<string, any> {\n const out: Record<string, any> = {}\n for (const [k, v] of Object.entries(input || {})) {\n const key = k.startsWith('cf_') ? k.replace(/^cf_/, '') : k\n out[key] = v\n }\n return out\n}\n\nconst deleteResponseSchema = z.object({\n ok: z.literal(true),\n})\n\nconst errorSchema = z.object({\n error: z.string(),\n}).passthrough()\n\nexport const openApi: OpenApiRouteDoc = {\n tag: 'Entities',\n summary: 'CRUD operations on entity records',\n methods: {\n GET: {\n summary: 'List records',\n description:\n 'Returns paginated records for the supplied entity. Supports custom field filters, exports, and soft-delete toggles.',\n query: listRecordsQuerySchema,\n responses: [\n {\n status: 200,\n description: 'Paginated records',\n schema: listRecordsResponseSchema,\n },\n {\n status: 400,\n description: 'Missing entity id',\n schema: errorSchema,\n },\n {\n status: 401,\n description: 'Missing authentication',\n schema: errorSchema,\n },\n {\n status: 500,\n description: 'Unexpected failure',\n schema: errorSchema,\n },\n ],\n },\n POST: {\n summary: 'Create record',\n description:\n 'Creates a record for the given entity. When `recordId` is omitted or not a UUID the data engine will generate one automatically.',\n requestBody: {\n contentType: 'application/json',\n schema: postBodySchema,\n },\n responses: [\n {\n status: 200,\n description: 'Record created',\n schema: mutationResponseSchema,\n },\n {\n status: 400,\n description: 'Validation failure',\n schema: errorSchema,\n },\n {\n status: 401,\n description: 'Missing authentication',\n schema: errorSchema,\n },\n {\n status: 500,\n description: 'Unexpected failure',\n schema: errorSchema,\n },\n ],\n },\n PUT: {\n summary: 'Update record',\n description:\n 'Updates an existing record. If the provided recordId is not a UUID the record will be created instead to support optimistic flows.',\n requestBody: {\n contentType: 'application/json',\n schema: putBodySchema,\n },\n responses: [\n {\n status: 200,\n description: 'Record updated',\n schema: mutationResponseSchema,\n },\n {\n status: 400,\n description: 'Validation failure',\n schema: errorSchema,\n },\n {\n status: 401,\n description: 'Missing authentication',\n schema: errorSchema,\n },\n {\n status: 500,\n description: 'Unexpected failure',\n schema: errorSchema,\n },\n ],\n },\n DELETE: {\n summary: 'Delete record',\n description: 'Soft deletes the specified record within the current tenant/org scope.',\n requestBody: {\n contentType: 'application/json',\n schema: deleteBodySchema,\n },\n responses: [\n {\n status: 200,\n description: 'Record deleted',\n schema: deleteResponseSchema,\n },\n {\n status: 400,\n description: 'Missing entity id or record id',\n schema: errorSchema,\n },\n {\n status: 401,\n description: 'Missing authentication',\n schema: errorSchema,\n },\n {\n status: 404,\n description: 'Record not found',\n schema: errorSchema,\n },\n {\n status: 500,\n description: 'Unexpected failure',\n schema: errorSchema,\n },\n ],\n },\n },\n}\n"],
|
|
5
|
+
"mappings": "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",
|
|
6
6
|
"names": ["parsed"]
|
|
7
7
|
}
|
|
@@ -3,8 +3,9 @@ import { z } from "zod";
|
|
|
3
3
|
import { getAuthFromRequest } from "@open-mercato/shared/lib/auth/server";
|
|
4
4
|
import { createRequestContainer } from "@open-mercato/shared/lib/di/container";
|
|
5
5
|
import { CustomEntity } from "@open-mercato/core/modules/entities/data/entities";
|
|
6
|
+
import { canReadEntityMetadata, getDeclaredCustomEntityRestriction } from "../lib/entityAcl.js";
|
|
6
7
|
const metadata = {
|
|
7
|
-
GET: { requireAuth: true }
|
|
8
|
+
GET: { requireAuth: true, requireFeatures: ["entities.records.view"] }
|
|
8
9
|
};
|
|
9
10
|
async function GET(req) {
|
|
10
11
|
const auth = await getAuthFromRequest(req);
|
|
@@ -12,6 +13,11 @@ async function GET(req) {
|
|
|
12
13
|
const { resolve } = await createRequestContainer();
|
|
13
14
|
const em = resolve("em");
|
|
14
15
|
const cache = resolve("cache");
|
|
16
|
+
const rbac = resolve("rbacService");
|
|
17
|
+
const acl = await rbac.loadAcl(auth.sub ?? "", {
|
|
18
|
+
tenantId: auth.tenantId,
|
|
19
|
+
organizationId: auth.orgId ?? null
|
|
20
|
+
});
|
|
15
21
|
const where = {
|
|
16
22
|
isActive: true,
|
|
17
23
|
showInSidebar: true
|
|
@@ -20,25 +26,41 @@ async function GET(req) {
|
|
|
20
26
|
// { $or: [ { organizationId: auth.orgId ?? undefined as any }, { organizationId: null } ] }, // the entities and custom fields are defined per tenant
|
|
21
27
|
{ $or: [{ tenantId: auth.tenantId ?? void 0 }, { tenantId: null }] }
|
|
22
28
|
];
|
|
23
|
-
const cacheKey = `entities:sidebar:${auth.tenantId || "null"}`;
|
|
29
|
+
const cacheKey = `entities:sidebar:v2:${auth.tenantId || "null"}`;
|
|
30
|
+
let candidates = null;
|
|
24
31
|
try {
|
|
25
32
|
if (cache) {
|
|
26
33
|
const cached = await cache.get(cacheKey);
|
|
27
|
-
if (cached && Array.isArray(cached.
|
|
34
|
+
if (cached && Array.isArray(cached.candidates)) candidates = cached.candidates;
|
|
28
35
|
}
|
|
29
36
|
} catch {
|
|
30
37
|
}
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
38
|
+
if (!candidates) {
|
|
39
|
+
const entities = await em.find(CustomEntity, where, { orderBy: { label: "asc" } });
|
|
40
|
+
candidates = entities.map((entity) => ({
|
|
41
|
+
entityId: entity.entityId,
|
|
42
|
+
label: entity.label,
|
|
43
|
+
href: `/backend/entities/user/${encodeURIComponent(entity.entityId)}/records`,
|
|
44
|
+
accessRestricted: getDeclaredCustomEntityRestriction(entity.entityId) ?? entity.accessRestricted === true
|
|
45
|
+
}));
|
|
46
|
+
try {
|
|
47
|
+
if (cache) {
|
|
48
|
+
await cache.set(cacheKey, { candidates }, { tags: [`nav:entities:${auth.tenantId || "null"}`] });
|
|
49
|
+
}
|
|
50
|
+
} catch {
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
const items = candidates.filter((entity) => canReadEntityMetadata({
|
|
54
|
+
entityId: entity.entityId,
|
|
55
|
+
isCustomEntity: true,
|
|
56
|
+
isRestricted: entity.accessRestricted,
|
|
57
|
+
acl
|
|
58
|
+
})).map((entity) => ({
|
|
59
|
+
entityId: entity.entityId,
|
|
60
|
+
label: entity.label,
|
|
61
|
+
href: entity.href
|
|
36
62
|
}));
|
|
37
63
|
const payload = { items };
|
|
38
|
-
try {
|
|
39
|
-
if (cache) await cache.set(cacheKey, payload, { tags: [`nav:entities:${auth.tenantId || "null"}`] });
|
|
40
|
-
} catch {
|
|
41
|
-
}
|
|
42
64
|
return NextResponse.json(payload);
|
|
43
65
|
}
|
|
44
66
|
const sidebarEntitiesResponseSchema = z.object({
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"version": 3,
|
|
3
3
|
"sources": ["../../../../src/modules/entities/api/sidebar-entities.ts"],
|
|
4
|
-
"sourcesContent": ["import { NextResponse } from 'next/server'\nimport { z } from 'zod'\nimport { getAuthFromRequest } from '@open-mercato/shared/lib/auth/server'\nimport { createRequestContainer } from '@open-mercato/shared/lib/di/container'\nimport { EntityManager } from '@mikro-orm/core'\nimport { CustomEntity } from '@open-mercato/core/modules/entities/data/entities'\nimport type { OpenApiRouteDoc } from '@open-mercato/shared/lib/openapi'\n\nexport const metadata = {\n GET: { requireAuth: true },\n}\n\nexport async function GET(req: Request) {\n const auth = await getAuthFromRequest(req)\n if (!auth || !auth.tenantId || (!auth.orgId && !auth.isSuperAdmin)) return NextResponse.json({ error: 'Unauthorized' }, { status: 401 })\n\n const { resolve } = await createRequestContainer()\n const em = resolve('em') as EntityManager\n const cache = resolve('cache') as any\n\n const where: any = { \n isActive: true,\n showInSidebar: true\n }\n where.$and = [\n// { $or: [ { organizationId: auth.orgId ?? undefined as any }, { organizationId: null } ] }, // the entities and custom fields are defined per tenant\n { $or: [ { tenantId: auth.tenantId ?? undefined as any }, { tenantId: null } ] },\n ]\n \n
|
|
5
|
-
"mappings": "AAAA,SAAS,oBAAoB;AAC7B,SAAS,SAAS;AAClB,SAAS,0BAA0B;AACnC,SAAS,8BAA8B;AAEvC,SAAS,oBAAoB;
|
|
4
|
+
"sourcesContent": ["import { NextResponse } from 'next/server'\nimport { z } from 'zod'\nimport { getAuthFromRequest } from '@open-mercato/shared/lib/auth/server'\nimport { createRequestContainer } from '@open-mercato/shared/lib/di/container'\nimport { EntityManager } from '@mikro-orm/core'\nimport { CustomEntity } from '@open-mercato/core/modules/entities/data/entities'\nimport type { OpenApiRouteDoc } from '@open-mercato/shared/lib/openapi'\nimport type { RbacService } from '@open-mercato/core/modules/auth/services/rbacService'\nimport { canReadEntityMetadata, getDeclaredCustomEntityRestriction } from '../lib/entityAcl'\n\nexport const metadata = {\n GET: { requireAuth: true, requireFeatures: ['entities.records.view'] },\n}\n\nexport async function GET(req: Request) {\n const auth = await getAuthFromRequest(req)\n if (!auth || !auth.tenantId || (!auth.orgId && !auth.isSuperAdmin)) return NextResponse.json({ error: 'Unauthorized' }, { status: 401 })\n\n const { resolve } = await createRequestContainer()\n const em = resolve('em') as EntityManager\n const cache = resolve('cache') as any\n const rbac = resolve('rbacService') as RbacService\n const acl = await rbac.loadAcl(auth.sub ?? '', {\n tenantId: auth.tenantId,\n organizationId: auth.orgId ?? null,\n })\n\n const where: any = { \n isActive: true,\n showInSidebar: true\n }\n where.$and = [\n// { $or: [ { organizationId: auth.orgId ?? undefined as any }, { organizationId: null } ] }, // the entities and custom fields are defined per tenant\n { $or: [ { tenantId: auth.tenantId ?? undefined as any }, { tenantId: null } ] },\n ]\n \n const cacheKey = `entities:sidebar:v2:${auth.tenantId || 'null'}`\n let candidates: Array<{\n entityId: string\n label: string\n href: string\n accessRestricted: boolean\n }> | null = null\n try {\n if (cache) {\n const cached = await cache.get(cacheKey)\n if (cached && Array.isArray(cached.candidates)) candidates = cached.candidates\n }\n } catch {}\n\n if (!candidates) {\n const entities = await em.find(CustomEntity as any, where as any, { orderBy: { label: 'asc' } as any })\n candidates = (entities as any[]).map((entity) => ({\n entityId: entity.entityId,\n label: entity.label,\n href: `/backend/entities/user/${encodeURIComponent(entity.entityId)}/records`,\n accessRestricted: getDeclaredCustomEntityRestriction(entity.entityId)\n ?? entity.accessRestricted === true,\n }))\n try {\n if (cache) {\n await cache.set(cacheKey, { candidates }, { tags: [`nav:entities:${auth.tenantId || 'null'}`] })\n }\n } catch {}\n }\n\n const items = candidates\n .filter((entity) => canReadEntityMetadata({\n entityId: entity.entityId,\n isCustomEntity: true,\n isRestricted: entity.accessRestricted,\n acl,\n }))\n .map((entity) => ({\n entityId: entity.entityId,\n label: entity.label,\n href: entity.href,\n }))\n const payload = { items }\n return NextResponse.json(payload)\n}\n\nconst sidebarEntitiesResponseSchema = z.object({\n items: z.array(\n z.object({\n entityId: z.string(),\n label: z.string(),\n href: z.string(),\n })\n ),\n})\n\nexport const openApi: OpenApiRouteDoc = {\n tag: 'Entities',\n summary: 'List sidebar entities',\n methods: {\n GET: {\n summary: 'Get sidebar entities',\n description: 'Returns custom entities flagged with `showInSidebar` for the current tenant/org scope.',\n responses: [\n {\n status: 200,\n description: 'Sidebar entities for navigation',\n schema: sidebarEntitiesResponseSchema,\n },\n {\n status: 401,\n description: 'Missing authentication',\n schema: z.object({ error: z.string() }),\n },\n ],\n },\n },\n}\n"],
|
|
5
|
+
"mappings": "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",
|
|
6
6
|
"names": []
|
|
7
7
|
}
|