Init directs (#95)

Signed-off-by: Kristina Fefelova <kristin.fefelova@gmail.com>
This commit is contained in:
Kristina
2025-08-25 09:50:39 +04:00
committed by GitHub
parent f7d1b98df4
commit 407c52ca43
29 changed files with 711 additions and 61 deletions
+42 -3
View File
@@ -45,7 +45,8 @@ import {
type BlobID,
type AttachmentData,
type AttachmentID,
type AttachmentUpdateData, WithTotal
type AttachmentUpdateData, WithTotal, PeerKind, PeerExtra,
FindPeersParams, Peer, FindThreadParams
} from '@hcengineering/communication-types'
import type {
DbAdapter,
@@ -66,11 +67,13 @@ import { formatName } from './utils'
import { initSchema } from './init'
import { LabelsDb } from './db/label'
import { SqlClient } from './client'
import { PeersDb } from './db/peer'
export class CockroachAdapter implements DbAdapter {
private readonly message: MessagesDb
private readonly notification: NotificationsDb
private readonly label: LabelsDb
private readonly peer: PeersDb
constructor (
private readonly sql: SqlClient,
@@ -81,6 +84,7 @@ export class CockroachAdapter implements DbAdapter {
this.message = new MessagesDb(this.sql, this.workspace, logger, options)
this.notification = new NotificationsDb(this.sql, this.workspace, logger, options)
this.label = new LabelsDb(this.sql, this.workspace, logger, options)
this.peer = new PeersDb(this.sql, this.workspace, logger, options)
}
async createMessage (
@@ -201,8 +205,8 @@ export class CockroachAdapter implements DbAdapter {
return await this.message.findMessagesGroups(params)
}
async findThread (thread: CardID): Promise<Thread | undefined> {
return await this.message.findThread(thread)
async findThreads (params: FindThreadParams): Promise<Thread[]> {
return await this.message.findThreads(params)
}
async addCollaborators (
@@ -316,6 +320,28 @@ export class CockroachAdapter implements DbAdapter {
return this.label.updateLabels(card, updates)
}
async createPeer (
workspaceId: WorkspaceID,
cardId: CardID,
kind: PeerKind,
value: string,
extra: PeerExtra,
date: Date
): Promise<void> {
await this.peer.createPeer(workspaceId, cardId, kind, value, extra, date)
}
async removePeer (workspaceId: WorkspaceID,
cardId: CardID,
kind: PeerKind,
value: string): Promise<void> {
await this.peer.removePeer(workspaceId, cardId, kind, value)
}
findPeers (params: FindPeersParams): Promise<Peer[]> {
return this.peer.findPeers(params)
}
async getAccountsByPersonIds (ids: string[]): Promise<AccountID[]> {
if (ids.length === 0) return []
const sql = `SELECT data ->> 'personUuid' AS "personUuid"
@@ -350,6 +376,19 @@ export class CockroachAdapter implements DbAdapter {
return result[0]?.title
}
// TODO: remove later
async getCardSpaceMembers (cardId: CardID): Promise<AccountID[]> {
const sql = `SELECT s.members
FROM public.space AS s
JOIN public.card AS c ON c.space = s._id
WHERE c."workspaceId" = $1::uuid
AND c."_id" = $2::text
LIMIT 1`
const result = await this.sql.execute(sql, [this.workspace, cardId])
return result[0]?.members ?? []
}
async getMessageCreated (cardId: CardID, messageId: MessageID): Promise<Date | undefined> {
return await this.message.getMessageCreated(cardId, messageId)
}
+32 -1
View File
@@ -35,7 +35,10 @@ import {
type AccountID,
type MessageExtra,
AttachmentID,
Attachment
Attachment,
Peer,
WorkspaceID,
PeerExtra
} from '@hcengineering/communication-types'
import { Domain } from '@hcengineering/communication-sdk-types'
import { applyPatches } from '@hcengineering/communication-shared'
@@ -296,3 +299,31 @@ export function toLabel (raw: DbModel<Domain.Label>): Label {
created: new Date(raw.created)
}
}
export function toPeer (
raw: DbModel<Domain.Peer> & { members?: { workspace_id: WorkspaceID, card_id: CardID, extra?: PeerExtra }[] }
): Peer {
const peer: Peer = {
workspaceId: raw.workspace_id,
cardId: raw.card_id,
kind: raw.kind,
value: raw.value,
extra: raw.extra,
created: new Date(raw.created)
}
if (peer.kind === 'card') {
return {
...peer,
kind: 'card',
members:
raw.members?.map((it) => ({
workspaceId: it.workspace_id,
cardId: it.card_id,
extra: it.extra ?? {}
})) ?? []
}
}
return peer
}
+46 -14
View File
@@ -25,6 +25,7 @@ import {
type CardType,
type FindMessagesGroupsParams,
type FindMessagesParams,
FindThreadParams,
type Markdown,
type Message,
type MessageExtra,
@@ -753,21 +754,52 @@ export class MessagesDb extends BaseDb {
return { where: `WHERE ${where.join(' AND ')}`, values }
}
// Find thread
async findThread (thread: CardID): Promise<Thread | undefined> {
const sql = `SELECT t.card_id,
t.message_id::text,
t.thread_id,
t.thread_type,
t.replies_count::int,
t.last_reply
FROM ${Domain.Thread} t
WHERE t.workspace_id = $1::uuid
AND t.thread_id = $2::varchar
LIMIT 1;`
// Find threads
async findThreads (params: FindThreadParams): Promise<Thread[]> {
const { where, values } = this.buildThreadWhere(params)
const select = `
SELECT *
FROM ${Domain.Thread} t
`
const result = await this.execute(sql, [this.workspace, thread], 'find thread')
return result.map((it: any) => toThread(it))[0]
const limit = params.limit != null ? ` LIMIT ${params.limit}` : ''
const orderBy =
params.order != null ? `ORDER BY t.date ${params.order === SortingOrder.Ascending ? 'ASC' : 'DESC'}` : ''
const sql = [select, where, orderBy, limit].join(' ')
const result = await this.execute(sql, values, 'find threads')
return result.map((it: any) => toThread(it))
}
private buildThreadWhere (
params: FindThreadParams,
startIndex: number = 0,
prefix: string = 't.'
): { where: string, values: any[] } {
const where: string[] = []
const values: any[] = []
let index = startIndex + 1
where.push(`${prefix}workspace_id = $${index++}::uuid`)
values.push(this.workspace)
if (params.cardId != null) {
where.push(`${prefix}card_id = $${index++}::varchar`)
values.push(params.cardId)
}
if (params.messageId != null) {
where.push(`${prefix}message_id = $${index++}::varchar`)
values.push(params.messageId)
}
if (params.threadId != null) {
where.push(`${prefix}thread_id = $${index++}::varchar`)
values.push(params.threadId)
}
return { where: `WHERE ${where.join(' AND ')}`, values }
}
// Find messages groups
+135
View File
@@ -0,0 +1,135 @@
//
// Copyright © 2025 Hardcore Engineering Inc.
//
// Licensed under the Eclipse Public License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License. You may
// obtain a copy of the License at https://www.eclipse.org/legal/epl-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
//
// See the License for the specific language governing permissions and
// limitations under the License.
//
import {
WorkspaceID,
type CardID,
PeerKind,
PeerExtra,
FindPeersParams,
SortingOrder,
Peer
} from '@hcengineering/communication-types'
import { Domain } from '@hcengineering/communication-sdk-types'
import { BaseDb } from './base'
import { DbModel, DbModelFilter } from '../schema'
import { toPeer } from './mapping'
export class PeersDb extends BaseDb {
async createPeer (
workspaceId: WorkspaceID,
cardId: CardID,
kind: PeerKind,
value: string,
extra: PeerExtra,
date: Date
): Promise<void> {
const db: DbModel<Domain.Peer> = {
workspace_id: workspaceId,
card_id: cardId,
kind,
value,
extra,
created: date
}
const { sql, values } = this.getInsertSql(Domain.Peer, db, [])
await this.execute(sql, values, 'insert peer')
}
async removePeer (workspaceId: WorkspaceID, cardId: CardID, kind: PeerKind, value: string): Promise<void> {
const filter: DbModelFilter<Domain.Peer> = [
{
column: 'workspace_id',
value: workspaceId
},
{
column: 'card_id',
value: cardId
},
{
column: 'kind',
value: kind
},
{
column: 'value',
value
}
]
if (filter.length === 0) return
const { sql, values } = this.getDeleteSql(Domain.Peer, filter)
await this.execute(sql, values, 'remove peer')
}
async findPeers (params: FindPeersParams): Promise<Peer[]> {
const select = `SELECT *, COALESCE(members.members, '[]') AS members
FROM ${Domain.Peer} p`
const { where, values } = this.buildWhere(params)
const limit = params.limit != null ? `LIMIT ${params.limit}` : ''
const orderBy =
params.order != null ? `ORDER BY p.created ${params.order === SortingOrder.Ascending ? 'ASC' : 'DESC'}` : ''
const join = `LEFT JOIN LATERAL (
SELECT json_agg(
json_build_object(
'workspace_id', p2.workspace_id,
'card_id', p2.card_id,
'extra', p2.extra
)
) AS members
FROM ${Domain.Peer} AS p2
WHERE p2.value = p.value
AND p2.kind = 'card'
AND NOT (p2.workspace_id = p.workspace_id AND p2.card_id = p.card_id)
) members ON true`
const sql = [select, join, where, orderBy, limit].join(' ')
const result = await this.execute(sql, values, 'find peers')
return result.map((it: any) => toPeer(it))
}
buildWhere (params: FindPeersParams, startIndex: number = 0, prefix = 'p.'): { where: string, values: any[] } {
const where: string[] = []
const values: any[] = []
let index = startIndex + 1
if (params.workspaceId != null) {
where.push(`${prefix}workspace_id = $${index++}::uuid`)
values.push(params.workspaceId)
}
if (params.cardId != null) {
where.push(`${prefix}card_id = $${index++}::varchar`)
values.push(params.cardId)
}
if (params.kind != null) {
where.push(`${prefix}kind = $${index++}::varchar`)
values.push(params.kind)
}
if (params.value != null) {
where.push(`${prefix}value = $${index++}::varchar`)
values.push(params.value)
}
return { where: where.length > 0 ? `WHERE ${where.join(' AND ')}` : '', values }
}
}
+40 -12
View File
@@ -129,7 +129,8 @@ function getMigrations (): [string, string][] {
migrationV7_3(),
migrationV8_1(),
migrationV8_2(),
migrationV8_3()
migrationV8_3(),
migrationV9_1()
]
}
@@ -497,7 +498,8 @@ function migrationV6_7 (): [string, string] {
CREATE INDEX IF NOT EXISTS idx_reactions_workspace_card_message
ON communication.reactions (workspace_id, card_id, message_id);
ALTER TABLE communication.thread ADD CONSTRAINT thread_unique_constraint UNIQUE (workspace_id, card_id, message_id);
ALTER TABLE communication.thread
ADD CONSTRAINT thread_unique_constraint UNIQUE (workspace_id, card_id, message_id);
CREATE INDEX IF NOT EXISTS idx_thread_workspace_card_message
ON communication.thread (workspace_id, card_id, message_id);
@@ -541,9 +543,8 @@ function migrationV7_2 (): [string, string] {
FROM communication.notification_context AS nc
JOIN communication.messages_groups AS mg
ON mg.workspace_id = nc.workspace_id
AND mg.card_id = nc.card_id
WHERE
n.context_id = nc.id
AND mg.card_id = nc.card_id
WHERE n.context_id = nc.id
AND n.message_created BETWEEN mg.from_date AND mg.to_date
AND n.blob_id IS NULL;
`
@@ -552,12 +553,12 @@ function migrationV7_2 (): [string, string] {
function migrationV7_3 (): [string, string] {
const sql = `
UPDATE communication.notification_context
SET last_notify = last_update
WHERE last_notify IS NULL;
UPDATE communication.notification_context
SET last_notify = last_update
WHERE last_notify IS NULL;
ALTER TABLE communication.notification_context
ALTER COLUMN last_notify SET NOT NULL;
ALTER TABLE communication.notification_context
ALTER COLUMN last_notify SET NOT NULL;
`
return ['make_last_notify_not_null-v7_3', sql]
}
@@ -600,7 +601,34 @@ function migrationV8_2 (): [string, string] {
function migrationV8_3 (): [string, string] {
const sql = `
CREATE INDEX IF NOT EXISTS attachment_workspace_card_message_idx ON ${Domain.Attachment} (workspace_id, card_id, message_id)
`
CREATE INDEX IF NOT EXISTS attachment_workspace_card_message_idx ON ${Domain.Attachment} (workspace_id, card_id, message_id)
`
return ['add_attachment_indexes-v8_3', sql]
}
// CREATE TABLE ${Domain.CardPeerGroup}
// (
// group_id UUID NOT NULL,
// workspace_id UUID NOT NULL,
// card_id VARCHAR(255) NOT NULL,
// created TIMESTAMPTZ NOT NULL DEFAULT now(),
// PRIMARY KEY (group_id,workspace_id, card_id)
// );
function migrationV9_1 (): [string, string] {
const sql = `
CREATE TABLE IF NOT EXISTS ${Domain.Peer}
(
workspace_id UUID NOT NULL,
card_id VARCHAR(255) NOT NULL,
kind TEXT NOT NULL,
value TEXT NOT NULL,
extra JSONB NOT NULL DEFAULT '{}',
created TIMESTAMPTZ NOT NULL DEFAULT now(),
PRIMARY KEY (workspace_id, card_id, kind, value)
);
CREATE INDEX IF NOT EXISTS peer_workspace_card_kind ON ${Domain.Peer} (workspace_id, card_id, kind);
CREATE INDEX IF NOT EXISTS peer_kind_value ON ${Domain.Peer} (kind, value);`
return ['init_peer_tables-v9_1', sql]
}
+20 -2
View File
@@ -28,7 +28,8 @@ import {
type LabelID,
type CardType,
NotificationContent,
NotificationType, AttachmentID
NotificationType, AttachmentID,
PeerKind, PeerExtra
} from '@hcengineering/communication-types'
import { Domain } from '@hcengineering/communication-sdk-types'
@@ -130,6 +131,14 @@ export const schemas = {
last_view: 'timestamptz',
last_update: 'timestamptz',
last_notify: 'timestamptz'
},
[Domain.Peer]: {
workspace_id: 'uuid',
card_id: 'varchar',
kind: 'varchar',
value: 'varchar',
extra: 'jsonb',
created: 'timestamptz'
}
} as const
@@ -141,11 +150,11 @@ export interface DomainDbModel {
[Domain.Reaction]: ReactionDbModel
[Domain.Thread]: ThreadDbModel
[Domain.Attachment]: AttachmentDbModel
// [Domain.LinkPreview]: LinkPreviewDbModel
[Domain.Notification]: NotificationDbModel
[Domain.NotificationContext]: ContextDbModel
[Domain.Collaborator]: CollaboratorDbModel
[Domain.Label]: LabelDbModel
[Domain.Peer]: PeerDbModel
}
export type DbModel<D extends keyof DomainDbModel> = DomainDbModel[D]
@@ -274,3 +283,12 @@ interface LabelDbModel {
account: AccountID
created: Date
}
interface PeerDbModel {
workspace_id: WorkspaceID
card_id: CardID
kind: PeerKind
value: string
extra: PeerExtra
created: Date
}
+19 -2
View File
@@ -44,7 +44,7 @@ import {
NotificationType,
AttachmentData,
AttachmentID,
AttachmentUpdateData, WithTotal
AttachmentUpdateData, WithTotal, WorkspaceID, PeerKind, PeerExtra, FindPeersParams, Peer, FindThreadParams
} from '@hcengineering/communication-types'
export interface DbAdapter {
@@ -82,9 +82,25 @@ export interface DbAdapter {
removeThreads: (query: ThreadQuery) => Promise<void>
updateThread: (cardId: CardID, messageId: MessageID, thread: CardID, update: ThreadUpdates, socialId: SocialID, date: Date) => Promise<void>
createPeer: (
workspaceId: WorkspaceID,
cardId: CardID,
kind: PeerKind,
value: string,
extra: PeerExtra,
date: Date
) => Promise<void>
removePeer: (workspaceId: WorkspaceID,
cardId: CardID,
kind: PeerKind,
value: string) => Promise<void>
findPeers: (params: FindPeersParams) => Promise<Peer[]>
findMessages: (params: FindMessagesParams) => Promise<Message[]>
findMessagesGroups: (params: FindMessagesGroupsParams) => Promise<MessagesGroup[]>
findThread: (threadId: CardID) => Promise<Thread | undefined>
findThreads: (params: FindThreadParams) => Promise<Thread[]>
addCollaborators: (cardId: CardID, cardType: CardType, collaborators: AccountID[], date: Date) => Promise<AccountID[]>
removeCollaborators: (cardId: CardID, accounts: AccountID[], unsafe?: boolean) => Promise<void>
@@ -126,6 +142,7 @@ export interface DbAdapter {
updateLabels: (cardId: CardID, update: LabelUpdates) => Promise<void>
getCardTitle: (cardId: CardID) => Promise<string | undefined>
getCardSpaceMembers: (cardId: CardID) => Promise<AccountID[]>
getAccountsByPersonIds: (ids: string[]) => Promise<AccountID[]>
getNameByAccount: (id: AccountID) => Promise<string | undefined>
getMessageCreated: (cardId: CardID, messageId: MessageID) => Promise<Date | undefined>
+2 -1
View File
@@ -26,7 +26,8 @@ export enum Domain {
Collaborator = 'communication.collaborator',
Label = 'communication.label',
// LinkPreview = 'communication.link_preview'
Peer = 'communication.peer'
}
export const Domains = Object.values(Domain)
-2
View File
@@ -18,14 +18,12 @@ import type { CardID, CardType, SocialID } from '@hcengineering/communication-ty
import type { BaseEvent } from './common'
export enum CardEventType {
// Internal
UpdateCardType = 'updateCardType',
RemoveCard = 'removeCard'
}
export type CardEvent = UpdateCardTypeEvent | RemoveCardEvent
// Internal
export interface UpdateCardTypeEvent extends BaseEvent {
type: CardEventType.UpdateCardType
cardId: CardID
+4 -2
View File
@@ -15,13 +15,15 @@ import type { LabelEvent, LabelEventType } from './label'
import type { MessageEventResult, MessageEventType, MessageEvent } from './message'
import type { NotificationEventResult, NotificationEvent, NotificationEventType } from './notification'
import type { CardEvent, CardEventType } from './card'
import { PeerEvent, PeerEventType } from './peer'
export * from './message'
export * from './notification'
export * from './label'
export * from './card'
export * from './peer'
export type EventType = MessageEventType | NotificationEventType | LabelEventType | CardEventType
export type Event = MessageEvent | NotificationEvent | LabelEvent | CardEvent
export type EventType = MessageEventType | NotificationEventType | LabelEventType | CardEventType | PeerEventType
export type Event = MessageEvent | NotificationEvent | LabelEvent | CardEvent | PeerEvent
// eslint-disable-next-line @typescript-eslint/ban-types
export type EventResult = MessageEventResult | NotificationEventResult | {}
+44
View File
@@ -0,0 +1,44 @@
//
// Copyright © 2025 Hardcore Engineering Inc.
//
// Licensed under the Eclipse Public License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License. You may
// obtain a copy of the License at https://www.eclipse.org/legal/epl-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
//
// See the License for the specific language governing permissions and
// limitations under the License.
//
import type { BaseEvent } from './common'
import { CardID, PeerKind, PeerExtra, WorkspaceID } from '@hcengineering/communication-types'
// Peer events only for system
export enum PeerEventType {
CreatePeer = 'createPeer',
RemovePeer = 'removePeer'
}
export type PeerEvent = CreatePeerEvent | RemovePeerEvent
export interface CreatePeerEvent extends BaseEvent {
type: PeerEventType.CreatePeer
workspaceId: WorkspaceID
cardId: CardID
kind: PeerKind
value: string
extra?: PeerExtra
date?: Date
}
export interface RemovePeerEvent extends BaseEvent {
type: PeerEventType.RemovePeer
workspaceId: WorkspaceID
cardId: CardID
kind: PeerKind
value: string
date?: Date
}
+5 -1
View File
@@ -25,7 +25,8 @@ import type {
FindLabelsParams,
Label,
FindCollaboratorsParams,
Collaborator
Collaborator,
FindThreadParams, Thread, FindPeersParams, Peer
} from '@hcengineering/communication-types'
import type { Account, MeasureContext } from '@hcengineering/core'
@@ -61,6 +62,9 @@ export interface ServerApi {
findLabels: (session: SessionData, params: FindLabelsParams) => Promise<Label[]>
findCollaborators: (session: SessionData, params: FindCollaboratorsParams) => Promise<Collaborator[]>
findThreads: (session: SessionData, params: FindThreadParams) => Promise<Thread[]>
findPeers: (session: SessionData, params: FindPeersParams) => Promise<Peer[]>
event: (session: SessionData, event: Event) => Promise<EventResult>
unsubscribeQuery: (session: SessionData, id: number) => Promise<void>
+12 -2
View File
@@ -27,7 +27,8 @@ import type {
FindLabelsParams,
Label,
FindCollaboratorsParams,
Collaborator
Collaborator, FindPeersParams, Peer, Thread,
FindThreadParams
} from '@hcengineering/communication-types'
import { createDbAdapter } from '@hcengineering/communication-cockroach'
import type { EventResult, Event, ServerApi, SessionData } from '@hcengineering/communication-sdk-types'
@@ -52,8 +53,9 @@ export class Api implements ServerApi {
withLogs: process.env.COMMUNICATION_TIME_LOGGING_ENABLED === 'true'
})
const peers = await db.findPeers({ workspaceId: workspace })
const metadata = getMetadata()
const middleware = await buildMiddlewares(ctx, workspace, metadata, db, callbacks)
const middleware = await buildMiddlewares(ctx, workspace, metadata, db, callbacks, peers)
return new Api(ctx, middleware)
}
@@ -90,6 +92,14 @@ export class Api implements ServerApi {
return await this.middlewares.findCollaborators(session, params)
}
async findPeers (session: SessionData, params: FindPeersParams): Promise<Peer[]> {
return await this.middlewares.findPeers(session, params)
}
async findThreads (session: SessionData, params: FindThreadParams): Promise<Thread[]> {
return await this.middlewares.findThreads(session, params)
}
async unsubscribeQuery (session: SessionData, id: number): Promise<void> {
await this.middlewares.unsubscribeQuery(session, id)
}
+27 -1
View File
@@ -26,7 +26,11 @@ import type {
FindLabelsParams,
Label,
FindCollaboratorsParams,
Collaborator
Collaborator,
FindPeersParams,
Peer,
FindThreadParams,
Thread
} from '@hcengineering/communication-types'
import type { Enriched, Middleware, MiddlewareContext, QueryId } from '../types'
@@ -73,6 +77,14 @@ export class BaseMiddleware implements Middleware {
return await this.provideFindCollaborators(session, params)
}
async findPeers (session: SessionData, params: FindPeersParams): Promise<Peer[]> {
return await this.provideFindPeers(session, params)
}
async findThreads (session: SessionData, params: FindThreadParams): Promise<Thread[]> {
return await this.provideFindThreads(session, params)
}
async event (session: SessionData, event: Enriched<Event>, derived: boolean): Promise<EventResult> {
return await this.provideEvent(session, event, derived)
}
@@ -162,6 +174,20 @@ export class BaseMiddleware implements Middleware {
return []
}
protected async provideFindPeers (session: SessionData, params: FindPeersParams): Promise<Peer[]> {
if (this.next !== undefined) {
return await this.next.findPeers(session, params)
}
return []
}
protected async provideFindThreads (session: SessionData, params: FindThreadParams): Promise<Thread[]> {
if (this.next !== undefined) {
return await this.next.findThreads(session, params)
}
return []
}
protected provideHandleBroadcast (session: SessionData, events: Enriched<Event>[]): void {
if (this.next !== undefined) {
this.next.handleBroadcast(session, events)
@@ -20,6 +20,7 @@ import {
LabelEventType,
MessageEventType,
NotificationEventType,
PeerEventType,
type SessionData
} from '@hcengineering/communication-sdk-types'
import type {
@@ -223,6 +224,9 @@ export class BroadcastMiddleware extends BaseMiddleware implements Middleware {
case CardEventType.UpdateCardType:
case CardEventType.RemoveCard:
return true
case PeerEventType.RemovePeer:
case PeerEventType.CreatePeer:
return false
}
}
+2
View File
@@ -34,6 +34,8 @@ export class DateMiddleware extends BaseMiddleware implements Middleware {
event.date = new Date()
}
event._eventExtra = {}
return await this.provideEvent(session, event, derived)
}
+35 -2
View File
@@ -23,6 +23,8 @@ import {
type FindMessagesParams,
type FindNotificationContextParams,
type FindNotificationsParams,
FindPeersParams,
FindThreadParams,
type Label,
type Message,
MessageID,
@@ -30,7 +32,9 @@ import {
type Notification,
type NotificationContext,
PatchType,
Peer,
SocialID,
Thread,
UpdatePatchData
} from '@hcengineering/communication-types'
import {
@@ -62,7 +66,10 @@ import {
ThreadPatchEvent,
EventResult,
AttachmentPatchEvent,
BlobPatchEvent
BlobPatchEvent,
PeerEventType,
CreatePeerEvent,
RemovePeerEvent
} from '@hcengineering/communication-sdk-types'
import type { Enriched, Middleware, MiddlewareContext } from '../types'
@@ -109,11 +116,21 @@ export class DatabaseMiddleware extends BaseMiddleware implements Middleware {
return await this.db.findCollaborators(params)
}
async event (session: SessionData, event: Enriched<Event>): Promise<EventResult> {
async findPeers (_: SessionData, params: FindPeersParams): Promise<Peer[]> {
return await this.db.findPeers(params)
}
async findThreads (_: SessionData, params: FindThreadParams): Promise<Thread[]> {
return await this.db.findThreads(params)
}
async event (session: SessionData, event: Enriched<Event>, derived: boolean): Promise<EventResult> {
const result = await this.processEvent(session, event)
if (result.skipPropagate === true) {
event.skipPropagate = true
} else {
await this.provideEvent(session, event, derived)
}
return result.result ?? {}
@@ -153,6 +170,12 @@ export class DatabaseMiddleware extends BaseMiddleware implements Middleware {
case CardEventType.RemoveCard:
return await this.removeCard(event)
// Peers
case PeerEventType.RemovePeer:
return await this.removePeer(event)
case PeerEventType.CreatePeer:
return await this.createPeer(event)
// Collaborators
case NotificationEventType.AddCollaborators:
return await this.addCollaborators(event)
@@ -460,6 +483,16 @@ export class DatabaseMiddleware extends BaseMiddleware implements Middleware {
return {}
}
private async createPeer (event: Enriched<CreatePeerEvent>): Promise<Result> {
await this.db.createPeer(event.workspaceId, event.cardId, event.kind, event.value, event.extra ?? {}, event.date)
return {}
}
private async removePeer (event: Enriched<RemovePeerEvent>): Promise<Result> {
await this.db.removePeer(event.workspaceId, event.cardId, event.kind, event.value)
return {}
}
private async updateCardType (event: Enriched<UpdateCardTypeEvent>): Promise<Result> {
return {}
}
+60
View File
@@ -0,0 +1,60 @@
//
// Copyright © 2025 Hardcore Engineering Inc.
//
// Licensed under the Eclipse Public License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License. You may
// obtain a copy of the License at https://www.eclipse.org/legal/epl-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
//
// See the License for the specific language governing permissions and
// limitations under the License.
//
import {
type Event,
EventResult,
MessageEventType,
PeerEventType,
type SessionData
} from '@hcengineering/communication-sdk-types'
import type { Enriched, Middleware, MiddlewareContext } from '../types'
import { BaseMiddleware } from './base'
export class PeerMiddleware extends BaseMiddleware implements Middleware {
constructor (
readonly context: MiddlewareContext,
next?: Middleware
) {
super(context, next)
}
async event (session: SessionData, event: Enriched<Event>, derived: boolean): Promise<EventResult> {
switch (event.type) {
case PeerEventType.CreatePeer:
this.context.cadsWithPeers.add(event.cardId)
break
case MessageEventType.CreateMessage:
case MessageEventType.UpdatePatch:
case MessageEventType.RemovePatch:
case MessageEventType.AttachmentPatch:
case MessageEventType.ReactionPatch:
case MessageEventType.ThreadPatch:
case MessageEventType.BlobPatch: {
if (this.context.cadsWithPeers.has(event.cardId)) {
event._eventExtra.peers =
(await this.context.head?.findPeers(session, {
workspaceId: this.context.workspace,
cardId: event.cardId
})) ?? []
}
break
}
}
return await this.provideEvent(session, event, derived)
}
}
@@ -19,6 +19,7 @@ import {
EventResult,
MessageEventType,
NotificationEventType,
PeerEventType,
type SessionData
} from '@hcengineering/communication-sdk-types'
import { AccountRole, systemAccountUuid } from '@hcengineering/core'
@@ -66,6 +67,8 @@ export class PermissionsMiddleware extends BaseMiddleware implements Middleware
this.checkAccount(session, event.account)
break
}
case PeerEventType.CreatePeer:
case PeerEventType.RemovePeer:
case MessageEventType.CreateMessagesGroup:
case MessageEventType.RemoveMessagesGroup: {
this.onlySystemAccount(session)
@@ -23,6 +23,7 @@ import { notify } from '../notification/notification'
export class TriggersMiddleware extends BaseMiddleware implements Middleware {
private ctx: MeasureContext
private processedPeersEvents = new Set<string>()
constructor (
private readonly callbacks: CommunicationCallbacks,
@@ -60,6 +61,7 @@ export class TriggersMiddleware extends BaseMiddleware implements Middleware {
registeredCards: this.context.registeredCards,
accountBySocialID: this.context.accountBySocialID,
removedContexts: this.context.removedContexts,
processedPeersEvents: this.processedPeersEvents,
derived,
execute: async (event: Event) => {
// Will be enriched in head
@@ -90,6 +92,7 @@ export class TriggersMiddleware extends BaseMiddleware implements Middleware {
(session.asyncData as Enriched<Event>[]).sort((a, b) => a.date.getTime() - b.date.getTime())
)
session.asyncData = []
this.processedPeersEvents = new Set()
}
}
}
+30 -2
View File
@@ -18,7 +18,8 @@ import {
MessageEventType,
NotificationEventType,
type Event,
type SessionData
type SessionData,
PeerEventType
} from '@hcengineering/communication-sdk-types'
import {
type Collaborator,
@@ -151,11 +152,18 @@ export class ValidateMiddleware extends BaseMiddleware implements Middleware {
case NotificationEventType.UpdateNotificationContext:
this.validate(event, UpdateNotificationContextEventSchema)
break
case PeerEventType.CreatePeer:
this.validate(event, CreatePeerEventSchema)
break
case PeerEventType.RemovePeer:
this.validate(event, RemovePeerEventSchema)
break
}
return await this.provideEvent(session, deserializeEvent(event), derived)
}
}
const WorkspaceIDSchema = z.string().uuid()
const AccountIDSchema = z.string()
const BlobIDSchema = z.string().uuid()
const AttachmentIDSchema = z.string().uuid()
@@ -290,7 +298,8 @@ const FindCollaboratorsParamsSchema = FindParamsSchema.extend({
const BaseEventSchema = z
.object({
_id: z.string().optional()
_id: z.string().optional(),
_eventExtra: z.record(z.any()).optional()
})
.strict()
@@ -472,6 +481,25 @@ const RemoveCollaboratorsEventSchema = BaseEventSchema.extend({
date: DateSchema
}).strict()
const CreatePeerEventSchema = BaseEventSchema.extend({
type: z.literal(PeerEventType.CreatePeer),
workspaceId: WorkspaceIDSchema,
cardId: CardIDSchema,
kind: z.string().nonempty(),
value: z.string().nonempty(),
extra: z.record(z.any()).optional(),
date: DateSchema
}).strict()
const RemovePeerEventSchema = BaseEventSchema.extend({
type: z.literal(PeerEventType.RemovePeer),
workspaceId: WorkspaceIDSchema,
cardId: CardIDSchema,
kind: z.string().nonempty(),
value: z.string().nonempty(),
date: DateSchema
}).strict()
function deserializeEvent (event: Enriched<Event>): Enriched<Event> {
switch (event.type) {
case MessageEventType.CreateMessagesGroup:
+21 -5
View File
@@ -13,7 +13,7 @@
// limitations under the License.
//
import type { MeasureContext } from '@hcengineering/core'
import { MeasureContext } from '@hcengineering/core'
import type { DbAdapter, EventResult, Event, SessionData } from '@hcengineering/communication-sdk-types'
import type {
Collaborator,
@@ -23,11 +23,14 @@ import type {
FindMessagesParams,
FindNotificationContextParams,
FindNotificationsParams,
FindPeersParams,
FindThreadParams,
Label,
Message,
MessagesGroup,
Notification,
NotificationContext,
NotificationContext, Peer,
Thread,
WorkspaceID
} from '@hcengineering/communication-types'
@@ -48,13 +51,14 @@ import { ValidateMiddleware } from './middleware/validate'
import { DateMiddleware } from './middleware/date'
import { IdentityMiddleware } from './middleware/indentity'
import { IdMiddleware } from './middleware/id'
import { PeerMiddleware } from './middleware/peer'
export async function buildMiddlewares (
ctx: MeasureContext,
workspace: WorkspaceID,
metadata: Metadata,
db: DbAdapter,
callbacks: CommunicationCallbacks
callbacks: CommunicationCallbacks, peers: Peer[]
): Promise<Middlewares> {
const createFns: MiddlewareCreateFn[] = [
// Enrich events
@@ -69,7 +73,8 @@ export async function buildMiddlewares (
// Process events
async (context, next) => new TriggersMiddleware(callbacks, db, context, next),
async (context, next) => new BroadcastMiddleware(callbacks, context, next),
async (context, next) => new DatabaseMiddleware(db, context, next)
async (context, next) => new DatabaseMiddleware(db, context, next),
async (context, next) => new PeerMiddleware(context, next)
]
const context: MiddlewareContext = {
@@ -78,7 +83,8 @@ export async function buildMiddlewares (
workspace,
registeredCards: new Set(),
accountBySocialID: new Map(),
removedContexts: new Map()
removedContexts: new Map(),
cadsWithPeers: new Set(peers.map(it => it.cardId))
}
return await Middlewares.create(ctx, context, createFns)
@@ -169,6 +175,16 @@ export class Middlewares {
return await this.head.findCollaborators(session, params)
}
async findPeers (session: SessionData, params: FindPeersParams): Promise<Peer[]> {
if (this.head === undefined) return []
return await this.head.findPeers(session, params)
}
async findThreads (session: SessionData, params: FindThreadParams): Promise<Thread[]> {
if (this.head === undefined) return []
return await this.head.findThreads(session, params)
}
async unsubscribeQuery (session: SessionData, id: number): Promise<void> {
if (this.head === undefined) return
this.head?.unsubscribeQuery(session, id)
@@ -220,6 +220,7 @@ async function notifyMessage (
date: Date
): Promise<Event[]> {
const cursor = ctx.db.getCollaboratorsCursor(cardId, date, BATCH_SIZE)
const spaceMembers = await ctx.db.getCardSpaceMembers(cardId)
const creatorAccount = await findAccount(ctx, socialId)
const result: Event[] = []
@@ -235,6 +236,7 @@ async function notifyMessage (
})
for (const collaborator of collaborators) {
if (!spaceMembers.includes(collaborator)) continue
try {
const context = contexts.find((it) => it.account === collaborator)
const res = await processCollaborator(
+1 -1
View File
@@ -52,7 +52,7 @@ async function onCardTypeUpdates (ctx: TriggerCtx, event: Enriched<UpdateCardTyp
await ctx.db.updateCollaborators({ card: event.cardId }, { cardType: event.cardType })
await ctx.db.updateLabels(event.cardId, { cardType: event.cardType })
const thread = await ctx.db.findThread(event.cardId)
const thread = (await ctx.db.findThreads({ threadId: event.cardId, limit: 1 }))[0]
if (thread === undefined) return []
return [
+50 -5
View File
@@ -23,9 +23,9 @@ import {
RemovePatchEvent,
ThreadPatchEvent
} from '@hcengineering/communication-sdk-types'
import { type CardID, MessageType } from '@hcengineering/communication-types'
import { type CardID, CardPeer, MessageType, Peer } from '@hcengineering/communication-types'
import { generateToken } from '@hcengineering/server-token'
import { type AccountUuid, concatLink, systemAccountUuid } from '@hcengineering/core'
import { type AccountUuid, concatLink, generateId, systemAccountUuid } from '@hcengineering/core'
import { extractReferences } from '@hcengineering/text-core'
import { markdownToMarkup } from '@hcengineering/text-markdown'
@@ -41,7 +41,7 @@ async function onMessagesGroupCreated (ctx: TriggerCtx, event: CreateMessagesGro
async function onMessageRemoved (ctx: TriggerCtx, event: Enriched<RemovePatchEvent>): Promise<Event[]> {
const { cardId } = event
const thread = await ctx.db.findThread(cardId)
const thread = (await ctx.db.findThreads({ threadId: cardId, limit: 1 }))[0]
if (thread === undefined) return []
return [
@@ -126,7 +126,7 @@ async function addThreadReply (ctx: TriggerCtx, event: Enriched<CreateMessageEve
return []
}
const { cardId, socialId, date } = event
const thread = await ctx.db.findThread(cardId)
const thread = (await ctx.db.findThreads({ threadId: cardId, limit: 1 }))[0]
if (thread === undefined) return []
@@ -201,6 +201,43 @@ async function onThreadAttached (ctx: TriggerCtx, event: Enriched<ThreadPatchEve
return result
}
async function checkPeers (ctx: TriggerCtx, event: Enriched<CreateMessageEvent | PatchEvent>): Promise<Event[]> {
if (ctx.processedPeersEvents.has(event._id)) return []
if (event.type === MessageEventType.CreateMessage) {
if (event.messageType === MessageType.Activity) {
return []
}
}
if (event.type === MessageEventType.ThreadPatch) {
return []
}
const cardPeers = new Set(
(((event._eventExtra.peers ?? []) as Peer[]).filter((it) => it.kind === 'card') as CardPeer[])
.flatMap((it) => it.members)
.filter((it) => it.workspaceId === ctx.workspace && it.cardId !== event.cardId)
.map((it) => it.cardId)
)
if (cardPeers.size === 0) return []
const res: Event[] = []
for (const peer of cardPeers) {
const ev = {
...event,
_id: generateId(),
cardId: peer
}
ctx.processedPeersEvents.add(ev._id)
res.push(ev)
}
return res
}
const triggers: Triggers = [
['add_collaborators_on_message_created', MessageEventType.CreateMessage, addCollaborators as TriggerFn],
['add_thread_reply_on_message_created', MessageEventType.CreateMessage, addThreadReply as TriggerFn],
@@ -214,7 +251,15 @@ const triggers: Triggers = [
['on_messages_group_created', MessageEventType.CreateMessagesGroup, onMessagesGroupCreated as TriggerFn],
['remove_reply_on_messages_removed', MessageEventType.RemovePatch, onMessageRemoved as TriggerFn],
['on_thread_created', MessageEventType.ThreadPatch, onThreadAttached as TriggerFn]
['on_thread_created', MessageEventType.ThreadPatch, onThreadAttached as TriggerFn],
['check_peers_on_message_created', MessageEventType.CreateMessage, checkPeers as TriggerFn],
['check_peers_on_update_patch', MessageEventType.UpdatePatch, checkPeers as TriggerFn],
['check_peers_on_remove_patch', MessageEventType.RemovePatch, checkPeers as TriggerFn],
['check_peers_on_reaction_patch', MessageEventType.ReactionPatch, checkPeers as TriggerFn],
['check_peers_on_blob_patch', MessageEventType.BlobPatch, checkPeers as TriggerFn],
['check_peers_on_attachment_patch', MessageEventType.AttachmentPatch, checkPeers as TriggerFn],
['check_peers_on_thread_patch', MessageEventType.ThreadPatch, checkPeers as TriggerFn]
]
export default triggers
+13 -2
View File
@@ -23,19 +23,23 @@ import type {
import type {
AccountID,
CardID,
Collaborator, ContextID,
Collaborator,
ContextID,
FindCollaboratorsParams,
FindLabelsParams,
FindMessagesGroupsParams,
FindMessagesParams,
FindNotificationContextParams,
FindNotificationsParams,
FindPeersParams,
FindThreadParams,
Label,
Message,
MessagesGroup,
Notification,
NotificationContext,
SocialID,
Peer,
SocialID, Thread,
WorkspaceID
} from '@hcengineering/communication-types'
@@ -69,6 +73,8 @@ export interface Middleware {
findLabels: (session: SessionData, params: FindLabelsParams, queryId?: QueryId) => Promise<Label[]>
findCollaborators: (session: SessionData, params: FindCollaboratorsParams) => Promise<Collaborator[]>
findPeers: (session: SessionData, params: FindPeersParams) => Promise<Peer[]>
findThreads: (session: SessionData, params: FindThreadParams) => Promise<Thread[]>
event: (session: SessionData, event: Enriched<Event>, derived: boolean) => Promise<EventResult>
@@ -88,6 +94,8 @@ export interface MiddlewareContext {
accountBySocialID: Map<SocialID, AccountID>
removedContexts: Map<ContextID, NotificationContext>
cadsWithPeers: Set<CardID>
derived?: Middleware
head?: Middleware
}
@@ -110,6 +118,7 @@ export interface TriggerCtx {
accountBySocialID: Map<SocialID, AccountID>
removedContexts: Map<ContextID, NotificationContext>
derived: boolean
processedPeersEvents: Set<string>
execute: (event: Event) => Promise<EventResult>
}
@@ -117,6 +126,8 @@ export type TriggerFn = (ctx: TriggerCtx, event: Enriched<Event>) => Promise<Eve
export type Triggers = [string, EventType, TriggerFn][]
export type Enriched<T> = T & {
_id: string
skipPropagate?: boolean
date: Date
_eventExtra: Record<string, any>
}
+1
View File
@@ -20,3 +20,4 @@ export * from './notification'
export * from './query'
export * from './label'
export * from './patch'
export * from './peer'
+43
View File
@@ -0,0 +1,43 @@
// Copyright © 2025 Hardcore Engineering Inc.
//
// Licensed under the Eclipse Public License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License. You may
// obtain a copy of the License at https://www.eclipse.org/legal/epl-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
//
// See the License for the specific language governing permissions and
// limitations under the License.
import { CardID, WorkspaceID } from './core'
export type PeerKind = 'card' | string
export type PeerExtra = Record<string, any>
interface BasePeer {
workspaceId: WorkspaceID
cardId: CardID
kind: PeerKind
value: string
extra: PeerExtra
created: Date
}
export interface CardPeer extends BasePeer {
kind: 'card'
members: CardPeerMember[]
}
export interface ExternalPeer extends BasePeer {
kind: string
}
export type Peer = CardPeer | ExternalPeer
export interface CardPeerMember {
workspaceId: WorkspaceID
cardId: CardID
extra: PeerExtra
}
+15 -1
View File
@@ -17,8 +17,9 @@ import { SortingOrder } from '@hcengineering/core'
import type { MessageID } from './message'
import type { ContextID, NotificationID, NotificationType } from './notification'
import type { AccountID, BlobID, CardID, CardType } from './core'
import type { AccountID, BlobID, CardID, CardType, WorkspaceID } from './core'
import type { LabelID } from './label'
import { PeerKind } from './peer'
export { SortingOrder }
@@ -101,4 +102,17 @@ export interface FindLabelsParams extends FindParams {
account?: AccountID
}
export interface FindThreadParams extends FindParams {
cardId?: CardID
messageId?: MessageID
threadId?: CardID
}
export interface FindPeersParams extends FindParams {
workspaceId?: WorkspaceID
cardId?: CardID
kind?: PeerKind
value?: string
}
export type WithTotal<T> = T[] & { total: number }