@open-mercato/core 0.6.6-develop.6135.1.95b98004bd → 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/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/package.json +7 -7
- 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
|
@@ -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
|
}
|
|
@@ -3,6 +3,11 @@ import { z } from "zod";
|
|
|
3
3
|
import { getAuthFromRequest } from "@open-mercato/shared/lib/auth/server";
|
|
4
4
|
import { createRequestContainer } from "@open-mercato/shared/lib/di/container";
|
|
5
5
|
import { getIntegration } from "@open-mercato/shared/modules/integrations/types";
|
|
6
|
+
import {
|
|
7
|
+
resolveUserFeatures,
|
|
8
|
+
runIntegrationMutationGuardAfterSuccess,
|
|
9
|
+
runIntegrationMutationGuards
|
|
10
|
+
} from "../../guards.js";
|
|
6
11
|
const idParamsSchema = z.object({ id: z.string().min(1) });
|
|
7
12
|
const metadata = {
|
|
8
13
|
POST: { requireAuth: true, requireFeatures: ["integrations.manage"] }
|
|
@@ -26,11 +31,39 @@ async function POST(req, ctx) {
|
|
|
26
31
|
return NextResponse.json({ error: "Integration not found" }, { status: 404 });
|
|
27
32
|
}
|
|
28
33
|
const container = await createRequestContainer();
|
|
34
|
+
const guardResult = await runIntegrationMutationGuards(
|
|
35
|
+
container,
|
|
36
|
+
{
|
|
37
|
+
tenantId: auth.tenantId,
|
|
38
|
+
organizationId: auth.orgId,
|
|
39
|
+
userId: auth.sub ?? "",
|
|
40
|
+
resourceKind: "integrations.integration",
|
|
41
|
+
resourceId: integration.id,
|
|
42
|
+
operation: "update",
|
|
43
|
+
requestMethod: req.method,
|
|
44
|
+
requestHeaders: req.headers,
|
|
45
|
+
mutationPayload: { integrationId: integration.id }
|
|
46
|
+
},
|
|
47
|
+
resolveUserFeatures(auth)
|
|
48
|
+
);
|
|
49
|
+
if (!guardResult.ok) {
|
|
50
|
+
return NextResponse.json(guardResult.errorBody ?? { error: "Operation blocked by guard" }, { status: guardResult.errorStatus ?? 422 });
|
|
51
|
+
}
|
|
29
52
|
const healthService = container.resolve("integrationHealthService");
|
|
30
53
|
const result = await healthService.runHealthCheck(
|
|
31
54
|
integration.id,
|
|
32
55
|
{ organizationId: auth.orgId, tenantId: auth.tenantId }
|
|
33
56
|
);
|
|
57
|
+
await runIntegrationMutationGuardAfterSuccess(guardResult.afterSuccessCallbacks, {
|
|
58
|
+
tenantId: auth.tenantId,
|
|
59
|
+
organizationId: auth.orgId,
|
|
60
|
+
userId: auth.sub ?? "",
|
|
61
|
+
resourceKind: "integrations.integration",
|
|
62
|
+
resourceId: integration.id,
|
|
63
|
+
operation: "update",
|
|
64
|
+
requestMethod: req.method,
|
|
65
|
+
requestHeaders: req.headers
|
|
66
|
+
});
|
|
34
67
|
return NextResponse.json({
|
|
35
68
|
status: result.status,
|
|
36
69
|
message: result.message ?? null,
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"version": 3,
|
|
3
3
|
"sources": ["../../../../../../src/modules/integrations/api/%5Bid%5D/health/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 { getIntegration } from '@open-mercato/shared/modules/integrations/types'\nimport type { IntegrationHealthService } from '../../../lib/health-service'\n\nconst idParamsSchema = z.object({ id: z.string().min(1) })\n\nexport const metadata = {\n POST: { requireAuth: true, requireFeatures: ['integrations.manage'] },\n}\n\nexport const openApi = {\n tags: ['Integrations'],\n summary: 'Run health check for an integration',\n}\n\nexport async function POST(req: Request, ctx: { params?: Promise<{ id?: string }> | { id?: string } }) {\n const auth = await getAuthFromRequest(req)\n if (!auth?.tenantId || !auth.orgId) {\n return NextResponse.json({ error: 'Unauthorized' }, { status: 401 })\n }\n\n const rawParams = (ctx.params && typeof (ctx.params as Promise<unknown>).then === 'function')\n ? await (ctx.params as Promise<{ id?: string }>)\n : (ctx.params as { id?: string } | undefined)\n\n const parsedParams = idParamsSchema.safeParse(rawParams)\n if (!parsedParams.success) {\n return NextResponse.json({ error: 'Invalid integration id' }, { status: 400 })\n }\n\n const integration = getIntegration(parsedParams.data.id)\n if (!integration) {\n return NextResponse.json({ error: 'Integration not found' }, { status: 404 })\n }\n\n const container = await createRequestContainer()\n const healthService = container.resolve('integrationHealthService') as IntegrationHealthService\n\n const result = await healthService.runHealthCheck(\n integration.id,\n { organizationId: auth.orgId as string, tenantId: auth.tenantId },\n )\n\n return NextResponse.json({\n status: result.status,\n message: result.message ?? null,\n details: result.details ?? null,\n latencyMs: result.latencyMs,\n checkedAt: result.checkedAt,\n })\n}\n"],
|
|
5
|
-
"mappings": "AAAA,SAAS,oBAAoB;AAC7B,SAAS,SAAS;AAClB,SAAS,0BAA0B;AACnC,SAAS,8BAA8B;AACvC,SAAS,sBAAsB;
|
|
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 { getIntegration } from '@open-mercato/shared/modules/integrations/types'\nimport type { IntegrationHealthService } from '../../../lib/health-service'\nimport {\n resolveUserFeatures,\n runIntegrationMutationGuardAfterSuccess,\n runIntegrationMutationGuards,\n} from '../../guards'\n\nconst idParamsSchema = z.object({ id: z.string().min(1) })\n\nexport const metadata = {\n POST: { requireAuth: true, requireFeatures: ['integrations.manage'] },\n}\n\nexport const openApi = {\n tags: ['Integrations'],\n summary: 'Run health check for an integration',\n}\n\nexport async function POST(req: Request, ctx: { params?: Promise<{ id?: string }> | { id?: string } }) {\n const auth = await getAuthFromRequest(req)\n if (!auth?.tenantId || !auth.orgId) {\n return NextResponse.json({ error: 'Unauthorized' }, { status: 401 })\n }\n\n const rawParams = (ctx.params && typeof (ctx.params as Promise<unknown>).then === 'function')\n ? await (ctx.params as Promise<{ id?: string }>)\n : (ctx.params as { id?: string } | undefined)\n\n const parsedParams = idParamsSchema.safeParse(rawParams)\n if (!parsedParams.success) {\n return NextResponse.json({ error: 'Invalid integration id' }, { status: 400 })\n }\n\n const integration = getIntegration(parsedParams.data.id)\n if (!integration) {\n return NextResponse.json({ error: 'Integration not found' }, { status: 404 })\n }\n\n const container = await createRequestContainer()\n const guardResult = await runIntegrationMutationGuards(\n container,\n {\n tenantId: auth.tenantId,\n organizationId: auth.orgId,\n userId: auth.sub ?? '',\n resourceKind: 'integrations.integration',\n resourceId: integration.id,\n operation: 'update',\n requestMethod: req.method,\n requestHeaders: req.headers,\n mutationPayload: { integrationId: integration.id },\n },\n resolveUserFeatures(auth),\n )\n if (!guardResult.ok) {\n return NextResponse.json(guardResult.errorBody ?? { error: 'Operation blocked by guard' }, { status: guardResult.errorStatus ?? 422 })\n }\n\n const healthService = container.resolve('integrationHealthService') as IntegrationHealthService\n\n const result = await healthService.runHealthCheck(\n integration.id,\n { organizationId: auth.orgId as string, tenantId: auth.tenantId },\n )\n\n await runIntegrationMutationGuardAfterSuccess(guardResult.afterSuccessCallbacks, {\n tenantId: auth.tenantId,\n organizationId: auth.orgId,\n userId: auth.sub ?? '',\n resourceKind: 'integrations.integration',\n resourceId: integration.id,\n operation: 'update',\n requestMethod: req.method,\n requestHeaders: req.headers,\n })\n\n return NextResponse.json({\n status: result.status,\n message: result.message ?? null,\n details: result.details ?? null,\n latencyMs: result.latencyMs,\n checkedAt: result.checkedAt,\n })\n}\n"],
|
|
5
|
+
"mappings": "AAAA,SAAS,oBAAoB;AAC7B,SAAS,SAAS;AAClB,SAAS,0BAA0B;AACnC,SAAS,8BAA8B;AACvC,SAAS,sBAAsB;AAE/B;AAAA,EACE;AAAA,EACA;AAAA,EACA;AAAA,OACK;AAEP,MAAM,iBAAiB,EAAE,OAAO,EAAE,IAAI,EAAE,OAAO,EAAE,IAAI,CAAC,EAAE,CAAC;AAElD,MAAM,WAAW;AAAA,EACtB,MAAM,EAAE,aAAa,MAAM,iBAAiB,CAAC,qBAAqB,EAAE;AACtE;AAEO,MAAM,UAAU;AAAA,EACrB,MAAM,CAAC,cAAc;AAAA,EACrB,SAAS;AACX;AAEA,eAAsB,KAAK,KAAc,KAA8D;AACrG,QAAM,OAAO,MAAM,mBAAmB,GAAG;AACzC,MAAI,CAAC,MAAM,YAAY,CAAC,KAAK,OAAO;AAClC,WAAO,aAAa,KAAK,EAAE,OAAO,eAAe,GAAG,EAAE,QAAQ,IAAI,CAAC;AAAA,EACrE;AAEA,QAAM,YAAa,IAAI,UAAU,OAAQ,IAAI,OAA4B,SAAS,aAC9E,MAAO,IAAI,SACV,IAAI;AAET,QAAM,eAAe,eAAe,UAAU,SAAS;AACvD,MAAI,CAAC,aAAa,SAAS;AACzB,WAAO,aAAa,KAAK,EAAE,OAAO,yBAAyB,GAAG,EAAE,QAAQ,IAAI,CAAC;AAAA,EAC/E;AAEA,QAAM,cAAc,eAAe,aAAa,KAAK,EAAE;AACvD,MAAI,CAAC,aAAa;AAChB,WAAO,aAAa,KAAK,EAAE,OAAO,wBAAwB,GAAG,EAAE,QAAQ,IAAI,CAAC;AAAA,EAC9E;AAEA,QAAM,YAAY,MAAM,uBAAuB;AAC/C,QAAM,cAAc,MAAM;AAAA,IACxB;AAAA,IACA;AAAA,MACE,UAAU,KAAK;AAAA,MACf,gBAAgB,KAAK;AAAA,MACrB,QAAQ,KAAK,OAAO;AAAA,MACpB,cAAc;AAAA,MACd,YAAY,YAAY;AAAA,MACxB,WAAW;AAAA,MACX,eAAe,IAAI;AAAA,MACnB,gBAAgB,IAAI;AAAA,MACpB,iBAAiB,EAAE,eAAe,YAAY,GAAG;AAAA,IACnD;AAAA,IACA,oBAAoB,IAAI;AAAA,EAC1B;AACA,MAAI,CAAC,YAAY,IAAI;AACnB,WAAO,aAAa,KAAK,YAAY,aAAa,EAAE,OAAO,6BAA6B,GAAG,EAAE,QAAQ,YAAY,eAAe,IAAI,CAAC;AAAA,EACvI;AAEA,QAAM,gBAAgB,UAAU,QAAQ,0BAA0B;AAElE,QAAM,SAAS,MAAM,cAAc;AAAA,IACjC,YAAY;AAAA,IACZ,EAAE,gBAAgB,KAAK,OAAiB,UAAU,KAAK,SAAS;AAAA,EAClE;AAEA,QAAM,wCAAwC,YAAY,uBAAuB;AAAA,IAC/E,UAAU,KAAK;AAAA,IACf,gBAAgB,KAAK;AAAA,IACrB,QAAQ,KAAK,OAAO;AAAA,IACpB,cAAc;AAAA,IACd,YAAY,YAAY;AAAA,IACxB,WAAW;AAAA,IACX,eAAe,IAAI;AAAA,IACnB,gBAAgB,IAAI;AAAA,EACtB,CAAC;AAED,SAAO,aAAa,KAAK;AAAA,IACvB,QAAQ,OAAO;AAAA,IACf,SAAS,OAAO,WAAW;AAAA,IAC3B,SAAS,OAAO,WAAW;AAAA,IAC3B,WAAW,OAAO;AAAA,IAClB,WAAW,OAAO;AAAA,EACpB,CAAC;AACH;",
|
|
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.6140.1.d3fbb77495",
|
|
4
4
|
"license": "MIT",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "./dist/index.js",
|
|
@@ -246,16 +246,16 @@
|
|
|
246
246
|
"zod": "^4.4.3"
|
|
247
247
|
},
|
|
248
248
|
"peerDependencies": {
|
|
249
|
-
"@open-mercato/ai-assistant": "0.6.6-develop.
|
|
250
|
-
"@open-mercato/shared": "0.6.6-develop.
|
|
251
|
-
"@open-mercato/ui": "0.6.6-develop.
|
|
249
|
+
"@open-mercato/ai-assistant": "0.6.6-develop.6140.1.d3fbb77495",
|
|
250
|
+
"@open-mercato/shared": "0.6.6-develop.6140.1.d3fbb77495",
|
|
251
|
+
"@open-mercato/ui": "0.6.6-develop.6140.1.d3fbb77495",
|
|
252
252
|
"react": "^19.0.0",
|
|
253
253
|
"react-dom": "^19.0.0"
|
|
254
254
|
},
|
|
255
255
|
"devDependencies": {
|
|
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.6140.1.d3fbb77495",
|
|
257
|
+
"@open-mercato/shared": "0.6.6-develop.6140.1.d3fbb77495",
|
|
258
|
+
"@open-mercato/ui": "0.6.6-develop.6140.1.d3fbb77495",
|
|
259
259
|
"@testing-library/dom": "^10.4.1",
|
|
260
260
|
"@testing-library/jest-dom": "^6.9.1",
|
|
261
261
|
"@testing-library/react": "^16.3.1",
|
|
@@ -5,6 +5,10 @@ import { createRequestContainer } from '@open-mercato/shared/lib/di/container'
|
|
|
5
5
|
import { DashboardLayout } from '@open-mercato/core/modules/dashboards/data/entities'
|
|
6
6
|
import { dashboardLayoutItemPatchSchema } from '@open-mercato/core/modules/dashboards/data/validators'
|
|
7
7
|
import { hasFeature } from '@open-mercato/shared/security/features'
|
|
8
|
+
import {
|
|
9
|
+
runCrudMutationGuardAfterSuccess,
|
|
10
|
+
validateCrudMutationGuard,
|
|
11
|
+
} from '@open-mercato/shared/lib/crud/mutation-guard'
|
|
8
12
|
import type { OpenApiMethodDoc, OpenApiRouteDoc } from '@open-mercato/shared/lib/openapi'
|
|
9
13
|
import {
|
|
10
14
|
dashboardsTag,
|
|
@@ -14,6 +18,7 @@ import {
|
|
|
14
18
|
} from '../../openapi'
|
|
15
19
|
|
|
16
20
|
const DEFAULT_SIZE = 'md'
|
|
21
|
+
const RESOURCE_KIND = 'dashboards.layout'
|
|
17
22
|
|
|
18
23
|
export const metadata = {
|
|
19
24
|
PATCH: { requireAuth: true, requireFeatures: ['dashboards.configure'] },
|
|
@@ -39,7 +44,8 @@ export async function PATCH(req: Request, ctx: { params?: { itemId?: string } })
|
|
|
39
44
|
return NextResponse.json({ error: 'Invalid payload', issues: parsed.error.issues }, { status: 400 })
|
|
40
45
|
}
|
|
41
46
|
|
|
42
|
-
const
|
|
47
|
+
const container = await createRequestContainer()
|
|
48
|
+
const { resolve } = container
|
|
43
49
|
const em = resolve('em') as any
|
|
44
50
|
const rbac = resolve('rbacService') as any
|
|
45
51
|
|
|
@@ -54,6 +60,21 @@ export async function PATCH(req: Request, ctx: { params?: { itemId?: string } })
|
|
|
54
60
|
return NextResponse.json({ error: 'Forbidden' }, { status: 403 })
|
|
55
61
|
}
|
|
56
62
|
|
|
63
|
+
const guardResult = await validateCrudMutationGuard(container, {
|
|
64
|
+
tenantId: scope.tenantId ?? '',
|
|
65
|
+
organizationId: scope.organizationId,
|
|
66
|
+
userId: scope.userId,
|
|
67
|
+
resourceKind: RESOURCE_KIND,
|
|
68
|
+
resourceId: layoutItemId,
|
|
69
|
+
operation: 'update',
|
|
70
|
+
requestMethod: req.method,
|
|
71
|
+
requestHeaders: req.headers,
|
|
72
|
+
mutationPayload: { id: layoutItemId, size: parsed.data.size, settings: parsed.data.settings },
|
|
73
|
+
})
|
|
74
|
+
if (guardResult && !guardResult.ok) {
|
|
75
|
+
return NextResponse.json(guardResult.body, { status: guardResult.status })
|
|
76
|
+
}
|
|
77
|
+
|
|
57
78
|
const layout = await em.findOne(DashboardLayout, {
|
|
58
79
|
userId: scope.userId,
|
|
59
80
|
tenantId: scope.tenantId,
|
|
@@ -77,6 +98,20 @@ export async function PATCH(req: Request, ctx: { params?: { itemId?: string } })
|
|
|
77
98
|
}
|
|
78
99
|
await em.flush()
|
|
79
100
|
|
|
101
|
+
if (guardResult?.ok && guardResult.shouldRunAfterSuccess) {
|
|
102
|
+
await runCrudMutationGuardAfterSuccess(container, {
|
|
103
|
+
tenantId: scope.tenantId ?? '',
|
|
104
|
+
organizationId: scope.organizationId,
|
|
105
|
+
userId: scope.userId,
|
|
106
|
+
resourceKind: RESOURCE_KIND,
|
|
107
|
+
resourceId: layoutItemId,
|
|
108
|
+
operation: 'update',
|
|
109
|
+
requestMethod: req.method,
|
|
110
|
+
requestHeaders: req.headers,
|
|
111
|
+
metadata: guardResult.metadata ?? null,
|
|
112
|
+
})
|
|
113
|
+
}
|
|
114
|
+
|
|
80
115
|
return NextResponse.json({ ok: true })
|
|
81
116
|
}
|
|
82
117
|
|
|
@@ -9,6 +9,10 @@ import { resolveAllowedWidgetIds } from '@open-mercato/core/modules/dashboards/l
|
|
|
9
9
|
import { hasFeature } from '@open-mercato/shared/security/features'
|
|
10
10
|
import { User } from '@open-mercato/core/modules/auth/data/entities'
|
|
11
11
|
import { findOneWithDecryption } from '@open-mercato/shared/lib/encryption/find'
|
|
12
|
+
import {
|
|
13
|
+
runCrudMutationGuardAfterSuccess,
|
|
14
|
+
validateCrudMutationGuard,
|
|
15
|
+
} from '@open-mercato/shared/lib/crud/mutation-guard'
|
|
12
16
|
import type { OpenApiMethodDoc, OpenApiRouteDoc } from '@open-mercato/shared/lib/openapi'
|
|
13
17
|
import {
|
|
14
18
|
dashboardsTag,
|
|
@@ -18,6 +22,7 @@ import {
|
|
|
18
22
|
} from '../openapi'
|
|
19
23
|
|
|
20
24
|
const DEFAULT_SIZE = 'md'
|
|
25
|
+
const RESOURCE_KIND = 'dashboards.layout'
|
|
21
26
|
|
|
22
27
|
export const metadata = {
|
|
23
28
|
GET: { requireAuth: true, requireFeatures: ['dashboards.view'] },
|
|
@@ -206,7 +211,8 @@ export async function PUT(req: Request) {
|
|
|
206
211
|
return NextResponse.json({ error: 'Invalid layout payload', issues: parsed.error.issues }, { status: 400 })
|
|
207
212
|
}
|
|
208
213
|
|
|
209
|
-
const
|
|
214
|
+
const container = await createRequestContainer()
|
|
215
|
+
const { resolve } = container
|
|
210
216
|
const em = (resolve('em') as any).fork({ clear: true, freshEventManager: true, useContext: true })
|
|
211
217
|
const rbac = resolve('rbacService') as any
|
|
212
218
|
|
|
@@ -221,6 +227,21 @@ export async function PUT(req: Request) {
|
|
|
221
227
|
return NextResponse.json({ error: 'Forbidden' }, { status: 403 })
|
|
222
228
|
}
|
|
223
229
|
|
|
230
|
+
const guardResult = await validateCrudMutationGuard(container, {
|
|
231
|
+
tenantId: scope.tenantId ?? '',
|
|
232
|
+
organizationId: scope.organizationId,
|
|
233
|
+
userId: scope.userId,
|
|
234
|
+
resourceKind: RESOURCE_KIND,
|
|
235
|
+
resourceId: scope.userId,
|
|
236
|
+
operation: 'update',
|
|
237
|
+
requestMethod: req.method,
|
|
238
|
+
requestHeaders: req.headers,
|
|
239
|
+
mutationPayload: { items: parsed.data.items },
|
|
240
|
+
})
|
|
241
|
+
if (guardResult && !guardResult.ok) {
|
|
242
|
+
return NextResponse.json(guardResult.body, { status: guardResult.status })
|
|
243
|
+
}
|
|
244
|
+
|
|
224
245
|
const widgets = await loadAllWidgets()
|
|
225
246
|
const allowedIds = await resolveAllowedWidgetIds(
|
|
226
247
|
em,
|
|
@@ -266,6 +287,20 @@ export async function PUT(req: Request) {
|
|
|
266
287
|
}
|
|
267
288
|
await em.flush()
|
|
268
289
|
|
|
290
|
+
if (guardResult?.ok && guardResult.shouldRunAfterSuccess) {
|
|
291
|
+
await runCrudMutationGuardAfterSuccess(container, {
|
|
292
|
+
tenantId: scope.tenantId ?? '',
|
|
293
|
+
organizationId: scope.organizationId,
|
|
294
|
+
userId: scope.userId,
|
|
295
|
+
resourceKind: RESOURCE_KIND,
|
|
296
|
+
resourceId: scope.userId,
|
|
297
|
+
operation: 'update',
|
|
298
|
+
requestMethod: req.method,
|
|
299
|
+
requestHeaders: req.headers,
|
|
300
|
+
metadata: guardResult.metadata ?? null,
|
|
301
|
+
})
|
|
302
|
+
}
|
|
303
|
+
|
|
269
304
|
return NextResponse.json({ ok: true })
|
|
270
305
|
}
|
|
271
306
|
|