@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.
- package/dist/modules/dashboards/api/roles/widgets/route.js +22 -3
- package/dist/modules/dashboards/api/roles/widgets/route.js.map +2 -2
- package/dist/modules/dashboards/api/users/widgets/route.js +22 -3
- package/dist/modules/dashboards/api/users/widgets/route.js.map +2 -2
- package/dist/modules/dashboards/lib/widgetAssignmentScope.js +9 -1
- package/dist/modules/dashboards/lib/widgetAssignmentScope.js.map +2 -2
- package/dist/modules/dictionaries/commands/entries.js +7 -14
- package/dist/modules/dictionaries/commands/entries.js.map +2 -2
- package/dist/modules/dictionaries/commands/entry-operations.js +7 -16
- package/dist/modules/dictionaries/commands/entry-operations.js.map +2 -2
- package/dist/modules/dictionaries/commands/factory.js +9 -3
- package/dist/modules/dictionaries/commands/factory.js.map +2 -2
- package/package.json +7 -7
- package/src/modules/dashboards/api/roles/widgets/route.ts +22 -3
- package/src/modules/dashboards/api/users/widgets/route.ts +22 -3
- package/src/modules/dashboards/lib/widgetAssignmentScope.ts +18 -0
- package/src/modules/dictionaries/commands/entries.ts +7 -16
- package/src/modules/dictionaries/commands/entry-operations.ts +8 -18
- package/src/modules/dictionaries/commands/factory.ts +8 -2
|
@@ -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 {
|
|
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
|
-
|
|
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
|
-
|
|
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 {
|
|
5
|
-
"mappings": "AAAA,SAAS,oBAAoB;AAC7B,SAAS,SAAS;AAClB,SAAS,0BAA0B;AACnC,SAAS,8BAA8B;AACvC,SAAS,4BAA4B;AACrC,SAAS,YAAY;AACrB,SAAS,gCAAgC;AACzC,SAAS,sBAAsB;AAC/B,
|
|
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 {
|
|
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
|
-
|
|
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
|
-
|
|
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 {
|
|
5
|
-
"mappings": "AAAA,SAAS,oBAAoB;AAC7B,SAAS,SAAS;AAClB,SAAS,0BAA0B;AACnC,SAAS,8BAA8B;AACvC,SAAS,4BAA4B;AACrC,SAAS,YAAY;AACrB,SAAS,gCAAgC;AACzC,SAAS,sBAAsB;AAC/B,SAAS,+BAA+B;AACxC,
|
|
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": "
|
|
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 {
|
|
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
|
-
|
|
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
|
-
|
|
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 {
|
|
5
|
-
"mappings": "AACA,SAAS,YAAY,uBAAuB;AAC5C;AAAA,EACE;AAAA,EACA;AAAA,OACK;AACP,
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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": "AACA,SAAS,YAAY,uBAAuB;AAC5C;AAAA,EACE;AAAA,EACA;AAAA,OAGK;AACP,SAAS,
|
|
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
|
-
|
|
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": "AACA,SAAS,YAAY,uBAAuB;AAC5C;AAAA,EACE;AAAA,EACA;AAAA,EACA;AAAA,OACK;AACP;AAAA,EACE;AAAA,OAGK;AACP,SAAS,0BAA0B;AACnC,SAAS,cAAc,iBAAiB;AACxC,SAAS,qBAAqB;AAC9B,SAAS,2BAA2B;AAEpC,SAAS,6BAA6B;
|
|
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.
|
|
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.
|
|
257
|
-
"@open-mercato/shared": "0.6.6-develop.
|
|
258
|
-
"@open-mercato/ui": "0.6.6-develop.
|
|
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.
|
|
264
|
-
"@open-mercato/shared": "0.6.6-develop.
|
|
265
|
-
"@open-mercato/ui": "0.6.6-develop.
|
|
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 {
|
|
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
|
-
|
|
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
|
-
|
|
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 {
|
|
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
|
-
|
|
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
|
-
|
|
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 {
|
|
8
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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 {
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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> {
|