diff --git a/packages/cockroach/src/adapter.ts b/packages/cockroach/src/adapter.ts index 0349e1e269..56d065d5a6 100644 --- a/packages/cockroach/src/adapter.ts +++ b/packages/cockroach/src/adapter.ts @@ -45,7 +45,7 @@ import { type LinkPreviewData, type LinkPreviewID, type MessageExtra, - type BlobData + type BlobData, BlobUpdateData } from '@hcengineering/communication-types' import type { DbAdapter, @@ -162,6 +162,16 @@ export class CockroachAdapter implements DbAdapter { await this.message.setBlobs(cardId, messageId, blobs, socialId, date) } + async updateBlobs ( + cardId: CardID, + messageId: MessageID, + blobs: BlobUpdateData[], + socialId: SocialID, + date: Date + ): Promise { + await this.message.updateBlobs(cardId, messageId, blobs, socialId, date) + } + async attachLinkPreviews ( cardId: CardID, messageId: MessageID, diff --git a/packages/cockroach/src/db/message.ts b/packages/cockroach/src/db/message.ts index a5cef55d01..eaffbee23d 100644 --- a/packages/cockroach/src/db/message.ts +++ b/packages/cockroach/src/db/message.ts @@ -20,6 +20,7 @@ import { AttachThreadPatchData, type BlobData, type BlobID, + BlobUpdateData, type CardID, type CardType, DetachBlobsPatchData, @@ -40,6 +41,7 @@ import { type SocialID, SortingOrder, type Thread, + UpdateBlobsPatchData, UpdateThreadPatchData } from '@hcengineering/communication-types' import type { ThreadUpdates, ThreadQuery } from '@hcengineering/communication-sdk-types' @@ -298,6 +300,65 @@ export class MessagesDb extends BaseDb { }) } + async updateBlobs ( + cardId: CardID, + messageId: MessageID, + blobs: BlobUpdateData[], + socialId: SocialID, + date: Date + ): Promise { + if (blobs.length === 0) return + + const colMap = { + mimeType: { col: 'type', cast: '::varchar' }, + fileName: { col: 'filename', cast: '::varchar' }, + size: { col: 'size', cast: '::int8' }, + metadata: { col: 'meta', cast: '::jsonb' } + } as const + type UpdateKey = keyof typeof colMap + const updateKeys = Object.keys(colMap) as UpdateKey[] + + const params: any[] = [this.workspace, cardId, messageId] + + const rowLen = 1 + updateKeys.length + + const tuples = blobs.map((blob, i) => { + params.push(blob.blobId) + updateKeys.forEach((k) => params.push(blob[k] ?? null)) + + const offset = 3 + i * rowLen + const casts = ['::uuid', ...updateKeys.map((k) => colMap[k].cast)] + const placeholders = casts.map((cast, idx) => `$${offset + idx + 1}${cast}`) + return `(${placeholders.join(', ')})` + }) + + const setClauses = updateKeys.map((k) => { + const col = colMap[k].col + return `${col} = COALESCE(v.${col}, f.${col})` + }) + + const updateSql = ` + UPDATE ${TableName.File} AS f + SET ${setClauses.join(',\n ')} + FROM (VALUES ${tuples.join(',\n ')}) AS v(blob_id, ${updateKeys.map((k) => colMap[k].col).join(', ')}) + WHERE f.workspace_id = $1::uuid + AND f.card_id = $2::varchar + AND f.message_id = $3::varchar + AND f.blob_id = v.blob_id; + ` + + const inDb = await this.isMessageInDb(cardId, messageId) + if (!inDb) { + await this.getRowClient().begin(async (txn) => { + await this.execute(updateSql, params, 'update blobs', txn) + const data: UpdateBlobsPatchData = { operation: 'update', blobs } + await this.createPatch(cardId, messageId, PatchType.blob, data, socialId, date, txn) + }) + } else { + await this.execute(updateSql, params, 'update blobs') + } + } + async attachLinkPreviews ( cardId: CardID, messageId: MessageID, diff --git a/packages/sdk-types/src/db.ts b/packages/sdk-types/src/db.ts index b651db6dcb..820ee92746 100644 --- a/packages/sdk-types/src/db.ts +++ b/packages/sdk-types/src/db.ts @@ -44,7 +44,7 @@ import { NotificationType, BlobData, LinkPreviewData, - LinkPreviewID + LinkPreviewID, BlobUpdateData } from '@hcengineering/communication-types' export interface DbAdapter { @@ -76,6 +76,7 @@ export interface DbAdapter { attachBlobs: (cardId: CardID, messageId: MessageID, data: BlobData[], socialId: SocialID, date: Date) => Promise detachBlobs: (card: CardID, messageId: MessageID, blobId: BlobID[], socialId: SocialID, date: Date) => Promise setBlobs: (cardId: CardID, messageId: MessageID, data: BlobData[], socialId: SocialID, date: Date) => Promise + updateBlobs: (cardId: CardID, messageId: MessageID, data: BlobUpdateData[], socialId: SocialID, date: Date) => Promise attachLinkPreviews: ( cardId: CardID, diff --git a/packages/sdk-types/src/events/message.ts b/packages/sdk-types/src/events/message.ts index 06000715a0..28a50e0b82 100644 --- a/packages/sdk-types/src/events/message.ts +++ b/packages/sdk-types/src/events/message.ts @@ -10,7 +10,8 @@ import type { MessagesGroup, MessageExtra, BlobData, - LinkPreviewData + LinkPreviewData, + BlobUpdateData } from '@hcengineering/communication-types' import type { BaseEvent } from './common' @@ -134,6 +135,11 @@ export interface SetBlobsOperation { blobs: BlobData[] } +export interface UpdateBlobsOperation { + opcode: 'update' + blobs: BlobUpdateData[] +} + // For system and message author export interface BlobPatchEvent extends BaseEvent { type: MessageEventType.BlobPatch @@ -141,7 +147,7 @@ export interface BlobPatchEvent extends BaseEvent { cardId: CardID messageId: MessageID - operations: (AttachBlobsOperation | DetachBlobsOperation | SetBlobsOperation)[] + operations: (AttachBlobsOperation | DetachBlobsOperation | SetBlobsOperation | UpdateBlobsOperation)[] socialId: SocialID date?: Date diff --git a/packages/server/src/middleware/db.ts b/packages/server/src/middleware/db.ts index e05bdb3229..b3374ac99b 100644 --- a/packages/server/src/middleware/db.ts +++ b/packages/server/src/middleware/db.ts @@ -271,6 +271,8 @@ export class DatabaseMiddleware extends BaseMiddleware implements Middleware { await this.db.detachBlobs(event.cardId, event.messageId, operation.blobIds, event.socialId, event.date) } else if (operation.opcode === 'set') { await this.db.setBlobs(event.cardId, event.messageId, operation.blobs, event.socialId, event.date) + } else if (operation.opcode === 'update') { + await this.db.updateBlobs(event.cardId, event.messageId, operation.blobs, event.socialId, event.date) } } diff --git a/packages/server/src/middleware/validate.ts b/packages/server/src/middleware/validate.ts index d3193dfdc8..f734275c54 100644 --- a/packages/server/src/middleware/validate.ts +++ b/packages/server/src/middleware/validate.ts @@ -167,6 +167,14 @@ const BlobDataSchema = z.object({ metadata: z.record(z.string(), z.any()).optional() }) +const UpdateBlobDataSchema = z.object({ + blobId: BlobIDSchema, + mimeType: z.string().optional(), + fileName: z.string().optional(), + size: z.number().optional(), + metadata: z.record(z.string(), z.any()).optional() +}) + const LinkPreviewDataSchema = z .object({ previewId: LinkPreviewIDSchema, @@ -329,7 +337,8 @@ const ReactionPatchEventSchema = BaseEventSchema.extend({ const BlobOperationSchema = z.union([ z.object({ opcode: z.literal('attach'), blobs: z.array(BlobDataSchema).nonempty() }), z.object({ opcode: z.literal('detach'), blobIds: z.array(BlobIDSchema).nonempty() }), - z.object({ opcode: z.literal('set'), blobs: z.array(BlobDataSchema).nonempty() }) + z.object({ opcode: z.literal('set'), blobs: z.array(BlobDataSchema).nonempty() }), + z.object({ opcode: z.literal('update'), blobs: z.array(UpdateBlobDataSchema).nonempty() }) ]) const BlobPatchEventSchema = BaseEventSchema.extend({ diff --git a/packages/shared/src/patch.ts b/packages/shared/src/patch.ts index 8c04113ab2..7bdabb5fdc 100644 --- a/packages/shared/src/patch.ts +++ b/packages/shared/src/patch.ts @@ -28,7 +28,8 @@ import { PatchType, ReactionPatch, SocialID, - ThreadPatch + ThreadPatch, + BlobUpdateData } from '@hcengineering/communication-types' export function applyPatches (message: Message, patches: Patch[], allowedPatchTypes: PatchType[] = []): Message { @@ -85,6 +86,8 @@ function patchBlobs (message: Message, patch: BlobPatch): Message { return detachBlobs(message, patch.data.blobIds) } else if (patch.data.operation === 'set') { return setBlobs(message, patch.data.blobs, patch.created, patch.creator) + } else if (patch.data.operation === 'update') { + return updateBlobs(message, patch.data.blobs) } return message } @@ -149,6 +152,27 @@ function attachBlobs (message: Message, data: BlobData[], created: Date, creator } } +function updateBlobs (message: Message, updates: BlobUpdateData[]): Message { + if (updates.length === 0) return message + const updatedBlobs = [] + for (const blob of message.blobs) { + const update = updates.find((it) => it.blobId === blob.blobId) + if (update === undefined) { + updatedBlobs.push(blob) + } else { + updatedBlobs.push({ + ...blob, + ...update + }) + } + } + + return { + ...message, + blobs: updatedBlobs + } +} + function detachBlobs (message: Message, blobIds: BlobID[]): Message { const blobs = message.blobs.filter((it) => !blobIds.includes(it.blobId)) if (blobs.length === message.blobs.length) return message diff --git a/packages/shared/src/processor.ts b/packages/shared/src/processor.ts index f09e1fa917..0a96908bd3 100644 --- a/packages/shared/src/processor.ts +++ b/packages/shared/src/processor.ts @@ -27,6 +27,7 @@ import { PatchType, SetBlobsPatchData, SetLinkPreviewsPatchData, + UpdateBlobsPatchData, UpdateThreadPatchData } from '@hcengineering/communication-types' import { @@ -43,6 +44,7 @@ import { RemoveNotificationContextEvent, SetBlobsOperation, SetLinkPreviewsOperation, + UpdateBlobsOperation, UpdateNotificationContextEvent, UpdateThreadOperation } from '@hcengineering/communication-sdk-types' @@ -216,8 +218,8 @@ export class NotificationProcessor { } function blobOperationToPatchData ( - operation: AttachBlobsOperation | DetachBlobsOperation | SetBlobsOperation -): AttachBlobsPatchData | DetachBlobsPatchData | SetBlobsPatchData | undefined { + operation: AttachBlobsOperation | DetachBlobsOperation | SetBlobsOperation | UpdateBlobsOperation +): AttachBlobsPatchData | DetachBlobsPatchData | SetBlobsPatchData | UpdateBlobsPatchData | undefined { if (operation.opcode === 'attach') { return { operation: 'attach', @@ -233,6 +235,11 @@ function blobOperationToPatchData ( operation: 'set', blobs: operation.blobs } + } else if (operation.opcode === 'update') { + return { + operation: 'update', + blobs: operation.blobs + } } return undefined diff --git a/packages/types/src/message.ts b/packages/types/src/message.ts index d60b1853f5..633a405ddd 100644 --- a/packages/types/src/message.ts +++ b/packages/types/src/message.ts @@ -151,7 +151,7 @@ export interface RemoveReactionPatchData { export interface BlobPatch extends BasePatch { type: PatchType.blob - data: AttachBlobsPatchData | DetachBlobsPatchData | SetBlobsPatchData + data: AttachBlobsPatchData | DetachBlobsPatchData | SetBlobsPatchData | UpdateBlobsPatchData } export interface AttachBlobsPatchData { @@ -169,6 +169,11 @@ export interface SetBlobsPatchData { blobs: BlobData[] } +export interface UpdateBlobsPatchData { + operation: 'update' + blobs: BlobUpdateData[] +} + export interface LinkPreviewPatch extends BasePatch { type: PatchType.linkPreview data: AttachLinkPreviewsPatchData | DetachLinkPreviewsPatchData | SetLinkPreviewsPatchData @@ -233,6 +238,8 @@ export interface BlobData { metadata?: BlobMetadata } +export type BlobUpdateData = { blobId: BlobID } & Partial + export interface AttachedBlob extends BlobData { creator: SocialID created: Date