@open-mercato/core 0.6.6-develop.6133.1.f01540250e → 0.6.6-develop.6140.1.d3fbb77495
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.turbo/turbo-build.log +1 -1
- package/dist/modules/customer_accounts/api/admin/users-invite.js +9 -0
- package/dist/modules/customer_accounts/api/admin/users-invite.js.map +2 -2
- package/dist/modules/customer_accounts/api/portal/users-invite.js +9 -0
- package/dist/modules/customer_accounts/api/portal/users-invite.js.map +2 -2
- package/dist/modules/customer_accounts/events.js +1 -0
- package/dist/modules/customer_accounts/events.js.map +2 -2
- package/dist/modules/dashboards/api/layout/[itemId]/route.js +34 -1
- package/dist/modules/dashboards/api/layout/[itemId]/route.js.map +2 -2
- package/dist/modules/dashboards/api/layout/route.js +34 -1
- package/dist/modules/dashboards/api/layout/route.js.map +2 -2
- package/dist/modules/dashboards/api/roles/widgets/route.js +47 -1
- package/dist/modules/dashboards/api/roles/widgets/route.js.map +2 -2
- package/dist/modules/dashboards/api/users/widgets/route.js +47 -1
- package/dist/modules/dashboards/api/users/widgets/route.js.map +2 -2
- package/dist/modules/directory/api/tenants/route.js +19 -23
- package/dist/modules/directory/api/tenants/route.js.map +2 -2
- package/dist/modules/directory/api/tenants/tenant-cf-filter.js +72 -0
- package/dist/modules/directory/api/tenants/tenant-cf-filter.js.map +7 -0
- package/dist/modules/feature_toggles/components/FeatureToggleDetailsCard.js +8 -6
- package/dist/modules/feature_toggles/components/FeatureToggleDetailsCard.js.map +2 -2
- package/dist/modules/integrations/api/[id]/health/route.js +33 -0
- package/dist/modules/integrations/api/[id]/health/route.js.map +2 -2
- package/dist/modules/notifications/api/unread-count/route.js +6 -3
- package/dist/modules/notifications/api/unread-count/route.js.map +2 -2
- package/package.json +7 -7
- package/src/modules/customer_accounts/AGENTS.md +1 -0
- package/src/modules/customer_accounts/api/admin/users-invite.ts +10 -0
- package/src/modules/customer_accounts/api/portal/users-invite.ts +10 -0
- package/src/modules/customer_accounts/events.ts +1 -0
- package/src/modules/dashboards/api/layout/[itemId]/route.ts +36 -1
- package/src/modules/dashboards/api/layout/route.ts +36 -1
- package/src/modules/dashboards/api/roles/widgets/route.ts +49 -1
- package/src/modules/dashboards/api/users/widgets/route.ts +49 -1
- package/src/modules/directory/api/tenants/route.ts +25 -26
- package/src/modules/directory/api/tenants/tenant-cf-filter.ts +97 -0
- package/src/modules/feature_toggles/components/FeatureToggleDetailsCard.tsx +10 -8
- package/src/modules/integrations/api/[id]/health/route.ts +35 -0
- package/src/modules/notifications/api/unread-count/route.ts +6 -4
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"version": 3,
|
|
3
3
|
"sources": ["../../../../../../src/modules/dashboards/api/roles/widgets/route.ts"],
|
|
4
|
-
"sourcesContent": ["import { NextResponse } from 'next/server'\nimport { z } from 'zod'\nimport { getAuthFromRequest } from '@open-mercato/shared/lib/auth/server'\nimport { createRequestContainer } from '@open-mercato/shared/lib/di/container'\nimport { DashboardRoleWidgets } from '@open-mercato/core/modules/dashboards/data/entities'\nimport { Role } from '@open-mercato/core/modules/auth/data/entities'\nimport { roleWidgetSettingsSchema } from '@open-mercato/core/modules/dashboards/data/validators'\nimport { loadAllWidgets } from '@open-mercato/core/modules/dashboards/lib/widgets'\nimport { resolveWidgetAssignmentReadScope } from '@open-mercato/core/modules/dashboards/lib/widgetAssignmentScope'\nimport { hasFeature } from '@open-mercato/shared/security/features'\nimport 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'\n\nfunction pickBestRecord(records: DashboardRoleWidgets[], tenantId: string | null, organizationId: string | null): DashboardRoleWidgets | null {\n let best: DashboardRoleWidgets | null = null\n let bestScore = -1\n for (const record of records) {\n if (record.deletedAt) continue\n if (record.tenantId && tenantId && record.tenantId !== tenantId) continue\n if (record.tenantId && !tenantId) continue\n if (record.organizationId && organizationId && record.organizationId !== organizationId) continue\n if (record.organizationId && !organizationId) continue\n const score = (record.tenantId ? 1 : 0) + (record.organizationId ? 2 : 0)\n if (score > bestScore) {\n best = record\n bestScore = score\n }\n }\n return best\n}\n\nexport const metadata = {\n GET: { requireAuth: true, requireFeatures: [FEATURE] },\n PUT: { requireAuth: true, requireFeatures: [FEATURE] },\n}\n\nexport async function GET(req: Request) {\n const auth = await getAuthFromRequest(req)\n if (!auth) return NextResponse.json({ error: 'Unauthorized' }, { status: 401 })\n\n const url = new URL(req.url)\n const roleId = url.searchParams.get('roleId')\n if (!roleId) return NextResponse.json({ error: 'roleId is required' }, { status: 400 })\n\n const container = await createRequestContainer()\n const em = container.resolve('em') as any\n const rbac = container.resolve('rbacService') as any\n const acl = await rbac.loadAcl(auth.sub, { tenantId: auth.tenantId ?? null, organizationId: auth.orgId ?? null })\n if (!acl.isSuperAdmin && !hasFeature(acl.features, FEATURE)) {\n return NextResponse.json({ error: 'Forbidden' }, { status: 403 })\n }\n\n const { tenantId, organizationId } = resolveWidgetAssignmentReadScope({\n auth: { tenantId: auth.tenantId ?? null, orgId: auth.orgId ?? null },\n isSuperAdmin: !!acl.isSuperAdmin,\n queryTenantId: url.searchParams.get('tenantId'),\n queryOrganizationId: url.searchParams.get('organizationId'),\n })\n\n const role = await em.findOne(Role, { id: roleId, deletedAt: null })\n if (!role || (tenantId && role.tenantId !== tenantId)) {\n return NextResponse.json({ error: 'Role not found' }, { status: 404 })\n }\n\n const records = await em.find(DashboardRoleWidgets, { roleId, deletedAt: null })\n const best = pickBestRecord(records, tenantId, organizationId)\n\n const response = {\n widgetIds: best ? best.widgetIdsJson : [],\n hasCustom: !!best,\n scope: {\n tenantId,\n organizationId,\n },\n }\n\n return NextResponse.json(response)\n}\n\nexport async function PUT(req: Request) {\n const auth = await getAuthFromRequest(req)\n if (!auth) return NextResponse.json({ error: 'Unauthorized' }, { status: 401 })\n\n let payload: unknown\n try {\n payload = await req.json()\n } catch {\n return NextResponse.json({ error: 'Invalid JSON body' }, { status: 400 })\n }\n const parsed = roleWidgetSettingsSchema.safeParse(payload)\n if (!parsed.success) {\n return NextResponse.json({ error: 'Invalid payload', issues: parsed.error.issues }, { status: 400 })\n }\n\n const
|
|
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,wCAAwC;AACjD,SAAS,kBAAkB;
|
|
4
|
+
"sourcesContent": ["import { NextResponse } from 'next/server'\nimport { z } from 'zod'\nimport { getAuthFromRequest } from '@open-mercato/shared/lib/auth/server'\nimport { createRequestContainer } from '@open-mercato/shared/lib/di/container'\nimport { DashboardRoleWidgets } from '@open-mercato/core/modules/dashboards/data/entities'\nimport { Role } from '@open-mercato/core/modules/auth/data/entities'\nimport { roleWidgetSettingsSchema } from '@open-mercato/core/modules/dashboards/data/validators'\nimport { loadAllWidgets } from '@open-mercato/core/modules/dashboards/lib/widgets'\nimport { resolveWidgetAssignmentReadScope } from '@open-mercato/core/modules/dashboards/lib/widgetAssignmentScope'\nimport { hasFeature } from '@open-mercato/shared/security/features'\nimport {\n runCrudMutationGuardAfterSuccess,\n validateCrudMutationGuard,\n} from '@open-mercato/shared/lib/crud/mutation-guard'\nimport type { OpenApiMethodDoc, OpenApiRouteDoc } from '@open-mercato/shared/lib/openapi'\nimport {\n dashboardsTag,\n dashboardsErrorSchema,\n dashboardRoleWidgetsResponseSchema,\n dashboardRoleWidgetsUpdateResponseSchema,\n} from '../../openapi'\n\nconst FEATURE = 'dashboards.admin.assign-widgets'\nconst RESOURCE_KIND = 'dashboards.roleWidgets'\n\nfunction pickBestRecord(records: DashboardRoleWidgets[], tenantId: string | null, organizationId: string | null): DashboardRoleWidgets | null {\n let best: DashboardRoleWidgets | null = null\n let bestScore = -1\n for (const record of records) {\n if (record.deletedAt) continue\n if (record.tenantId && tenantId && record.tenantId !== tenantId) continue\n if (record.tenantId && !tenantId) continue\n if (record.organizationId && organizationId && record.organizationId !== organizationId) continue\n if (record.organizationId && !organizationId) continue\n const score = (record.tenantId ? 1 : 0) + (record.organizationId ? 2 : 0)\n if (score > bestScore) {\n best = record\n bestScore = score\n }\n }\n return best\n}\n\nexport const metadata = {\n GET: { requireAuth: true, requireFeatures: [FEATURE] },\n PUT: { requireAuth: true, requireFeatures: [FEATURE] },\n}\n\nexport async function GET(req: Request) {\n const auth = await getAuthFromRequest(req)\n if (!auth) return NextResponse.json({ error: 'Unauthorized' }, { status: 401 })\n\n const url = new URL(req.url)\n const roleId = url.searchParams.get('roleId')\n if (!roleId) return NextResponse.json({ error: 'roleId is required' }, { status: 400 })\n\n const container = await createRequestContainer()\n const em = container.resolve('em') as any\n const rbac = container.resolve('rbacService') as any\n const acl = await rbac.loadAcl(auth.sub, { tenantId: auth.tenantId ?? null, organizationId: auth.orgId ?? null })\n if (!acl.isSuperAdmin && !hasFeature(acl.features, FEATURE)) {\n return NextResponse.json({ error: 'Forbidden' }, { status: 403 })\n }\n\n const { tenantId, organizationId } = resolveWidgetAssignmentReadScope({\n auth: { tenantId: auth.tenantId ?? null, orgId: auth.orgId ?? null },\n isSuperAdmin: !!acl.isSuperAdmin,\n queryTenantId: url.searchParams.get('tenantId'),\n queryOrganizationId: url.searchParams.get('organizationId'),\n })\n\n const role = await em.findOne(Role, { id: roleId, deletedAt: null })\n if (!role || (tenantId && role.tenantId !== tenantId)) {\n return NextResponse.json({ error: 'Role not found' }, { status: 404 })\n }\n\n const records = await em.find(DashboardRoleWidgets, { roleId, deletedAt: null })\n const best = pickBestRecord(records, tenantId, organizationId)\n\n const response = {\n widgetIds: best ? best.widgetIdsJson : [],\n hasCustom: !!best,\n scope: {\n tenantId,\n organizationId,\n },\n }\n\n return NextResponse.json(response)\n}\n\nexport async function PUT(req: Request) {\n const auth = await getAuthFromRequest(req)\n if (!auth) return NextResponse.json({ error: 'Unauthorized' }, { status: 401 })\n\n let payload: unknown\n try {\n payload = await req.json()\n } catch {\n return NextResponse.json({ error: 'Invalid JSON body' }, { status: 400 })\n }\n const parsed = roleWidgetSettingsSchema.safeParse(payload)\n if (!parsed.success) {\n return NextResponse.json({ error: 'Invalid payload', issues: parsed.error.issues }, { status: 400 })\n }\n\n const container = await createRequestContainer()\n const { resolve } = container\n const em = resolve('em') as any\n const rbac = resolve('rbacService') as any\n const acl = await rbac.loadAcl(auth.sub, { tenantId: auth.tenantId ?? null, organizationId: auth.orgId ?? null })\n if (!acl.isSuperAdmin && !hasFeature(acl.features, FEATURE)) {\n return NextResponse.json({ error: 'Forbidden' }, { status: 403 })\n }\n\n const widgets = await loadAllWidgets()\n const validWidgetIds = new Set(widgets.map((w) => w.metadata.id))\n const widgetIds = parsed.data.widgetIds.filter((id) => validWidgetIds.has(id))\n\n const tenantId = auth.tenantId ?? null\n const organizationId = auth.orgId ?? null\n\n const role = await em.findOne(Role, { id: parsed.data.roleId, deletedAt: null })\n if (!role || (tenantId && role.tenantId !== tenantId)) {\n return NextResponse.json({ error: 'Role not found' }, { status: 404 })\n }\n\n const guardResult = await validateCrudMutationGuard(container, {\n tenantId: tenantId ?? '',\n organizationId,\n userId: String(auth.sub),\n resourceKind: RESOURCE_KIND,\n resourceId: parsed.data.roleId,\n operation: 'update',\n requestMethod: req.method,\n requestHeaders: req.headers,\n mutationPayload: { roleId: parsed.data.roleId, widgetIds },\n })\n if (guardResult && !guardResult.ok) {\n return NextResponse.json(guardResult.body, { status: guardResult.status })\n }\n\n let record = await em.findOne(DashboardRoleWidgets, {\n roleId: parsed.data.roleId,\n tenantId,\n organizationId,\n deletedAt: null,\n })\n\n if (!widgetIds.length) {\n if (record) {\n await em.remove(record).flush()\n }\n if (guardResult?.ok && guardResult.shouldRunAfterSuccess) {\n await runCrudMutationGuardAfterSuccess(container, {\n tenantId: tenantId ?? '',\n organizationId,\n userId: String(auth.sub),\n resourceKind: RESOURCE_KIND,\n resourceId: parsed.data.roleId,\n operation: 'update',\n requestMethod: req.method,\n requestHeaders: req.headers,\n metadata: guardResult.metadata ?? null,\n })\n }\n return NextResponse.json({ ok: true, widgetIds: [] })\n }\n\n if (!record) {\n record = em.create(DashboardRoleWidgets, {\n roleId: parsed.data.roleId,\n tenantId,\n organizationId,\n widgetIdsJson: widgetIds,\n })\n em.persist(record)\n } else {\n record.widgetIdsJson = widgetIds\n }\n await em.flush()\n\n if (guardResult?.ok && guardResult.shouldRunAfterSuccess) {\n await runCrudMutationGuardAfterSuccess(container, {\n tenantId: tenantId ?? '',\n organizationId,\n userId: String(auth.sub),\n resourceKind: RESOURCE_KIND,\n resourceId: parsed.data.roleId,\n operation: 'update',\n requestMethod: req.method,\n requestHeaders: req.headers,\n metadata: guardResult.metadata ?? null,\n })\n }\n\n return NextResponse.json({ ok: true, widgetIds })\n}\n\nconst roleWidgetsQuerySchema = z.object({\n roleId: z.string().uuid(),\n tenantId: z.string().uuid().optional(),\n organizationId: z.string().uuid().optional(),\n})\n\nconst roleWidgetsGetDoc: OpenApiMethodDoc = {\n summary: 'Fetch widget assignments for a role',\n description: 'Returns the widgets explicitly assigned to the given role together with the evaluation scope.',\n tags: [dashboardsTag],\n query: roleWidgetsQuerySchema,\n responses: [\n { status: 200, description: 'Current widget configuration for the role.', schema: dashboardRoleWidgetsResponseSchema },\n ],\n errors: [\n { status: 400, description: 'Missing role identifier', schema: dashboardsErrorSchema },\n { status: 401, description: 'Authentication required', schema: dashboardsErrorSchema },\n { status: 403, description: 'Insufficient permissions to manage role widgets', schema: dashboardsErrorSchema },\n ],\n}\n\nconst roleWidgetsPutDoc: OpenApiMethodDoc = {\n summary: 'Update widgets assigned to a role',\n description: 'Persists the widget list for a role within the provided tenant and organization scope.',\n tags: [dashboardsTag],\n requestBody: {\n contentType: 'application/json',\n schema: roleWidgetSettingsSchema,\n description: 'Role identifier and the widget ids that should remain assigned. Scope is derived from the authenticated session.',\n },\n responses: [\n { status: 200, description: 'Widgets updated successfully.', schema: dashboardRoleWidgetsUpdateResponseSchema },\n ],\n errors: [\n { status: 400, description: 'Invalid payload or unknown widgets', schema: dashboardsErrorSchema },\n { status: 401, description: 'Authentication required', schema: dashboardsErrorSchema },\n { status: 403, description: 'Insufficient permissions to manage role widgets', schema: dashboardsErrorSchema },\n ],\n}\n\nexport const openApi: OpenApiRouteDoc = {\n tag: dashboardsTag,\n summary: 'Manage dashboard widgets assigned to a role',\n methods: {\n GET: roleWidgetsGetDoc,\n PUT: roleWidgetsPutDoc,\n },\n}\n"],
|
|
5
|
+
"mappings": "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",
|
|
6
6
|
"names": []
|
|
7
7
|
}
|
|
@@ -9,6 +9,10 @@ import { loadAllWidgets } from "@open-mercato/core/modules/dashboards/lib/widget
|
|
|
9
9
|
import { resolveAllowedWidgetIds } from "@open-mercato/core/modules/dashboards/lib/access";
|
|
10
10
|
import { resolveWidgetAssignmentReadScope } from "@open-mercato/core/modules/dashboards/lib/widgetAssignmentScope";
|
|
11
11
|
import { hasFeature } from "@open-mercato/shared/security/features";
|
|
12
|
+
import {
|
|
13
|
+
runCrudMutationGuardAfterSuccess,
|
|
14
|
+
validateCrudMutationGuard
|
|
15
|
+
} from "@open-mercato/shared/lib/crud/mutation-guard";
|
|
12
16
|
import {
|
|
13
17
|
dashboardsTag,
|
|
14
18
|
dashboardsErrorSchema,
|
|
@@ -16,6 +20,7 @@ import {
|
|
|
16
20
|
dashboardUserWidgetsUpdateResponseSchema
|
|
17
21
|
} from "../../openapi.js";
|
|
18
22
|
const FEATURE = "dashboards.admin.assign-widgets";
|
|
23
|
+
const RESOURCE_KIND = "dashboards.userWidgets";
|
|
19
24
|
const metadata = {
|
|
20
25
|
GET: { requireAuth: true, requireFeatures: [FEATURE] },
|
|
21
26
|
PUT: { requireAuth: true, requireFeatures: [FEATURE] }
|
|
@@ -84,7 +89,8 @@ async function PUT(req) {
|
|
|
84
89
|
if (!parsed.success) {
|
|
85
90
|
return NextResponse.json({ error: "Invalid payload", issues: parsed.error.issues }, { status: 400 });
|
|
86
91
|
}
|
|
87
|
-
const
|
|
92
|
+
const container = await createRequestContainer();
|
|
93
|
+
const { resolve } = container;
|
|
88
94
|
const em = resolve("em");
|
|
89
95
|
const rbac = resolve("rbacService");
|
|
90
96
|
const acl = await rbac.loadAcl(auth.sub, { tenantId: auth.tenantId ?? null, organizationId: auth.orgId ?? null });
|
|
@@ -100,6 +106,20 @@ async function PUT(req) {
|
|
|
100
106
|
if (!targetUser || tenantId && (targetUser.tenantId ?? null) !== tenantId) {
|
|
101
107
|
return NextResponse.json({ error: "User not found" }, { status: 404 });
|
|
102
108
|
}
|
|
109
|
+
const guardResult = await validateCrudMutationGuard(container, {
|
|
110
|
+
tenantId: tenantId ?? "",
|
|
111
|
+
organizationId,
|
|
112
|
+
userId: String(auth.sub),
|
|
113
|
+
resourceKind: RESOURCE_KIND,
|
|
114
|
+
resourceId: parsed.data.userId,
|
|
115
|
+
operation: "update",
|
|
116
|
+
requestMethod: req.method,
|
|
117
|
+
requestHeaders: req.headers,
|
|
118
|
+
mutationPayload: { userId: parsed.data.userId, mode: parsed.data.mode, widgetIds }
|
|
119
|
+
});
|
|
120
|
+
if (guardResult && !guardResult.ok) {
|
|
121
|
+
return NextResponse.json(guardResult.body, { status: guardResult.status });
|
|
122
|
+
}
|
|
103
123
|
let record = await em.findOne(DashboardUserWidgets, {
|
|
104
124
|
userId: parsed.data.userId,
|
|
105
125
|
tenantId,
|
|
@@ -110,6 +130,19 @@ async function PUT(req) {
|
|
|
110
130
|
if (record) {
|
|
111
131
|
await em.remove(record).flush();
|
|
112
132
|
}
|
|
133
|
+
if (guardResult?.ok && guardResult.shouldRunAfterSuccess) {
|
|
134
|
+
await runCrudMutationGuardAfterSuccess(container, {
|
|
135
|
+
tenantId: tenantId ?? "",
|
|
136
|
+
organizationId,
|
|
137
|
+
userId: String(auth.sub),
|
|
138
|
+
resourceKind: RESOURCE_KIND,
|
|
139
|
+
resourceId: parsed.data.userId,
|
|
140
|
+
operation: "update",
|
|
141
|
+
requestMethod: req.method,
|
|
142
|
+
requestHeaders: req.headers,
|
|
143
|
+
metadata: guardResult.metadata ?? null
|
|
144
|
+
});
|
|
145
|
+
}
|
|
113
146
|
return NextResponse.json({ ok: true, mode: "inherit", widgetIds: [] });
|
|
114
147
|
}
|
|
115
148
|
if (!record) {
|
|
@@ -126,6 +159,19 @@ async function PUT(req) {
|
|
|
126
159
|
record.widgetIdsJson = widgetIds;
|
|
127
160
|
}
|
|
128
161
|
await em.flush();
|
|
162
|
+
if (guardResult?.ok && guardResult.shouldRunAfterSuccess) {
|
|
163
|
+
await runCrudMutationGuardAfterSuccess(container, {
|
|
164
|
+
tenantId: tenantId ?? "",
|
|
165
|
+
organizationId,
|
|
166
|
+
userId: String(auth.sub),
|
|
167
|
+
resourceKind: RESOURCE_KIND,
|
|
168
|
+
resourceId: parsed.data.userId,
|
|
169
|
+
operation: "update",
|
|
170
|
+
requestMethod: req.method,
|
|
171
|
+
requestHeaders: req.headers,
|
|
172
|
+
metadata: guardResult.metadata ?? null
|
|
173
|
+
});
|
|
174
|
+
}
|
|
129
175
|
return NextResponse.json({ ok: true, mode: "override", widgetIds });
|
|
130
176
|
}
|
|
131
177
|
const userWidgetsQuerySchema = z.object({
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"version": 3,
|
|
3
3
|
"sources": ["../../../../../../src/modules/dashboards/api/users/widgets/route.ts"],
|
|
4
|
-
"sourcesContent": ["import { NextResponse } from 'next/server'\nimport { z } from 'zod'\nimport { getAuthFromRequest } from '@open-mercato/shared/lib/auth/server'\nimport { createRequestContainer } from '@open-mercato/shared/lib/di/container'\nimport { DashboardUserWidgets } from '@open-mercato/core/modules/dashboards/data/entities'\nimport { User } from '@open-mercato/core/modules/auth/data/entities'\nimport { userWidgetSettingsSchema } from '@open-mercato/core/modules/dashboards/data/validators'\nimport { loadAllWidgets } from '@open-mercato/core/modules/dashboards/lib/widgets'\nimport { resolveAllowedWidgetIds } from '@open-mercato/core/modules/dashboards/lib/access'\nimport { resolveWidgetAssignmentReadScope } from '@open-mercato/core/modules/dashboards/lib/widgetAssignmentScope'\nimport { hasFeature } from '@open-mercato/shared/security/features'\nimport 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'\n\nexport const metadata = {\n GET: { requireAuth: true, requireFeatures: [FEATURE] },\n PUT: { requireAuth: true, requireFeatures: [FEATURE] },\n}\n\nexport async function GET(req: Request) {\n const auth = await getAuthFromRequest(req)\n if (!auth) return NextResponse.json({ error: 'Unauthorized' }, { status: 401 })\n\n const url = new URL(req.url)\n const userId = url.searchParams.get('userId')\n if (!userId) return NextResponse.json({ error: 'userId is required' }, { status: 400 })\n\n const container = await createRequestContainer()\n const em = container.resolve('em') as any\n const rbac = container.resolve('rbacService') as any\n const acl = await rbac.loadAcl(auth.sub, { tenantId: auth.tenantId ?? null, organizationId: auth.orgId ?? null })\n if (!acl.isSuperAdmin && !hasFeature(acl.features, FEATURE)) {\n return NextResponse.json({ error: 'Forbidden' }, { status: 403 })\n }\n\n const { tenantId, organizationId } = resolveWidgetAssignmentReadScope({\n auth: { tenantId: auth.tenantId ?? null, orgId: auth.orgId ?? null },\n isSuperAdmin: !!acl.isSuperAdmin,\n queryTenantId: url.searchParams.get('tenantId'),\n queryOrganizationId: url.searchParams.get('organizationId'),\n })\n\n const targetUserForRead = await em.findOne(User, { id: userId, deletedAt: null })\n if (!targetUserForRead || (tenantId && (targetUserForRead.tenantId ?? null) !== tenantId)) {\n return NextResponse.json({ error: 'User not found' }, { status: 404 })\n }\n\n const widgets = await loadAllWidgets()\n const targetAcl = await rbac.loadAcl(userId, { tenantId, organizationId })\n const allowed = await resolveAllowedWidgetIds(\n em,\n {\n userId,\n tenantId,\n organizationId,\n features: targetAcl.features ?? [],\n isSuperAdmin: !!targetAcl.isSuperAdmin,\n },\n widgets,\n )\n\n const record = await em.findOne(DashboardUserWidgets, {\n userId,\n tenantId,\n organizationId,\n deletedAt: null,\n })\n\n const response = {\n mode: record ? record.mode : 'inherit',\n widgetIds: record && record.mode === 'override' ? record.widgetIdsJson : [],\n hasCustom: !!record && record.mode === 'override',\n effectiveWidgetIds: allowed,\n scope: { tenantId, organizationId },\n }\n\n return NextResponse.json(response)\n}\n\nexport async function PUT(req: Request) {\n const auth = await getAuthFromRequest(req)\n if (!auth) return NextResponse.json({ error: 'Unauthorized' }, { status: 401 })\n\n let payload: unknown\n try {\n payload = await req.json()\n } catch {\n return NextResponse.json({ error: 'Invalid JSON body' }, { status: 400 })\n }\n const parsed = userWidgetSettingsSchema.safeParse(payload)\n if (!parsed.success) {\n return NextResponse.json({ error: 'Invalid payload', issues: parsed.error.issues }, { status: 400 })\n }\n\n const
|
|
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,SAAS,wCAAwC;AACjD,SAAS,kBAAkB;
|
|
4
|
+
"sourcesContent": ["import { NextResponse } from 'next/server'\nimport { z } from 'zod'\nimport { getAuthFromRequest } from '@open-mercato/shared/lib/auth/server'\nimport { createRequestContainer } from '@open-mercato/shared/lib/di/container'\nimport { DashboardUserWidgets } from '@open-mercato/core/modules/dashboards/data/entities'\nimport { User } from '@open-mercato/core/modules/auth/data/entities'\nimport { userWidgetSettingsSchema } from '@open-mercato/core/modules/dashboards/data/validators'\nimport { loadAllWidgets } from '@open-mercato/core/modules/dashboards/lib/widgets'\nimport { resolveAllowedWidgetIds } from '@open-mercato/core/modules/dashboards/lib/access'\nimport { resolveWidgetAssignmentReadScope } from '@open-mercato/core/modules/dashboards/lib/widgetAssignmentScope'\nimport { hasFeature } from '@open-mercato/shared/security/features'\nimport {\n runCrudMutationGuardAfterSuccess,\n validateCrudMutationGuard,\n} from '@open-mercato/shared/lib/crud/mutation-guard'\nimport type { OpenApiMethodDoc, OpenApiRouteDoc } from '@open-mercato/shared/lib/openapi'\nimport {\n dashboardsTag,\n dashboardsErrorSchema,\n dashboardUserWidgetsResponseSchema,\n dashboardUserWidgetsUpdateResponseSchema,\n} from '../../openapi'\n\nconst FEATURE = 'dashboards.admin.assign-widgets'\nconst RESOURCE_KIND = 'dashboards.userWidgets'\n\nexport const metadata = {\n GET: { requireAuth: true, requireFeatures: [FEATURE] },\n PUT: { requireAuth: true, requireFeatures: [FEATURE] },\n}\n\nexport async function GET(req: Request) {\n const auth = await getAuthFromRequest(req)\n if (!auth) return NextResponse.json({ error: 'Unauthorized' }, { status: 401 })\n\n const url = new URL(req.url)\n const userId = url.searchParams.get('userId')\n if (!userId) return NextResponse.json({ error: 'userId is required' }, { status: 400 })\n\n const container = await createRequestContainer()\n const em = container.resolve('em') as any\n const rbac = container.resolve('rbacService') as any\n const acl = await rbac.loadAcl(auth.sub, { tenantId: auth.tenantId ?? null, organizationId: auth.orgId ?? null })\n if (!acl.isSuperAdmin && !hasFeature(acl.features, FEATURE)) {\n return NextResponse.json({ error: 'Forbidden' }, { status: 403 })\n }\n\n const { tenantId, organizationId } = resolveWidgetAssignmentReadScope({\n auth: { tenantId: auth.tenantId ?? null, orgId: auth.orgId ?? null },\n isSuperAdmin: !!acl.isSuperAdmin,\n queryTenantId: url.searchParams.get('tenantId'),\n queryOrganizationId: url.searchParams.get('organizationId'),\n })\n\n const targetUserForRead = await em.findOne(User, { id: userId, deletedAt: null })\n if (!targetUserForRead || (tenantId && (targetUserForRead.tenantId ?? null) !== tenantId)) {\n return NextResponse.json({ error: 'User not found' }, { status: 404 })\n }\n\n const widgets = await loadAllWidgets()\n const targetAcl = await rbac.loadAcl(userId, { tenantId, organizationId })\n const allowed = await resolveAllowedWidgetIds(\n em,\n {\n userId,\n tenantId,\n organizationId,\n features: targetAcl.features ?? [],\n isSuperAdmin: !!targetAcl.isSuperAdmin,\n },\n widgets,\n )\n\n const record = await em.findOne(DashboardUserWidgets, {\n userId,\n tenantId,\n organizationId,\n deletedAt: null,\n })\n\n const response = {\n mode: record ? record.mode : 'inherit',\n widgetIds: record && record.mode === 'override' ? record.widgetIdsJson : [],\n hasCustom: !!record && record.mode === 'override',\n effectiveWidgetIds: allowed,\n scope: { tenantId, organizationId },\n }\n\n return NextResponse.json(response)\n}\n\nexport async function PUT(req: Request) {\n const auth = await getAuthFromRequest(req)\n if (!auth) return NextResponse.json({ error: 'Unauthorized' }, { status: 401 })\n\n let payload: unknown\n try {\n payload = await req.json()\n } catch {\n return NextResponse.json({ error: 'Invalid JSON body' }, { status: 400 })\n }\n const parsed = userWidgetSettingsSchema.safeParse(payload)\n if (!parsed.success) {\n return NextResponse.json({ error: 'Invalid payload', issues: parsed.error.issues }, { status: 400 })\n }\n\n const container = await createRequestContainer()\n const { resolve } = container\n const em = resolve('em') as any\n const rbac = resolve('rbacService') as any\n const acl = await rbac.loadAcl(auth.sub, { tenantId: auth.tenantId ?? null, organizationId: auth.orgId ?? null })\n if (!acl.isSuperAdmin && !hasFeature(acl.features, FEATURE)) {\n return NextResponse.json({ error: 'Forbidden' }, { status: 403 })\n }\n\n const widgets = await loadAllWidgets()\n const validWidgetIds = new Set(widgets.map((w) => w.metadata.id))\n const widgetIds = parsed.data.widgetIds.filter((id) => validWidgetIds.has(id))\n\n const tenantId = auth.tenantId ?? null\n const organizationId = auth.orgId ?? null\n\n const targetUser = await em.findOne(User, { id: parsed.data.userId, deletedAt: null })\n if (!targetUser || (tenantId && (targetUser.tenantId ?? null) !== tenantId)) {\n return NextResponse.json({ error: 'User not found' }, { status: 404 })\n }\n\n const guardResult = await validateCrudMutationGuard(container, {\n tenantId: tenantId ?? '',\n organizationId,\n userId: String(auth.sub),\n resourceKind: RESOURCE_KIND,\n resourceId: parsed.data.userId,\n operation: 'update',\n requestMethod: req.method,\n requestHeaders: req.headers,\n mutationPayload: { userId: parsed.data.userId, mode: parsed.data.mode, widgetIds },\n })\n if (guardResult && !guardResult.ok) {\n return NextResponse.json(guardResult.body, { status: guardResult.status })\n }\n\n let record = await em.findOne(DashboardUserWidgets, {\n userId: parsed.data.userId,\n tenantId,\n organizationId,\n deletedAt: null,\n })\n\n if (parsed.data.mode === 'inherit') {\n if (record) {\n await em.remove(record).flush()\n }\n if (guardResult?.ok && guardResult.shouldRunAfterSuccess) {\n await runCrudMutationGuardAfterSuccess(container, {\n tenantId: tenantId ?? '',\n organizationId,\n userId: String(auth.sub),\n resourceKind: RESOURCE_KIND,\n resourceId: parsed.data.userId,\n operation: 'update',\n requestMethod: req.method,\n requestHeaders: req.headers,\n metadata: guardResult.metadata ?? null,\n })\n }\n return NextResponse.json({ ok: true, mode: 'inherit', widgetIds: [] })\n }\n\n if (!record) {\n record = em.create(DashboardUserWidgets, {\n userId: parsed.data.userId,\n tenantId,\n organizationId,\n mode: 'override',\n widgetIdsJson: widgetIds,\n })\n em.persist(record)\n } else {\n record.mode = 'override'\n record.widgetIdsJson = widgetIds\n }\n await em.flush()\n\n if (guardResult?.ok && guardResult.shouldRunAfterSuccess) {\n await runCrudMutationGuardAfterSuccess(container, {\n tenantId: tenantId ?? '',\n organizationId,\n userId: String(auth.sub),\n resourceKind: RESOURCE_KIND,\n resourceId: parsed.data.userId,\n operation: 'update',\n requestMethod: req.method,\n requestHeaders: req.headers,\n metadata: guardResult.metadata ?? null,\n })\n }\n\n return NextResponse.json({ ok: true, mode: 'override', widgetIds })\n}\n\nconst userWidgetsQuerySchema = z.object({\n userId: z.string().uuid(),\n tenantId: z.string().uuid().optional(),\n organizationId: z.string().uuid().optional(),\n})\n\nconst userWidgetsGetDoc: OpenApiMethodDoc = {\n summary: 'Read widget overrides for a user',\n description: 'Returns the widgets inherited and explicitly configured for the requested user within the current scope.',\n tags: [dashboardsTag],\n query: userWidgetsQuerySchema,\n responses: [\n { status: 200, description: 'Widget settings for the user.', schema: dashboardUserWidgetsResponseSchema },\n ],\n errors: [\n { status: 400, description: 'Missing user identifier', schema: dashboardsErrorSchema },\n { status: 401, description: 'Authentication required', schema: dashboardsErrorSchema },\n { status: 403, description: 'Insufficient permissions to manage user widgets', schema: dashboardsErrorSchema },\n ],\n}\n\nconst userWidgetsPutDoc: OpenApiMethodDoc = {\n summary: 'Update user-specific dashboard widgets',\n description: 'Sets the widget override mode and allowed widgets for a user. Passing `mode: inherit` clears overrides.',\n tags: [dashboardsTag],\n requestBody: {\n contentType: 'application/json',\n schema: userWidgetSettingsSchema,\n description: 'User identifier, override mode, and widget ids. Scope is derived from the authenticated session.',\n },\n responses: [\n { status: 200, description: 'Overrides saved.', schema: dashboardUserWidgetsUpdateResponseSchema },\n ],\n errors: [\n { status: 400, description: 'Invalid payload or unknown widgets', schema: dashboardsErrorSchema },\n { status: 401, description: 'Authentication required', schema: dashboardsErrorSchema },\n { status: 403, description: 'Insufficient permissions to manage user widgets', schema: dashboardsErrorSchema },\n ],\n}\n\nexport const openApi: OpenApiRouteDoc = {\n tag: dashboardsTag,\n summary: 'Manage user-level dashboard widgets',\n methods: {\n GET: userWidgetsGetDoc,\n PUT: userWidgetsPutDoc,\n },\n}\n"],
|
|
5
|
+
"mappings": "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",
|
|
6
6
|
"names": []
|
|
7
7
|
}
|
|
@@ -6,6 +6,7 @@ import { createRequestContainer } from "@open-mercato/shared/lib/di/container";
|
|
|
6
6
|
import { Tenant } from "@open-mercato/core/modules/directory/data/entities";
|
|
7
7
|
import { tenantCreateSchema, tenantUpdateSchema } from "@open-mercato/core/modules/directory/data/validators";
|
|
8
8
|
import { loadCustomFieldValues, buildCustomFieldFiltersFromQuery } from "@open-mercato/shared/lib/crud/custom-fields";
|
|
9
|
+
import { resolveRecordIdsForCustomFieldFilters } from "./tenant-cf-filter.js";
|
|
9
10
|
import { E } from "../../../../generated/entities.ids.generated.js";
|
|
10
11
|
import { tenantCrudEvents, tenantCrudIndexer } from "@open-mercato/core/modules/directory/commands/tenants";
|
|
11
12
|
import { directoryTag, directoryErrorSchema, directoryOkSchema, tenantListResponseSchema } from "../openapi.js";
|
|
@@ -67,10 +68,6 @@ const toRow = (tenant, cf) => ({
|
|
|
67
68
|
updatedAt: tenant.updatedAt ? tenant.updatedAt.toISOString() : null,
|
|
68
69
|
...cf
|
|
69
70
|
});
|
|
70
|
-
const matchesValue = (current, expected) => {
|
|
71
|
-
if (Array.isArray(current)) return current.some((item) => item === expected);
|
|
72
|
-
return current === expected;
|
|
73
|
-
};
|
|
74
71
|
async function GET(req) {
|
|
75
72
|
const auth = await getAuthFromRequest(req);
|
|
76
73
|
if (!auth) {
|
|
@@ -149,26 +146,25 @@ async function GET(req) {
|
|
|
149
146
|
let total;
|
|
150
147
|
let cfValues;
|
|
151
148
|
if (cfFilterEntries.length) {
|
|
152
|
-
const
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
return cfFilterEntries.every(([key, expected]) => {
|
|
158
|
-
const value = payload[`cf_${key}`];
|
|
159
|
-
if (expected && typeof expected === "object" && !Array.isArray(expected)) {
|
|
160
|
-
const maybeIn = expected.$in;
|
|
161
|
-
if (Array.isArray(maybeIn)) {
|
|
162
|
-
if (value === void 0 || value === null) return false;
|
|
163
|
-
if (Array.isArray(value)) return value.some((val) => maybeIn.includes(val));
|
|
164
|
-
return maybeIn.includes(value);
|
|
165
|
-
}
|
|
166
|
-
}
|
|
167
|
-
return matchesValue(value, expected);
|
|
168
|
-
});
|
|
149
|
+
const matchedIds = await resolveRecordIdsForCustomFieldFilters({
|
|
150
|
+
em,
|
|
151
|
+
entityId: E.directory.tenant,
|
|
152
|
+
tenantId: auth.tenantId ?? null,
|
|
153
|
+
filters: cfFilterEntries
|
|
169
154
|
});
|
|
170
|
-
|
|
171
|
-
|
|
155
|
+
const existingId = where.id;
|
|
156
|
+
const idFilter = typeof existingId === "string" ? matchedIds.has(existingId) ? [existingId] : [] : Array.from(matchedIds);
|
|
157
|
+
if (!idFilter.length) {
|
|
158
|
+
total = 0;
|
|
159
|
+
pageTenants = [];
|
|
160
|
+
cfValues = {};
|
|
161
|
+
} else {
|
|
162
|
+
where.id = { $in: idFilter };
|
|
163
|
+
const [rows, count] = await em.findAndCount(Tenant, where, { orderBy, limit: pageSize, offset: start });
|
|
164
|
+
total = count;
|
|
165
|
+
pageTenants = rows;
|
|
166
|
+
cfValues = await loadCfValues(rows);
|
|
167
|
+
}
|
|
172
168
|
} else {
|
|
173
169
|
const [rows, count] = await em.findAndCount(Tenant, where, { orderBy, limit: pageSize, offset: start });
|
|
174
170
|
total = count;
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"version": 3,
|
|
3
3
|
"sources": ["../../../../../src/modules/directory/api/tenants/route.ts"],
|
|
4
|
-
"sourcesContent": ["import { NextResponse } from 'next/server'\nimport { z } from 'zod'\nimport { logCrudAccess, makeCrudRoute } from '@open-mercato/shared/lib/crud/factory'\nimport { getAuthFromRequest } from '@open-mercato/shared/lib/auth/server'\nimport { createRequestContainer } from '@open-mercato/shared/lib/di/container'\nimport { Tenant } from '@open-mercato/core/modules/directory/data/entities'\nimport { tenantCreateSchema, tenantUpdateSchema } from '@open-mercato/core/modules/directory/data/validators'\nimport { loadCustomFieldValues, buildCustomFieldFiltersFromQuery } from '@open-mercato/shared/lib/crud/custom-fields'\nimport { E } from '#generated/entities.ids.generated'\nimport type { DataEngine } from '@open-mercato/shared/lib/data/engine'\nimport type { EntityManager } from '@mikro-orm/postgresql'\nimport type { FilterQuery } from '@mikro-orm/core'\nimport { tenantCrudEvents, tenantCrudIndexer } from '@open-mercato/core/modules/directory/commands/tenants'\nimport type { OpenApiMethodDoc, OpenApiRouteDoc } from '@open-mercato/shared/lib/openapi'\nimport { directoryTag, directoryErrorSchema, directoryOkSchema, tenantListResponseSchema } from '../openapi'\nimport { escapeLikePattern } from '@open-mercato/shared/lib/db/escapeLikePattern'\nimport { parseBooleanToken } from '@open-mercato/shared/lib/boolean'\n\nconst listQuerySchema = z.object({\n id: z.string().uuid().optional(),\n page: z.coerce.number().min(1).default(1),\n pageSize: z.coerce.number().min(1).max(100).default(50),\n search: z.string().optional(),\n sortField: z.enum(['name', 'createdAt', 'updatedAt']).optional(),\n sortDir: z.enum(['asc', 'desc']).optional(),\n isActive: z.enum(['true', 'false']).optional(),\n}).passthrough()\n\ntype TenantRow = {\n id: string\n name: string\n isActive: boolean\n createdAt: string | null\n updatedAt: string | null\n} & Record<string, unknown>\n\nconst routeMetadata = {\n GET: { requireAuth: true, requireFeatures: ['directory.tenants.view'] },\n POST: { requireAuth: true, requireFeatures: ['directory.tenants.manage'] },\n PUT: { requireAuth: true, requireFeatures: ['directory.tenants.manage'] },\n DELETE: { requireAuth: true, requireFeatures: ['directory.tenants.manage'] },\n}\n\nexport const metadata = routeMetadata\n\nconst rawBodySchema = z.object({}).passthrough()\ntype CrudInput = Record<string, unknown>\n\nconst crud = makeCrudRoute<CrudInput, CrudInput, Record<string, unknown>>({\n metadata: routeMetadata,\n orm: {\n entity: Tenant,\n idField: 'id',\n orgField: null,\n tenantField: null,\n softDeleteField: 'deletedAt',\n },\n events: tenantCrudEvents,\n indexer: tenantCrudIndexer,\n actions: {\n create: {\n commandId: 'directory.tenants.create',\n schema: rawBodySchema,\n mapInput: ({ parsed }) => parsed,\n response: ({ result }) => ({ id: String(result.id) }),\n status: 201,\n },\n update: {\n commandId: 'directory.tenants.update',\n schema: rawBodySchema,\n mapInput: ({ parsed }) => parsed,\n response: () => ({ ok: true }),\n },\n delete: {\n commandId: 'directory.tenants.delete',\n response: () => ({ ok: true }),\n },\n },\n})\n\nconst toRow = (tenant: Tenant, cf: Record<string, unknown>): TenantRow => ({\n id: String(tenant.id),\n name: String(tenant.name),\n isActive: !!tenant.isActive,\n createdAt: tenant.createdAt ? tenant.createdAt.toISOString() : null,\n updatedAt: tenant.updatedAt ? tenant.updatedAt.toISOString() : null,\n ...cf,\n})\n\nconst matchesValue = (current: unknown, expected: unknown): boolean => {\n if (Array.isArray(current)) return current.some((item) => item === expected)\n return current === expected\n}\n\nexport async function GET(req: Request) {\n const auth = await getAuthFromRequest(req)\n if (!auth) {\n return NextResponse.json({ items: [], total: 0, page: 1, pageSize: 50, totalPages: 1 }, { status: 401 })\n }\n\n const url = new URL(req.url)\n const parsed = listQuerySchema.safeParse({\n id: url.searchParams.get('id') ?? undefined,\n page: url.searchParams.get('page') ?? undefined,\n pageSize: url.searchParams.get('pageSize') ?? undefined,\n search: url.searchParams.get('search') ?? undefined,\n sortField: url.searchParams.get('sortField') ?? undefined,\n sortDir: url.searchParams.get('sortDir') ?? undefined,\n isActive: url.searchParams.get('isActive') ?? undefined,\n })\n if (!parsed.success) {\n return NextResponse.json(\n { error: 'Invalid query parameters', details: parsed.error.flatten() },\n { status: 400 },\n )\n }\n\n const container = await createRequestContainer()\n const em = container.resolve('em') as EntityManager\n\n const { id, page, pageSize, search, sortField, sortDir, isActive } = parsed.data\n const where: FilterQuery<Tenant> = { deletedAt: null }\n if (id) where.id = id\n if (search) {\n Object.assign(where, { name: { $ilike: `%${escapeLikePattern(search)}%` } })\n }\n const active = parseBooleanToken(isActive)\n if (active !== null) where.isActive = active\n\n const fieldMap: Record<string, string> = { name: 'name', createdAt: 'createdAt', updatedAt: 'updatedAt' }\n const orderBy: Record<string, 'ASC' | 'DESC'> = {}\n if (sortField) {\n const mapped = fieldMap[sortField] || 'name'\n orderBy[mapped] = sortDir === 'desc' ? 'DESC' : 'ASC'\n } else {\n orderBy.name = 'ASC'\n }\n\n const rawQuery = Array.from(url.searchParams.keys()).reduce<Record<string, unknown>>((acc, key) => {\n const values = url.searchParams.getAll(key)\n if (!values.length) return acc\n acc[key] = values.length === 1 ? values[0] : values\n return acc\n }, {})\n\n const cfFilters = await buildCustomFieldFiltersFromQuery({\n entityId: E.directory.tenant,\n query: rawQuery,\n em,\n tenantId: auth.tenantId ?? null,\n })\n const cfFilterEntries = Object.entries(cfFilters).map(([rawKey, condition]) => {\n const normalizedKey = rawKey.startsWith('cf:') ? rawKey.slice(3) : rawKey.replace(/^cf_/, '')\n return [normalizedKey, condition] as const\n })\n\n const loadCfValues = (tenants: Tenant[]): Promise<Record<string, Record<string, unknown>>> => {\n const recordIds = tenants.map((tenant) => String(tenant.id))\n if (!recordIds.length) return Promise.resolve({})\n const tenantIdByRecord: Record<string, string | null> = {}\n const organizationIdByRecord: Record<string, string | null> = {}\n for (const rid of recordIds) {\n tenantIdByRecord[rid] = null\n organizationIdByRecord[rid] = null\n }\n return loadCustomFieldValues({\n em,\n entityId: E.directory.tenant,\n recordIds,\n tenantIdByRecord,\n organizationIdByRecord,\n tenantFallbacks: auth.tenantId ? [auth.tenantId] : [],\n })\n }\n\n const start = (page - 1) * pageSize\n\n let pageTenants: Tenant[]\n let total: number\n let cfValues: Record<string, Record<string, unknown>>\n\n if (cfFilterEntries.length) {\n // Custom-field filters are matched in JS against separately-loaded values,\n // so the full result set must be fetched before filtering and paging.\n const all = await em.find(Tenant, where, { orderBy })\n cfValues = await loadCfValues(all)\n const filtered = all.filter((tenant) => {\n const rid = String(tenant.id)\n const payload = cfValues[rid] ?? {}\n return cfFilterEntries.every(([key, expected]) => {\n const value = payload[`cf_${key}`]\n if (expected && typeof expected === 'object' && !Array.isArray(expected)) {\n const maybeIn = (expected as { $in?: unknown[] }).$in\n if (Array.isArray(maybeIn)) {\n if (value === undefined || value === null) return false\n if (Array.isArray(value)) return value.some((val) => maybeIn.includes(val))\n return maybeIn.includes(value)\n }\n }\n return matchesValue(value, expected)\n })\n })\n total = filtered.length\n pageTenants = filtered.slice(start, start + pageSize)\n } else {\n // No JS-side filtering applies, so pagination is pushed to the database\n // instead of fetching the whole table and slicing in memory.\n const [rows, count] = await em.findAndCount(Tenant, where, { orderBy, limit: pageSize, offset: start })\n total = count\n pageTenants = rows\n cfValues = await loadCfValues(rows)\n }\n\n const items = pageTenants.map((tenant) => {\n const rid = String(tenant.id)\n const cf = cfValues[rid] ?? {}\n return toRow(tenant, cf)\n })\n const totalPages = Math.max(1, Math.ceil(total / pageSize))\n\n await logCrudAccess({\n container,\n auth,\n request: req,\n items,\n idField: 'id',\n resourceKind: 'directory.tenant',\n organizationId: null,\n tenantId: auth.tenantId ?? null,\n query: rawQuery,\n accessType: id ? 'read:item' : undefined,\n })\n\n return NextResponse.json({ items, total, page, pageSize, totalPages })\n}\n\nexport const POST = crud.POST\nexport const PUT = crud.PUT\nexport const DELETE = crud.DELETE\n\nconst tenantCreateResponseSchema = z.object({\n id: z.string().uuid(),\n})\n\nconst tenantDeleteRequestSchema = z.object({\n id: z.string().uuid(),\n})\n\nconst tenantGetDoc: OpenApiMethodDoc = {\n summary: 'List tenants',\n description: 'Returns tenants visible to the current user with optional search and pagination.',\n tags: [directoryTag],\n query: listQuerySchema,\n responses: [\n { status: 200, description: 'Paged list of tenants.', schema: tenantListResponseSchema },\n ],\n errors: [\n { status: 400, description: 'Invalid query parameters', schema: directoryErrorSchema },\n { status: 401, description: 'Authentication required', schema: directoryErrorSchema },\n ],\n}\n\nconst tenantPostDoc: OpenApiMethodDoc = {\n summary: 'Create tenant',\n description: 'Creates a new tenant and returns its identifier.',\n tags: [directoryTag],\n requestBody: {\n contentType: 'application/json',\n schema: tenantCreateSchema,\n description: 'Tenant name and optional activation flag.',\n },\n responses: [\n { status: 201, description: 'Tenant created.', schema: tenantCreateResponseSchema },\n ],\n errors: [\n { status: 400, description: 'Validation failed', schema: directoryErrorSchema },\n { status: 401, description: 'Authentication required', schema: directoryErrorSchema },\n { status: 403, description: 'Missing directory.tenants.manage feature', schema: directoryErrorSchema },\n ],\n}\n\nconst tenantPutDoc: OpenApiMethodDoc = {\n summary: 'Update tenant',\n description: 'Updates tenant properties such as name or activation state.',\n tags: [directoryTag],\n requestBody: {\n contentType: 'application/json',\n schema: tenantUpdateSchema,\n description: 'Tenant identifier with fields to update.',\n },\n responses: [\n { status: 200, description: 'Tenant updated.', schema: directoryOkSchema },\n ],\n errors: [\n { status: 400, description: 'Validation failed', schema: directoryErrorSchema },\n { status: 401, description: 'Authentication required', schema: directoryErrorSchema },\n { status: 403, description: 'Missing directory.tenants.manage feature', schema: directoryErrorSchema },\n ],\n}\n\nconst tenantDeleteDoc: OpenApiMethodDoc = {\n summary: 'Delete tenant',\n description: 'Soft deletes the tenant identified by id.',\n tags: [directoryTag],\n requestBody: {\n contentType: 'application/json',\n schema: tenantDeleteRequestSchema,\n description: 'Identifier of the tenant to remove.',\n },\n responses: [\n { status: 200, description: 'Tenant removed.', schema: directoryOkSchema },\n ],\n errors: [\n { status: 400, description: 'Validation failed', schema: directoryErrorSchema },\n { status: 401, description: 'Authentication required', schema: directoryErrorSchema },\n { status: 403, description: 'Missing directory.tenants.manage feature', schema: directoryErrorSchema },\n ],\n}\n\nexport const openApi: OpenApiRouteDoc = {\n tag: directoryTag,\n summary: 'Manage tenants',\n methods: {\n GET: tenantGetDoc,\n POST: tenantPostDoc,\n PUT: tenantPutDoc,\n DELETE: tenantDeleteDoc,\n },\n}\n"],
|
|
5
|
-
"mappings": "AAAA,SAAS,oBAAoB;AAC7B,SAAS,SAAS;AAClB,SAAS,eAAe,qBAAqB;AAC7C,SAAS,0BAA0B;AACnC,SAAS,8BAA8B;AACvC,SAAS,cAAc;AACvB,SAAS,oBAAoB,0BAA0B;AACvD,SAAS,uBAAuB,wCAAwC;AACxE,SAAS,SAAS;AAIlB,SAAS,kBAAkB,yBAAyB;AAEpD,SAAS,cAAc,sBAAsB,mBAAmB,gCAAgC;AAChG,SAAS,yBAAyB;AAClC,SAAS,yBAAyB;AAElC,MAAM,kBAAkB,EAAE,OAAO;AAAA,EAC/B,IAAI,EAAE,OAAO,EAAE,KAAK,EAAE,SAAS;AAAA,EAC/B,MAAM,EAAE,OAAO,OAAO,EAAE,IAAI,CAAC,EAAE,QAAQ,CAAC;AAAA,EACxC,UAAU,EAAE,OAAO,OAAO,EAAE,IAAI,CAAC,EAAE,IAAI,GAAG,EAAE,QAAQ,EAAE;AAAA,EACtD,QAAQ,EAAE,OAAO,EAAE,SAAS;AAAA,EAC5B,WAAW,EAAE,KAAK,CAAC,QAAQ,aAAa,WAAW,CAAC,EAAE,SAAS;AAAA,EAC/D,SAAS,EAAE,KAAK,CAAC,OAAO,MAAM,CAAC,EAAE,SAAS;AAAA,EAC1C,UAAU,EAAE,KAAK,CAAC,QAAQ,OAAO,CAAC,EAAE,SAAS;AAC/C,CAAC,EAAE,YAAY;AAUf,MAAM,gBAAgB;AAAA,EACpB,KAAK,EAAE,aAAa,MAAM,iBAAiB,CAAC,wBAAwB,EAAE;AAAA,EACtE,MAAM,EAAE,aAAa,MAAM,iBAAiB,CAAC,0BAA0B,EAAE;AAAA,EACzE,KAAK,EAAE,aAAa,MAAM,iBAAiB,CAAC,0BAA0B,EAAE;AAAA,EACxE,QAAQ,EAAE,aAAa,MAAM,iBAAiB,CAAC,0BAA0B,EAAE;AAC7E;AAEO,MAAM,WAAW;AAExB,MAAM,gBAAgB,EAAE,OAAO,CAAC,CAAC,EAAE,YAAY;AAG/C,MAAM,OAAO,cAA6D;AAAA,EACxE,UAAU;AAAA,EACV,KAAK;AAAA,IACH,QAAQ;AAAA,IACR,SAAS;AAAA,IACT,UAAU;AAAA,IACV,aAAa;AAAA,IACb,iBAAiB;AAAA,EACnB;AAAA,EACA,QAAQ;AAAA,EACR,SAAS;AAAA,EACT,SAAS;AAAA,IACP,QAAQ;AAAA,MACN,WAAW;AAAA,MACX,QAAQ;AAAA,MACR,UAAU,CAAC,EAAE,OAAO,MAAM;AAAA,MAC1B,UAAU,CAAC,EAAE,OAAO,OAAO,EAAE,IAAI,OAAO,OAAO,EAAE,EAAE;AAAA,MACnD,QAAQ;AAAA,IACV;AAAA,IACA,QAAQ;AAAA,MACN,WAAW;AAAA,MACX,QAAQ;AAAA,MACR,UAAU,CAAC,EAAE,OAAO,MAAM;AAAA,MAC1B,UAAU,OAAO,EAAE,IAAI,KAAK;AAAA,IAC9B;AAAA,IACA,QAAQ;AAAA,MACN,WAAW;AAAA,MACX,UAAU,OAAO,EAAE,IAAI,KAAK;AAAA,IAC9B;AAAA,EACF;AACF,CAAC;AAED,MAAM,QAAQ,CAAC,QAAgB,QAA4C;AAAA,EACzE,IAAI,OAAO,OAAO,EAAE;AAAA,EACpB,MAAM,OAAO,OAAO,IAAI;AAAA,EACxB,UAAU,CAAC,CAAC,OAAO;AAAA,EACnB,WAAW,OAAO,YAAY,OAAO,UAAU,YAAY,IAAI;AAAA,EAC/D,WAAW,OAAO,YAAY,OAAO,UAAU,YAAY,IAAI;AAAA,EAC/D,GAAG;AACL;AAEA,
|
|
4
|
+
"sourcesContent": ["import { NextResponse } from 'next/server'\nimport { z } from 'zod'\nimport { logCrudAccess, makeCrudRoute } from '@open-mercato/shared/lib/crud/factory'\nimport { getAuthFromRequest } from '@open-mercato/shared/lib/auth/server'\nimport { createRequestContainer } from '@open-mercato/shared/lib/di/container'\nimport { Tenant } from '@open-mercato/core/modules/directory/data/entities'\nimport { tenantCreateSchema, tenantUpdateSchema } from '@open-mercato/core/modules/directory/data/validators'\nimport { loadCustomFieldValues, buildCustomFieldFiltersFromQuery } from '@open-mercato/shared/lib/crud/custom-fields'\nimport { resolveRecordIdsForCustomFieldFilters } from './tenant-cf-filter'\nimport { E } from '#generated/entities.ids.generated'\nimport type { DataEngine } from '@open-mercato/shared/lib/data/engine'\nimport type { EntityManager } from '@mikro-orm/postgresql'\nimport type { FilterQuery } from '@mikro-orm/core'\nimport { tenantCrudEvents, tenantCrudIndexer } from '@open-mercato/core/modules/directory/commands/tenants'\nimport type { OpenApiMethodDoc, OpenApiRouteDoc } from '@open-mercato/shared/lib/openapi'\nimport { directoryTag, directoryErrorSchema, directoryOkSchema, tenantListResponseSchema } from '../openapi'\nimport { escapeLikePattern } from '@open-mercato/shared/lib/db/escapeLikePattern'\nimport { parseBooleanToken } from '@open-mercato/shared/lib/boolean'\n\nconst listQuerySchema = z.object({\n id: z.string().uuid().optional(),\n page: z.coerce.number().min(1).default(1),\n pageSize: z.coerce.number().min(1).max(100).default(50),\n search: z.string().optional(),\n sortField: z.enum(['name', 'createdAt', 'updatedAt']).optional(),\n sortDir: z.enum(['asc', 'desc']).optional(),\n isActive: z.enum(['true', 'false']).optional(),\n}).passthrough()\n\ntype TenantRow = {\n id: string\n name: string\n isActive: boolean\n createdAt: string | null\n updatedAt: string | null\n} & Record<string, unknown>\n\nconst routeMetadata = {\n GET: { requireAuth: true, requireFeatures: ['directory.tenants.view'] },\n POST: { requireAuth: true, requireFeatures: ['directory.tenants.manage'] },\n PUT: { requireAuth: true, requireFeatures: ['directory.tenants.manage'] },\n DELETE: { requireAuth: true, requireFeatures: ['directory.tenants.manage'] },\n}\n\nexport const metadata = routeMetadata\n\nconst rawBodySchema = z.object({}).passthrough()\ntype CrudInput = Record<string, unknown>\n\nconst crud = makeCrudRoute<CrudInput, CrudInput, Record<string, unknown>>({\n metadata: routeMetadata,\n orm: {\n entity: Tenant,\n idField: 'id',\n orgField: null,\n tenantField: null,\n softDeleteField: 'deletedAt',\n },\n events: tenantCrudEvents,\n indexer: tenantCrudIndexer,\n actions: {\n create: {\n commandId: 'directory.tenants.create',\n schema: rawBodySchema,\n mapInput: ({ parsed }) => parsed,\n response: ({ result }) => ({ id: String(result.id) }),\n status: 201,\n },\n update: {\n commandId: 'directory.tenants.update',\n schema: rawBodySchema,\n mapInput: ({ parsed }) => parsed,\n response: () => ({ ok: true }),\n },\n delete: {\n commandId: 'directory.tenants.delete',\n response: () => ({ ok: true }),\n },\n },\n})\n\nconst toRow = (tenant: Tenant, cf: Record<string, unknown>): TenantRow => ({\n id: String(tenant.id),\n name: String(tenant.name),\n isActive: !!tenant.isActive,\n createdAt: tenant.createdAt ? tenant.createdAt.toISOString() : null,\n updatedAt: tenant.updatedAt ? tenant.updatedAt.toISOString() : null,\n ...cf,\n})\n\nexport async function GET(req: Request) {\n const auth = await getAuthFromRequest(req)\n if (!auth) {\n return NextResponse.json({ items: [], total: 0, page: 1, pageSize: 50, totalPages: 1 }, { status: 401 })\n }\n\n const url = new URL(req.url)\n const parsed = listQuerySchema.safeParse({\n id: url.searchParams.get('id') ?? undefined,\n page: url.searchParams.get('page') ?? undefined,\n pageSize: url.searchParams.get('pageSize') ?? undefined,\n search: url.searchParams.get('search') ?? undefined,\n sortField: url.searchParams.get('sortField') ?? undefined,\n sortDir: url.searchParams.get('sortDir') ?? undefined,\n isActive: url.searchParams.get('isActive') ?? undefined,\n })\n if (!parsed.success) {\n return NextResponse.json(\n { error: 'Invalid query parameters', details: parsed.error.flatten() },\n { status: 400 },\n )\n }\n\n const container = await createRequestContainer()\n const em = container.resolve('em') as EntityManager\n\n const { id, page, pageSize, search, sortField, sortDir, isActive } = parsed.data\n const where: FilterQuery<Tenant> = { deletedAt: null }\n if (id) where.id = id\n if (search) {\n Object.assign(where, { name: { $ilike: `%${escapeLikePattern(search)}%` } })\n }\n const active = parseBooleanToken(isActive)\n if (active !== null) where.isActive = active\n\n const fieldMap: Record<string, string> = { name: 'name', createdAt: 'createdAt', updatedAt: 'updatedAt' }\n const orderBy: Record<string, 'ASC' | 'DESC'> = {}\n if (sortField) {\n const mapped = fieldMap[sortField] || 'name'\n orderBy[mapped] = sortDir === 'desc' ? 'DESC' : 'ASC'\n } else {\n orderBy.name = 'ASC'\n }\n\n const rawQuery = Array.from(url.searchParams.keys()).reduce<Record<string, unknown>>((acc, key) => {\n const values = url.searchParams.getAll(key)\n if (!values.length) return acc\n acc[key] = values.length === 1 ? values[0] : values\n return acc\n }, {})\n\n const cfFilters = await buildCustomFieldFiltersFromQuery({\n entityId: E.directory.tenant,\n query: rawQuery,\n em,\n tenantId: auth.tenantId ?? null,\n })\n const cfFilterEntries = Object.entries(cfFilters).map(([rawKey, condition]) => {\n const normalizedKey = rawKey.startsWith('cf:') ? rawKey.slice(3) : rawKey.replace(/^cf_/, '')\n return [normalizedKey, condition] as const\n })\n\n const loadCfValues = (tenants: Tenant[]): Promise<Record<string, Record<string, unknown>>> => {\n const recordIds = tenants.map((tenant) => String(tenant.id))\n if (!recordIds.length) return Promise.resolve({})\n const tenantIdByRecord: Record<string, string | null> = {}\n const organizationIdByRecord: Record<string, string | null> = {}\n for (const rid of recordIds) {\n tenantIdByRecord[rid] = null\n organizationIdByRecord[rid] = null\n }\n return loadCustomFieldValues({\n em,\n entityId: E.directory.tenant,\n recordIds,\n tenantIdByRecord,\n organizationIdByRecord,\n tenantFallbacks: auth.tenantId ? [auth.tenantId] : [],\n })\n }\n\n const start = (page - 1) * pageSize\n\n let pageTenants: Tenant[]\n let total: number\n let cfValues: Record<string, Record<string, unknown>>\n\n if (cfFilterEntries.length) {\n // Custom-field values live in a separate store, so first resolve the bounded\n // set of matching tenant ids from that store, then push pagination to the\n // database via findAndCount instead of loading the whole tenant table.\n const matchedIds = await resolveRecordIdsForCustomFieldFilters({\n em,\n entityId: E.directory.tenant,\n tenantId: auth.tenantId ?? null,\n filters: cfFilterEntries,\n })\n const existingId = where.id\n const idFilter = typeof existingId === 'string'\n ? (matchedIds.has(existingId) ? [existingId] : [])\n : Array.from(matchedIds)\n\n if (!idFilter.length) {\n total = 0\n pageTenants = []\n cfValues = {}\n } else {\n where.id = { $in: idFilter }\n const [rows, count] = await em.findAndCount(Tenant, where, { orderBy, limit: pageSize, offset: start })\n total = count\n pageTenants = rows\n cfValues = await loadCfValues(rows)\n }\n } else {\n // No JS-side filtering applies, so pagination is pushed to the database\n // instead of fetching the whole table and slicing in memory.\n const [rows, count] = await em.findAndCount(Tenant, where, { orderBy, limit: pageSize, offset: start })\n total = count\n pageTenants = rows\n cfValues = await loadCfValues(rows)\n }\n\n const items = pageTenants.map((tenant) => {\n const rid = String(tenant.id)\n const cf = cfValues[rid] ?? {}\n return toRow(tenant, cf)\n })\n const totalPages = Math.max(1, Math.ceil(total / pageSize))\n\n await logCrudAccess({\n container,\n auth,\n request: req,\n items,\n idField: 'id',\n resourceKind: 'directory.tenant',\n organizationId: null,\n tenantId: auth.tenantId ?? null,\n query: rawQuery,\n accessType: id ? 'read:item' : undefined,\n })\n\n return NextResponse.json({ items, total, page, pageSize, totalPages })\n}\n\nexport const POST = crud.POST\nexport const PUT = crud.PUT\nexport const DELETE = crud.DELETE\n\nconst tenantCreateResponseSchema = z.object({\n id: z.string().uuid(),\n})\n\nconst tenantDeleteRequestSchema = z.object({\n id: z.string().uuid(),\n})\n\nconst tenantGetDoc: OpenApiMethodDoc = {\n summary: 'List tenants',\n description: 'Returns tenants visible to the current user with optional search and pagination.',\n tags: [directoryTag],\n query: listQuerySchema,\n responses: [\n { status: 200, description: 'Paged list of tenants.', schema: tenantListResponseSchema },\n ],\n errors: [\n { status: 400, description: 'Invalid query parameters', schema: directoryErrorSchema },\n { status: 401, description: 'Authentication required', schema: directoryErrorSchema },\n ],\n}\n\nconst tenantPostDoc: OpenApiMethodDoc = {\n summary: 'Create tenant',\n description: 'Creates a new tenant and returns its identifier.',\n tags: [directoryTag],\n requestBody: {\n contentType: 'application/json',\n schema: tenantCreateSchema,\n description: 'Tenant name and optional activation flag.',\n },\n responses: [\n { status: 201, description: 'Tenant created.', schema: tenantCreateResponseSchema },\n ],\n errors: [\n { status: 400, description: 'Validation failed', schema: directoryErrorSchema },\n { status: 401, description: 'Authentication required', schema: directoryErrorSchema },\n { status: 403, description: 'Missing directory.tenants.manage feature', schema: directoryErrorSchema },\n ],\n}\n\nconst tenantPutDoc: OpenApiMethodDoc = {\n summary: 'Update tenant',\n description: 'Updates tenant properties such as name or activation state.',\n tags: [directoryTag],\n requestBody: {\n contentType: 'application/json',\n schema: tenantUpdateSchema,\n description: 'Tenant identifier with fields to update.',\n },\n responses: [\n { status: 200, description: 'Tenant updated.', schema: directoryOkSchema },\n ],\n errors: [\n { status: 400, description: 'Validation failed', schema: directoryErrorSchema },\n { status: 401, description: 'Authentication required', schema: directoryErrorSchema },\n { status: 403, description: 'Missing directory.tenants.manage feature', schema: directoryErrorSchema },\n ],\n}\n\nconst tenantDeleteDoc: OpenApiMethodDoc = {\n summary: 'Delete tenant',\n description: 'Soft deletes the tenant identified by id.',\n tags: [directoryTag],\n requestBody: {\n contentType: 'application/json',\n schema: tenantDeleteRequestSchema,\n description: 'Identifier of the tenant to remove.',\n },\n responses: [\n { status: 200, description: 'Tenant removed.', schema: directoryOkSchema },\n ],\n errors: [\n { status: 400, description: 'Validation failed', schema: directoryErrorSchema },\n { status: 401, description: 'Authentication required', schema: directoryErrorSchema },\n { status: 403, description: 'Missing directory.tenants.manage feature', schema: directoryErrorSchema },\n ],\n}\n\nexport const openApi: OpenApiRouteDoc = {\n tag: directoryTag,\n summary: 'Manage tenants',\n methods: {\n GET: tenantGetDoc,\n POST: tenantPostDoc,\n PUT: tenantPutDoc,\n DELETE: tenantDeleteDoc,\n },\n}\n"],
|
|
5
|
+
"mappings": "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",
|
|
6
6
|
"names": []
|
|
7
7
|
}
|
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
import { CustomFieldDef, CustomFieldValue } from "@open-mercato/core/modules/entities/data/entities";
|
|
2
|
+
const valueColumnForKind = (kind) => {
|
|
3
|
+
switch (kind) {
|
|
4
|
+
case "integer":
|
|
5
|
+
return "valueInt";
|
|
6
|
+
case "float":
|
|
7
|
+
return "valueFloat";
|
|
8
|
+
case "boolean":
|
|
9
|
+
return "valueBool";
|
|
10
|
+
case "multiline":
|
|
11
|
+
return "valueMultiline";
|
|
12
|
+
default:
|
|
13
|
+
return "valueText";
|
|
14
|
+
}
|
|
15
|
+
};
|
|
16
|
+
const expectedValuesFor = (condition) => {
|
|
17
|
+
if (condition && typeof condition === "object" && !Array.isArray(condition)) {
|
|
18
|
+
const maybeIn = condition.$in;
|
|
19
|
+
if (Array.isArray(maybeIn)) return maybeIn;
|
|
20
|
+
}
|
|
21
|
+
return [condition];
|
|
22
|
+
};
|
|
23
|
+
const intersect = (current, next) => {
|
|
24
|
+
if (current === null) return next;
|
|
25
|
+
const result = /* @__PURE__ */ new Set();
|
|
26
|
+
for (const id of next) {
|
|
27
|
+
if (current.has(id)) result.add(id);
|
|
28
|
+
}
|
|
29
|
+
return result;
|
|
30
|
+
};
|
|
31
|
+
async function resolveRecordIdsForCustomFieldFilters(opts) {
|
|
32
|
+
const { em, entityId, tenantId, filters } = opts;
|
|
33
|
+
if (!filters.length) return /* @__PURE__ */ new Set();
|
|
34
|
+
const keys = Array.from(new Set(filters.map(([key]) => key)));
|
|
35
|
+
const tenantScope = tenantId ? { tenantId: { $in: [tenantId, null] } } : { tenantId: null };
|
|
36
|
+
const defs = await em.find(CustomFieldDef, {
|
|
37
|
+
entityId: String(entityId),
|
|
38
|
+
key: { $in: keys },
|
|
39
|
+
isActive: true,
|
|
40
|
+
deletedAt: null,
|
|
41
|
+
...tenantScope
|
|
42
|
+
});
|
|
43
|
+
const kindByKey = /* @__PURE__ */ new Map();
|
|
44
|
+
for (const def of defs) {
|
|
45
|
+
if (!kindByKey.has(def.key)) kindByKey.set(def.key, def.kind);
|
|
46
|
+
}
|
|
47
|
+
let matched = null;
|
|
48
|
+
for (const [key, condition] of filters) {
|
|
49
|
+
const column = valueColumnForKind(kindByKey.get(key));
|
|
50
|
+
const expectedValues = expectedValuesFor(condition).filter((value) => value !== void 0 && value !== null);
|
|
51
|
+
if (!expectedValues.length) return /* @__PURE__ */ new Set();
|
|
52
|
+
const rows = await em.find(
|
|
53
|
+
CustomFieldValue,
|
|
54
|
+
{
|
|
55
|
+
entityId: String(entityId),
|
|
56
|
+
fieldKey: key,
|
|
57
|
+
deletedAt: null,
|
|
58
|
+
[column]: { $in: expectedValues },
|
|
59
|
+
...tenantScope
|
|
60
|
+
},
|
|
61
|
+
{ fields: ["recordId"] }
|
|
62
|
+
);
|
|
63
|
+
const ids = new Set(rows.map((row) => String(row.recordId)));
|
|
64
|
+
matched = intersect(matched, ids);
|
|
65
|
+
if (matched.size === 0) return matched;
|
|
66
|
+
}
|
|
67
|
+
return matched ?? /* @__PURE__ */ new Set();
|
|
68
|
+
}
|
|
69
|
+
export {
|
|
70
|
+
resolveRecordIdsForCustomFieldFilters
|
|
71
|
+
};
|
|
72
|
+
//# sourceMappingURL=tenant-cf-filter.js.map
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
{
|
|
2
|
+
"version": 3,
|
|
3
|
+
"sources": ["../../../../../src/modules/directory/api/tenants/tenant-cf-filter.ts"],
|
|
4
|
+
"sourcesContent": ["import type { EntityManager } from '@mikro-orm/postgresql'\nimport type { FilterQuery } from '@mikro-orm/core'\nimport type { EntityId } from '@open-mercato/shared/modules/entities'\nimport { CustomFieldDef, CustomFieldValue } from '@open-mercato/core/modules/entities/data/entities'\n\nexport type CustomFieldFilterEntry = readonly [string, unknown]\n\ntype ValueColumn = 'valueText' | 'valueMultiline' | 'valueInt' | 'valueFloat' | 'valueBool'\n\nconst valueColumnForKind = (kind: string | null | undefined): ValueColumn => {\n switch (kind) {\n case 'integer':\n return 'valueInt'\n case 'float':\n return 'valueFloat'\n case 'boolean':\n return 'valueBool'\n case 'multiline':\n return 'valueMultiline'\n default:\n return 'valueText'\n }\n}\n\nconst expectedValuesFor = (condition: unknown): unknown[] => {\n if (condition && typeof condition === 'object' && !Array.isArray(condition)) {\n const maybeIn = (condition as { $in?: unknown[] }).$in\n if (Array.isArray(maybeIn)) return maybeIn\n }\n return [condition]\n}\n\nconst intersect = (current: Set<string> | null, next: Set<string>): Set<string> => {\n if (current === null) return next\n const result = new Set<string>()\n for (const id of next) {\n if (current.has(id)) result.add(id)\n }\n return result\n}\n\n/**\n * Resolve the bounded set of record ids that satisfy the given custom-field\n * filters by querying the custom-field value store directly, instead of loading\n * every entity row and matching the values in memory. Filters are combined with\n * AND semantics. Returns an empty set when any filter matches nothing.\n */\nexport async function resolveRecordIdsForCustomFieldFilters(opts: {\n em: EntityManager\n entityId: EntityId\n tenantId: string | null\n filters: CustomFieldFilterEntry[]\n}): Promise<Set<string>> {\n const { em, entityId, tenantId, filters } = opts\n if (!filters.length) return new Set<string>()\n\n const keys = Array.from(new Set(filters.map(([key]) => key)))\n const tenantScope: Record<string, unknown> = tenantId\n ? { tenantId: { $in: [tenantId, null] } }\n : { tenantId: null }\n\n const defs = await em.find(CustomFieldDef, {\n entityId: String(entityId),\n key: { $in: keys },\n isActive: true,\n deletedAt: null,\n ...tenantScope,\n } as FilterQuery<CustomFieldDef>)\n const kindByKey = new Map<string, string>()\n for (const def of defs) {\n if (!kindByKey.has(def.key)) kindByKey.set(def.key, def.kind)\n }\n\n let matched: Set<string> | null = null\n for (const [key, condition] of filters) {\n const column = valueColumnForKind(kindByKey.get(key))\n const expectedValues = expectedValuesFor(condition).filter((value) => value !== undefined && value !== null)\n if (!expectedValues.length) return new Set<string>()\n\n const rows = await em.find(\n CustomFieldValue,\n {\n entityId: String(entityId),\n fieldKey: key,\n deletedAt: null,\n [column]: { $in: expectedValues },\n ...tenantScope,\n } as FilterQuery<CustomFieldValue>,\n { fields: ['recordId'] },\n )\n const ids = new Set<string>(rows.map((row) => String(row.recordId)))\n matched = intersect(matched, ids)\n if (matched.size === 0) return matched\n }\n\n return matched ?? new Set<string>()\n}\n"],
|
|
5
|
+
"mappings": "AAGA,SAAS,gBAAgB,wBAAwB;AAMjD,MAAM,qBAAqB,CAAC,SAAiD;AAC3E,UAAQ,MAAM;AAAA,IACZ,KAAK;AACH,aAAO;AAAA,IACT,KAAK;AACH,aAAO;AAAA,IACT,KAAK;AACH,aAAO;AAAA,IACT,KAAK;AACH,aAAO;AAAA,IACT;AACE,aAAO;AAAA,EACX;AACF;AAEA,MAAM,oBAAoB,CAAC,cAAkC;AAC3D,MAAI,aAAa,OAAO,cAAc,YAAY,CAAC,MAAM,QAAQ,SAAS,GAAG;AAC3E,UAAM,UAAW,UAAkC;AACnD,QAAI,MAAM,QAAQ,OAAO,EAAG,QAAO;AAAA,EACrC;AACA,SAAO,CAAC,SAAS;AACnB;AAEA,MAAM,YAAY,CAAC,SAA6B,SAAmC;AACjF,MAAI,YAAY,KAAM,QAAO;AAC7B,QAAM,SAAS,oBAAI,IAAY;AAC/B,aAAW,MAAM,MAAM;AACrB,QAAI,QAAQ,IAAI,EAAE,EAAG,QAAO,IAAI,EAAE;AAAA,EACpC;AACA,SAAO;AACT;AAQA,eAAsB,sCAAsC,MAKnC;AACvB,QAAM,EAAE,IAAI,UAAU,UAAU,QAAQ,IAAI;AAC5C,MAAI,CAAC,QAAQ,OAAQ,QAAO,oBAAI,IAAY;AAE5C,QAAM,OAAO,MAAM,KAAK,IAAI,IAAI,QAAQ,IAAI,CAAC,CAAC,GAAG,MAAM,GAAG,CAAC,CAAC;AAC5D,QAAM,cAAuC,WACzC,EAAE,UAAU,EAAE,KAAK,CAAC,UAAU,IAAI,EAAE,EAAE,IACtC,EAAE,UAAU,KAAK;AAErB,QAAM,OAAO,MAAM,GAAG,KAAK,gBAAgB;AAAA,IACzC,UAAU,OAAO,QAAQ;AAAA,IACzB,KAAK,EAAE,KAAK,KAAK;AAAA,IACjB,UAAU;AAAA,IACV,WAAW;AAAA,IACX,GAAG;AAAA,EACL,CAAgC;AAChC,QAAM,YAAY,oBAAI,IAAoB;AAC1C,aAAW,OAAO,MAAM;AACtB,QAAI,CAAC,UAAU,IAAI,IAAI,GAAG,EAAG,WAAU,IAAI,IAAI,KAAK,IAAI,IAAI;AAAA,EAC9D;AAEA,MAAI,UAA8B;AAClC,aAAW,CAAC,KAAK,SAAS,KAAK,SAAS;AACtC,UAAM,SAAS,mBAAmB,UAAU,IAAI,GAAG,CAAC;AACpD,UAAM,iBAAiB,kBAAkB,SAAS,EAAE,OAAO,CAAC,UAAU,UAAU,UAAa,UAAU,IAAI;AAC3G,QAAI,CAAC,eAAe,OAAQ,QAAO,oBAAI,IAAY;AAEnD,UAAM,OAAO,MAAM,GAAG;AAAA,MACpB;AAAA,MACA;AAAA,QACE,UAAU,OAAO,QAAQ;AAAA,QACzB,UAAU;AAAA,QACV,WAAW;AAAA,QACX,CAAC,MAAM,GAAG,EAAE,KAAK,eAAe;AAAA,QAChC,GAAG;AAAA,MACL;AAAA,MACA,EAAE,QAAQ,CAAC,UAAU,EAAE;AAAA,IACzB;AACA,UAAM,MAAM,IAAI,IAAY,KAAK,IAAI,CAAC,QAAQ,OAAO,IAAI,QAAQ,CAAC,CAAC;AACnE,cAAU,UAAU,SAAS,GAAG;AAChC,QAAI,QAAQ,SAAS,EAAG,QAAO;AAAA,EACjC;AAEA,SAAO,WAAW,oBAAI,IAAY;AACpC;",
|
|
6
|
+
"names": []
|
|
7
|
+
}
|
|
@@ -2,18 +2,20 @@
|
|
|
2
2
|
import { jsx, jsxs } from "react/jsx-runtime";
|
|
3
3
|
import { useT } from "@open-mercato/shared/lib/i18n/context";
|
|
4
4
|
function DefaultValueDisplay({ featureToggleItem }) {
|
|
5
|
-
|
|
5
|
+
const t = useT();
|
|
6
|
+
const defaultValue = featureToggleItem?.defaultValue;
|
|
7
|
+
if (defaultValue === null || defaultValue === void 0) {
|
|
6
8
|
return /* @__PURE__ */ jsx("p", { className: "text-base text-muted-foreground", children: "-" });
|
|
7
9
|
}
|
|
8
|
-
switch (featureToggleItem
|
|
10
|
+
switch (featureToggleItem?.type) {
|
|
9
11
|
case "boolean":
|
|
10
|
-
return /* @__PURE__ */ jsx("p", { className: "text-base font-semibold text-foreground", children:
|
|
12
|
+
return /* @__PURE__ */ jsx("p", { className: "text-base font-semibold text-foreground", children: defaultValue ? t("feature_toggles.values.true", "True") : t("feature_toggles.values.false", "False") });
|
|
11
13
|
case "string":
|
|
12
|
-
return /* @__PURE__ */ jsx("p", { className: "text-base font-semibold text-foreground", children:
|
|
14
|
+
return /* @__PURE__ */ jsx("p", { className: "text-base font-semibold text-foreground", children: defaultValue });
|
|
13
15
|
case "number":
|
|
14
|
-
return /* @__PURE__ */ jsx("p", { className: "text-base font-semibold text-foreground", children:
|
|
16
|
+
return /* @__PURE__ */ jsx("p", { className: "text-base font-semibold text-foreground", children: defaultValue });
|
|
15
17
|
case "json":
|
|
16
|
-
return /* @__PURE__ */ jsx("div", { className: "text-base font-semibold text-foreground", children: /* @__PURE__ */ jsx("pre", { className: "bg-muted/30 p-2 rounded text-sm font-mono overflow-x-auto whitespace-pre-wrap", children: typeof
|
|
18
|
+
return /* @__PURE__ */ jsx("div", { className: "text-base font-semibold text-foreground", children: /* @__PURE__ */ jsx("pre", { className: "bg-muted/30 p-2 rounded text-sm font-mono overflow-x-auto whitespace-pre-wrap", children: typeof defaultValue === "object" ? JSON.stringify(defaultValue, null, 2) : defaultValue }) });
|
|
17
19
|
default:
|
|
18
20
|
return /* @__PURE__ */ jsx("p", { className: "text-base text-muted-foreground", children: "-" });
|
|
19
21
|
}
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"version": 3,
|
|
3
3
|
"sources": ["../../../../src/modules/feature_toggles/components/FeatureToggleDetailsCard.tsx"],
|
|
4
|
-
"sourcesContent": ["\"use client\"\nimport { useT } from '@open-mercato/shared/lib/i18n/context'\nimport { FeatureToggle } from '../data/validators'\n\ntype FeatureToggleDetailsCardProps = {\n featureToggleItem?: FeatureToggle\n}\n\nfunction DefaultValueDisplay({ featureToggleItem }: { featureToggleItem?: FeatureToggle }) {\n\n
|
|
5
|
-
"mappings": ";
|
|
4
|
+
"sourcesContent": ["\"use client\"\nimport { useT } from '@open-mercato/shared/lib/i18n/context'\nimport { FeatureToggle } from '../data/validators'\n\ntype FeatureToggleDetailsCardProps = {\n featureToggleItem?: FeatureToggle\n}\n\nfunction DefaultValueDisplay({ featureToggleItem }: { featureToggleItem?: FeatureToggle }) {\n const t = useT()\n\n const defaultValue = featureToggleItem?.defaultValue\n if (defaultValue === null || defaultValue === undefined) {\n return <p className=\"text-base text-muted-foreground\">-</p>\n }\n\n switch (featureToggleItem?.type) {\n case 'boolean':\n return (\n <p className=\"text-base font-semibold text-foreground\">\n {defaultValue ? t('feature_toggles.values.true', 'True') : t('feature_toggles.values.false', 'False')}\n </p>\n )\n\n case 'string':\n return (\n <p className=\"text-base font-semibold text-foreground\">\n {defaultValue}\n </p>\n )\n\n case 'number':\n return (\n <p className=\"text-base font-semibold text-foreground\">\n {defaultValue}\n </p>\n )\n\n case 'json':\n return (\n <div className=\"text-base font-semibold text-foreground\">\n <pre className=\"bg-muted/30 p-2 rounded text-sm font-mono overflow-x-auto whitespace-pre-wrap\">\n {typeof defaultValue === 'object'\n ? JSON.stringify(defaultValue, null, 2)\n : defaultValue}\n </pre>\n </div>\n )\n\n default:\n return <p className=\"text-base text-muted-foreground\">-</p>\n }\n}\n\nexport function FeatureToggleDetailsCard({ featureToggleItem }: FeatureToggleDetailsCardProps) {\n const t = useT()\n return (\n <div className=\"space-y-6\">\n <div className=\"rounded-lg border bg-card p-4\">\n <h2 className=\"mb-4 text-sm font-semibold uppercase text-muted-foreground\">\n {t('feature_toggles.detail.details', 'Details')}\n </h2>\n <div className=\"grid gap-4 sm:grid-cols-2\">\n <div>\n <p className=\"text-xs font-medium uppercase text-muted-foreground\">\n {t('feature_toggles.detail.fields.name', 'Name')}\n </p>\n <p className=\"text-base font-semibold text-foreground\">{featureToggleItem?.name ?? '-'}</p>\n </div>\n <div>\n <p className=\"text-xs font-medium uppercase text-muted-foreground\">\n {t('feature_toggles.detail.fields.identifier', 'Identifier')}\n </p>\n <p className=\"text-base font-semibold text-foreground\">{featureToggleItem?.identifier ?? '-'}</p>\n </div>\n <div>\n <p className=\"text-xs font-medium uppercase text-muted-foreground\">\n {t('feature_toggles.detail.fields.category', 'Category')}\n </p>\n <p className=\"text-base text-foreground\">{featureToggleItem?.category ?? '-'}</p>\n </div>\n </div>\n </div>\n <div className=\"rounded-lg border bg-card p-4\">\n <h2 className=\"mb-4 text-sm font-semibold uppercase text-muted-foreground\">{t('feature_toggles.detail.description', 'Description')}</h2>\n <p className=\"text-base text-foreground\">{featureToggleItem?.description ?? '-'}</p>\n </div>\n <div className=\"rounded-lg border bg-card p-4\">\n <h2 className=\"mb-4 text-sm font-semibold uppercase text-muted-foreground\">{t('feature_toggles.detail.defaultValue', 'Default Value')}</h2>\n <DefaultValueDisplay featureToggleItem={featureToggleItem} />\n </div>\n </div>\n );\n}\n"],
|
|
5
|
+
"mappings": ";AAaW,cAkDD,YAlDC;AAZX,SAAS,YAAY;AAOrB,SAAS,oBAAoB,EAAE,kBAAkB,GAA0C;AACzF,QAAM,IAAI,KAAK;AAEf,QAAM,eAAe,mBAAmB;AACxC,MAAI,iBAAiB,QAAQ,iBAAiB,QAAW;AACvD,WAAO,oBAAC,OAAE,WAAU,mCAAkC,eAAC;AAAA,EACzD;AAEA,UAAQ,mBAAmB,MAAM;AAAA,IAC/B,KAAK;AACH,aACE,oBAAC,OAAE,WAAU,2CACV,yBAAe,EAAE,+BAA+B,MAAM,IAAI,EAAE,gCAAgC,OAAO,GACtG;AAAA,IAGJ,KAAK;AACH,aACE,oBAAC,OAAE,WAAU,2CACV,wBACH;AAAA,IAGJ,KAAK;AACH,aACE,oBAAC,OAAE,WAAU,2CACV,wBACH;AAAA,IAGJ,KAAK;AACH,aACE,oBAAC,SAAI,WAAU,2CACb,8BAAC,SAAI,WAAU,iFACZ,iBAAO,iBAAiB,WACrB,KAAK,UAAU,cAAc,MAAM,CAAC,IACpC,cACN,GACF;AAAA,IAGJ;AACE,aAAO,oBAAC,OAAE,WAAU,mCAAkC,eAAC;AAAA,EAC3D;AACF;AAEO,SAAS,yBAAyB,EAAE,kBAAkB,GAAkC;AAC7F,QAAM,IAAI,KAAK;AACf,SACE,qBAAC,SAAI,WAAU,aACb;AAAA,yBAAC,SAAI,WAAU,iCACb;AAAA,0BAAC,QAAG,WAAU,8DACX,YAAE,kCAAkC,SAAS,GAChD;AAAA,MACA,qBAAC,SAAI,WAAU,6BACb;AAAA,6BAAC,SACC;AAAA,8BAAC,OAAE,WAAU,uDACV,YAAE,sCAAsC,MAAM,GACjD;AAAA,UACA,oBAAC,OAAE,WAAU,2CAA2C,6BAAmB,QAAQ,KAAI;AAAA,WACzF;AAAA,QACA,qBAAC,SACC;AAAA,8BAAC,OAAE,WAAU,uDACV,YAAE,4CAA4C,YAAY,GAC7D;AAAA,UACA,oBAAC,OAAE,WAAU,2CAA2C,6BAAmB,cAAc,KAAI;AAAA,WAC/F;AAAA,QACA,qBAAC,SACC;AAAA,8BAAC,OAAE,WAAU,uDACV,YAAE,0CAA0C,UAAU,GACzD;AAAA,UACA,oBAAC,OAAE,WAAU,6BAA6B,6BAAmB,YAAY,KAAI;AAAA,WAC/E;AAAA,SACF;AAAA,OACF;AAAA,IACA,qBAAC,SAAI,WAAU,iCACb;AAAA,0BAAC,QAAG,WAAU,8DAA8D,YAAE,sCAAsC,aAAa,GAAE;AAAA,MACnI,oBAAC,OAAE,WAAU,6BAA6B,6BAAmB,eAAe,KAAI;AAAA,OAClF;AAAA,IACA,qBAAC,SAAI,WAAU,iCACb;AAAA,0BAAC,QAAG,WAAU,8DAA8D,YAAE,uCAAuC,eAAe,GAAE;AAAA,MACtI,oBAAC,uBAAoB,mBAAsC;AAAA,OAC7D;AAAA,KACF;AAEJ;",
|
|
6
6
|
"names": []
|
|
7
7
|
}
|