@open-mercato/core 0.6.6-develop.6541.1.c6f8fa5d2b → 0.6.6-develop.6543.1.49fadb4c31

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.
@@ -6,7 +6,10 @@ import { DashboardRoleWidgets } from "@open-mercato/core/modules/dashboards/data
6
6
  import { Role } from "@open-mercato/core/modules/auth/data/entities";
7
7
  import { roleWidgetSettingsSchema } from "@open-mercato/core/modules/dashboards/data/validators";
8
8
  import { loadAllWidgets } from "@open-mercato/core/modules/dashboards/lib/widgets";
9
- import { resolveWidgetAssignmentReadScope } from "@open-mercato/core/modules/dashboards/lib/widgetAssignmentScope";
9
+ import {
10
+ resolveWidgetAssignmentReadScope,
11
+ resolveWidgetAssignmentTargetAccess
12
+ } from "@open-mercato/core/modules/dashboards/lib/widgetAssignmentScope";
10
13
  import { hasFeature } from "@open-mercato/shared/security/features";
11
14
  import {
12
15
  runCrudMutationGuardAfterSuccess,
@@ -61,7 +64,15 @@ async function GET(req) {
61
64
  queryOrganizationId: url.searchParams.get("organizationId")
62
65
  });
63
66
  const role = await em.findOne(Role, { id: roleId, deletedAt: null });
64
- if (!role || tenantId && role.tenantId !== tenantId) {
67
+ const access = resolveWidgetAssignmentTargetAccess({
68
+ isSuperAdmin: !!acl.isSuperAdmin,
69
+ scopeTenantId: tenantId,
70
+ target: role
71
+ });
72
+ if (access === "forbidden") {
73
+ return NextResponse.json({ error: "Forbidden" }, { status: 403 });
74
+ }
75
+ if (access === "not-found") {
65
76
  return NextResponse.json({ error: "Role not found" }, { status: 404 });
66
77
  }
67
78
  const records = await em.find(DashboardRoleWidgets, { roleId, deletedAt: null });
@@ -103,7 +114,15 @@ async function PUT(req) {
103
114
  const tenantId = auth.tenantId ?? null;
104
115
  const organizationId = auth.orgId ?? null;
105
116
  const role = await em.findOne(Role, { id: parsed.data.roleId, deletedAt: null });
106
- if (!role || tenantId && role.tenantId !== tenantId) {
117
+ const access = resolveWidgetAssignmentTargetAccess({
118
+ isSuperAdmin: !!acl.isSuperAdmin,
119
+ scopeTenantId: tenantId,
120
+ target: role
121
+ });
122
+ if (access === "forbidden") {
123
+ return NextResponse.json({ error: "Forbidden" }, { status: 403 });
124
+ }
125
+ if (access === "not-found") {
107
126
  return NextResponse.json({ error: "Role not found" }, { status: 404 });
108
127
  }
109
128
  const guardResult = await validateCrudMutationGuard(container, {
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "version": 3,
3
3
  "sources": ["../../../../../../src/modules/dashboards/api/roles/widgets/route.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 { DashboardRoleWidgets } from '@open-mercato/core/modules/dashboards/data/entities'\nimport { Role } from '@open-mercato/core/modules/auth/data/entities'\nimport { roleWidgetSettingsSchema } from '@open-mercato/core/modules/dashboards/data/validators'\nimport { loadAllWidgets } from '@open-mercato/core/modules/dashboards/lib/widgets'\nimport { resolveWidgetAssignmentReadScope } from '@open-mercato/core/modules/dashboards/lib/widgetAssignmentScope'\nimport { hasFeature } from '@open-mercato/shared/security/features'\nimport {\n runCrudMutationGuardAfterSuccess,\n validateCrudMutationGuard,\n} from '@open-mercato/shared/lib/crud/mutation-guard'\nimport type { OpenApiMethodDoc, OpenApiRouteDoc } from '@open-mercato/shared/lib/openapi'\nimport {\n dashboardsTag,\n dashboardsErrorSchema,\n dashboardRoleWidgetsResponseSchema,\n dashboardRoleWidgetsUpdateResponseSchema,\n} from '../../openapi'\n\nconst FEATURE = 'dashboards.admin.assign-widgets'\nconst RESOURCE_KIND = 'dashboards.roleWidgets'\n\nfunction pickBestRecord(records: DashboardRoleWidgets[], tenantId: string | null, organizationId: string | null): DashboardRoleWidgets | null {\n let best: DashboardRoleWidgets | null = null\n let bestScore = -1\n for (const record of records) {\n if (record.deletedAt) continue\n if (record.tenantId && tenantId && record.tenantId !== tenantId) continue\n if (record.tenantId && !tenantId) continue\n if (record.organizationId && organizationId && record.organizationId !== organizationId) continue\n if (record.organizationId && !organizationId) continue\n const score = (record.tenantId ? 1 : 0) + (record.organizationId ? 2 : 0)\n if (score > bestScore) {\n best = record\n bestScore = score\n }\n }\n return best\n}\n\nexport const metadata = {\n GET: { requireAuth: true, requireFeatures: [FEATURE] },\n PUT: { requireAuth: true, requireFeatures: [FEATURE] },\n}\n\nexport async function GET(req: Request) {\n const auth = await getAuthFromRequest(req)\n if (!auth) return NextResponse.json({ error: 'Unauthorized' }, { status: 401 })\n\n const url = new URL(req.url)\n const roleId = url.searchParams.get('roleId')\n if (!roleId) return NextResponse.json({ error: 'roleId is required' }, { status: 400 })\n\n const container = await createRequestContainer()\n const em = container.resolve('em') as any\n const rbac = container.resolve('rbacService') as any\n const acl = await rbac.loadAcl(auth.sub, { tenantId: auth.tenantId ?? null, organizationId: auth.orgId ?? null })\n if (!acl.isSuperAdmin && !hasFeature(acl.features, FEATURE)) {\n return NextResponse.json({ error: 'Forbidden' }, { status: 403 })\n }\n\n const { tenantId, organizationId } = resolveWidgetAssignmentReadScope({\n auth: { tenantId: auth.tenantId ?? null, orgId: auth.orgId ?? null },\n isSuperAdmin: !!acl.isSuperAdmin,\n queryTenantId: url.searchParams.get('tenantId'),\n queryOrganizationId: url.searchParams.get('organizationId'),\n })\n\n const role = await em.findOne(Role, { id: roleId, deletedAt: null })\n if (!role || (tenantId && role.tenantId !== tenantId)) {\n return NextResponse.json({ error: 'Role not found' }, { status: 404 })\n }\n\n const records = await em.find(DashboardRoleWidgets, { roleId, deletedAt: null })\n const best = pickBestRecord(records, tenantId, organizationId)\n\n const response = {\n widgetIds: best ? best.widgetIdsJson : [],\n hasCustom: !!best,\n scope: {\n tenantId,\n organizationId,\n },\n }\n\n return NextResponse.json(response)\n}\n\nexport async function PUT(req: Request) {\n const auth = await getAuthFromRequest(req)\n if (!auth) return NextResponse.json({ error: 'Unauthorized' }, { status: 401 })\n\n let payload: unknown\n try {\n payload = await req.json()\n } catch {\n return NextResponse.json({ error: 'Invalid JSON body' }, { status: 400 })\n }\n const parsed = roleWidgetSettingsSchema.safeParse(payload)\n if (!parsed.success) {\n return NextResponse.json({ error: 'Invalid payload', issues: parsed.error.issues }, { status: 400 })\n }\n\n const container = await createRequestContainer()\n const { resolve } = container\n const em = resolve('em') as any\n const rbac = resolve('rbacService') as any\n const acl = await rbac.loadAcl(auth.sub, { tenantId: auth.tenantId ?? null, organizationId: auth.orgId ?? null })\n if (!acl.isSuperAdmin && !hasFeature(acl.features, FEATURE)) {\n return NextResponse.json({ error: 'Forbidden' }, { status: 403 })\n }\n\n const widgets = await loadAllWidgets()\n const validWidgetIds = new Set(widgets.map((w) => w.metadata.id))\n const widgetIds = parsed.data.widgetIds.filter((id) => validWidgetIds.has(id))\n\n const tenantId = auth.tenantId ?? null\n const organizationId = auth.orgId ?? null\n\n const role = await em.findOne(Role, { id: parsed.data.roleId, deletedAt: null })\n if (!role || (tenantId && role.tenantId !== tenantId)) {\n return NextResponse.json({ error: 'Role not found' }, { status: 404 })\n }\n\n const guardResult = await validateCrudMutationGuard(container, {\n tenantId: tenantId ?? '',\n organizationId,\n userId: String(auth.sub),\n resourceKind: RESOURCE_KIND,\n resourceId: parsed.data.roleId,\n operation: 'update',\n requestMethod: req.method,\n requestHeaders: req.headers,\n mutationPayload: { roleId: parsed.data.roleId, widgetIds },\n })\n if (guardResult && !guardResult.ok) {\n return NextResponse.json(guardResult.body, { status: guardResult.status })\n }\n\n let record = await em.findOne(DashboardRoleWidgets, {\n roleId: parsed.data.roleId,\n tenantId,\n organizationId,\n deletedAt: null,\n })\n\n if (!widgetIds.length) {\n if (record) {\n await em.remove(record).flush()\n }\n if (guardResult?.ok && guardResult.shouldRunAfterSuccess) {\n await runCrudMutationGuardAfterSuccess(container, {\n tenantId: tenantId ?? '',\n organizationId,\n userId: String(auth.sub),\n resourceKind: RESOURCE_KIND,\n resourceId: parsed.data.roleId,\n operation: 'update',\n requestMethod: req.method,\n requestHeaders: req.headers,\n metadata: guardResult.metadata ?? null,\n })\n }\n return NextResponse.json({ ok: true, widgetIds: [] })\n }\n\n if (!record) {\n record = em.create(DashboardRoleWidgets, {\n roleId: parsed.data.roleId,\n tenantId,\n organizationId,\n widgetIdsJson: widgetIds,\n })\n em.persist(record)\n } else {\n record.widgetIdsJson = widgetIds\n }\n await em.flush()\n\n if (guardResult?.ok && guardResult.shouldRunAfterSuccess) {\n await runCrudMutationGuardAfterSuccess(container, {\n tenantId: tenantId ?? '',\n organizationId,\n userId: String(auth.sub),\n resourceKind: RESOURCE_KIND,\n resourceId: parsed.data.roleId,\n operation: 'update',\n requestMethod: req.method,\n requestHeaders: req.headers,\n metadata: guardResult.metadata ?? null,\n })\n }\n\n return NextResponse.json({ ok: true, widgetIds })\n}\n\nconst roleWidgetsQuerySchema = z.object({\n roleId: z.string().uuid(),\n tenantId: z.string().uuid().optional(),\n organizationId: z.string().uuid().optional(),\n})\n\nconst roleWidgetsGetDoc: OpenApiMethodDoc = {\n summary: 'Fetch widget assignments for a role',\n description: 'Returns the widgets explicitly assigned to the given role together with the evaluation scope.',\n tags: [dashboardsTag],\n query: roleWidgetsQuerySchema,\n responses: [\n { status: 200, description: 'Current widget configuration for the role.', schema: dashboardRoleWidgetsResponseSchema },\n ],\n errors: [\n { status: 400, description: 'Missing role identifier', schema: dashboardsErrorSchema },\n { status: 401, description: 'Authentication required', schema: dashboardsErrorSchema },\n { status: 403, description: 'Insufficient permissions to manage role widgets', schema: dashboardsErrorSchema },\n ],\n}\n\nconst roleWidgetsPutDoc: OpenApiMethodDoc = {\n summary: 'Update widgets assigned to a role',\n description: 'Persists the widget list for a role within the provided tenant and organization scope.',\n tags: [dashboardsTag],\n requestBody: {\n contentType: 'application/json',\n schema: roleWidgetSettingsSchema,\n description: 'Role identifier and the widget ids that should remain assigned. Scope is derived from the authenticated session.',\n },\n responses: [\n { status: 200, description: 'Widgets updated successfully.', schema: dashboardRoleWidgetsUpdateResponseSchema },\n ],\n errors: [\n { status: 400, description: 'Invalid payload or unknown widgets', schema: dashboardsErrorSchema },\n { status: 401, description: 'Authentication required', schema: dashboardsErrorSchema },\n { status: 403, description: 'Insufficient permissions to manage role widgets', schema: dashboardsErrorSchema },\n ],\n}\n\nexport const openApi: OpenApiRouteDoc = {\n tag: dashboardsTag,\n summary: 'Manage dashboard widgets assigned to a role',\n methods: {\n GET: roleWidgetsGetDoc,\n PUT: roleWidgetsPutDoc,\n },\n}\n"],
5
- "mappings": "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",
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 { DashboardRoleWidgets } from '@open-mercato/core/modules/dashboards/data/entities'\nimport { Role } from '@open-mercato/core/modules/auth/data/entities'\nimport { roleWidgetSettingsSchema } from '@open-mercato/core/modules/dashboards/data/validators'\nimport { loadAllWidgets } from '@open-mercato/core/modules/dashboards/lib/widgets'\nimport {\n resolveWidgetAssignmentReadScope,\n resolveWidgetAssignmentTargetAccess,\n} from '@open-mercato/core/modules/dashboards/lib/widgetAssignmentScope'\nimport { hasFeature } from '@open-mercato/shared/security/features'\nimport {\n runCrudMutationGuardAfterSuccess,\n validateCrudMutationGuard,\n} from '@open-mercato/shared/lib/crud/mutation-guard'\nimport type { OpenApiMethodDoc, OpenApiRouteDoc } from '@open-mercato/shared/lib/openapi'\nimport {\n dashboardsTag,\n dashboardsErrorSchema,\n dashboardRoleWidgetsResponseSchema,\n dashboardRoleWidgetsUpdateResponseSchema,\n} from '../../openapi'\n\nconst FEATURE = 'dashboards.admin.assign-widgets'\nconst RESOURCE_KIND = 'dashboards.roleWidgets'\n\nfunction pickBestRecord(records: DashboardRoleWidgets[], tenantId: string | null, organizationId: string | null): DashboardRoleWidgets | null {\n let best: DashboardRoleWidgets | null = null\n let bestScore = -1\n for (const record of records) {\n if (record.deletedAt) continue\n if (record.tenantId && tenantId && record.tenantId !== tenantId) continue\n if (record.tenantId && !tenantId) continue\n if (record.organizationId && organizationId && record.organizationId !== organizationId) continue\n if (record.organizationId && !organizationId) continue\n const score = (record.tenantId ? 1 : 0) + (record.organizationId ? 2 : 0)\n if (score > bestScore) {\n best = record\n bestScore = score\n }\n }\n return best\n}\n\nexport const metadata = {\n GET: { requireAuth: true, requireFeatures: [FEATURE] },\n PUT: { requireAuth: true, requireFeatures: [FEATURE] },\n}\n\nexport async function GET(req: Request) {\n const auth = await getAuthFromRequest(req)\n if (!auth) return NextResponse.json({ error: 'Unauthorized' }, { status: 401 })\n\n const url = new URL(req.url)\n const roleId = url.searchParams.get('roleId')\n if (!roleId) return NextResponse.json({ error: 'roleId is required' }, { status: 400 })\n\n const container = await createRequestContainer()\n const em = container.resolve('em') as any\n const rbac = container.resolve('rbacService') as any\n const acl = await rbac.loadAcl(auth.sub, { tenantId: auth.tenantId ?? null, organizationId: auth.orgId ?? null })\n if (!acl.isSuperAdmin && !hasFeature(acl.features, FEATURE)) {\n return NextResponse.json({ error: 'Forbidden' }, { status: 403 })\n }\n\n const { tenantId, organizationId } = resolveWidgetAssignmentReadScope({\n auth: { tenantId: auth.tenantId ?? null, orgId: auth.orgId ?? null },\n isSuperAdmin: !!acl.isSuperAdmin,\n queryTenantId: url.searchParams.get('tenantId'),\n queryOrganizationId: url.searchParams.get('organizationId'),\n })\n\n const role = await em.findOne(Role, { id: roleId, deletedAt: null })\n const access = resolveWidgetAssignmentTargetAccess({\n isSuperAdmin: !!acl.isSuperAdmin,\n scopeTenantId: tenantId,\n target: role,\n })\n if (access === 'forbidden') {\n return NextResponse.json({ error: 'Forbidden' }, { status: 403 })\n }\n if (access === 'not-found') {\n return NextResponse.json({ error: 'Role not found' }, { status: 404 })\n }\n\n const records = await em.find(DashboardRoleWidgets, { roleId, deletedAt: null })\n const best = pickBestRecord(records, tenantId, organizationId)\n\n const response = {\n widgetIds: best ? best.widgetIdsJson : [],\n hasCustom: !!best,\n scope: {\n tenantId,\n organizationId,\n },\n }\n\n return NextResponse.json(response)\n}\n\nexport async function PUT(req: Request) {\n const auth = await getAuthFromRequest(req)\n if (!auth) return NextResponse.json({ error: 'Unauthorized' }, { status: 401 })\n\n let payload: unknown\n try {\n payload = await req.json()\n } catch {\n return NextResponse.json({ error: 'Invalid JSON body' }, { status: 400 })\n }\n const parsed = roleWidgetSettingsSchema.safeParse(payload)\n if (!parsed.success) {\n return NextResponse.json({ error: 'Invalid payload', issues: parsed.error.issues }, { status: 400 })\n }\n\n const container = await createRequestContainer()\n const { resolve } = container\n const em = resolve('em') as any\n const rbac = resolve('rbacService') as any\n const acl = await rbac.loadAcl(auth.sub, { tenantId: auth.tenantId ?? null, organizationId: auth.orgId ?? null })\n if (!acl.isSuperAdmin && !hasFeature(acl.features, FEATURE)) {\n return NextResponse.json({ error: 'Forbidden' }, { status: 403 })\n }\n\n const widgets = await loadAllWidgets()\n const validWidgetIds = new Set(widgets.map((w) => w.metadata.id))\n const widgetIds = parsed.data.widgetIds.filter((id) => validWidgetIds.has(id))\n\n const tenantId = auth.tenantId ?? null\n const organizationId = auth.orgId ?? null\n\n const role = await em.findOne(Role, { id: parsed.data.roleId, deletedAt: null })\n const access = resolveWidgetAssignmentTargetAccess({\n isSuperAdmin: !!acl.isSuperAdmin,\n scopeTenantId: tenantId,\n target: role,\n })\n if (access === 'forbidden') {\n return NextResponse.json({ error: 'Forbidden' }, { status: 403 })\n }\n if (access === 'not-found') {\n return NextResponse.json({ error: 'Role not found' }, { status: 404 })\n }\n\n const guardResult = await validateCrudMutationGuard(container, {\n tenantId: tenantId ?? '',\n organizationId,\n userId: String(auth.sub),\n resourceKind: RESOURCE_KIND,\n resourceId: parsed.data.roleId,\n operation: 'update',\n requestMethod: req.method,\n requestHeaders: req.headers,\n mutationPayload: { roleId: parsed.data.roleId, widgetIds },\n })\n if (guardResult && !guardResult.ok) {\n return NextResponse.json(guardResult.body, { status: guardResult.status })\n }\n\n let record = await em.findOne(DashboardRoleWidgets, {\n roleId: parsed.data.roleId,\n tenantId,\n organizationId,\n deletedAt: null,\n })\n\n if (!widgetIds.length) {\n if (record) {\n await em.remove(record).flush()\n }\n if (guardResult?.ok && guardResult.shouldRunAfterSuccess) {\n await runCrudMutationGuardAfterSuccess(container, {\n tenantId: tenantId ?? '',\n organizationId,\n userId: String(auth.sub),\n resourceKind: RESOURCE_KIND,\n resourceId: parsed.data.roleId,\n operation: 'update',\n requestMethod: req.method,\n requestHeaders: req.headers,\n metadata: guardResult.metadata ?? null,\n })\n }\n return NextResponse.json({ ok: true, widgetIds: [] })\n }\n\n if (!record) {\n record = em.create(DashboardRoleWidgets, {\n roleId: parsed.data.roleId,\n tenantId,\n organizationId,\n widgetIdsJson: widgetIds,\n })\n em.persist(record)\n } else {\n record.widgetIdsJson = widgetIds\n }\n await em.flush()\n\n if (guardResult?.ok && guardResult.shouldRunAfterSuccess) {\n await runCrudMutationGuardAfterSuccess(container, {\n tenantId: tenantId ?? '',\n organizationId,\n userId: String(auth.sub),\n resourceKind: RESOURCE_KIND,\n resourceId: parsed.data.roleId,\n operation: 'update',\n requestMethod: req.method,\n requestHeaders: req.headers,\n metadata: guardResult.metadata ?? null,\n })\n }\n\n return NextResponse.json({ ok: true, widgetIds })\n}\n\nconst roleWidgetsQuerySchema = z.object({\n roleId: z.string().uuid(),\n tenantId: z.string().uuid().optional(),\n organizationId: z.string().uuid().optional(),\n})\n\nconst roleWidgetsGetDoc: OpenApiMethodDoc = {\n summary: 'Fetch widget assignments for a role',\n description: 'Returns the widgets explicitly assigned to the given role together with the evaluation scope.',\n tags: [dashboardsTag],\n query: roleWidgetsQuerySchema,\n responses: [\n { status: 200, description: 'Current widget configuration for the role.', schema: dashboardRoleWidgetsResponseSchema },\n ],\n errors: [\n { status: 400, description: 'Missing role identifier', schema: dashboardsErrorSchema },\n { status: 401, description: 'Authentication required', schema: dashboardsErrorSchema },\n { status: 403, description: 'Insufficient permissions to manage role widgets', schema: dashboardsErrorSchema },\n ],\n}\n\nconst roleWidgetsPutDoc: OpenApiMethodDoc = {\n summary: 'Update widgets assigned to a role',\n description: 'Persists the widget list for a role within the provided tenant and organization scope.',\n tags: [dashboardsTag],\n requestBody: {\n contentType: 'application/json',\n schema: roleWidgetSettingsSchema,\n description: 'Role identifier and the widget ids that should remain assigned. Scope is derived from the authenticated session.',\n },\n responses: [\n { status: 200, description: 'Widgets updated successfully.', schema: dashboardRoleWidgetsUpdateResponseSchema },\n ],\n errors: [\n { status: 400, description: 'Invalid payload or unknown widgets', schema: dashboardsErrorSchema },\n { status: 401, description: 'Authentication required', schema: dashboardsErrorSchema },\n { status: 403, description: 'Insufficient permissions to manage role widgets', schema: dashboardsErrorSchema },\n ],\n}\n\nexport const openApi: OpenApiRouteDoc = {\n tag: dashboardsTag,\n summary: 'Manage dashboard widgets assigned to a role',\n methods: {\n GET: roleWidgetsGetDoc,\n PUT: roleWidgetsPutDoc,\n },\n}\n"],
5
+ "mappings": "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",
6
6
  "names": []
7
7
  }
@@ -7,7 +7,10 @@ import { User } from "@open-mercato/core/modules/auth/data/entities";
7
7
  import { userWidgetSettingsSchema } from "@open-mercato/core/modules/dashboards/data/validators";
8
8
  import { loadAllWidgets } from "@open-mercato/core/modules/dashboards/lib/widgets";
9
9
  import { resolveAllowedWidgetIds } from "@open-mercato/core/modules/dashboards/lib/access";
10
- import { resolveWidgetAssignmentReadScope } from "@open-mercato/core/modules/dashboards/lib/widgetAssignmentScope";
10
+ import {
11
+ resolveWidgetAssignmentReadScope,
12
+ resolveWidgetAssignmentTargetAccess
13
+ } from "@open-mercato/core/modules/dashboards/lib/widgetAssignmentScope";
11
14
  import { hasFeature } from "@open-mercato/shared/security/features";
12
15
  import {
13
16
  runCrudMutationGuardAfterSuccess,
@@ -45,7 +48,15 @@ async function GET(req) {
45
48
  queryOrganizationId: url.searchParams.get("organizationId")
46
49
  });
47
50
  const targetUserForRead = await em.findOne(User, { id: userId, deletedAt: null });
48
- if (!targetUserForRead || tenantId && (targetUserForRead.tenantId ?? null) !== tenantId) {
51
+ const readAccess = resolveWidgetAssignmentTargetAccess({
52
+ isSuperAdmin: !!acl.isSuperAdmin,
53
+ scopeTenantId: tenantId,
54
+ target: targetUserForRead
55
+ });
56
+ if (readAccess === "forbidden") {
57
+ return NextResponse.json({ error: "Forbidden" }, { status: 403 });
58
+ }
59
+ if (readAccess === "not-found") {
49
60
  return NextResponse.json({ error: "User not found" }, { status: 404 });
50
61
  }
51
62
  const widgets = await loadAllWidgets();
@@ -103,7 +114,15 @@ async function PUT(req) {
103
114
  const tenantId = auth.tenantId ?? null;
104
115
  const organizationId = auth.orgId ?? null;
105
116
  const targetUser = await em.findOne(User, { id: parsed.data.userId, deletedAt: null });
106
- if (!targetUser || tenantId && (targetUser.tenantId ?? null) !== tenantId) {
117
+ const writeAccess = resolveWidgetAssignmentTargetAccess({
118
+ isSuperAdmin: !!acl.isSuperAdmin,
119
+ scopeTenantId: tenantId,
120
+ target: targetUser
121
+ });
122
+ if (writeAccess === "forbidden") {
123
+ return NextResponse.json({ error: "Forbidden" }, { status: 403 });
124
+ }
125
+ if (writeAccess === "not-found") {
107
126
  return NextResponse.json({ error: "User not found" }, { status: 404 });
108
127
  }
109
128
  const guardResult = await validateCrudMutationGuard(container, {
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "version": 3,
3
3
  "sources": ["../../../../../../src/modules/dashboards/api/users/widgets/route.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 { DashboardUserWidgets } from '@open-mercato/core/modules/dashboards/data/entities'\nimport { User } from '@open-mercato/core/modules/auth/data/entities'\nimport { userWidgetSettingsSchema } from '@open-mercato/core/modules/dashboards/data/validators'\nimport { loadAllWidgets } from '@open-mercato/core/modules/dashboards/lib/widgets'\nimport { resolveAllowedWidgetIds } from '@open-mercato/core/modules/dashboards/lib/access'\nimport { resolveWidgetAssignmentReadScope } from '@open-mercato/core/modules/dashboards/lib/widgetAssignmentScope'\nimport { hasFeature } from '@open-mercato/shared/security/features'\nimport {\n runCrudMutationGuardAfterSuccess,\n validateCrudMutationGuard,\n} from '@open-mercato/shared/lib/crud/mutation-guard'\nimport type { OpenApiMethodDoc, OpenApiRouteDoc } from '@open-mercato/shared/lib/openapi'\nimport {\n dashboardsTag,\n dashboardsErrorSchema,\n dashboardUserWidgetsResponseSchema,\n dashboardUserWidgetsUpdateResponseSchema,\n} from '../../openapi'\n\nconst FEATURE = 'dashboards.admin.assign-widgets'\nconst RESOURCE_KIND = 'dashboards.userWidgets'\n\nexport const metadata = {\n GET: { requireAuth: true, requireFeatures: [FEATURE] },\n PUT: { requireAuth: true, requireFeatures: [FEATURE] },\n}\n\nexport async function GET(req: Request) {\n const auth = await getAuthFromRequest(req)\n if (!auth) return NextResponse.json({ error: 'Unauthorized' }, { status: 401 })\n\n const url = new URL(req.url)\n const userId = url.searchParams.get('userId')\n if (!userId) return NextResponse.json({ error: 'userId is required' }, { status: 400 })\n\n const container = await createRequestContainer()\n const em = container.resolve('em') as any\n const rbac = container.resolve('rbacService') as any\n const acl = await rbac.loadAcl(auth.sub, { tenantId: auth.tenantId ?? null, organizationId: auth.orgId ?? null })\n if (!acl.isSuperAdmin && !hasFeature(acl.features, FEATURE)) {\n return NextResponse.json({ error: 'Forbidden' }, { status: 403 })\n }\n\n const { tenantId, organizationId } = resolveWidgetAssignmentReadScope({\n auth: { tenantId: auth.tenantId ?? null, orgId: auth.orgId ?? null },\n isSuperAdmin: !!acl.isSuperAdmin,\n queryTenantId: url.searchParams.get('tenantId'),\n queryOrganizationId: url.searchParams.get('organizationId'),\n })\n\n const targetUserForRead = await em.findOne(User, { id: userId, deletedAt: null })\n if (!targetUserForRead || (tenantId && (targetUserForRead.tenantId ?? null) !== tenantId)) {\n return NextResponse.json({ error: 'User not found' }, { status: 404 })\n }\n\n const widgets = await loadAllWidgets()\n const targetAcl = await rbac.loadAcl(userId, { tenantId, organizationId })\n const allowed = await resolveAllowedWidgetIds(\n em,\n {\n userId,\n tenantId,\n organizationId,\n features: targetAcl.features ?? [],\n isSuperAdmin: !!targetAcl.isSuperAdmin,\n },\n widgets,\n )\n\n const record = await em.findOne(DashboardUserWidgets, {\n userId,\n tenantId,\n organizationId,\n deletedAt: null,\n })\n\n const response = {\n mode: record ? record.mode : 'inherit',\n widgetIds: record && record.mode === 'override' ? record.widgetIdsJson : [],\n hasCustom: !!record && record.mode === 'override',\n effectiveWidgetIds: allowed,\n scope: { tenantId, organizationId },\n }\n\n return NextResponse.json(response)\n}\n\nexport async function PUT(req: Request) {\n const auth = await getAuthFromRequest(req)\n if (!auth) return NextResponse.json({ error: 'Unauthorized' }, { status: 401 })\n\n let payload: unknown\n try {\n payload = await req.json()\n } catch {\n return NextResponse.json({ error: 'Invalid JSON body' }, { status: 400 })\n }\n const parsed = userWidgetSettingsSchema.safeParse(payload)\n if (!parsed.success) {\n return NextResponse.json({ error: 'Invalid payload', issues: parsed.error.issues }, { status: 400 })\n }\n\n const container = await createRequestContainer()\n const { resolve } = container\n const em = resolve('em') as any\n const rbac = resolve('rbacService') as any\n const acl = await rbac.loadAcl(auth.sub, { tenantId: auth.tenantId ?? null, organizationId: auth.orgId ?? null })\n if (!acl.isSuperAdmin && !hasFeature(acl.features, FEATURE)) {\n return NextResponse.json({ error: 'Forbidden' }, { status: 403 })\n }\n\n const widgets = await loadAllWidgets()\n const validWidgetIds = new Set(widgets.map((w) => w.metadata.id))\n const widgetIds = parsed.data.widgetIds.filter((id) => validWidgetIds.has(id))\n\n const tenantId = auth.tenantId ?? null\n const organizationId = auth.orgId ?? null\n\n const targetUser = await em.findOne(User, { id: parsed.data.userId, deletedAt: null })\n if (!targetUser || (tenantId && (targetUser.tenantId ?? null) !== tenantId)) {\n return NextResponse.json({ error: 'User not found' }, { status: 404 })\n }\n\n const guardResult = await validateCrudMutationGuard(container, {\n tenantId: tenantId ?? '',\n organizationId,\n userId: String(auth.sub),\n resourceKind: RESOURCE_KIND,\n resourceId: parsed.data.userId,\n operation: 'update',\n requestMethod: req.method,\n requestHeaders: req.headers,\n mutationPayload: { userId: parsed.data.userId, mode: parsed.data.mode, widgetIds },\n })\n if (guardResult && !guardResult.ok) {\n return NextResponse.json(guardResult.body, { status: guardResult.status })\n }\n\n let record = await em.findOne(DashboardUserWidgets, {\n userId: parsed.data.userId,\n tenantId,\n organizationId,\n deletedAt: null,\n })\n\n if (parsed.data.mode === 'inherit') {\n if (record) {\n await em.remove(record).flush()\n }\n if (guardResult?.ok && guardResult.shouldRunAfterSuccess) {\n await runCrudMutationGuardAfterSuccess(container, {\n tenantId: tenantId ?? '',\n organizationId,\n userId: String(auth.sub),\n resourceKind: RESOURCE_KIND,\n resourceId: parsed.data.userId,\n operation: 'update',\n requestMethod: req.method,\n requestHeaders: req.headers,\n metadata: guardResult.metadata ?? null,\n })\n }\n return NextResponse.json({ ok: true, mode: 'inherit', widgetIds: [] })\n }\n\n if (!record) {\n record = em.create(DashboardUserWidgets, {\n userId: parsed.data.userId,\n tenantId,\n organizationId,\n mode: 'override',\n widgetIdsJson: widgetIds,\n })\n em.persist(record)\n } else {\n record.mode = 'override'\n record.widgetIdsJson = widgetIds\n }\n await em.flush()\n\n if (guardResult?.ok && guardResult.shouldRunAfterSuccess) {\n await runCrudMutationGuardAfterSuccess(container, {\n tenantId: tenantId ?? '',\n organizationId,\n userId: String(auth.sub),\n resourceKind: RESOURCE_KIND,\n resourceId: parsed.data.userId,\n operation: 'update',\n requestMethod: req.method,\n requestHeaders: req.headers,\n metadata: guardResult.metadata ?? null,\n })\n }\n\n return NextResponse.json({ ok: true, mode: 'override', widgetIds })\n}\n\nconst userWidgetsQuerySchema = z.object({\n userId: z.string().uuid(),\n tenantId: z.string().uuid().optional(),\n organizationId: z.string().uuid().optional(),\n})\n\nconst userWidgetsGetDoc: OpenApiMethodDoc = {\n summary: 'Read widget overrides for a user',\n description: 'Returns the widgets inherited and explicitly configured for the requested user within the current scope.',\n tags: [dashboardsTag],\n query: userWidgetsQuerySchema,\n responses: [\n { status: 200, description: 'Widget settings for the user.', schema: dashboardUserWidgetsResponseSchema },\n ],\n errors: [\n { status: 400, description: 'Missing user identifier', schema: dashboardsErrorSchema },\n { status: 401, description: 'Authentication required', schema: dashboardsErrorSchema },\n { status: 403, description: 'Insufficient permissions to manage user widgets', schema: dashboardsErrorSchema },\n ],\n}\n\nconst userWidgetsPutDoc: OpenApiMethodDoc = {\n summary: 'Update user-specific dashboard widgets',\n description: 'Sets the widget override mode and allowed widgets for a user. Passing `mode: inherit` clears overrides.',\n tags: [dashboardsTag],\n requestBody: {\n contentType: 'application/json',\n schema: userWidgetSettingsSchema,\n description: 'User identifier, override mode, and widget ids. Scope is derived from the authenticated session.',\n },\n responses: [\n { status: 200, description: 'Overrides saved.', schema: dashboardUserWidgetsUpdateResponseSchema },\n ],\n errors: [\n { status: 400, description: 'Invalid payload or unknown widgets', schema: dashboardsErrorSchema },\n { status: 401, description: 'Authentication required', schema: dashboardsErrorSchema },\n { status: 403, description: 'Insufficient permissions to manage user widgets', schema: dashboardsErrorSchema },\n ],\n}\n\nexport const openApi: OpenApiRouteDoc = {\n tag: dashboardsTag,\n summary: 'Manage user-level dashboard widgets',\n methods: {\n GET: userWidgetsGetDoc,\n PUT: userWidgetsPutDoc,\n },\n}\n"],
5
- "mappings": "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",
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 { DashboardUserWidgets } from '@open-mercato/core/modules/dashboards/data/entities'\nimport { User } from '@open-mercato/core/modules/auth/data/entities'\nimport { userWidgetSettingsSchema } from '@open-mercato/core/modules/dashboards/data/validators'\nimport { loadAllWidgets } from '@open-mercato/core/modules/dashboards/lib/widgets'\nimport { resolveAllowedWidgetIds } from '@open-mercato/core/modules/dashboards/lib/access'\nimport {\n resolveWidgetAssignmentReadScope,\n resolveWidgetAssignmentTargetAccess,\n} from '@open-mercato/core/modules/dashboards/lib/widgetAssignmentScope'\nimport { hasFeature } from '@open-mercato/shared/security/features'\nimport {\n runCrudMutationGuardAfterSuccess,\n validateCrudMutationGuard,\n} from '@open-mercato/shared/lib/crud/mutation-guard'\nimport type { OpenApiMethodDoc, OpenApiRouteDoc } from '@open-mercato/shared/lib/openapi'\nimport {\n dashboardsTag,\n dashboardsErrorSchema,\n dashboardUserWidgetsResponseSchema,\n dashboardUserWidgetsUpdateResponseSchema,\n} from '../../openapi'\n\nconst FEATURE = 'dashboards.admin.assign-widgets'\nconst RESOURCE_KIND = 'dashboards.userWidgets'\n\nexport const metadata = {\n GET: { requireAuth: true, requireFeatures: [FEATURE] },\n PUT: { requireAuth: true, requireFeatures: [FEATURE] },\n}\n\nexport async function GET(req: Request) {\n const auth = await getAuthFromRequest(req)\n if (!auth) return NextResponse.json({ error: 'Unauthorized' }, { status: 401 })\n\n const url = new URL(req.url)\n const userId = url.searchParams.get('userId')\n if (!userId) return NextResponse.json({ error: 'userId is required' }, { status: 400 })\n\n const container = await createRequestContainer()\n const em = container.resolve('em') as any\n const rbac = container.resolve('rbacService') as any\n const acl = await rbac.loadAcl(auth.sub, { tenantId: auth.tenantId ?? null, organizationId: auth.orgId ?? null })\n if (!acl.isSuperAdmin && !hasFeature(acl.features, FEATURE)) {\n return NextResponse.json({ error: 'Forbidden' }, { status: 403 })\n }\n\n const { tenantId, organizationId } = resolveWidgetAssignmentReadScope({\n auth: { tenantId: auth.tenantId ?? null, orgId: auth.orgId ?? null },\n isSuperAdmin: !!acl.isSuperAdmin,\n queryTenantId: url.searchParams.get('tenantId'),\n queryOrganizationId: url.searchParams.get('organizationId'),\n })\n\n const targetUserForRead = await em.findOne(User, { id: userId, deletedAt: null })\n const readAccess = resolveWidgetAssignmentTargetAccess({\n isSuperAdmin: !!acl.isSuperAdmin,\n scopeTenantId: tenantId,\n target: targetUserForRead,\n })\n if (readAccess === 'forbidden') {\n return NextResponse.json({ error: 'Forbidden' }, { status: 403 })\n }\n if (readAccess === 'not-found') {\n return NextResponse.json({ error: 'User not found' }, { status: 404 })\n }\n\n const widgets = await loadAllWidgets()\n const targetAcl = await rbac.loadAcl(userId, { tenantId, organizationId })\n const allowed = await resolveAllowedWidgetIds(\n em,\n {\n userId,\n tenantId,\n organizationId,\n features: targetAcl.features ?? [],\n isSuperAdmin: !!targetAcl.isSuperAdmin,\n },\n widgets,\n )\n\n const record = await em.findOne(DashboardUserWidgets, {\n userId,\n tenantId,\n organizationId,\n deletedAt: null,\n })\n\n const response = {\n mode: record ? record.mode : 'inherit',\n widgetIds: record && record.mode === 'override' ? record.widgetIdsJson : [],\n hasCustom: !!record && record.mode === 'override',\n effectiveWidgetIds: allowed,\n scope: { tenantId, organizationId },\n }\n\n return NextResponse.json(response)\n}\n\nexport async function PUT(req: Request) {\n const auth = await getAuthFromRequest(req)\n if (!auth) return NextResponse.json({ error: 'Unauthorized' }, { status: 401 })\n\n let payload: unknown\n try {\n payload = await req.json()\n } catch {\n return NextResponse.json({ error: 'Invalid JSON body' }, { status: 400 })\n }\n const parsed = userWidgetSettingsSchema.safeParse(payload)\n if (!parsed.success) {\n return NextResponse.json({ error: 'Invalid payload', issues: parsed.error.issues }, { status: 400 })\n }\n\n const container = await createRequestContainer()\n const { resolve } = container\n const em = resolve('em') as any\n const rbac = resolve('rbacService') as any\n const acl = await rbac.loadAcl(auth.sub, { tenantId: auth.tenantId ?? null, organizationId: auth.orgId ?? null })\n if (!acl.isSuperAdmin && !hasFeature(acl.features, FEATURE)) {\n return NextResponse.json({ error: 'Forbidden' }, { status: 403 })\n }\n\n const widgets = await loadAllWidgets()\n const validWidgetIds = new Set(widgets.map((w) => w.metadata.id))\n const widgetIds = parsed.data.widgetIds.filter((id) => validWidgetIds.has(id))\n\n const tenantId = auth.tenantId ?? null\n const organizationId = auth.orgId ?? null\n\n const targetUser = await em.findOne(User, { id: parsed.data.userId, deletedAt: null })\n const writeAccess = resolveWidgetAssignmentTargetAccess({\n isSuperAdmin: !!acl.isSuperAdmin,\n scopeTenantId: tenantId,\n target: targetUser,\n })\n if (writeAccess === 'forbidden') {\n return NextResponse.json({ error: 'Forbidden' }, { status: 403 })\n }\n if (writeAccess === 'not-found') {\n return NextResponse.json({ error: 'User not found' }, { status: 404 })\n }\n\n const guardResult = await validateCrudMutationGuard(container, {\n tenantId: tenantId ?? '',\n organizationId,\n userId: String(auth.sub),\n resourceKind: RESOURCE_KIND,\n resourceId: parsed.data.userId,\n operation: 'update',\n requestMethod: req.method,\n requestHeaders: req.headers,\n mutationPayload: { userId: parsed.data.userId, mode: parsed.data.mode, widgetIds },\n })\n if (guardResult && !guardResult.ok) {\n return NextResponse.json(guardResult.body, { status: guardResult.status })\n }\n\n let record = await em.findOne(DashboardUserWidgets, {\n userId: parsed.data.userId,\n tenantId,\n organizationId,\n deletedAt: null,\n })\n\n if (parsed.data.mode === 'inherit') {\n if (record) {\n await em.remove(record).flush()\n }\n if (guardResult?.ok && guardResult.shouldRunAfterSuccess) {\n await runCrudMutationGuardAfterSuccess(container, {\n tenantId: tenantId ?? '',\n organizationId,\n userId: String(auth.sub),\n resourceKind: RESOURCE_KIND,\n resourceId: parsed.data.userId,\n operation: 'update',\n requestMethod: req.method,\n requestHeaders: req.headers,\n metadata: guardResult.metadata ?? null,\n })\n }\n return NextResponse.json({ ok: true, mode: 'inherit', widgetIds: [] })\n }\n\n if (!record) {\n record = em.create(DashboardUserWidgets, {\n userId: parsed.data.userId,\n tenantId,\n organizationId,\n mode: 'override',\n widgetIdsJson: widgetIds,\n })\n em.persist(record)\n } else {\n record.mode = 'override'\n record.widgetIdsJson = widgetIds\n }\n await em.flush()\n\n if (guardResult?.ok && guardResult.shouldRunAfterSuccess) {\n await runCrudMutationGuardAfterSuccess(container, {\n tenantId: tenantId ?? '',\n organizationId,\n userId: String(auth.sub),\n resourceKind: RESOURCE_KIND,\n resourceId: parsed.data.userId,\n operation: 'update',\n requestMethod: req.method,\n requestHeaders: req.headers,\n metadata: guardResult.metadata ?? null,\n })\n }\n\n return NextResponse.json({ ok: true, mode: 'override', widgetIds })\n}\n\nconst userWidgetsQuerySchema = z.object({\n userId: z.string().uuid(),\n tenantId: z.string().uuid().optional(),\n organizationId: z.string().uuid().optional(),\n})\n\nconst userWidgetsGetDoc: OpenApiMethodDoc = {\n summary: 'Read widget overrides for a user',\n description: 'Returns the widgets inherited and explicitly configured for the requested user within the current scope.',\n tags: [dashboardsTag],\n query: userWidgetsQuerySchema,\n responses: [\n { status: 200, description: 'Widget settings for the user.', schema: dashboardUserWidgetsResponseSchema },\n ],\n errors: [\n { status: 400, description: 'Missing user identifier', schema: dashboardsErrorSchema },\n { status: 401, description: 'Authentication required', schema: dashboardsErrorSchema },\n { status: 403, description: 'Insufficient permissions to manage user widgets', schema: dashboardsErrorSchema },\n ],\n}\n\nconst userWidgetsPutDoc: OpenApiMethodDoc = {\n summary: 'Update user-specific dashboard widgets',\n description: 'Sets the widget override mode and allowed widgets for a user. Passing `mode: inherit` clears overrides.',\n tags: [dashboardsTag],\n requestBody: {\n contentType: 'application/json',\n schema: userWidgetSettingsSchema,\n description: 'User identifier, override mode, and widget ids. Scope is derived from the authenticated session.',\n },\n responses: [\n { status: 200, description: 'Overrides saved.', schema: dashboardUserWidgetsUpdateResponseSchema },\n ],\n errors: [\n { status: 400, description: 'Invalid payload or unknown widgets', schema: dashboardsErrorSchema },\n { status: 401, description: 'Authentication required', schema: dashboardsErrorSchema },\n { status: 403, description: 'Insufficient permissions to manage user widgets', schema: dashboardsErrorSchema },\n ],\n}\n\nexport const openApi: OpenApiRouteDoc = {\n tag: dashboardsTag,\n summary: 'Manage user-level dashboard widgets',\n methods: {\n GET: userWidgetsGetDoc,\n PUT: userWidgetsPutDoc,\n },\n}\n"],
5
+ "mappings": "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",
6
6
  "names": []
7
7
  }
@@ -1,3 +1,10 @@
1
+ function resolveWidgetAssignmentTargetAccess(params) {
2
+ const { isSuperAdmin, scopeTenantId, target } = params;
3
+ if (!isSuperAdmin && !scopeTenantId) return "forbidden";
4
+ if (!target) return "not-found";
5
+ if (scopeTenantId && (target.tenantId ?? null) !== scopeTenantId) return "not-found";
6
+ return "allowed";
7
+ }
1
8
  function resolveWidgetAssignmentReadScope(params) {
2
9
  const { auth, isSuperAdmin, queryTenantId, queryOrganizationId } = params;
3
10
  if (isSuperAdmin) {
@@ -12,6 +19,7 @@ function resolveWidgetAssignmentReadScope(params) {
12
19
  };
13
20
  }
14
21
  export {
15
- resolveWidgetAssignmentReadScope
22
+ resolveWidgetAssignmentReadScope,
23
+ resolveWidgetAssignmentTargetAccess
16
24
  };
17
25
  //# sourceMappingURL=widgetAssignmentScope.js.map
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "version": 3,
3
3
  "sources": ["../../../../src/modules/dashboards/lib/widgetAssignmentScope.ts"],
4
- "sourcesContent": ["export type AuthScopeInput = {\n tenantId?: string | null\n orgId?: string | null\n}\n\nexport type ResolvedAssignmentScope = {\n tenantId: string | null\n organizationId: string | null\n}\n\nexport function resolveWidgetAssignmentReadScope(params: {\n auth: AuthScopeInput\n isSuperAdmin: boolean\n queryTenantId?: string | null\n queryOrganizationId?: string | null\n}): ResolvedAssignmentScope {\n const { auth, isSuperAdmin, queryTenantId, queryOrganizationId } = params\n if (isSuperAdmin) {\n return {\n tenantId: (queryTenantId && queryTenantId.length ? queryTenantId : auth.tenantId) ?? null,\n organizationId:\n (queryOrganizationId && queryOrganizationId.length ? queryOrganizationId : auth.orgId) ?? null,\n }\n }\n return {\n tenantId: auth.tenantId ?? null,\n organizationId: auth.orgId ?? null,\n }\n}\n"],
5
- "mappings": "AAUO,SAAS,iCAAiC,QAKrB;AAC1B,QAAM,EAAE,MAAM,cAAc,eAAe,oBAAoB,IAAI;AACnE,MAAI,cAAc;AAChB,WAAO;AAAA,MACL,WAAW,iBAAiB,cAAc,SAAS,gBAAgB,KAAK,aAAa;AAAA,MACrF,iBACG,uBAAuB,oBAAoB,SAAS,sBAAsB,KAAK,UAAU;AAAA,IAC9F;AAAA,EACF;AACA,SAAO;AAAA,IACL,UAAU,KAAK,YAAY;AAAA,IAC3B,gBAAgB,KAAK,SAAS;AAAA,EAChC;AACF;",
4
+ "sourcesContent": ["export type AuthScopeInput = {\n tenantId?: string | null\n orgId?: string | null\n}\n\nexport type ResolvedAssignmentScope = {\n tenantId: string | null\n organizationId: string | null\n}\n\nexport type WidgetAssignmentTarget = {\n tenantId?: string | null\n}\n\nexport type WidgetAssignmentTargetAccess = 'allowed' | 'forbidden' | 'not-found'\n\nexport function resolveWidgetAssignmentTargetAccess(params: {\n isSuperAdmin: boolean\n scopeTenantId: string | null\n target: WidgetAssignmentTarget | null | undefined\n}): WidgetAssignmentTargetAccess {\n const { isSuperAdmin, scopeTenantId, target } = params\n if (!isSuperAdmin && !scopeTenantId) return 'forbidden'\n if (!target) return 'not-found'\n if (scopeTenantId && (target.tenantId ?? null) !== scopeTenantId) return 'not-found'\n return 'allowed'\n}\n\nexport function resolveWidgetAssignmentReadScope(params: {\n auth: AuthScopeInput\n isSuperAdmin: boolean\n queryTenantId?: string | null\n queryOrganizationId?: string | null\n}): ResolvedAssignmentScope {\n const { auth, isSuperAdmin, queryTenantId, queryOrganizationId } = params\n if (isSuperAdmin) {\n return {\n tenantId: (queryTenantId && queryTenantId.length ? queryTenantId : auth.tenantId) ?? null,\n organizationId:\n (queryOrganizationId && queryOrganizationId.length ? queryOrganizationId : auth.orgId) ?? null,\n }\n }\n return {\n tenantId: auth.tenantId ?? null,\n organizationId: auth.orgId ?? null,\n }\n}\n"],
5
+ "mappings": "AAgBO,SAAS,oCAAoC,QAInB;AAC/B,QAAM,EAAE,cAAc,eAAe,OAAO,IAAI;AAChD,MAAI,CAAC,gBAAgB,CAAC,cAAe,QAAO;AAC5C,MAAI,CAAC,OAAQ,QAAO;AACpB,MAAI,kBAAkB,OAAO,YAAY,UAAU,cAAe,QAAO;AACzE,SAAO;AACT;AAEO,SAAS,iCAAiC,QAKrB;AAC1B,QAAM,EAAE,MAAM,cAAc,eAAe,oBAAoB,IAAI;AACnE,MAAI,cAAc;AAChB,WAAO;AAAA,MACL,WAAW,iBAAiB,cAAc,SAAS,gBAAgB,KAAK,aAAa;AAAA,MACrF,iBACG,uBAAuB,oBAAoB,SAAS,sBAAsB,KAAK,UAAU;AAAA,IAC9F;AAAA,EACF;AACA,SAAO;AAAA,IACL,UAAU,KAAK,YAAY;AAAA,IAC3B,gBAAgB,KAAK,SAAS;AAAA,EAChC;AACF;",
6
6
  "names": []
7
7
  }
@@ -3,19 +3,12 @@ import {
3
3
  dictionaryEntryCommandCreateSchema,
4
4
  dictionaryEntryCommandUpdateSchema
5
5
  } from "@open-mercato/core/modules/dictionaries/data/validators";
6
- import { registerDictionaryEntryCommands } from "@open-mercato/core/modules/dictionaries/commands/factory";
6
+ import {
7
+ ensureDictionaryEntryScope,
8
+ registerDictionaryEntryCommands
9
+ } from "@open-mercato/core/modules/dictionaries/commands/factory";
7
10
  import { CrudHttpError } from "@open-mercato/shared/lib/crud/errors";
8
11
  import { findOneWithDecryption } from "@open-mercato/shared/lib/encryption/find";
9
- function ensureScope(ctx, scope) {
10
- const tenantId = ctx.auth?.tenantId ?? null;
11
- if (tenantId && tenantId !== scope.tenantId) {
12
- throw new CrudHttpError(403, { error: "Forbidden" });
13
- }
14
- const organizationId = ctx.selectedOrganizationId ?? ctx.auth?.orgId ?? null;
15
- if (organizationId && organizationId !== scope.organizationId) {
16
- throw new CrudHttpError(403, { error: "Forbidden" });
17
- }
18
- }
19
12
  async function ensureDictionary(em, id) {
20
13
  const dictionary = await em.findOne(Dictionary, {
21
14
  id,
@@ -32,12 +25,12 @@ registerDictionaryEntryCommands({
32
25
  translationKeyPrefix: "dictionaries.entries",
33
26
  createSchema: dictionaryEntryCommandCreateSchema,
34
27
  updateSchema: dictionaryEntryCommandUpdateSchema,
35
- ensureScope,
28
+ ensureScope: ensureDictionaryEntryScope,
36
29
  duplicateError: "An entry with this value already exists.",
37
30
  resolveDictionaryForCreate: async ({ em, ctx, parsed }) => {
38
31
  const dictionary = await ensureDictionary(em, parsed.dictionaryId);
39
32
  const scope = { tenantId: dictionary.tenantId, organizationId: dictionary.organizationId };
40
- ensureScope(ctx, scope);
33
+ ensureDictionaryEntryScope(ctx, scope);
41
34
  return { dictionary, scope };
42
35
  },
43
36
  resolveEntry: async ({ em, ctx, id }) => {
@@ -49,7 +42,7 @@ registerDictionaryEntryCommands({
49
42
  throw new CrudHttpError(404, { error: "Dictionary not found" });
50
43
  }
51
44
  const scope = { tenantId: entry.tenantId, organizationId: entry.organizationId };
52
- ensureScope(ctx, scope);
45
+ ensureDictionaryEntryScope(ctx, scope);
53
46
  return { entry, dictionary: entry.dictionary, scope };
54
47
  },
55
48
  ensureDictionaryForUndo: async ({ em, snapshot }) => {
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "version": 3,
3
3
  "sources": ["../../../../src/modules/dictionaries/commands/entries.ts"],
4
- "sourcesContent": ["import type { EntityManager } from '@mikro-orm/postgresql'\nimport { Dictionary, DictionaryEntry } from '@open-mercato/core/modules/dictionaries/data/entities'\nimport {\n dictionaryEntryCommandCreateSchema,\n dictionaryEntryCommandUpdateSchema,\n} from '@open-mercato/core/modules/dictionaries/data/validators'\nimport { registerDictionaryEntryCommands } from '@open-mercato/core/modules/dictionaries/commands/factory'\nimport type { CommandRuntimeContext } from '@open-mercato/shared/lib/commands'\nimport { CrudHttpError } from '@open-mercato/shared/lib/crud/errors'\nimport { findOneWithDecryption } from '@open-mercato/shared/lib/encryption/find'\n\nfunction ensureScope(ctx: CommandRuntimeContext, scope: { tenantId: string; organizationId: string }): void {\n const tenantId = ctx.auth?.tenantId ?? null\n if (tenantId && tenantId !== scope.tenantId) {\n throw new CrudHttpError(403, { error: 'Forbidden' })\n }\n const organizationId = ctx.selectedOrganizationId ?? ctx.auth?.orgId ?? null\n if (organizationId && organizationId !== scope.organizationId) {\n throw new CrudHttpError(403, { error: 'Forbidden' })\n }\n}\n\nasync function ensureDictionary(em: EntityManager, id: string): Promise<Dictionary> {\n const dictionary = await em.findOne(Dictionary, {\n id,\n deletedAt: null,\n })\n if (!dictionary) {\n throw new CrudHttpError(404, { error: 'Dictionary not found' })\n }\n return dictionary\n}\n\nregisterDictionaryEntryCommands({\n commandPrefix: 'dictionaries.entries',\n resourceKind: 'dictionaries.entry',\n translationKeyPrefix: 'dictionaries.entries',\n createSchema: dictionaryEntryCommandCreateSchema,\n updateSchema: dictionaryEntryCommandUpdateSchema,\n ensureScope,\n duplicateError: 'An entry with this value already exists.',\n resolveDictionaryForCreate: async ({ em, ctx, parsed }) => {\n const dictionary = await ensureDictionary(em, parsed.dictionaryId)\n const scope = { tenantId: dictionary.tenantId, organizationId: dictionary.organizationId }\n ensureScope(ctx, scope)\n return { dictionary, scope }\n },\n resolveEntry: async ({ em, ctx, id }) => {\n const entry = await findOneWithDecryption(em, DictionaryEntry, id, { populate: ['dictionary'] })\n if (!entry) {\n throw new CrudHttpError(404, { error: 'Dictionary entry not found' })\n }\n if (entry.dictionary.deletedAt) {\n throw new CrudHttpError(404, { error: 'Dictionary not found' })\n }\n const scope = { tenantId: entry.tenantId, organizationId: entry.organizationId }\n ensureScope(ctx, scope)\n return { entry, dictionary: entry.dictionary, scope }\n },\n ensureDictionaryForUndo: async ({ em, snapshot }) => {\n return em.findOne(Dictionary, snapshot.dictionaryId, { filters: false })\n },\n})\n"],
5
- "mappings": "AACA,SAAS,YAAY,uBAAuB;AAC5C;AAAA,EACE;AAAA,EACA;AAAA,OACK;AACP,SAAS,uCAAuC;AAEhD,SAAS,qBAAqB;AAC9B,SAAS,6BAA6B;AAEtC,SAAS,YAAY,KAA4B,OAA2D;AAC1G,QAAM,WAAW,IAAI,MAAM,YAAY;AACvC,MAAI,YAAY,aAAa,MAAM,UAAU;AAC3C,UAAM,IAAI,cAAc,KAAK,EAAE,OAAO,YAAY,CAAC;AAAA,EACrD;AACA,QAAM,iBAAiB,IAAI,0BAA0B,IAAI,MAAM,SAAS;AACxE,MAAI,kBAAkB,mBAAmB,MAAM,gBAAgB;AAC7D,UAAM,IAAI,cAAc,KAAK,EAAE,OAAO,YAAY,CAAC;AAAA,EACrD;AACF;AAEA,eAAe,iBAAiB,IAAmB,IAAiC;AAClF,QAAM,aAAa,MAAM,GAAG,QAAQ,YAAY;AAAA,IAC9C;AAAA,IACA,WAAW;AAAA,EACb,CAAC;AACD,MAAI,CAAC,YAAY;AACf,UAAM,IAAI,cAAc,KAAK,EAAE,OAAO,uBAAuB,CAAC;AAAA,EAChE;AACA,SAAO;AACT;AAEA,gCAAgC;AAAA,EAC9B,eAAe;AAAA,EACf,cAAc;AAAA,EACd,sBAAsB;AAAA,EACtB,cAAc;AAAA,EACd,cAAc;AAAA,EACd;AAAA,EACA,gBAAgB;AAAA,EAChB,4BAA4B,OAAO,EAAE,IAAI,KAAK,OAAO,MAAM;AACzD,UAAM,aAAa,MAAM,iBAAiB,IAAI,OAAO,YAAY;AACjE,UAAM,QAAQ,EAAE,UAAU,WAAW,UAAU,gBAAgB,WAAW,eAAe;AACzF,gBAAY,KAAK,KAAK;AACtB,WAAO,EAAE,YAAY,MAAM;AAAA,EAC7B;AAAA,EACA,cAAc,OAAO,EAAE,IAAI,KAAK,GAAG,MAAM;AACvC,UAAM,QAAQ,MAAM,sBAAsB,IAAI,iBAAiB,IAAI,EAAE,UAAU,CAAC,YAAY,EAAE,CAAC;AAC/F,QAAI,CAAC,OAAO;AACV,YAAM,IAAI,cAAc,KAAK,EAAE,OAAO,6BAA6B,CAAC;AAAA,IACtE;AACA,QAAI,MAAM,WAAW,WAAW;AAC9B,YAAM,IAAI,cAAc,KAAK,EAAE,OAAO,uBAAuB,CAAC;AAAA,IAChE;AACA,UAAM,QAAQ,EAAE,UAAU,MAAM,UAAU,gBAAgB,MAAM,eAAe;AAC/E,gBAAY,KAAK,KAAK;AACtB,WAAO,EAAE,OAAO,YAAY,MAAM,YAAY,MAAM;AAAA,EACtD;AAAA,EACA,yBAAyB,OAAO,EAAE,IAAI,SAAS,MAAM;AACnD,WAAO,GAAG,QAAQ,YAAY,SAAS,cAAc,EAAE,SAAS,MAAM,CAAC;AAAA,EACzE;AACF,CAAC;",
4
+ "sourcesContent": ["import type { EntityManager } from '@mikro-orm/postgresql'\nimport { Dictionary, DictionaryEntry } from '@open-mercato/core/modules/dictionaries/data/entities'\nimport {\n dictionaryEntryCommandCreateSchema,\n dictionaryEntryCommandUpdateSchema,\n} from '@open-mercato/core/modules/dictionaries/data/validators'\nimport {\n ensureDictionaryEntryScope,\n registerDictionaryEntryCommands,\n} from '@open-mercato/core/modules/dictionaries/commands/factory'\nimport { CrudHttpError } from '@open-mercato/shared/lib/crud/errors'\nimport { findOneWithDecryption } from '@open-mercato/shared/lib/encryption/find'\n\nasync function ensureDictionary(em: EntityManager, id: string): Promise<Dictionary> {\n const dictionary = await em.findOne(Dictionary, {\n id,\n deletedAt: null,\n })\n if (!dictionary) {\n throw new CrudHttpError(404, { error: 'Dictionary not found' })\n }\n return dictionary\n}\n\nregisterDictionaryEntryCommands({\n commandPrefix: 'dictionaries.entries',\n resourceKind: 'dictionaries.entry',\n translationKeyPrefix: 'dictionaries.entries',\n createSchema: dictionaryEntryCommandCreateSchema,\n updateSchema: dictionaryEntryCommandUpdateSchema,\n ensureScope: ensureDictionaryEntryScope,\n duplicateError: 'An entry with this value already exists.',\n resolveDictionaryForCreate: async ({ em, ctx, parsed }) => {\n const dictionary = await ensureDictionary(em, parsed.dictionaryId)\n const scope = { tenantId: dictionary.tenantId, organizationId: dictionary.organizationId }\n ensureDictionaryEntryScope(ctx, scope)\n return { dictionary, scope }\n },\n resolveEntry: async ({ em, ctx, id }) => {\n const entry = await findOneWithDecryption(em, DictionaryEntry, id, { populate: ['dictionary'] })\n if (!entry) {\n throw new CrudHttpError(404, { error: 'Dictionary entry not found' })\n }\n if (entry.dictionary.deletedAt) {\n throw new CrudHttpError(404, { error: 'Dictionary not found' })\n }\n const scope = { tenantId: entry.tenantId, organizationId: entry.organizationId }\n ensureDictionaryEntryScope(ctx, scope)\n return { entry, dictionary: entry.dictionary, scope }\n },\n ensureDictionaryForUndo: async ({ em, snapshot }) => {\n return em.findOne(Dictionary, snapshot.dictionaryId, { filters: false })\n },\n})\n"],
5
+ "mappings": "AACA,SAAS,YAAY,uBAAuB;AAC5C;AAAA,EACE;AAAA,EACA;AAAA,OACK;AACP;AAAA,EACE;AAAA,EACA;AAAA,OACK;AACP,SAAS,qBAAqB;AAC9B,SAAS,6BAA6B;AAEtC,eAAe,iBAAiB,IAAmB,IAAiC;AAClF,QAAM,aAAa,MAAM,GAAG,QAAQ,YAAY;AAAA,IAC9C;AAAA,IACA,WAAW;AAAA,EACb,CAAC;AACD,MAAI,CAAC,YAAY;AACf,UAAM,IAAI,cAAc,KAAK,EAAE,OAAO,uBAAuB,CAAC;AAAA,EAChE;AACA,SAAO;AACT;AAEA,gCAAgC;AAAA,EAC9B,eAAe;AAAA,EACf,cAAc;AAAA,EACd,sBAAsB;AAAA,EACtB,cAAc;AAAA,EACd,cAAc;AAAA,EACd,aAAa;AAAA,EACb,gBAAgB;AAAA,EAChB,4BAA4B,OAAO,EAAE,IAAI,KAAK,OAAO,MAAM;AACzD,UAAM,aAAa,MAAM,iBAAiB,IAAI,OAAO,YAAY;AACjE,UAAM,QAAQ,EAAE,UAAU,WAAW,UAAU,gBAAgB,WAAW,eAAe;AACzF,+BAA2B,KAAK,KAAK;AACrC,WAAO,EAAE,YAAY,MAAM;AAAA,EAC7B;AAAA,EACA,cAAc,OAAO,EAAE,IAAI,KAAK,GAAG,MAAM;AACvC,UAAM,QAAQ,MAAM,sBAAsB,IAAI,iBAAiB,IAAI,EAAE,UAAU,CAAC,YAAY,EAAE,CAAC;AAC/F,QAAI,CAAC,OAAO;AACV,YAAM,IAAI,cAAc,KAAK,EAAE,OAAO,6BAA6B,CAAC;AAAA,IACtE;AACA,QAAI,MAAM,WAAW,WAAW;AAC9B,YAAM,IAAI,cAAc,KAAK,EAAE,OAAO,uBAAuB,CAAC;AAAA,IAChE;AACA,UAAM,QAAQ,EAAE,UAAU,MAAM,UAAU,gBAAgB,MAAM,eAAe;AAC/E,+BAA2B,KAAK,KAAK;AACrC,WAAO,EAAE,OAAO,YAAY,MAAM,YAAY,MAAM;AAAA,EACtD;AAAA,EACA,yBAAyB,OAAO,EAAE,IAAI,SAAS,MAAM;AACnD,WAAO,GAAG,QAAQ,YAAY,SAAS,cAAc,EAAE,SAAS,MAAM,CAAC;AAAA,EACzE;AACF,CAAC;",
6
6
  "names": []
7
7
  }
@@ -3,6 +3,7 @@ import {
3
3
  reorderDictionaryEntriesCommandSchema,
4
4
  setDefaultDictionaryEntryCommandSchema
5
5
  } from "@open-mercato/core/modules/dictionaries/data/validators";
6
+ import { ensureDictionaryEntryScope } from "@open-mercato/core/modules/dictionaries/commands/factory";
6
7
  import { registerCommand } from "@open-mercato/shared/lib/commands";
7
8
  import { extractUndoPayload } from "@open-mercato/shared/lib/commands/undo";
8
9
  import { withAtomicFlush } from "@open-mercato/shared/lib/commands/flush";
@@ -23,16 +24,6 @@ const dictionaryCrudEvents = {
23
24
  ...ctx.syncOrigin ? { syncOrigin: ctx.syncOrigin } : {}
24
25
  })
25
26
  };
26
- function ensureScope(ctx, scope) {
27
- const tenantId = ctx.auth?.tenantId ?? null;
28
- if (tenantId && tenantId !== scope.tenantId) {
29
- throw new CrudHttpError(403, { error: "Forbidden" });
30
- }
31
- const organizationId = ctx.selectedOrganizationId ?? ctx.auth?.orgId ?? null;
32
- if (organizationId && organizationId !== scope.organizationId) {
33
- throw new CrudHttpError(403, { error: "Forbidden" });
34
- }
35
- }
36
27
  async function requireDictionary(em, id, scope) {
37
28
  const dictionary = await findOneWithDecryption(
38
29
  em,
@@ -51,7 +42,7 @@ const reorderDictionaryEntriesCommand = {
51
42
  async prepare(rawInput, ctx) {
52
43
  const parsed = reorderDictionaryEntriesCommandSchema.parse(rawInput);
53
44
  const scope = { tenantId: parsed.tenantId, organizationId: parsed.organizationId };
54
- ensureScope(ctx, scope);
45
+ ensureDictionaryEntryScope(ctx, scope);
55
46
  const em = ctx.container.resolve("em").fork();
56
47
  const ids = parsed.entries.map((e) => e.id);
57
48
  const existing = await findWithDecryption(
@@ -75,7 +66,7 @@ const reorderDictionaryEntriesCommand = {
75
66
  async execute(rawInput, ctx) {
76
67
  const parsed = reorderDictionaryEntriesCommandSchema.parse(rawInput);
77
68
  const scope = { tenantId: parsed.tenantId, organizationId: parsed.organizationId };
78
- ensureScope(ctx, scope);
69
+ ensureDictionaryEntryScope(ctx, scope);
79
70
  const em = ctx.container.resolve("em").fork();
80
71
  const dictionary = await requireDictionary(em, parsed.dictionaryId, scope);
81
72
  const ids = parsed.entries.map((e) => e.id);
@@ -164,7 +155,7 @@ const reorderDictionaryEntriesCommand = {
164
155
  const undo = extractUndoPayload(logEntry);
165
156
  const before = undo?.before ?? logEntry?.snapshotBefore ?? null;
166
157
  if (!before) return;
167
- ensureScope(ctx, before.scope);
158
+ ensureDictionaryEntryScope(ctx, before.scope);
168
159
  const em = ctx.container.resolve("em").fork();
169
160
  const ids = before.positions.map((p) => p.id);
170
161
  const entries = await findWithDecryption(
@@ -209,7 +200,7 @@ const setDefaultDictionaryEntryCommand = {
209
200
  async prepare(rawInput, ctx) {
210
201
  const parsed = setDefaultDictionaryEntryCommandSchema.parse(rawInput);
211
202
  const scope = { tenantId: parsed.tenantId, organizationId: parsed.organizationId };
212
- ensureScope(ctx, scope);
203
+ ensureDictionaryEntryScope(ctx, scope);
213
204
  const em = ctx.container.resolve("em").fork();
214
205
  const dictionary = await requireDictionary(em, parsed.dictionaryId, scope);
215
206
  const previousDefaults = await findWithDecryption(
@@ -235,7 +226,7 @@ const setDefaultDictionaryEntryCommand = {
235
226
  async execute(rawInput, ctx) {
236
227
  const parsed = setDefaultDictionaryEntryCommandSchema.parse(rawInput);
237
228
  const scope = { tenantId: parsed.tenantId, organizationId: parsed.organizationId };
238
- ensureScope(ctx, scope);
229
+ ensureDictionaryEntryScope(ctx, scope);
239
230
  const em = ctx.container.resolve("em").fork();
240
231
  const dictionary = await requireDictionary(em, parsed.dictionaryId, scope);
241
232
  const targetEntry = await findOneWithDecryption(
@@ -330,7 +321,7 @@ const setDefaultDictionaryEntryCommand = {
330
321
  const undo = extractUndoPayload(logEntry);
331
322
  const before = undo?.before ?? logEntry?.snapshotBefore ?? null;
332
323
  if (!before) return;
333
- ensureScope(ctx, before.scope);
324
+ ensureDictionaryEntryScope(ctx, before.scope);
334
325
  const em = ctx.container.resolve("em").fork();
335
326
  const dictionary = await requireDictionary(em, before.dictionaryId, before.scope);
336
327
  const newDefault = before.newDefaultId ? await findOneWithDecryption(
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "version": 3,
3
3
  "sources": ["../../../../src/modules/dictionaries/commands/entry-operations.ts"],
4
- "sourcesContent": ["import type { EntityManager, FilterQuery } from '@mikro-orm/postgresql'\nimport { Dictionary, DictionaryEntry } from '@open-mercato/core/modules/dictionaries/data/entities'\nimport {\n reorderDictionaryEntriesCommandSchema,\n setDefaultDictionaryEntryCommandSchema,\n type ReorderDictionaryEntriesCommandInput,\n type SetDefaultDictionaryEntryCommandInput,\n} from '@open-mercato/core/modules/dictionaries/data/validators'\nimport { registerCommand, type CommandHandler, type CommandRuntimeContext } from '@open-mercato/shared/lib/commands'\nimport { extractUndoPayload } from '@open-mercato/shared/lib/commands/undo'\nimport { withAtomicFlush } from '@open-mercato/shared/lib/commands/flush'\nimport { emitCrudSideEffects, emitCrudUndoSideEffects } from '@open-mercato/shared/lib/commands/helpers'\nimport type { CrudEventsConfig } from '@open-mercato/shared/lib/crud/types'\nimport { CrudHttpError } from '@open-mercato/shared/lib/crud/errors'\nimport type { DataEngine } from '@open-mercato/shared/lib/data/engine'\nimport { findOneWithDecryption, findWithDecryption } from '@open-mercato/shared/lib/encryption/find'\nimport { resolveTranslations } from '@open-mercato/shared/lib/i18n/server'\n\nconst RESOURCE_KIND = 'dictionaries.dictionary'\nconst INDEXER_ENTITY_TYPE = 'dictionaries:entry'\n\ntype DictionaryScope = {\n tenantId: string\n organizationId: string\n}\n\nconst dictionaryCrudEvents: CrudEventsConfig = {\n module: 'dictionaries',\n entity: 'entry',\n persistent: true,\n buildPayload: (ctx) => ({\n id: ctx.identifiers.id,\n organizationId: ctx.identifiers.organizationId,\n tenantId: ctx.identifiers.tenantId,\n ...(ctx.syncOrigin ? { syncOrigin: ctx.syncOrigin } : {}),\n }),\n}\n\nfunction ensureScope(ctx: CommandRuntimeContext, scope: DictionaryScope): void {\n const tenantId = ctx.auth?.tenantId ?? null\n if (tenantId && tenantId !== scope.tenantId) {\n throw new CrudHttpError(403, { error: 'Forbidden' })\n }\n const organizationId = ctx.selectedOrganizationId ?? ctx.auth?.orgId ?? null\n if (organizationId && organizationId !== scope.organizationId) {\n throw new CrudHttpError(403, { error: 'Forbidden' })\n }\n}\n\nasync function requireDictionary(\n em: EntityManager,\n id: string,\n scope: DictionaryScope,\n): Promise<Dictionary> {\n const dictionary = await findOneWithDecryption(\n em,\n Dictionary,\n { id, tenantId: scope.tenantId, organizationId: scope.organizationId, deletedAt: null },\n undefined,\n scope,\n )\n if (!dictionary) {\n throw new CrudHttpError(404, { error: 'Dictionary not found' })\n }\n return dictionary\n}\n\ntype ReorderSnapshot = {\n dictionaryId: string\n scope: DictionaryScope\n positions: Array<{ id: string; position: number }>\n}\n\ntype ReorderUndoPayload = {\n before?: ReorderSnapshot | null\n after?: ReorderSnapshot | null\n}\n\nconst reorderDictionaryEntriesCommand: CommandHandler<\n ReorderDictionaryEntriesCommandInput,\n { dictionaryId: string; updatedIds: string[] }\n> = {\n id: 'dictionaries.entries.reorder',\n async prepare(rawInput, ctx) {\n const parsed = reorderDictionaryEntriesCommandSchema.parse(rawInput)\n const scope: DictionaryScope = { tenantId: parsed.tenantId, organizationId: parsed.organizationId }\n ensureScope(ctx, scope)\n const em = (ctx.container.resolve('em') as EntityManager).fork()\n const ids = parsed.entries.map((e) => e.id)\n const existing = await findWithDecryption(\n em,\n DictionaryEntry,\n {\n id: { $in: ids },\n tenantId: scope.tenantId,\n organizationId: scope.organizationId,\n } as FilterQuery<DictionaryEntry>,\n undefined,\n scope,\n )\n const before: ReorderSnapshot = {\n dictionaryId: parsed.dictionaryId,\n scope,\n positions: existing.map((entry) => ({ id: entry.id, position: entry.position ?? 0 })),\n }\n return { before }\n },\n async execute(rawInput, ctx) {\n const parsed = reorderDictionaryEntriesCommandSchema.parse(rawInput)\n const scope: DictionaryScope = { tenantId: parsed.tenantId, organizationId: parsed.organizationId }\n ensureScope(ctx, scope)\n\n const em = (ctx.container.resolve('em') as EntityManager).fork()\n const dictionary = await requireDictionary(em, parsed.dictionaryId, scope)\n const ids = parsed.entries.map((e) => e.id)\n const entries = await findWithDecryption(\n em,\n DictionaryEntry,\n {\n id: { $in: ids },\n dictionary,\n tenantId: scope.tenantId,\n organizationId: scope.organizationId,\n } as FilterQuery<DictionaryEntry>,\n undefined,\n scope,\n )\n const entryMap = new Map<string, DictionaryEntry>()\n for (const entry of entries) entryMap.set(entry.id, entry)\n\n const updatedIds: string[] = []\n await withAtomicFlush(em, [\n () => {\n for (const { id, position } of parsed.entries) {\n const entry = entryMap.get(id)\n if (!entry) continue\n entry.position = position\n entry.updatedAt = new Date()\n updatedIds.push(id)\n }\n },\n ], { transaction: true })\n\n const dataEngine = ctx.container.resolve('dataEngine') as DataEngine\n for (const entry of entries) {\n await emitCrudSideEffects({\n dataEngine,\n action: 'updated',\n entity: entry,\n identifiers: { id: entry.id, tenantId: entry.tenantId, organizationId: entry.organizationId },\n syncOrigin: ctx.syncOrigin,\n events: dictionaryCrudEvents,\n indexer: { entityType: INDEXER_ENTITY_TYPE },\n })\n }\n\n return { dictionaryId: parsed.dictionaryId, updatedIds }\n },\n captureAfter: async (rawInput, _result, ctx) => {\n const parsed = reorderDictionaryEntriesCommandSchema.parse(rawInput)\n const em = ctx.container.resolve('em') as EntityManager\n const scope: DictionaryScope = { tenantId: parsed.tenantId, organizationId: parsed.organizationId }\n const ids = parsed.entries.map((e) => e.id)\n const current = await findWithDecryption(\n em,\n DictionaryEntry,\n {\n id: { $in: ids },\n tenantId: scope.tenantId,\n organizationId: scope.organizationId,\n } as FilterQuery<DictionaryEntry>,\n undefined,\n scope,\n )\n const after: ReorderSnapshot = {\n dictionaryId: parsed.dictionaryId,\n scope,\n positions: current.map((entry) => ({ id: entry.id, position: entry.position ?? 0 })),\n }\n return after\n },\n buildLog: async ({ snapshots, result }) => {\n const { translate } = await resolveTranslations()\n const before = snapshots.before as ReorderSnapshot | undefined\n const after = snapshots.after as ReorderSnapshot | undefined\n if (!before || !after) return null\n return {\n actionLabel: translate('dictionaries.entries.audit.reorder', 'Reorder dictionary entries'),\n resourceKind: RESOURCE_KIND,\n resourceId: result.dictionaryId,\n tenantId: before.scope.tenantId,\n organizationId: before.scope.organizationId,\n snapshotBefore: before,\n snapshotAfter: after,\n payload: {\n undo: { before, after } satisfies ReorderUndoPayload,\n },\n }\n },\n undo: async ({ logEntry, ctx }) => {\n const undo = extractUndoPayload<ReorderUndoPayload>(logEntry)\n const before = undo?.before ?? (logEntry?.snapshotBefore as ReorderSnapshot | null | undefined) ?? null\n if (!before) return\n ensureScope(ctx, before.scope)\n\n const em = (ctx.container.resolve('em') as EntityManager).fork()\n const ids = before.positions.map((p) => p.id)\n const entries = await findWithDecryption(\n em,\n DictionaryEntry,\n {\n id: { $in: ids },\n tenantId: before.scope.tenantId,\n organizationId: before.scope.organizationId,\n } as FilterQuery<DictionaryEntry>,\n undefined,\n before.scope,\n )\n const entryMap = new Map<string, DictionaryEntry>()\n for (const entry of entries) entryMap.set(entry.id, entry)\n\n await withAtomicFlush(em, [\n () => {\n for (const { id, position } of before.positions) {\n const entry = entryMap.get(id)\n if (!entry) continue\n entry.position = position\n entry.updatedAt = new Date()\n }\n },\n ], { transaction: true })\n\n const dataEngine = ctx.container.resolve('dataEngine') as DataEngine\n for (const entry of entries) {\n await emitCrudUndoSideEffects({\n dataEngine,\n action: 'updated',\n entity: entry,\n identifiers: { id: entry.id, tenantId: entry.tenantId, organizationId: entry.organizationId },\n syncOrigin: ctx.syncOrigin,\n events: dictionaryCrudEvents,\n indexer: { entityType: INDEXER_ENTITY_TYPE },\n })\n }\n },\n}\n\ntype DefaultSnapshot = {\n dictionaryId: string\n scope: DictionaryScope\n previousDefaultIds: string[]\n newDefaultId: string\n}\n\ntype DefaultUndoPayload = {\n before?: DefaultSnapshot | null\n after?: DefaultSnapshot | null\n}\n\nconst setDefaultDictionaryEntryCommand: CommandHandler<\n SetDefaultDictionaryEntryCommandInput,\n { dictionaryId: string; entryId: string; clearedIds: string[] }\n> = {\n id: 'dictionaries.entries.set_default',\n async prepare(rawInput, ctx) {\n const parsed = setDefaultDictionaryEntryCommandSchema.parse(rawInput)\n const scope: DictionaryScope = { tenantId: parsed.tenantId, organizationId: parsed.organizationId }\n ensureScope(ctx, scope)\n const em = (ctx.container.resolve('em') as EntityManager).fork()\n const dictionary = await requireDictionary(em, parsed.dictionaryId, scope)\n const previousDefaults = await findWithDecryption(\n em,\n DictionaryEntry,\n {\n dictionary,\n tenantId: scope.tenantId,\n organizationId: scope.organizationId,\n isDefault: true,\n } as FilterQuery<DictionaryEntry>,\n undefined,\n scope,\n )\n const before: DefaultSnapshot = {\n dictionaryId: parsed.dictionaryId,\n scope,\n previousDefaultIds: previousDefaults.map((entry) => entry.id),\n newDefaultId: parsed.entryId,\n }\n return { before }\n },\n async execute(rawInput, ctx) {\n const parsed = setDefaultDictionaryEntryCommandSchema.parse(rawInput)\n const scope: DictionaryScope = { tenantId: parsed.tenantId, organizationId: parsed.organizationId }\n ensureScope(ctx, scope)\n\n const em = (ctx.container.resolve('em') as EntityManager).fork()\n const dictionary = await requireDictionary(em, parsed.dictionaryId, scope)\n const targetEntry = await findOneWithDecryption(\n em,\n DictionaryEntry,\n {\n id: parsed.entryId,\n dictionary,\n tenantId: scope.tenantId,\n organizationId: scope.organizationId,\n } as FilterQuery<DictionaryEntry>,\n undefined,\n scope,\n )\n if (!targetEntry) {\n throw new CrudHttpError(404, { error: 'Dictionary entry not found' })\n }\n\n const existingDefaults = await findWithDecryption(\n em,\n DictionaryEntry,\n {\n dictionary,\n tenantId: scope.tenantId,\n organizationId: scope.organizationId,\n isDefault: true,\n } as FilterQuery<DictionaryEntry>,\n undefined,\n scope,\n )\n const clearedIds: string[] = []\n\n // The partial unique index `dictionary_entries_one_default_per_dict`\n // (dictionary_id, organization_id, tenant_id) WHERE is_default = true forbids\n // two default entries in the same dictionary. PostgreSQL checks partial unique\n // indexes per-statement (no deferral) and MikroORM does not guarantee that the\n // is_default=false UPDATEs run before the is_default=true UPDATE within a single\n // flush, so clearing and setting in one flush races to a 23505. Clear the prior\n // default(s) in their own flush first, then set the new default in a second\n // flush, so Postgres never observes two default rows at once.\n await withAtomicFlush(em, [\n () => {\n for (const entry of existingDefaults) {\n if (entry.id === targetEntry.id) continue\n entry.isDefault = false\n entry.updatedAt = new Date()\n clearedIds.push(entry.id)\n }\n },\n ], { transaction: true })\n await withAtomicFlush(em, [\n () => {\n targetEntry.isDefault = true\n targetEntry.updatedAt = new Date()\n },\n ], { transaction: true })\n\n const dataEngine = ctx.container.resolve('dataEngine') as DataEngine\n const touched = [targetEntry, ...existingDefaults.filter((entry) => entry.id !== targetEntry.id)]\n for (const entry of touched) {\n await emitCrudSideEffects({\n dataEngine,\n action: 'updated',\n entity: entry,\n identifiers: { id: entry.id, tenantId: entry.tenantId, organizationId: entry.organizationId },\n syncOrigin: ctx.syncOrigin,\n events: dictionaryCrudEvents,\n indexer: { entityType: INDEXER_ENTITY_TYPE },\n })\n }\n\n return { dictionaryId: parsed.dictionaryId, entryId: targetEntry.id, clearedIds }\n },\n captureAfter: async (rawInput, result, _ctx) => {\n const parsed = setDefaultDictionaryEntryCommandSchema.parse(rawInput)\n const scope: DictionaryScope = { tenantId: parsed.tenantId, organizationId: parsed.organizationId }\n const after: DefaultSnapshot = {\n dictionaryId: parsed.dictionaryId,\n scope,\n previousDefaultIds: result.clearedIds,\n newDefaultId: result.entryId,\n }\n return after\n },\n buildLog: async ({ snapshots, result }) => {\n const { translate } = await resolveTranslations()\n const before = snapshots.before as DefaultSnapshot | undefined\n const after = snapshots.after as DefaultSnapshot | undefined\n if (!before || !after) return null\n return {\n actionLabel: translate('dictionaries.entries.audit.set_default', 'Set default dictionary entry'),\n resourceKind: RESOURCE_KIND,\n resourceId: result.dictionaryId,\n tenantId: before.scope.tenantId,\n organizationId: before.scope.organizationId,\n snapshotBefore: before,\n snapshotAfter: after,\n payload: {\n undo: { before, after } satisfies DefaultUndoPayload,\n },\n }\n },\n undo: async ({ logEntry, ctx }) => {\n const undo = extractUndoPayload<DefaultUndoPayload>(logEntry)\n const before = undo?.before ?? (logEntry?.snapshotBefore as DefaultSnapshot | null | undefined) ?? null\n if (!before) return\n ensureScope(ctx, before.scope)\n\n const em = (ctx.container.resolve('em') as EntityManager).fork()\n const dictionary = await requireDictionary(em, before.dictionaryId, before.scope)\n\n const newDefault = before.newDefaultId\n ? await findOneWithDecryption(\n em,\n DictionaryEntry,\n {\n id: before.newDefaultId,\n dictionary,\n tenantId: before.scope.tenantId,\n organizationId: before.scope.organizationId,\n } as FilterQuery<DictionaryEntry>,\n undefined,\n before.scope,\n )\n : null\n const previousDefaults = before.previousDefaultIds.length > 0\n ? await findWithDecryption(\n em,\n DictionaryEntry,\n {\n id: { $in: before.previousDefaultIds },\n dictionary,\n tenantId: before.scope.tenantId,\n organizationId: before.scope.organizationId,\n } as FilterQuery<DictionaryEntry>,\n undefined,\n before.scope,\n )\n : []\n\n // Same partial-unique-index ordering constraint as `execute`: clear the\n // current default in its own flush before restoring the previous default(s),\n // so Postgres never sees two is_default=true rows in the dictionary at once.\n await withAtomicFlush(em, [\n () => {\n if (newDefault) {\n newDefault.isDefault = false\n newDefault.updatedAt = new Date()\n }\n },\n ], { transaction: true })\n await withAtomicFlush(em, [\n () => {\n for (const entry of previousDefaults) {\n entry.isDefault = true\n entry.updatedAt = new Date()\n }\n },\n ], { transaction: true })\n\n const dataEngine = ctx.container.resolve('dataEngine') as DataEngine\n const touched = [\n ...(newDefault ? [newDefault] : []),\n ...previousDefaults,\n ]\n for (const entry of touched) {\n await emitCrudUndoSideEffects({\n dataEngine,\n action: 'updated',\n entity: entry,\n identifiers: { id: entry.id, tenantId: entry.tenantId, organizationId: entry.organizationId },\n syncOrigin: ctx.syncOrigin,\n events: dictionaryCrudEvents,\n indexer: { entityType: INDEXER_ENTITY_TYPE },\n })\n }\n },\n}\n\nregisterCommand(reorderDictionaryEntriesCommand)\nregisterCommand(setDefaultDictionaryEntryCommand)\n"],
5
- "mappings": "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",
4
+ "sourcesContent": ["import type { EntityManager, FilterQuery } from '@mikro-orm/postgresql'\nimport { Dictionary, DictionaryEntry } from '@open-mercato/core/modules/dictionaries/data/entities'\nimport {\n reorderDictionaryEntriesCommandSchema,\n setDefaultDictionaryEntryCommandSchema,\n type ReorderDictionaryEntriesCommandInput,\n type SetDefaultDictionaryEntryCommandInput,\n} from '@open-mercato/core/modules/dictionaries/data/validators'\nimport { ensureDictionaryEntryScope } from '@open-mercato/core/modules/dictionaries/commands/factory'\nimport { registerCommand, type CommandHandler } from '@open-mercato/shared/lib/commands'\nimport { extractUndoPayload } from '@open-mercato/shared/lib/commands/undo'\nimport { withAtomicFlush } from '@open-mercato/shared/lib/commands/flush'\nimport { emitCrudSideEffects, emitCrudUndoSideEffects } from '@open-mercato/shared/lib/commands/helpers'\nimport type { CrudEventsConfig } from '@open-mercato/shared/lib/crud/types'\nimport { CrudHttpError } from '@open-mercato/shared/lib/crud/errors'\nimport type { DataEngine } from '@open-mercato/shared/lib/data/engine'\nimport { findOneWithDecryption, findWithDecryption } from '@open-mercato/shared/lib/encryption/find'\nimport { resolveTranslations } from '@open-mercato/shared/lib/i18n/server'\n\nconst RESOURCE_KIND = 'dictionaries.dictionary'\nconst INDEXER_ENTITY_TYPE = 'dictionaries:entry'\n\ntype DictionaryScope = {\n tenantId: string\n organizationId: string\n}\n\nconst dictionaryCrudEvents: CrudEventsConfig = {\n module: 'dictionaries',\n entity: 'entry',\n persistent: true,\n buildPayload: (ctx) => ({\n id: ctx.identifiers.id,\n organizationId: ctx.identifiers.organizationId,\n tenantId: ctx.identifiers.tenantId,\n ...(ctx.syncOrigin ? { syncOrigin: ctx.syncOrigin } : {}),\n }),\n}\n\nasync function requireDictionary(\n em: EntityManager,\n id: string,\n scope: DictionaryScope,\n): Promise<Dictionary> {\n const dictionary = await findOneWithDecryption(\n em,\n Dictionary,\n { id, tenantId: scope.tenantId, organizationId: scope.organizationId, deletedAt: null },\n undefined,\n scope,\n )\n if (!dictionary) {\n throw new CrudHttpError(404, { error: 'Dictionary not found' })\n }\n return dictionary\n}\n\ntype ReorderSnapshot = {\n dictionaryId: string\n scope: DictionaryScope\n positions: Array<{ id: string; position: number }>\n}\n\ntype ReorderUndoPayload = {\n before?: ReorderSnapshot | null\n after?: ReorderSnapshot | null\n}\n\nconst reorderDictionaryEntriesCommand: CommandHandler<\n ReorderDictionaryEntriesCommandInput,\n { dictionaryId: string; updatedIds: string[] }\n> = {\n id: 'dictionaries.entries.reorder',\n async prepare(rawInput, ctx) {\n const parsed = reorderDictionaryEntriesCommandSchema.parse(rawInput)\n const scope: DictionaryScope = { tenantId: parsed.tenantId, organizationId: parsed.organizationId }\n ensureDictionaryEntryScope(ctx, scope)\n const em = (ctx.container.resolve('em') as EntityManager).fork()\n const ids = parsed.entries.map((e) => e.id)\n const existing = await findWithDecryption(\n em,\n DictionaryEntry,\n {\n id: { $in: ids },\n tenantId: scope.tenantId,\n organizationId: scope.organizationId,\n } as FilterQuery<DictionaryEntry>,\n undefined,\n scope,\n )\n const before: ReorderSnapshot = {\n dictionaryId: parsed.dictionaryId,\n scope,\n positions: existing.map((entry) => ({ id: entry.id, position: entry.position ?? 0 })),\n }\n return { before }\n },\n async execute(rawInput, ctx) {\n const parsed = reorderDictionaryEntriesCommandSchema.parse(rawInput)\n const scope: DictionaryScope = { tenantId: parsed.tenantId, organizationId: parsed.organizationId }\n ensureDictionaryEntryScope(ctx, scope)\n\n const em = (ctx.container.resolve('em') as EntityManager).fork()\n const dictionary = await requireDictionary(em, parsed.dictionaryId, scope)\n const ids = parsed.entries.map((e) => e.id)\n const entries = await findWithDecryption(\n em,\n DictionaryEntry,\n {\n id: { $in: ids },\n dictionary,\n tenantId: scope.tenantId,\n organizationId: scope.organizationId,\n } as FilterQuery<DictionaryEntry>,\n undefined,\n scope,\n )\n const entryMap = new Map<string, DictionaryEntry>()\n for (const entry of entries) entryMap.set(entry.id, entry)\n\n const updatedIds: string[] = []\n await withAtomicFlush(em, [\n () => {\n for (const { id, position } of parsed.entries) {\n const entry = entryMap.get(id)\n if (!entry) continue\n entry.position = position\n entry.updatedAt = new Date()\n updatedIds.push(id)\n }\n },\n ], { transaction: true })\n\n const dataEngine = ctx.container.resolve('dataEngine') as DataEngine\n for (const entry of entries) {\n await emitCrudSideEffects({\n dataEngine,\n action: 'updated',\n entity: entry,\n identifiers: { id: entry.id, tenantId: entry.tenantId, organizationId: entry.organizationId },\n syncOrigin: ctx.syncOrigin,\n events: dictionaryCrudEvents,\n indexer: { entityType: INDEXER_ENTITY_TYPE },\n })\n }\n\n return { dictionaryId: parsed.dictionaryId, updatedIds }\n },\n captureAfter: async (rawInput, _result, ctx) => {\n const parsed = reorderDictionaryEntriesCommandSchema.parse(rawInput)\n const em = ctx.container.resolve('em') as EntityManager\n const scope: DictionaryScope = { tenantId: parsed.tenantId, organizationId: parsed.organizationId }\n const ids = parsed.entries.map((e) => e.id)\n const current = await findWithDecryption(\n em,\n DictionaryEntry,\n {\n id: { $in: ids },\n tenantId: scope.tenantId,\n organizationId: scope.organizationId,\n } as FilterQuery<DictionaryEntry>,\n undefined,\n scope,\n )\n const after: ReorderSnapshot = {\n dictionaryId: parsed.dictionaryId,\n scope,\n positions: current.map((entry) => ({ id: entry.id, position: entry.position ?? 0 })),\n }\n return after\n },\n buildLog: async ({ snapshots, result }) => {\n const { translate } = await resolveTranslations()\n const before = snapshots.before as ReorderSnapshot | undefined\n const after = snapshots.after as ReorderSnapshot | undefined\n if (!before || !after) return null\n return {\n actionLabel: translate('dictionaries.entries.audit.reorder', 'Reorder dictionary entries'),\n resourceKind: RESOURCE_KIND,\n resourceId: result.dictionaryId,\n tenantId: before.scope.tenantId,\n organizationId: before.scope.organizationId,\n snapshotBefore: before,\n snapshotAfter: after,\n payload: {\n undo: { before, after } satisfies ReorderUndoPayload,\n },\n }\n },\n undo: async ({ logEntry, ctx }) => {\n const undo = extractUndoPayload<ReorderUndoPayload>(logEntry)\n const before = undo?.before ?? (logEntry?.snapshotBefore as ReorderSnapshot | null | undefined) ?? null\n if (!before) return\n ensureDictionaryEntryScope(ctx, before.scope)\n\n const em = (ctx.container.resolve('em') as EntityManager).fork()\n const ids = before.positions.map((p) => p.id)\n const entries = await findWithDecryption(\n em,\n DictionaryEntry,\n {\n id: { $in: ids },\n tenantId: before.scope.tenantId,\n organizationId: before.scope.organizationId,\n } as FilterQuery<DictionaryEntry>,\n undefined,\n before.scope,\n )\n const entryMap = new Map<string, DictionaryEntry>()\n for (const entry of entries) entryMap.set(entry.id, entry)\n\n await withAtomicFlush(em, [\n () => {\n for (const { id, position } of before.positions) {\n const entry = entryMap.get(id)\n if (!entry) continue\n entry.position = position\n entry.updatedAt = new Date()\n }\n },\n ], { transaction: true })\n\n const dataEngine = ctx.container.resolve('dataEngine') as DataEngine\n for (const entry of entries) {\n await emitCrudUndoSideEffects({\n dataEngine,\n action: 'updated',\n entity: entry,\n identifiers: { id: entry.id, tenantId: entry.tenantId, organizationId: entry.organizationId },\n syncOrigin: ctx.syncOrigin,\n events: dictionaryCrudEvents,\n indexer: { entityType: INDEXER_ENTITY_TYPE },\n })\n }\n },\n}\n\ntype DefaultSnapshot = {\n dictionaryId: string\n scope: DictionaryScope\n previousDefaultIds: string[]\n newDefaultId: string\n}\n\ntype DefaultUndoPayload = {\n before?: DefaultSnapshot | null\n after?: DefaultSnapshot | null\n}\n\nconst setDefaultDictionaryEntryCommand: CommandHandler<\n SetDefaultDictionaryEntryCommandInput,\n { dictionaryId: string; entryId: string; clearedIds: string[] }\n> = {\n id: 'dictionaries.entries.set_default',\n async prepare(rawInput, ctx) {\n const parsed = setDefaultDictionaryEntryCommandSchema.parse(rawInput)\n const scope: DictionaryScope = { tenantId: parsed.tenantId, organizationId: parsed.organizationId }\n ensureDictionaryEntryScope(ctx, scope)\n const em = (ctx.container.resolve('em') as EntityManager).fork()\n const dictionary = await requireDictionary(em, parsed.dictionaryId, scope)\n const previousDefaults = await findWithDecryption(\n em,\n DictionaryEntry,\n {\n dictionary,\n tenantId: scope.tenantId,\n organizationId: scope.organizationId,\n isDefault: true,\n } as FilterQuery<DictionaryEntry>,\n undefined,\n scope,\n )\n const before: DefaultSnapshot = {\n dictionaryId: parsed.dictionaryId,\n scope,\n previousDefaultIds: previousDefaults.map((entry) => entry.id),\n newDefaultId: parsed.entryId,\n }\n return { before }\n },\n async execute(rawInput, ctx) {\n const parsed = setDefaultDictionaryEntryCommandSchema.parse(rawInput)\n const scope: DictionaryScope = { tenantId: parsed.tenantId, organizationId: parsed.organizationId }\n ensureDictionaryEntryScope(ctx, scope)\n\n const em = (ctx.container.resolve('em') as EntityManager).fork()\n const dictionary = await requireDictionary(em, parsed.dictionaryId, scope)\n const targetEntry = await findOneWithDecryption(\n em,\n DictionaryEntry,\n {\n id: parsed.entryId,\n dictionary,\n tenantId: scope.tenantId,\n organizationId: scope.organizationId,\n } as FilterQuery<DictionaryEntry>,\n undefined,\n scope,\n )\n if (!targetEntry) {\n throw new CrudHttpError(404, { error: 'Dictionary entry not found' })\n }\n\n const existingDefaults = await findWithDecryption(\n em,\n DictionaryEntry,\n {\n dictionary,\n tenantId: scope.tenantId,\n organizationId: scope.organizationId,\n isDefault: true,\n } as FilterQuery<DictionaryEntry>,\n undefined,\n scope,\n )\n const clearedIds: string[] = []\n\n // The partial unique index `dictionary_entries_one_default_per_dict`\n // (dictionary_id, organization_id, tenant_id) WHERE is_default = true forbids\n // two default entries in the same dictionary. PostgreSQL checks partial unique\n // indexes per-statement (no deferral) and MikroORM does not guarantee that the\n // is_default=false UPDATEs run before the is_default=true UPDATE within a single\n // flush, so clearing and setting in one flush races to a 23505. Clear the prior\n // default(s) in their own flush first, then set the new default in a second\n // flush, so Postgres never observes two default rows at once.\n await withAtomicFlush(em, [\n () => {\n for (const entry of existingDefaults) {\n if (entry.id === targetEntry.id) continue\n entry.isDefault = false\n entry.updatedAt = new Date()\n clearedIds.push(entry.id)\n }\n },\n ], { transaction: true })\n await withAtomicFlush(em, [\n () => {\n targetEntry.isDefault = true\n targetEntry.updatedAt = new Date()\n },\n ], { transaction: true })\n\n const dataEngine = ctx.container.resolve('dataEngine') as DataEngine\n const touched = [targetEntry, ...existingDefaults.filter((entry) => entry.id !== targetEntry.id)]\n for (const entry of touched) {\n await emitCrudSideEffects({\n dataEngine,\n action: 'updated',\n entity: entry,\n identifiers: { id: entry.id, tenantId: entry.tenantId, organizationId: entry.organizationId },\n syncOrigin: ctx.syncOrigin,\n events: dictionaryCrudEvents,\n indexer: { entityType: INDEXER_ENTITY_TYPE },\n })\n }\n\n return { dictionaryId: parsed.dictionaryId, entryId: targetEntry.id, clearedIds }\n },\n captureAfter: async (rawInput, result, _ctx) => {\n const parsed = setDefaultDictionaryEntryCommandSchema.parse(rawInput)\n const scope: DictionaryScope = { tenantId: parsed.tenantId, organizationId: parsed.organizationId }\n const after: DefaultSnapshot = {\n dictionaryId: parsed.dictionaryId,\n scope,\n previousDefaultIds: result.clearedIds,\n newDefaultId: result.entryId,\n }\n return after\n },\n buildLog: async ({ snapshots, result }) => {\n const { translate } = await resolveTranslations()\n const before = snapshots.before as DefaultSnapshot | undefined\n const after = snapshots.after as DefaultSnapshot | undefined\n if (!before || !after) return null\n return {\n actionLabel: translate('dictionaries.entries.audit.set_default', 'Set default dictionary entry'),\n resourceKind: RESOURCE_KIND,\n resourceId: result.dictionaryId,\n tenantId: before.scope.tenantId,\n organizationId: before.scope.organizationId,\n snapshotBefore: before,\n snapshotAfter: after,\n payload: {\n undo: { before, after } satisfies DefaultUndoPayload,\n },\n }\n },\n undo: async ({ logEntry, ctx }) => {\n const undo = extractUndoPayload<DefaultUndoPayload>(logEntry)\n const before = undo?.before ?? (logEntry?.snapshotBefore as DefaultSnapshot | null | undefined) ?? null\n if (!before) return\n ensureDictionaryEntryScope(ctx, before.scope)\n\n const em = (ctx.container.resolve('em') as EntityManager).fork()\n const dictionary = await requireDictionary(em, before.dictionaryId, before.scope)\n\n const newDefault = before.newDefaultId\n ? await findOneWithDecryption(\n em,\n DictionaryEntry,\n {\n id: before.newDefaultId,\n dictionary,\n tenantId: before.scope.tenantId,\n organizationId: before.scope.organizationId,\n } as FilterQuery<DictionaryEntry>,\n undefined,\n before.scope,\n )\n : null\n const previousDefaults = before.previousDefaultIds.length > 0\n ? await findWithDecryption(\n em,\n DictionaryEntry,\n {\n id: { $in: before.previousDefaultIds },\n dictionary,\n tenantId: before.scope.tenantId,\n organizationId: before.scope.organizationId,\n } as FilterQuery<DictionaryEntry>,\n undefined,\n before.scope,\n )\n : []\n\n // Same partial-unique-index ordering constraint as `execute`: clear the\n // current default in its own flush before restoring the previous default(s),\n // so Postgres never sees two is_default=true rows in the dictionary at once.\n await withAtomicFlush(em, [\n () => {\n if (newDefault) {\n newDefault.isDefault = false\n newDefault.updatedAt = new Date()\n }\n },\n ], { transaction: true })\n await withAtomicFlush(em, [\n () => {\n for (const entry of previousDefaults) {\n entry.isDefault = true\n entry.updatedAt = new Date()\n }\n },\n ], { transaction: true })\n\n const dataEngine = ctx.container.resolve('dataEngine') as DataEngine\n const touched = [\n ...(newDefault ? [newDefault] : []),\n ...previousDefaults,\n ]\n for (const entry of touched) {\n await emitCrudUndoSideEffects({\n dataEngine,\n action: 'updated',\n entity: entry,\n identifiers: { id: entry.id, tenantId: entry.tenantId, organizationId: entry.organizationId },\n syncOrigin: ctx.syncOrigin,\n events: dictionaryCrudEvents,\n indexer: { entityType: INDEXER_ENTITY_TYPE },\n })\n }\n },\n}\n\nregisterCommand(reorderDictionaryEntriesCommand)\nregisterCommand(setDefaultDictionaryEntryCommand)\n"],
5
+ "mappings": "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",
6
6
  "names": []
7
7
  }
@@ -9,13 +9,17 @@ import {
9
9
  } from "@open-mercato/shared/lib/commands";
10
10
  import { extractUndoPayload } from "@open-mercato/shared/lib/commands/undo";
11
11
  import { buildChanges, requireId } from "@open-mercato/shared/lib/commands/helpers";
12
+ import { ensureOrganizationScope, ensureTenantScope } from "@open-mercato/shared/lib/commands/scope";
12
13
  import { CrudHttpError } from "@open-mercato/shared/lib/crud/errors";
13
14
  import { resolveTranslations } from "@open-mercato/shared/lib/i18n/server";
14
15
  import { findOneWithDecryption } from "@open-mercato/shared/lib/encryption/find";
16
+ function ensureDictionaryEntryScope(ctx, scope) {
17
+ ensureTenantScope(ctx, scope.tenantId);
18
+ ensureOrganizationScope(ctx, scope.organizationId);
19
+ }
15
20
  const ENTRY_CHANGE_KEYS = ["value", "label", "color", "icon", "position", "isDefault"];
16
21
  function toScopeEnsurer(fn) {
17
- return typeof fn === "function" ? fn : () => {
18
- };
22
+ return typeof fn === "function" ? fn : ensureDictionaryEntryScope;
19
23
  }
20
24
  async function loadSnapshot(em, id) {
21
25
  const entry = await findOneWithDecryption(em, DictionaryEntry, id, { populate: ["dictionary"] });
@@ -365,6 +369,8 @@ function registerDictionaryEntryCommands(config) {
365
369
  registerCommand(deleteCommand);
366
370
  }
367
371
  export {
368
- registerDictionaryEntryCommands
372
+ ensureDictionaryEntryScope,
373
+ registerDictionaryEntryCommands,
374
+ toScopeEnsurer
369
375
  };
370
376
  //# sourceMappingURL=factory.js.map
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "version": 3,
3
3
  "sources": ["../../../../src/modules/dictionaries/commands/factory.ts"],
4
- "sourcesContent": ["import type { EntityManager } from '@mikro-orm/postgresql'\nimport { Dictionary, DictionaryEntry } from '@open-mercato/core/modules/dictionaries/data/entities'\nimport {\n normalizeDictionaryValue,\n sanitizeDictionaryColor,\n sanitizeDictionaryIcon,\n} from '@open-mercato/core/modules/dictionaries/lib/utils'\nimport {\n registerCommand,\n type CommandHandler,\n type CommandRuntimeContext,\n} from '@open-mercato/shared/lib/commands'\nimport { extractUndoPayload } from '@open-mercato/shared/lib/commands/undo'\nimport { buildChanges, requireId } from '@open-mercato/shared/lib/commands/helpers'\nimport { CrudHttpError } from '@open-mercato/shared/lib/crud/errors'\nimport { resolveTranslations } from '@open-mercato/shared/lib/i18n/server'\nimport type { z } from 'zod'\nimport { findOneWithDecryption } from '@open-mercato/shared/lib/encryption/find'\n\ntype DictionaryScope = {\n tenantId: string\n organizationId: string\n}\n\nexport type DictionaryEntrySnapshot = {\n id: string\n dictionaryId: string\n dictionaryKey: string\n organizationId: string\n tenantId: string\n value: string\n label: string\n color: string | null\n icon: string | null\n createdAt: string\n updatedAt: string\n position?: number\n isDefault?: boolean\n}\n\nexport type DictionaryEntryUndoPayload = {\n before?: DictionaryEntrySnapshot | null\n after?: DictionaryEntrySnapshot | null\n}\n\nconst ENTRY_CHANGE_KEYS = ['value', 'label', 'color', 'icon', 'position', 'isDefault'] as const\n\ntype ResolveDictionaryForCreate<TCreate> = (options: {\n em: EntityManager\n ctx: CommandRuntimeContext\n parsed: TCreate\n}) => Promise<{ dictionary: Dictionary; scope: DictionaryScope }>\n\ntype ResolveEntry<TInput> = (options: {\n em: EntityManager\n ctx: CommandRuntimeContext\n id: string\n parsed?: TInput\n}) => Promise<{ entry: DictionaryEntry; dictionary: Dictionary; scope: DictionaryScope }>\n\ntype EnsureDictionaryForUndo = (options: {\n em: EntityManager\n ctx: CommandRuntimeContext\n snapshot: DictionaryEntrySnapshot\n}) => Promise<Dictionary | null>\n\ntype EnsureScopeFn = (ctx: CommandRuntimeContext, scope: DictionaryScope) => void\n\ntype DictionaryEntryCommandLabels = {\n singular?: string\n create?: string\n update?: string\n delete?: string\n}\n\nexport type DictionaryEntryCommandConfig<TCreate, TUpdate> = {\n commandPrefix: string\n resourceKind: string\n translationKeyPrefix?: string\n createSchema: z.ZodType<TCreate>\n updateSchema: z.ZodType<TUpdate>\n resolveDictionaryForCreate: ResolveDictionaryForCreate<TCreate>\n resolveEntry: ResolveEntry<TUpdate>\n ensureScope?: EnsureScopeFn\n ensureDictionaryForUndo?: EnsureDictionaryForUndo\n duplicateError?: string\n labels?: DictionaryEntryCommandLabels\n}\n\nfunction toScopeEnsurer(fn: EnsureScopeFn | undefined): EnsureScopeFn {\n return typeof fn === 'function' ? fn : () => {}\n}\n\nasync function loadSnapshot(em: EntityManager, id: string): Promise<DictionaryEntrySnapshot | null> {\n const entry = await findOneWithDecryption(em, DictionaryEntry, id, { populate: ['dictionary'] })\n if (!entry) return null\n return {\n id: entry.id,\n dictionaryId: entry.dictionary.id,\n dictionaryKey: entry.dictionary.key,\n organizationId: entry.organizationId,\n tenantId: entry.tenantId,\n value: entry.value,\n label: entry.label,\n color: entry.color ?? null,\n icon: entry.icon ?? null,\n position: entry.position ?? 0,\n isDefault: entry.isDefault ?? false,\n createdAt:\n entry.createdAt instanceof Date\n ? entry.createdAt.toISOString()\n : new Date(entry.createdAt).toISOString(),\n updatedAt:\n entry.updatedAt instanceof Date\n ? entry.updatedAt.toISOString()\n : new Date(entry.updatedAt).toISOString(),\n }\n}\n\nfunction applySnapshot(entry: DictionaryEntry, snapshot: DictionaryEntrySnapshot): void {\n entry.value = snapshot.value\n entry.normalizedValue = normalizeDictionaryValue(snapshot.value)\n entry.label = snapshot.label\n entry.color = snapshot.color ?? null\n entry.icon = snapshot.icon ?? null\n if (snapshot.position !== undefined) entry.position = snapshot.position\n if (snapshot.isDefault !== undefined) entry.isDefault = snapshot.isDefault\n entry.organizationId = snapshot.organizationId\n entry.tenantId = snapshot.tenantId\n entry.createdAt = new Date(snapshot.createdAt)\n entry.updatedAt = new Date(snapshot.updatedAt)\n}\n\nfunction ensureDictionaryForUndoFactory(\n fallback?: EnsureDictionaryForUndo\n): EnsureDictionaryForUndo {\n if (fallback) return fallback\n return async ({ em, snapshot }) => {\n const dictionary = await em.findOne(Dictionary, snapshot.dictionaryId)\n return dictionary ?? null\n }\n}\n\nfunction getDuplicateError(config: DictionaryEntryCommandConfig<any, any>): string {\n return config.duplicateError ?? 'Value already exists in this dictionary.'\n}\n\nfunction sanitizeCreatePayload(input: any): {\n value: string\n normalized: string\n label: string\n color: string | null\n icon: string | null\n position: number | undefined\n} {\n const rawValue = typeof input?.value === 'string' ? input.value.trim() : ''\n if (!rawValue) {\n throw new CrudHttpError(400, { error: 'Dictionary entry value is required.' })\n }\n const normalized = normalizeDictionaryValue(rawValue)\n const rawLabel = typeof input?.label === 'string' ? input.label.trim() : ''\n const label = rawLabel || rawValue\n const color = sanitizeDictionaryColor(input?.color ?? null)\n const icon = sanitizeDictionaryIcon(input?.icon ?? null)\n const position = typeof input?.position === 'number' ? input.position : undefined\n return { value: rawValue, normalized, label, color, icon, position }\n}\n\nfunction sanitizeUpdatePayload(input: any): {\n value?: string\n normalized?: string\n label?: string\n color?: string | null\n icon?: string | null\n position?: number\n isDefault?: boolean\n} {\n const payload: {\n value?: string\n normalized?: string\n label?: string\n color?: string | null\n icon?: string | null\n position?: number\n isDefault?: boolean\n } = {}\n if (Object.prototype.hasOwnProperty.call(input, 'value')) {\n const rawValue = typeof input?.value === 'string' ? input.value.trim() : ''\n if (!rawValue) {\n throw new CrudHttpError(400, { error: 'Dictionary entry value is required.' })\n }\n payload.value = rawValue\n payload.normalized = normalizeDictionaryValue(rawValue)\n }\n if (Object.prototype.hasOwnProperty.call(input, 'label')) {\n const rawLabel = typeof input?.label === 'string' ? input.label.trim() : ''\n payload.label = rawLabel\n }\n if (Object.prototype.hasOwnProperty.call(input, 'color')) {\n payload.color = sanitizeDictionaryColor(input?.color ?? null)\n }\n if (Object.prototype.hasOwnProperty.call(input, 'icon')) {\n payload.icon = sanitizeDictionaryIcon(input?.icon ?? null)\n }\n if (Object.prototype.hasOwnProperty.call(input, 'position')) {\n const rawPosition = typeof input?.position === 'number' ? input.position : undefined\n if (rawPosition !== undefined) {\n payload.position = rawPosition\n }\n }\n if (Object.prototype.hasOwnProperty.call(input, 'isDefault')) {\n payload.isDefault = typeof input?.isDefault === 'boolean' ? input.isDefault : undefined\n }\n return payload\n}\n\nexport function registerDictionaryEntryCommands<TCreate, TUpdate>(\n config: DictionaryEntryCommandConfig<TCreate, TUpdate>\n): void {\n const scopeEnsurer = toScopeEnsurer(config.ensureScope)\n const ensureDictionaryForUndo = ensureDictionaryForUndoFactory(config.ensureDictionaryForUndo)\n const translationKeyPrefix = config.translationKeyPrefix ?? config.commandPrefix\n const fallbackSingular = config.labels?.singular ?? 'Dictionary entry'\n const fallbackCreate = config.labels?.create ?? `Create ${fallbackSingular}`\n const fallbackUpdate = config.labels?.update ?? `Update ${fallbackSingular}`\n const fallbackDelete = config.labels?.delete ?? `Delete ${fallbackSingular}`\n const duplicateMessage = getDuplicateError(config)\n\n const createCommand: CommandHandler<\n ReturnType<typeof config.createSchema['parse']>,\n { entryId: string }\n > = {\n id: `${config.commandPrefix}.create`,\n async execute(rawInput, ctx) {\n const parsed = config.createSchema.parse(rawInput)\n const em = (ctx.container.resolve('em') as EntityManager).fork()\n const { dictionary, scope } = await config.resolveDictionaryForCreate({ em, ctx, parsed })\n scopeEnsurer(ctx, scope)\n\n const { value, normalized, label, color, icon, position } = sanitizeCreatePayload(parsed)\n const duplicate = await em.findOne(DictionaryEntry, {\n dictionary,\n tenantId: scope.tenantId,\n organizationId: scope.organizationId,\n normalizedValue: normalized,\n })\n if (duplicate) {\n throw new CrudHttpError(409, { error: duplicateMessage })\n }\n\n const entry = em.create(DictionaryEntry, {\n dictionary,\n tenantId: scope.tenantId,\n organizationId: scope.organizationId,\n value,\n normalizedValue: normalized,\n label,\n color,\n icon,\n ...(position !== undefined ? { position } : {}),\n createdAt: new Date(),\n updatedAt: new Date(),\n })\n em.persist(entry)\n await em.flush()\n return { entryId: entry.id }\n },\n captureAfter: async (_input, result, ctx) => {\n const em = (ctx.container.resolve('em') as EntityManager).fork()\n return loadSnapshot(em, result.entryId)\n },\n buildLog: async ({ result, snapshots }) => {\n const after = snapshots.after as DictionaryEntrySnapshot | undefined\n if (!after) return null\n const { translate } = await resolveTranslations()\n return {\n actionLabel: translate(`${translationKeyPrefix}.audit.create`, fallbackCreate),\n resourceKind: config.resourceKind,\n resourceId: result.entryId,\n tenantId: after.tenantId,\n organizationId: after.organizationId,\n snapshotAfter: after,\n payload: {\n undo: { after } satisfies DictionaryEntryUndoPayload,\n },\n }\n },\n undo: async ({ logEntry, ctx }) => {\n const undo = extractUndoPayload<DictionaryEntryUndoPayload>(logEntry)\n const after =\n undo?.after ??\n (logEntry?.snapshotAfter as DictionaryEntrySnapshot | null | undefined) ??\n null\n if (!after) return\n const em = (ctx.container.resolve('em') as EntityManager).fork()\n scopeEnsurer(ctx, { tenantId: after.tenantId, organizationId: after.organizationId })\n const entry = await em.findOne(DictionaryEntry, after.id)\n if (entry) {\n await em.remove(entry).flush()\n return\n }\n await em.nativeDelete(DictionaryEntry, { id: after.id })\n },\n}\n\n const updateCommand: CommandHandler<\n ReturnType<typeof config.updateSchema['parse']>,\n { entryId: string }\n > = {\n id: `${config.commandPrefix}.update`,\n async prepare(input, ctx) {\n const id = requireId(input, 'Dictionary entry id is required')\n const em = (ctx.container.resolve('em') as EntityManager)\n const snapshot = await loadSnapshot(em, id)\n if (snapshot) {\n scopeEnsurer(ctx, { tenantId: snapshot.tenantId, organizationId: snapshot.organizationId })\n }\n return snapshot ? { before: snapshot } : {}\n },\n async execute(rawInput, ctx) {\n const parsed = config.updateSchema.parse(rawInput)\n const em = (ctx.container.resolve('em') as EntityManager).fork()\n const id = requireId(parsed, 'Dictionary entry id is required')\n const { entry, dictionary, scope } = await config.resolveEntry({ em, ctx, id, parsed })\n scopeEnsurer(ctx, scope)\n\n const updates = sanitizeUpdatePayload(parsed)\n if (updates.value !== undefined && updates.normalized !== undefined) {\n if (updates.normalized !== entry.normalizedValue) {\n const duplicate = await em.findOne(DictionaryEntry, {\n dictionary,\n tenantId: scope.tenantId,\n organizationId: scope.organizationId,\n normalizedValue: updates.normalized,\n id: { $ne: entry.id },\n } as any)\n if (duplicate) {\n throw new CrudHttpError(409, { error: duplicateMessage })\n }\n entry.value = updates.value\n entry.normalizedValue = updates.normalized\n if (updates.label === undefined) {\n entry.label = entry.value\n }\n }\n }\n if (updates.label !== undefined) {\n entry.label = updates.label || entry.value\n }\n if (Object.prototype.hasOwnProperty.call(updates, 'color')) {\n entry.color = updates.color ?? null\n }\n if (Object.prototype.hasOwnProperty.call(updates, 'icon')) {\n entry.icon = updates.icon ?? null\n }\n if (updates.position !== undefined) {\n entry.position = updates.position\n }\n if (updates.isDefault !== undefined) {\n entry.isDefault = updates.isDefault\n }\n await em.flush()\n return { entryId: entry.id }\n },\n captureAfter: async (_input, result, ctx) => {\n const em = (ctx.container.resolve('em') as EntityManager).fork()\n return loadSnapshot(em, result.entryId)\n },\n buildLog: async ({ snapshots }) => {\n const before = snapshots.before as DictionaryEntrySnapshot | undefined\n const after = snapshots.after as DictionaryEntrySnapshot | undefined\n if (!before || !after) return null\n const { translate } = await resolveTranslations()\n return {\n actionLabel: translate(`${translationKeyPrefix}.audit.update`, fallbackUpdate),\n resourceKind: config.resourceKind,\n resourceId: before.id,\n tenantId: before.tenantId,\n organizationId: before.organizationId,\n snapshotBefore: before,\n snapshotAfter: after,\n changes: buildChanges(\n before as Record<string, unknown>,\n after as Record<string, unknown>,\n ENTRY_CHANGE_KEYS,\n ),\n payload: {\n undo: { before, after } satisfies DictionaryEntryUndoPayload,\n },\n }\n },\n undo: async ({ logEntry, ctx }) => {\n const undo = extractUndoPayload<DictionaryEntryUndoPayload>(logEntry)\n const before =\n undo?.before ??\n (logEntry?.snapshotBefore as DictionaryEntrySnapshot | null | undefined) ??\n null\n if (!before) return\n const em = (ctx.container.resolve('em') as EntityManager).fork()\n scopeEnsurer(ctx, { tenantId: before.tenantId, organizationId: before.organizationId })\n const dictionary = await ensureDictionaryForUndo({ em, ctx, snapshot: before })\n if (!dictionary) return\n let entry = await em.findOne(DictionaryEntry, before.id)\n if (!entry) {\n entry = em.create(DictionaryEntry, {\n id: before.id,\n dictionary,\n tenantId: before.tenantId,\n organizationId: before.organizationId,\n value: before.value,\n normalizedValue: normalizeDictionaryValue(before.value),\n label: before.label,\n color: before.color,\n icon: before.icon,\n createdAt: new Date(before.createdAt),\n updatedAt: new Date(before.updatedAt),\n })\n em.persist(entry)\n } else {\n entry.dictionary = dictionary\n applySnapshot(entry, before)\n }\n await em.flush()\n },\n }\n\n const deleteCommand: CommandHandler<\n { body?: Record<string, unknown>; query?: Record<string, unknown> },\n { entryId: string }\n > = {\n id: `${config.commandPrefix}.delete`,\n async prepare(input, ctx) {\n const id = requireId(input, 'Dictionary entry id is required')\n const em = (ctx.container.resolve('em') as EntityManager)\n const snapshot = await loadSnapshot(em, id)\n if (snapshot) {\n scopeEnsurer(ctx, { tenantId: snapshot.tenantId, organizationId: snapshot.organizationId })\n }\n return snapshot ? { before: snapshot } : {}\n },\n async execute(input, ctx) {\n const id = requireId(input, 'Dictionary entry id is required')\n const em = (ctx.container.resolve('em') as EntityManager).fork()\n const { entry, scope } = await config.resolveEntry({ em, ctx, id })\n scopeEnsurer(ctx, scope)\n em.remove(entry)\n await em.flush()\n return { entryId: id }\n },\n buildLog: async ({ snapshots }) => {\n const before = snapshots.before as DictionaryEntrySnapshot | undefined\n if (!before) return null\n const { translate } = await resolveTranslations()\n return {\n actionLabel: translate(`${translationKeyPrefix}.audit.delete`, fallbackDelete),\n resourceKind: config.resourceKind,\n resourceId: before.id,\n tenantId: before.tenantId,\n organizationId: before.organizationId,\n snapshotBefore: before,\n payload: {\n undo: { before } satisfies DictionaryEntryUndoPayload,\n },\n }\n },\n undo: async ({ logEntry, ctx }) => {\n const undo = extractUndoPayload<DictionaryEntryUndoPayload>(logEntry)\n const before =\n undo?.before ??\n (logEntry?.snapshotBefore as DictionaryEntrySnapshot | null | undefined) ??\n null\n if (!before) return\n const em = (ctx.container.resolve('em') as EntityManager).fork()\n scopeEnsurer(ctx, { tenantId: before.tenantId, organizationId: before.organizationId })\n const dictionary = await ensureDictionaryForUndo({ em, ctx, snapshot: before })\n if (!dictionary) return\n let entry = await em.findOne(DictionaryEntry, before.id)\n if (!entry) {\n entry = em.create(DictionaryEntry, {\n id: before.id,\n dictionary,\n tenantId: before.tenantId,\n organizationId: before.organizationId,\n value: before.value,\n normalizedValue: normalizeDictionaryValue(before.value),\n label: before.label,\n color: before.color,\n icon: before.icon,\n createdAt: new Date(before.createdAt),\n updatedAt: new Date(before.updatedAt),\n })\n em.persist(entry)\n } else {\n entry.dictionary = dictionary\n applySnapshot(entry, before)\n }\n await em.flush()\n },\n }\n\n registerCommand(createCommand)\n registerCommand(updateCommand)\n registerCommand(deleteCommand)\n}\n"],
5
- "mappings": "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",
4
+ "sourcesContent": ["import type { EntityManager } from '@mikro-orm/postgresql'\nimport { Dictionary, DictionaryEntry } from '@open-mercato/core/modules/dictionaries/data/entities'\nimport {\n normalizeDictionaryValue,\n sanitizeDictionaryColor,\n sanitizeDictionaryIcon,\n} from '@open-mercato/core/modules/dictionaries/lib/utils'\nimport {\n registerCommand,\n type CommandHandler,\n type CommandRuntimeContext,\n} from '@open-mercato/shared/lib/commands'\nimport { extractUndoPayload } from '@open-mercato/shared/lib/commands/undo'\nimport { buildChanges, requireId } from '@open-mercato/shared/lib/commands/helpers'\nimport { ensureOrganizationScope, ensureTenantScope } from '@open-mercato/shared/lib/commands/scope'\nimport { CrudHttpError } from '@open-mercato/shared/lib/crud/errors'\nimport { resolveTranslations } from '@open-mercato/shared/lib/i18n/server'\nimport type { z } from 'zod'\nimport { findOneWithDecryption } from '@open-mercato/shared/lib/encryption/find'\n\ntype DictionaryScope = {\n tenantId: string\n organizationId: string\n}\n\nexport function ensureDictionaryEntryScope(ctx: CommandRuntimeContext, scope: DictionaryScope): void {\n ensureTenantScope(ctx, scope.tenantId)\n ensureOrganizationScope(ctx, scope.organizationId)\n}\n\nexport type DictionaryEntrySnapshot = {\n id: string\n dictionaryId: string\n dictionaryKey: string\n organizationId: string\n tenantId: string\n value: string\n label: string\n color: string | null\n icon: string | null\n createdAt: string\n updatedAt: string\n position?: number\n isDefault?: boolean\n}\n\nexport type DictionaryEntryUndoPayload = {\n before?: DictionaryEntrySnapshot | null\n after?: DictionaryEntrySnapshot | null\n}\n\nconst ENTRY_CHANGE_KEYS = ['value', 'label', 'color', 'icon', 'position', 'isDefault'] as const\n\ntype ResolveDictionaryForCreate<TCreate> = (options: {\n em: EntityManager\n ctx: CommandRuntimeContext\n parsed: TCreate\n}) => Promise<{ dictionary: Dictionary; scope: DictionaryScope }>\n\ntype ResolveEntry<TInput> = (options: {\n em: EntityManager\n ctx: CommandRuntimeContext\n id: string\n parsed?: TInput\n}) => Promise<{ entry: DictionaryEntry; dictionary: Dictionary; scope: DictionaryScope }>\n\ntype EnsureDictionaryForUndo = (options: {\n em: EntityManager\n ctx: CommandRuntimeContext\n snapshot: DictionaryEntrySnapshot\n}) => Promise<Dictionary | null>\n\ntype EnsureScopeFn = (ctx: CommandRuntimeContext, scope: DictionaryScope) => void\n\ntype DictionaryEntryCommandLabels = {\n singular?: string\n create?: string\n update?: string\n delete?: string\n}\n\nexport type DictionaryEntryCommandConfig<TCreate, TUpdate> = {\n commandPrefix: string\n resourceKind: string\n translationKeyPrefix?: string\n createSchema: z.ZodType<TCreate>\n updateSchema: z.ZodType<TUpdate>\n resolveDictionaryForCreate: ResolveDictionaryForCreate<TCreate>\n resolveEntry: ResolveEntry<TUpdate>\n ensureScope?: EnsureScopeFn\n ensureDictionaryForUndo?: EnsureDictionaryForUndo\n duplicateError?: string\n labels?: DictionaryEntryCommandLabels\n}\n\nexport function toScopeEnsurer(fn: EnsureScopeFn | undefined): EnsureScopeFn {\n return typeof fn === 'function' ? fn : ensureDictionaryEntryScope\n}\n\nasync function loadSnapshot(em: EntityManager, id: string): Promise<DictionaryEntrySnapshot | null> {\n const entry = await findOneWithDecryption(em, DictionaryEntry, id, { populate: ['dictionary'] })\n if (!entry) return null\n return {\n id: entry.id,\n dictionaryId: entry.dictionary.id,\n dictionaryKey: entry.dictionary.key,\n organizationId: entry.organizationId,\n tenantId: entry.tenantId,\n value: entry.value,\n label: entry.label,\n color: entry.color ?? null,\n icon: entry.icon ?? null,\n position: entry.position ?? 0,\n isDefault: entry.isDefault ?? false,\n createdAt:\n entry.createdAt instanceof Date\n ? entry.createdAt.toISOString()\n : new Date(entry.createdAt).toISOString(),\n updatedAt:\n entry.updatedAt instanceof Date\n ? entry.updatedAt.toISOString()\n : new Date(entry.updatedAt).toISOString(),\n }\n}\n\nfunction applySnapshot(entry: DictionaryEntry, snapshot: DictionaryEntrySnapshot): void {\n entry.value = snapshot.value\n entry.normalizedValue = normalizeDictionaryValue(snapshot.value)\n entry.label = snapshot.label\n entry.color = snapshot.color ?? null\n entry.icon = snapshot.icon ?? null\n if (snapshot.position !== undefined) entry.position = snapshot.position\n if (snapshot.isDefault !== undefined) entry.isDefault = snapshot.isDefault\n entry.organizationId = snapshot.organizationId\n entry.tenantId = snapshot.tenantId\n entry.createdAt = new Date(snapshot.createdAt)\n entry.updatedAt = new Date(snapshot.updatedAt)\n}\n\nfunction ensureDictionaryForUndoFactory(\n fallback?: EnsureDictionaryForUndo\n): EnsureDictionaryForUndo {\n if (fallback) return fallback\n return async ({ em, snapshot }) => {\n const dictionary = await em.findOne(Dictionary, snapshot.dictionaryId)\n return dictionary ?? null\n }\n}\n\nfunction getDuplicateError(config: DictionaryEntryCommandConfig<any, any>): string {\n return config.duplicateError ?? 'Value already exists in this dictionary.'\n}\n\nfunction sanitizeCreatePayload(input: any): {\n value: string\n normalized: string\n label: string\n color: string | null\n icon: string | null\n position: number | undefined\n} {\n const rawValue = typeof input?.value === 'string' ? input.value.trim() : ''\n if (!rawValue) {\n throw new CrudHttpError(400, { error: 'Dictionary entry value is required.' })\n }\n const normalized = normalizeDictionaryValue(rawValue)\n const rawLabel = typeof input?.label === 'string' ? input.label.trim() : ''\n const label = rawLabel || rawValue\n const color = sanitizeDictionaryColor(input?.color ?? null)\n const icon = sanitizeDictionaryIcon(input?.icon ?? null)\n const position = typeof input?.position === 'number' ? input.position : undefined\n return { value: rawValue, normalized, label, color, icon, position }\n}\n\nfunction sanitizeUpdatePayload(input: any): {\n value?: string\n normalized?: string\n label?: string\n color?: string | null\n icon?: string | null\n position?: number\n isDefault?: boolean\n} {\n const payload: {\n value?: string\n normalized?: string\n label?: string\n color?: string | null\n icon?: string | null\n position?: number\n isDefault?: boolean\n } = {}\n if (Object.prototype.hasOwnProperty.call(input, 'value')) {\n const rawValue = typeof input?.value === 'string' ? input.value.trim() : ''\n if (!rawValue) {\n throw new CrudHttpError(400, { error: 'Dictionary entry value is required.' })\n }\n payload.value = rawValue\n payload.normalized = normalizeDictionaryValue(rawValue)\n }\n if (Object.prototype.hasOwnProperty.call(input, 'label')) {\n const rawLabel = typeof input?.label === 'string' ? input.label.trim() : ''\n payload.label = rawLabel\n }\n if (Object.prototype.hasOwnProperty.call(input, 'color')) {\n payload.color = sanitizeDictionaryColor(input?.color ?? null)\n }\n if (Object.prototype.hasOwnProperty.call(input, 'icon')) {\n payload.icon = sanitizeDictionaryIcon(input?.icon ?? null)\n }\n if (Object.prototype.hasOwnProperty.call(input, 'position')) {\n const rawPosition = typeof input?.position === 'number' ? input.position : undefined\n if (rawPosition !== undefined) {\n payload.position = rawPosition\n }\n }\n if (Object.prototype.hasOwnProperty.call(input, 'isDefault')) {\n payload.isDefault = typeof input?.isDefault === 'boolean' ? input.isDefault : undefined\n }\n return payload\n}\n\nexport function registerDictionaryEntryCommands<TCreate, TUpdate>(\n config: DictionaryEntryCommandConfig<TCreate, TUpdate>\n): void {\n const scopeEnsurer = toScopeEnsurer(config.ensureScope)\n const ensureDictionaryForUndo = ensureDictionaryForUndoFactory(config.ensureDictionaryForUndo)\n const translationKeyPrefix = config.translationKeyPrefix ?? config.commandPrefix\n const fallbackSingular = config.labels?.singular ?? 'Dictionary entry'\n const fallbackCreate = config.labels?.create ?? `Create ${fallbackSingular}`\n const fallbackUpdate = config.labels?.update ?? `Update ${fallbackSingular}`\n const fallbackDelete = config.labels?.delete ?? `Delete ${fallbackSingular}`\n const duplicateMessage = getDuplicateError(config)\n\n const createCommand: CommandHandler<\n ReturnType<typeof config.createSchema['parse']>,\n { entryId: string }\n > = {\n id: `${config.commandPrefix}.create`,\n async execute(rawInput, ctx) {\n const parsed = config.createSchema.parse(rawInput)\n const em = (ctx.container.resolve('em') as EntityManager).fork()\n const { dictionary, scope } = await config.resolveDictionaryForCreate({ em, ctx, parsed })\n scopeEnsurer(ctx, scope)\n\n const { value, normalized, label, color, icon, position } = sanitizeCreatePayload(parsed)\n const duplicate = await em.findOne(DictionaryEntry, {\n dictionary,\n tenantId: scope.tenantId,\n organizationId: scope.organizationId,\n normalizedValue: normalized,\n })\n if (duplicate) {\n throw new CrudHttpError(409, { error: duplicateMessage })\n }\n\n const entry = em.create(DictionaryEntry, {\n dictionary,\n tenantId: scope.tenantId,\n organizationId: scope.organizationId,\n value,\n normalizedValue: normalized,\n label,\n color,\n icon,\n ...(position !== undefined ? { position } : {}),\n createdAt: new Date(),\n updatedAt: new Date(),\n })\n em.persist(entry)\n await em.flush()\n return { entryId: entry.id }\n },\n captureAfter: async (_input, result, ctx) => {\n const em = (ctx.container.resolve('em') as EntityManager).fork()\n return loadSnapshot(em, result.entryId)\n },\n buildLog: async ({ result, snapshots }) => {\n const after = snapshots.after as DictionaryEntrySnapshot | undefined\n if (!after) return null\n const { translate } = await resolveTranslations()\n return {\n actionLabel: translate(`${translationKeyPrefix}.audit.create`, fallbackCreate),\n resourceKind: config.resourceKind,\n resourceId: result.entryId,\n tenantId: after.tenantId,\n organizationId: after.organizationId,\n snapshotAfter: after,\n payload: {\n undo: { after } satisfies DictionaryEntryUndoPayload,\n },\n }\n },\n undo: async ({ logEntry, ctx }) => {\n const undo = extractUndoPayload<DictionaryEntryUndoPayload>(logEntry)\n const after =\n undo?.after ??\n (logEntry?.snapshotAfter as DictionaryEntrySnapshot | null | undefined) ??\n null\n if (!after) return\n const em = (ctx.container.resolve('em') as EntityManager).fork()\n scopeEnsurer(ctx, { tenantId: after.tenantId, organizationId: after.organizationId })\n const entry = await em.findOne(DictionaryEntry, after.id)\n if (entry) {\n await em.remove(entry).flush()\n return\n }\n await em.nativeDelete(DictionaryEntry, { id: after.id })\n },\n}\n\n const updateCommand: CommandHandler<\n ReturnType<typeof config.updateSchema['parse']>,\n { entryId: string }\n > = {\n id: `${config.commandPrefix}.update`,\n async prepare(input, ctx) {\n const id = requireId(input, 'Dictionary entry id is required')\n const em = (ctx.container.resolve('em') as EntityManager)\n const snapshot = await loadSnapshot(em, id)\n if (snapshot) {\n scopeEnsurer(ctx, { tenantId: snapshot.tenantId, organizationId: snapshot.organizationId })\n }\n return snapshot ? { before: snapshot } : {}\n },\n async execute(rawInput, ctx) {\n const parsed = config.updateSchema.parse(rawInput)\n const em = (ctx.container.resolve('em') as EntityManager).fork()\n const id = requireId(parsed, 'Dictionary entry id is required')\n const { entry, dictionary, scope } = await config.resolveEntry({ em, ctx, id, parsed })\n scopeEnsurer(ctx, scope)\n\n const updates = sanitizeUpdatePayload(parsed)\n if (updates.value !== undefined && updates.normalized !== undefined) {\n if (updates.normalized !== entry.normalizedValue) {\n const duplicate = await em.findOne(DictionaryEntry, {\n dictionary,\n tenantId: scope.tenantId,\n organizationId: scope.organizationId,\n normalizedValue: updates.normalized,\n id: { $ne: entry.id },\n } as any)\n if (duplicate) {\n throw new CrudHttpError(409, { error: duplicateMessage })\n }\n entry.value = updates.value\n entry.normalizedValue = updates.normalized\n if (updates.label === undefined) {\n entry.label = entry.value\n }\n }\n }\n if (updates.label !== undefined) {\n entry.label = updates.label || entry.value\n }\n if (Object.prototype.hasOwnProperty.call(updates, 'color')) {\n entry.color = updates.color ?? null\n }\n if (Object.prototype.hasOwnProperty.call(updates, 'icon')) {\n entry.icon = updates.icon ?? null\n }\n if (updates.position !== undefined) {\n entry.position = updates.position\n }\n if (updates.isDefault !== undefined) {\n entry.isDefault = updates.isDefault\n }\n await em.flush()\n return { entryId: entry.id }\n },\n captureAfter: async (_input, result, ctx) => {\n const em = (ctx.container.resolve('em') as EntityManager).fork()\n return loadSnapshot(em, result.entryId)\n },\n buildLog: async ({ snapshots }) => {\n const before = snapshots.before as DictionaryEntrySnapshot | undefined\n const after = snapshots.after as DictionaryEntrySnapshot | undefined\n if (!before || !after) return null\n const { translate } = await resolveTranslations()\n return {\n actionLabel: translate(`${translationKeyPrefix}.audit.update`, fallbackUpdate),\n resourceKind: config.resourceKind,\n resourceId: before.id,\n tenantId: before.tenantId,\n organizationId: before.organizationId,\n snapshotBefore: before,\n snapshotAfter: after,\n changes: buildChanges(\n before as Record<string, unknown>,\n after as Record<string, unknown>,\n ENTRY_CHANGE_KEYS,\n ),\n payload: {\n undo: { before, after } satisfies DictionaryEntryUndoPayload,\n },\n }\n },\n undo: async ({ logEntry, ctx }) => {\n const undo = extractUndoPayload<DictionaryEntryUndoPayload>(logEntry)\n const before =\n undo?.before ??\n (logEntry?.snapshotBefore as DictionaryEntrySnapshot | null | undefined) ??\n null\n if (!before) return\n const em = (ctx.container.resolve('em') as EntityManager).fork()\n scopeEnsurer(ctx, { tenantId: before.tenantId, organizationId: before.organizationId })\n const dictionary = await ensureDictionaryForUndo({ em, ctx, snapshot: before })\n if (!dictionary) return\n let entry = await em.findOne(DictionaryEntry, before.id)\n if (!entry) {\n entry = em.create(DictionaryEntry, {\n id: before.id,\n dictionary,\n tenantId: before.tenantId,\n organizationId: before.organizationId,\n value: before.value,\n normalizedValue: normalizeDictionaryValue(before.value),\n label: before.label,\n color: before.color,\n icon: before.icon,\n createdAt: new Date(before.createdAt),\n updatedAt: new Date(before.updatedAt),\n })\n em.persist(entry)\n } else {\n entry.dictionary = dictionary\n applySnapshot(entry, before)\n }\n await em.flush()\n },\n }\n\n const deleteCommand: CommandHandler<\n { body?: Record<string, unknown>; query?: Record<string, unknown> },\n { entryId: string }\n > = {\n id: `${config.commandPrefix}.delete`,\n async prepare(input, ctx) {\n const id = requireId(input, 'Dictionary entry id is required')\n const em = (ctx.container.resolve('em') as EntityManager)\n const snapshot = await loadSnapshot(em, id)\n if (snapshot) {\n scopeEnsurer(ctx, { tenantId: snapshot.tenantId, organizationId: snapshot.organizationId })\n }\n return snapshot ? { before: snapshot } : {}\n },\n async execute(input, ctx) {\n const id = requireId(input, 'Dictionary entry id is required')\n const em = (ctx.container.resolve('em') as EntityManager).fork()\n const { entry, scope } = await config.resolveEntry({ em, ctx, id })\n scopeEnsurer(ctx, scope)\n em.remove(entry)\n await em.flush()\n return { entryId: id }\n },\n buildLog: async ({ snapshots }) => {\n const before = snapshots.before as DictionaryEntrySnapshot | undefined\n if (!before) return null\n const { translate } = await resolveTranslations()\n return {\n actionLabel: translate(`${translationKeyPrefix}.audit.delete`, fallbackDelete),\n resourceKind: config.resourceKind,\n resourceId: before.id,\n tenantId: before.tenantId,\n organizationId: before.organizationId,\n snapshotBefore: before,\n payload: {\n undo: { before } satisfies DictionaryEntryUndoPayload,\n },\n }\n },\n undo: async ({ logEntry, ctx }) => {\n const undo = extractUndoPayload<DictionaryEntryUndoPayload>(logEntry)\n const before =\n undo?.before ??\n (logEntry?.snapshotBefore as DictionaryEntrySnapshot | null | undefined) ??\n null\n if (!before) return\n const em = (ctx.container.resolve('em') as EntityManager).fork()\n scopeEnsurer(ctx, { tenantId: before.tenantId, organizationId: before.organizationId })\n const dictionary = await ensureDictionaryForUndo({ em, ctx, snapshot: before })\n if (!dictionary) return\n let entry = await em.findOne(DictionaryEntry, before.id)\n if (!entry) {\n entry = em.create(DictionaryEntry, {\n id: before.id,\n dictionary,\n tenantId: before.tenantId,\n organizationId: before.organizationId,\n value: before.value,\n normalizedValue: normalizeDictionaryValue(before.value),\n label: before.label,\n color: before.color,\n icon: before.icon,\n createdAt: new Date(before.createdAt),\n updatedAt: new Date(before.updatedAt),\n })\n em.persist(entry)\n } else {\n entry.dictionary = dictionary\n applySnapshot(entry, before)\n }\n await em.flush()\n },\n }\n\n registerCommand(createCommand)\n registerCommand(updateCommand)\n registerCommand(deleteCommand)\n}\n"],
5
+ "mappings": "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",
6
6
  "names": []
7
7
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@open-mercato/core",
3
- "version": "0.6.6-develop.6541.1.c6f8fa5d2b",
3
+ "version": "0.6.6-develop.6543.1.49fadb4c31",
4
4
  "license": "MIT",
5
5
  "type": "module",
6
6
  "main": "./dist/index.js",
@@ -253,16 +253,16 @@
253
253
  "zod": "^4.4.3"
254
254
  },
255
255
  "peerDependencies": {
256
- "@open-mercato/ai-assistant": "0.6.6-develop.6541.1.c6f8fa5d2b",
257
- "@open-mercato/shared": "0.6.6-develop.6541.1.c6f8fa5d2b",
258
- "@open-mercato/ui": "0.6.6-develop.6541.1.c6f8fa5d2b",
256
+ "@open-mercato/ai-assistant": "0.6.6-develop.6543.1.49fadb4c31",
257
+ "@open-mercato/shared": "0.6.6-develop.6543.1.49fadb4c31",
258
+ "@open-mercato/ui": "0.6.6-develop.6543.1.49fadb4c31",
259
259
  "react": "^19.0.0",
260
260
  "react-dom": "^19.0.0"
261
261
  },
262
262
  "devDependencies": {
263
- "@open-mercato/ai-assistant": "0.6.6-develop.6541.1.c6f8fa5d2b",
264
- "@open-mercato/shared": "0.6.6-develop.6541.1.c6f8fa5d2b",
265
- "@open-mercato/ui": "0.6.6-develop.6541.1.c6f8fa5d2b",
263
+ "@open-mercato/ai-assistant": "0.6.6-develop.6543.1.49fadb4c31",
264
+ "@open-mercato/shared": "0.6.6-develop.6543.1.49fadb4c31",
265
+ "@open-mercato/ui": "0.6.6-develop.6543.1.49fadb4c31",
266
266
  "@testing-library/dom": "^10.4.1",
267
267
  "@testing-library/jest-dom": "^6.9.1",
268
268
  "@testing-library/react": "^16.3.1",
@@ -6,7 +6,10 @@ import { DashboardRoleWidgets } from '@open-mercato/core/modules/dashboards/data
6
6
  import { Role } from '@open-mercato/core/modules/auth/data/entities'
7
7
  import { roleWidgetSettingsSchema } from '@open-mercato/core/modules/dashboards/data/validators'
8
8
  import { loadAllWidgets } from '@open-mercato/core/modules/dashboards/lib/widgets'
9
- import { resolveWidgetAssignmentReadScope } from '@open-mercato/core/modules/dashboards/lib/widgetAssignmentScope'
9
+ import {
10
+ resolveWidgetAssignmentReadScope,
11
+ resolveWidgetAssignmentTargetAccess,
12
+ } from '@open-mercato/core/modules/dashboards/lib/widgetAssignmentScope'
10
13
  import { hasFeature } from '@open-mercato/shared/security/features'
11
14
  import {
12
15
  runCrudMutationGuardAfterSuccess,
@@ -70,7 +73,15 @@ export async function GET(req: Request) {
70
73
  })
71
74
 
72
75
  const role = await em.findOne(Role, { id: roleId, deletedAt: null })
73
- if (!role || (tenantId && role.tenantId !== tenantId)) {
76
+ const access = resolveWidgetAssignmentTargetAccess({
77
+ isSuperAdmin: !!acl.isSuperAdmin,
78
+ scopeTenantId: tenantId,
79
+ target: role,
80
+ })
81
+ if (access === 'forbidden') {
82
+ return NextResponse.json({ error: 'Forbidden' }, { status: 403 })
83
+ }
84
+ if (access === 'not-found') {
74
85
  return NextResponse.json({ error: 'Role not found' }, { status: 404 })
75
86
  }
76
87
 
@@ -121,7 +132,15 @@ export async function PUT(req: Request) {
121
132
  const organizationId = auth.orgId ?? null
122
133
 
123
134
  const role = await em.findOne(Role, { id: parsed.data.roleId, deletedAt: null })
124
- if (!role || (tenantId && role.tenantId !== tenantId)) {
135
+ const access = resolveWidgetAssignmentTargetAccess({
136
+ isSuperAdmin: !!acl.isSuperAdmin,
137
+ scopeTenantId: tenantId,
138
+ target: role,
139
+ })
140
+ if (access === 'forbidden') {
141
+ return NextResponse.json({ error: 'Forbidden' }, { status: 403 })
142
+ }
143
+ if (access === 'not-found') {
125
144
  return NextResponse.json({ error: 'Role not found' }, { status: 404 })
126
145
  }
127
146
 
@@ -7,7 +7,10 @@ import { User } from '@open-mercato/core/modules/auth/data/entities'
7
7
  import { userWidgetSettingsSchema } from '@open-mercato/core/modules/dashboards/data/validators'
8
8
  import { loadAllWidgets } from '@open-mercato/core/modules/dashboards/lib/widgets'
9
9
  import { resolveAllowedWidgetIds } from '@open-mercato/core/modules/dashboards/lib/access'
10
- import { resolveWidgetAssignmentReadScope } from '@open-mercato/core/modules/dashboards/lib/widgetAssignmentScope'
10
+ import {
11
+ resolveWidgetAssignmentReadScope,
12
+ resolveWidgetAssignmentTargetAccess,
13
+ } from '@open-mercato/core/modules/dashboards/lib/widgetAssignmentScope'
11
14
  import { hasFeature } from '@open-mercato/shared/security/features'
12
15
  import {
13
16
  runCrudMutationGuardAfterSuccess,
@@ -53,7 +56,15 @@ export async function GET(req: Request) {
53
56
  })
54
57
 
55
58
  const targetUserForRead = await em.findOne(User, { id: userId, deletedAt: null })
56
- if (!targetUserForRead || (tenantId && (targetUserForRead.tenantId ?? null) !== tenantId)) {
59
+ const readAccess = resolveWidgetAssignmentTargetAccess({
60
+ isSuperAdmin: !!acl.isSuperAdmin,
61
+ scopeTenantId: tenantId,
62
+ target: targetUserForRead,
63
+ })
64
+ if (readAccess === 'forbidden') {
65
+ return NextResponse.json({ error: 'Forbidden' }, { status: 403 })
66
+ }
67
+ if (readAccess === 'not-found') {
57
68
  return NextResponse.json({ error: 'User not found' }, { status: 404 })
58
69
  }
59
70
 
@@ -121,7 +132,15 @@ export async function PUT(req: Request) {
121
132
  const organizationId = auth.orgId ?? null
122
133
 
123
134
  const targetUser = await em.findOne(User, { id: parsed.data.userId, deletedAt: null })
124
- if (!targetUser || (tenantId && (targetUser.tenantId ?? null) !== tenantId)) {
135
+ const writeAccess = resolveWidgetAssignmentTargetAccess({
136
+ isSuperAdmin: !!acl.isSuperAdmin,
137
+ scopeTenantId: tenantId,
138
+ target: targetUser,
139
+ })
140
+ if (writeAccess === 'forbidden') {
141
+ return NextResponse.json({ error: 'Forbidden' }, { status: 403 })
142
+ }
143
+ if (writeAccess === 'not-found') {
125
144
  return NextResponse.json({ error: 'User not found' }, { status: 404 })
126
145
  }
127
146
 
@@ -8,6 +8,24 @@ export type ResolvedAssignmentScope = {
8
8
  organizationId: string | null
9
9
  }
10
10
 
11
+ export type WidgetAssignmentTarget = {
12
+ tenantId?: string | null
13
+ }
14
+
15
+ export type WidgetAssignmentTargetAccess = 'allowed' | 'forbidden' | 'not-found'
16
+
17
+ export function resolveWidgetAssignmentTargetAccess(params: {
18
+ isSuperAdmin: boolean
19
+ scopeTenantId: string | null
20
+ target: WidgetAssignmentTarget | null | undefined
21
+ }): WidgetAssignmentTargetAccess {
22
+ const { isSuperAdmin, scopeTenantId, target } = params
23
+ if (!isSuperAdmin && !scopeTenantId) return 'forbidden'
24
+ if (!target) return 'not-found'
25
+ if (scopeTenantId && (target.tenantId ?? null) !== scopeTenantId) return 'not-found'
26
+ return 'allowed'
27
+ }
28
+
11
29
  export function resolveWidgetAssignmentReadScope(params: {
12
30
  auth: AuthScopeInput
13
31
  isSuperAdmin: boolean
@@ -4,22 +4,13 @@ import {
4
4
  dictionaryEntryCommandCreateSchema,
5
5
  dictionaryEntryCommandUpdateSchema,
6
6
  } from '@open-mercato/core/modules/dictionaries/data/validators'
7
- import { registerDictionaryEntryCommands } from '@open-mercato/core/modules/dictionaries/commands/factory'
8
- import type { CommandRuntimeContext } from '@open-mercato/shared/lib/commands'
7
+ import {
8
+ ensureDictionaryEntryScope,
9
+ registerDictionaryEntryCommands,
10
+ } from '@open-mercato/core/modules/dictionaries/commands/factory'
9
11
  import { CrudHttpError } from '@open-mercato/shared/lib/crud/errors'
10
12
  import { findOneWithDecryption } from '@open-mercato/shared/lib/encryption/find'
11
13
 
12
- function ensureScope(ctx: CommandRuntimeContext, scope: { tenantId: string; organizationId: string }): void {
13
- const tenantId = ctx.auth?.tenantId ?? null
14
- if (tenantId && tenantId !== scope.tenantId) {
15
- throw new CrudHttpError(403, { error: 'Forbidden' })
16
- }
17
- const organizationId = ctx.selectedOrganizationId ?? ctx.auth?.orgId ?? null
18
- if (organizationId && organizationId !== scope.organizationId) {
19
- throw new CrudHttpError(403, { error: 'Forbidden' })
20
- }
21
- }
22
-
23
14
  async function ensureDictionary(em: EntityManager, id: string): Promise<Dictionary> {
24
15
  const dictionary = await em.findOne(Dictionary, {
25
16
  id,
@@ -37,12 +28,12 @@ registerDictionaryEntryCommands({
37
28
  translationKeyPrefix: 'dictionaries.entries',
38
29
  createSchema: dictionaryEntryCommandCreateSchema,
39
30
  updateSchema: dictionaryEntryCommandUpdateSchema,
40
- ensureScope,
31
+ ensureScope: ensureDictionaryEntryScope,
41
32
  duplicateError: 'An entry with this value already exists.',
42
33
  resolveDictionaryForCreate: async ({ em, ctx, parsed }) => {
43
34
  const dictionary = await ensureDictionary(em, parsed.dictionaryId)
44
35
  const scope = { tenantId: dictionary.tenantId, organizationId: dictionary.organizationId }
45
- ensureScope(ctx, scope)
36
+ ensureDictionaryEntryScope(ctx, scope)
46
37
  return { dictionary, scope }
47
38
  },
48
39
  resolveEntry: async ({ em, ctx, id }) => {
@@ -54,7 +45,7 @@ registerDictionaryEntryCommands({
54
45
  throw new CrudHttpError(404, { error: 'Dictionary not found' })
55
46
  }
56
47
  const scope = { tenantId: entry.tenantId, organizationId: entry.organizationId }
57
- ensureScope(ctx, scope)
48
+ ensureDictionaryEntryScope(ctx, scope)
58
49
  return { entry, dictionary: entry.dictionary, scope }
59
50
  },
60
51
  ensureDictionaryForUndo: async ({ em, snapshot }) => {
@@ -6,7 +6,8 @@ import {
6
6
  type ReorderDictionaryEntriesCommandInput,
7
7
  type SetDefaultDictionaryEntryCommandInput,
8
8
  } from '@open-mercato/core/modules/dictionaries/data/validators'
9
- import { registerCommand, type CommandHandler, type CommandRuntimeContext } from '@open-mercato/shared/lib/commands'
9
+ import { ensureDictionaryEntryScope } from '@open-mercato/core/modules/dictionaries/commands/factory'
10
+ import { registerCommand, type CommandHandler } from '@open-mercato/shared/lib/commands'
10
11
  import { extractUndoPayload } from '@open-mercato/shared/lib/commands/undo'
11
12
  import { withAtomicFlush } from '@open-mercato/shared/lib/commands/flush'
12
13
  import { emitCrudSideEffects, emitCrudUndoSideEffects } from '@open-mercato/shared/lib/commands/helpers'
@@ -36,17 +37,6 @@ const dictionaryCrudEvents: CrudEventsConfig = {
36
37
  }),
37
38
  }
38
39
 
39
- function ensureScope(ctx: CommandRuntimeContext, scope: DictionaryScope): void {
40
- const tenantId = ctx.auth?.tenantId ?? null
41
- if (tenantId && tenantId !== scope.tenantId) {
42
- throw new CrudHttpError(403, { error: 'Forbidden' })
43
- }
44
- const organizationId = ctx.selectedOrganizationId ?? ctx.auth?.orgId ?? null
45
- if (organizationId && organizationId !== scope.organizationId) {
46
- throw new CrudHttpError(403, { error: 'Forbidden' })
47
- }
48
- }
49
-
50
40
  async function requireDictionary(
51
41
  em: EntityManager,
52
42
  id: string,
@@ -84,7 +74,7 @@ const reorderDictionaryEntriesCommand: CommandHandler<
84
74
  async prepare(rawInput, ctx) {
85
75
  const parsed = reorderDictionaryEntriesCommandSchema.parse(rawInput)
86
76
  const scope: DictionaryScope = { tenantId: parsed.tenantId, organizationId: parsed.organizationId }
87
- ensureScope(ctx, scope)
77
+ ensureDictionaryEntryScope(ctx, scope)
88
78
  const em = (ctx.container.resolve('em') as EntityManager).fork()
89
79
  const ids = parsed.entries.map((e) => e.id)
90
80
  const existing = await findWithDecryption(
@@ -108,7 +98,7 @@ const reorderDictionaryEntriesCommand: CommandHandler<
108
98
  async execute(rawInput, ctx) {
109
99
  const parsed = reorderDictionaryEntriesCommandSchema.parse(rawInput)
110
100
  const scope: DictionaryScope = { tenantId: parsed.tenantId, organizationId: parsed.organizationId }
111
- ensureScope(ctx, scope)
101
+ ensureDictionaryEntryScope(ctx, scope)
112
102
 
113
103
  const em = (ctx.container.resolve('em') as EntityManager).fork()
114
104
  const dictionary = await requireDictionary(em, parsed.dictionaryId, scope)
@@ -201,7 +191,7 @@ const reorderDictionaryEntriesCommand: CommandHandler<
201
191
  const undo = extractUndoPayload<ReorderUndoPayload>(logEntry)
202
192
  const before = undo?.before ?? (logEntry?.snapshotBefore as ReorderSnapshot | null | undefined) ?? null
203
193
  if (!before) return
204
- ensureScope(ctx, before.scope)
194
+ ensureDictionaryEntryScope(ctx, before.scope)
205
195
 
206
196
  const em = (ctx.container.resolve('em') as EntityManager).fork()
207
197
  const ids = before.positions.map((p) => p.id)
@@ -265,7 +255,7 @@ const setDefaultDictionaryEntryCommand: CommandHandler<
265
255
  async prepare(rawInput, ctx) {
266
256
  const parsed = setDefaultDictionaryEntryCommandSchema.parse(rawInput)
267
257
  const scope: DictionaryScope = { tenantId: parsed.tenantId, organizationId: parsed.organizationId }
268
- ensureScope(ctx, scope)
258
+ ensureDictionaryEntryScope(ctx, scope)
269
259
  const em = (ctx.container.resolve('em') as EntityManager).fork()
270
260
  const dictionary = await requireDictionary(em, parsed.dictionaryId, scope)
271
261
  const previousDefaults = await findWithDecryption(
@@ -291,7 +281,7 @@ const setDefaultDictionaryEntryCommand: CommandHandler<
291
281
  async execute(rawInput, ctx) {
292
282
  const parsed = setDefaultDictionaryEntryCommandSchema.parse(rawInput)
293
283
  const scope: DictionaryScope = { tenantId: parsed.tenantId, organizationId: parsed.organizationId }
294
- ensureScope(ctx, scope)
284
+ ensureDictionaryEntryScope(ctx, scope)
295
285
 
296
286
  const em = (ctx.container.resolve('em') as EntityManager).fork()
297
287
  const dictionary = await requireDictionary(em, parsed.dictionaryId, scope)
@@ -399,7 +389,7 @@ const setDefaultDictionaryEntryCommand: CommandHandler<
399
389
  const undo = extractUndoPayload<DefaultUndoPayload>(logEntry)
400
390
  const before = undo?.before ?? (logEntry?.snapshotBefore as DefaultSnapshot | null | undefined) ?? null
401
391
  if (!before) return
402
- ensureScope(ctx, before.scope)
392
+ ensureDictionaryEntryScope(ctx, before.scope)
403
393
 
404
394
  const em = (ctx.container.resolve('em') as EntityManager).fork()
405
395
  const dictionary = await requireDictionary(em, before.dictionaryId, before.scope)
@@ -12,6 +12,7 @@ import {
12
12
  } from '@open-mercato/shared/lib/commands'
13
13
  import { extractUndoPayload } from '@open-mercato/shared/lib/commands/undo'
14
14
  import { buildChanges, requireId } from '@open-mercato/shared/lib/commands/helpers'
15
+ import { ensureOrganizationScope, ensureTenantScope } from '@open-mercato/shared/lib/commands/scope'
15
16
  import { CrudHttpError } from '@open-mercato/shared/lib/crud/errors'
16
17
  import { resolveTranslations } from '@open-mercato/shared/lib/i18n/server'
17
18
  import type { z } from 'zod'
@@ -22,6 +23,11 @@ type DictionaryScope = {
22
23
  organizationId: string
23
24
  }
24
25
 
26
+ export function ensureDictionaryEntryScope(ctx: CommandRuntimeContext, scope: DictionaryScope): void {
27
+ ensureTenantScope(ctx, scope.tenantId)
28
+ ensureOrganizationScope(ctx, scope.organizationId)
29
+ }
30
+
25
31
  export type DictionaryEntrySnapshot = {
26
32
  id: string
27
33
  dictionaryId: string
@@ -87,8 +93,8 @@ export type DictionaryEntryCommandConfig<TCreate, TUpdate> = {
87
93
  labels?: DictionaryEntryCommandLabels
88
94
  }
89
95
 
90
- function toScopeEnsurer(fn: EnsureScopeFn | undefined): EnsureScopeFn {
91
- return typeof fn === 'function' ? fn : () => {}
96
+ export function toScopeEnsurer(fn: EnsureScopeFn | undefined): EnsureScopeFn {
97
+ return typeof fn === 'function' ? fn : ensureDictionaryEntryScope
92
98
  }
93
99
 
94
100
  async function loadSnapshot(em: EntityManager, id: string): Promise<DictionaryEntrySnapshot | null> {