@open-mercato/core 0.10.1-develop.7937.1.cf4cfe1a1f → 0.10.1-develop.7939.1.a561278b7a

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.
@@ -1,4 +1,4 @@
1
- [build:core] found 6275 entry points
1
+ [build:core] found 6276 entry points
2
2
  [build:core] built successfully
3
3
  [build:core:generated] found 271 entry points
4
4
  [build:core:generated] built successfully
@@ -4,6 +4,7 @@ import { getAuthFromRequest } from "@open-mercato/shared/lib/auth/server";
4
4
  import { createRequestContainer } from "@open-mercato/shared/lib/di/container";
5
5
  import { withAtomicFlush } from "@open-mercato/shared/lib/commands/flush";
6
6
  import { isCrudHttpError, isUniqueViolation } from "@open-mercato/shared/lib/crud/errors";
7
+ import { resolveTranslations } from "@open-mercato/shared/lib/i18n/server";
7
8
  import {
8
9
  runCrudMutationGuardAfterSuccess,
9
10
  validateCrudMutationGuard
@@ -249,14 +250,22 @@ async function POST(req, ctx) {
249
250
  }
250
251
  ], { transaction: true });
251
252
  } catch (err) {
253
+ if (isUniqueViolation(err) || isCrudHttpError(err) && err.body.code === "duplicate_name") {
254
+ const { translate } = await resolveTranslations();
255
+ return NextResponse.json({
256
+ error: translate("ui.perspectives.error.nameExists", "A view with this name already exists."),
257
+ code: "duplicate_name"
258
+ }, { status: 409 });
259
+ }
252
260
  if (isCrudHttpError(err)) {
253
261
  return NextResponse.json(err.body, { status: err.status });
254
262
  }
255
- if (isUniqueViolation(err)) {
263
+ if (err instanceof Error && "code" in err && err.code === "NOT_FOUND") {
264
+ const { translate } = await resolveTranslations();
256
265
  return NextResponse.json({
257
- error: "A view with this name already exists.",
258
- code: "duplicate_name"
259
- }, { status: 409 });
266
+ error: translate("api.errors.notFound", "Not Found"),
267
+ code: "NOT_FOUND"
268
+ }, { status: 404 });
260
269
  }
261
270
  throw err;
262
271
  }
@@ -324,6 +333,7 @@ const perspectivesPostDoc = {
324
333
  { status: 400, description: "Validation failed or invalid roles provided", schema: perspectivesErrorSchema },
325
334
  { status: 401, description: "Authentication required", schema: perspectivesErrorSchema },
326
335
  { status: 403, description: "Missing perspectives.role_defaults feature for role updates", schema: perspectivesErrorSchema },
336
+ { status: 404, description: "Personal perspective does not exist in the current user scope", schema: perspectivesErrorSchema },
327
337
  { status: 409, description: "Optimistic lock conflict or perspective name already exists", schema: perspectivesErrorSchema }
328
338
  ]
329
339
  };
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "version": 3,
3
3
  "sources": ["../../../../../src/modules/perspectives/api/%5BtableId%5D/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 { withAtomicFlush } from '@open-mercato/shared/lib/commands/flush'\nimport { isCrudHttpError, isUniqueViolation } from '@open-mercato/shared/lib/crud/errors'\nimport {\n runCrudMutationGuardAfterSuccess,\n validateCrudMutationGuard,\n} from '@open-mercato/shared/lib/crud/mutation-guard'\nimport { perspectiveSaveSchema } from '@open-mercato/core/modules/perspectives/data/validators'\nimport {\n loadPerspectivesState,\n saveUserPerspective,\n saveRolePerspectives,\n clearRolePerspectives,\n type PerspectiveScope,\n} from '@open-mercato/core/modules/perspectives/services/perspectiveService'\nimport { Role } from '@open-mercato/core/modules/auth/data/entities'\nimport type { OpenApiMethodDoc, OpenApiRouteDoc } from '@open-mercato/shared/lib/openapi'\nimport {\n perspectivesTag,\n perspectivesErrorSchema,\n perspectivesIndexResponseSchema,\n perspectiveSaveResponseSchema,\n} from '../openapi'\n\nexport const metadata = {\n GET: { requireAuth: true, requireFeatures: ['perspectives.use'] },\n POST: { requireAuth: true, requireFeatures: ['perspectives.use'] },\n}\n\nconst decodeParam = (value: string | string[] | undefined): string => {\n if (!value) return ''\n const raw = Array.isArray(value) ? value[0] : value\n try {\n return decodeURIComponent(raw)\n } catch {\n return raw\n }\n}\n\nfunction buildScope(auth: NonNullable<Awaited<ReturnType<typeof getAuthFromRequest>>>): PerspectiveScope {\n return {\n userId: auth.sub,\n tenantId: auth.tenantId ?? null,\n organizationId: auth.orgId ?? null,\n }\n}\n\nexport async function GET(_req: Request, ctx: { params: { tableId: string } }) {\n const auth = await getAuthFromRequest(_req)\n if (!auth) return NextResponse.json({ error: 'Unauthorized' }, { status: 401 })\n\n const tableId = decodeParam(ctx.params?.tableId).trim()\n if (!tableId) return NextResponse.json({ error: 'Invalid table id' }, { status: 400 })\n\n const container = await createRequestContainer()\n const em = container.resolve('em') as import('@mikro-orm/postgresql').EntityManager\n const cache = ((): import('@open-mercato/cache').CacheStrategy | null => {\n try {\n return container.resolve('cache') as import('@open-mercato/cache').CacheStrategy\n } catch {\n return null\n }\n })()\n const rbac = container.resolve('rbacService') as {\n userHasAllFeatures?: (\n userId: string,\n features: string[],\n scope: { tenantId: string | null; organizationId: string | null },\n ) => Promise<boolean>\n }\n\n const assignedRoleNames = Array.isArray(auth.roles)\n ? Array.from(new Set(auth.roles.filter((role): role is string => typeof role === 'string' && role.trim().length > 0)))\n : []\n const assignedRoles = assignedRoleNames.length && auth.tenantId\n ? await em.find(Role, {\n name: { $in: assignedRoleNames as any },\n tenantId: auth.tenantId,\n deletedAt: null,\n } as any, { orderBy: { name: 'asc' } })\n : []\n const assignedRoleIds = assignedRoles.map((role) => role.id)\n\n const canApplyToRoles = await rbac.userHasAllFeatures?.(\n auth.sub,\n ['perspectives.role_defaults'],\n { tenantId: auth.tenantId ?? null, organizationId: auth.orgId ?? null },\n ) ?? false\n\n const roleScope = auth.tenantId\n ? { $or: [{ tenantId: auth.tenantId }, { tenantId: null }] }\n : { tenantId: null }\n const availableRoles = canApplyToRoles\n ? await em.find(Role, { ...roleScope as any, deletedAt: null } as any, { orderBy: { name: 'asc' } })\n : assignedRoles\n const manageableRoleIds = canApplyToRoles\n ? availableRoles.map((role) => role.id)\n : assignedRoleIds\n\n const state = await loadPerspectivesState(em, cache, {\n scope: buildScope(auth),\n tableId,\n roleIds: assignedRoleIds,\n })\n const manageableState = manageableRoleIds.length === assignedRoleIds.length\n && manageableRoleIds.every((roleId) => assignedRoleIds.includes(roleId))\n ? state\n : await loadPerspectivesState(em, cache, {\n scope: buildScope(auth),\n tableId,\n roleIds: manageableRoleIds,\n })\n\n const rolePerspectiveByRole = new Map<string, { hasDefault: boolean; count: number }>()\n for (const item of manageableState.rolePerspectives) {\n const entry = rolePerspectiveByRole.get(item.roleId) ?? { hasDefault: false, count: 0 }\n entry.count += 1\n entry.hasDefault = entry.hasDefault || item.isDefault\n rolePerspectiveByRole.set(item.roleId, entry)\n }\n\n return NextResponse.json({\n tableId,\n perspectives: state.personal,\n defaultPerspectiveId: state.personalDefaultId,\n rolePerspectives: state.rolePerspectives.map((rp) => ({\n ...rp,\n roleName: availableRoles.find((role) => role.id === rp.roleId)?.name ?? assignedRoles.find((role) => role.id === rp.roleId)?.name ?? null,\n })),\n manageableRolePerspectives: manageableState.rolePerspectives.map((rp) => ({\n ...rp,\n roleName: availableRoles.find((role) => role.id === rp.roleId)?.name ?? assignedRoles.find((role) => role.id === rp.roleId)?.name ?? null,\n })),\n roles: availableRoles.map((role) => {\n const stats = rolePerspectiveByRole.get(role.id)\n return {\n id: role.id,\n name: role.name,\n hasPerspective: Boolean(stats?.count),\n hasDefault: Boolean(stats?.hasDefault),\n }\n }),\n canApplyToRoles,\n })\n}\n\nexport async function POST(req: Request, ctx: { params: { tableId: string } }) {\n const auth = await getAuthFromRequest(req)\n if (!auth) return NextResponse.json({ error: 'Unauthorized' }, { status: 401 })\n\n const tableId = decodeParam(ctx.params?.tableId).trim()\n if (!tableId) return NextResponse.json({ error: 'Invalid table id' }, { status: 400 })\n\n let parsedBody: unknown\n try {\n parsedBody = await req.json()\n } catch {\n return NextResponse.json({ error: 'Invalid JSON' }, { status: 400 })\n }\n\n const parsed = perspectiveSaveSchema.safeParse(parsedBody)\n if (!parsed.success) {\n return NextResponse.json({ error: 'Invalid payload', details: parsed.error.flatten() }, { status: 400 })\n }\n\n const container = await createRequestContainer()\n const em = container.resolve('em') as import('@mikro-orm/postgresql').EntityManager\n const cache = ((): import('@open-mercato/cache').CacheStrategy | null => {\n try {\n return container.resolve('cache') as import('@open-mercato/cache').CacheStrategy\n } catch {\n return null\n }\n })()\n const rbac = container.resolve('rbacService') as {\n userHasAllFeatures?: (\n userId: string,\n features: string[],\n scope: { tenantId: string | null; organizationId: string | null },\n ) => Promise<boolean>\n }\n\n const scope = buildScope(auth)\n\n const applyToRoles = Array.from(new Set(parsed.data.applyToRoles ?? [])).filter((id) => id.trim().length > 0)\n const clearRoleIds = Array.from(new Set(parsed.data.clearRoleIds ?? [])).filter((id) => id.trim().length > 0)\n const hasRoleOps = applyToRoles.length > 0 || clearRoleIds.length > 0\n const targetRoleIds = Array.from(new Set([...applyToRoles, ...clearRoleIds]))\n\n if (hasRoleOps) {\n const canApplyToRoles = await rbac.userHasAllFeatures?.(\n auth.sub,\n ['perspectives.role_defaults'],\n { tenantId: auth.tenantId ?? null, organizationId: auth.orgId ?? null },\n ) ?? false\n\n if (!canApplyToRoles) {\n return NextResponse.json({ error: 'Forbidden', requiredFeatures: ['perspectives.role_defaults'] }, { status: 403 })\n }\n\n const roleScope = auth.tenantId\n ? { $or: [{ tenantId: auth.tenantId }, { tenantId: null }] }\n : { tenantId: null }\n const roles = await em.find(Role, {\n id: { $in: targetRoleIds as any },\n ...(roleScope as any),\n deletedAt: null,\n } as any)\n const validRoleIds = new Set(roles.map((role) => role.id))\n\n const missing = targetRoleIds.filter((id) => !validRoleIds.has(id))\n if (missing.length) {\n return NextResponse.json({ error: 'Invalid roles', missing }, { status: 400 })\n }\n }\n\n const guardResourceId = parsed.data.perspectiveId ?? tableId\n const guardResult = await validateCrudMutationGuard(container, {\n tenantId: auth.tenantId ?? '',\n organizationId: auth.orgId ?? null,\n userId: auth.sub,\n resourceKind: 'perspectives.perspective',\n resourceId: guardResourceId,\n operation: 'custom',\n requestMethod: req.method,\n requestHeaders: req.headers,\n mutationPayload: { ...parsed.data, tableId },\n })\n if (guardResult && !guardResult.ok) {\n return NextResponse.json(guardResult.body, { status: guardResult.status })\n }\n\n let roleGuardResult: Awaited<ReturnType<typeof validateCrudMutationGuard>> | null = null\n if (hasRoleOps) {\n roleGuardResult = await validateCrudMutationGuard(container, {\n tenantId: auth.tenantId ?? '',\n organizationId: auth.orgId ?? null,\n userId: auth.sub,\n resourceKind: 'perspectives.role_perspective',\n resourceId: targetRoleIds.join(','),\n operation: 'custom',\n requestMethod: req.method,\n requestHeaders: req.headers,\n mutationPayload: {\n tableId,\n applyToRoles,\n clearRoleIds,\n name: parsed.data.name,\n settings: parsed.data.settings,\n setRoleDefault: parsed.data.setRoleDefault ?? false,\n },\n })\n }\n if (roleGuardResult && !roleGuardResult.ok) {\n return NextResponse.json(roleGuardResult.body, { status: roleGuardResult.status })\n }\n\n let saved: Awaited<ReturnType<typeof saveUserPerspective>> | null = null\n let updatedRolePerspectives: Awaited<ReturnType<typeof saveRolePerspectives>> | null = null\n let clearedRolePerspectiveCount = 0\n\n try {\n await withAtomicFlush(em, [\n async () => {\n saved = await saveUserPerspective(em, cache, {\n scope,\n tableId,\n input: parsed.data,\n request: req,\n })\n },\n async () => {\n if (applyToRoles.length) {\n updatedRolePerspectives = await saveRolePerspectives(em, cache, {\n tableId,\n tenantId: auth.tenantId ?? null,\n organizationId: auth.orgId ?? null,\n input: {\n roleIds: applyToRoles,\n name: parsed.data.name,\n settings: parsed.data.settings,\n setDefault: parsed.data.setRoleDefault ?? false,\n },\n expectedUpdatedAtByRoleId: parsed.data.roleExpectedUpdatedAtByRoleId,\n expectedUpdatedAtByPerspectiveId: parsed.data.roleExpectedUpdatedAtByPerspectiveId,\n request: req,\n })\n }\n },\n async () => {\n if (clearRoleIds.length) {\n clearedRolePerspectiveCount = await clearRolePerspectives(em, cache, {\n tableId,\n tenantId: auth.tenantId ?? null,\n organizationId: auth.orgId ?? null,\n roleIds: clearRoleIds,\n expectedUpdatedAtByRoleId: parsed.data.roleExpectedUpdatedAtByRoleId,\n expectedUpdatedAtByPerspectiveId: parsed.data.roleExpectedUpdatedAtByPerspectiveId,\n request: req,\n })\n }\n },\n ], { transaction: true })\n } catch (err) {\n if (isCrudHttpError(err)) {\n return NextResponse.json(err.body, { status: err.status })\n }\n if (isUniqueViolation(err)) {\n return NextResponse.json({\n error: 'A view with this name already exists.',\n code: 'duplicate_name',\n }, { status: 409 })\n }\n throw err\n }\n\n if (guardResult?.ok && guardResult.shouldRunAfterSuccess) {\n await runCrudMutationGuardAfterSuccess(container, {\n tenantId: auth.tenantId ?? '',\n organizationId: auth.orgId ?? null,\n userId: auth.sub,\n resourceKind: 'perspectives.perspective',\n resourceId: guardResourceId,\n operation: 'custom',\n requestMethod: req.method,\n requestHeaders: req.headers,\n metadata: guardResult.metadata ?? null,\n })\n }\n\n const didWriteRolePerspectives = applyToRoles.length > 0 || clearedRolePerspectiveCount > 0\n if (didWriteRolePerspectives && roleGuardResult?.ok && roleGuardResult.shouldRunAfterSuccess) {\n await runCrudMutationGuardAfterSuccess(container, {\n tenantId: auth.tenantId ?? '',\n organizationId: auth.orgId ?? null,\n userId: auth.sub,\n resourceKind: 'perspectives.role_perspective',\n resourceId: targetRoleIds.join(','),\n operation: 'custom',\n requestMethod: req.method,\n requestHeaders: req.headers,\n metadata: roleGuardResult.metadata ?? null,\n })\n }\n\n return NextResponse.json({\n perspective: saved,\n rolePerspectives: updatedRolePerspectives ?? [],\n clearedRoleIds: clearRoleIds ?? [],\n })\n}\n\nconst perspectivePathParamsSchema = z.object({\n tableId: z.string().min(1),\n})\n\nconst perspectivesGetDoc: OpenApiMethodDoc = {\n summary: 'Load perspectives for a table',\n description: 'Returns personal perspectives and available role defaults for the requested table identifier.',\n tags: [perspectivesTag],\n responses: [\n { status: 200, description: 'Current perspectives and defaults.', schema: perspectivesIndexResponseSchema },\n ],\n errors: [\n { status: 400, description: 'Invalid table identifier', schema: perspectivesErrorSchema },\n { status: 401, description: 'Authentication required', schema: perspectivesErrorSchema },\n ],\n}\n\nconst perspectivesPostDoc: OpenApiMethodDoc = {\n summary: 'Create or update a perspective',\n description: 'Saves a personal perspective and optionally applies the same configuration to selected roles.',\n tags: [perspectivesTag],\n requestBody: {\n contentType: 'application/json',\n schema: perspectiveSaveSchema,\n description: 'Perspective payload including optional role defaults.',\n },\n responses: [\n { status: 200, description: 'Perspective saved successfully.', schema: perspectiveSaveResponseSchema },\n ],\n errors: [\n { status: 400, description: 'Validation failed or invalid roles provided', schema: perspectivesErrorSchema },\n { status: 401, description: 'Authentication required', schema: perspectivesErrorSchema },\n { status: 403, description: 'Missing perspectives.role_defaults feature for role updates', schema: perspectivesErrorSchema },\n { status: 409, description: 'Optimistic lock conflict or perspective name already exists', schema: perspectivesErrorSchema },\n ],\n}\n\nexport const openApi: OpenApiRouteDoc = {\n tag: perspectivesTag,\n summary: 'Manage table perspectives',\n pathParams: perspectivePathParamsSchema,\n methods: {\n GET: perspectivesGetDoc,\n POST: perspectivesPostDoc,\n },\n}\n"],
5
- "mappings": "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",
4
+ "sourcesContent": ["import { NextResponse } from 'next/server'\nimport { z } from 'zod'\nimport { getAuthFromRequest } from '@open-mercato/shared/lib/auth/server'\nimport { createRequestContainer } from '@open-mercato/shared/lib/di/container'\nimport { withAtomicFlush } from '@open-mercato/shared/lib/commands/flush'\nimport { isCrudHttpError, isUniqueViolation } from '@open-mercato/shared/lib/crud/errors'\nimport { resolveTranslations } from '@open-mercato/shared/lib/i18n/server'\nimport {\n runCrudMutationGuardAfterSuccess,\n validateCrudMutationGuard,\n} from '@open-mercato/shared/lib/crud/mutation-guard'\nimport { perspectiveSaveSchema } from '@open-mercato/core/modules/perspectives/data/validators'\nimport {\n loadPerspectivesState,\n saveUserPerspective,\n saveRolePerspectives,\n clearRolePerspectives,\n type PerspectiveScope,\n} from '@open-mercato/core/modules/perspectives/services/perspectiveService'\nimport { Role } from '@open-mercato/core/modules/auth/data/entities'\nimport type { OpenApiMethodDoc, OpenApiRouteDoc } from '@open-mercato/shared/lib/openapi'\nimport {\n perspectivesTag,\n perspectivesErrorSchema,\n perspectivesIndexResponseSchema,\n perspectiveSaveResponseSchema,\n} from '../openapi'\n\nexport const metadata = {\n GET: { requireAuth: true, requireFeatures: ['perspectives.use'] },\n POST: { requireAuth: true, requireFeatures: ['perspectives.use'] },\n}\n\nconst decodeParam = (value: string | string[] | undefined): string => {\n if (!value) return ''\n const raw = Array.isArray(value) ? value[0] : value\n try {\n return decodeURIComponent(raw)\n } catch {\n return raw\n }\n}\n\nfunction buildScope(auth: NonNullable<Awaited<ReturnType<typeof getAuthFromRequest>>>): PerspectiveScope {\n return {\n userId: auth.sub,\n tenantId: auth.tenantId ?? null,\n organizationId: auth.orgId ?? null,\n }\n}\n\nexport async function GET(_req: Request, ctx: { params: { tableId: string } }) {\n const auth = await getAuthFromRequest(_req)\n if (!auth) return NextResponse.json({ error: 'Unauthorized' }, { status: 401 })\n\n const tableId = decodeParam(ctx.params?.tableId).trim()\n if (!tableId) return NextResponse.json({ error: 'Invalid table id' }, { status: 400 })\n\n const container = await createRequestContainer()\n const em = container.resolve('em') as import('@mikro-orm/postgresql').EntityManager\n const cache = ((): import('@open-mercato/cache').CacheStrategy | null => {\n try {\n return container.resolve('cache') as import('@open-mercato/cache').CacheStrategy\n } catch {\n return null\n }\n })()\n const rbac = container.resolve('rbacService') as {\n userHasAllFeatures?: (\n userId: string,\n features: string[],\n scope: { tenantId: string | null; organizationId: string | null },\n ) => Promise<boolean>\n }\n\n const assignedRoleNames = Array.isArray(auth.roles)\n ? Array.from(new Set(auth.roles.filter((role): role is string => typeof role === 'string' && role.trim().length > 0)))\n : []\n const assignedRoles = assignedRoleNames.length && auth.tenantId\n ? await em.find(Role, {\n name: { $in: assignedRoleNames as any },\n tenantId: auth.tenantId,\n deletedAt: null,\n } as any, { orderBy: { name: 'asc' } })\n : []\n const assignedRoleIds = assignedRoles.map((role) => role.id)\n\n const canApplyToRoles = await rbac.userHasAllFeatures?.(\n auth.sub,\n ['perspectives.role_defaults'],\n { tenantId: auth.tenantId ?? null, organizationId: auth.orgId ?? null },\n ) ?? false\n\n const roleScope = auth.tenantId\n ? { $or: [{ tenantId: auth.tenantId }, { tenantId: null }] }\n : { tenantId: null }\n const availableRoles = canApplyToRoles\n ? await em.find(Role, { ...roleScope as any, deletedAt: null } as any, { orderBy: { name: 'asc' } })\n : assignedRoles\n const manageableRoleIds = canApplyToRoles\n ? availableRoles.map((role) => role.id)\n : assignedRoleIds\n\n const state = await loadPerspectivesState(em, cache, {\n scope: buildScope(auth),\n tableId,\n roleIds: assignedRoleIds,\n })\n const manageableState = manageableRoleIds.length === assignedRoleIds.length\n && manageableRoleIds.every((roleId) => assignedRoleIds.includes(roleId))\n ? state\n : await loadPerspectivesState(em, cache, {\n scope: buildScope(auth),\n tableId,\n roleIds: manageableRoleIds,\n })\n\n const rolePerspectiveByRole = new Map<string, { hasDefault: boolean; count: number }>()\n for (const item of manageableState.rolePerspectives) {\n const entry = rolePerspectiveByRole.get(item.roleId) ?? { hasDefault: false, count: 0 }\n entry.count += 1\n entry.hasDefault = entry.hasDefault || item.isDefault\n rolePerspectiveByRole.set(item.roleId, entry)\n }\n\n return NextResponse.json({\n tableId,\n perspectives: state.personal,\n defaultPerspectiveId: state.personalDefaultId,\n rolePerspectives: state.rolePerspectives.map((rp) => ({\n ...rp,\n roleName: availableRoles.find((role) => role.id === rp.roleId)?.name ?? assignedRoles.find((role) => role.id === rp.roleId)?.name ?? null,\n })),\n manageableRolePerspectives: manageableState.rolePerspectives.map((rp) => ({\n ...rp,\n roleName: availableRoles.find((role) => role.id === rp.roleId)?.name ?? assignedRoles.find((role) => role.id === rp.roleId)?.name ?? null,\n })),\n roles: availableRoles.map((role) => {\n const stats = rolePerspectiveByRole.get(role.id)\n return {\n id: role.id,\n name: role.name,\n hasPerspective: Boolean(stats?.count),\n hasDefault: Boolean(stats?.hasDefault),\n }\n }),\n canApplyToRoles,\n })\n}\n\nexport async function POST(req: Request, ctx: { params: { tableId: string } }) {\n const auth = await getAuthFromRequest(req)\n if (!auth) return NextResponse.json({ error: 'Unauthorized' }, { status: 401 })\n\n const tableId = decodeParam(ctx.params?.tableId).trim()\n if (!tableId) return NextResponse.json({ error: 'Invalid table id' }, { status: 400 })\n\n let parsedBody: unknown\n try {\n parsedBody = await req.json()\n } catch {\n return NextResponse.json({ error: 'Invalid JSON' }, { status: 400 })\n }\n\n const parsed = perspectiveSaveSchema.safeParse(parsedBody)\n if (!parsed.success) {\n return NextResponse.json({ error: 'Invalid payload', details: parsed.error.flatten() }, { status: 400 })\n }\n\n const container = await createRequestContainer()\n const em = container.resolve('em') as import('@mikro-orm/postgresql').EntityManager\n const cache = ((): import('@open-mercato/cache').CacheStrategy | null => {\n try {\n return container.resolve('cache') as import('@open-mercato/cache').CacheStrategy\n } catch {\n return null\n }\n })()\n const rbac = container.resolve('rbacService') as {\n userHasAllFeatures?: (\n userId: string,\n features: string[],\n scope: { tenantId: string | null; organizationId: string | null },\n ) => Promise<boolean>\n }\n\n const scope = buildScope(auth)\n\n const applyToRoles = Array.from(new Set(parsed.data.applyToRoles ?? [])).filter((id) => id.trim().length > 0)\n const clearRoleIds = Array.from(new Set(parsed.data.clearRoleIds ?? [])).filter((id) => id.trim().length > 0)\n const hasRoleOps = applyToRoles.length > 0 || clearRoleIds.length > 0\n const targetRoleIds = Array.from(new Set([...applyToRoles, ...clearRoleIds]))\n\n if (hasRoleOps) {\n const canApplyToRoles = await rbac.userHasAllFeatures?.(\n auth.sub,\n ['perspectives.role_defaults'],\n { tenantId: auth.tenantId ?? null, organizationId: auth.orgId ?? null },\n ) ?? false\n\n if (!canApplyToRoles) {\n return NextResponse.json({ error: 'Forbidden', requiredFeatures: ['perspectives.role_defaults'] }, { status: 403 })\n }\n\n const roleScope = auth.tenantId\n ? { $or: [{ tenantId: auth.tenantId }, { tenantId: null }] }\n : { tenantId: null }\n const roles = await em.find(Role, {\n id: { $in: targetRoleIds as any },\n ...(roleScope as any),\n deletedAt: null,\n } as any)\n const validRoleIds = new Set(roles.map((role) => role.id))\n\n const missing = targetRoleIds.filter((id) => !validRoleIds.has(id))\n if (missing.length) {\n return NextResponse.json({ error: 'Invalid roles', missing }, { status: 400 })\n }\n }\n\n const guardResourceId = parsed.data.perspectiveId ?? tableId\n const guardResult = await validateCrudMutationGuard(container, {\n tenantId: auth.tenantId ?? '',\n organizationId: auth.orgId ?? null,\n userId: auth.sub,\n resourceKind: 'perspectives.perspective',\n resourceId: guardResourceId,\n operation: 'custom',\n requestMethod: req.method,\n requestHeaders: req.headers,\n mutationPayload: { ...parsed.data, tableId },\n })\n if (guardResult && !guardResult.ok) {\n return NextResponse.json(guardResult.body, { status: guardResult.status })\n }\n\n let roleGuardResult: Awaited<ReturnType<typeof validateCrudMutationGuard>> | null = null\n if (hasRoleOps) {\n roleGuardResult = await validateCrudMutationGuard(container, {\n tenantId: auth.tenantId ?? '',\n organizationId: auth.orgId ?? null,\n userId: auth.sub,\n resourceKind: 'perspectives.role_perspective',\n resourceId: targetRoleIds.join(','),\n operation: 'custom',\n requestMethod: req.method,\n requestHeaders: req.headers,\n mutationPayload: {\n tableId,\n applyToRoles,\n clearRoleIds,\n name: parsed.data.name,\n settings: parsed.data.settings,\n setRoleDefault: parsed.data.setRoleDefault ?? false,\n },\n })\n }\n if (roleGuardResult && !roleGuardResult.ok) {\n return NextResponse.json(roleGuardResult.body, { status: roleGuardResult.status })\n }\n\n let saved: Awaited<ReturnType<typeof saveUserPerspective>> | null = null\n let updatedRolePerspectives: Awaited<ReturnType<typeof saveRolePerspectives>> | null = null\n let clearedRolePerspectiveCount = 0\n\n try {\n await withAtomicFlush(em, [\n async () => {\n saved = await saveUserPerspective(em, cache, {\n scope,\n tableId,\n input: parsed.data,\n request: req,\n })\n },\n async () => {\n if (applyToRoles.length) {\n updatedRolePerspectives = await saveRolePerspectives(em, cache, {\n tableId,\n tenantId: auth.tenantId ?? null,\n organizationId: auth.orgId ?? null,\n input: {\n roleIds: applyToRoles,\n name: parsed.data.name,\n settings: parsed.data.settings,\n setDefault: parsed.data.setRoleDefault ?? false,\n },\n expectedUpdatedAtByRoleId: parsed.data.roleExpectedUpdatedAtByRoleId,\n expectedUpdatedAtByPerspectiveId: parsed.data.roleExpectedUpdatedAtByPerspectiveId,\n request: req,\n })\n }\n },\n async () => {\n if (clearRoleIds.length) {\n clearedRolePerspectiveCount = await clearRolePerspectives(em, cache, {\n tableId,\n tenantId: auth.tenantId ?? null,\n organizationId: auth.orgId ?? null,\n roleIds: clearRoleIds,\n expectedUpdatedAtByRoleId: parsed.data.roleExpectedUpdatedAtByRoleId,\n expectedUpdatedAtByPerspectiveId: parsed.data.roleExpectedUpdatedAtByPerspectiveId,\n request: req,\n })\n }\n },\n ], { transaction: true })\n } catch (err) {\n if (isUniqueViolation(err) || (isCrudHttpError(err) && err.body.code === 'duplicate_name')) {\n const { translate } = await resolveTranslations()\n return NextResponse.json({\n error: translate('ui.perspectives.error.nameExists', 'A view with this name already exists.'),\n code: 'duplicate_name',\n }, { status: 409 })\n }\n if (isCrudHttpError(err)) {\n return NextResponse.json(err.body, { status: err.status })\n }\n if (err instanceof Error && 'code' in err && err.code === 'NOT_FOUND') {\n const { translate } = await resolveTranslations()\n return NextResponse.json({\n error: translate('api.errors.notFound', 'Not Found'),\n code: 'NOT_FOUND',\n }, { status: 404 })\n }\n throw err\n }\n\n if (guardResult?.ok && guardResult.shouldRunAfterSuccess) {\n await runCrudMutationGuardAfterSuccess(container, {\n tenantId: auth.tenantId ?? '',\n organizationId: auth.orgId ?? null,\n userId: auth.sub,\n resourceKind: 'perspectives.perspective',\n resourceId: guardResourceId,\n operation: 'custom',\n requestMethod: req.method,\n requestHeaders: req.headers,\n metadata: guardResult.metadata ?? null,\n })\n }\n\n const didWriteRolePerspectives = applyToRoles.length > 0 || clearedRolePerspectiveCount > 0\n if (didWriteRolePerspectives && roleGuardResult?.ok && roleGuardResult.shouldRunAfterSuccess) {\n await runCrudMutationGuardAfterSuccess(container, {\n tenantId: auth.tenantId ?? '',\n organizationId: auth.orgId ?? null,\n userId: auth.sub,\n resourceKind: 'perspectives.role_perspective',\n resourceId: targetRoleIds.join(','),\n operation: 'custom',\n requestMethod: req.method,\n requestHeaders: req.headers,\n metadata: roleGuardResult.metadata ?? null,\n })\n }\n\n return NextResponse.json({\n perspective: saved,\n rolePerspectives: updatedRolePerspectives ?? [],\n clearedRoleIds: clearRoleIds ?? [],\n })\n}\n\nconst perspectivePathParamsSchema = z.object({\n tableId: z.string().min(1),\n})\n\nconst perspectivesGetDoc: OpenApiMethodDoc = {\n summary: 'Load perspectives for a table',\n description: 'Returns personal perspectives and available role defaults for the requested table identifier.',\n tags: [perspectivesTag],\n responses: [\n { status: 200, description: 'Current perspectives and defaults.', schema: perspectivesIndexResponseSchema },\n ],\n errors: [\n { status: 400, description: 'Invalid table identifier', schema: perspectivesErrorSchema },\n { status: 401, description: 'Authentication required', schema: perspectivesErrorSchema },\n ],\n}\n\nconst perspectivesPostDoc: OpenApiMethodDoc = {\n summary: 'Create or update a perspective',\n description: 'Saves a personal perspective and optionally applies the same configuration to selected roles.',\n tags: [perspectivesTag],\n requestBody: {\n contentType: 'application/json',\n schema: perspectiveSaveSchema,\n description: 'Perspective payload including optional role defaults.',\n },\n responses: [\n { status: 200, description: 'Perspective saved successfully.', schema: perspectiveSaveResponseSchema },\n ],\n errors: [\n { status: 400, description: 'Validation failed or invalid roles provided', schema: perspectivesErrorSchema },\n { status: 401, description: 'Authentication required', schema: perspectivesErrorSchema },\n { status: 403, description: 'Missing perspectives.role_defaults feature for role updates', schema: perspectivesErrorSchema },\n { status: 404, description: 'Personal perspective does not exist in the current user scope', schema: perspectivesErrorSchema },\n { status: 409, description: 'Optimistic lock conflict or perspective name already exists', schema: perspectivesErrorSchema },\n ],\n}\n\nexport const openApi: OpenApiRouteDoc = {\n tag: perspectivesTag,\n summary: 'Manage table perspectives',\n pathParams: perspectivePathParamsSchema,\n methods: {\n GET: perspectivesGetDoc,\n POST: perspectivesPostDoc,\n },\n}\n"],
5
+ "mappings": "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",
6
6
  "names": []
7
7
  }
@@ -20,7 +20,8 @@ const perspectiveSaveSchema = z.object({
20
20
  clearRoleIds: z.array(z.string().uuid()).optional(),
21
21
  roleExpectedUpdatedAtByRoleId: z.record(z.string().uuid(), z.string().min(1).nullable()).optional(),
22
22
  roleExpectedUpdatedAtByPerspectiveId: z.record(z.string().uuid(), z.string().min(1).nullable()).optional(),
23
- setRoleDefault: z.boolean().optional()
23
+ setRoleDefault: z.boolean().optional(),
24
+ createOnly: z.boolean().optional()
24
25
  });
25
26
  const perspectiveDeleteSchema = z.object({
26
27
  force: z.boolean().optional()
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "version": 3,
3
3
  "sources": ["../../../../src/modules/perspectives/data/validators.ts"],
4
- "sourcesContent": ["import { z } from 'zod'\nimport type { PerspectiveSettings } from '@open-mercato/shared/modules/perspectives/types'\n\nexport const perspectiveSettingsSchema: z.ZodType<PerspectiveSettings> = z.object({\n columnOrder: z.array(z.string().min(1)).max(120).optional(),\n columnVisibility: z.record(z.string(), z.boolean()).optional(),\n // User-resized column widths in pixels, keyed by column id (#1835). Clamped to\n // the same [60, 900] bound the client enforces so a saved perspective cannot\n // carry a column width that collapses or blows out the table.\n columnSizing: z.record(z.string().min(1), z.number().int().min(60).max(900)).optional(),\n filters: z.record(z.string(), z.unknown()).optional(),\n sorting: z\n .array(z.object({ id: z.string().min(1), desc: z.boolean().optional() }))\n .max(20)\n .optional(),\n pageSize: z.number().int().positive().max(500).optional(),\n searchValue: z.string().max(200).optional(),\n})\n\nexport type { PerspectiveSettings }\n\nexport const perspectiveSaveSchema = z.object({\n perspectiveId: z.string().uuid().optional(),\n name: z.string().min(1).max(120),\n settings: perspectiveSettingsSchema,\n isDefault: z.boolean().optional(),\n applyToRoles: z.array(z.string().uuid()).optional(),\n clearRoleIds: z.array(z.string().uuid()).optional(),\n roleExpectedUpdatedAtByRoleId: z.record(z.string().uuid(), z.string().min(1).nullable()).optional(),\n roleExpectedUpdatedAtByPerspectiveId: z.record(z.string().uuid(), z.string().min(1).nullable()).optional(),\n setRoleDefault: z.boolean().optional(),\n})\n\nexport const perspectiveDeleteSchema = z.object({\n force: z.boolean().optional(),\n})\n\nexport const rolePerspectiveSaveSchema = z.object({\n roleIds: z.array(z.string().uuid()).min(1),\n name: z.string().min(1).max(120),\n settings: perspectiveSettingsSchema,\n setDefault: z.boolean().optional(),\n})\n\nexport type PerspectiveSaveInput = z.infer<typeof perspectiveSaveSchema>\nexport type RolePerspectiveSaveInput = z.infer<typeof rolePerspectiveSaveSchema>\n"],
5
- "mappings": "AAAA,SAAS,SAAS;AAGX,MAAM,4BAA4D,EAAE,OAAO;AAAA,EAChF,aAAa,EAAE,MAAM,EAAE,OAAO,EAAE,IAAI,CAAC,CAAC,EAAE,IAAI,GAAG,EAAE,SAAS;AAAA,EAC1D,kBAAkB,EAAE,OAAO,EAAE,OAAO,GAAG,EAAE,QAAQ,CAAC,EAAE,SAAS;AAAA;AAAA;AAAA;AAAA,EAI7D,cAAc,EAAE,OAAO,EAAE,OAAO,EAAE,IAAI,CAAC,GAAG,EAAE,OAAO,EAAE,IAAI,EAAE,IAAI,EAAE,EAAE,IAAI,GAAG,CAAC,EAAE,SAAS;AAAA,EACtF,SAAS,EAAE,OAAO,EAAE,OAAO,GAAG,EAAE,QAAQ,CAAC,EAAE,SAAS;AAAA,EACpD,SAAS,EACN,MAAM,EAAE,OAAO,EAAE,IAAI,EAAE,OAAO,EAAE,IAAI,CAAC,GAAG,MAAM,EAAE,QAAQ,EAAE,SAAS,EAAE,CAAC,CAAC,EACvE,IAAI,EAAE,EACN,SAAS;AAAA,EACZ,UAAU,EAAE,OAAO,EAAE,IAAI,EAAE,SAAS,EAAE,IAAI,GAAG,EAAE,SAAS;AAAA,EACxD,aAAa,EAAE,OAAO,EAAE,IAAI,GAAG,EAAE,SAAS;AAC5C,CAAC;AAIM,MAAM,wBAAwB,EAAE,OAAO;AAAA,EAC5C,eAAe,EAAE,OAAO,EAAE,KAAK,EAAE,SAAS;AAAA,EAC1C,MAAM,EAAE,OAAO,EAAE,IAAI,CAAC,EAAE,IAAI,GAAG;AAAA,EAC/B,UAAU;AAAA,EACV,WAAW,EAAE,QAAQ,EAAE,SAAS;AAAA,EAChC,cAAc,EAAE,MAAM,EAAE,OAAO,EAAE,KAAK,CAAC,EAAE,SAAS;AAAA,EAClD,cAAc,EAAE,MAAM,EAAE,OAAO,EAAE,KAAK,CAAC,EAAE,SAAS;AAAA,EAClD,+BAA+B,EAAE,OAAO,EAAE,OAAO,EAAE,KAAK,GAAG,EAAE,OAAO,EAAE,IAAI,CAAC,EAAE,SAAS,CAAC,EAAE,SAAS;AAAA,EAClG,sCAAsC,EAAE,OAAO,EAAE,OAAO,EAAE,KAAK,GAAG,EAAE,OAAO,EAAE,IAAI,CAAC,EAAE,SAAS,CAAC,EAAE,SAAS;AAAA,EACzG,gBAAgB,EAAE,QAAQ,EAAE,SAAS;AACvC,CAAC;AAEM,MAAM,0BAA0B,EAAE,OAAO;AAAA,EAC9C,OAAO,EAAE,QAAQ,EAAE,SAAS;AAC9B,CAAC;AAEM,MAAM,4BAA4B,EAAE,OAAO;AAAA,EAChD,SAAS,EAAE,MAAM,EAAE,OAAO,EAAE,KAAK,CAAC,EAAE,IAAI,CAAC;AAAA,EACzC,MAAM,EAAE,OAAO,EAAE,IAAI,CAAC,EAAE,IAAI,GAAG;AAAA,EAC/B,UAAU;AAAA,EACV,YAAY,EAAE,QAAQ,EAAE,SAAS;AACnC,CAAC;",
4
+ "sourcesContent": ["import { z } from 'zod'\nimport type { PerspectiveSettings } from '@open-mercato/shared/modules/perspectives/types'\n\nexport const perspectiveSettingsSchema: z.ZodType<PerspectiveSettings> = z.object({\n columnOrder: z.array(z.string().min(1)).max(120).optional(),\n columnVisibility: z.record(z.string(), z.boolean()).optional(),\n // User-resized column widths in pixels, keyed by column id (#1835). Clamped to\n // the same [60, 900] bound the client enforces so a saved perspective cannot\n // carry a column width that collapses or blows out the table.\n columnSizing: z.record(z.string().min(1), z.number().int().min(60).max(900)).optional(),\n filters: z.record(z.string(), z.unknown()).optional(),\n sorting: z\n .array(z.object({ id: z.string().min(1), desc: z.boolean().optional() }))\n .max(20)\n .optional(),\n pageSize: z.number().int().positive().max(500).optional(),\n searchValue: z.string().max(200).optional(),\n})\n\nexport type { PerspectiveSettings }\n\nexport const perspectiveSaveSchema = z.object({\n perspectiveId: z.string().uuid().optional(),\n name: z.string().min(1).max(120),\n settings: perspectiveSettingsSchema,\n isDefault: z.boolean().optional(),\n applyToRoles: z.array(z.string().uuid()).optional(),\n clearRoleIds: z.array(z.string().uuid()).optional(),\n roleExpectedUpdatedAtByRoleId: z.record(z.string().uuid(), z.string().min(1).nullable()).optional(),\n roleExpectedUpdatedAtByPerspectiveId: z.record(z.string().uuid(), z.string().min(1).nullable()).optional(),\n setRoleDefault: z.boolean().optional(),\n createOnly: z.boolean().optional(),\n})\n\nexport const perspectiveDeleteSchema = z.object({\n force: z.boolean().optional(),\n})\n\nexport const rolePerspectiveSaveSchema = z.object({\n roleIds: z.array(z.string().uuid()).min(1),\n name: z.string().min(1).max(120),\n settings: perspectiveSettingsSchema,\n setDefault: z.boolean().optional(),\n})\n\nexport type PerspectiveSaveInput = z.infer<typeof perspectiveSaveSchema>\nexport type RolePerspectiveSaveInput = z.infer<typeof rolePerspectiveSaveSchema>\n"],
5
+ "mappings": "AAAA,SAAS,SAAS;AAGX,MAAM,4BAA4D,EAAE,OAAO;AAAA,EAChF,aAAa,EAAE,MAAM,EAAE,OAAO,EAAE,IAAI,CAAC,CAAC,EAAE,IAAI,GAAG,EAAE,SAAS;AAAA,EAC1D,kBAAkB,EAAE,OAAO,EAAE,OAAO,GAAG,EAAE,QAAQ,CAAC,EAAE,SAAS;AAAA;AAAA;AAAA;AAAA,EAI7D,cAAc,EAAE,OAAO,EAAE,OAAO,EAAE,IAAI,CAAC,GAAG,EAAE,OAAO,EAAE,IAAI,EAAE,IAAI,EAAE,EAAE,IAAI,GAAG,CAAC,EAAE,SAAS;AAAA,EACtF,SAAS,EAAE,OAAO,EAAE,OAAO,GAAG,EAAE,QAAQ,CAAC,EAAE,SAAS;AAAA,EACpD,SAAS,EACN,MAAM,EAAE,OAAO,EAAE,IAAI,EAAE,OAAO,EAAE,IAAI,CAAC,GAAG,MAAM,EAAE,QAAQ,EAAE,SAAS,EAAE,CAAC,CAAC,EACvE,IAAI,EAAE,EACN,SAAS;AAAA,EACZ,UAAU,EAAE,OAAO,EAAE,IAAI,EAAE,SAAS,EAAE,IAAI,GAAG,EAAE,SAAS;AAAA,EACxD,aAAa,EAAE,OAAO,EAAE,IAAI,GAAG,EAAE,SAAS;AAC5C,CAAC;AAIM,MAAM,wBAAwB,EAAE,OAAO;AAAA,EAC5C,eAAe,EAAE,OAAO,EAAE,KAAK,EAAE,SAAS;AAAA,EAC1C,MAAM,EAAE,OAAO,EAAE,IAAI,CAAC,EAAE,IAAI,GAAG;AAAA,EAC/B,UAAU;AAAA,EACV,WAAW,EAAE,QAAQ,EAAE,SAAS;AAAA,EAChC,cAAc,EAAE,MAAM,EAAE,OAAO,EAAE,KAAK,CAAC,EAAE,SAAS;AAAA,EAClD,cAAc,EAAE,MAAM,EAAE,OAAO,EAAE,KAAK,CAAC,EAAE,SAAS;AAAA,EAClD,+BAA+B,EAAE,OAAO,EAAE,OAAO,EAAE,KAAK,GAAG,EAAE,OAAO,EAAE,IAAI,CAAC,EAAE,SAAS,CAAC,EAAE,SAAS;AAAA,EAClG,sCAAsC,EAAE,OAAO,EAAE,OAAO,EAAE,KAAK,GAAG,EAAE,OAAO,EAAE,IAAI,CAAC,EAAE,SAAS,CAAC,EAAE,SAAS;AAAA,EACzG,gBAAgB,EAAE,QAAQ,EAAE,SAAS;AAAA,EACrC,YAAY,EAAE,QAAQ,EAAE,SAAS;AACnC,CAAC;AAEM,MAAM,0BAA0B,EAAE,OAAO;AAAA,EAC9C,OAAO,EAAE,QAAQ,EAAE,SAAS;AAC9B,CAAC;AAEM,MAAM,4BAA4B,EAAE,OAAO;AAAA,EAChD,SAAS,EAAE,MAAM,EAAE,OAAO,EAAE,KAAK,CAAC,EAAE,IAAI,CAAC;AAAA,EACzC,MAAM,EAAE,OAAO,EAAE,IAAI,CAAC,EAAE,IAAI,GAAG;AAAA,EAC/B,UAAU;AAAA,EACV,YAAY,EAAE,QAAQ,EAAE,SAAS;AACnC,CAAC;",
6
6
  "names": []
7
7
  }
@@ -195,6 +195,12 @@ async function saveUserPerspective(em, cache, options) {
195
195
  name: input.name,
196
196
  deletedAt: null
197
197
  });
198
+ if (entity && input.createOnly) {
199
+ throw new CrudHttpError(409, {
200
+ error: "A view with this name already exists.",
201
+ code: "duplicate_name"
202
+ });
203
+ }
198
204
  }
199
205
  const now = /* @__PURE__ */ new Date();
200
206
  if (!entity) {
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "version": 3,
3
3
  "sources": ["../../../../src/modules/perspectives/services/perspectiveService.ts"],
4
- "sourcesContent": ["import type { EntityManager } from '@mikro-orm/postgresql'\nimport type { CacheStrategy } from '@open-mercato/cache'\nimport { CrudHttpError } from '@open-mercato/shared/lib/crud/errors'\nimport {\n buildOptimisticLockConflictBody,\n enforceCommandOptimisticLock,\n enforceRecordGoneIsConflict,\n} from '@open-mercato/shared/lib/crud/optimistic-lock-command'\nimport { Perspective, RolePerspective } from '../data/entities'\nimport type {\n PerspectiveSettings,\n PerspectiveSaveInput,\n RolePerspectiveSaveInput,\n} from '../data/validators'\nimport { createLogger } from '@open-mercato/shared/lib/logger'\n\nconst logger = createLogger('perspectives').child({ component: 'perspective-service' })\n\nexport type PerspectiveScope = {\n userId: string\n tenantId?: string | null\n organizationId?: string | null\n}\n\nexport type ResolvedPerspective = {\n id: string\n name: string\n tableId: string\n settings: PerspectiveSettings\n isDefault: boolean\n createdAt: string\n updatedAt?: string | null\n}\n\nexport type ResolvedRolePerspective = {\n id: string\n roleId: string\n tableId: string\n name: string\n settings: PerspectiveSettings\n isDefault: boolean\n tenantId: string | null\n organizationId: string | null\n createdAt: string\n updatedAt?: string | null\n}\n\nexport type PerspectivesState = {\n tableId: string\n personal: ResolvedPerspective[]\n personalDefaultId: string | null\n rolePerspectives: ResolvedRolePerspective[]\n}\n\ntype ExpectedUpdatedAtById = Record<string, string | Date | null | undefined>\n\nconst PERSPECTIVE_RESOURCE_KIND = 'perspectives.perspective'\nconst ROLE_PERSPECTIVE_RESOURCE_KIND = 'perspectives.role_perspective'\n\nfunction resolveRoleLockInput(\n expectedUpdatedAtByRoleId: ExpectedUpdatedAtById | null | undefined,\n roleId: string,\n request: Request | Headers | null | undefined,\n): Pick<Parameters<typeof enforceCommandOptimisticLock>[0], 'expected' | 'request'> {\n if (expectedUpdatedAtByRoleId && Object.prototype.hasOwnProperty.call(expectedUpdatedAtByRoleId, roleId)) {\n return { expected: expectedUpdatedAtByRoleId[roleId] ?? null, request: null }\n }\n return { expected: undefined, request: request ?? null }\n}\n\nfunction resolveRoleRecordLockInput(\n expectedUpdatedAtByPerspectiveId: ExpectedUpdatedAtById | null | undefined,\n expectedUpdatedAtByRoleId: ExpectedUpdatedAtById | null | undefined,\n record: RolePerspective,\n request: Request | Headers | null | undefined,\n): Pick<Parameters<typeof enforceCommandOptimisticLock>[0], 'expected' | 'request'> {\n if (expectedUpdatedAtByPerspectiveId && Object.prototype.hasOwnProperty.call(expectedUpdatedAtByPerspectiveId, record.id)) {\n return { expected: expectedUpdatedAtByPerspectiveId[record.id] ?? null, request: null }\n }\n return resolveRoleLockInput(expectedUpdatedAtByRoleId, record.roleId, request)\n}\n\nfunction firstExpectedUpdatedAt(expectedUpdatedAtById: ExpectedUpdatedAtById | null | undefined): string | Date | null {\n if (!expectedUpdatedAtById) return null\n for (const value of Object.values(expectedUpdatedAtById)) {\n if (value instanceof Date) return value\n if (typeof value === 'string' && value.trim().length > 0) return value\n }\n return null\n}\n\nfunction throwMissingRoleRecordVersionConflict(record: RolePerspective): never {\n const current = record.updatedAt instanceof Date && Number.isFinite(record.updatedAt.getTime())\n ? record.updatedAt.toISOString()\n : new Date(0).toISOString()\n throw new CrudHttpError(409, buildOptimisticLockConflictBody(current, current))\n}\n\nconst CACHE_TTL_MS = 5 * 60 * 1000\n\nconst nullish = <T extends string | null | undefined>(value: T): string | null =>\n value == null ? null : value\n\nconst scopeKey = (scope: PerspectiveScope) =>\n `${scope.userId}:${scope.tenantId ?? 'null'}:${scope.organizationId ?? 'null'}`\n\nconst userCacheKey = (scope: PerspectiveScope, tableId: string, roleIds: string[]) =>\n `perspectives:user-state:${scopeKey(scope)}:${tableId}:${roleIds.sort((a, b) => a.localeCompare(b)).join(',')}`\n\nconst userTag = (scope: PerspectiveScope, tableId?: string) =>\n tableId\n ? `perspectives:user:${scopeKey(scope)}:${tableId}`\n : `perspectives:user:${scopeKey(scope)}`\n\nconst roleTag = (roleId: string, tableId?: string, tenantId?: string | null) => {\n const tenant = tenantId ?? 'null'\n return tableId ? `perspectives:role:${roleId}:${tenant}:${tableId}` : `perspectives:role:${roleId}:${tenant}`\n}\n\nfunction isResolvedPerspective(value: unknown): value is ResolvedPerspective {\n if (typeof value !== 'object' || value === null) return false\n const record = value as Partial<ResolvedPerspective>\n return typeof record.id === 'string'\n && typeof record.name === 'string'\n && typeof record.tableId === 'string'\n && typeof record.isDefault === 'boolean'\n && typeof record.createdAt === 'string'\n}\n\nfunction isResolvedRolePerspective(value: unknown): value is ResolvedRolePerspective {\n if (typeof value !== 'object' || value === null) return false\n const record = value as Partial<ResolvedRolePerspective>\n return typeof record.id === 'string'\n && typeof record.roleId === 'string'\n && typeof record.tableId === 'string'\n && typeof record.name === 'string'\n && typeof record.isDefault === 'boolean'\n && typeof record.createdAt === 'string'\n}\n\nfunction isPerspectivesState(value: unknown): value is PerspectivesState {\n if (typeof value !== 'object' || value === null) return false\n const record = value as Partial<PerspectivesState>\n if (typeof record.tableId !== 'string') return false\n if (!Array.isArray(record.personal) || record.personal.some((item) => !isResolvedPerspective(item))) return false\n if (record.personalDefaultId !== null && typeof record.personalDefaultId !== 'string') return false\n if (!Array.isArray(record.rolePerspectives) || record.rolePerspectives.some((item) => !isResolvedRolePerspective(item))) return false\n return true\n}\n\n/**\n * Defensive migration for legacy filter state shapes captured before the\n * advanced-filter tree (SPEC-048). Existing perspectives only store either\n * advanced-filter URL params (tree shape with `v:2` or a `root` key) or\n * undefined \u2014 this helper is a safety net for legacy `FilterValues`-shaped\n * records (flat key/value records of column filters) that could only appear\n * if old saved-view JSON were imported.\n *\n * - Tree-shaped state (`v:2` or `root` key) is passed through unchanged.\n * - Undefined / null filters are passed through unchanged.\n * - Legacy `FilterValues`-shaped records are dropped (set to `undefined`)\n * because there is no reliable mapping back to the new operator model;\n * the user sees an empty tree and can recreate.\n */\nexport function maybeMigrateLegacyFilterValues(settings: PerspectiveSettings): PerspectiveSettings {\n const filters = settings.filters\n if (!filters || typeof filters !== 'object') return settings\n const record = filters as Record<string, unknown>\n if ('v' in record && record.v === 2) return settings\n if ('root' in record) return settings\n logger.warn('Dropping legacy filterValues shape; please re-create the perspective with the new filter UI.')\n return { ...settings, filters: undefined }\n}\n\nfunction toResolvedPerspective(entity: Perspective): ResolvedPerspective {\n const settings = maybeMigrateLegacyFilterValues((entity.settingsJson ?? {}) as PerspectiveSettings)\n return {\n id: entity.id,\n name: entity.name,\n tableId: entity.tableId,\n isDefault: !!entity.isDefault,\n settings,\n createdAt: entity.createdAt.toISOString(),\n updatedAt: entity.updatedAt ? entity.updatedAt.toISOString() : null,\n }\n}\n\nfunction toResolvedRolePerspective(entity: RolePerspective): ResolvedRolePerspective {\n const settings = maybeMigrateLegacyFilterValues((entity.settingsJson ?? {}) as PerspectiveSettings)\n return {\n id: entity.id,\n roleId: entity.roleId,\n tableId: entity.tableId,\n name: entity.name,\n isDefault: !!entity.isDefault,\n settings,\n tenantId: nullish(entity.tenantId),\n organizationId: nullish(entity.organizationId),\n createdAt: entity.createdAt.toISOString(),\n updatedAt: entity.updatedAt ? entity.updatedAt.toISOString() : null,\n }\n}\n\nexport async function loadPerspectivesState(\n em: EntityManager,\n cache: CacheStrategy | null | undefined,\n options: { scope: PerspectiveScope; tableId: string; roleIds?: string[] },\n): Promise<PerspectivesState> {\n const { scope, tableId } = options\n const roleIds = Array.isArray(options.roleIds) ? options.roleIds.filter((id) => id && id.length > 0) : []\n const uniqueRoles = Array.from(new Set(roleIds))\n const cacheKey = cache && uniqueRoles.length <= 16 ? userCacheKey(scope, tableId, uniqueRoles) : null\n\n if (cache && cacheKey) {\n const cached = await cache.get(cacheKey)\n if (cached && isPerspectivesState(cached)) return cached\n }\n\n const tenantId = scope.tenantId ?? null\n const organizationId = scope.organizationId ?? null\n\n const [personal, roleRecords] = await Promise.all([\n em.find(Perspective, {\n userId: scope.userId,\n tenantId,\n organizationId,\n tableId,\n deletedAt: null,\n }, { orderBy: { updatedAt: 'desc' } }),\n uniqueRoles.length\n ? em.find(RolePerspective, {\n roleId: { $in: uniqueRoles as any },\n tableId,\n deletedAt: null,\n $and: [\n { $or: [{ tenantId }, { tenantId: null }] },\n { $or: [{ organizationId }, { organizationId: null }] },\n ],\n } as any, { orderBy: { updatedAt: 'desc' } })\n : [],\n ])\n\n const personalResolved = personal.map(toResolvedPerspective)\n const personalDefaultId = personalResolved.find((p) => p.isDefault)?.id ?? null\n const roleResolved = roleRecords.map(toResolvedRolePerspective)\n\n const state: PerspectivesState = {\n tableId,\n personal: personalResolved,\n personalDefaultId,\n rolePerspectives: roleResolved,\n }\n\n if (cache && cacheKey) {\n await cache.set(cacheKey, state, {\n ttl: CACHE_TTL_MS,\n tags: [\n userTag(scope, tableId),\n ...uniqueRoles.map((roleId) => roleTag(roleId, tableId, tenantId)),\n ],\n })\n }\n\n return state\n}\n\nexport async function saveUserPerspective(\n em: EntityManager,\n cache: CacheStrategy | null | undefined,\n options: {\n scope: PerspectiveScope\n tableId: string\n input: PerspectiveSaveInput\n request?: Request | Headers | null\n },\n): Promise<ResolvedPerspective> {\n const { scope, tableId, input } = options\n const tenantId = scope.tenantId ?? null\n const organizationId = scope.organizationId ?? null\n\n let entity: Perspective | null = null\n if (input.perspectiveId) {\n entity = await em.findOne(Perspective, {\n id: input.perspectiveId,\n userId: scope.userId,\n tenantId,\n organizationId,\n tableId,\n deletedAt: null,\n })\n if (!entity) {\n throw Object.assign(new Error('Perspective not found'), { code: 'NOT_FOUND' })\n }\n enforceCommandOptimisticLock({\n resourceKind: PERSPECTIVE_RESOURCE_KIND,\n resourceId: entity.id,\n current: entity.updatedAt ?? null,\n request: options.request ?? null,\n })\n if (entity.name !== input.name) {\n const duplicate = await em.findOne(Perspective, {\n userId: scope.userId,\n tenantId,\n organizationId,\n tableId,\n name: input.name,\n id: { $ne: entity.id } as any,\n deletedAt: null,\n })\n if (duplicate) {\n throw new CrudHttpError(409, {\n error: 'A view with this name already exists.',\n code: 'duplicate_name',\n })\n }\n }\n } else {\n entity = await em.findOne(Perspective, {\n userId: scope.userId,\n tenantId,\n organizationId,\n tableId,\n name: input.name,\n deletedAt: null,\n })\n }\n\n const now = new Date()\n if (!entity) {\n entity = em.create(Perspective, {\n userId: scope.userId,\n tenantId,\n organizationId,\n tableId,\n name: input.name,\n settingsJson: input.settings,\n isDefault: Boolean(input.isDefault),\n createdAt: now,\n updatedAt: now,\n })\n em.persist(entity)\n } else {\n entity.name = input.name\n entity.settingsJson = input.settings\n entity.updatedAt = now\n if (input.isDefault === true) entity.isDefault = true\n if (input.isDefault === false) entity.isDefault = false\n }\n\n if (input.isDefault === true) {\n await em.nativeUpdate(\n Perspective,\n {\n userId: scope.userId,\n tenantId,\n organizationId,\n tableId,\n id: { $ne: entity.id } as any,\n deletedAt: null,\n },\n { isDefault: false, updatedAt: now },\n )\n entity.isDefault = true\n }\n\n await em.flush()\n\n if (cache?.deleteByTags) {\n await cache.deleteByTags([userTag(scope, tableId)])\n }\n\n return toResolvedPerspective(entity)\n}\n\nexport async function deleteUserPerspective(\n em: EntityManager,\n cache: CacheStrategy | null | undefined,\n options: {\n scope: PerspectiveScope\n tableId: string\n perspectiveId: string\n request?: Request | Headers | null\n },\n): Promise<boolean> {\n const { scope, tableId, perspectiveId } = options\n const tenantId = scope.tenantId ?? null\n const organizationId = scope.organizationId ?? null\n\n const existing = await em.findOne(Perspective, {\n id: perspectiveId,\n userId: scope.userId,\n tenantId,\n organizationId,\n tableId,\n deletedAt: null,\n })\n if (!existing) {\n enforceRecordGoneIsConflict({\n resourceKind: PERSPECTIVE_RESOURCE_KIND,\n resourceId: perspectiveId,\n request: options.request ?? null,\n })\n return false\n }\n\n enforceCommandOptimisticLock({\n resourceKind: PERSPECTIVE_RESOURCE_KIND,\n resourceId: existing.id,\n current: existing.updatedAt ?? null,\n request: options.request ?? null,\n })\n\n existing.deletedAt = new Date()\n existing.isDefault = false\n await em.flush()\n\n if (cache?.deleteByTags) {\n await cache.deleteByTags([userTag(scope, tableId)])\n }\n\n return true\n}\n\nexport async function saveRolePerspectives(\n em: EntityManager,\n cache: CacheStrategy | null | undefined,\n options: {\n tableId: string\n tenantId?: string | null\n organizationId?: string | null\n input: RolePerspectiveSaveInput\n expectedUpdatedAtByRoleId?: ExpectedUpdatedAtById\n expectedUpdatedAtByPerspectiveId?: ExpectedUpdatedAtById\n request?: Request | Headers | null\n },\n): Promise<ResolvedRolePerspective[]> {\n const { tableId, input } = options\n const tenantId = options.tenantId ?? null\n const organizationId = options.organizationId ?? null\n const now = new Date()\n const touchedRoleIds = new Set<string>()\n\n const resultRecords: RolePerspective[] = []\n\n // Prefetch every matching role perspective in a single query, then index by role id\n // so the loop resolves create/update without a lookup per role.\n const recordByRole = new Map<string, RolePerspective>()\n if (input.roleIds.length) {\n const existingRecords = await em.find(RolePerspective, {\n roleId: { $in: input.roleIds },\n tableId,\n tenantId,\n organizationId,\n name: input.name,\n deletedAt: null,\n })\n for (const existing of existingRecords) recordByRole.set(existing.roleId, existing)\n }\n const defaultRecordsByRole = new Map<string, RolePerspective[]>()\n if (input.setDefault === true && input.roleIds.length) {\n const existingDefaultRecords = await em.find(RolePerspective, {\n roleId: { $in: input.roleIds },\n tableId,\n tenantId,\n organizationId,\n isDefault: true,\n deletedAt: null,\n })\n for (const existing of existingDefaultRecords) {\n const records = defaultRecordsByRole.get(existing.roleId) ?? []\n records.push(existing)\n defaultRecordsByRole.set(existing.roleId, records)\n }\n }\n\n for (const roleId of input.roleIds) {\n let record = recordByRole.get(roleId) ?? null\n if (!record) {\n record = em.create(RolePerspective, {\n roleId,\n tableId,\n tenantId,\n organizationId,\n name: input.name,\n settingsJson: input.settings,\n isDefault: Boolean(input.setDefault),\n createdAt: now,\n updatedAt: now,\n })\n em.persist(record)\n recordByRole.set(roleId, record)\n } else {\n enforceCommandOptimisticLock({\n resourceKind: ROLE_PERSPECTIVE_RESOURCE_KIND,\n resourceId: record.id,\n current: record.updatedAt ?? null,\n ...resolveRoleRecordLockInput(\n options.expectedUpdatedAtByPerspectiveId,\n options.expectedUpdatedAtByRoleId,\n record,\n options.request ?? null,\n ),\n })\n record.settingsJson = input.settings\n record.updatedAt = now\n if (input.setDefault === true) record.isDefault = true\n if (input.setDefault === false) record.isDefault = false\n }\n\n if (input.setDefault === true) {\n for (const defaultRecord of defaultRecordsByRole.get(roleId) ?? []) {\n if (defaultRecord.id === record.id) continue\n enforceCommandOptimisticLock({\n resourceKind: ROLE_PERSPECTIVE_RESOURCE_KIND,\n resourceId: defaultRecord.id,\n current: defaultRecord.updatedAt ?? null,\n ...resolveRoleRecordLockInput(\n options.expectedUpdatedAtByPerspectiveId,\n options.expectedUpdatedAtByRoleId,\n defaultRecord,\n options.request ?? null,\n ),\n })\n }\n await em.nativeUpdate(\n RolePerspective,\n {\n roleId,\n tableId,\n tenantId,\n organizationId,\n id: { $ne: record.id } as any,\n isDefault: true,\n deletedAt: null,\n },\n { isDefault: false, updatedAt: now },\n )\n record.isDefault = true\n }\n\n touchedRoleIds.add(roleId)\n resultRecords.push(record)\n }\n\n if (input.roleIds.length) {\n await em.flush()\n }\n\n if (cache?.deleteByTags && touchedRoleIds.size > 0) {\n const tags = Array.from(touchedRoleIds).map((roleId) => roleTag(roleId, tableId, tenantId))\n await cache.deleteByTags(tags)\n }\n\n return resultRecords.map(toResolvedRolePerspective)\n}\n\nexport async function clearRolePerspectives(\n em: EntityManager,\n cache: CacheStrategy | null | undefined,\n options: {\n tableId: string\n tenantId?: string | null\n organizationId?: string | null\n roleIds: string[]\n expectedUpdatedAtByRoleId?: ExpectedUpdatedAtById\n expectedUpdatedAtByPerspectiveId?: ExpectedUpdatedAtById\n request?: Request | Headers | null\n },\n): Promise<number> {\n const { tableId, roleIds } = options\n const tenantId = options.tenantId ?? null\n const organizationId = options.organizationId ?? null\n if (!roleIds.length) return 0\n\n const existingRecords = await em.find(RolePerspective, {\n roleId: { $in: roleIds as any },\n tableId,\n tenantId,\n organizationId,\n deletedAt: null,\n })\n const recordsByRole = new Map<string, RolePerspective[]>()\n for (const record of existingRecords) {\n const records = recordsByRole.get(record.roleId) ?? []\n records.push(record)\n recordsByRole.set(record.roleId, records)\n }\n\n for (const roleId of roleIds) {\n const records = recordsByRole.get(roleId) ?? []\n const lockInput = resolveRoleLockInput(options.expectedUpdatedAtByRoleId, roleId, options.request ?? null)\n if (!records.length) {\n const expected = firstExpectedUpdatedAt(options.expectedUpdatedAtByPerspectiveId)\n enforceRecordGoneIsConflict({\n resourceKind: ROLE_PERSPECTIVE_RESOURCE_KIND,\n resourceId: roleId,\n expected: expected ?? lockInput.expected,\n request: expected ? null : lockInput.request,\n })\n continue\n }\n for (const record of records) {\n if (\n options.expectedUpdatedAtByPerspectiveId\n && !Object.prototype.hasOwnProperty.call(options.expectedUpdatedAtByPerspectiveId, record.id)\n ) {\n throwMissingRoleRecordVersionConflict(record)\n }\n enforceCommandOptimisticLock({\n resourceKind: ROLE_PERSPECTIVE_RESOURCE_KIND,\n resourceId: record.id,\n current: record.updatedAt ?? null,\n ...resolveRoleRecordLockInput(\n options.expectedUpdatedAtByPerspectiveId,\n options.expectedUpdatedAtByRoleId,\n record,\n options.request ?? null,\n ),\n })\n }\n }\n\n const affected = await em.nativeUpdate(\n RolePerspective,\n {\n roleId: { $in: roleIds as any },\n tableId,\n tenantId,\n organizationId,\n deletedAt: null,\n },\n { deletedAt: new Date(), isDefault: false },\n )\n\n if (cache?.deleteByTags) {\n const tags = roleIds.map((roleId) => roleTag(roleId, tableId, tenantId))\n await cache.deleteByTags(tags)\n }\n\n return affected\n}\n"],
5
- "mappings": "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",
4
+ "sourcesContent": ["import type { EntityManager } from '@mikro-orm/postgresql'\nimport type { CacheStrategy } from '@open-mercato/cache'\nimport { CrudHttpError } from '@open-mercato/shared/lib/crud/errors'\nimport {\n buildOptimisticLockConflictBody,\n enforceCommandOptimisticLock,\n enforceRecordGoneIsConflict,\n} from '@open-mercato/shared/lib/crud/optimistic-lock-command'\nimport { Perspective, RolePerspective } from '../data/entities'\nimport type {\n PerspectiveSettings,\n PerspectiveSaveInput,\n RolePerspectiveSaveInput,\n} from '../data/validators'\nimport { createLogger } from '@open-mercato/shared/lib/logger'\n\nconst logger = createLogger('perspectives').child({ component: 'perspective-service' })\n\nexport type PerspectiveScope = {\n userId: string\n tenantId?: string | null\n organizationId?: string | null\n}\n\nexport type ResolvedPerspective = {\n id: string\n name: string\n tableId: string\n settings: PerspectiveSettings\n isDefault: boolean\n createdAt: string\n updatedAt?: string | null\n}\n\nexport type ResolvedRolePerspective = {\n id: string\n roleId: string\n tableId: string\n name: string\n settings: PerspectiveSettings\n isDefault: boolean\n tenantId: string | null\n organizationId: string | null\n createdAt: string\n updatedAt?: string | null\n}\n\nexport type PerspectivesState = {\n tableId: string\n personal: ResolvedPerspective[]\n personalDefaultId: string | null\n rolePerspectives: ResolvedRolePerspective[]\n}\n\ntype ExpectedUpdatedAtById = Record<string, string | Date | null | undefined>\n\nconst PERSPECTIVE_RESOURCE_KIND = 'perspectives.perspective'\nconst ROLE_PERSPECTIVE_RESOURCE_KIND = 'perspectives.role_perspective'\n\nfunction resolveRoleLockInput(\n expectedUpdatedAtByRoleId: ExpectedUpdatedAtById | null | undefined,\n roleId: string,\n request: Request | Headers | null | undefined,\n): Pick<Parameters<typeof enforceCommandOptimisticLock>[0], 'expected' | 'request'> {\n if (expectedUpdatedAtByRoleId && Object.prototype.hasOwnProperty.call(expectedUpdatedAtByRoleId, roleId)) {\n return { expected: expectedUpdatedAtByRoleId[roleId] ?? null, request: null }\n }\n return { expected: undefined, request: request ?? null }\n}\n\nfunction resolveRoleRecordLockInput(\n expectedUpdatedAtByPerspectiveId: ExpectedUpdatedAtById | null | undefined,\n expectedUpdatedAtByRoleId: ExpectedUpdatedAtById | null | undefined,\n record: RolePerspective,\n request: Request | Headers | null | undefined,\n): Pick<Parameters<typeof enforceCommandOptimisticLock>[0], 'expected' | 'request'> {\n if (expectedUpdatedAtByPerspectiveId && Object.prototype.hasOwnProperty.call(expectedUpdatedAtByPerspectiveId, record.id)) {\n return { expected: expectedUpdatedAtByPerspectiveId[record.id] ?? null, request: null }\n }\n return resolveRoleLockInput(expectedUpdatedAtByRoleId, record.roleId, request)\n}\n\nfunction firstExpectedUpdatedAt(expectedUpdatedAtById: ExpectedUpdatedAtById | null | undefined): string | Date | null {\n if (!expectedUpdatedAtById) return null\n for (const value of Object.values(expectedUpdatedAtById)) {\n if (value instanceof Date) return value\n if (typeof value === 'string' && value.trim().length > 0) return value\n }\n return null\n}\n\nfunction throwMissingRoleRecordVersionConflict(record: RolePerspective): never {\n const current = record.updatedAt instanceof Date && Number.isFinite(record.updatedAt.getTime())\n ? record.updatedAt.toISOString()\n : new Date(0).toISOString()\n throw new CrudHttpError(409, buildOptimisticLockConflictBody(current, current))\n}\n\nconst CACHE_TTL_MS = 5 * 60 * 1000\n\nconst nullish = <T extends string | null | undefined>(value: T): string | null =>\n value == null ? null : value\n\nconst scopeKey = (scope: PerspectiveScope) =>\n `${scope.userId}:${scope.tenantId ?? 'null'}:${scope.organizationId ?? 'null'}`\n\nconst userCacheKey = (scope: PerspectiveScope, tableId: string, roleIds: string[]) =>\n `perspectives:user-state:${scopeKey(scope)}:${tableId}:${roleIds.sort((a, b) => a.localeCompare(b)).join(',')}`\n\nconst userTag = (scope: PerspectiveScope, tableId?: string) =>\n tableId\n ? `perspectives:user:${scopeKey(scope)}:${tableId}`\n : `perspectives:user:${scopeKey(scope)}`\n\nconst roleTag = (roleId: string, tableId?: string, tenantId?: string | null) => {\n const tenant = tenantId ?? 'null'\n return tableId ? `perspectives:role:${roleId}:${tenant}:${tableId}` : `perspectives:role:${roleId}:${tenant}`\n}\n\nfunction isResolvedPerspective(value: unknown): value is ResolvedPerspective {\n if (typeof value !== 'object' || value === null) return false\n const record = value as Partial<ResolvedPerspective>\n return typeof record.id === 'string'\n && typeof record.name === 'string'\n && typeof record.tableId === 'string'\n && typeof record.isDefault === 'boolean'\n && typeof record.createdAt === 'string'\n}\n\nfunction isResolvedRolePerspective(value: unknown): value is ResolvedRolePerspective {\n if (typeof value !== 'object' || value === null) return false\n const record = value as Partial<ResolvedRolePerspective>\n return typeof record.id === 'string'\n && typeof record.roleId === 'string'\n && typeof record.tableId === 'string'\n && typeof record.name === 'string'\n && typeof record.isDefault === 'boolean'\n && typeof record.createdAt === 'string'\n}\n\nfunction isPerspectivesState(value: unknown): value is PerspectivesState {\n if (typeof value !== 'object' || value === null) return false\n const record = value as Partial<PerspectivesState>\n if (typeof record.tableId !== 'string') return false\n if (!Array.isArray(record.personal) || record.personal.some((item) => !isResolvedPerspective(item))) return false\n if (record.personalDefaultId !== null && typeof record.personalDefaultId !== 'string') return false\n if (!Array.isArray(record.rolePerspectives) || record.rolePerspectives.some((item) => !isResolvedRolePerspective(item))) return false\n return true\n}\n\n/**\n * Defensive migration for legacy filter state shapes captured before the\n * advanced-filter tree (SPEC-048). Existing perspectives only store either\n * advanced-filter URL params (tree shape with `v:2` or a `root` key) or\n * undefined \u2014 this helper is a safety net for legacy `FilterValues`-shaped\n * records (flat key/value records of column filters) that could only appear\n * if old saved-view JSON were imported.\n *\n * - Tree-shaped state (`v:2` or `root` key) is passed through unchanged.\n * - Undefined / null filters are passed through unchanged.\n * - Legacy `FilterValues`-shaped records are dropped (set to `undefined`)\n * because there is no reliable mapping back to the new operator model;\n * the user sees an empty tree and can recreate.\n */\nexport function maybeMigrateLegacyFilterValues(settings: PerspectiveSettings): PerspectiveSettings {\n const filters = settings.filters\n if (!filters || typeof filters !== 'object') return settings\n const record = filters as Record<string, unknown>\n if ('v' in record && record.v === 2) return settings\n if ('root' in record) return settings\n logger.warn('Dropping legacy filterValues shape; please re-create the perspective with the new filter UI.')\n return { ...settings, filters: undefined }\n}\n\nfunction toResolvedPerspective(entity: Perspective): ResolvedPerspective {\n const settings = maybeMigrateLegacyFilterValues((entity.settingsJson ?? {}) as PerspectiveSettings)\n return {\n id: entity.id,\n name: entity.name,\n tableId: entity.tableId,\n isDefault: !!entity.isDefault,\n settings,\n createdAt: entity.createdAt.toISOString(),\n updatedAt: entity.updatedAt ? entity.updatedAt.toISOString() : null,\n }\n}\n\nfunction toResolvedRolePerspective(entity: RolePerspective): ResolvedRolePerspective {\n const settings = maybeMigrateLegacyFilterValues((entity.settingsJson ?? {}) as PerspectiveSettings)\n return {\n id: entity.id,\n roleId: entity.roleId,\n tableId: entity.tableId,\n name: entity.name,\n isDefault: !!entity.isDefault,\n settings,\n tenantId: nullish(entity.tenantId),\n organizationId: nullish(entity.organizationId),\n createdAt: entity.createdAt.toISOString(),\n updatedAt: entity.updatedAt ? entity.updatedAt.toISOString() : null,\n }\n}\n\nexport async function loadPerspectivesState(\n em: EntityManager,\n cache: CacheStrategy | null | undefined,\n options: { scope: PerspectiveScope; tableId: string; roleIds?: string[] },\n): Promise<PerspectivesState> {\n const { scope, tableId } = options\n const roleIds = Array.isArray(options.roleIds) ? options.roleIds.filter((id) => id && id.length > 0) : []\n const uniqueRoles = Array.from(new Set(roleIds))\n const cacheKey = cache && uniqueRoles.length <= 16 ? userCacheKey(scope, tableId, uniqueRoles) : null\n\n if (cache && cacheKey) {\n const cached = await cache.get(cacheKey)\n if (cached && isPerspectivesState(cached)) return cached\n }\n\n const tenantId = scope.tenantId ?? null\n const organizationId = scope.organizationId ?? null\n\n const [personal, roleRecords] = await Promise.all([\n em.find(Perspective, {\n userId: scope.userId,\n tenantId,\n organizationId,\n tableId,\n deletedAt: null,\n }, { orderBy: { updatedAt: 'desc' } }),\n uniqueRoles.length\n ? em.find(RolePerspective, {\n roleId: { $in: uniqueRoles as any },\n tableId,\n deletedAt: null,\n $and: [\n { $or: [{ tenantId }, { tenantId: null }] },\n { $or: [{ organizationId }, { organizationId: null }] },\n ],\n } as any, { orderBy: { updatedAt: 'desc' } })\n : [],\n ])\n\n const personalResolved = personal.map(toResolvedPerspective)\n const personalDefaultId = personalResolved.find((p) => p.isDefault)?.id ?? null\n const roleResolved = roleRecords.map(toResolvedRolePerspective)\n\n const state: PerspectivesState = {\n tableId,\n personal: personalResolved,\n personalDefaultId,\n rolePerspectives: roleResolved,\n }\n\n if (cache && cacheKey) {\n await cache.set(cacheKey, state, {\n ttl: CACHE_TTL_MS,\n tags: [\n userTag(scope, tableId),\n ...uniqueRoles.map((roleId) => roleTag(roleId, tableId, tenantId)),\n ],\n })\n }\n\n return state\n}\n\nexport async function saveUserPerspective(\n em: EntityManager,\n cache: CacheStrategy | null | undefined,\n options: {\n scope: PerspectiveScope\n tableId: string\n input: PerspectiveSaveInput\n request?: Request | Headers | null\n },\n): Promise<ResolvedPerspective> {\n const { scope, tableId, input } = options\n const tenantId = scope.tenantId ?? null\n const organizationId = scope.organizationId ?? null\n\n let entity: Perspective | null = null\n if (input.perspectiveId) {\n entity = await em.findOne(Perspective, {\n id: input.perspectiveId,\n userId: scope.userId,\n tenantId,\n organizationId,\n tableId,\n deletedAt: null,\n })\n if (!entity) {\n throw Object.assign(new Error('Perspective not found'), { code: 'NOT_FOUND' })\n }\n enforceCommandOptimisticLock({\n resourceKind: PERSPECTIVE_RESOURCE_KIND,\n resourceId: entity.id,\n current: entity.updatedAt ?? null,\n request: options.request ?? null,\n })\n if (entity.name !== input.name) {\n const duplicate = await em.findOne(Perspective, {\n userId: scope.userId,\n tenantId,\n organizationId,\n tableId,\n name: input.name,\n id: { $ne: entity.id } as any,\n deletedAt: null,\n })\n if (duplicate) {\n throw new CrudHttpError(409, {\n error: 'A view with this name already exists.',\n code: 'duplicate_name',\n })\n }\n }\n } else {\n entity = await em.findOne(Perspective, {\n userId: scope.userId,\n tenantId,\n organizationId,\n tableId,\n name: input.name,\n deletedAt: null,\n })\n if (entity && input.createOnly) {\n throw new CrudHttpError(409, {\n error: 'A view with this name already exists.',\n code: 'duplicate_name',\n })\n }\n }\n\n const now = new Date()\n if (!entity) {\n entity = em.create(Perspective, {\n userId: scope.userId,\n tenantId,\n organizationId,\n tableId,\n name: input.name,\n settingsJson: input.settings,\n isDefault: Boolean(input.isDefault),\n createdAt: now,\n updatedAt: now,\n })\n em.persist(entity)\n } else {\n entity.name = input.name\n entity.settingsJson = input.settings\n entity.updatedAt = now\n if (input.isDefault === true) entity.isDefault = true\n if (input.isDefault === false) entity.isDefault = false\n }\n\n if (input.isDefault === true) {\n await em.nativeUpdate(\n Perspective,\n {\n userId: scope.userId,\n tenantId,\n organizationId,\n tableId,\n id: { $ne: entity.id } as any,\n deletedAt: null,\n },\n { isDefault: false, updatedAt: now },\n )\n entity.isDefault = true\n }\n\n await em.flush()\n\n if (cache?.deleteByTags) {\n await cache.deleteByTags([userTag(scope, tableId)])\n }\n\n return toResolvedPerspective(entity)\n}\n\nexport async function deleteUserPerspective(\n em: EntityManager,\n cache: CacheStrategy | null | undefined,\n options: {\n scope: PerspectiveScope\n tableId: string\n perspectiveId: string\n request?: Request | Headers | null\n },\n): Promise<boolean> {\n const { scope, tableId, perspectiveId } = options\n const tenantId = scope.tenantId ?? null\n const organizationId = scope.organizationId ?? null\n\n const existing = await em.findOne(Perspective, {\n id: perspectiveId,\n userId: scope.userId,\n tenantId,\n organizationId,\n tableId,\n deletedAt: null,\n })\n if (!existing) {\n enforceRecordGoneIsConflict({\n resourceKind: PERSPECTIVE_RESOURCE_KIND,\n resourceId: perspectiveId,\n request: options.request ?? null,\n })\n return false\n }\n\n enforceCommandOptimisticLock({\n resourceKind: PERSPECTIVE_RESOURCE_KIND,\n resourceId: existing.id,\n current: existing.updatedAt ?? null,\n request: options.request ?? null,\n })\n\n existing.deletedAt = new Date()\n existing.isDefault = false\n await em.flush()\n\n if (cache?.deleteByTags) {\n await cache.deleteByTags([userTag(scope, tableId)])\n }\n\n return true\n}\n\nexport async function saveRolePerspectives(\n em: EntityManager,\n cache: CacheStrategy | null | undefined,\n options: {\n tableId: string\n tenantId?: string | null\n organizationId?: string | null\n input: RolePerspectiveSaveInput\n expectedUpdatedAtByRoleId?: ExpectedUpdatedAtById\n expectedUpdatedAtByPerspectiveId?: ExpectedUpdatedAtById\n request?: Request | Headers | null\n },\n): Promise<ResolvedRolePerspective[]> {\n const { tableId, input } = options\n const tenantId = options.tenantId ?? null\n const organizationId = options.organizationId ?? null\n const now = new Date()\n const touchedRoleIds = new Set<string>()\n\n const resultRecords: RolePerspective[] = []\n\n // Prefetch every matching role perspective in a single query, then index by role id\n // so the loop resolves create/update without a lookup per role.\n const recordByRole = new Map<string, RolePerspective>()\n if (input.roleIds.length) {\n const existingRecords = await em.find(RolePerspective, {\n roleId: { $in: input.roleIds },\n tableId,\n tenantId,\n organizationId,\n name: input.name,\n deletedAt: null,\n })\n for (const existing of existingRecords) recordByRole.set(existing.roleId, existing)\n }\n const defaultRecordsByRole = new Map<string, RolePerspective[]>()\n if (input.setDefault === true && input.roleIds.length) {\n const existingDefaultRecords = await em.find(RolePerspective, {\n roleId: { $in: input.roleIds },\n tableId,\n tenantId,\n organizationId,\n isDefault: true,\n deletedAt: null,\n })\n for (const existing of existingDefaultRecords) {\n const records = defaultRecordsByRole.get(existing.roleId) ?? []\n records.push(existing)\n defaultRecordsByRole.set(existing.roleId, records)\n }\n }\n\n for (const roleId of input.roleIds) {\n let record = recordByRole.get(roleId) ?? null\n if (!record) {\n record = em.create(RolePerspective, {\n roleId,\n tableId,\n tenantId,\n organizationId,\n name: input.name,\n settingsJson: input.settings,\n isDefault: Boolean(input.setDefault),\n createdAt: now,\n updatedAt: now,\n })\n em.persist(record)\n recordByRole.set(roleId, record)\n } else {\n enforceCommandOptimisticLock({\n resourceKind: ROLE_PERSPECTIVE_RESOURCE_KIND,\n resourceId: record.id,\n current: record.updatedAt ?? null,\n ...resolveRoleRecordLockInput(\n options.expectedUpdatedAtByPerspectiveId,\n options.expectedUpdatedAtByRoleId,\n record,\n options.request ?? null,\n ),\n })\n record.settingsJson = input.settings\n record.updatedAt = now\n if (input.setDefault === true) record.isDefault = true\n if (input.setDefault === false) record.isDefault = false\n }\n\n if (input.setDefault === true) {\n for (const defaultRecord of defaultRecordsByRole.get(roleId) ?? []) {\n if (defaultRecord.id === record.id) continue\n enforceCommandOptimisticLock({\n resourceKind: ROLE_PERSPECTIVE_RESOURCE_KIND,\n resourceId: defaultRecord.id,\n current: defaultRecord.updatedAt ?? null,\n ...resolveRoleRecordLockInput(\n options.expectedUpdatedAtByPerspectiveId,\n options.expectedUpdatedAtByRoleId,\n defaultRecord,\n options.request ?? null,\n ),\n })\n }\n await em.nativeUpdate(\n RolePerspective,\n {\n roleId,\n tableId,\n tenantId,\n organizationId,\n id: { $ne: record.id } as any,\n isDefault: true,\n deletedAt: null,\n },\n { isDefault: false, updatedAt: now },\n )\n record.isDefault = true\n }\n\n touchedRoleIds.add(roleId)\n resultRecords.push(record)\n }\n\n if (input.roleIds.length) {\n await em.flush()\n }\n\n if (cache?.deleteByTags && touchedRoleIds.size > 0) {\n const tags = Array.from(touchedRoleIds).map((roleId) => roleTag(roleId, tableId, tenantId))\n await cache.deleteByTags(tags)\n }\n\n return resultRecords.map(toResolvedRolePerspective)\n}\n\nexport async function clearRolePerspectives(\n em: EntityManager,\n cache: CacheStrategy | null | undefined,\n options: {\n tableId: string\n tenantId?: string | null\n organizationId?: string | null\n roleIds: string[]\n expectedUpdatedAtByRoleId?: ExpectedUpdatedAtById\n expectedUpdatedAtByPerspectiveId?: ExpectedUpdatedAtById\n request?: Request | Headers | null\n },\n): Promise<number> {\n const { tableId, roleIds } = options\n const tenantId = options.tenantId ?? null\n const organizationId = options.organizationId ?? null\n if (!roleIds.length) return 0\n\n const existingRecords = await em.find(RolePerspective, {\n roleId: { $in: roleIds as any },\n tableId,\n tenantId,\n organizationId,\n deletedAt: null,\n })\n const recordsByRole = new Map<string, RolePerspective[]>()\n for (const record of existingRecords) {\n const records = recordsByRole.get(record.roleId) ?? []\n records.push(record)\n recordsByRole.set(record.roleId, records)\n }\n\n for (const roleId of roleIds) {\n const records = recordsByRole.get(roleId) ?? []\n const lockInput = resolveRoleLockInput(options.expectedUpdatedAtByRoleId, roleId, options.request ?? null)\n if (!records.length) {\n const expected = firstExpectedUpdatedAt(options.expectedUpdatedAtByPerspectiveId)\n enforceRecordGoneIsConflict({\n resourceKind: ROLE_PERSPECTIVE_RESOURCE_KIND,\n resourceId: roleId,\n expected: expected ?? lockInput.expected,\n request: expected ? null : lockInput.request,\n })\n continue\n }\n for (const record of records) {\n if (\n options.expectedUpdatedAtByPerspectiveId\n && !Object.prototype.hasOwnProperty.call(options.expectedUpdatedAtByPerspectiveId, record.id)\n ) {\n throwMissingRoleRecordVersionConflict(record)\n }\n enforceCommandOptimisticLock({\n resourceKind: ROLE_PERSPECTIVE_RESOURCE_KIND,\n resourceId: record.id,\n current: record.updatedAt ?? null,\n ...resolveRoleRecordLockInput(\n options.expectedUpdatedAtByPerspectiveId,\n options.expectedUpdatedAtByRoleId,\n record,\n options.request ?? null,\n ),\n })\n }\n }\n\n const affected = await em.nativeUpdate(\n RolePerspective,\n {\n roleId: { $in: roleIds as any },\n tableId,\n tenantId,\n organizationId,\n deletedAt: null,\n },\n { deletedAt: new Date(), isDefault: false },\n )\n\n if (cache?.deleteByTags) {\n const tags = roleIds.map((roleId) => roleTag(roleId, tableId, tenantId))\n await cache.deleteByTags(tags)\n }\n\n return affected\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.10.1-develop.7937.1.cf4cfe1a1f",
3
+ "version": "0.10.1-develop.7939.1.a561278b7a",
4
4
  "license": "MIT",
5
5
  "type": "module",
6
6
  "main": "./dist/index.js",
@@ -266,16 +266,16 @@
266
266
  "zod": "^4.6.6"
267
267
  },
268
268
  "peerDependencies": {
269
- "@open-mercato/ai-assistant": "0.10.1-develop.7937.1.cf4cfe1a1f",
270
- "@open-mercato/shared": "0.10.1-develop.7937.1.cf4cfe1a1f",
271
- "@open-mercato/ui": "0.10.1-develop.7937.1.cf4cfe1a1f",
269
+ "@open-mercato/ai-assistant": "0.10.1-develop.7939.1.a561278b7a",
270
+ "@open-mercato/shared": "0.10.1-develop.7939.1.a561278b7a",
271
+ "@open-mercato/ui": "0.10.1-develop.7939.1.a561278b7a",
272
272
  "react": "^19.0.0",
273
273
  "react-dom": "^19.0.0"
274
274
  },
275
275
  "devDependencies": {
276
- "@open-mercato/ai-assistant": "0.10.1-develop.7937.1.cf4cfe1a1f",
277
- "@open-mercato/shared": "0.10.1-develop.7937.1.cf4cfe1a1f",
278
- "@open-mercato/ui": "0.10.1-develop.7937.1.cf4cfe1a1f",
276
+ "@open-mercato/ai-assistant": "0.10.1-develop.7939.1.a561278b7a",
277
+ "@open-mercato/shared": "0.10.1-develop.7939.1.a561278b7a",
278
+ "@open-mercato/ui": "0.10.1-develop.7939.1.a561278b7a",
279
279
  "@testing-library/dom": "^10.4.1",
280
280
  "@testing-library/jest-dom": "^7.0.1",
281
281
  "@testing-library/react": "^16.3.3",
@@ -4,6 +4,7 @@ import { getAuthFromRequest } from '@open-mercato/shared/lib/auth/server'
4
4
  import { createRequestContainer } from '@open-mercato/shared/lib/di/container'
5
5
  import { withAtomicFlush } from '@open-mercato/shared/lib/commands/flush'
6
6
  import { isCrudHttpError, isUniqueViolation } from '@open-mercato/shared/lib/crud/errors'
7
+ import { resolveTranslations } from '@open-mercato/shared/lib/i18n/server'
7
8
  import {
8
9
  runCrudMutationGuardAfterSuccess,
9
10
  validateCrudMutationGuard,
@@ -305,14 +306,22 @@ export async function POST(req: Request, ctx: { params: { tableId: string } }) {
305
306
  },
306
307
  ], { transaction: true })
307
308
  } catch (err) {
309
+ if (isUniqueViolation(err) || (isCrudHttpError(err) && err.body.code === 'duplicate_name')) {
310
+ const { translate } = await resolveTranslations()
311
+ return NextResponse.json({
312
+ error: translate('ui.perspectives.error.nameExists', 'A view with this name already exists.'),
313
+ code: 'duplicate_name',
314
+ }, { status: 409 })
315
+ }
308
316
  if (isCrudHttpError(err)) {
309
317
  return NextResponse.json(err.body, { status: err.status })
310
318
  }
311
- if (isUniqueViolation(err)) {
319
+ if (err instanceof Error && 'code' in err && err.code === 'NOT_FOUND') {
320
+ const { translate } = await resolveTranslations()
312
321
  return NextResponse.json({
313
- error: 'A view with this name already exists.',
314
- code: 'duplicate_name',
315
- }, { status: 409 })
322
+ error: translate('api.errors.notFound', 'Not Found'),
323
+ code: 'NOT_FOUND',
324
+ }, { status: 404 })
316
325
  }
317
326
  throw err
318
327
  }
@@ -386,6 +395,7 @@ const perspectivesPostDoc: OpenApiMethodDoc = {
386
395
  { status: 400, description: 'Validation failed or invalid roles provided', schema: perspectivesErrorSchema },
387
396
  { status: 401, description: 'Authentication required', schema: perspectivesErrorSchema },
388
397
  { status: 403, description: 'Missing perspectives.role_defaults feature for role updates', schema: perspectivesErrorSchema },
398
+ { status: 404, description: 'Personal perspective does not exist in the current user scope', schema: perspectivesErrorSchema },
389
399
  { status: 409, description: 'Optimistic lock conflict or perspective name already exists', schema: perspectivesErrorSchema },
390
400
  ],
391
401
  }
@@ -29,6 +29,7 @@ export const perspectiveSaveSchema = z.object({
29
29
  roleExpectedUpdatedAtByRoleId: z.record(z.string().uuid(), z.string().min(1).nullable()).optional(),
30
30
  roleExpectedUpdatedAtByPerspectiveId: z.record(z.string().uuid(), z.string().min(1).nullable()).optional(),
31
31
  setRoleDefault: z.boolean().optional(),
32
+ createOnly: z.boolean().optional(),
32
33
  })
33
34
 
34
35
  export const perspectiveDeleteSchema = z.object({
@@ -323,6 +323,12 @@ export async function saveUserPerspective(
323
323
  name: input.name,
324
324
  deletedAt: null,
325
325
  })
326
+ if (entity && input.createOnly) {
327
+ throw new CrudHttpError(409, {
328
+ error: 'A view with this name already exists.',
329
+ code: 'duplicate_name',
330
+ })
331
+ }
326
332
  }
327
333
 
328
334
  const now = new Date()