@open-mercato/core 0.6.7-develop.6603.1.78e5b7a65d → 0.6.7-develop.6605.1.761dd4c810
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/modules/auth/api/roles/route.js +62 -3
- package/dist/modules/auth/api/roles/route.js.map +2 -2
- package/dist/modules/entities/lib/field-definitions.js +1 -0
- package/dist/modules/entities/lib/field-definitions.js.map +2 -2
- package/dist/modules/entities/lib/install-from-ce.js +11 -2
- package/dist/modules/entities/lib/install-from-ce.js.map +2 -2
- package/package.json +7 -7
- package/src/modules/auth/api/roles/route.ts +75 -3
- package/src/modules/entities/lib/field-definitions.ts +1 -0
- package/src/modules/entities/lib/install-from-ce.ts +17 -2
|
@@ -13,8 +13,29 @@ import { roleCrudEvents, roleCrudIndexer } from "@open-mercato/core/modules/auth
|
|
|
13
13
|
import { escapeLikePattern } from "@open-mercato/shared/lib/db/escapeLikePattern";
|
|
14
14
|
import { assertActorCanModifySuperAdminRoleTarget } from "@open-mercato/core/modules/auth/lib/grantChecks";
|
|
15
15
|
import { enforceRoleTenantAccess } from "@open-mercato/core/modules/auth/lib/roleTenantGuard";
|
|
16
|
+
import { runWithCacheTenant } from "@open-mercato/cache";
|
|
17
|
+
import {
|
|
18
|
+
buildCollectionTags,
|
|
19
|
+
isCrudCacheEnabled,
|
|
20
|
+
resolveCrudCache
|
|
21
|
+
} from "@open-mercato/shared/lib/crud/cache";
|
|
16
22
|
import { createLogger } from "@open-mercato/shared/lib/logger";
|
|
23
|
+
const ROLES_LIST_CACHE_TTL_MS = 12e4;
|
|
17
24
|
const logger = createLogger("auth").child({ component: "roles" });
|
|
25
|
+
function buildRolesListCacheKey(scope) {
|
|
26
|
+
const parts = [
|
|
27
|
+
"auth:roles:list",
|
|
28
|
+
`tenant:${scope.tenantId ?? "null"}`,
|
|
29
|
+
`super:${scope.isSuperAdmin ? "1" : "0"}`,
|
|
30
|
+
`actor:${scope.actorTenantId ?? "null"}`,
|
|
31
|
+
`filter:${scope.tenantFilter ?? "null"}`,
|
|
32
|
+
`id:${scope.id ?? ""}`,
|
|
33
|
+
`page:${scope.page}`,
|
|
34
|
+
`size:${scope.pageSize}`,
|
|
35
|
+
`q:${scope.search ?? ""}`
|
|
36
|
+
];
|
|
37
|
+
return parts.join("|");
|
|
38
|
+
}
|
|
18
39
|
const querySchema = z.object({
|
|
19
40
|
id: z.string().uuid().optional(),
|
|
20
41
|
page: z.coerce.number().min(1).default(1),
|
|
@@ -127,6 +148,24 @@ async function GET(req) {
|
|
|
127
148
|
if (!isSuperAdmin && !actorTenantId) {
|
|
128
149
|
return NextResponse.json({ items: [], total: 0, totalPages: 1, isSuperAdmin });
|
|
129
150
|
}
|
|
151
|
+
const { id, page, pageSize, search, tenantId: requestedTenantId } = parsed.data;
|
|
152
|
+
const tenantFilter = isSuperAdmin && requestedTenantId ? String(requestedTenantId) : null;
|
|
153
|
+
const cacheTenantId = isSuperAdmin ? tenantFilter : actorTenantId;
|
|
154
|
+
const cache = isCrudCacheEnabled() && cacheTenantId ? resolveCrudCache(container) : null;
|
|
155
|
+
const cacheKey = cache ? buildRolesListCacheKey({
|
|
156
|
+
tenantId: cacheTenantId,
|
|
157
|
+
isSuperAdmin,
|
|
158
|
+
actorTenantId,
|
|
159
|
+
tenantFilter,
|
|
160
|
+
id,
|
|
161
|
+
page,
|
|
162
|
+
pageSize,
|
|
163
|
+
search
|
|
164
|
+
}) : null;
|
|
165
|
+
if (cache && cacheKey) {
|
|
166
|
+
const cached = await runWithCacheTenant(cacheTenantId, () => cache.get(cacheKey));
|
|
167
|
+
if (cached) return NextResponse.json(cached);
|
|
168
|
+
}
|
|
130
169
|
let superAdminRoleIds = null;
|
|
131
170
|
if (!isSuperAdmin && actorTenantId) {
|
|
132
171
|
const superAdminAcls = await findWithDecryption(em, RoleAcl, { tenantId: actorTenantId, isSuperAdmin: true }, {}, { tenantId: actorTenantId, organizationId: null });
|
|
@@ -142,8 +181,6 @@ async function GET(req) {
|
|
|
142
181
|
superAdminRoleIds = /* @__PURE__ */ new Set();
|
|
143
182
|
}
|
|
144
183
|
}
|
|
145
|
-
const { id, page, pageSize, search, tenantId: requestedTenantId } = parsed.data;
|
|
146
|
-
const tenantFilter = isSuperAdmin && requestedTenantId ? String(requestedTenantId) : null;
|
|
147
184
|
const filters = [{ deletedAt: null }];
|
|
148
185
|
if (id) filters.push({ id });
|
|
149
186
|
if (search) filters.push({ name: { $ilike: `%${escapeLikePattern(search)}%` } });
|
|
@@ -228,7 +265,29 @@ async function GET(req) {
|
|
|
228
265
|
query: parsed.data,
|
|
229
266
|
accessType: id ? "read:item" : void 0
|
|
230
267
|
});
|
|
231
|
-
|
|
268
|
+
const payload = { items, total: count, totalPages, isSuperAdmin };
|
|
269
|
+
if (cache && cacheKey) {
|
|
270
|
+
try {
|
|
271
|
+
await runWithCacheTenant(
|
|
272
|
+
cacheTenantId,
|
|
273
|
+
() => cache.set(cacheKey, payload, {
|
|
274
|
+
ttl: ROLES_LIST_CACHE_TTL_MS,
|
|
275
|
+
tags: [
|
|
276
|
+
// Roles are tenant-level (org:null) — flushed on auth.roles.* command writes.
|
|
277
|
+
...buildCollectionTags("auth.role", cacheTenantId, [null]),
|
|
278
|
+
// usersCount derives from UserRole grants — auth.users.* command writes
|
|
279
|
+
// flush both tenant-level and record-organization collection tags.
|
|
280
|
+
...buildCollectionTags("auth.user", cacheTenantId, [null]),
|
|
281
|
+
// Role-ACL feature changes flush rbac:tenant:<T> from the roles/acl route.
|
|
282
|
+
`rbac:tenant:${cacheTenantId ?? "null"}`
|
|
283
|
+
]
|
|
284
|
+
})
|
|
285
|
+
);
|
|
286
|
+
} catch (err) {
|
|
287
|
+
logger.warn("Failed to set roles list cache", { err });
|
|
288
|
+
}
|
|
289
|
+
}
|
|
290
|
+
return NextResponse.json(payload);
|
|
232
291
|
}
|
|
233
292
|
const POST = crud.POST;
|
|
234
293
|
const PUT = crud.PUT;
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"version": 3,
|
|
3
3
|
"sources": ["../../../../../src/modules/auth/api/roles/route.ts"],
|
|
4
|
-
"sourcesContent": ["/* eslint-disable @typescript-eslint/no-explicit-any */\nimport { NextResponse } from 'next/server'\nimport { z } from 'zod'\nimport type { OpenApiRouteDoc } from '@open-mercato/shared/lib/openapi'\nimport { logCrudAccess, makeCrudRoute } from '@open-mercato/shared/lib/crud/factory'\nimport { CrudHttpError } from '@open-mercato/shared/lib/crud/errors'\nimport { getAuthFromRequest } from '@open-mercato/shared/lib/auth/server'\nimport { createRequestContainer } from '@open-mercato/shared/lib/di/container'\nimport { Role, RoleAcl, UserRole } from '@open-mercato/core/modules/auth/data/entities'\nimport { Tenant } from '@open-mercato/core/modules/directory/data/entities'\nimport { E } from '#generated/entities.ids.generated'\nimport { loadCustomFieldValues } from '@open-mercato/shared/lib/crud/custom-fields'\nimport { findWithDecryption } from '@open-mercato/shared/lib/encryption/find'\nimport type { EntityManager, FilterQuery } from '@mikro-orm/postgresql'\nimport { roleCrudEvents, roleCrudIndexer } from '@open-mercato/core/modules/auth/commands/roles'\nimport { escapeLikePattern } from '@open-mercato/shared/lib/db/escapeLikePattern'\nimport { assertActorCanModifySuperAdminRoleTarget } from '@open-mercato/core/modules/auth/lib/grantChecks'\nimport { enforceRoleTenantAccess } from '@open-mercato/core/modules/auth/lib/roleTenantGuard'\nimport type { RbacService } from '@open-mercato/core/modules/auth/services/rbacService'\nimport { createLogger } from '@open-mercato/shared/lib/logger'\n\nconst logger = createLogger('auth').child({ component: 'roles' })\n\nconst querySchema = 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 tenantId: z.string().uuid().optional(),\n}).passthrough()\n\nconst roleCreateSchema = z.object({\n name: z.string().min(2).max(100),\n tenantId: z.string().uuid().optional(),\n})\n\nconst roleUpdateSchema = z.object({\n id: z.string().uuid(),\n name: z.string().min(2).max(100).optional(),\n tenantId: z.string().uuid().optional(),\n})\n\nconst roleListItemSchema = z.object({\n id: z.string().uuid(),\n name: z.string(),\n usersCount: z.number().int().nonnegative(),\n tenantId: z.string().uuid().nullable(),\n tenantIds: z.array(z.string().uuid()).optional(),\n tenantName: z.string().nullable(),\n updatedAt: z.string().nullable().optional(),\n})\n\nconst roleListResponseSchema = z.object({\n items: z.array(roleListItemSchema),\n total: z.number().int().nonnegative(),\n totalPages: z.number().int().positive(),\n isSuperAdmin: z.boolean().optional(),\n})\n\nconst okResponseSchema = z.object({ ok: z.literal(true) })\n\nconst errorResponseSchema = z.object({ error: z.string() })\n\nconst routeMetadata = {\n GET: { requireAuth: true, requireFeatures: ['auth.roles.list'] },\n POST: { requireAuth: true, requireFeatures: ['auth.roles.manage'] },\n PUT: { requireAuth: true, requireFeatures: ['auth.roles.manage'] },\n DELETE: { requireAuth: true, requireFeatures: ['auth.roles.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: Role,\n idField: 'id',\n orgField: null,\n tenantField: null,\n softDeleteField: 'deletedAt',\n },\n events: roleCrudEvents,\n indexer: roleCrudIndexer,\n actions: {\n create: {\n commandId: 'auth.roles.create',\n schema: rawBodySchema,\n mapInput: async ({ parsed, ctx }) =>\n enforceRoleTenantAccess('create', parsed, { auth: ctx.auth, container: ctx.container }),\n response: ({ result }) => ({ id: String(result.id) }),\n status: 201,\n },\n update: {\n commandId: 'auth.roles.update',\n schema: rawBodySchema,\n mapInput: async ({ parsed, ctx }) => {\n if (ctx.request && typeof parsed.id === 'string' && parsed.id.length) {\n await assertCanModifySuperAdminRole(ctx.request, parsed.id)\n }\n return enforceRoleTenantAccess('update', parsed, { auth: ctx.auth, container: ctx.container })\n },\n response: () => ({ ok: true }),\n },\n delete: {\n commandId: 'auth.roles.delete',\n mapInput: async ({ parsed, raw, ctx }) => {\n const targetId = resolveDeleteTargetId(parsed, raw)\n if (ctx.request && targetId) {\n await assertCanModifySuperAdminRole(ctx.request, targetId)\n }\n return enforceRoleTenantAccess('delete', parsed, { auth: ctx.auth, container: ctx.container })\n },\n response: () => ({ ok: true }),\n },\n },\n})\n\nexport async function GET(req: Request) {\n const auth = await getAuthFromRequest(req)\n if (!auth) return NextResponse.json({ items: [], total: 0, totalPages: 1 })\n const url = new URL(req.url)\n const parsed = querySchema.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 tenantId: url.searchParams.get('tenantId') || undefined,\n })\n if (!parsed.success) return NextResponse.json({ items: [], total: 0, totalPages: 1 })\n const container = await createRequestContainer()\n const em = (container.resolve('em') as EntityManager)\n let isSuperAdmin = false\n try {\n if (auth.sub) {\n const rbacService = container.resolve('rbacService') as any\n const acl = await rbacService.loadAcl(auth.sub, { tenantId: auth.tenantId ?? null, organizationId: auth.orgId ?? null })\n isSuperAdmin = !!acl?.isSuperAdmin\n }\n } catch (err) {\n logger.error('Failed to resolve rbac', { err })\n }\n const actorTenantId = auth.tenantId ? String(auth.tenantId) : null\n if (!isSuperAdmin && !actorTenantId) {\n return NextResponse.json({ items: [], total: 0, totalPages: 1, isSuperAdmin })\n }\n let superAdminRoleIds: Set<string> | null = null\n if (!isSuperAdmin && actorTenantId) {\n const superAdminAcls = await findWithDecryption(em, RoleAcl, { tenantId: actorTenantId, isSuperAdmin: true }, {}, { tenantId: actorTenantId, organizationId: null })\n if (superAdminAcls.length) {\n superAdminRoleIds = new Set(\n superAdminAcls\n .map((acl) => {\n const roleRef = acl.role\n const idValue = roleRef?.id\n return idValue ? String(idValue) : null\n })\n .filter((id): id is string => !!id),\n )\n } else {\n superAdminRoleIds = new Set()\n }\n }\n const { id, page, pageSize, search, tenantId: requestedTenantId } = parsed.data\n const tenantFilter = isSuperAdmin && requestedTenantId ? String(requestedTenantId) : null\n const filters: any[] = [{ deletedAt: null }]\n if (id) filters.push({ id })\n if (search) filters.push({ name: { $ilike: `%${escapeLikePattern(search)}%` } })\n if (!isSuperAdmin && actorTenantId) {\n filters.push({ tenantId: actorTenantId })\n filters.push({ name: { $ne: 'superadmin' } })\n if (superAdminRoleIds && superAdminRoleIds.size) {\n filters.push({ id: { $nin: Array.from(superAdminRoleIds) } })\n }\n } else if (tenantFilter) {\n filters.push({ tenantId: tenantFilter })\n }\n const where = filters.length > 1 ? { $and: filters } : filters[0]\n const [rows, count] = await em.findAndCount(Role, where, { limit: pageSize, offset: (page - 1) * pageSize })\n const roleIds = rows.map((r: any) => String(r.id))\n const counts: Record<string, number> = {}\n if (roleIds.length) {\n const userRoleFilter: FilterQuery<UserRole> = { role: { $in: roleIds }, deletedAt: null }\n const links = await findWithDecryption(em, UserRole, userRoleFilter, {}, { tenantId: null, organizationId: null })\n for (const l of links) {\n const rid = String((l as any).role?.id || (l as any).role)\n counts[rid] = (counts[rid] || 0) + 1\n }\n }\n const roleTenantIds = rows\n .map((role: any) => (role.tenantId ? String(role.tenantId) : null))\n .filter((tenantId): tenantId is string => typeof tenantId === 'string' && tenantId.length > 0)\n const uniqueTenantIds = Array.from(new Set(roleTenantIds))\n let tenantMap: Record<string, string> = {}\n if (uniqueTenantIds.length) {\n const tenants = await findWithDecryption(em, Tenant, { id: { $in: uniqueTenantIds as any }, deletedAt: null }, {}, { tenantId: null, organizationId: null })\n tenantMap = tenants.reduce<Record<string, string>>((acc, tenant) => {\n const tid = tenant?.id ? String(tenant.id) : null\n if (!tid) return acc\n const rawName = (tenant as any)?.name\n const name = typeof rawName === 'string' && rawName.length > 0 ? rawName : tid\n acc[tid] = name\n return acc\n }, {})\n }\n const tenantByRole: Record<string, string | null> = {}\n for (const role of rows) {\n const rid = String(role.id)\n tenantByRole[rid] = role.tenantId ? String(role.tenantId) : null\n }\n const tenantFallbacks = Array.from(new Set<string | null>([\n auth.tenantId ?? null,\n tenantFilter ?? null,\n ...Object.values(tenantByRole),\n ]))\n const cfByRole = roleIds.length\n ? await loadCustomFieldValues({\n em,\n entityId: E.auth.role,\n recordIds: roleIds,\n tenantIdByRecord: tenantByRole,\n tenantFallbacks,\n })\n : {}\n const items = rows.map((r: any) => {\n const idStr = String(r.id)\n const tenantId = tenantByRole[idStr]\n const tenantName = tenantId ? tenantMap[tenantId] ?? tenantId : null\n const exposeTenant = isSuperAdmin || (tenantId && auth.tenantId && tenantId === auth.tenantId)\n return {\n id: idStr,\n name: String(r.name),\n usersCount: counts[idStr] || 0,\n tenantId: tenantId ?? null,\n tenantIds: exposeTenant && tenantId ? [tenantId] : [],\n tenantName: exposeTenant ? tenantName : null,\n updatedAt: r.updatedAt instanceof Date ? r.updatedAt.toISOString() : null,\n ...(cfByRole[idStr] || {}),\n }\n })\n const totalPages = Math.max(1, Math.ceil(count / pageSize))\n await logCrudAccess({\n container,\n auth,\n request: req,\n items,\n idField: 'id',\n resourceKind: 'auth.role',\n organizationId: null,\n tenantId: auth.tenantId ?? null,\n query: parsed.data,\n accessType: id ? 'read:item' : undefined,\n })\n return NextResponse.json({ items, total: count, totalPages, isSuperAdmin })\n}\n\nexport const POST = crud.POST\nexport const PUT = crud.PUT\nexport const DELETE = async (req: Request) => {\n const targetId = new URL(req.url).searchParams.get('id')\n if (targetId) {\n try {\n await assertCanModifySuperAdminRole(req, targetId)\n } catch (err) {\n if (err instanceof CrudHttpError) {\n return NextResponse.json(err.body, { status: err.status })\n }\n throw err\n }\n }\n return crud.DELETE(req)\n}\n\nasync function assertCanModifySuperAdminRole(req: Request, targetRoleId: string) {\n const auth = await getAuthFromRequest(req)\n if (!auth?.sub) throw new CrudHttpError(401, { error: 'Unauthorized' })\n const container = await createRequestContainer()\n const em = container.resolve('em') as EntityManager\n await assertActorCanModifySuperAdminRoleTarget({\n em,\n rbacService: container.resolve('rbacService') as RbacService,\n actorUserId: auth.sub,\n tenantId: auth.tenantId ?? null,\n organizationId: auth.orgId ?? null,\n targetRoleId,\n })\n}\n\nfunction resolveDeleteTargetId(parsed: unknown, raw: unknown): string | null {\n const fromParsed = readId(parsed as Record<string, unknown> | null | undefined)\n if (fromParsed) return fromParsed\n const rawRecord = raw as { body?: Record<string, unknown>; query?: Record<string, unknown> } | null | undefined\n return readId(rawRecord?.query) ?? readId(rawRecord?.body)\n}\n\nfunction readId(record: Record<string, unknown> | null | undefined): string | null {\n const value = record?.id\n return typeof value === 'string' && value.length > 0 ? value : null\n}\n\nexport const openApi: OpenApiRouteDoc = {\n tag: 'Authentication & Accounts',\n summary: 'Role management',\n methods: {\n GET: {\n summary: 'List roles',\n description:\n 'Returns available roles within the current tenant. Super administrators receive visibility across tenants.',\n query: querySchema,\n responses: [\n { status: 200, description: 'Role collection', schema: roleListResponseSchema },\n ],\n },\n POST: {\n summary: 'Create role',\n description: 'Creates a new role anchored to the caller\\'s tenant. Non-superadmins cannot target another tenant; supplying a foreign `tenantId` is rejected.',\n requestBody: {\n contentType: 'application/json',\n schema: roleCreateSchema,\n },\n responses: [\n {\n status: 201,\n description: 'Role created',\n schema: z.object({ id: z.string().uuid() }),\n },\n ],\n errors: [\n { status: 400, description: 'Invalid payload', schema: errorResponseSchema },\n { status: 401, description: 'Unauthorized', schema: errorResponseSchema },\n ],\n },\n PUT: {\n summary: 'Update role',\n description: 'Updates mutable fields on an existing role.',\n requestBody: {\n contentType: 'application/json',\n schema: roleUpdateSchema,\n },\n responses: [\n {\n status: 200,\n description: 'Role updated',\n schema: okResponseSchema,\n },\n ],\n errors: [\n { status: 400, description: 'Invalid payload', schema: errorResponseSchema },\n { status: 401, description: 'Unauthorized', schema: errorResponseSchema },\n { status: 404, description: 'Role not found', schema: errorResponseSchema },\n ],\n },\n DELETE: {\n summary: 'Delete role',\n description: 'Deletes a role by identifier. Fails when users remain assigned.',\n query: z.object({ id: z.string().uuid().describe('Role identifier') }),\n responses: [\n { status: 200, description: 'Role deleted', schema: okResponseSchema },\n ],\n errors: [\n { status: 400, description: 'Role cannot be deleted', schema: errorResponseSchema },\n { status: 401, description: 'Unauthorized', schema: errorResponseSchema },\n { status: 404, description: 'Role not found', schema: errorResponseSchema },\n ],\n },\n },\n}\n"],
|
|
5
|
-
"mappings": "AACA,SAAS,oBAAoB;AAC7B,SAAS,SAAS;AAElB,SAAS,eAAe,qBAAqB;AAC7C,SAAS,qBAAqB;AAC9B,SAAS,0BAA0B;AACnC,SAAS,8BAA8B;AACvC,SAAS,MAAM,SAAS,gBAAgB;AACxC,SAAS,cAAc;AACvB,SAAS,SAAS;AAClB,SAAS,6BAA6B;AACtC,SAAS,0BAA0B;AAEnC,SAAS,gBAAgB,uBAAuB;AAChD,SAAS,yBAAyB;AAClC,SAAS,gDAAgD;AACzD,SAAS,+BAA+B;AAExC,SAAS,oBAAoB;AAE7B,MAAM,SAAS,aAAa,MAAM,EAAE,MAAM,EAAE,WAAW,QAAQ,CAAC;AAEhE,MAAM,cAAc,EAAE,OAAO;AAAA,EAC3B,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,UAAU,EAAE,OAAO,EAAE,KAAK,EAAE,SAAS;AACvC,CAAC,EAAE,YAAY;AAEf,MAAM,mBAAmB,EAAE,OAAO;AAAA,EAChC,MAAM,EAAE,OAAO,EAAE,IAAI,CAAC,EAAE,IAAI,GAAG;AAAA,EAC/B,UAAU,EAAE,OAAO,EAAE,KAAK,EAAE,SAAS;AACvC,CAAC;AAED,MAAM,mBAAmB,EAAE,OAAO;AAAA,EAChC,IAAI,EAAE,OAAO,EAAE,KAAK;AAAA,EACpB,MAAM,EAAE,OAAO,EAAE,IAAI,CAAC,EAAE,IAAI,GAAG,EAAE,SAAS;AAAA,EAC1C,UAAU,EAAE,OAAO,EAAE,KAAK,EAAE,SAAS;AACvC,CAAC;AAED,MAAM,qBAAqB,EAAE,OAAO;AAAA,EAClC,IAAI,EAAE,OAAO,EAAE,KAAK;AAAA,EACpB,MAAM,EAAE,OAAO;AAAA,EACf,YAAY,EAAE,OAAO,EAAE,IAAI,EAAE,YAAY;AAAA,EACzC,UAAU,EAAE,OAAO,EAAE,KAAK,EAAE,SAAS;AAAA,EACrC,WAAW,EAAE,MAAM,EAAE,OAAO,EAAE,KAAK,CAAC,EAAE,SAAS;AAAA,EAC/C,YAAY,EAAE,OAAO,EAAE,SAAS;AAAA,EAChC,WAAW,EAAE,OAAO,EAAE,SAAS,EAAE,SAAS;AAC5C,CAAC;AAED,MAAM,yBAAyB,EAAE,OAAO;AAAA,EACtC,OAAO,EAAE,MAAM,kBAAkB;AAAA,EACjC,OAAO,EAAE,OAAO,EAAE,IAAI,EAAE,YAAY;AAAA,EACpC,YAAY,EAAE,OAAO,EAAE,IAAI,EAAE,SAAS;AAAA,EACtC,cAAc,EAAE,QAAQ,EAAE,SAAS;AACrC,CAAC;AAED,MAAM,mBAAmB,EAAE,OAAO,EAAE,IAAI,EAAE,QAAQ,IAAI,EAAE,CAAC;AAEzD,MAAM,sBAAsB,EAAE,OAAO,EAAE,OAAO,EAAE,OAAO,EAAE,CAAC;AAE1D,MAAM,gBAAgB;AAAA,EACpB,KAAK,EAAE,aAAa,MAAM,iBAAiB,CAAC,iBAAiB,EAAE;AAAA,EAC/D,MAAM,EAAE,aAAa,MAAM,iBAAiB,CAAC,mBAAmB,EAAE;AAAA,EAClE,KAAK,EAAE,aAAa,MAAM,iBAAiB,CAAC,mBAAmB,EAAE;AAAA,EACjE,QAAQ,EAAE,aAAa,MAAM,iBAAiB,CAAC,mBAAmB,EAAE;AACtE;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,OAAO,EAAE,QAAQ,IAAI,MAC7B,wBAAwB,UAAU,QAAQ,EAAE,MAAM,IAAI,MAAM,WAAW,IAAI,UAAU,CAAC;AAAA,MACxF,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,OAAO,EAAE,QAAQ,IAAI,MAAM;AACnC,YAAI,IAAI,WAAW,OAAO,OAAO,OAAO,YAAY,OAAO,GAAG,QAAQ;AACpE,gBAAM,8BAA8B,IAAI,SAAS,OAAO,EAAE;AAAA,QAC5D;AACA,eAAO,wBAAwB,UAAU,QAAQ,EAAE,MAAM,IAAI,MAAM,WAAW,IAAI,UAAU,CAAC;AAAA,MAC/F;AAAA,MACA,UAAU,OAAO,EAAE,IAAI,KAAK;AAAA,IAC9B;AAAA,IACA,QAAQ;AAAA,MACN,WAAW;AAAA,MACX,UAAU,OAAO,EAAE,QAAQ,KAAK,IAAI,MAAM;AACxC,cAAM,WAAW,sBAAsB,QAAQ,GAAG;AAClD,YAAI,IAAI,WAAW,UAAU;AAC3B,gBAAM,8BAA8B,IAAI,SAAS,QAAQ;AAAA,QAC3D;AACA,eAAO,wBAAwB,UAAU,QAAQ,EAAE,MAAM,IAAI,MAAM,WAAW,IAAI,UAAU,CAAC;AAAA,MAC/F;AAAA,MACA,UAAU,OAAO,EAAE,IAAI,KAAK;AAAA,IAC9B;AAAA,EACF;AACF,CAAC;AAED,eAAsB,IAAI,KAAc;AACtC,QAAM,OAAO,MAAM,mBAAmB,GAAG;AACzC,MAAI,CAAC,KAAM,QAAO,aAAa,KAAK,EAAE,OAAO,CAAC,GAAG,OAAO,GAAG,YAAY,EAAE,CAAC;AAC1E,QAAM,MAAM,IAAI,IAAI,IAAI,GAAG;AAC3B,QAAM,SAAS,YAAY,UAAU;AAAA,IACnC,IAAI,IAAI,aAAa,IAAI,IAAI,KAAK;AAAA,IAClC,MAAM,IAAI,aAAa,IAAI,MAAM,KAAK;AAAA,IACtC,UAAU,IAAI,aAAa,IAAI,UAAU,KAAK;AAAA,IAC9C,QAAQ,IAAI,aAAa,IAAI,QAAQ,KAAK;AAAA,IAC1C,UAAU,IAAI,aAAa,IAAI,UAAU,KAAK;AAAA,EAChD,CAAC;AACD,MAAI,CAAC,OAAO,QAAS,QAAO,aAAa,KAAK,EAAE,OAAO,CAAC,GAAG,OAAO,GAAG,YAAY,EAAE,CAAC;AACpF,QAAM,YAAY,MAAM,uBAAuB;AAC/C,QAAM,KAAM,UAAU,QAAQ,IAAI;AAClC,MAAI,eAAe;AACnB,MAAI;AACF,QAAI,KAAK,KAAK;AACZ,YAAM,cAAc,UAAU,QAAQ,aAAa;AACnD,YAAM,MAAM,MAAM,YAAY,QAAQ,KAAK,KAAK,EAAE,UAAU,KAAK,YAAY,MAAM,gBAAgB,KAAK,SAAS,KAAK,CAAC;AACvH,qBAAe,CAAC,CAAC,KAAK;AAAA,IACxB;AAAA,EACF,SAAS,KAAK;AACZ,WAAO,MAAM,0BAA0B,EAAE,IAAI,CAAC;AAAA,EAChD;AACA,QAAM,gBAAgB,KAAK,WAAW,OAAO,KAAK,QAAQ,IAAI;AAC9D,MAAI,CAAC,gBAAgB,CAAC,eAAe;AACnC,WAAO,aAAa,KAAK,EAAE,OAAO,CAAC,GAAG,OAAO,GAAG,YAAY,GAAG,aAAa,CAAC;AAAA,EAC/E;AACA,MAAI,oBAAwC;AAC5C,MAAI,CAAC,gBAAgB,eAAe;AAClC,UAAM,iBAAiB,MAAM,mBAAmB,IAAI,SAAS,EAAE,UAAU,eAAe,cAAc,KAAK,GAAG,CAAC,GAAG,EAAE,UAAU,eAAe,gBAAgB,KAAK,CAAC;AACnK,QAAI,eAAe,QAAQ;AACzB,0BAAoB,IAAI;AAAA,QACtB,eACG,IAAI,CAAC,QAAQ;AACZ,gBAAM,UAAU,IAAI;AACpB,gBAAM,UAAU,SAAS;AACzB,iBAAO,UAAU,OAAO,OAAO,IAAI;AAAA,QACrC,CAAC,EACA,OAAO,CAACA,QAAqB,CAAC,CAACA,GAAE;AAAA,MACtC;AAAA,IACF,OAAO;AACL,0BAAoB,oBAAI,IAAI;AAAA,IAC9B;AAAA,EACF;AACA,QAAM,EAAE,IAAI,MAAM,UAAU,QAAQ,UAAU,kBAAkB,IAAI,OAAO;AAC3E,QAAM,eAAe,gBAAgB,oBAAoB,OAAO,iBAAiB,IAAI;AACrF,QAAM,UAAiB,CAAC,EAAE,WAAW,KAAK,CAAC;AAC3C,MAAI,GAAI,SAAQ,KAAK,EAAE,GAAG,CAAC;AAC3B,MAAI,OAAQ,SAAQ,KAAK,EAAE,MAAM,EAAE,QAAQ,IAAI,kBAAkB,MAAM,CAAC,IAAI,EAAE,CAAC;AAC/E,MAAI,CAAC,gBAAgB,eAAe;AAClC,YAAQ,KAAK,EAAE,UAAU,cAAc,CAAC;AACxC,YAAQ,KAAK,EAAE,MAAM,EAAE,KAAK,aAAa,EAAE,CAAC;AAC5C,QAAI,qBAAqB,kBAAkB,MAAM;AAC/C,cAAQ,KAAK,EAAE,IAAI,EAAE,MAAM,MAAM,KAAK,iBAAiB,EAAE,EAAE,CAAC;AAAA,IAC9D;AAAA,EACF,WAAW,cAAc;AACvB,YAAQ,KAAK,EAAE,UAAU,aAAa,CAAC;AAAA,EACzC;AACA,QAAM,QAAQ,QAAQ,SAAS,IAAI,EAAE,MAAM,QAAQ,IAAI,QAAQ,CAAC;AAChE,QAAM,CAAC,MAAM,KAAK,IAAI,MAAM,GAAG,aAAa,MAAM,OAAO,EAAE,OAAO,UAAU,SAAS,OAAO,KAAK,SAAS,CAAC;AAC3G,QAAM,UAAU,KAAK,IAAI,CAAC,MAAW,OAAO,EAAE,EAAE,CAAC;AACjD,QAAM,SAAiC,CAAC;AACxC,MAAI,QAAQ,QAAQ;AAClB,UAAM,iBAAwC,EAAE,MAAM,EAAE,KAAK,QAAQ,GAAG,WAAW,KAAK;AACxF,UAAM,QAAQ,MAAM,mBAAmB,IAAI,UAAU,gBAAgB,CAAC,GAAG,EAAE,UAAU,MAAM,gBAAgB,KAAK,CAAC;AACjH,eAAW,KAAK,OAAO;AACrB,YAAM,MAAM,OAAQ,EAAU,MAAM,MAAO,EAAU,IAAI;AACzD,aAAO,GAAG,KAAK,OAAO,GAAG,KAAK,KAAK;AAAA,IACrC;AAAA,EACF;AACA,QAAM,gBAAgB,KACnB,IAAI,CAAC,SAAe,KAAK,WAAW,OAAO,KAAK,QAAQ,IAAI,IAAK,EACjE,OAAO,CAAC,aAAiC,OAAO,aAAa,YAAY,SAAS,SAAS,CAAC;AAC/F,QAAM,kBAAkB,MAAM,KAAK,IAAI,IAAI,aAAa,CAAC;AACzD,MAAI,YAAoC,CAAC;AACzC,MAAI,gBAAgB,QAAQ;AAC1B,UAAM,UAAU,MAAM,mBAAmB,IAAI,QAAQ,EAAE,IAAI,EAAE,KAAK,gBAAuB,GAAG,WAAW,KAAK,GAAG,CAAC,GAAG,EAAE,UAAU,MAAM,gBAAgB,KAAK,CAAC;AAC3J,gBAAY,QAAQ,OAA+B,CAAC,KAAK,WAAW;AAClE,YAAM,MAAM,QAAQ,KAAK,OAAO,OAAO,EAAE,IAAI;AAC7C,UAAI,CAAC,IAAK,QAAO;AACjB,YAAM,UAAW,QAAgB;AACjC,YAAM,OAAO,OAAO,YAAY,YAAY,QAAQ,SAAS,IAAI,UAAU;AAC3E,UAAI,GAAG,IAAI;AACX,aAAO;AAAA,IACT,GAAG,CAAC,CAAC;AAAA,EACP;AACA,QAAM,eAA8C,CAAC;AACrD,aAAW,QAAQ,MAAM;AACvB,UAAM,MAAM,OAAO,KAAK,EAAE;AAC1B,iBAAa,GAAG,IAAI,KAAK,WAAW,OAAO,KAAK,QAAQ,IAAI;AAAA,EAC9D;AACA,QAAM,kBAAkB,MAAM,KAAK,oBAAI,IAAmB;AAAA,IACxD,KAAK,YAAY;AAAA,IACjB,gBAAgB;AAAA,IAChB,GAAG,OAAO,OAAO,YAAY;AAAA,EAC/B,CAAC,CAAC;AACF,QAAM,WAAW,QAAQ,SACrB,MAAM,sBAAsB;AAAA,IAC1B;AAAA,IACA,UAAU,EAAE,KAAK;AAAA,IACjB,WAAW;AAAA,IACX,kBAAkB;AAAA,IAClB;AAAA,EACF,CAAC,IACD,CAAC;AACL,QAAM,QAAQ,KAAK,IAAI,CAAC,MAAW;AACjC,UAAM,QAAQ,OAAO,EAAE,EAAE;AACzB,UAAM,WAAW,aAAa,KAAK;AACnC,UAAM,aAAa,WAAW,UAAU,QAAQ,KAAK,WAAW;AAChE,UAAM,eAAe,gBAAiB,YAAY,KAAK,YAAY,aAAa,KAAK;AACrF,WAAO;AAAA,MACL,IAAI;AAAA,MACJ,MAAM,OAAO,EAAE,IAAI;AAAA,MACnB,YAAY,OAAO,KAAK,KAAK;AAAA,MAC7B,UAAU,YAAY;AAAA,MACtB,WAAW,gBAAgB,WAAW,CAAC,QAAQ,IAAI,CAAC;AAAA,MACpD,YAAY,eAAe,aAAa;AAAA,MACxC,WAAW,EAAE,qBAAqB,OAAO,EAAE,UAAU,YAAY,IAAI;AAAA,MACrE,GAAI,SAAS,KAAK,KAAK,CAAC;AAAA,IAC1B;AAAA,EACF,CAAC;AACD,QAAM,aAAa,KAAK,IAAI,GAAG,KAAK,KAAK,QAAQ,QAAQ,CAAC;AAC1D,QAAM,cAAc;AAAA,IAClB;AAAA,IACA;AAAA,IACA,SAAS;AAAA,IACT;AAAA,IACA,SAAS;AAAA,IACT,cAAc;AAAA,IACd,gBAAgB;AAAA,IAChB,UAAU,KAAK,YAAY;AAAA,IAC3B,OAAO,OAAO;AAAA,IACd,YAAY,KAAK,cAAc;AAAA,EACjC,CAAC;AACD,SAAO,aAAa,KAAK,EAAE,OAAO,OAAO,OAAO,YAAY,aAAa,CAAC;AAC5E;AAEO,MAAM,OAAO,KAAK;AAClB,MAAM,MAAM,KAAK;AACjB,MAAM,SAAS,OAAO,QAAiB;AAC5C,QAAM,WAAW,IAAI,IAAI,IAAI,GAAG,EAAE,aAAa,IAAI,IAAI;AACvD,MAAI,UAAU;AACZ,QAAI;AACF,YAAM,8BAA8B,KAAK,QAAQ;AAAA,IACnD,SAAS,KAAK;AACZ,UAAI,eAAe,eAAe;AAChC,eAAO,aAAa,KAAK,IAAI,MAAM,EAAE,QAAQ,IAAI,OAAO,CAAC;AAAA,MAC3D;AACA,YAAM;AAAA,IACR;AAAA,EACF;AACA,SAAO,KAAK,OAAO,GAAG;AACxB;AAEA,eAAe,8BAA8B,KAAc,cAAsB;AAC/E,QAAM,OAAO,MAAM,mBAAmB,GAAG;AACzC,MAAI,CAAC,MAAM,IAAK,OAAM,IAAI,cAAc,KAAK,EAAE,OAAO,eAAe,CAAC;AACtE,QAAM,YAAY,MAAM,uBAAuB;AAC/C,QAAM,KAAK,UAAU,QAAQ,IAAI;AACjC,QAAM,yCAAyC;AAAA,IAC7C;AAAA,IACA,aAAa,UAAU,QAAQ,aAAa;AAAA,IAC5C,aAAa,KAAK;AAAA,IAClB,UAAU,KAAK,YAAY;AAAA,IAC3B,gBAAgB,KAAK,SAAS;AAAA,IAC9B;AAAA,EACF,CAAC;AACH;AAEA,SAAS,sBAAsB,QAAiB,KAA6B;AAC3E,QAAM,aAAa,OAAO,MAAoD;AAC9E,MAAI,WAAY,QAAO;AACvB,QAAM,YAAY;AAClB,SAAO,OAAO,WAAW,KAAK,KAAK,OAAO,WAAW,IAAI;AAC3D;AAEA,SAAS,OAAO,QAAmE;AACjF,QAAM,QAAQ,QAAQ;AACtB,SAAO,OAAO,UAAU,YAAY,MAAM,SAAS,IAAI,QAAQ;AACjE;AAEO,MAAM,UAA2B;AAAA,EACtC,KAAK;AAAA,EACL,SAAS;AAAA,EACT,SAAS;AAAA,IACP,KAAK;AAAA,MACH,SAAS;AAAA,MACT,aACE;AAAA,MACF,OAAO;AAAA,MACP,WAAW;AAAA,QACT,EAAE,QAAQ,KAAK,aAAa,mBAAmB,QAAQ,uBAAuB;AAAA,MAChF;AAAA,IACF;AAAA,IACA,MAAM;AAAA,MACJ,SAAS;AAAA,MACT,aAAa;AAAA,MACb,aAAa;AAAA,QACX,aAAa;AAAA,QACb,QAAQ;AAAA,MACV;AAAA,MACA,WAAW;AAAA,QACT;AAAA,UACE,QAAQ;AAAA,UACR,aAAa;AAAA,UACb,QAAQ,EAAE,OAAO,EAAE,IAAI,EAAE,OAAO,EAAE,KAAK,EAAE,CAAC;AAAA,QAC5C;AAAA,MACF;AAAA,MACA,QAAQ;AAAA,QACN,EAAE,QAAQ,KAAK,aAAa,mBAAmB,QAAQ,oBAAoB;AAAA,QAC3E,EAAE,QAAQ,KAAK,aAAa,gBAAgB,QAAQ,oBAAoB;AAAA,MAC1E;AAAA,IACF;AAAA,IACA,KAAK;AAAA,MACH,SAAS;AAAA,MACT,aAAa;AAAA,MACb,aAAa;AAAA,QACX,aAAa;AAAA,QACb,QAAQ;AAAA,MACV;AAAA,MACA,WAAW;AAAA,QACT;AAAA,UACE,QAAQ;AAAA,UACR,aAAa;AAAA,UACb,QAAQ;AAAA,QACV;AAAA,MACF;AAAA,MACA,QAAQ;AAAA,QACN,EAAE,QAAQ,KAAK,aAAa,mBAAmB,QAAQ,oBAAoB;AAAA,QAC3E,EAAE,QAAQ,KAAK,aAAa,gBAAgB,QAAQ,oBAAoB;AAAA,QACxE,EAAE,QAAQ,KAAK,aAAa,kBAAkB,QAAQ,oBAAoB;AAAA,MAC5E;AAAA,IACF;AAAA,IACA,QAAQ;AAAA,MACN,SAAS;AAAA,MACT,aAAa;AAAA,MACb,OAAO,EAAE,OAAO,EAAE,IAAI,EAAE,OAAO,EAAE,KAAK,EAAE,SAAS,iBAAiB,EAAE,CAAC;AAAA,MACrE,WAAW;AAAA,QACT,EAAE,QAAQ,KAAK,aAAa,gBAAgB,QAAQ,iBAAiB;AAAA,MACvE;AAAA,MACA,QAAQ;AAAA,QACN,EAAE,QAAQ,KAAK,aAAa,0BAA0B,QAAQ,oBAAoB;AAAA,QAClF,EAAE,QAAQ,KAAK,aAAa,gBAAgB,QAAQ,oBAAoB;AAAA,QACxE,EAAE,QAAQ,KAAK,aAAa,kBAAkB,QAAQ,oBAAoB;AAAA,MAC5E;AAAA,IACF;AAAA,EACF;AACF;",
|
|
4
|
+
"sourcesContent": ["/* eslint-disable @typescript-eslint/no-explicit-any */\nimport { NextResponse } from 'next/server'\nimport { z } from 'zod'\nimport type { OpenApiRouteDoc } from '@open-mercato/shared/lib/openapi'\nimport { logCrudAccess, makeCrudRoute } from '@open-mercato/shared/lib/crud/factory'\nimport { CrudHttpError } from '@open-mercato/shared/lib/crud/errors'\nimport { getAuthFromRequest } from '@open-mercato/shared/lib/auth/server'\nimport { createRequestContainer } from '@open-mercato/shared/lib/di/container'\nimport { Role, RoleAcl, UserRole } from '@open-mercato/core/modules/auth/data/entities'\nimport { Tenant } from '@open-mercato/core/modules/directory/data/entities'\nimport { E } from '#generated/entities.ids.generated'\nimport { loadCustomFieldValues } from '@open-mercato/shared/lib/crud/custom-fields'\nimport { findWithDecryption } from '@open-mercato/shared/lib/encryption/find'\nimport type { EntityManager, FilterQuery } from '@mikro-orm/postgresql'\nimport { roleCrudEvents, roleCrudIndexer } from '@open-mercato/core/modules/auth/commands/roles'\nimport { escapeLikePattern } from '@open-mercato/shared/lib/db/escapeLikePattern'\nimport { assertActorCanModifySuperAdminRoleTarget } from '@open-mercato/core/modules/auth/lib/grantChecks'\nimport { enforceRoleTenantAccess } from '@open-mercato/core/modules/auth/lib/roleTenantGuard'\nimport type { RbacService } from '@open-mercato/core/modules/auth/services/rbacService'\nimport { runWithCacheTenant } from '@open-mercato/cache'\nimport {\n buildCollectionTags,\n isCrudCacheEnabled,\n resolveCrudCache,\n} from '@open-mercato/shared/lib/crud/cache'\nimport { createLogger } from '@open-mercato/shared/lib/logger'\n\nconst ROLES_LIST_CACHE_TTL_MS = 120_000\nconst logger = createLogger('auth').child({ component: 'roles' })\n\nfunction buildRolesListCacheKey(scope: {\n tenantId: string | null\n isSuperAdmin: boolean\n actorTenantId: string | null\n tenantFilter: string | null\n id?: string\n page: number\n pageSize: number\n search?: string\n}): string {\n const parts = [\n 'auth:roles:list',\n `tenant:${scope.tenantId ?? 'null'}`,\n `super:${scope.isSuperAdmin ? '1' : '0'}`,\n `actor:${scope.actorTenantId ?? 'null'}`,\n `filter:${scope.tenantFilter ?? 'null'}`,\n `id:${scope.id ?? ''}`,\n `page:${scope.page}`,\n `size:${scope.pageSize}`,\n `q:${scope.search ?? ''}`,\n ]\n return parts.join('|')\n}\n\nconst querySchema = 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 tenantId: z.string().uuid().optional(),\n}).passthrough()\n\nconst roleCreateSchema = z.object({\n name: z.string().min(2).max(100),\n tenantId: z.string().uuid().optional(),\n})\n\nconst roleUpdateSchema = z.object({\n id: z.string().uuid(),\n name: z.string().min(2).max(100).optional(),\n tenantId: z.string().uuid().optional(),\n})\n\nconst roleListItemSchema = z.object({\n id: z.string().uuid(),\n name: z.string(),\n usersCount: z.number().int().nonnegative(),\n tenantId: z.string().uuid().nullable(),\n tenantIds: z.array(z.string().uuid()).optional(),\n tenantName: z.string().nullable(),\n updatedAt: z.string().nullable().optional(),\n})\n\nconst roleListResponseSchema = z.object({\n items: z.array(roleListItemSchema),\n total: z.number().int().nonnegative(),\n totalPages: z.number().int().positive(),\n isSuperAdmin: z.boolean().optional(),\n})\n\nconst okResponseSchema = z.object({ ok: z.literal(true) })\n\nconst errorResponseSchema = z.object({ error: z.string() })\n\nconst routeMetadata = {\n GET: { requireAuth: true, requireFeatures: ['auth.roles.list'] },\n POST: { requireAuth: true, requireFeatures: ['auth.roles.manage'] },\n PUT: { requireAuth: true, requireFeatures: ['auth.roles.manage'] },\n DELETE: { requireAuth: true, requireFeatures: ['auth.roles.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: Role,\n idField: 'id',\n orgField: null,\n tenantField: null,\n softDeleteField: 'deletedAt',\n },\n events: roleCrudEvents,\n indexer: roleCrudIndexer,\n actions: {\n create: {\n commandId: 'auth.roles.create',\n schema: rawBodySchema,\n mapInput: async ({ parsed, ctx }) =>\n enforceRoleTenantAccess('create', parsed, { auth: ctx.auth, container: ctx.container }),\n response: ({ result }) => ({ id: String(result.id) }),\n status: 201,\n },\n update: {\n commandId: 'auth.roles.update',\n schema: rawBodySchema,\n mapInput: async ({ parsed, ctx }) => {\n if (ctx.request && typeof parsed.id === 'string' && parsed.id.length) {\n await assertCanModifySuperAdminRole(ctx.request, parsed.id)\n }\n return enforceRoleTenantAccess('update', parsed, { auth: ctx.auth, container: ctx.container })\n },\n response: () => ({ ok: true }),\n },\n delete: {\n commandId: 'auth.roles.delete',\n mapInput: async ({ parsed, raw, ctx }) => {\n const targetId = resolveDeleteTargetId(parsed, raw)\n if (ctx.request && targetId) {\n await assertCanModifySuperAdminRole(ctx.request, targetId)\n }\n return enforceRoleTenantAccess('delete', parsed, { auth: ctx.auth, container: ctx.container })\n },\n response: () => ({ ok: true }),\n },\n },\n})\n\nexport async function GET(req: Request) {\n const auth = await getAuthFromRequest(req)\n if (!auth) return NextResponse.json({ items: [], total: 0, totalPages: 1 })\n const url = new URL(req.url)\n const parsed = querySchema.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 tenantId: url.searchParams.get('tenantId') || undefined,\n })\n if (!parsed.success) return NextResponse.json({ items: [], total: 0, totalPages: 1 })\n const container = await createRequestContainer()\n const em = (container.resolve('em') as EntityManager)\n let isSuperAdmin = false\n try {\n if (auth.sub) {\n const rbacService = container.resolve('rbacService') as any\n const acl = await rbacService.loadAcl(auth.sub, { tenantId: auth.tenantId ?? null, organizationId: auth.orgId ?? null })\n isSuperAdmin = !!acl?.isSuperAdmin\n }\n } catch (err) {\n logger.error('Failed to resolve rbac', { err })\n }\n const actorTenantId = auth.tenantId ? String(auth.tenantId) : null\n if (!isSuperAdmin && !actorTenantId) {\n return NextResponse.json({ items: [], total: 0, totalPages: 1, isSuperAdmin })\n }\n const { id, page, pageSize, search, tenantId: requestedTenantId } = parsed.data\n const tenantFilter = isSuperAdmin && requestedTenantId ? String(requestedTenantId) : null\n const cacheTenantId = isSuperAdmin ? tenantFilter : actorTenantId\n // An unfiltered super-admin response spans tenants and cannot be invalidated\n // by any one tenant's collection tags, so keep that view uncached.\n const cache = isCrudCacheEnabled() && cacheTenantId ? resolveCrudCache(container) : null\n const cacheKey = cache\n ? buildRolesListCacheKey({\n tenantId: cacheTenantId,\n isSuperAdmin,\n actorTenantId,\n tenantFilter,\n id,\n page,\n pageSize,\n search,\n })\n : null\n if (cache && cacheKey) {\n const cached = await runWithCacheTenant(cacheTenantId, () => cache.get(cacheKey))\n if (cached) return NextResponse.json(cached)\n }\n let superAdminRoleIds: Set<string> | null = null\n if (!isSuperAdmin && actorTenantId) {\n const superAdminAcls = await findWithDecryption(em, RoleAcl, { tenantId: actorTenantId, isSuperAdmin: true }, {}, { tenantId: actorTenantId, organizationId: null })\n if (superAdminAcls.length) {\n superAdminRoleIds = new Set(\n superAdminAcls\n .map((acl) => {\n const roleRef = acl.role\n const idValue = roleRef?.id\n return idValue ? String(idValue) : null\n })\n .filter((id): id is string => !!id),\n )\n } else {\n superAdminRoleIds = new Set()\n }\n }\n const filters: any[] = [{ deletedAt: null }]\n if (id) filters.push({ id })\n if (search) filters.push({ name: { $ilike: `%${escapeLikePattern(search)}%` } })\n if (!isSuperAdmin && actorTenantId) {\n filters.push({ tenantId: actorTenantId })\n filters.push({ name: { $ne: 'superadmin' } })\n if (superAdminRoleIds && superAdminRoleIds.size) {\n filters.push({ id: { $nin: Array.from(superAdminRoleIds) } })\n }\n } else if (tenantFilter) {\n filters.push({ tenantId: tenantFilter })\n }\n const where = filters.length > 1 ? { $and: filters } : filters[0]\n const [rows, count] = await em.findAndCount(Role, where, { limit: pageSize, offset: (page - 1) * pageSize })\n const roleIds = rows.map((r: any) => String(r.id))\n const counts: Record<string, number> = {}\n if (roleIds.length) {\n const userRoleFilter: FilterQuery<UserRole> = { role: { $in: roleIds }, deletedAt: null }\n const links = await findWithDecryption(em, UserRole, userRoleFilter, {}, { tenantId: null, organizationId: null })\n for (const l of links) {\n const rid = String((l as any).role?.id || (l as any).role)\n counts[rid] = (counts[rid] || 0) + 1\n }\n }\n const roleTenantIds = rows\n .map((role: any) => (role.tenantId ? String(role.tenantId) : null))\n .filter((tenantId): tenantId is string => typeof tenantId === 'string' && tenantId.length > 0)\n const uniqueTenantIds = Array.from(new Set(roleTenantIds))\n let tenantMap: Record<string, string> = {}\n if (uniqueTenantIds.length) {\n const tenants = await findWithDecryption(em, Tenant, { id: { $in: uniqueTenantIds as any }, deletedAt: null }, {}, { tenantId: null, organizationId: null })\n tenantMap = tenants.reduce<Record<string, string>>((acc, tenant) => {\n const tid = tenant?.id ? String(tenant.id) : null\n if (!tid) return acc\n const rawName = (tenant as any)?.name\n const name = typeof rawName === 'string' && rawName.length > 0 ? rawName : tid\n acc[tid] = name\n return acc\n }, {})\n }\n const tenantByRole: Record<string, string | null> = {}\n for (const role of rows) {\n const rid = String(role.id)\n tenantByRole[rid] = role.tenantId ? String(role.tenantId) : null\n }\n const tenantFallbacks = Array.from(new Set<string | null>([\n auth.tenantId ?? null,\n tenantFilter ?? null,\n ...Object.values(tenantByRole),\n ]))\n const cfByRole = roleIds.length\n ? await loadCustomFieldValues({\n em,\n entityId: E.auth.role,\n recordIds: roleIds,\n tenantIdByRecord: tenantByRole,\n tenantFallbacks,\n })\n : {}\n const items = rows.map((r: any) => {\n const idStr = String(r.id)\n const tenantId = tenantByRole[idStr]\n const tenantName = tenantId ? tenantMap[tenantId] ?? tenantId : null\n const exposeTenant = isSuperAdmin || (tenantId && auth.tenantId && tenantId === auth.tenantId)\n return {\n id: idStr,\n name: String(r.name),\n usersCount: counts[idStr] || 0,\n tenantId: tenantId ?? null,\n tenantIds: exposeTenant && tenantId ? [tenantId] : [],\n tenantName: exposeTenant ? tenantName : null,\n updatedAt: r.updatedAt instanceof Date ? r.updatedAt.toISOString() : null,\n ...(cfByRole[idStr] || {}),\n }\n })\n const totalPages = Math.max(1, Math.ceil(count / pageSize))\n await logCrudAccess({\n container,\n auth,\n request: req,\n items,\n idField: 'id',\n resourceKind: 'auth.role',\n organizationId: null,\n tenantId: auth.tenantId ?? null,\n query: parsed.data,\n accessType: id ? 'read:item' : undefined,\n })\n const payload = { items, total: count, totalPages, isSuperAdmin }\n if (cache && cacheKey) {\n try {\n await runWithCacheTenant(cacheTenantId, () =>\n cache.set(cacheKey, payload, {\n ttl: ROLES_LIST_CACHE_TTL_MS,\n tags: [\n // Roles are tenant-level (org:null) \u2014 flushed on auth.roles.* command writes.\n ...buildCollectionTags('auth.role', cacheTenantId, [null]),\n // usersCount derives from UserRole grants \u2014 auth.users.* command writes\n // flush both tenant-level and record-organization collection tags.\n ...buildCollectionTags('auth.user', cacheTenantId, [null]),\n // Role-ACL feature changes flush rbac:tenant:<T> from the roles/acl route.\n `rbac:tenant:${cacheTenantId ?? 'null'}`,\n ],\n }),\n )\n } catch (err) {\n logger.warn('Failed to set roles list cache', { err })\n }\n }\n return NextResponse.json(payload)\n}\n\nexport const POST = crud.POST\nexport const PUT = crud.PUT\nexport const DELETE = async (req: Request) => {\n const targetId = new URL(req.url).searchParams.get('id')\n if (targetId) {\n try {\n await assertCanModifySuperAdminRole(req, targetId)\n } catch (err) {\n if (err instanceof CrudHttpError) {\n return NextResponse.json(err.body, { status: err.status })\n }\n throw err\n }\n }\n return crud.DELETE(req)\n}\n\nasync function assertCanModifySuperAdminRole(req: Request, targetRoleId: string) {\n const auth = await getAuthFromRequest(req)\n if (!auth?.sub) throw new CrudHttpError(401, { error: 'Unauthorized' })\n const container = await createRequestContainer()\n const em = container.resolve('em') as EntityManager\n await assertActorCanModifySuperAdminRoleTarget({\n em,\n rbacService: container.resolve('rbacService') as RbacService,\n actorUserId: auth.sub,\n tenantId: auth.tenantId ?? null,\n organizationId: auth.orgId ?? null,\n targetRoleId,\n })\n}\n\nfunction resolveDeleteTargetId(parsed: unknown, raw: unknown): string | null {\n const fromParsed = readId(parsed as Record<string, unknown> | null | undefined)\n if (fromParsed) return fromParsed\n const rawRecord = raw as { body?: Record<string, unknown>; query?: Record<string, unknown> } | null | undefined\n return readId(rawRecord?.query) ?? readId(rawRecord?.body)\n}\n\nfunction readId(record: Record<string, unknown> | null | undefined): string | null {\n const value = record?.id\n return typeof value === 'string' && value.length > 0 ? value : null\n}\n\nexport const openApi: OpenApiRouteDoc = {\n tag: 'Authentication & Accounts',\n summary: 'Role management',\n methods: {\n GET: {\n summary: 'List roles',\n description:\n 'Returns available roles within the current tenant. Super administrators receive visibility across tenants.',\n query: querySchema,\n responses: [\n { status: 200, description: 'Role collection', schema: roleListResponseSchema },\n ],\n },\n POST: {\n summary: 'Create role',\n description: 'Creates a new role anchored to the caller\\'s tenant. Non-superadmins cannot target another tenant; supplying a foreign `tenantId` is rejected.',\n requestBody: {\n contentType: 'application/json',\n schema: roleCreateSchema,\n },\n responses: [\n {\n status: 201,\n description: 'Role created',\n schema: z.object({ id: z.string().uuid() }),\n },\n ],\n errors: [\n { status: 400, description: 'Invalid payload', schema: errorResponseSchema },\n { status: 401, description: 'Unauthorized', schema: errorResponseSchema },\n ],\n },\n PUT: {\n summary: 'Update role',\n description: 'Updates mutable fields on an existing role.',\n requestBody: {\n contentType: 'application/json',\n schema: roleUpdateSchema,\n },\n responses: [\n {\n status: 200,\n description: 'Role updated',\n schema: okResponseSchema,\n },\n ],\n errors: [\n { status: 400, description: 'Invalid payload', schema: errorResponseSchema },\n { status: 401, description: 'Unauthorized', schema: errorResponseSchema },\n { status: 404, description: 'Role not found', schema: errorResponseSchema },\n ],\n },\n DELETE: {\n summary: 'Delete role',\n description: 'Deletes a role by identifier. Fails when users remain assigned.',\n query: z.object({ id: z.string().uuid().describe('Role identifier') }),\n responses: [\n { status: 200, description: 'Role deleted', schema: okResponseSchema },\n ],\n errors: [\n { status: 400, description: 'Role cannot be deleted', schema: errorResponseSchema },\n { status: 401, description: 'Unauthorized', schema: errorResponseSchema },\n { status: 404, description: 'Role not found', schema: errorResponseSchema },\n ],\n },\n },\n}\n"],
|
|
5
|
+
"mappings": "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",
|
|
6
6
|
"names": ["id"]
|
|
7
7
|
}
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"version": 3,
|
|
3
3
|
"sources": ["../../../../src/modules/entities/lib/field-definitions.ts"],
|
|
4
|
-
"sourcesContent": ["import type { EntityManager } from '@mikro-orm/postgresql'\nimport { CustomFieldDef } from '../data/entities'\nimport type { CustomFieldDefinition } from '@open-mercato/shared/modules/entities'\n\nexport type FieldSetInput = {\n entity: string\n fields: CustomFieldDefinition[]\n source?: string\n}\n\nexport type EnsureFieldDefinitionsOptions = {\n organizationId: string | null\n tenantId: string | null\n dryRun?: boolean\n createOnly?: boolean\n}\n\nexport type EnsureFieldDefinitionsResult = {\n created: number\n updated: number\n unchanged: number\n}\n\nconst CONFIG_PASSTHROUGH_KEYS: Array<keyof CustomFieldDefinition> = [\n 'label',\n 'description',\n 'fieldset',\n 'fieldsets',\n 'group',\n 'options',\n 'optionsUrl',\n 'defaultValue',\n 'required',\n 'multi',\n 'filterable',\n 'formEditable',\n 'listVisible',\n 'indexed',\n 'editor',\n 'input',\n 'relatedEntityId',\n 'dictionaryId',\n 'dictionaryInlineCreate',\n 'validation',\n 'maxAttachmentSizeMb',\n 'acceptExtensions',\n 'sourceMetadata',\n]\n\nfunction normalizeValue(value: unknown): unknown {\n if (Array.isArray(value)) return value.map((item) => normalizeValue(item))\n if (value && typeof value === 'object') {\n return Object.keys(value as Record<string, unknown>)\n .sort((a, b) => a.localeCompare(b))\n .reduce<Record<string, unknown>>((acc, key) => {\n acc[key] = normalizeValue((value as Record<string, unknown>)[key])\n return acc\n }, {})\n }\n return value\n}\n\nfunction configEquals(a: unknown, b: unknown): boolean {\n return JSON.stringify(normalizeValue(a ?? null)) === JSON.stringify(normalizeValue(b ?? null))\n}\n\nexport async function ensureCustomFieldDefinitions(\n em: EntityManager,\n sets: FieldSetInput[],\n scope: EnsureFieldDefinitionsOptions\n): Promise<EnsureFieldDefinitionsResult> {\n let created = 0\n let updated = 0\n let unchanged = 0\n\n // Prefetch every existing definition the batch could touch in a single query,\n // then index by composite key so the nested loop never issues per-field lookups.\n const entityIds = Array.from(new Set(sets.map((set) => set.entity)))\n const fieldKeys = Array.from(new Set(sets.flatMap((set) => set.fields.map((field) => field.key))))\n const existingByKey = new Map<string, CustomFieldDef>()\n if (entityIds.length > 0 && fieldKeys.length > 0) {\n const existingDefs = await em.find(CustomFieldDef, {\n entityId: { $in: entityIds },\n organizationId: scope.organizationId,\n tenantId: scope.tenantId,\n key: { $in: fieldKeys },\n })\n for (const def of existingDefs) {\n existingByKey.set(`${def.entityId}|${def.key}`, def)\n }\n }\n\n let dirty = false\n\n for (const set of sets) {\n for (const field of set.fields) {\n const existing = existingByKey.get(`${set.entity}|${field.key}`) ?? null\n const configJson: Record<string, unknown> = {}\n\n for (const key of CONFIG_PASSTHROUGH_KEYS) {\n const value = field[key]\n if (value !== undefined) configJson[key] = value as unknown\n }\n\n if (!existing) {\n if (!scope.dryRun) {\n const createdDef = em.create(CustomFieldDef, {\n entityId: set.entity,\n organizationId: scope.organizationId,\n tenantId: scope.tenantId,\n key: field.key,\n kind: field.kind,\n configJson,\n isActive: true,\n createdAt: new Date(),\n updatedAt: new Date(),\n })\n em.persist(createdDef)\n // Track so duplicate (entity, key) pairs within the batch update in memory instead of double-inserting.\n existingByKey.set(`${set.entity}|${field.key}`, createdDef)\n dirty = true\n }\n created++\n continue\n }\n\n const kindChanged = existing.kind !== field.kind\n const configChanged = !configEquals(existing.configJson ?? null, configJson)\n const needsActivation = existing.isActive !== true || existing.deletedAt != null\n if (scope.createOnly) {\n unchanged++\n continue\n }\n if (!kindChanged && !configChanged && !needsActivation) {\n unchanged++\n continue\n }\n\n if (!scope.dryRun) {\n existing.kind = field.kind\n ;(existing as any).configJson = configJson\n existing.isActive = true\n existing.updatedAt = new Date()\n if (existing.deletedAt) existing.deletedAt = null\n em.persist(existing)\n dirty = true\n }\n updated++\n }\n }\n\n if (dirty) {\n // Single flush for the whole batch instead of one round trip per field.\n await em.flush()\n }\n\n return { created, updated, unchanged }\n}\n"],
|
|
5
|
-
"mappings": "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",
|
|
4
|
+
"sourcesContent": ["import type { EntityManager } from '@mikro-orm/postgresql'\nimport { CustomFieldDef } from '../data/entities'\nimport type { CustomFieldDefinition } from '@open-mercato/shared/modules/entities'\n\nexport type FieldSetInput = {\n entity: string\n fields: CustomFieldDefinition[]\n source?: string\n}\n\nexport type EnsureFieldDefinitionsOptions = {\n organizationId: string | null\n tenantId: string | null\n dryRun?: boolean\n createOnly?: boolean\n}\n\nexport type EnsureFieldDefinitionsResult = {\n created: number\n updated: number\n unchanged: number\n}\n\nconst CONFIG_PASSTHROUGH_KEYS: Array<keyof CustomFieldDefinition> = [\n 'label',\n 'description',\n 'fieldset',\n 'fieldsets',\n 'group',\n 'options',\n 'optionsUrl',\n 'defaultValue',\n 'required',\n 'multi',\n 'filterable',\n 'formEditable',\n 'listVisible',\n 'indexed',\n 'priority',\n 'editor',\n 'input',\n 'relatedEntityId',\n 'dictionaryId',\n 'dictionaryInlineCreate',\n 'validation',\n 'maxAttachmentSizeMb',\n 'acceptExtensions',\n 'sourceMetadata',\n]\n\nfunction normalizeValue(value: unknown): unknown {\n if (Array.isArray(value)) return value.map((item) => normalizeValue(item))\n if (value && typeof value === 'object') {\n return Object.keys(value as Record<string, unknown>)\n .sort((a, b) => a.localeCompare(b))\n .reduce<Record<string, unknown>>((acc, key) => {\n acc[key] = normalizeValue((value as Record<string, unknown>)[key])\n return acc\n }, {})\n }\n return value\n}\n\nfunction configEquals(a: unknown, b: unknown): boolean {\n return JSON.stringify(normalizeValue(a ?? null)) === JSON.stringify(normalizeValue(b ?? null))\n}\n\nexport async function ensureCustomFieldDefinitions(\n em: EntityManager,\n sets: FieldSetInput[],\n scope: EnsureFieldDefinitionsOptions\n): Promise<EnsureFieldDefinitionsResult> {\n let created = 0\n let updated = 0\n let unchanged = 0\n\n // Prefetch every existing definition the batch could touch in a single query,\n // then index by composite key so the nested loop never issues per-field lookups.\n const entityIds = Array.from(new Set(sets.map((set) => set.entity)))\n const fieldKeys = Array.from(new Set(sets.flatMap((set) => set.fields.map((field) => field.key))))\n const existingByKey = new Map<string, CustomFieldDef>()\n if (entityIds.length > 0 && fieldKeys.length > 0) {\n const existingDefs = await em.find(CustomFieldDef, {\n entityId: { $in: entityIds },\n organizationId: scope.organizationId,\n tenantId: scope.tenantId,\n key: { $in: fieldKeys },\n })\n for (const def of existingDefs) {\n existingByKey.set(`${def.entityId}|${def.key}`, def)\n }\n }\n\n let dirty = false\n\n for (const set of sets) {\n for (const field of set.fields) {\n const existing = existingByKey.get(`${set.entity}|${field.key}`) ?? null\n const configJson: Record<string, unknown> = {}\n\n for (const key of CONFIG_PASSTHROUGH_KEYS) {\n const value = field[key]\n if (value !== undefined) configJson[key] = value as unknown\n }\n\n if (!existing) {\n if (!scope.dryRun) {\n const createdDef = em.create(CustomFieldDef, {\n entityId: set.entity,\n organizationId: scope.organizationId,\n tenantId: scope.tenantId,\n key: field.key,\n kind: field.kind,\n configJson,\n isActive: true,\n createdAt: new Date(),\n updatedAt: new Date(),\n })\n em.persist(createdDef)\n // Track so duplicate (entity, key) pairs within the batch update in memory instead of double-inserting.\n existingByKey.set(`${set.entity}|${field.key}`, createdDef)\n dirty = true\n }\n created++\n continue\n }\n\n const kindChanged = existing.kind !== field.kind\n const configChanged = !configEquals(existing.configJson ?? null, configJson)\n const needsActivation = existing.isActive !== true || existing.deletedAt != null\n if (scope.createOnly) {\n unchanged++\n continue\n }\n if (!kindChanged && !configChanged && !needsActivation) {\n unchanged++\n continue\n }\n\n if (!scope.dryRun) {\n existing.kind = field.kind\n ;(existing as any).configJson = configJson\n existing.isActive = true\n existing.updatedAt = new Date()\n if (existing.deletedAt) existing.deletedAt = null\n em.persist(existing)\n dirty = true\n }\n updated++\n }\n }\n\n if (dirty) {\n // Single flush for the whole batch instead of one round trip per field.\n await em.flush()\n }\n\n return { created, updated, unchanged }\n}\n"],
|
|
5
|
+
"mappings": "AACA,SAAS,sBAAsB;AAsB/B,MAAM,0BAA8D;AAAA,EAClE;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AACF;AAEA,SAAS,eAAe,OAAyB;AAC/C,MAAI,MAAM,QAAQ,KAAK,EAAG,QAAO,MAAM,IAAI,CAAC,SAAS,eAAe,IAAI,CAAC;AACzE,MAAI,SAAS,OAAO,UAAU,UAAU;AACtC,WAAO,OAAO,KAAK,KAAgC,EAChD,KAAK,CAAC,GAAG,MAAM,EAAE,cAAc,CAAC,CAAC,EACjC,OAAgC,CAAC,KAAK,QAAQ;AAC7C,UAAI,GAAG,IAAI,eAAgB,MAAkC,GAAG,CAAC;AACjE,aAAO;AAAA,IACT,GAAG,CAAC,CAAC;AAAA,EACT;AACA,SAAO;AACT;AAEA,SAAS,aAAa,GAAY,GAAqB;AACrD,SAAO,KAAK,UAAU,eAAe,KAAK,IAAI,CAAC,MAAM,KAAK,UAAU,eAAe,KAAK,IAAI,CAAC;AAC/F;AAEA,eAAsB,6BACpB,IACA,MACA,OACuC;AACvC,MAAI,UAAU;AACd,MAAI,UAAU;AACd,MAAI,YAAY;AAIhB,QAAM,YAAY,MAAM,KAAK,IAAI,IAAI,KAAK,IAAI,CAAC,QAAQ,IAAI,MAAM,CAAC,CAAC;AACnE,QAAM,YAAY,MAAM,KAAK,IAAI,IAAI,KAAK,QAAQ,CAAC,QAAQ,IAAI,OAAO,IAAI,CAAC,UAAU,MAAM,GAAG,CAAC,CAAC,CAAC;AACjG,QAAM,gBAAgB,oBAAI,IAA4B;AACtD,MAAI,UAAU,SAAS,KAAK,UAAU,SAAS,GAAG;AAChD,UAAM,eAAe,MAAM,GAAG,KAAK,gBAAgB;AAAA,MACjD,UAAU,EAAE,KAAK,UAAU;AAAA,MAC3B,gBAAgB,MAAM;AAAA,MACtB,UAAU,MAAM;AAAA,MAChB,KAAK,EAAE,KAAK,UAAU;AAAA,IACxB,CAAC;AACD,eAAW,OAAO,cAAc;AAC9B,oBAAc,IAAI,GAAG,IAAI,QAAQ,IAAI,IAAI,GAAG,IAAI,GAAG;AAAA,IACrD;AAAA,EACF;AAEA,MAAI,QAAQ;AAEZ,aAAW,OAAO,MAAM;AACtB,eAAW,SAAS,IAAI,QAAQ;AAC9B,YAAM,WAAW,cAAc,IAAI,GAAG,IAAI,MAAM,IAAI,MAAM,GAAG,EAAE,KAAK;AACpE,YAAM,aAAsC,CAAC;AAE7C,iBAAW,OAAO,yBAAyB;AACzC,cAAM,QAAQ,MAAM,GAAG;AACvB,YAAI,UAAU,OAAW,YAAW,GAAG,IAAI;AAAA,MAC7C;AAEA,UAAI,CAAC,UAAU;AACb,YAAI,CAAC,MAAM,QAAQ;AACjB,gBAAM,aAAa,GAAG,OAAO,gBAAgB;AAAA,YAC3C,UAAU,IAAI;AAAA,YACd,gBAAgB,MAAM;AAAA,YACtB,UAAU,MAAM;AAAA,YAChB,KAAK,MAAM;AAAA,YACX,MAAM,MAAM;AAAA,YACZ;AAAA,YACA,UAAU;AAAA,YACV,WAAW,oBAAI,KAAK;AAAA,YACpB,WAAW,oBAAI,KAAK;AAAA,UACtB,CAAC;AACD,aAAG,QAAQ,UAAU;AAErB,wBAAc,IAAI,GAAG,IAAI,MAAM,IAAI,MAAM,GAAG,IAAI,UAAU;AAC1D,kBAAQ;AAAA,QACV;AACA;AACA;AAAA,MACF;AAEA,YAAM,cAAc,SAAS,SAAS,MAAM;AAC5C,YAAM,gBAAgB,CAAC,aAAa,SAAS,cAAc,MAAM,UAAU;AAC3E,YAAM,kBAAkB,SAAS,aAAa,QAAQ,SAAS,aAAa;AAC5E,UAAI,MAAM,YAAY;AACpB;AACA;AAAA,MACF;AACA,UAAI,CAAC,eAAe,CAAC,iBAAiB,CAAC,iBAAiB;AACtD;AACA;AAAA,MACF;AAEA,UAAI,CAAC,MAAM,QAAQ;AACjB,iBAAS,OAAO,MAAM;AACrB,QAAC,SAAiB,aAAa;AAChC,iBAAS,WAAW;AACpB,iBAAS,YAAY,oBAAI,KAAK;AAC9B,YAAI,SAAS,UAAW,UAAS,YAAY;AAC7C,WAAG,QAAQ,QAAQ;AACnB,gBAAQ;AAAA,MACV;AACA;AAAA,IACF;AAAA,EACF;AAEA,MAAI,OAAO;AAET,UAAM,GAAG,MAAM;AAAA,EACjB;AAEA,SAAO,EAAE,SAAS,SAAS,UAAU;AACvC;",
|
|
6
6
|
"names": []
|
|
7
7
|
}
|
|
@@ -17,6 +17,7 @@ const FIELD_DETAIL_KEYS = [
|
|
|
17
17
|
"formEditable",
|
|
18
18
|
"listVisible",
|
|
19
19
|
"indexed",
|
|
20
|
+
"priority",
|
|
20
21
|
"editor",
|
|
21
22
|
"input",
|
|
22
23
|
"relatedEntityId",
|
|
@@ -71,12 +72,19 @@ function buildAggregatedConfigs() {
|
|
|
71
72
|
}
|
|
72
73
|
function resolveFields(fieldSets) {
|
|
73
74
|
const byKey = /* @__PURE__ */ new Map();
|
|
75
|
+
const declarationOrder = /* @__PURE__ */ new Map();
|
|
74
76
|
for (const set of fieldSets) {
|
|
75
77
|
for (const field of set.fields ?? []) {
|
|
78
|
+
if (!declarationOrder.has(field.key)) declarationOrder.set(field.key, declarationOrder.size);
|
|
76
79
|
byKey.set(field.key, { ...field });
|
|
77
80
|
}
|
|
78
81
|
}
|
|
79
|
-
|
|
82
|
+
const resolved = Array.from(byKey.values()).map((field) => typeof field.priority === "number" ? field : { ...field, priority: declarationOrder.get(field.key) ?? 0 });
|
|
83
|
+
return resolved.sort((a, b) => {
|
|
84
|
+
const byPriority = (a.priority ?? 0) - (b.priority ?? 0);
|
|
85
|
+
if (byPriority !== 0) return byPriority;
|
|
86
|
+
return (declarationOrder.get(a.key) ?? 0) - (declarationOrder.get(b.key) ?? 0);
|
|
87
|
+
});
|
|
80
88
|
}
|
|
81
89
|
function normalizeField(field) {
|
|
82
90
|
const payload = {
|
|
@@ -247,6 +255,7 @@ function getAggregatedCustomEntityConfigs() {
|
|
|
247
255
|
export {
|
|
248
256
|
getAggregatedCustomEntityConfigs,
|
|
249
257
|
installCustomEntitiesFromModules,
|
|
250
|
-
listCustomEntityIds
|
|
258
|
+
listCustomEntityIds,
|
|
259
|
+
resolveFields
|
|
251
260
|
};
|
|
252
261
|
//# sourceMappingURL=install-from-ce.js.map
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"version": 3,
|
|
3
3
|
"sources": ["../../../../src/modules/entities/lib/install-from-ce.ts"],
|
|
4
|
-
"sourcesContent": ["import crypto from 'node:crypto'\nimport type { EntityManager } from '@mikro-orm/postgresql'\nimport type { CacheStrategy } from '@open-mercato/cache/types'\nimport type { CustomFieldDefinition, CustomFieldSet, CustomEntitySpec } from '@open-mercato/shared/modules/entities'\nimport { Tenant } from '@open-mercato/core/modules/directory/data/entities'\nimport { getModules } from '@open-mercato/shared/lib/i18n/server'\nimport { getEntityIds } from '@open-mercato/shared/lib/encryption/entityIds'\nimport { ensureCustomFieldDefinitions } from './field-definitions'\nimport { upsertCustomEntity, type UpsertCustomEntityResult } from './register'\nimport { invalidateDefinitionsCache } from '../api/definitions.cache'\n\ntype InstallScope = {\n tenantId: string | null\n}\n\nexport type InstallEntitiesOptions = {\n tenantIds?: string[]\n entityIds?: string[]\n includeGlobal?: boolean\n dryRun?: boolean\n createOnly?: boolean\n force?: boolean\n logger?: (message: string) => void\n}\n\nexport type InstallEntitiesResult = {\n processed: number\n synchronized: number\n skipped: number\n fieldChanges: number\n}\n\nexport type AggregatedEntityConfig = {\n entityId: string\n moduleIds: Set<string>\n spec?: CustomEntitySpec\n fieldSets: CustomFieldSet[]\n}\n\nconst FIELD_DETAIL_KEYS: Array<keyof CustomFieldDefinition> = [\n 'label',\n 'description',\n 'options',\n 'optionsUrl',\n 'defaultValue',\n 'required',\n 'multi',\n 'filterable',\n 'formEditable',\n 'listVisible',\n 'indexed',\n 'editor',\n 'input',\n 'relatedEntityId',\n 'validation',\n 'maxAttachmentSizeMb',\n 'acceptExtensions',\n]\n\nfunction normalizeValue(value: unknown): unknown {\n if (Array.isArray(value)) return value.map((item) => normalizeValue(item))\n if (value && typeof value === 'object') {\n return Object.keys(value as Record<string, unknown>)\n .sort((a, b) => a.localeCompare(b))\n .reduce<Record<string, unknown>>((acc, key) => {\n acc[key] = normalizeValue((value as Record<string, unknown>)[key])\n return acc\n }, {})\n }\n return value\n}\n\nfunction computeChecksum(payload: unknown): string {\n return crypto.createHash('md5').update(JSON.stringify(normalizeValue(payload))).digest('hex')\n}\n\nfunction systemEntityIds(): Set<string> {\n const ids = new Set<string>()\n const GeneratedEntities = getEntityIds()\n for (const moduleEntities of Object.values(GeneratedEntities)) {\n for (const id of Object.values(moduleEntities as Record<string, string>)) {\n ids.add(id)\n }\n }\n return ids\n}\n\nfunction buildAggregatedConfigs(): AggregatedEntityConfig[] {\n const map = new Map<string, AggregatedEntityConfig>()\n const modules = getModules()\n for (const mod of modules) {\n const moduleId = mod.id\n const entitySpecs = ((mod as any).customEntities as CustomEntitySpec[] | undefined) ?? []\n for (const spec of entitySpecs) {\n const existing = map.get(spec.id) ?? { entityId: spec.id, moduleIds: new Set<string>(), spec: undefined, fieldSets: [] }\n existing.moduleIds.add(moduleId)\n if (!existing.spec) existing.spec = spec\n map.set(spec.id, existing)\n }\n const fieldSets = ((mod as any).customFieldSets as CustomFieldSet[] | undefined) ?? []\n for (const set of fieldSets) {\n const existing = map.get(set.entity) ?? { entityId: set.entity, moduleIds: new Set<string>(), spec: undefined, fieldSets: [] }\n existing.moduleIds.add(moduleId)\n existing.fieldSets.push(set)\n map.set(set.entity, existing)\n }\n }\n return Array.from(map.values())\n}\n\nfunction resolveFields(fieldSets: CustomFieldSet[]): CustomFieldDefinition[] {\n const byKey = new Map<string, CustomFieldDefinition>()\n for (const set of fieldSets) {\n for (const field of set.fields ?? []) {\n byKey.set(field.key, { ...field })\n }\n }\n return Array.from(byKey.values()).sort((a, b) => a.key.localeCompare(b.key))\n}\n\nfunction normalizeField(field: CustomFieldDefinition) {\n const payload: Record<string, unknown> = {\n key: field.key,\n kind: field.kind,\n }\n for (const key of FIELD_DETAIL_KEYS) {\n const value = field[key]\n if (value !== undefined) payload[key] = value as unknown\n }\n if (field.id) payload.id = field.id\n return payload\n}\n\nfunction buildChecksumPayload(params: {\n entityId: string\n scope: InstallScope\n spec: CustomEntitySpec | undefined\n global: boolean\n fields: CustomFieldDefinition[]\n}) {\n const { entityId, scope, spec, global, fields } = params\n return {\n entityId,\n scope,\n global,\n label: spec?.label ?? null,\n description: spec?.description ?? null,\n labelField: spec?.labelField ?? null,\n defaultEditor: spec?.defaultEditor ?? null,\n showInSidebar: spec?.showInSidebar ?? false,\n accessRestricted: spec?.accessRestricted ?? false,\n fields: fields.map((f) => normalizeField(f)),\n }\n}\n\nexport async function installCustomEntitiesFromModules(\n em: EntityManager,\n cache: CacheStrategy | null | undefined,\n options: InstallEntitiesOptions = {}\n): Promise<InstallEntitiesResult> {\n const requestedEntityIds = options.entityIds\n ? new Set(options.entityIds.filter((id): id is string => typeof id === 'string' && id.length > 0))\n : null\n const aggregated = buildAggregatedConfigs().filter((entry) => {\n if (!requestedEntityIds) return true\n return requestedEntityIds.has(entry.entityId)\n })\n const systemIds = systemEntityIds()\n const includeGlobal = options.includeGlobal !== false\n const dryRun = options.dryRun === true\n const createOnly = options.createOnly === true\n const force = options.force === true\n const logger = options.logger\n\n let tenantIds: string[] | undefined\n if (options.tenantIds !== undefined) {\n tenantIds = Array.from(new Set(options.tenantIds.filter((id): id is string => typeof id === 'string' && id.length > 0)))\n }\n\n const ensureTenantIds = async (): Promise<string[]> => {\n if (tenantIds !== undefined) return tenantIds\n const rows = await em.find(Tenant, { deletedAt: null } as any, { fields: ['id'] as any })\n tenantIds = rows.map((row) => row.id)\n return tenantIds ?? []\n }\n\n let processed = 0\n let synchronized = 0\n let skipped = 0\n let fieldChanges = 0\n\n for (const entry of aggregated) {\n const { entityId } = entry\n const spec = entry.spec\n const fields = resolveFields(entry.fieldSets)\n const isSystem = systemIds.has(entityId)\n const registerEntity = !isSystem && !!spec\n const isGlobal = spec?.global === true\n\n const scopes: InstallScope[] = []\n if (isGlobal) {\n if (includeGlobal) scopes.push({ tenantId: null })\n } else {\n const ids = await ensureTenantIds()\n if (!ids.length) {\n skipped++\n continue\n }\n for (const tenantId of ids) scopes.push({ tenantId })\n }\n\n if (!scopes.length && fields.length === 0 && !registerEntity) {\n skipped++\n continue\n }\n\n for (const scope of scopes) {\n processed++\n const scopeKey = scope.tenantId ? `tenant:${scope.tenantId}` : 'global'\n const checksumPayload = buildChecksumPayload({\n entityId,\n scope,\n spec,\n global: isGlobal,\n fields,\n })\n const checksum = computeChecksum(checksumPayload)\n const cacheKey = `custom-entities:v1:${scopeKey}:${entityId}`\n\n if (!dryRun && !force && cache) {\n try {\n const cached = await cache.get(cacheKey)\n if (typeof cached === 'string' && cached === checksum) {\n skipped++\n continue\n }\n } catch {}\n }\n\n let entityResult: UpsertCustomEntityResult = 'unchanged'\n if (registerEntity) {\n entityResult = await upsertCustomEntity(em, entityId, {\n label: spec?.label ?? entityId,\n description: spec?.description ?? null,\n organizationId: null,\n tenantId: scope.tenantId,\n showInSidebar: spec?.showInSidebar ?? false,\n accessRestricted: spec?.accessRestricted ?? false,\n labelField: spec?.labelField ?? null,\n defaultEditor: spec?.defaultEditor ?? null,\n isActive: true,\n dryRun,\n createOnly,\n })\n }\n\n let fieldResult = { created: 0, updated: 0, unchanged: 0 }\n if (fields.length) {\n fieldResult = await ensureCustomFieldDefinitions(\n em,\n [{ entity: entityId, fields }],\n { organizationId: null, tenantId: scope.tenantId, dryRun, createOnly }\n )\n }\n\n const changed = (entityResult !== 'unchanged') || fieldResult.created > 0 || fieldResult.updated > 0\n const shouldInvalidateDefinitions = !dryRun && (changed || force)\n if (shouldInvalidateDefinitions) {\n await invalidateDefinitionsCache(cache ?? undefined, {\n tenantId: scope.tenantId,\n organizationId: null,\n entityIds: [entityId],\n })\n }\n\n if (changed) {\n synchronized++\n fieldChanges += fieldResult.created + fieldResult.updated\n if (!dryRun && cache) {\n try {\n await cache.set(cacheKey, checksum, { tags: [`custom-entity:${entityId}`, `custom-entity-scope:${scopeKey}`] })\n } catch {}\n }\n if (logger) {\n const parts: string[] = []\n if (entityResult !== 'unchanged') parts.push(`entity ${entityResult}`)\n if (fieldResult.created || fieldResult.updated) {\n parts.push(`fields +${fieldResult.created} / ~${fieldResult.updated}`)\n }\n logger(`Synced ${entityId} for ${scopeKey}${parts.length ? ` (${parts.join(', ')})` : ''}`)\n }\n } else {\n skipped++\n if (!dryRun && cache) {\n try {\n await cache.set(cacheKey, checksum, { tags: [`custom-entity:${entityId}`, `custom-entity-scope:${scopeKey}`] })\n } catch {}\n }\n }\n }\n }\n\n return { processed, synchronized, skipped, fieldChanges }\n}\n\nexport function listCustomEntityIds(): string[] {\n return buildAggregatedConfigs().map((entry) => entry.entityId)\n}\n\nexport function getAggregatedCustomEntityConfigs(): AggregatedEntityConfig[] {\n return buildAggregatedConfigs()\n}\n"],
|
|
5
|
-
"mappings": "AAAA,OAAO,YAAY;AAInB,SAAS,cAAc;AACvB,SAAS,kBAAkB;AAC3B,SAAS,oBAAoB;AAC7B,SAAS,oCAAoC;AAC7C,SAAS,0BAAyD;AAClE,SAAS,kCAAkC;AA8B3C,MAAM,oBAAwD;AAAA,EAC5D;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AACF;AAEA,SAAS,eAAe,OAAyB;AAC/C,MAAI,MAAM,QAAQ,KAAK,EAAG,QAAO,MAAM,IAAI,CAAC,SAAS,eAAe,IAAI,CAAC;AACzE,MAAI,SAAS,OAAO,UAAU,UAAU;AACtC,WAAO,OAAO,KAAK,KAAgC,EAChD,KAAK,CAAC,GAAG,MAAM,EAAE,cAAc,CAAC,CAAC,EACjC,OAAgC,CAAC,KAAK,QAAQ;AAC7C,UAAI,GAAG,IAAI,eAAgB,MAAkC,GAAG,CAAC;AACjE,aAAO;AAAA,IACT,GAAG,CAAC,CAAC;AAAA,EACT;AACA,SAAO;AACT;AAEA,SAAS,gBAAgB,SAA0B;AACjD,SAAO,OAAO,WAAW,KAAK,EAAE,OAAO,KAAK,UAAU,eAAe,OAAO,CAAC,CAAC,EAAE,OAAO,KAAK;AAC9F;AAEA,SAAS,kBAA+B;AACtC,QAAM,MAAM,oBAAI,IAAY;AAC5B,QAAM,oBAAoB,aAAa;AACvC,aAAW,kBAAkB,OAAO,OAAO,iBAAiB,GAAG;AAC7D,eAAW,MAAM,OAAO,OAAO,cAAwC,GAAG;AACxE,UAAI,IAAI,EAAE;AAAA,IACZ;AAAA,EACF;AACA,SAAO;AACT;AAEA,SAAS,yBAAmD;AAC1D,QAAM,MAAM,oBAAI,IAAoC;AACpD,QAAM,UAAU,WAAW;AAC3B,aAAW,OAAO,SAAS;AACzB,UAAM,WAAW,IAAI;AACrB,UAAM,cAAgB,IAAY,kBAAqD,CAAC;AACxF,eAAW,QAAQ,aAAa;AAC9B,YAAM,WAAW,IAAI,IAAI,KAAK,EAAE,KAAK,EAAE,UAAU,KAAK,IAAI,WAAW,oBAAI,IAAY,GAAG,MAAM,QAAW,WAAW,CAAC,EAAE;AACvH,eAAS,UAAU,IAAI,QAAQ;AAC/B,UAAI,CAAC,SAAS,KAAM,UAAS,OAAO;AACpC,UAAI,IAAI,KAAK,IAAI,QAAQ;AAAA,IAC3B;AACA,UAAM,YAAc,IAAY,mBAAoD,CAAC;AACrF,eAAW,OAAO,WAAW;AAC3B,YAAM,WAAW,IAAI,IAAI,IAAI,MAAM,KAAK,EAAE,UAAU,IAAI,QAAQ,WAAW,oBAAI,IAAY,GAAG,MAAM,QAAW,WAAW,CAAC,EAAE;AAC7H,eAAS,UAAU,IAAI,QAAQ;AAC/B,eAAS,UAAU,KAAK,GAAG;AAC3B,UAAI,IAAI,IAAI,QAAQ,QAAQ;AAAA,IAC9B;AAAA,EACF;AACA,SAAO,MAAM,KAAK,IAAI,OAAO,CAAC;AAChC;
|
|
4
|
+
"sourcesContent": ["import crypto from 'node:crypto'\nimport type { EntityManager } from '@mikro-orm/postgresql'\nimport type { CacheStrategy } from '@open-mercato/cache/types'\nimport type { CustomFieldDefinition, CustomFieldSet, CustomEntitySpec } from '@open-mercato/shared/modules/entities'\nimport { Tenant } from '@open-mercato/core/modules/directory/data/entities'\nimport { getModules } from '@open-mercato/shared/lib/i18n/server'\nimport { getEntityIds } from '@open-mercato/shared/lib/encryption/entityIds'\nimport { ensureCustomFieldDefinitions } from './field-definitions'\nimport { upsertCustomEntity, type UpsertCustomEntityResult } from './register'\nimport { invalidateDefinitionsCache } from '../api/definitions.cache'\n\ntype InstallScope = {\n tenantId: string | null\n}\n\nexport type InstallEntitiesOptions = {\n tenantIds?: string[]\n entityIds?: string[]\n includeGlobal?: boolean\n dryRun?: boolean\n createOnly?: boolean\n force?: boolean\n logger?: (message: string) => void\n}\n\nexport type InstallEntitiesResult = {\n processed: number\n synchronized: number\n skipped: number\n fieldChanges: number\n}\n\nexport type AggregatedEntityConfig = {\n entityId: string\n moduleIds: Set<string>\n spec?: CustomEntitySpec\n fieldSets: CustomFieldSet[]\n}\n\nconst FIELD_DETAIL_KEYS: Array<keyof CustomFieldDefinition> = [\n 'label',\n 'description',\n 'options',\n 'optionsUrl',\n 'defaultValue',\n 'required',\n 'multi',\n 'filterable',\n 'formEditable',\n 'listVisible',\n 'indexed',\n 'priority',\n 'editor',\n 'input',\n 'relatedEntityId',\n 'validation',\n 'maxAttachmentSizeMb',\n 'acceptExtensions',\n]\n\nfunction normalizeValue(value: unknown): unknown {\n if (Array.isArray(value)) return value.map((item) => normalizeValue(item))\n if (value && typeof value === 'object') {\n return Object.keys(value as Record<string, unknown>)\n .sort((a, b) => a.localeCompare(b))\n .reduce<Record<string, unknown>>((acc, key) => {\n acc[key] = normalizeValue((value as Record<string, unknown>)[key])\n return acc\n }, {})\n }\n return value\n}\n\nfunction computeChecksum(payload: unknown): string {\n return crypto.createHash('md5').update(JSON.stringify(normalizeValue(payload))).digest('hex')\n}\n\nfunction systemEntityIds(): Set<string> {\n const ids = new Set<string>()\n const GeneratedEntities = getEntityIds()\n for (const moduleEntities of Object.values(GeneratedEntities)) {\n for (const id of Object.values(moduleEntities as Record<string, string>)) {\n ids.add(id)\n }\n }\n return ids\n}\n\nfunction buildAggregatedConfigs(): AggregatedEntityConfig[] {\n const map = new Map<string, AggregatedEntityConfig>()\n const modules = getModules()\n for (const mod of modules) {\n const moduleId = mod.id\n const entitySpecs = ((mod as any).customEntities as CustomEntitySpec[] | undefined) ?? []\n for (const spec of entitySpecs) {\n const existing = map.get(spec.id) ?? { entityId: spec.id, moduleIds: new Set<string>(), spec: undefined, fieldSets: [] }\n existing.moduleIds.add(moduleId)\n if (!existing.spec) existing.spec = spec\n map.set(spec.id, existing)\n }\n const fieldSets = ((mod as any).customFieldSets as CustomFieldSet[] | undefined) ?? []\n for (const set of fieldSets) {\n const existing = map.get(set.entity) ?? { entityId: set.entity, moduleIds: new Set<string>(), spec: undefined, fieldSets: [] }\n existing.moduleIds.add(moduleId)\n existing.fieldSets.push(set)\n map.set(set.entity, existing)\n }\n }\n return Array.from(map.values())\n}\n\nexport function resolveFields(fieldSets: CustomFieldSet[]): CustomFieldDefinition[] {\n const byKey = new Map<string, CustomFieldDefinition>()\n const declarationOrder = new Map<string, number>()\n for (const set of fieldSets) {\n for (const field of set.fields ?? []) {\n if (!declarationOrder.has(field.key)) declarationOrder.set(field.key, declarationOrder.size)\n byKey.set(field.key, { ...field })\n }\n }\n // Declaration order is the author's intent, so it drives `priority` (and the\n // resulting render order) unless a field states its own. Sorting alphabetically\n // by key here would silently discard that intent (#4378).\n const resolved = Array.from(byKey.values()).map((field) => (\n typeof field.priority === 'number'\n ? field\n : { ...field, priority: declarationOrder.get(field.key) ?? 0 }\n ))\n return resolved.sort((a, b) => {\n const byPriority = (a.priority ?? 0) - (b.priority ?? 0)\n if (byPriority !== 0) return byPriority\n return (declarationOrder.get(a.key) ?? 0) - (declarationOrder.get(b.key) ?? 0)\n })\n}\n\nfunction normalizeField(field: CustomFieldDefinition) {\n const payload: Record<string, unknown> = {\n key: field.key,\n kind: field.kind,\n }\n for (const key of FIELD_DETAIL_KEYS) {\n const value = field[key]\n if (value !== undefined) payload[key] = value as unknown\n }\n if (field.id) payload.id = field.id\n return payload\n}\n\nfunction buildChecksumPayload(params: {\n entityId: string\n scope: InstallScope\n spec: CustomEntitySpec | undefined\n global: boolean\n fields: CustomFieldDefinition[]\n}) {\n const { entityId, scope, spec, global, fields } = params\n return {\n entityId,\n scope,\n global,\n label: spec?.label ?? null,\n description: spec?.description ?? null,\n labelField: spec?.labelField ?? null,\n defaultEditor: spec?.defaultEditor ?? null,\n showInSidebar: spec?.showInSidebar ?? false,\n accessRestricted: spec?.accessRestricted ?? false,\n fields: fields.map((f) => normalizeField(f)),\n }\n}\n\nexport async function installCustomEntitiesFromModules(\n em: EntityManager,\n cache: CacheStrategy | null | undefined,\n options: InstallEntitiesOptions = {}\n): Promise<InstallEntitiesResult> {\n const requestedEntityIds = options.entityIds\n ? new Set(options.entityIds.filter((id): id is string => typeof id === 'string' && id.length > 0))\n : null\n const aggregated = buildAggregatedConfigs().filter((entry) => {\n if (!requestedEntityIds) return true\n return requestedEntityIds.has(entry.entityId)\n })\n const systemIds = systemEntityIds()\n const includeGlobal = options.includeGlobal !== false\n const dryRun = options.dryRun === true\n const createOnly = options.createOnly === true\n const force = options.force === true\n const logger = options.logger\n\n let tenantIds: string[] | undefined\n if (options.tenantIds !== undefined) {\n tenantIds = Array.from(new Set(options.tenantIds.filter((id): id is string => typeof id === 'string' && id.length > 0)))\n }\n\n const ensureTenantIds = async (): Promise<string[]> => {\n if (tenantIds !== undefined) return tenantIds\n const rows = await em.find(Tenant, { deletedAt: null } as any, { fields: ['id'] as any })\n tenantIds = rows.map((row) => row.id)\n return tenantIds ?? []\n }\n\n let processed = 0\n let synchronized = 0\n let skipped = 0\n let fieldChanges = 0\n\n for (const entry of aggregated) {\n const { entityId } = entry\n const spec = entry.spec\n const fields = resolveFields(entry.fieldSets)\n const isSystem = systemIds.has(entityId)\n const registerEntity = !isSystem && !!spec\n const isGlobal = spec?.global === true\n\n const scopes: InstallScope[] = []\n if (isGlobal) {\n if (includeGlobal) scopes.push({ tenantId: null })\n } else {\n const ids = await ensureTenantIds()\n if (!ids.length) {\n skipped++\n continue\n }\n for (const tenantId of ids) scopes.push({ tenantId })\n }\n\n if (!scopes.length && fields.length === 0 && !registerEntity) {\n skipped++\n continue\n }\n\n for (const scope of scopes) {\n processed++\n const scopeKey = scope.tenantId ? `tenant:${scope.tenantId}` : 'global'\n const checksumPayload = buildChecksumPayload({\n entityId,\n scope,\n spec,\n global: isGlobal,\n fields,\n })\n const checksum = computeChecksum(checksumPayload)\n const cacheKey = `custom-entities:v1:${scopeKey}:${entityId}`\n\n if (!dryRun && !force && cache) {\n try {\n const cached = await cache.get(cacheKey)\n if (typeof cached === 'string' && cached === checksum) {\n skipped++\n continue\n }\n } catch {}\n }\n\n let entityResult: UpsertCustomEntityResult = 'unchanged'\n if (registerEntity) {\n entityResult = await upsertCustomEntity(em, entityId, {\n label: spec?.label ?? entityId,\n description: spec?.description ?? null,\n organizationId: null,\n tenantId: scope.tenantId,\n showInSidebar: spec?.showInSidebar ?? false,\n accessRestricted: spec?.accessRestricted ?? false,\n labelField: spec?.labelField ?? null,\n defaultEditor: spec?.defaultEditor ?? null,\n isActive: true,\n dryRun,\n createOnly,\n })\n }\n\n let fieldResult = { created: 0, updated: 0, unchanged: 0 }\n if (fields.length) {\n fieldResult = await ensureCustomFieldDefinitions(\n em,\n [{ entity: entityId, fields }],\n { organizationId: null, tenantId: scope.tenantId, dryRun, createOnly }\n )\n }\n\n const changed = (entityResult !== 'unchanged') || fieldResult.created > 0 || fieldResult.updated > 0\n const shouldInvalidateDefinitions = !dryRun && (changed || force)\n if (shouldInvalidateDefinitions) {\n await invalidateDefinitionsCache(cache ?? undefined, {\n tenantId: scope.tenantId,\n organizationId: null,\n entityIds: [entityId],\n })\n }\n\n if (changed) {\n synchronized++\n fieldChanges += fieldResult.created + fieldResult.updated\n if (!dryRun && cache) {\n try {\n await cache.set(cacheKey, checksum, { tags: [`custom-entity:${entityId}`, `custom-entity-scope:${scopeKey}`] })\n } catch {}\n }\n if (logger) {\n const parts: string[] = []\n if (entityResult !== 'unchanged') parts.push(`entity ${entityResult}`)\n if (fieldResult.created || fieldResult.updated) {\n parts.push(`fields +${fieldResult.created} / ~${fieldResult.updated}`)\n }\n logger(`Synced ${entityId} for ${scopeKey}${parts.length ? ` (${parts.join(', ')})` : ''}`)\n }\n } else {\n skipped++\n if (!dryRun && cache) {\n try {\n await cache.set(cacheKey, checksum, { tags: [`custom-entity:${entityId}`, `custom-entity-scope:${scopeKey}`] })\n } catch {}\n }\n }\n }\n }\n\n return { processed, synchronized, skipped, fieldChanges }\n}\n\nexport function listCustomEntityIds(): string[] {\n return buildAggregatedConfigs().map((entry) => entry.entityId)\n}\n\nexport function getAggregatedCustomEntityConfigs(): AggregatedEntityConfig[] {\n return buildAggregatedConfigs()\n}\n"],
|
|
5
|
+
"mappings": "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",
|
|
6
6
|
"names": []
|
|
7
7
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@open-mercato/core",
|
|
3
|
-
"version": "0.6.7-develop.
|
|
3
|
+
"version": "0.6.7-develop.6605.1.761dd4c810",
|
|
4
4
|
"license": "MIT",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "./dist/index.js",
|
|
@@ -254,16 +254,16 @@
|
|
|
254
254
|
"zod": "^4.4.3"
|
|
255
255
|
},
|
|
256
256
|
"peerDependencies": {
|
|
257
|
-
"@open-mercato/ai-assistant": "0.6.7-develop.
|
|
258
|
-
"@open-mercato/shared": "0.6.7-develop.
|
|
259
|
-
"@open-mercato/ui": "0.6.7-develop.
|
|
257
|
+
"@open-mercato/ai-assistant": "0.6.7-develop.6605.1.761dd4c810",
|
|
258
|
+
"@open-mercato/shared": "0.6.7-develop.6605.1.761dd4c810",
|
|
259
|
+
"@open-mercato/ui": "0.6.7-develop.6605.1.761dd4c810",
|
|
260
260
|
"react": "^19.0.0",
|
|
261
261
|
"react-dom": "^19.0.0"
|
|
262
262
|
},
|
|
263
263
|
"devDependencies": {
|
|
264
|
-
"@open-mercato/ai-assistant": "0.6.7-develop.
|
|
265
|
-
"@open-mercato/shared": "0.6.7-develop.
|
|
266
|
-
"@open-mercato/ui": "0.6.7-develop.
|
|
264
|
+
"@open-mercato/ai-assistant": "0.6.7-develop.6605.1.761dd4c810",
|
|
265
|
+
"@open-mercato/shared": "0.6.7-develop.6605.1.761dd4c810",
|
|
266
|
+
"@open-mercato/ui": "0.6.7-develop.6605.1.761dd4c810",
|
|
267
267
|
"@testing-library/dom": "^10.4.1",
|
|
268
268
|
"@testing-library/jest-dom": "^6.9.1",
|
|
269
269
|
"@testing-library/react": "^16.3.1",
|
|
@@ -17,10 +17,41 @@ import { escapeLikePattern } from '@open-mercato/shared/lib/db/escapeLikePattern
|
|
|
17
17
|
import { assertActorCanModifySuperAdminRoleTarget } from '@open-mercato/core/modules/auth/lib/grantChecks'
|
|
18
18
|
import { enforceRoleTenantAccess } from '@open-mercato/core/modules/auth/lib/roleTenantGuard'
|
|
19
19
|
import type { RbacService } from '@open-mercato/core/modules/auth/services/rbacService'
|
|
20
|
+
import { runWithCacheTenant } from '@open-mercato/cache'
|
|
21
|
+
import {
|
|
22
|
+
buildCollectionTags,
|
|
23
|
+
isCrudCacheEnabled,
|
|
24
|
+
resolveCrudCache,
|
|
25
|
+
} from '@open-mercato/shared/lib/crud/cache'
|
|
20
26
|
import { createLogger } from '@open-mercato/shared/lib/logger'
|
|
21
27
|
|
|
28
|
+
const ROLES_LIST_CACHE_TTL_MS = 120_000
|
|
22
29
|
const logger = createLogger('auth').child({ component: 'roles' })
|
|
23
30
|
|
|
31
|
+
function buildRolesListCacheKey(scope: {
|
|
32
|
+
tenantId: string | null
|
|
33
|
+
isSuperAdmin: boolean
|
|
34
|
+
actorTenantId: string | null
|
|
35
|
+
tenantFilter: string | null
|
|
36
|
+
id?: string
|
|
37
|
+
page: number
|
|
38
|
+
pageSize: number
|
|
39
|
+
search?: string
|
|
40
|
+
}): string {
|
|
41
|
+
const parts = [
|
|
42
|
+
'auth:roles:list',
|
|
43
|
+
`tenant:${scope.tenantId ?? 'null'}`,
|
|
44
|
+
`super:${scope.isSuperAdmin ? '1' : '0'}`,
|
|
45
|
+
`actor:${scope.actorTenantId ?? 'null'}`,
|
|
46
|
+
`filter:${scope.tenantFilter ?? 'null'}`,
|
|
47
|
+
`id:${scope.id ?? ''}`,
|
|
48
|
+
`page:${scope.page}`,
|
|
49
|
+
`size:${scope.pageSize}`,
|
|
50
|
+
`q:${scope.search ?? ''}`,
|
|
51
|
+
]
|
|
52
|
+
return parts.join('|')
|
|
53
|
+
}
|
|
54
|
+
|
|
24
55
|
const querySchema = z.object({
|
|
25
56
|
id: z.string().uuid().optional(),
|
|
26
57
|
page: z.coerce.number().min(1).default(1),
|
|
@@ -146,6 +177,28 @@ export async function GET(req: Request) {
|
|
|
146
177
|
if (!isSuperAdmin && !actorTenantId) {
|
|
147
178
|
return NextResponse.json({ items: [], total: 0, totalPages: 1, isSuperAdmin })
|
|
148
179
|
}
|
|
180
|
+
const { id, page, pageSize, search, tenantId: requestedTenantId } = parsed.data
|
|
181
|
+
const tenantFilter = isSuperAdmin && requestedTenantId ? String(requestedTenantId) : null
|
|
182
|
+
const cacheTenantId = isSuperAdmin ? tenantFilter : actorTenantId
|
|
183
|
+
// An unfiltered super-admin response spans tenants and cannot be invalidated
|
|
184
|
+
// by any one tenant's collection tags, so keep that view uncached.
|
|
185
|
+
const cache = isCrudCacheEnabled() && cacheTenantId ? resolveCrudCache(container) : null
|
|
186
|
+
const cacheKey = cache
|
|
187
|
+
? buildRolesListCacheKey({
|
|
188
|
+
tenantId: cacheTenantId,
|
|
189
|
+
isSuperAdmin,
|
|
190
|
+
actorTenantId,
|
|
191
|
+
tenantFilter,
|
|
192
|
+
id,
|
|
193
|
+
page,
|
|
194
|
+
pageSize,
|
|
195
|
+
search,
|
|
196
|
+
})
|
|
197
|
+
: null
|
|
198
|
+
if (cache && cacheKey) {
|
|
199
|
+
const cached = await runWithCacheTenant(cacheTenantId, () => cache.get(cacheKey))
|
|
200
|
+
if (cached) return NextResponse.json(cached)
|
|
201
|
+
}
|
|
149
202
|
let superAdminRoleIds: Set<string> | null = null
|
|
150
203
|
if (!isSuperAdmin && actorTenantId) {
|
|
151
204
|
const superAdminAcls = await findWithDecryption(em, RoleAcl, { tenantId: actorTenantId, isSuperAdmin: true }, {}, { tenantId: actorTenantId, organizationId: null })
|
|
@@ -163,8 +216,6 @@ export async function GET(req: Request) {
|
|
|
163
216
|
superAdminRoleIds = new Set()
|
|
164
217
|
}
|
|
165
218
|
}
|
|
166
|
-
const { id, page, pageSize, search, tenantId: requestedTenantId } = parsed.data
|
|
167
|
-
const tenantFilter = isSuperAdmin && requestedTenantId ? String(requestedTenantId) : null
|
|
168
219
|
const filters: any[] = [{ deletedAt: null }]
|
|
169
220
|
if (id) filters.push({ id })
|
|
170
221
|
if (search) filters.push({ name: { $ilike: `%${escapeLikePattern(search)}%` } })
|
|
@@ -253,7 +304,28 @@ export async function GET(req: Request) {
|
|
|
253
304
|
query: parsed.data,
|
|
254
305
|
accessType: id ? 'read:item' : undefined,
|
|
255
306
|
})
|
|
256
|
-
|
|
307
|
+
const payload = { items, total: count, totalPages, isSuperAdmin }
|
|
308
|
+
if (cache && cacheKey) {
|
|
309
|
+
try {
|
|
310
|
+
await runWithCacheTenant(cacheTenantId, () =>
|
|
311
|
+
cache.set(cacheKey, payload, {
|
|
312
|
+
ttl: ROLES_LIST_CACHE_TTL_MS,
|
|
313
|
+
tags: [
|
|
314
|
+
// Roles are tenant-level (org:null) — flushed on auth.roles.* command writes.
|
|
315
|
+
...buildCollectionTags('auth.role', cacheTenantId, [null]),
|
|
316
|
+
// usersCount derives from UserRole grants — auth.users.* command writes
|
|
317
|
+
// flush both tenant-level and record-organization collection tags.
|
|
318
|
+
...buildCollectionTags('auth.user', cacheTenantId, [null]),
|
|
319
|
+
// Role-ACL feature changes flush rbac:tenant:<T> from the roles/acl route.
|
|
320
|
+
`rbac:tenant:${cacheTenantId ?? 'null'}`,
|
|
321
|
+
],
|
|
322
|
+
}),
|
|
323
|
+
)
|
|
324
|
+
} catch (err) {
|
|
325
|
+
logger.warn('Failed to set roles list cache', { err })
|
|
326
|
+
}
|
|
327
|
+
}
|
|
328
|
+
return NextResponse.json(payload)
|
|
257
329
|
}
|
|
258
330
|
|
|
259
331
|
export const POST = crud.POST
|
|
@@ -49,6 +49,7 @@ const FIELD_DETAIL_KEYS: Array<keyof CustomFieldDefinition> = [
|
|
|
49
49
|
'formEditable',
|
|
50
50
|
'listVisible',
|
|
51
51
|
'indexed',
|
|
52
|
+
'priority',
|
|
52
53
|
'editor',
|
|
53
54
|
'input',
|
|
54
55
|
'relatedEntityId',
|
|
@@ -108,14 +109,28 @@ function buildAggregatedConfigs(): AggregatedEntityConfig[] {
|
|
|
108
109
|
return Array.from(map.values())
|
|
109
110
|
}
|
|
110
111
|
|
|
111
|
-
function resolveFields(fieldSets: CustomFieldSet[]): CustomFieldDefinition[] {
|
|
112
|
+
export function resolveFields(fieldSets: CustomFieldSet[]): CustomFieldDefinition[] {
|
|
112
113
|
const byKey = new Map<string, CustomFieldDefinition>()
|
|
114
|
+
const declarationOrder = new Map<string, number>()
|
|
113
115
|
for (const set of fieldSets) {
|
|
114
116
|
for (const field of set.fields ?? []) {
|
|
117
|
+
if (!declarationOrder.has(field.key)) declarationOrder.set(field.key, declarationOrder.size)
|
|
115
118
|
byKey.set(field.key, { ...field })
|
|
116
119
|
}
|
|
117
120
|
}
|
|
118
|
-
|
|
121
|
+
// Declaration order is the author's intent, so it drives `priority` (and the
|
|
122
|
+
// resulting render order) unless a field states its own. Sorting alphabetically
|
|
123
|
+
// by key here would silently discard that intent (#4378).
|
|
124
|
+
const resolved = Array.from(byKey.values()).map((field) => (
|
|
125
|
+
typeof field.priority === 'number'
|
|
126
|
+
? field
|
|
127
|
+
: { ...field, priority: declarationOrder.get(field.key) ?? 0 }
|
|
128
|
+
))
|
|
129
|
+
return resolved.sort((a, b) => {
|
|
130
|
+
const byPriority = (a.priority ?? 0) - (b.priority ?? 0)
|
|
131
|
+
if (byPriority !== 0) return byPriority
|
|
132
|
+
return (declarationOrder.get(a.key) ?? 0) - (declarationOrder.get(b.key) ?? 0)
|
|
133
|
+
})
|
|
119
134
|
}
|
|
120
135
|
|
|
121
136
|
function normalizeField(field: CustomFieldDefinition) {
|