@open-mercato/core 0.6.6-develop.6255.1.a6ee4a57c1 → 0.6.6-develop.6257.1.6d0af84d26

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.
Files changed (49) hide show
  1. package/.turbo/turbo-build.log +1 -1
  2. package/dist/modules/messages/api/[id]/conversation/archive/route.js +25 -7
  3. package/dist/modules/messages/api/[id]/conversation/archive/route.js.map +2 -2
  4. package/dist/modules/messages/api/[id]/conversation/read/route.js +24 -6
  5. package/dist/modules/messages/api/[id]/conversation/read/route.js.map +2 -2
  6. package/dist/modules/messages/api/[id]/route.js +13 -1
  7. package/dist/modules/messages/api/[id]/route.js.map +2 -2
  8. package/dist/modules/messages/api/openapi.js +3 -1
  9. package/dist/modules/messages/api/openapi.js.map +2 -2
  10. package/dist/modules/messages/commands/conversation.js +77 -0
  11. package/dist/modules/messages/commands/conversation.js.map +2 -2
  12. package/dist/modules/messages/components/MessageDetailPageClient.js +16 -4
  13. package/dist/modules/messages/components/MessageDetailPageClient.js.map +2 -2
  14. package/dist/modules/messages/components/message-detail/hooks/useMessageDetailsActions.js +18 -0
  15. package/dist/modules/messages/components/message-detail/hooks/useMessageDetailsActions.js.map +2 -2
  16. package/dist/modules/messages/components/message-detail/panels/MainMessageHeader.js +13 -11
  17. package/dist/modules/messages/components/message-detail/panels/MainMessageHeader.js.map +2 -2
  18. package/dist/modules/perspectives/api/[tableId]/[perspectiveId]/route.js +20 -9
  19. package/dist/modules/perspectives/api/[tableId]/[perspectiveId]/route.js.map +2 -2
  20. package/dist/modules/perspectives/api/[tableId]/roles/[roleId]/route.js +43 -6
  21. package/dist/modules/perspectives/api/[tableId]/roles/[roleId]/route.js.map +2 -2
  22. package/dist/modules/perspectives/api/[tableId]/route.js +21 -4
  23. package/dist/modules/perspectives/api/[tableId]/route.js.map +2 -2
  24. package/dist/modules/perspectives/api/openapi.js +1 -0
  25. package/dist/modules/perspectives/api/openapi.js.map +2 -2
  26. package/dist/modules/perspectives/data/validators.js +2 -0
  27. package/dist/modules/perspectives/data/validators.js.map +2 -2
  28. package/dist/modules/perspectives/services/perspectiveService.js +135 -6
  29. package/dist/modules/perspectives/services/perspectiveService.js.map +2 -2
  30. package/package.json +7 -7
  31. package/src/modules/messages/api/[id]/conversation/archive/route.ts +29 -6
  32. package/src/modules/messages/api/[id]/conversation/read/route.ts +29 -6
  33. package/src/modules/messages/api/[id]/route.ts +15 -0
  34. package/src/modules/messages/api/openapi.ts +2 -0
  35. package/src/modules/messages/commands/conversation.ts +87 -0
  36. package/src/modules/messages/components/MessageDetailPageClient.tsx +16 -4
  37. package/src/modules/messages/components/message-detail/hooks/useMessageDetailsActions.ts +26 -1
  38. package/src/modules/messages/components/message-detail/panels/MainMessageHeader.tsx +29 -14
  39. package/src/modules/messages/components/message-detail/types.ts +2 -0
  40. package/src/modules/messages/i18n/de.json +4 -0
  41. package/src/modules/messages/i18n/en.json +4 -0
  42. package/src/modules/messages/i18n/es.json +4 -0
  43. package/src/modules/messages/i18n/pl.json +4 -0
  44. package/src/modules/perspectives/api/[tableId]/[perspectiveId]/route.ts +20 -9
  45. package/src/modules/perspectives/api/[tableId]/roles/[roleId]/route.ts +45 -6
  46. package/src/modules/perspectives/api/[tableId]/route.ts +23 -1
  47. package/src/modules/perspectives/api/openapi.ts +1 -1
  48. package/src/modules/perspectives/data/validators.ts +2 -0
  49. package/src/modules/perspectives/services/perspectiveService.ts +171 -7
@@ -2,6 +2,7 @@ import { NextResponse } from "next/server";
2
2
  import { z } from "zod";
3
3
  import { getAuthFromRequest } from "@open-mercato/shared/lib/auth/server";
4
4
  import { createRequestContainer } from "@open-mercato/shared/lib/di/container";
5
+ import { isCrudHttpError } from "@open-mercato/shared/lib/crud/errors";
5
6
  import {
6
7
  runCrudMutationGuardAfterSuccess,
7
8
  validateCrudMutationGuard
@@ -21,6 +22,10 @@ const decodeParam = (value) => {
21
22
  return raw;
22
23
  }
23
24
  };
25
+ const rolePerspectiveDeleteBodySchema = z.object({
26
+ roleExpectedUpdatedAtByRoleId: z.record(z.string().uuid(), z.string().min(1).nullable()).optional(),
27
+ roleExpectedUpdatedAtByPerspectiveId: z.record(z.string().uuid(), z.string().min(1).nullable()).optional()
28
+ }).optional();
24
29
  async function DELETE(req, ctx) {
25
30
  const auth = await getAuthFromRequest(req);
26
31
  if (!auth) return NextResponse.json({ error: "Unauthorized" }, { status: 401 });
@@ -29,6 +34,21 @@ async function DELETE(req, ctx) {
29
34
  if (!tableId || !roleId) {
30
35
  return NextResponse.json({ error: "Invalid parameters" }, { status: 400 });
31
36
  }
37
+ let parsedBody = void 0;
38
+ if (req.body && (req.headers.get("content-type") ?? "").includes("application/json")) {
39
+ try {
40
+ const rawBody = await req.text();
41
+ if (rawBody.trim().length > 0) {
42
+ const parsed = rolePerspectiveDeleteBodySchema.safeParse(JSON.parse(rawBody));
43
+ if (!parsed.success) {
44
+ return NextResponse.json({ error: "Invalid payload", details: parsed.error.flatten() }, { status: 400 });
45
+ }
46
+ parsedBody = parsed.data;
47
+ }
48
+ } catch {
49
+ return NextResponse.json({ error: "Invalid JSON" }, { status: 400 });
50
+ }
51
+ }
32
52
  const container = await createRequestContainer();
33
53
  const em = container.resolve("em");
34
54
  const cache = (() => {
@@ -55,12 +75,23 @@ async function DELETE(req, ctx) {
55
75
  if (guardResult && !guardResult.ok) {
56
76
  return NextResponse.json(guardResult.body, { status: guardResult.status });
57
77
  }
58
- const clearedCount = await clearRolePerspectives(em, cache, {
59
- tableId,
60
- tenantId: auth.tenantId ?? null,
61
- organizationId: auth.orgId ?? null,
62
- roleIds: [roleId]
63
- });
78
+ let clearedCount = 0;
79
+ try {
80
+ clearedCount = await clearRolePerspectives(em, cache, {
81
+ tableId,
82
+ tenantId: auth.tenantId ?? null,
83
+ organizationId: auth.orgId ?? null,
84
+ roleIds: [roleId],
85
+ expectedUpdatedAtByRoleId: parsedBody?.roleExpectedUpdatedAtByRoleId,
86
+ expectedUpdatedAtByPerspectiveId: parsedBody?.roleExpectedUpdatedAtByPerspectiveId,
87
+ request: req
88
+ });
89
+ } catch (err) {
90
+ if (isCrudHttpError(err)) {
91
+ return NextResponse.json(err.body, { status: err.status });
92
+ }
93
+ throw err;
94
+ }
64
95
  if (clearedCount > 0 && guardResult?.ok && guardResult.shouldRunAfterSuccess) {
65
96
  await runCrudMutationGuardAfterSuccess(container, {
66
97
  tenantId: auth.tenantId ?? "",
@@ -84,6 +115,11 @@ const rolePerspectiveDeleteDoc = {
84
115
  summary: "Clear role perspectives for a table",
85
116
  description: "Removes all role-level perspectives associated with the provided role identifier for the table.",
86
117
  tags: [perspectivesTag],
118
+ requestBody: {
119
+ contentType: "application/json",
120
+ schema: rolePerspectiveDeleteBodySchema,
121
+ description: "Optional per-role or per-perspective optimistic-lock tokens."
122
+ },
87
123
  responses: [
88
124
  { status: 200, description: "Role perspectives cleared.", schema: perspectivesSuccessSchema }
89
125
  ],
@@ -91,6 +127,7 @@ const rolePerspectiveDeleteDoc = {
91
127
  { status: 400, description: "Invalid identifiers supplied", schema: perspectivesErrorSchema },
92
128
  { status: 401, description: "Authentication required", schema: perspectivesErrorSchema },
93
129
  { status: 403, description: "Missing perspectives.role_defaults feature", schema: perspectivesErrorSchema },
130
+ { status: 409, description: "Optimistic lock conflict", schema: perspectivesErrorSchema },
94
131
  { status: 404, description: "Role not found in scope", schema: perspectivesErrorSchema }
95
132
  ]
96
133
  };
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "version": 3,
3
3
  "sources": ["../../../../../../../src/modules/perspectives/api/%5BtableId%5D/roles/%5BroleId%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 {\n runCrudMutationGuardAfterSuccess,\n validateCrudMutationGuard,\n} from '@open-mercato/shared/lib/crud/mutation-guard'\nimport { clearRolePerspectives } 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 { perspectivesTag, perspectivesErrorSchema, perspectivesSuccessSchema } from '../../../openapi'\n\nexport const metadata = {\n DELETE: { requireAuth: true, requireFeatures: ['perspectives.role_defaults'] },\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\nexport async function DELETE(req: Request, ctx: { params: { tableId: string; roleId: 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 const roleId = decodeParam(ctx.params?.roleId).trim()\n if (!tableId || !roleId) {\n return NextResponse.json({ error: 'Invalid parameters' }, { 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\n const scope = auth.tenantId\n ? { $or: [{ tenantId: auth.tenantId }, { tenantId: null }] }\n : { tenantId: null }\n\n const role = await em.findOne(Role, { id: roleId, deletedAt: null, ...(scope as any) } as any)\n if (!role) return NextResponse.json({ error: 'Role not found' }, { status: 404 })\n\n const guardResult = await validateCrudMutationGuard(container, {\n tenantId: auth.tenantId ?? '',\n organizationId: auth.orgId ?? null,\n userId: auth.sub,\n resourceKind: 'perspectives.role_perspective',\n resourceId: roleId,\n operation: 'delete',\n requestMethod: req.method,\n requestHeaders: req.headers,\n mutationPayload: { tableId, roleId },\n })\n if (guardResult && !guardResult.ok) {\n return NextResponse.json(guardResult.body, { status: guardResult.status })\n }\n\n const clearedCount = await clearRolePerspectives(em, cache, {\n tableId,\n tenantId: auth.tenantId ?? null,\n organizationId: auth.orgId ?? null,\n roleIds: [roleId],\n })\n\n if (clearedCount > 0 && guardResult?.ok && guardResult.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: roleId,\n operation: 'delete',\n requestMethod: req.method,\n requestHeaders: req.headers,\n metadata: guardResult.metadata ?? null,\n })\n }\n\n return NextResponse.json({ success: true })\n}\n\nconst rolePerspectiveDeleteParamsSchema = z.object({\n tableId: z.string().min(1),\n roleId: z.string().uuid(),\n})\n\nconst rolePerspectiveDeleteDoc: OpenApiMethodDoc = {\n summary: 'Clear role perspectives for a table',\n description: 'Removes all role-level perspectives associated with the provided role identifier for the table.',\n tags: [perspectivesTag],\n responses: [\n { status: 200, description: 'Role perspectives cleared.', schema: perspectivesSuccessSchema },\n ],\n errors: [\n { status: 400, description: 'Invalid identifiers supplied', schema: perspectivesErrorSchema },\n { status: 401, description: 'Authentication required', schema: perspectivesErrorSchema },\n { status: 403, description: 'Missing perspectives.role_defaults feature', schema: perspectivesErrorSchema },\n { status: 404, description: 'Role not found in scope', schema: perspectivesErrorSchema },\n ],\n}\n\nexport const openApi: OpenApiRouteDoc = {\n tag: perspectivesTag,\n summary: 'Delete role-level perspectives',\n pathParams: rolePerspectiveDeleteParamsSchema,\n methods: {\n DELETE: rolePerspectiveDeleteDoc,\n },\n}\n"],
5
- "mappings": "AAAA,SAAS,oBAAoB;AAC7B,SAAS,SAAS;AAClB,SAAS,0BAA0B;AACnC,SAAS,8BAA8B;AACvC;AAAA,EACE;AAAA,EACA;AAAA,OACK;AACP,SAAS,6BAA6B;AACtC,SAAS,YAAY;AAErB,SAAS,iBAAiB,yBAAyB,iCAAiC;AAE7E,MAAM,WAAW;AAAA,EACtB,QAAQ,EAAE,aAAa,MAAM,iBAAiB,CAAC,4BAA4B,EAAE;AAC/E;AAEA,MAAM,cAAc,CAAC,UAAiD;AACpE,MAAI,CAAC,MAAO,QAAO;AACnB,QAAM,MAAM,MAAM,QAAQ,KAAK,IAAI,MAAM,CAAC,IAAI;AAC9C,MAAI;AACF,WAAO,mBAAmB,GAAG;AAAA,EAC/B,QAAQ;AACN,WAAO;AAAA,EACT;AACF;AAEA,eAAsB,OAAO,KAAc,KAAsD;AAC/F,QAAM,OAAO,MAAM,mBAAmB,GAAG;AACzC,MAAI,CAAC,KAAM,QAAO,aAAa,KAAK,EAAE,OAAO,eAAe,GAAG,EAAE,QAAQ,IAAI,CAAC;AAE9E,QAAM,UAAU,YAAY,IAAI,QAAQ,OAAO,EAAE,KAAK;AACtD,QAAM,SAAS,YAAY,IAAI,QAAQ,MAAM,EAAE,KAAK;AACpD,MAAI,CAAC,WAAW,CAAC,QAAQ;AACvB,WAAO,aAAa,KAAK,EAAE,OAAO,qBAAqB,GAAG,EAAE,QAAQ,IAAI,CAAC;AAAA,EAC3E;AAEA,QAAM,YAAY,MAAM,uBAAuB;AAC/C,QAAM,KAAK,UAAU,QAAQ,IAAI;AACjC,QAAM,SAAS,MAA0D;AACvE,QAAI;AACF,aAAO,UAAU,QAAQ,OAAO;AAAA,IAClC,QAAQ;AACN,aAAO;AAAA,IACT;AAAA,EACF,GAAG;AAEH,QAAM,QAAQ,KAAK,WACf,EAAE,KAAK,CAAC,EAAE,UAAU,KAAK,SAAS,GAAG,EAAE,UAAU,KAAK,CAAC,EAAE,IACzD,EAAE,UAAU,KAAK;AAErB,QAAM,OAAO,MAAM,GAAG,QAAQ,MAAM,EAAE,IAAI,QAAQ,WAAW,MAAM,GAAI,MAAc,CAAQ;AAC7F,MAAI,CAAC,KAAM,QAAO,aAAa,KAAK,EAAE,OAAO,iBAAiB,GAAG,EAAE,QAAQ,IAAI,CAAC;AAEhF,QAAM,cAAc,MAAM,0BAA0B,WAAW;AAAA,IAC7D,UAAU,KAAK,YAAY;AAAA,IAC3B,gBAAgB,KAAK,SAAS;AAAA,IAC9B,QAAQ,KAAK;AAAA,IACb,cAAc;AAAA,IACd,YAAY;AAAA,IACZ,WAAW;AAAA,IACX,eAAe,IAAI;AAAA,IACnB,gBAAgB,IAAI;AAAA,IACpB,iBAAiB,EAAE,SAAS,OAAO;AAAA,EACrC,CAAC;AACD,MAAI,eAAe,CAAC,YAAY,IAAI;AAClC,WAAO,aAAa,KAAK,YAAY,MAAM,EAAE,QAAQ,YAAY,OAAO,CAAC;AAAA,EAC3E;AAEA,QAAM,eAAe,MAAM,sBAAsB,IAAI,OAAO;AAAA,IAC1D;AAAA,IACA,UAAU,KAAK,YAAY;AAAA,IAC3B,gBAAgB,KAAK,SAAS;AAAA,IAC9B,SAAS,CAAC,MAAM;AAAA,EAClB,CAAC;AAED,MAAI,eAAe,KAAK,aAAa,MAAM,YAAY,uBAAuB;AAC5E,UAAM,iCAAiC,WAAW;AAAA,MAChD,UAAU,KAAK,YAAY;AAAA,MAC3B,gBAAgB,KAAK,SAAS;AAAA,MAC9B,QAAQ,KAAK;AAAA,MACb,cAAc;AAAA,MACd,YAAY;AAAA,MACZ,WAAW;AAAA,MACX,eAAe,IAAI;AAAA,MACnB,gBAAgB,IAAI;AAAA,MACpB,UAAU,YAAY,YAAY;AAAA,IACpC,CAAC;AAAA,EACH;AAEA,SAAO,aAAa,KAAK,EAAE,SAAS,KAAK,CAAC;AAC5C;AAEA,MAAM,oCAAoC,EAAE,OAAO;AAAA,EACjD,SAAS,EAAE,OAAO,EAAE,IAAI,CAAC;AAAA,EACzB,QAAQ,EAAE,OAAO,EAAE,KAAK;AAC1B,CAAC;AAED,MAAM,2BAA6C;AAAA,EACjD,SAAS;AAAA,EACT,aAAa;AAAA,EACb,MAAM,CAAC,eAAe;AAAA,EACtB,WAAW;AAAA,IACT,EAAE,QAAQ,KAAK,aAAa,8BAA8B,QAAQ,0BAA0B;AAAA,EAC9F;AAAA,EACA,QAAQ;AAAA,IACN,EAAE,QAAQ,KAAK,aAAa,gCAAgC,QAAQ,wBAAwB;AAAA,IAC5F,EAAE,QAAQ,KAAK,aAAa,2BAA2B,QAAQ,wBAAwB;AAAA,IACvF,EAAE,QAAQ,KAAK,aAAa,8CAA8C,QAAQ,wBAAwB;AAAA,IAC1G,EAAE,QAAQ,KAAK,aAAa,2BAA2B,QAAQ,wBAAwB;AAAA,EACzF;AACF;AAEO,MAAM,UAA2B;AAAA,EACtC,KAAK;AAAA,EACL,SAAS;AAAA,EACT,YAAY;AAAA,EACZ,SAAS;AAAA,IACP,QAAQ;AAAA,EACV;AACF;",
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 { isCrudHttpError } from '@open-mercato/shared/lib/crud/errors'\nimport {\n runCrudMutationGuardAfterSuccess,\n validateCrudMutationGuard,\n} from '@open-mercato/shared/lib/crud/mutation-guard'\nimport { clearRolePerspectives } 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 { perspectivesTag, perspectivesErrorSchema, perspectivesSuccessSchema } from '../../../openapi'\n\nexport const metadata = {\n DELETE: { requireAuth: true, requireFeatures: ['perspectives.role_defaults'] },\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\nconst rolePerspectiveDeleteBodySchema = z.object({\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}).optional()\n\nexport async function DELETE(req: Request, ctx: { params: { tableId: string; roleId: 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 const roleId = decodeParam(ctx.params?.roleId).trim()\n if (!tableId || !roleId) {\n return NextResponse.json({ error: 'Invalid parameters' }, { status: 400 })\n }\n\n let parsedBody: z.infer<typeof rolePerspectiveDeleteBodySchema> = undefined\n if (req.body && (req.headers.get('content-type') ?? '').includes('application/json')) {\n try {\n const rawBody = await req.text()\n if (rawBody.trim().length > 0) {\n const parsed = rolePerspectiveDeleteBodySchema.safeParse(JSON.parse(rawBody))\n if (!parsed.success) {\n return NextResponse.json({ error: 'Invalid payload', details: parsed.error.flatten() }, { status: 400 })\n }\n parsedBody = parsed.data\n }\n } catch {\n return NextResponse.json({ error: 'Invalid JSON' }, { status: 400 })\n }\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\n const scope = auth.tenantId\n ? { $or: [{ tenantId: auth.tenantId }, { tenantId: null }] }\n : { tenantId: null }\n\n const role = await em.findOne(Role, { id: roleId, deletedAt: null, ...(scope as any) } as any)\n if (!role) return NextResponse.json({ error: 'Role not found' }, { status: 404 })\n\n const guardResult = await validateCrudMutationGuard(container, {\n tenantId: auth.tenantId ?? '',\n organizationId: auth.orgId ?? null,\n userId: auth.sub,\n resourceKind: 'perspectives.role_perspective',\n resourceId: roleId,\n operation: 'delete',\n requestMethod: req.method,\n requestHeaders: req.headers,\n mutationPayload: { tableId, roleId },\n })\n if (guardResult && !guardResult.ok) {\n return NextResponse.json(guardResult.body, { status: guardResult.status })\n }\n\n let clearedCount = 0\n try {\n clearedCount = await clearRolePerspectives(em, cache, {\n tableId,\n tenantId: auth.tenantId ?? null,\n organizationId: auth.orgId ?? null,\n roleIds: [roleId],\n expectedUpdatedAtByRoleId: parsedBody?.roleExpectedUpdatedAtByRoleId,\n expectedUpdatedAtByPerspectiveId: parsedBody?.roleExpectedUpdatedAtByPerspectiveId,\n request: req,\n })\n } catch (err) {\n if (isCrudHttpError(err)) {\n return NextResponse.json(err.body, { status: err.status })\n }\n throw err\n }\n\n if (clearedCount > 0 && guardResult?.ok && guardResult.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: roleId,\n operation: 'delete',\n requestMethod: req.method,\n requestHeaders: req.headers,\n metadata: guardResult.metadata ?? null,\n })\n }\n\n return NextResponse.json({ success: true })\n}\n\nconst rolePerspectiveDeleteParamsSchema = z.object({\n tableId: z.string().min(1),\n roleId: z.string().uuid(),\n})\n\nconst rolePerspectiveDeleteDoc: OpenApiMethodDoc = {\n summary: 'Clear role perspectives for a table',\n description: 'Removes all role-level perspectives associated with the provided role identifier for the table.',\n tags: [perspectivesTag],\n requestBody: {\n contentType: 'application/json',\n schema: rolePerspectiveDeleteBodySchema,\n description: 'Optional per-role or per-perspective optimistic-lock tokens.',\n },\n responses: [\n { status: 200, description: 'Role perspectives cleared.', schema: perspectivesSuccessSchema },\n ],\n errors: [\n { status: 400, description: 'Invalid identifiers supplied', schema: perspectivesErrorSchema },\n { status: 401, description: 'Authentication required', schema: perspectivesErrorSchema },\n { status: 403, description: 'Missing perspectives.role_defaults feature', schema: perspectivesErrorSchema },\n { status: 409, description: 'Optimistic lock conflict', schema: perspectivesErrorSchema },\n { status: 404, description: 'Role not found in scope', schema: perspectivesErrorSchema },\n ],\n}\n\nexport const openApi: OpenApiRouteDoc = {\n tag: perspectivesTag,\n summary: 'Delete role-level perspectives',\n pathParams: rolePerspectiveDeleteParamsSchema,\n methods: {\n DELETE: rolePerspectiveDeleteDoc,\n },\n}\n"],
5
+ "mappings": "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",
6
6
  "names": []
7
7
  }
@@ -71,13 +71,19 @@ async function GET(_req, ctx) {
71
71
  ) ?? false;
72
72
  const roleScope = auth.tenantId ? { $or: [{ tenantId: auth.tenantId }, { tenantId: null }] } : { tenantId: null };
73
73
  const availableRoles = canApplyToRoles ? await em.find(Role, { ...roleScope, deletedAt: null }, { orderBy: { name: "asc" } }) : assignedRoles;
74
+ const manageableRoleIds = canApplyToRoles ? availableRoles.map((role) => role.id) : assignedRoleIds;
74
75
  const state = await loadPerspectivesState(em, cache, {
75
76
  scope: buildScope(auth),
76
77
  tableId,
77
78
  roleIds: assignedRoleIds
78
79
  });
80
+ const manageableState = manageableRoleIds.length === assignedRoleIds.length && manageableRoleIds.every((roleId) => assignedRoleIds.includes(roleId)) ? state : await loadPerspectivesState(em, cache, {
81
+ scope: buildScope(auth),
82
+ tableId,
83
+ roleIds: manageableRoleIds
84
+ });
79
85
  const rolePerspectiveByRole = /* @__PURE__ */ new Map();
80
- for (const item of state.rolePerspectives) {
86
+ for (const item of manageableState.rolePerspectives) {
81
87
  const entry = rolePerspectiveByRole.get(item.roleId) ?? { hasDefault: false, count: 0 };
82
88
  entry.count += 1;
83
89
  entry.hasDefault = entry.hasDefault || item.isDefault;
@@ -91,6 +97,10 @@ async function GET(_req, ctx) {
91
97
  ...rp,
92
98
  roleName: availableRoles.find((role) => role.id === rp.roleId)?.name ?? assignedRoles.find((role) => role.id === rp.roleId)?.name ?? null
93
99
  })),
100
+ manageableRolePerspectives: manageableState.rolePerspectives.map((rp) => ({
101
+ ...rp,
102
+ roleName: availableRoles.find((role) => role.id === rp.roleId)?.name ?? assignedRoles.find((role) => role.id === rp.roleId)?.name ?? null
103
+ })),
94
104
  roles: availableRoles.map((role) => {
95
105
  const stats = rolePerspectiveByRole.get(role.id);
96
106
  return {
@@ -217,7 +227,10 @@ async function POST(req, ctx) {
217
227
  name: parsed.data.name,
218
228
  settings: parsed.data.settings,
219
229
  setDefault: parsed.data.setRoleDefault ?? false
220
- }
230
+ },
231
+ expectedUpdatedAtByRoleId: parsed.data.roleExpectedUpdatedAtByRoleId,
232
+ expectedUpdatedAtByPerspectiveId: parsed.data.roleExpectedUpdatedAtByPerspectiveId,
233
+ request: req
221
234
  });
222
235
  }
223
236
  },
@@ -227,7 +240,10 @@ async function POST(req, ctx) {
227
240
  tableId,
228
241
  tenantId: auth.tenantId ?? null,
229
242
  organizationId: auth.orgId ?? null,
230
- roleIds: clearRoleIds
243
+ roleIds: clearRoleIds,
244
+ expectedUpdatedAtByRoleId: parsed.data.roleExpectedUpdatedAtByRoleId,
245
+ expectedUpdatedAtByPerspectiveId: parsed.data.roleExpectedUpdatedAtByPerspectiveId,
246
+ request: req
231
247
  });
232
248
  }
233
249
  }
@@ -301,7 +317,8 @@ const perspectivesPostDoc = {
301
317
  errors: [
302
318
  { status: 400, description: "Validation failed or invalid roles provided", schema: perspectivesErrorSchema },
303
319
  { status: 401, description: "Authentication required", schema: perspectivesErrorSchema },
304
- { status: 403, description: "Missing perspectives.role_defaults feature for role updates", schema: perspectivesErrorSchema }
320
+ { status: 403, description: "Missing perspectives.role_defaults feature for role updates", schema: perspectivesErrorSchema },
321
+ { status: 409, description: "Optimistic lock conflict", schema: perspectivesErrorSchema }
305
322
  ]
306
323
  };
307
324
  const openApi = {
@@ -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 } 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\n const state = await loadPerspectivesState(em, cache, {\n scope: buildScope(auth),\n tableId,\n roleIds: assignedRoleIds,\n })\n\n const rolePerspectiveByRole = new Map<string, { hasDefault: boolean; count: number }>()\n for (const item of state.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 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 })\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 })\n }\n },\n ], { transaction: true })\n } catch (err) {\n if (isCrudHttpError(err)) {\n return NextResponse.json(err.body, { status: err.status })\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 ],\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 } 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 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', 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
  }
@@ -24,6 +24,7 @@ const perspectivesIndexResponseSchema = z.object({
24
24
  perspectives: z.array(perspectiveDtoSchema),
25
25
  defaultPerspectiveId: z.string().uuid().nullable(),
26
26
  rolePerspectives: z.array(rolePerspectiveDtoSchema),
27
+ manageableRolePerspectives: z.array(rolePerspectiveDtoSchema),
27
28
  roles: z.array(
28
29
  z.object({
29
30
  id: z.string().uuid(),
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "version": 3,
3
3
  "sources": ["../../../../src/modules/perspectives/api/openapi.ts"],
4
- "sourcesContent": ["import { z } from 'zod'\nimport { perspectiveSettingsSchema } from '../data/validators'\n\nexport const perspectivesTag = 'Perspectives'\n\nexport const perspectivesErrorSchema = z.object({\n error: z.string(),\n}).passthrough()\n\nexport const perspectiveDtoSchema = z.object({\n id: z.string().uuid(),\n name: z.string(),\n tableId: z.string(),\n settings: perspectiveSettingsSchema,\n isDefault: z.boolean(),\n createdAt: z.string(),\n updatedAt: z.string().nullable(),\n})\n\nexport const rolePerspectiveDtoSchema = perspectiveDtoSchema.extend({\n roleId: z.string().uuid(),\n tenantId: z.string().uuid().nullable(),\n organizationId: z.string().uuid().nullable(),\n roleName: z.string().nullable(),\n})\n\nexport const perspectivesIndexResponseSchema = z.object({\n tableId: z.string(),\n perspectives: z.array(perspectiveDtoSchema),\n defaultPerspectiveId: z.string().uuid().nullable(),\n rolePerspectives: z.array(rolePerspectiveDtoSchema),\n roles: z.array(\n z.object({\n id: z.string().uuid(),\n name: z.string(),\n hasPerspective: z.boolean(),\n hasDefault: z.boolean(),\n }),\n ),\n canApplyToRoles: z.boolean(),\n})\n\nexport const perspectiveSaveResponseSchema = z.object({\n perspective: perspectiveDtoSchema,\n rolePerspectives: z.array(rolePerspectiveDtoSchema),\n clearedRoleIds: z.array(z.string().uuid()),\n})\n\nexport const perspectivesSuccessSchema = z.object({\n success: z.literal(true),\n})\n\n"],
5
- "mappings": "AAAA,SAAS,SAAS;AAClB,SAAS,iCAAiC;AAEnC,MAAM,kBAAkB;AAExB,MAAM,0BAA0B,EAAE,OAAO;AAAA,EAC9C,OAAO,EAAE,OAAO;AAClB,CAAC,EAAE,YAAY;AAER,MAAM,uBAAuB,EAAE,OAAO;AAAA,EAC3C,IAAI,EAAE,OAAO,EAAE,KAAK;AAAA,EACpB,MAAM,EAAE,OAAO;AAAA,EACf,SAAS,EAAE,OAAO;AAAA,EAClB,UAAU;AAAA,EACV,WAAW,EAAE,QAAQ;AAAA,EACrB,WAAW,EAAE,OAAO;AAAA,EACpB,WAAW,EAAE,OAAO,EAAE,SAAS;AACjC,CAAC;AAEM,MAAM,2BAA2B,qBAAqB,OAAO;AAAA,EAClE,QAAQ,EAAE,OAAO,EAAE,KAAK;AAAA,EACxB,UAAU,EAAE,OAAO,EAAE,KAAK,EAAE,SAAS;AAAA,EACrC,gBAAgB,EAAE,OAAO,EAAE,KAAK,EAAE,SAAS;AAAA,EAC3C,UAAU,EAAE,OAAO,EAAE,SAAS;AAChC,CAAC;AAEM,MAAM,kCAAkC,EAAE,OAAO;AAAA,EACtD,SAAS,EAAE,OAAO;AAAA,EAClB,cAAc,EAAE,MAAM,oBAAoB;AAAA,EAC1C,sBAAsB,EAAE,OAAO,EAAE,KAAK,EAAE,SAAS;AAAA,EACjD,kBAAkB,EAAE,MAAM,wBAAwB;AAAA,EAClD,OAAO,EAAE;AAAA,IACP,EAAE,OAAO;AAAA,MACP,IAAI,EAAE,OAAO,EAAE,KAAK;AAAA,MACpB,MAAM,EAAE,OAAO;AAAA,MACf,gBAAgB,EAAE,QAAQ;AAAA,MAC1B,YAAY,EAAE,QAAQ;AAAA,IACxB,CAAC;AAAA,EACH;AAAA,EACA,iBAAiB,EAAE,QAAQ;AAC7B,CAAC;AAEM,MAAM,gCAAgC,EAAE,OAAO;AAAA,EACpD,aAAa;AAAA,EACb,kBAAkB,EAAE,MAAM,wBAAwB;AAAA,EAClD,gBAAgB,EAAE,MAAM,EAAE,OAAO,EAAE,KAAK,CAAC;AAC3C,CAAC;AAEM,MAAM,4BAA4B,EAAE,OAAO;AAAA,EAChD,SAAS,EAAE,QAAQ,IAAI;AACzB,CAAC;",
4
+ "sourcesContent": ["import { z } from 'zod'\nimport { perspectiveSettingsSchema } from '../data/validators'\n\nexport const perspectivesTag = 'Perspectives'\n\nexport const perspectivesErrorSchema = z.object({\n error: z.string(),\n}).passthrough()\n\nexport const perspectiveDtoSchema = z.object({\n id: z.string().uuid(),\n name: z.string(),\n tableId: z.string(),\n settings: perspectiveSettingsSchema,\n isDefault: z.boolean(),\n createdAt: z.string(),\n updatedAt: z.string().nullable(),\n})\n\nexport const rolePerspectiveDtoSchema = perspectiveDtoSchema.extend({\n roleId: z.string().uuid(),\n tenantId: z.string().uuid().nullable(),\n organizationId: z.string().uuid().nullable(),\n roleName: z.string().nullable(),\n})\n\nexport const perspectivesIndexResponseSchema = z.object({\n tableId: z.string(),\n perspectives: z.array(perspectiveDtoSchema),\n defaultPerspectiveId: z.string().uuid().nullable(),\n rolePerspectives: z.array(rolePerspectiveDtoSchema),\n manageableRolePerspectives: z.array(rolePerspectiveDtoSchema),\n roles: z.array(\n z.object({\n id: z.string().uuid(),\n name: z.string(),\n hasPerspective: z.boolean(),\n hasDefault: z.boolean(),\n }),\n ),\n canApplyToRoles: z.boolean(),\n})\n\nexport const perspectiveSaveResponseSchema = z.object({\n perspective: perspectiveDtoSchema,\n rolePerspectives: z.array(rolePerspectiveDtoSchema),\n clearedRoleIds: z.array(z.string().uuid()),\n})\n\nexport const perspectivesSuccessSchema = z.object({\n success: z.literal(true),\n})\n"],
5
+ "mappings": "AAAA,SAAS,SAAS;AAClB,SAAS,iCAAiC;AAEnC,MAAM,kBAAkB;AAExB,MAAM,0BAA0B,EAAE,OAAO;AAAA,EAC9C,OAAO,EAAE,OAAO;AAClB,CAAC,EAAE,YAAY;AAER,MAAM,uBAAuB,EAAE,OAAO;AAAA,EAC3C,IAAI,EAAE,OAAO,EAAE,KAAK;AAAA,EACpB,MAAM,EAAE,OAAO;AAAA,EACf,SAAS,EAAE,OAAO;AAAA,EAClB,UAAU;AAAA,EACV,WAAW,EAAE,QAAQ;AAAA,EACrB,WAAW,EAAE,OAAO;AAAA,EACpB,WAAW,EAAE,OAAO,EAAE,SAAS;AACjC,CAAC;AAEM,MAAM,2BAA2B,qBAAqB,OAAO;AAAA,EAClE,QAAQ,EAAE,OAAO,EAAE,KAAK;AAAA,EACxB,UAAU,EAAE,OAAO,EAAE,KAAK,EAAE,SAAS;AAAA,EACrC,gBAAgB,EAAE,OAAO,EAAE,KAAK,EAAE,SAAS;AAAA,EAC3C,UAAU,EAAE,OAAO,EAAE,SAAS;AAChC,CAAC;AAEM,MAAM,kCAAkC,EAAE,OAAO;AAAA,EACtD,SAAS,EAAE,OAAO;AAAA,EAClB,cAAc,EAAE,MAAM,oBAAoB;AAAA,EAC1C,sBAAsB,EAAE,OAAO,EAAE,KAAK,EAAE,SAAS;AAAA,EACjD,kBAAkB,EAAE,MAAM,wBAAwB;AAAA,EAClD,4BAA4B,EAAE,MAAM,wBAAwB;AAAA,EAC5D,OAAO,EAAE;AAAA,IACP,EAAE,OAAO;AAAA,MACP,IAAI,EAAE,OAAO,EAAE,KAAK;AAAA,MACpB,MAAM,EAAE,OAAO;AAAA,MACf,gBAAgB,EAAE,QAAQ;AAAA,MAC1B,YAAY,EAAE,QAAQ;AAAA,IACxB,CAAC;AAAA,EACH;AAAA,EACA,iBAAiB,EAAE,QAAQ;AAC7B,CAAC;AAEM,MAAM,gCAAgC,EAAE,OAAO;AAAA,EACpD,aAAa;AAAA,EACb,kBAAkB,EAAE,MAAM,wBAAwB;AAAA,EAClD,gBAAgB,EAAE,MAAM,EAAE,OAAO,EAAE,KAAK,CAAC;AAC3C,CAAC;AAEM,MAAM,4BAA4B,EAAE,OAAO;AAAA,EAChD,SAAS,EAAE,QAAQ,IAAI;AACzB,CAAC;",
6
6
  "names": []
7
7
  }
@@ -14,6 +14,8 @@ const perspectiveSaveSchema = z.object({
14
14
  isDefault: z.boolean().optional(),
15
15
  applyToRoles: z.array(z.string().uuid()).optional(),
16
16
  clearRoleIds: z.array(z.string().uuid()).optional(),
17
+ roleExpectedUpdatedAtByRoleId: z.record(z.string().uuid(), z.string().min(1).nullable()).optional(),
18
+ roleExpectedUpdatedAtByPerspectiveId: z.record(z.string().uuid(), z.string().min(1).nullable()).optional(),
17
19
  setRoleDefault: z.boolean().optional()
18
20
  });
19
21
  const perspectiveDeleteSchema = z.object({
@@ -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 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 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,EAC7D,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,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 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,EAC7D,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;",
6
6
  "names": []
7
7
  }
@@ -1,5 +1,36 @@
1
- import { enforceCommandOptimisticLock } from "@open-mercato/shared/lib/crud/optimistic-lock-command";
1
+ import { CrudHttpError } from "@open-mercato/shared/lib/crud/errors";
2
+ import {
3
+ buildOptimisticLockConflictBody,
4
+ enforceCommandOptimisticLock,
5
+ enforceRecordGoneIsConflict
6
+ } from "@open-mercato/shared/lib/crud/optimistic-lock-command";
2
7
  import { Perspective, RolePerspective } from "../data/entities.js";
8
+ const PERSPECTIVE_RESOURCE_KIND = "perspectives.perspective";
9
+ const ROLE_PERSPECTIVE_RESOURCE_KIND = "perspectives.role_perspective";
10
+ function resolveRoleLockInput(expectedUpdatedAtByRoleId, roleId, request) {
11
+ if (expectedUpdatedAtByRoleId && Object.prototype.hasOwnProperty.call(expectedUpdatedAtByRoleId, roleId)) {
12
+ return { expected: expectedUpdatedAtByRoleId[roleId] ?? null, request: null };
13
+ }
14
+ return { expected: void 0, request: request ?? null };
15
+ }
16
+ function resolveRoleRecordLockInput(expectedUpdatedAtByPerspectiveId, expectedUpdatedAtByRoleId, record, request) {
17
+ if (expectedUpdatedAtByPerspectiveId && Object.prototype.hasOwnProperty.call(expectedUpdatedAtByPerspectiveId, record.id)) {
18
+ return { expected: expectedUpdatedAtByPerspectiveId[record.id] ?? null, request: null };
19
+ }
20
+ return resolveRoleLockInput(expectedUpdatedAtByRoleId, record.roleId, request);
21
+ }
22
+ function firstExpectedUpdatedAt(expectedUpdatedAtById) {
23
+ if (!expectedUpdatedAtById) return null;
24
+ for (const value of Object.values(expectedUpdatedAtById)) {
25
+ if (value instanceof Date) return value;
26
+ if (typeof value === "string" && value.trim().length > 0) return value;
27
+ }
28
+ return null;
29
+ }
30
+ function throwMissingRoleRecordVersionConflict(record) {
31
+ const current = record.updatedAt instanceof Date && Number.isFinite(record.updatedAt.getTime()) ? record.updatedAt.toISOString() : (/* @__PURE__ */ new Date(0)).toISOString();
32
+ throw new CrudHttpError(409, buildOptimisticLockConflictBody(current, current));
33
+ }
3
34
  const CACHE_TTL_MS = 5 * 60 * 1e3;
4
35
  const nullish = (value) => value == null ? null : value;
5
36
  const scopeKey = (scope) => `${scope.userId}:${scope.tenantId ?? "null"}:${scope.organizationId ?? "null"}`;
@@ -133,7 +164,7 @@ async function saveUserPerspective(em, cache, options) {
133
164
  throw Object.assign(new Error("Perspective not found"), { code: "NOT_FOUND" });
134
165
  }
135
166
  enforceCommandOptimisticLock({
136
- resourceKind: "perspectives.perspective",
167
+ resourceKind: PERSPECTIVE_RESOURCE_KIND,
137
168
  resourceId: entity.id,
138
169
  current: entity.updatedAt ?? null,
139
170
  request: options.request ?? null
@@ -202,7 +233,20 @@ async function deleteUserPerspective(em, cache, options) {
202
233
  tableId,
203
234
  deletedAt: null
204
235
  });
205
- if (!existing) return false;
236
+ if (!existing) {
237
+ enforceRecordGoneIsConflict({
238
+ resourceKind: PERSPECTIVE_RESOURCE_KIND,
239
+ resourceId: perspectiveId,
240
+ request: options.request ?? null
241
+ });
242
+ return false;
243
+ }
244
+ enforceCommandOptimisticLock({
245
+ resourceKind: PERSPECTIVE_RESOURCE_KIND,
246
+ resourceId: existing.id,
247
+ current: existing.updatedAt ?? null,
248
+ request: options.request ?? null
249
+ });
206
250
  existing.deletedAt = /* @__PURE__ */ new Date();
207
251
  existing.isDefault = false;
208
252
  await em.flush();
@@ -217,7 +261,7 @@ async function saveRolePerspectives(em, cache, options) {
217
261
  const organizationId = options.organizationId ?? null;
218
262
  const now = /* @__PURE__ */ new Date();
219
263
  const touchedRoleIds = /* @__PURE__ */ new Set();
220
- const results = [];
264
+ const resultRecords = [];
221
265
  const recordByRole = /* @__PURE__ */ new Map();
222
266
  if (input.roleIds.length) {
223
267
  const existingRecords = await em.find(RolePerspective, {
@@ -230,6 +274,22 @@ async function saveRolePerspectives(em, cache, options) {
230
274
  });
231
275
  for (const existing of existingRecords) recordByRole.set(existing.roleId, existing);
232
276
  }
277
+ const defaultRecordsByRole = /* @__PURE__ */ new Map();
278
+ if (input.setDefault === true && input.roleIds.length) {
279
+ const existingDefaultRecords = await em.find(RolePerspective, {
280
+ roleId: { $in: input.roleIds },
281
+ tableId,
282
+ tenantId,
283
+ organizationId,
284
+ isDefault: true,
285
+ deletedAt: null
286
+ });
287
+ for (const existing of existingDefaultRecords) {
288
+ const records = defaultRecordsByRole.get(existing.roleId) ?? [];
289
+ records.push(existing);
290
+ defaultRecordsByRole.set(existing.roleId, records);
291
+ }
292
+ }
233
293
  for (const roleId of input.roleIds) {
234
294
  let record = recordByRole.get(roleId) ?? null;
235
295
  if (!record) {
@@ -247,12 +307,37 @@ async function saveRolePerspectives(em, cache, options) {
247
307
  em.persist(record);
248
308
  recordByRole.set(roleId, record);
249
309
  } else {
310
+ enforceCommandOptimisticLock({
311
+ resourceKind: ROLE_PERSPECTIVE_RESOURCE_KIND,
312
+ resourceId: record.id,
313
+ current: record.updatedAt ?? null,
314
+ ...resolveRoleRecordLockInput(
315
+ options.expectedUpdatedAtByPerspectiveId,
316
+ options.expectedUpdatedAtByRoleId,
317
+ record,
318
+ options.request ?? null
319
+ )
320
+ });
250
321
  record.settingsJson = input.settings;
251
322
  record.updatedAt = now;
252
323
  if (input.setDefault === true) record.isDefault = true;
253
324
  if (input.setDefault === false) record.isDefault = false;
254
325
  }
255
326
  if (input.setDefault === true) {
327
+ for (const defaultRecord of defaultRecordsByRole.get(roleId) ?? []) {
328
+ if (defaultRecord.id === record.id) continue;
329
+ enforceCommandOptimisticLock({
330
+ resourceKind: ROLE_PERSPECTIVE_RESOURCE_KIND,
331
+ resourceId: defaultRecord.id,
332
+ current: defaultRecord.updatedAt ?? null,
333
+ ...resolveRoleRecordLockInput(
334
+ options.expectedUpdatedAtByPerspectiveId,
335
+ options.expectedUpdatedAtByRoleId,
336
+ defaultRecord,
337
+ options.request ?? null
338
+ )
339
+ });
340
+ }
256
341
  await em.nativeUpdate(
257
342
  RolePerspective,
258
343
  {
@@ -261,6 +346,7 @@ async function saveRolePerspectives(em, cache, options) {
261
346
  tenantId,
262
347
  organizationId,
263
348
  id: { $ne: record.id },
349
+ isDefault: true,
264
350
  deletedAt: null
265
351
  },
266
352
  { isDefault: false, updatedAt: now }
@@ -268,7 +354,7 @@ async function saveRolePerspectives(em, cache, options) {
268
354
  record.isDefault = true;
269
355
  }
270
356
  touchedRoleIds.add(roleId);
271
- results.push(toResolvedRolePerspective(record));
357
+ resultRecords.push(record);
272
358
  }
273
359
  if (input.roleIds.length) {
274
360
  await em.flush();
@@ -277,13 +363,56 @@ async function saveRolePerspectives(em, cache, options) {
277
363
  const tags = Array.from(touchedRoleIds).map((roleId) => roleTag(roleId, tableId, tenantId));
278
364
  await cache.deleteByTags(tags);
279
365
  }
280
- return results;
366
+ return resultRecords.map(toResolvedRolePerspective);
281
367
  }
282
368
  async function clearRolePerspectives(em, cache, options) {
283
369
  const { tableId, roleIds } = options;
284
370
  const tenantId = options.tenantId ?? null;
285
371
  const organizationId = options.organizationId ?? null;
286
372
  if (!roleIds.length) return 0;
373
+ const existingRecords = await em.find(RolePerspective, {
374
+ roleId: { $in: roleIds },
375
+ tableId,
376
+ tenantId,
377
+ organizationId,
378
+ deletedAt: null
379
+ });
380
+ const recordsByRole = /* @__PURE__ */ new Map();
381
+ for (const record of existingRecords) {
382
+ const records = recordsByRole.get(record.roleId) ?? [];
383
+ records.push(record);
384
+ recordsByRole.set(record.roleId, records);
385
+ }
386
+ for (const roleId of roleIds) {
387
+ const records = recordsByRole.get(roleId) ?? [];
388
+ const lockInput = resolveRoleLockInput(options.expectedUpdatedAtByRoleId, roleId, options.request ?? null);
389
+ if (!records.length) {
390
+ const expected = firstExpectedUpdatedAt(options.expectedUpdatedAtByPerspectiveId);
391
+ enforceRecordGoneIsConflict({
392
+ resourceKind: ROLE_PERSPECTIVE_RESOURCE_KIND,
393
+ resourceId: roleId,
394
+ expected: expected ?? lockInput.expected,
395
+ request: expected ? null : lockInput.request
396
+ });
397
+ continue;
398
+ }
399
+ for (const record of records) {
400
+ if (options.expectedUpdatedAtByPerspectiveId && !Object.prototype.hasOwnProperty.call(options.expectedUpdatedAtByPerspectiveId, record.id)) {
401
+ throwMissingRoleRecordVersionConflict(record);
402
+ }
403
+ enforceCommandOptimisticLock({
404
+ resourceKind: ROLE_PERSPECTIVE_RESOURCE_KIND,
405
+ resourceId: record.id,
406
+ current: record.updatedAt ?? null,
407
+ ...resolveRoleRecordLockInput(
408
+ options.expectedUpdatedAtByPerspectiveId,
409
+ options.expectedUpdatedAtByRoleId,
410
+ record,
411
+ options.request ?? null
412
+ )
413
+ });
414
+ }
415
+ }
287
416
  const affected = await em.nativeUpdate(
288
417
  RolePerspective,
289
418
  {