@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.
- package/.turbo/turbo-build.log +1 -1
- package/dist/modules/messages/api/[id]/conversation/archive/route.js +25 -7
- package/dist/modules/messages/api/[id]/conversation/archive/route.js.map +2 -2
- package/dist/modules/messages/api/[id]/conversation/read/route.js +24 -6
- package/dist/modules/messages/api/[id]/conversation/read/route.js.map +2 -2
- package/dist/modules/messages/api/[id]/route.js +13 -1
- package/dist/modules/messages/api/[id]/route.js.map +2 -2
- package/dist/modules/messages/api/openapi.js +3 -1
- package/dist/modules/messages/api/openapi.js.map +2 -2
- package/dist/modules/messages/commands/conversation.js +77 -0
- package/dist/modules/messages/commands/conversation.js.map +2 -2
- package/dist/modules/messages/components/MessageDetailPageClient.js +16 -4
- package/dist/modules/messages/components/MessageDetailPageClient.js.map +2 -2
- package/dist/modules/messages/components/message-detail/hooks/useMessageDetailsActions.js +18 -0
- package/dist/modules/messages/components/message-detail/hooks/useMessageDetailsActions.js.map +2 -2
- package/dist/modules/messages/components/message-detail/panels/MainMessageHeader.js +13 -11
- package/dist/modules/messages/components/message-detail/panels/MainMessageHeader.js.map +2 -2
- package/dist/modules/perspectives/api/[tableId]/[perspectiveId]/route.js +20 -9
- package/dist/modules/perspectives/api/[tableId]/[perspectiveId]/route.js.map +2 -2
- package/dist/modules/perspectives/api/[tableId]/roles/[roleId]/route.js +43 -6
- package/dist/modules/perspectives/api/[tableId]/roles/[roleId]/route.js.map +2 -2
- package/dist/modules/perspectives/api/[tableId]/route.js +21 -4
- package/dist/modules/perspectives/api/[tableId]/route.js.map +2 -2
- package/dist/modules/perspectives/api/openapi.js +1 -0
- package/dist/modules/perspectives/api/openapi.js.map +2 -2
- package/dist/modules/perspectives/data/validators.js +2 -0
- package/dist/modules/perspectives/data/validators.js.map +2 -2
- package/dist/modules/perspectives/services/perspectiveService.js +135 -6
- package/dist/modules/perspectives/services/perspectiveService.js.map +2 -2
- package/package.json +7 -7
- package/src/modules/messages/api/[id]/conversation/archive/route.ts +29 -6
- package/src/modules/messages/api/[id]/conversation/read/route.ts +29 -6
- package/src/modules/messages/api/[id]/route.ts +15 -0
- package/src/modules/messages/api/openapi.ts +2 -0
- package/src/modules/messages/commands/conversation.ts +87 -0
- package/src/modules/messages/components/MessageDetailPageClient.tsx +16 -4
- package/src/modules/messages/components/message-detail/hooks/useMessageDetailsActions.ts +26 -1
- package/src/modules/messages/components/message-detail/panels/MainMessageHeader.tsx +29 -14
- package/src/modules/messages/components/message-detail/types.ts +2 -0
- package/src/modules/messages/i18n/de.json +4 -0
- package/src/modules/messages/i18n/en.json +4 -0
- package/src/modules/messages/i18n/es.json +4 -0
- package/src/modules/messages/i18n/pl.json +4 -0
- package/src/modules/perspectives/api/[tableId]/[perspectiveId]/route.ts +20 -9
- package/src/modules/perspectives/api/[tableId]/roles/[roleId]/route.ts +45 -6
- package/src/modules/perspectives/api/[tableId]/route.ts +23 -1
- package/src/modules/perspectives/api/openapi.ts +1 -1
- package/src/modules/perspectives/data/validators.ts +2 -0
- package/src/modules/perspectives/services/perspectiveService.ts +171 -7
|
@@ -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 { enforceCommandOptimisticLock } 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'\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\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 if (typeof console !== 'undefined') {\n console.warn('[perspectives] Dropping legacy filterValues shape; please re-create the perspective with the new filter UI.')\n }\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: 'perspectives.perspective',\n resourceId: entity.id,\n current: entity.updatedAt ?? null,\n request: options.request ?? null,\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: { scope: PerspectiveScope; tableId: string; perspectiveId: string },\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) return false\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 },\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 results: ResolvedRolePerspective[] = []\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\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 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 await em.nativeUpdate(\n RolePerspective,\n {\n roleId,\n tableId,\n tenantId,\n organizationId,\n id: { $ne: record.id } as any,\n deletedAt: null,\n },\n { isDefault: false, updatedAt: now },\n )\n record.isDefault = true\n }\n\n touchedRoleIds.add(roleId)\n results.push(toResolvedRolePerspective(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 results\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 },\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 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'\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 if (typeof console !== 'undefined') {\n console.warn('[perspectives] Dropping legacy filterValues shape; please re-create the perspective with the new filter UI.')\n }\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 } 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": "AAEA,SAAS,qBAAqB;AAC9B;AAAA,EACE;AAAA,EACA;AAAA,EACA;AAAA,OACK;AACP,SAAS,aAAa,uBAAuB;AA6C7C,MAAM,4BAA4B;AAClC,MAAM,iCAAiC;AAEvC,SAAS,qBACP,2BACA,QACA,SACkF;AAClF,MAAI,6BAA6B,OAAO,UAAU,eAAe,KAAK,2BAA2B,MAAM,GAAG;AACxG,WAAO,EAAE,UAAU,0BAA0B,MAAM,KAAK,MAAM,SAAS,KAAK;AAAA,EAC9E;AACA,SAAO,EAAE,UAAU,QAAW,SAAS,WAAW,KAAK;AACzD;AAEA,SAAS,2BACP,kCACA,2BACA,QACA,SACkF;AAClF,MAAI,oCAAoC,OAAO,UAAU,eAAe,KAAK,kCAAkC,OAAO,EAAE,GAAG;AACzH,WAAO,EAAE,UAAU,iCAAiC,OAAO,EAAE,KAAK,MAAM,SAAS,KAAK;AAAA,EACxF;AACA,SAAO,qBAAqB,2BAA2B,OAAO,QAAQ,OAAO;AAC/E;AAEA,SAAS,uBAAuB,uBAAuF;AACrH,MAAI,CAAC,sBAAuB,QAAO;AACnC,aAAW,SAAS,OAAO,OAAO,qBAAqB,GAAG;AACxD,QAAI,iBAAiB,KAAM,QAAO;AAClC,QAAI,OAAO,UAAU,YAAY,MAAM,KAAK,EAAE,SAAS,EAAG,QAAO;AAAA,EACnE;AACA,SAAO;AACT;AAEA,SAAS,sCAAsC,QAAgC;AAC7E,QAAM,UAAU,OAAO,qBAAqB,QAAQ,OAAO,SAAS,OAAO,UAAU,QAAQ,CAAC,IAC1F,OAAO,UAAU,YAAY,KAC7B,oBAAI,KAAK,CAAC,GAAE,YAAY;AAC5B,QAAM,IAAI,cAAc,KAAK,gCAAgC,SAAS,OAAO,CAAC;AAChF;AAEA,MAAM,eAAe,IAAI,KAAK;AAE9B,MAAM,UAAU,CAAsC,UACpD,SAAS,OAAO,OAAO;AAEzB,MAAM,WAAW,CAAC,UAChB,GAAG,MAAM,MAAM,IAAI,MAAM,YAAY,MAAM,IAAI,MAAM,kBAAkB,MAAM;AAE/E,MAAM,eAAe,CAAC,OAAyB,SAAiB,YAC9D,2BAA2B,SAAS,KAAK,CAAC,IAAI,OAAO,IAAI,QAAQ,KAAK,CAAC,GAAG,MAAM,EAAE,cAAc,CAAC,CAAC,EAAE,KAAK,GAAG,CAAC;AAE/G,MAAM,UAAU,CAAC,OAAyB,YACxC,UACI,qBAAqB,SAAS,KAAK,CAAC,IAAI,OAAO,KAC/C,qBAAqB,SAAS,KAAK,CAAC;AAE1C,MAAM,UAAU,CAAC,QAAgB,SAAkB,aAA6B;AAC9E,QAAM,SAAS,YAAY;AAC3B,SAAO,UAAU,qBAAqB,MAAM,IAAI,MAAM,IAAI,OAAO,KAAK,qBAAqB,MAAM,IAAI,MAAM;AAC7G;AAEA,SAAS,sBAAsB,OAA8C;AAC3E,MAAI,OAAO,UAAU,YAAY,UAAU,KAAM,QAAO;AACxD,QAAM,SAAS;AACf,SAAO,OAAO,OAAO,OAAO,YACvB,OAAO,OAAO,SAAS,YACvB,OAAO,OAAO,YAAY,YAC1B,OAAO,OAAO,cAAc,aAC5B,OAAO,OAAO,cAAc;AACnC;AAEA,SAAS,0BAA0B,OAAkD;AACnF,MAAI,OAAO,UAAU,YAAY,UAAU,KAAM,QAAO;AACxD,QAAM,SAAS;AACf,SAAO,OAAO,OAAO,OAAO,YACvB,OAAO,OAAO,WAAW,YACzB,OAAO,OAAO,YAAY,YAC1B,OAAO,OAAO,SAAS,YACvB,OAAO,OAAO,cAAc,aAC5B,OAAO,OAAO,cAAc;AACnC;AAEA,SAAS,oBAAoB,OAA4C;AACvE,MAAI,OAAO,UAAU,YAAY,UAAU,KAAM,QAAO;AACxD,QAAM,SAAS;AACf,MAAI,OAAO,OAAO,YAAY,SAAU,QAAO;AAC/C,MAAI,CAAC,MAAM,QAAQ,OAAO,QAAQ,KAAK,OAAO,SAAS,KAAK,CAAC,SAAS,CAAC,sBAAsB,IAAI,CAAC,EAAG,QAAO;AAC5G,MAAI,OAAO,sBAAsB,QAAQ,OAAO,OAAO,sBAAsB,SAAU,QAAO;AAC9F,MAAI,CAAC,MAAM,QAAQ,OAAO,gBAAgB,KAAK,OAAO,iBAAiB,KAAK,CAAC,SAAS,CAAC,0BAA0B,IAAI,CAAC,EAAG,QAAO;AAChI,SAAO;AACT;AAgBO,SAAS,+BAA+B,UAAoD;AACjG,QAAM,UAAU,SAAS;AACzB,MAAI,CAAC,WAAW,OAAO,YAAY,SAAU,QAAO;AACpD,QAAM,SAAS;AACf,MAAI,OAAO,UAAU,OAAO,MAAM,EAAG,QAAO;AAC5C,MAAI,UAAU,OAAQ,QAAO;AAC7B,MAAI,OAAO,YAAY,aAAa;AAClC,YAAQ,KAAK,6GAA6G;AAAA,EAC5H;AACA,SAAO,EAAE,GAAG,UAAU,SAAS,OAAU;AAC3C;AAEA,SAAS,sBAAsB,QAA0C;AACvE,QAAM,WAAW,+BAAgC,OAAO,gBAAgB,CAAC,CAAyB;AAClG,SAAO;AAAA,IACL,IAAI,OAAO;AAAA,IACX,MAAM,OAAO;AAAA,IACb,SAAS,OAAO;AAAA,IAChB,WAAW,CAAC,CAAC,OAAO;AAAA,IACpB;AAAA,IACA,WAAW,OAAO,UAAU,YAAY;AAAA,IACxC,WAAW,OAAO,YAAY,OAAO,UAAU,YAAY,IAAI;AAAA,EACjE;AACF;AAEA,SAAS,0BAA0B,QAAkD;AACnF,QAAM,WAAW,+BAAgC,OAAO,gBAAgB,CAAC,CAAyB;AAClG,SAAO;AAAA,IACL,IAAI,OAAO;AAAA,IACX,QAAQ,OAAO;AAAA,IACf,SAAS,OAAO;AAAA,IAChB,MAAM,OAAO;AAAA,IACb,WAAW,CAAC,CAAC,OAAO;AAAA,IACpB;AAAA,IACA,UAAU,QAAQ,OAAO,QAAQ;AAAA,IACjC,gBAAgB,QAAQ,OAAO,cAAc;AAAA,IAC7C,WAAW,OAAO,UAAU,YAAY;AAAA,IACxC,WAAW,OAAO,YAAY,OAAO,UAAU,YAAY,IAAI;AAAA,EACjE;AACF;AAEA,eAAsB,sBACpB,IACA,OACA,SAC4B;AAC5B,QAAM,EAAE,OAAO,QAAQ,IAAI;AAC3B,QAAM,UAAU,MAAM,QAAQ,QAAQ,OAAO,IAAI,QAAQ,QAAQ,OAAO,CAAC,OAAO,MAAM,GAAG,SAAS,CAAC,IAAI,CAAC;AACxG,QAAM,cAAc,MAAM,KAAK,IAAI,IAAI,OAAO,CAAC;AAC/C,QAAM,WAAW,SAAS,YAAY,UAAU,KAAK,aAAa,OAAO,SAAS,WAAW,IAAI;AAEjG,MAAI,SAAS,UAAU;AACrB,UAAM,SAAS,MAAM,MAAM,IAAI,QAAQ;AACvC,QAAI,UAAU,oBAAoB,MAAM,EAAG,QAAO;AAAA,EACpD;AAEA,QAAM,WAAW,MAAM,YAAY;AACnC,QAAM,iBAAiB,MAAM,kBAAkB;AAE/C,QAAM,CAAC,UAAU,WAAW,IAAI,MAAM,QAAQ,IAAI;AAAA,IAChD,GAAG,KAAK,aAAa;AAAA,MACnB,QAAQ,MAAM;AAAA,MACd;AAAA,MACA;AAAA,MACA;AAAA,MACA,WAAW;AAAA,IACb,GAAG,EAAE,SAAS,EAAE,WAAW,OAAO,EAAE,CAAC;AAAA,IACrC,YAAY,SACR,GAAG,KAAK,iBAAiB;AAAA,MACvB,QAAQ,EAAE,KAAK,YAAmB;AAAA,MAClC;AAAA,MACA,WAAW;AAAA,MACX,MAAM;AAAA,QACJ,EAAE,KAAK,CAAC,EAAE,SAAS,GAAG,EAAE,UAAU,KAAK,CAAC,EAAE;AAAA,QAC1C,EAAE,KAAK,CAAC,EAAE,eAAe,GAAG,EAAE,gBAAgB,KAAK,CAAC,EAAE;AAAA,MACxD;AAAA,IACF,GAAU,EAAE,SAAS,EAAE,WAAW,OAAO,EAAE,CAAC,IAC5C,CAAC;AAAA,EACP,CAAC;AAED,QAAM,mBAAmB,SAAS,IAAI,qBAAqB;AAC3D,QAAM,oBAAoB,iBAAiB,KAAK,CAAC,MAAM,EAAE,SAAS,GAAG,MAAM;AAC3E,QAAM,eAAe,YAAY,IAAI,yBAAyB;AAE9D,QAAM,QAA2B;AAAA,IAC/B;AAAA,IACA,UAAU;AAAA,IACV;AAAA,IACA,kBAAkB;AAAA,EACpB;AAEA,MAAI,SAAS,UAAU;AACrB,UAAM,MAAM,IAAI,UAAU,OAAO;AAAA,MAC/B,KAAK;AAAA,MACL,MAAM;AAAA,QACJ,QAAQ,OAAO,OAAO;AAAA,QACtB,GAAG,YAAY,IAAI,CAAC,WAAW,QAAQ,QAAQ,SAAS,QAAQ,CAAC;AAAA,MACnE;AAAA,IACF,CAAC;AAAA,EACH;AAEA,SAAO;AACT;AAEA,eAAsB,oBACpB,IACA,OACA,SAM8B;AAC9B,QAAM,EAAE,OAAO,SAAS,MAAM,IAAI;AAClC,QAAM,WAAW,MAAM,YAAY;AACnC,QAAM,iBAAiB,MAAM,kBAAkB;AAE/C,MAAI,SAA6B;AACjC,MAAI,MAAM,eAAe;AACvB,aAAS,MAAM,GAAG,QAAQ,aAAa;AAAA,MACrC,IAAI,MAAM;AAAA,MACV,QAAQ,MAAM;AAAA,MACd;AAAA,MACA;AAAA,MACA;AAAA,MACA,WAAW;AAAA,IACb,CAAC;AACD,QAAI,CAAC,QAAQ;AACX,YAAM,OAAO,OAAO,IAAI,MAAM,uBAAuB,GAAG,EAAE,MAAM,YAAY,CAAC;AAAA,IAC/E;AACA,iCAA6B;AAAA,MAC3B,cAAc;AAAA,MACd,YAAY,OAAO;AAAA,MACnB,SAAS,OAAO,aAAa;AAAA,MAC7B,SAAS,QAAQ,WAAW;AAAA,IAC9B,CAAC;AAAA,EACH,OAAO;AACL,aAAS,MAAM,GAAG,QAAQ,aAAa;AAAA,MACrC,QAAQ,MAAM;AAAA,MACd;AAAA,MACA;AAAA,MACA;AAAA,MACA,MAAM,MAAM;AAAA,MACZ,WAAW;AAAA,IACb,CAAC;AAAA,EACH;AAEA,QAAM,MAAM,oBAAI,KAAK;AACrB,MAAI,CAAC,QAAQ;AACX,aAAS,GAAG,OAAO,aAAa;AAAA,MAC9B,QAAQ,MAAM;AAAA,MACd;AAAA,MACA;AAAA,MACA;AAAA,MACA,MAAM,MAAM;AAAA,MACZ,cAAc,MAAM;AAAA,MACpB,WAAW,QAAQ,MAAM,SAAS;AAAA,MAClC,WAAW;AAAA,MACX,WAAW;AAAA,IACb,CAAC;AACD,OAAG,QAAQ,MAAM;AAAA,EACnB,OAAO;AACL,WAAO,OAAO,MAAM;AACpB,WAAO,eAAe,MAAM;AAC5B,WAAO,YAAY;AACnB,QAAI,MAAM,cAAc,KAAM,QAAO,YAAY;AACjD,QAAI,MAAM,cAAc,MAAO,QAAO,YAAY;AAAA,EACpD;AAEA,MAAI,MAAM,cAAc,MAAM;AAC5B,UAAM,GAAG;AAAA,MACP;AAAA,MACA;AAAA,QACE,QAAQ,MAAM;AAAA,QACd;AAAA,QACA;AAAA,QACA;AAAA,QACA,IAAI,EAAE,KAAK,OAAO,GAAG;AAAA,QACrB,WAAW;AAAA,MACb;AAAA,MACA,EAAE,WAAW,OAAO,WAAW,IAAI;AAAA,IACrC;AACA,WAAO,YAAY;AAAA,EACrB;AAEA,QAAM,GAAG,MAAM;AAEf,MAAI,OAAO,cAAc;AACvB,UAAM,MAAM,aAAa,CAAC,QAAQ,OAAO,OAAO,CAAC,CAAC;AAAA,EACpD;AAEA,SAAO,sBAAsB,MAAM;AACrC;AAEA,eAAsB,sBACpB,IACA,OACA,SAMkB;AAClB,QAAM,EAAE,OAAO,SAAS,cAAc,IAAI;AAC1C,QAAM,WAAW,MAAM,YAAY;AACnC,QAAM,iBAAiB,MAAM,kBAAkB;AAE/C,QAAM,WAAW,MAAM,GAAG,QAAQ,aAAa;AAAA,IAC7C,IAAI;AAAA,IACJ,QAAQ,MAAM;AAAA,IACd;AAAA,IACA;AAAA,IACA;AAAA,IACA,WAAW;AAAA,EACb,CAAC;AACD,MAAI,CAAC,UAAU;AACb,gCAA4B;AAAA,MAC1B,cAAc;AAAA,MACd,YAAY;AAAA,MACZ,SAAS,QAAQ,WAAW;AAAA,IAC9B,CAAC;AACD,WAAO;AAAA,EACT;AAEA,+BAA6B;AAAA,IAC3B,cAAc;AAAA,IACd,YAAY,SAAS;AAAA,IACrB,SAAS,SAAS,aAAa;AAAA,IAC/B,SAAS,QAAQ,WAAW;AAAA,EAC9B,CAAC;AAED,WAAS,YAAY,oBAAI,KAAK;AAC9B,WAAS,YAAY;AACrB,QAAM,GAAG,MAAM;AAEf,MAAI,OAAO,cAAc;AACvB,UAAM,MAAM,aAAa,CAAC,QAAQ,OAAO,OAAO,CAAC,CAAC;AAAA,EACpD;AAEA,SAAO;AACT;AAEA,eAAsB,qBACpB,IACA,OACA,SASoC;AACpC,QAAM,EAAE,SAAS,MAAM,IAAI;AAC3B,QAAM,WAAW,QAAQ,YAAY;AACrC,QAAM,iBAAiB,QAAQ,kBAAkB;AACjD,QAAM,MAAM,oBAAI,KAAK;AACrB,QAAM,iBAAiB,oBAAI,IAAY;AAEvC,QAAM,gBAAmC,CAAC;AAI1C,QAAM,eAAe,oBAAI,IAA6B;AACtD,MAAI,MAAM,QAAQ,QAAQ;AACxB,UAAM,kBAAkB,MAAM,GAAG,KAAK,iBAAiB;AAAA,MACrD,QAAQ,EAAE,KAAK,MAAM,QAAQ;AAAA,MAC7B;AAAA,MACA;AAAA,MACA;AAAA,MACA,MAAM,MAAM;AAAA,MACZ,WAAW;AAAA,IACb,CAAC;AACD,eAAW,YAAY,gBAAiB,cAAa,IAAI,SAAS,QAAQ,QAAQ;AAAA,EACpF;AACA,QAAM,uBAAuB,oBAAI,IAA+B;AAChE,MAAI,MAAM,eAAe,QAAQ,MAAM,QAAQ,QAAQ;AACrD,UAAM,yBAAyB,MAAM,GAAG,KAAK,iBAAiB;AAAA,MAC5D,QAAQ,EAAE,KAAK,MAAM,QAAQ;AAAA,MAC7B;AAAA,MACA;AAAA,MACA;AAAA,MACA,WAAW;AAAA,MACX,WAAW;AAAA,IACb,CAAC;AACD,eAAW,YAAY,wBAAwB;AAC7C,YAAM,UAAU,qBAAqB,IAAI,SAAS,MAAM,KAAK,CAAC;AAC9D,cAAQ,KAAK,QAAQ;AACrB,2BAAqB,IAAI,SAAS,QAAQ,OAAO;AAAA,IACnD;AAAA,EACF;AAEA,aAAW,UAAU,MAAM,SAAS;AAClC,QAAI,SAAS,aAAa,IAAI,MAAM,KAAK;AACzC,QAAI,CAAC,QAAQ;AACX,eAAS,GAAG,OAAO,iBAAiB;AAAA,QAClC;AAAA,QACA;AAAA,QACA;AAAA,QACA;AAAA,QACA,MAAM,MAAM;AAAA,QACZ,cAAc,MAAM;AAAA,QACpB,WAAW,QAAQ,MAAM,UAAU;AAAA,QACnC,WAAW;AAAA,QACX,WAAW;AAAA,MACb,CAAC;AACD,SAAG,QAAQ,MAAM;AACjB,mBAAa,IAAI,QAAQ,MAAM;AAAA,IACjC,OAAO;AACL,mCAA6B;AAAA,QAC3B,cAAc;AAAA,QACd,YAAY,OAAO;AAAA,QACnB,SAAS,OAAO,aAAa;AAAA,QAC7B,GAAG;AAAA,UACD,QAAQ;AAAA,UACR,QAAQ;AAAA,UACR;AAAA,UACA,QAAQ,WAAW;AAAA,QACrB;AAAA,MACF,CAAC;AACD,aAAO,eAAe,MAAM;AAC5B,aAAO,YAAY;AACnB,UAAI,MAAM,eAAe,KAAM,QAAO,YAAY;AAClD,UAAI,MAAM,eAAe,MAAO,QAAO,YAAY;AAAA,IACrD;AAEA,QAAI,MAAM,eAAe,MAAM;AAC7B,iBAAW,iBAAiB,qBAAqB,IAAI,MAAM,KAAK,CAAC,GAAG;AAClE,YAAI,cAAc,OAAO,OAAO,GAAI;AACpC,qCAA6B;AAAA,UAC3B,cAAc;AAAA,UACd,YAAY,cAAc;AAAA,UAC1B,SAAS,cAAc,aAAa;AAAA,UACpC,GAAG;AAAA,YACD,QAAQ;AAAA,YACR,QAAQ;AAAA,YACR;AAAA,YACA,QAAQ,WAAW;AAAA,UACrB;AAAA,QACF,CAAC;AAAA,MACH;AACA,YAAM,GAAG;AAAA,QACP;AAAA,QACA;AAAA,UACE;AAAA,UACA;AAAA,UACA;AAAA,UACA;AAAA,UACA,IAAI,EAAE,KAAK,OAAO,GAAG;AAAA,UACrB,WAAW;AAAA,UACX,WAAW;AAAA,QACb;AAAA,QACA,EAAE,WAAW,OAAO,WAAW,IAAI;AAAA,MACrC;AACA,aAAO,YAAY;AAAA,IACrB;AAEA,mBAAe,IAAI,MAAM;AACzB,kBAAc,KAAK,MAAM;AAAA,EAC3B;AAEA,MAAI,MAAM,QAAQ,QAAQ;AACxB,UAAM,GAAG,MAAM;AAAA,EACjB;AAEA,MAAI,OAAO,gBAAgB,eAAe,OAAO,GAAG;AAClD,UAAM,OAAO,MAAM,KAAK,cAAc,EAAE,IAAI,CAAC,WAAW,QAAQ,QAAQ,SAAS,QAAQ,CAAC;AAC1F,UAAM,MAAM,aAAa,IAAI;AAAA,EAC/B;AAEA,SAAO,cAAc,IAAI,yBAAyB;AACpD;AAEA,eAAsB,sBACpB,IACA,OACA,SASiB;AACjB,QAAM,EAAE,SAAS,QAAQ,IAAI;AAC7B,QAAM,WAAW,QAAQ,YAAY;AACrC,QAAM,iBAAiB,QAAQ,kBAAkB;AACjD,MAAI,CAAC,QAAQ,OAAQ,QAAO;AAE5B,QAAM,kBAAkB,MAAM,GAAG,KAAK,iBAAiB;AAAA,IACrD,QAAQ,EAAE,KAAK,QAAe;AAAA,IAC9B;AAAA,IACA;AAAA,IACA;AAAA,IACA,WAAW;AAAA,EACb,CAAC;AACD,QAAM,gBAAgB,oBAAI,IAA+B;AACzD,aAAW,UAAU,iBAAiB;AACpC,UAAM,UAAU,cAAc,IAAI,OAAO,MAAM,KAAK,CAAC;AACrD,YAAQ,KAAK,MAAM;AACnB,kBAAc,IAAI,OAAO,QAAQ,OAAO;AAAA,EAC1C;AAEA,aAAW,UAAU,SAAS;AAC5B,UAAM,UAAU,cAAc,IAAI,MAAM,KAAK,CAAC;AAC9C,UAAM,YAAY,qBAAqB,QAAQ,2BAA2B,QAAQ,QAAQ,WAAW,IAAI;AACzG,QAAI,CAAC,QAAQ,QAAQ;AACnB,YAAM,WAAW,uBAAuB,QAAQ,gCAAgC;AAChF,kCAA4B;AAAA,QAC1B,cAAc;AAAA,QACd,YAAY;AAAA,QACZ,UAAU,YAAY,UAAU;AAAA,QAChC,SAAS,WAAW,OAAO,UAAU;AAAA,MACvC,CAAC;AACD;AAAA,IACF;AACA,eAAW,UAAU,SAAS;AAC5B,UACE,QAAQ,oCACL,CAAC,OAAO,UAAU,eAAe,KAAK,QAAQ,kCAAkC,OAAO,EAAE,GAC5F;AACA,8CAAsC,MAAM;AAAA,MAC9C;AACA,mCAA6B;AAAA,QAC3B,cAAc;AAAA,QACd,YAAY,OAAO;AAAA,QACnB,SAAS,OAAO,aAAa;AAAA,QAC7B,GAAG;AAAA,UACD,QAAQ;AAAA,UACR,QAAQ;AAAA,UACR;AAAA,UACA,QAAQ,WAAW;AAAA,QACrB;AAAA,MACF,CAAC;AAAA,IACH;AAAA,EACF;AAEA,QAAM,WAAW,MAAM,GAAG;AAAA,IACxB;AAAA,IACA;AAAA,MACE,QAAQ,EAAE,KAAK,QAAe;AAAA,MAC9B;AAAA,MACA;AAAA,MACA;AAAA,MACA,WAAW;AAAA,IACb;AAAA,IACA,EAAE,WAAW,oBAAI,KAAK,GAAG,WAAW,MAAM;AAAA,EAC5C;AAEA,MAAI,OAAO,cAAc;AACvB,UAAM,OAAO,QAAQ,IAAI,CAAC,WAAW,QAAQ,QAAQ,SAAS,QAAQ,CAAC;AACvE,UAAM,MAAM,aAAa,IAAI;AAAA,EAC/B;AAEA,SAAO;AACT;",
|
|
6
6
|
"names": []
|
|
7
7
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@open-mercato/core",
|
|
3
|
-
"version": "0.6.6-develop.
|
|
3
|
+
"version": "0.6.6-develop.6257.1.6d0af84d26",
|
|
4
4
|
"license": "MIT",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "./dist/index.js",
|
|
@@ -246,16 +246,16 @@
|
|
|
246
246
|
"zod": "^4.4.3"
|
|
247
247
|
},
|
|
248
248
|
"peerDependencies": {
|
|
249
|
-
"@open-mercato/ai-assistant": "0.6.6-develop.
|
|
250
|
-
"@open-mercato/shared": "0.6.6-develop.
|
|
251
|
-
"@open-mercato/ui": "0.6.6-develop.
|
|
249
|
+
"@open-mercato/ai-assistant": "0.6.6-develop.6257.1.6d0af84d26",
|
|
250
|
+
"@open-mercato/shared": "0.6.6-develop.6257.1.6d0af84d26",
|
|
251
|
+
"@open-mercato/ui": "0.6.6-develop.6257.1.6d0af84d26",
|
|
252
252
|
"react": "^19.0.0",
|
|
253
253
|
"react-dom": "^19.0.0"
|
|
254
254
|
},
|
|
255
255
|
"devDependencies": {
|
|
256
|
-
"@open-mercato/ai-assistant": "0.6.6-develop.
|
|
257
|
-
"@open-mercato/shared": "0.6.6-develop.
|
|
258
|
-
"@open-mercato/ui": "0.6.6-develop.
|
|
256
|
+
"@open-mercato/ai-assistant": "0.6.6-develop.6257.1.6d0af84d26",
|
|
257
|
+
"@open-mercato/shared": "0.6.6-develop.6257.1.6d0af84d26",
|
|
258
|
+
"@open-mercato/ui": "0.6.6-develop.6257.1.6d0af84d26",
|
|
259
259
|
"@testing-library/dom": "^10.4.1",
|
|
260
260
|
"@testing-library/jest-dom": "^6.9.1",
|
|
261
261
|
"@testing-library/react": "^16.3.1",
|
|
@@ -10,9 +10,14 @@ import {
|
|
|
10
10
|
|
|
11
11
|
export const metadata = {
|
|
12
12
|
PUT: { requireAuth: true, requireFeatures: ['messages.view'] },
|
|
13
|
+
DELETE: { requireAuth: true, requireFeatures: ['messages.view'] },
|
|
13
14
|
}
|
|
14
15
|
|
|
15
|
-
|
|
16
|
+
async function runConversationArchiveMutation(
|
|
17
|
+
req: Request,
|
|
18
|
+
id: string,
|
|
19
|
+
commandId: 'messages.conversation.archive_for_actor' | 'messages.conversation.unarchive_for_actor',
|
|
20
|
+
) {
|
|
16
21
|
const { ctx, scope } = await resolveMessageContext(req)
|
|
17
22
|
const commandBus = ctx.container.resolve('commandBus') as CommandBus
|
|
18
23
|
|
|
@@ -23,7 +28,7 @@ export async function PUT(req: Request, { params }: { params: { id: string } })
|
|
|
23
28
|
organizationId: scope.organizationId,
|
|
24
29
|
userId: scope.userId,
|
|
25
30
|
resourceKind: 'messages.conversation',
|
|
26
|
-
resourceId:
|
|
31
|
+
resourceId: id,
|
|
27
32
|
operation: 'update',
|
|
28
33
|
requestMethod: req.method,
|
|
29
34
|
requestHeaders: req.headers,
|
|
@@ -39,9 +44,9 @@ export async function PUT(req: Request, { params }: { params: { id: string } })
|
|
|
39
44
|
}
|
|
40
45
|
|
|
41
46
|
try {
|
|
42
|
-
const { result, logEntry } = await commandBus.execute(
|
|
47
|
+
const { result, logEntry } = await commandBus.execute(commandId, {
|
|
43
48
|
input: {
|
|
44
|
-
anchorMessageId:
|
|
49
|
+
anchorMessageId: id,
|
|
45
50
|
tenantId: scope.tenantId,
|
|
46
51
|
organizationId: scope.organizationId,
|
|
47
52
|
userId: scope.userId,
|
|
@@ -59,14 +64,14 @@ export async function PUT(req: Request, { params }: { params: { id: string } })
|
|
|
59
64
|
const response = Response.json(result)
|
|
60
65
|
attachOperationMetadataHeader(response, logEntry, {
|
|
61
66
|
resourceKind: 'messages.conversation',
|
|
62
|
-
resourceId:
|
|
67
|
+
resourceId: id,
|
|
63
68
|
})
|
|
64
69
|
await runMessageMutationGuardAfterSuccess(guardResult.afterSuccessCallbacks, {
|
|
65
70
|
tenantId: scope.tenantId,
|
|
66
71
|
organizationId: scope.organizationId,
|
|
67
72
|
userId: scope.userId,
|
|
68
73
|
resourceKind: 'messages.conversation',
|
|
69
|
-
resourceId:
|
|
74
|
+
resourceId: id,
|
|
70
75
|
operation: 'update',
|
|
71
76
|
requestMethod: req.method,
|
|
72
77
|
requestHeaders: req.headers,
|
|
@@ -85,6 +90,14 @@ export async function PUT(req: Request, { params }: { params: { id: string } })
|
|
|
85
90
|
}
|
|
86
91
|
}
|
|
87
92
|
|
|
93
|
+
export async function PUT(req: Request, { params }: { params: { id: string } }) {
|
|
94
|
+
return runConversationArchiveMutation(req, params.id, 'messages.conversation.archive_for_actor')
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
export async function DELETE(req: Request, { params }: { params: { id: string } }) {
|
|
98
|
+
return runConversationArchiveMutation(req, params.id, 'messages.conversation.unarchive_for_actor')
|
|
99
|
+
}
|
|
100
|
+
|
|
88
101
|
export const openApi: OpenApiRouteDoc = {
|
|
89
102
|
tag: 'Messages',
|
|
90
103
|
methods: {
|
|
@@ -98,5 +111,15 @@ export const openApi: OpenApiRouteDoc = {
|
|
|
98
111
|
{ status: 404, description: 'Message not found', schema: errorResponseSchema },
|
|
99
112
|
],
|
|
100
113
|
},
|
|
114
|
+
DELETE: {
|
|
115
|
+
summary: 'Unarchive conversation for current actor',
|
|
116
|
+
responses: [
|
|
117
|
+
{ status: 200, description: 'Conversation unarchived', schema: conversationMutationResponseSchema },
|
|
118
|
+
],
|
|
119
|
+
errors: [
|
|
120
|
+
{ status: 403, description: 'Access denied', schema: errorResponseSchema },
|
|
121
|
+
{ status: 404, description: 'Message not found', schema: errorResponseSchema },
|
|
122
|
+
],
|
|
123
|
+
},
|
|
101
124
|
},
|
|
102
125
|
}
|
|
@@ -9,10 +9,15 @@ import {
|
|
|
9
9
|
} from '../../../openapi'
|
|
10
10
|
|
|
11
11
|
export const metadata = {
|
|
12
|
+
PUT: { requireAuth: true, requireFeatures: ['messages.view'] },
|
|
12
13
|
DELETE: { requireAuth: true, requireFeatures: ['messages.view'] },
|
|
13
14
|
}
|
|
14
15
|
|
|
15
|
-
|
|
16
|
+
async function runConversationReadMutation(
|
|
17
|
+
req: Request,
|
|
18
|
+
id: string,
|
|
19
|
+
commandId: 'messages.conversation.mark_read_for_actor' | 'messages.conversation.mark_unread_for_actor',
|
|
20
|
+
) {
|
|
16
21
|
const { ctx, scope } = await resolveMessageContext(req)
|
|
17
22
|
const commandBus = ctx.container.resolve('commandBus') as CommandBus
|
|
18
23
|
|
|
@@ -23,7 +28,7 @@ export async function DELETE(req: Request, { params }: { params: { id: string }
|
|
|
23
28
|
organizationId: scope.organizationId,
|
|
24
29
|
userId: scope.userId,
|
|
25
30
|
resourceKind: 'messages.conversation',
|
|
26
|
-
resourceId:
|
|
31
|
+
resourceId: id,
|
|
27
32
|
operation: 'update',
|
|
28
33
|
requestMethod: req.method,
|
|
29
34
|
requestHeaders: req.headers,
|
|
@@ -39,9 +44,9 @@ export async function DELETE(req: Request, { params }: { params: { id: string }
|
|
|
39
44
|
}
|
|
40
45
|
|
|
41
46
|
try {
|
|
42
|
-
const { result, logEntry } = await commandBus.execute(
|
|
47
|
+
const { result, logEntry } = await commandBus.execute(commandId, {
|
|
43
48
|
input: {
|
|
44
|
-
anchorMessageId:
|
|
49
|
+
anchorMessageId: id,
|
|
45
50
|
tenantId: scope.tenantId,
|
|
46
51
|
organizationId: scope.organizationId,
|
|
47
52
|
userId: scope.userId,
|
|
@@ -59,14 +64,14 @@ export async function DELETE(req: Request, { params }: { params: { id: string }
|
|
|
59
64
|
const response = Response.json(result)
|
|
60
65
|
attachOperationMetadataHeader(response, logEntry, {
|
|
61
66
|
resourceKind: 'messages.conversation',
|
|
62
|
-
resourceId:
|
|
67
|
+
resourceId: id,
|
|
63
68
|
})
|
|
64
69
|
await runMessageMutationGuardAfterSuccess(guardResult.afterSuccessCallbacks, {
|
|
65
70
|
tenantId: scope.tenantId,
|
|
66
71
|
organizationId: scope.organizationId,
|
|
67
72
|
userId: scope.userId,
|
|
68
73
|
resourceKind: 'messages.conversation',
|
|
69
|
-
resourceId:
|
|
74
|
+
resourceId: id,
|
|
70
75
|
operation: 'update',
|
|
71
76
|
requestMethod: req.method,
|
|
72
77
|
requestHeaders: req.headers,
|
|
@@ -85,9 +90,27 @@ export async function DELETE(req: Request, { params }: { params: { id: string }
|
|
|
85
90
|
}
|
|
86
91
|
}
|
|
87
92
|
|
|
93
|
+
export async function PUT(req: Request, { params }: { params: { id: string } }) {
|
|
94
|
+
return runConversationReadMutation(req, params.id, 'messages.conversation.mark_read_for_actor')
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
export async function DELETE(req: Request, { params }: { params: { id: string } }) {
|
|
98
|
+
return runConversationReadMutation(req, params.id, 'messages.conversation.mark_unread_for_actor')
|
|
99
|
+
}
|
|
100
|
+
|
|
88
101
|
export const openApi: OpenApiRouteDoc = {
|
|
89
102
|
tag: 'Messages',
|
|
90
103
|
methods: {
|
|
104
|
+
PUT: {
|
|
105
|
+
summary: 'Mark entire conversation as read for current actor',
|
|
106
|
+
responses: [
|
|
107
|
+
{ status: 200, description: 'Conversation marked read', schema: conversationMutationResponseSchema },
|
|
108
|
+
],
|
|
109
|
+
errors: [
|
|
110
|
+
{ status: 403, description: 'Access denied', schema: errorResponseSchema },
|
|
111
|
+
{ status: 404, description: 'Message not found', schema: errorResponseSchema },
|
|
112
|
+
],
|
|
113
|
+
},
|
|
91
114
|
DELETE: {
|
|
92
115
|
summary: 'Mark entire conversation as unread for current actor',
|
|
93
116
|
responses: [
|
|
@@ -122,6 +122,19 @@ export async function GET(req: Request, { params }: { params: { id: string } })
|
|
|
122
122
|
threadMessage.senderUserId === scope.userId || visibleRecipientMessageIds.has(threadMessage.id)
|
|
123
123
|
))
|
|
124
124
|
|
|
125
|
+
const actorRecipientStatusByMessageId = new Map<string, string>()
|
|
126
|
+
for (const row of visibleRecipientRows) {
|
|
127
|
+
actorRecipientStatusByMessageId.set(row.messageId, row.status)
|
|
128
|
+
}
|
|
129
|
+
if (recipient) {
|
|
130
|
+
actorRecipientStatusByMessageId.set(params.id, autoMarkRead ? 'read' : recipient.status)
|
|
131
|
+
}
|
|
132
|
+
const actorRecipientStatuses = Array.from(actorRecipientStatusByMessageId.values())
|
|
133
|
+
const conversationArchived = actorRecipientStatuses.length > 0
|
|
134
|
+
&& actorRecipientStatuses.every((status) => status === 'archived')
|
|
135
|
+
const conversationAllUnread = actorRecipientStatuses.length > 0
|
|
136
|
+
&& actorRecipientStatuses.every((status) => status === 'unread')
|
|
137
|
+
|
|
125
138
|
const threadSenderIds = actorVisibleThreadMessages
|
|
126
139
|
.map((threadMessage) => threadMessage.senderUserId)
|
|
127
140
|
.filter((value): value is string => typeof value === 'string' && value.length > 0)
|
|
@@ -244,6 +257,8 @@ export async function GET(req: Request, { params }: { params: { id: string } })
|
|
|
244
257
|
}
|
|
245
258
|
}),
|
|
246
259
|
isRead: recipient ? (autoMarkRead || recipient.status !== 'unread') : true,
|
|
260
|
+
conversationArchived,
|
|
261
|
+
conversationAllUnread,
|
|
247
262
|
})
|
|
248
263
|
}
|
|
249
264
|
|
|
@@ -121,6 +121,8 @@ export const messageDetailResponseSchema = z.object({
|
|
|
121
121
|
})),
|
|
122
122
|
thread: z.array(messageThreadItemSchema),
|
|
123
123
|
isRead: z.boolean(),
|
|
124
|
+
conversationArchived: z.boolean(),
|
|
125
|
+
conversationAllUnread: z.boolean(),
|
|
124
126
|
})
|
|
125
127
|
|
|
126
128
|
export const messageTokenDetailResponseSchema = z.object({
|
|
@@ -228,6 +228,91 @@ const markConversationUnreadForActorCommand: CommandHandler<unknown, { ok: true;
|
|
|
228
228
|
},
|
|
229
229
|
}
|
|
230
230
|
|
|
231
|
+
const unarchiveConversationForActorCommand: CommandHandler<unknown, { ok: true; affectedCount: number }> = {
|
|
232
|
+
id: 'messages.conversation.unarchive_for_actor',
|
|
233
|
+
async execute(rawInput, ctx) {
|
|
234
|
+
const input = conversationCommandSchema.parse(rawInput)
|
|
235
|
+
const em = (ctx.container.resolve('em') as EntityManager).fork()
|
|
236
|
+
const scope = await resolveConversationScope(em, input)
|
|
237
|
+
const messageIdsToUnarchive = scope.messageIds.filter((messageId) => scope.recipientMessageIds.has(messageId))
|
|
238
|
+
|
|
239
|
+
if (messageIdsToUnarchive.length === 0) {
|
|
240
|
+
return { ok: true, affectedCount: 0 }
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
const unarchivedIds: string[] = []
|
|
244
|
+
await em.transactional(async (trx) => {
|
|
245
|
+
const recipients = await trx.find(MessageRecipient, {
|
|
246
|
+
messageId: { $in: messageIdsToUnarchive },
|
|
247
|
+
recipientUserId: input.userId,
|
|
248
|
+
deletedAt: null,
|
|
249
|
+
})
|
|
250
|
+
for (const recipient of recipients) {
|
|
251
|
+
if (recipient.status === 'archived' || recipient.archivedAt !== null) {
|
|
252
|
+
recipient.archivedAt = null
|
|
253
|
+
recipient.status = recipient.readAt ? 'read' : 'unread'
|
|
254
|
+
unarchivedIds.push(recipient.messageId)
|
|
255
|
+
}
|
|
256
|
+
}
|
|
257
|
+
})
|
|
258
|
+
|
|
259
|
+
for (const messageId of unarchivedIds) {
|
|
260
|
+
await emitMessagesEvent('messages.message.unarchived', {
|
|
261
|
+
messageId,
|
|
262
|
+
recipientUserId: input.userId,
|
|
263
|
+
userId: input.userId,
|
|
264
|
+
tenantId: input.tenantId,
|
|
265
|
+
organizationId: input.organizationId,
|
|
266
|
+
}, { persistent: true })
|
|
267
|
+
}
|
|
268
|
+
|
|
269
|
+
return { ok: true, affectedCount: unarchivedIds.length }
|
|
270
|
+
},
|
|
271
|
+
}
|
|
272
|
+
|
|
273
|
+
const markConversationReadForActorCommand: CommandHandler<unknown, { ok: true; affectedCount: number }> = {
|
|
274
|
+
id: 'messages.conversation.mark_read_for_actor',
|
|
275
|
+
async execute(rawInput, ctx) {
|
|
276
|
+
const input = conversationCommandSchema.parse(rawInput)
|
|
277
|
+
const em = (ctx.container.resolve('em') as EntityManager).fork()
|
|
278
|
+
const scope = await resolveConversationScope(em, input)
|
|
279
|
+
const messageIdsToMarkRead = scope.messageIds.filter((messageId) => scope.recipientMessageIds.has(messageId))
|
|
280
|
+
|
|
281
|
+
if (messageIdsToMarkRead.length === 0) {
|
|
282
|
+
return { ok: true, affectedCount: 0 }
|
|
283
|
+
}
|
|
284
|
+
|
|
285
|
+
const readAt = new Date()
|
|
286
|
+
const markedIds: string[] = []
|
|
287
|
+
await em.transactional(async (trx) => {
|
|
288
|
+
const recipients = await trx.find(MessageRecipient, {
|
|
289
|
+
messageId: { $in: messageIdsToMarkRead },
|
|
290
|
+
recipientUserId: input.userId,
|
|
291
|
+
deletedAt: null,
|
|
292
|
+
})
|
|
293
|
+
for (const recipient of recipients) {
|
|
294
|
+
if (recipient.status === 'unread') {
|
|
295
|
+
recipient.status = 'read'
|
|
296
|
+
recipient.readAt = readAt
|
|
297
|
+
markedIds.push(recipient.messageId)
|
|
298
|
+
}
|
|
299
|
+
}
|
|
300
|
+
})
|
|
301
|
+
|
|
302
|
+
for (const messageId of markedIds) {
|
|
303
|
+
await emitMessagesEvent('messages.message.read', {
|
|
304
|
+
messageId,
|
|
305
|
+
recipientUserId: input.userId,
|
|
306
|
+
userId: input.userId,
|
|
307
|
+
tenantId: input.tenantId,
|
|
308
|
+
organizationId: input.organizationId,
|
|
309
|
+
}, { persistent: true })
|
|
310
|
+
}
|
|
311
|
+
|
|
312
|
+
return { ok: true, affectedCount: markedIds.length }
|
|
313
|
+
},
|
|
314
|
+
}
|
|
315
|
+
|
|
231
316
|
type DeletedTarget = { messageId: string; target: 'sender' | 'recipient' }
|
|
232
317
|
|
|
233
318
|
const deleteConversationForActorCommand: CommandHandler<unknown, { ok: true; affectedCount: number }> = {
|
|
@@ -289,5 +374,7 @@ const deleteConversationForActorCommand: CommandHandler<unknown, { ok: true; aff
|
|
|
289
374
|
}
|
|
290
375
|
|
|
291
376
|
registerCommand(archiveConversationForActorCommand)
|
|
377
|
+
registerCommand(unarchiveConversationForActorCommand)
|
|
292
378
|
registerCommand(markConversationUnreadForActorCommand)
|
|
379
|
+
registerCommand(markConversationReadForActorCommand)
|
|
293
380
|
registerCommand(deleteConversationForActorCommand)
|
|
@@ -220,6 +220,8 @@ export function MessageDetailPageClient({ id }: { id: string }) {
|
|
|
220
220
|
const firstConversationMessageId = state.conversationItems[0]?.id ?? null
|
|
221
221
|
const latestConversationMessageId = state.conversationItems[state.conversationItems.length - 1]?.id ?? null
|
|
222
222
|
const canRunConversationActions = Boolean(firstConversationMessageId)
|
|
223
|
+
const conversationArchived = Boolean(detail.conversationArchived)
|
|
224
|
+
const conversationAllUnread = Boolean(detail.conversationAllUnread)
|
|
223
225
|
|
|
224
226
|
return (
|
|
225
227
|
<div className="space-y-3">
|
|
@@ -228,6 +230,8 @@ export function MessageDetailPageClient({ id }: { id: string }) {
|
|
|
228
230
|
priority={(detail.priority as 'low' | 'normal' | 'high' | 'urgent') ?? 'normal'}
|
|
229
231
|
canReply={!detail.isDraft && detail.typeDefinition.allowReply && Boolean(firstConversationMessageId)}
|
|
230
232
|
canForwardAll={!detail.isDraft && detail.typeDefinition.allowForward && Boolean(latestConversationMessageId)}
|
|
233
|
+
conversationArchived={conversationArchived}
|
|
234
|
+
conversationAllUnread={conversationAllUnread}
|
|
231
235
|
actionsDisabled={Boolean(state.activeConversationAction)}
|
|
232
236
|
activeActionId={state.activeConversationAction}
|
|
233
237
|
onReply={() => {
|
|
@@ -244,13 +248,21 @@ export function MessageDetailPageClient({ id }: { id: string }) {
|
|
|
244
248
|
messageId: latestConversationMessageId,
|
|
245
249
|
})
|
|
246
250
|
}}
|
|
247
|
-
|
|
251
|
+
onToggleArchiveConversation={() => {
|
|
248
252
|
if (!canRunConversationActions) return
|
|
249
|
-
|
|
253
|
+
if (conversationArchived) {
|
|
254
|
+
void state.unarchiveConversation(firstConversationMessageId ?? undefined)
|
|
255
|
+
} else {
|
|
256
|
+
void state.archiveConversation(firstConversationMessageId ?? undefined)
|
|
257
|
+
}
|
|
250
258
|
}}
|
|
251
|
-
|
|
259
|
+
onToggleReadConversation={() => {
|
|
252
260
|
if (!canRunConversationActions) return
|
|
253
|
-
|
|
261
|
+
if (conversationAllUnread) {
|
|
262
|
+
void state.markConversationRead(firstConversationMessageId ?? undefined)
|
|
263
|
+
} else {
|
|
264
|
+
void state.markConversationUnread(firstConversationMessageId ?? undefined)
|
|
265
|
+
}
|
|
254
266
|
}}
|
|
255
267
|
onDeleteConversation={() => {
|
|
256
268
|
if (!canRunConversationActions || state.activeConversationAction) return
|
|
@@ -20,7 +20,12 @@ type RequestAndRefreshOptions = {
|
|
|
20
20
|
skipDetailAutoMarkRead?: boolean
|
|
21
21
|
}
|
|
22
22
|
|
|
23
|
-
type ConversationActionKind =
|
|
23
|
+
type ConversationActionKind =
|
|
24
|
+
| 'archiveConversation'
|
|
25
|
+
| 'unarchiveConversation'
|
|
26
|
+
| 'deleteConversation'
|
|
27
|
+
| 'markAllUnread'
|
|
28
|
+
| 'markAllRead'
|
|
24
29
|
|
|
25
30
|
type MessageDetailMutationContext = {
|
|
26
31
|
resourceKind: 'message' | 'conversation'
|
|
@@ -188,6 +193,15 @@ export function useMessageDetailsActions({
|
|
|
188
193
|
})
|
|
189
194
|
}, [id, runConversationAction, t])
|
|
190
195
|
|
|
196
|
+
const unarchiveConversation = React.useCallback(async (messageId?: string) => {
|
|
197
|
+
const targetMessageId = messageId ?? id
|
|
198
|
+
await runConversationAction('unarchiveConversation', {
|
|
199
|
+
url: `/api/messages/${encodeURIComponent(targetMessageId)}/conversation/archive`,
|
|
200
|
+
method: 'DELETE',
|
|
201
|
+
successMessage: t('messages.flash.conversationUnarchived', 'Conversation restored from archive.'),
|
|
202
|
+
})
|
|
203
|
+
}, [id, runConversationAction, t])
|
|
204
|
+
|
|
191
205
|
const markConversationUnread = React.useCallback(async (messageId?: string) => {
|
|
192
206
|
const targetMessageId = messageId ?? id
|
|
193
207
|
await runConversationAction('markAllUnread', {
|
|
@@ -198,6 +212,15 @@ export function useMessageDetailsActions({
|
|
|
198
212
|
})
|
|
199
213
|
}, [id, runConversationAction, t])
|
|
200
214
|
|
|
215
|
+
const markConversationRead = React.useCallback(async (messageId?: string) => {
|
|
216
|
+
const targetMessageId = messageId ?? id
|
|
217
|
+
await runConversationAction('markAllRead', {
|
|
218
|
+
url: `/api/messages/${encodeURIComponent(targetMessageId)}/conversation/read`,
|
|
219
|
+
method: 'PUT',
|
|
220
|
+
successMessage: t('messages.flash.conversationMarkedRead', 'Conversation marked read.'),
|
|
221
|
+
})
|
|
222
|
+
}, [id, runConversationAction, t])
|
|
223
|
+
|
|
201
224
|
const deleteConversation = React.useCallback(async (messageId?: string) => {
|
|
202
225
|
const targetMessageId = messageId ?? id
|
|
203
226
|
await runConversationAction('deleteConversation', {
|
|
@@ -393,8 +416,10 @@ export function useMessageDetailsActions({
|
|
|
393
416
|
toggleRead,
|
|
394
417
|
toggleArchive,
|
|
395
418
|
archiveConversation,
|
|
419
|
+
unarchiveConversation,
|
|
396
420
|
deleteConversation,
|
|
397
421
|
markConversationUnread,
|
|
422
|
+
markConversationRead,
|
|
398
423
|
requestAndRefresh,
|
|
399
424
|
handleDelete,
|
|
400
425
|
handleDeleteDialogKeyDown,
|
|
@@ -4,7 +4,7 @@ import * as React from 'react'
|
|
|
4
4
|
import { useT } from '@open-mercato/shared/lib/i18n/context'
|
|
5
5
|
import { FormHeader, type ActionItem } from '@open-mercato/ui/backend/forms'
|
|
6
6
|
import { IconButton } from '@open-mercato/ui/primitives/icon-button'
|
|
7
|
-
import { Archive, Forward, MailMinus, Reply, Trash2 } from 'lucide-react'
|
|
7
|
+
import { Archive, ArchiveRestore, Forward, MailMinus, MailOpen, Reply, Trash2 } from 'lucide-react'
|
|
8
8
|
import { PriorityBadge } from '../../utils/PriorityBadge'
|
|
9
9
|
|
|
10
10
|
type MainMessageHeaderProps = {
|
|
@@ -12,12 +12,21 @@ type MainMessageHeaderProps = {
|
|
|
12
12
|
priority: 'low' | 'normal' | 'high' | 'urgent'
|
|
13
13
|
canReply: boolean
|
|
14
14
|
canForwardAll: boolean
|
|
15
|
+
conversationArchived: boolean
|
|
16
|
+
conversationAllUnread: boolean
|
|
15
17
|
actionsDisabled?: boolean
|
|
16
|
-
activeActionId?:
|
|
18
|
+
activeActionId?:
|
|
19
|
+
| 'forwardAll'
|
|
20
|
+
| 'archiveConversation'
|
|
21
|
+
| 'unarchiveConversation'
|
|
22
|
+
| 'markAllUnread'
|
|
23
|
+
| 'markAllRead'
|
|
24
|
+
| 'deleteConversation'
|
|
25
|
+
| null
|
|
17
26
|
onReply: () => void
|
|
18
27
|
onForwardAll: () => void
|
|
19
|
-
|
|
20
|
-
|
|
28
|
+
onToggleArchiveConversation: () => void
|
|
29
|
+
onToggleReadConversation: () => void
|
|
21
30
|
onDeleteConversation: () => void
|
|
22
31
|
}
|
|
23
32
|
|
|
@@ -42,19 +51,23 @@ export function MainMessageHeader(props: MainMessageHeaderProps) {
|
|
|
42
51
|
},
|
|
43
52
|
{
|
|
44
53
|
id: 'archive-conversation',
|
|
45
|
-
label:
|
|
46
|
-
|
|
47
|
-
|
|
54
|
+
label: props.conversationArchived
|
|
55
|
+
? t('messages.actions.unarchiveConversation', 'Unarchive conversation')
|
|
56
|
+
: t('messages.actions.archiveConversation', 'Archive conversation'),
|
|
57
|
+
icon: props.conversationArchived ? ArchiveRestore : Archive,
|
|
58
|
+
onSelect: props.onToggleArchiveConversation,
|
|
48
59
|
disabled: props.actionsDisabled,
|
|
49
|
-
loading: props.activeActionId === 'archiveConversation',
|
|
60
|
+
loading: props.activeActionId === 'archiveConversation' || props.activeActionId === 'unarchiveConversation',
|
|
50
61
|
},
|
|
51
62
|
{
|
|
52
63
|
id: 'mark-all-unread',
|
|
53
|
-
label:
|
|
54
|
-
|
|
55
|
-
|
|
64
|
+
label: props.conversationAllUnread
|
|
65
|
+
? t('messages.actions.markAllRead', 'Mark all read')
|
|
66
|
+
: t('messages.actions.markAllUnread', 'Mark all unread'),
|
|
67
|
+
icon: props.conversationAllUnread ? MailOpen : MailMinus,
|
|
68
|
+
onSelect: props.onToggleReadConversation,
|
|
56
69
|
disabled: props.actionsDisabled,
|
|
57
|
-
loading: props.activeActionId === 'markAllUnread',
|
|
70
|
+
loading: props.activeActionId === 'markAllUnread' || props.activeActionId === 'markAllRead',
|
|
58
71
|
},
|
|
59
72
|
{
|
|
60
73
|
id: 'delete-conversation',
|
|
@@ -68,10 +81,12 @@ export function MainMessageHeader(props: MainMessageHeaderProps) {
|
|
|
68
81
|
props.actionsDisabled,
|
|
69
82
|
props.canForwardAll,
|
|
70
83
|
props.canReply,
|
|
71
|
-
props.
|
|
84
|
+
props.conversationArchived,
|
|
85
|
+
props.conversationAllUnread,
|
|
86
|
+
props.onToggleArchiveConversation,
|
|
72
87
|
props.onDeleteConversation,
|
|
73
88
|
props.onForwardAll,
|
|
74
|
-
props.
|
|
89
|
+
props.onToggleReadConversation,
|
|
75
90
|
props.onReply,
|
|
76
91
|
props.activeActionId,
|
|
77
92
|
t,
|