mirror of
https://github.com/hcengineering/platform.git
synced 2026-09-06 17:57:42 +02:00
@@ -1,6 +1,7 @@
|
||||
{
|
||||
"name": "@communication/cockroach",
|
||||
"version": "0.1.0",
|
||||
"main": "src/index.ts",
|
||||
"module": "src/index.ts",
|
||||
"type": "module",
|
||||
"devDependencies": {
|
||||
@@ -8,8 +9,10 @@
|
||||
},
|
||||
"dependencies": {
|
||||
"@communication/types": "workspace:*",
|
||||
"@communication/sdk-types": "workspace:*",
|
||||
"pg": "8.12.0",
|
||||
"postgres": "^3.4.4"
|
||||
"postgres": "^3.4.4",
|
||||
"uuid": "^11.0.3"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"typescript": "^5.6.3"
|
||||
|
||||
@@ -0,0 +1,122 @@
|
||||
import type postgres from 'postgres'
|
||||
import {
|
||||
type Message,
|
||||
type FindMessagesParams,
|
||||
type CardID,
|
||||
type RichText,
|
||||
type SocialID,
|
||||
type MessageID,
|
||||
type ContextID,
|
||||
type NotificationContextUpdate,
|
||||
type FindNotificationContextParams,
|
||||
type NotificationContext,
|
||||
type FindNotificationsParams,
|
||||
type Notification
|
||||
} from '@communication/types'
|
||||
import type { DbAdapter } from '@communication/sdk-types'
|
||||
|
||||
import { MessagesDb } from './db/message'
|
||||
import { NotificationsDb } from './db/notification'
|
||||
import { connect, type PostgresClientReference } from './connection'
|
||||
|
||||
export class CockroachAdapter implements DbAdapter {
|
||||
private readonly message: MessagesDb
|
||||
private readonly notification: NotificationsDb
|
||||
|
||||
constructor(
|
||||
private readonly db: PostgresClientReference,
|
||||
private readonly sqlClient: postgres.Sql
|
||||
) {
|
||||
this.message = new MessagesDb(this.sqlClient)
|
||||
this.notification = new NotificationsDb(this.sqlClient)
|
||||
}
|
||||
|
||||
async createMessage(content: RichText, creator: SocialID, created: Date): Promise<MessageID> {
|
||||
return await this.message.createMessage(content, creator, created)
|
||||
}
|
||||
|
||||
async placeMessage(message: MessageID, card: CardID, workspace: string): Promise<void> {
|
||||
return await this.message.placeMessage(message, card, workspace)
|
||||
}
|
||||
|
||||
async createPatch(message: MessageID, content: RichText, creator: SocialID, created: Date): Promise<void> {
|
||||
return await this.message.createPatch(message, content, creator, created)
|
||||
}
|
||||
|
||||
async removeMessage(message: MessageID): Promise<void> {
|
||||
return await this.message.removeMessage(message)
|
||||
}
|
||||
|
||||
async createReaction(message: MessageID, reaction: string, creator: SocialID, created: Date): Promise<void> {
|
||||
return await this.message.createReaction(message, reaction, creator, created)
|
||||
}
|
||||
|
||||
async removeReaction(message: MessageID, reaction: string, creator: SocialID): Promise<void> {
|
||||
return await this.message.removeReaction(message, reaction, creator)
|
||||
}
|
||||
|
||||
async createAttachment(message: MessageID, card: CardID, creator: SocialID, created: Date): Promise<void> {
|
||||
return await this.message.createAttachment(message, card, creator, created)
|
||||
}
|
||||
|
||||
async removeAttachment(message: MessageID, card: CardID): Promise<void> {
|
||||
return await this.message.removeAttachment(message, card)
|
||||
}
|
||||
|
||||
async findMessages(workspace: string, params: FindMessagesParams): Promise<Message[]> {
|
||||
return await this.message.find(workspace, params)
|
||||
}
|
||||
|
||||
async createNotification(message: MessageID, context: ContextID): Promise<void> {
|
||||
return await this.notification.createNotification(message, context)
|
||||
}
|
||||
|
||||
async removeNotification(message: MessageID, context: ContextID): Promise<void> {
|
||||
return await this.notification.removeNotification(message, context)
|
||||
}
|
||||
|
||||
async createContext(
|
||||
workspace: string,
|
||||
card: CardID,
|
||||
personWorkspace: string,
|
||||
lastView?: Date,
|
||||
lastUpdate?: Date
|
||||
): Promise<ContextID> {
|
||||
return await this.notification.createContext(workspace, card, personWorkspace, lastView, lastUpdate)
|
||||
}
|
||||
|
||||
async updateContext(context: ContextID, update: NotificationContextUpdate): Promise<void> {
|
||||
return await this.notification.updateContext(context, update)
|
||||
}
|
||||
|
||||
async removeContext(context: ContextID): Promise<void> {
|
||||
return await this.notification.removeContext(context)
|
||||
}
|
||||
|
||||
async findContexts(
|
||||
params: FindNotificationContextParams,
|
||||
personWorkspaces: string[],
|
||||
workspace?: string
|
||||
): Promise<NotificationContext[]> {
|
||||
return await this.notification.findContexts(params, personWorkspaces, workspace)
|
||||
}
|
||||
|
||||
async findNotifications(
|
||||
params: FindNotificationsParams,
|
||||
personWorkspace: string,
|
||||
workspace?: string
|
||||
): Promise<Notification[]> {
|
||||
return await this.notification.findNotifications(params, personWorkspace, workspace)
|
||||
}
|
||||
|
||||
close(): void {
|
||||
this.db.close()
|
||||
}
|
||||
}
|
||||
|
||||
export async function createDbAdapter(connectionString: string): Promise<DbAdapter> {
|
||||
const db = connect(connectionString)
|
||||
const sqlClient = await db.getClient()
|
||||
|
||||
return new CockroachAdapter(db, sqlClient)
|
||||
}
|
||||
@@ -0,0 +1,104 @@
|
||||
//Full copy from @hcengineering/postgres
|
||||
import postgres from 'postgres'
|
||||
import { v4 as uuid } from 'uuid'
|
||||
|
||||
const connections = new Map<string, PostgresClientReferenceImpl>()
|
||||
const clientRefs = new Map<string, ClientRef>()
|
||||
|
||||
export interface PostgresClientReference {
|
||||
getClient: () => Promise<postgres.Sql>
|
||||
close: () => void
|
||||
}
|
||||
|
||||
class PostgresClientReferenceImpl {
|
||||
count: number
|
||||
client: postgres.Sql | Promise<postgres.Sql>
|
||||
|
||||
constructor(
|
||||
client: postgres.Sql | Promise<postgres.Sql>,
|
||||
readonly onclose: () => void
|
||||
) {
|
||||
this.count = 0
|
||||
this.client = client
|
||||
}
|
||||
|
||||
async getClient(): Promise<postgres.Sql> {
|
||||
if (this.client instanceof Promise) {
|
||||
this.client = await this.client
|
||||
}
|
||||
return this.client
|
||||
}
|
||||
|
||||
close(force: boolean = false): void {
|
||||
this.count--
|
||||
if (this.count === 0 || force) {
|
||||
if (force) {
|
||||
this.count = 0
|
||||
}
|
||||
void (async () => {
|
||||
this.onclose()
|
||||
const cl = await this.client
|
||||
await cl.end()
|
||||
console.log('Closed postgres connection')
|
||||
})()
|
||||
}
|
||||
}
|
||||
|
||||
addRef(): void {
|
||||
this.count++
|
||||
console.log('Add postgres connection', this.count)
|
||||
}
|
||||
}
|
||||
|
||||
export class ClientRef implements PostgresClientReference {
|
||||
id = uuid()
|
||||
constructor(readonly client: PostgresClientReferenceImpl) {
|
||||
clientRefs.set(this.id, this)
|
||||
}
|
||||
|
||||
closed = false
|
||||
async getClient(): Promise<postgres.Sql> {
|
||||
if (!this.closed) {
|
||||
return await this.client.getClient()
|
||||
} else {
|
||||
throw new Error('DB client-query is already closed')
|
||||
}
|
||||
}
|
||||
|
||||
close(): void {
|
||||
// Do not allow double close of connection client-query
|
||||
if (!this.closed) {
|
||||
clientRefs.delete(this.id)
|
||||
this.closed = true
|
||||
this.client.close()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export function connect(connectionString: string, database?: string): PostgresClientReference {
|
||||
const extraOptions = JSON.parse(process.env.POSTGRES_OPTIONS ?? '{}')
|
||||
const key = `${connectionString}${extraOptions}`
|
||||
let existing = connections.get(key)
|
||||
|
||||
if (existing === undefined) {
|
||||
const sql = postgres(connectionString, {
|
||||
connection: {
|
||||
application_name: 'communication'
|
||||
},
|
||||
database,
|
||||
max: 10,
|
||||
transform: {
|
||||
undefined: null
|
||||
},
|
||||
...extraOptions
|
||||
})
|
||||
|
||||
existing = new PostgresClientReferenceImpl(sql, () => {
|
||||
connections.delete(key)
|
||||
})
|
||||
connections.set(key, existing)
|
||||
}
|
||||
// Add reference and return once closable
|
||||
existing.addRef()
|
||||
return new ClientRef(existing)
|
||||
}
|
||||
@@ -0,0 +1,41 @@
|
||||
import type postgres from 'postgres'
|
||||
|
||||
export class BaseDb {
|
||||
constructor(
|
||||
readonly client: postgres.Sql
|
||||
) {}
|
||||
|
||||
async insert(table: string, data: Record<string, any>): Promise<void> {
|
||||
const keys = Object.keys(data)
|
||||
const values = Object.values(data)
|
||||
const sql = `
|
||||
INSERT INTO ${table} (${keys.map((k) => `"${k}"`).join(', ')})
|
||||
VALUES (${keys.map((_, idx) => `$${idx + 1}`).join(', ')});
|
||||
`
|
||||
await this.client.unsafe(sql, values)
|
||||
}
|
||||
|
||||
async insertWithReturn(table: string, data: Record<string, any>, returnField : string): Promise<any> {
|
||||
const keys = Object.keys(data)
|
||||
const values = Object.values(data)
|
||||
const sql = `
|
||||
INSERT INTO ${table} (${keys.map((k) => `"${k}"`).join(', ')})
|
||||
VALUES (${keys.map((_, idx) => `$${idx + 1}`).join(', ')})
|
||||
RETURNING ${returnField};`
|
||||
const result =await this.client.unsafe(sql, values)
|
||||
|
||||
return result[0][returnField]
|
||||
}
|
||||
|
||||
async remove(table: string, where: Record<string, any>): Promise<void> {
|
||||
const keys = Object.keys(where)
|
||||
const values = Object.values(where)
|
||||
|
||||
const sql = `
|
||||
DELETE
|
||||
FROM ${table}
|
||||
WHERE ${keys.map((k, idx) => `"${k}" = $${idx + 1}`).join(' AND ')};`
|
||||
|
||||
await this.client.unsafe(sql, values)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,228 @@
|
||||
import {
|
||||
type Message,
|
||||
type MessageID,
|
||||
type CardID,
|
||||
type FindMessagesParams,
|
||||
SortOrder,
|
||||
type SocialID,
|
||||
type RichText,
|
||||
Direction, type Reaction, type Attachment
|
||||
} from '@communication/types'
|
||||
|
||||
import {BaseDb} from './base.ts'
|
||||
import {
|
||||
TableName,
|
||||
type MessageDb,
|
||||
type MessagePlaceDb,
|
||||
type AttachmentDb,
|
||||
type ReactionDb,
|
||||
type PatchDb
|
||||
} from './types.ts'
|
||||
|
||||
export class MessagesDb extends BaseDb {
|
||||
//Message
|
||||
async createMessage(content: RichText, creator: SocialID, created: Date): Promise<MessageID> {
|
||||
const dbData: MessageDb = {
|
||||
content: content,
|
||||
creator: creator,
|
||||
created: created,
|
||||
}
|
||||
|
||||
const id = await this.insertWithReturn(TableName.Message, dbData, 'id')
|
||||
|
||||
return id as MessageID
|
||||
}
|
||||
|
||||
async removeMessage(message: MessageID): Promise<void> {
|
||||
await this.remove(TableName.Message, {id: message})
|
||||
}
|
||||
|
||||
async placeMessage(message: MessageID, card: CardID, workspace: string): Promise<void> {
|
||||
const dbData: MessagePlaceDb = {
|
||||
workspace_id: workspace,
|
||||
card_id: card,
|
||||
message_id: message
|
||||
}
|
||||
await this.insert(TableName.MessagePlace, dbData)
|
||||
}
|
||||
|
||||
async createPatch(message: MessageID, content: RichText, creator: SocialID, created: Date): Promise<void> {
|
||||
const dbData: PatchDb = {
|
||||
message_id: message,
|
||||
content: content,
|
||||
creator: creator,
|
||||
created: created
|
||||
}
|
||||
|
||||
await this.insert(TableName.Patch, dbData)
|
||||
}
|
||||
|
||||
//Attachment
|
||||
async createAttachment(message: MessageID, card: CardID, creator: SocialID, created: Date): Promise<void> {
|
||||
const dbData: AttachmentDb = {
|
||||
message_id: message,
|
||||
card_id: card,
|
||||
creator: creator,
|
||||
created: created
|
||||
}
|
||||
await this.insert(TableName.Attachment, dbData)
|
||||
}
|
||||
|
||||
async removeAttachment(message: MessageID, card: CardID): Promise<void> {
|
||||
await this.remove(TableName.Attachment, {
|
||||
message_id: message,
|
||||
card_id: card
|
||||
})
|
||||
}
|
||||
|
||||
//Reaction
|
||||
async createReaction(message: MessageID, reaction: string, creator: SocialID, created: Date): Promise<void> {
|
||||
const dbData: ReactionDb = {
|
||||
message_id: message,
|
||||
reaction: reaction,
|
||||
creator: creator,
|
||||
created: created
|
||||
}
|
||||
await this.insert(TableName.Reaction, dbData)
|
||||
}
|
||||
|
||||
async removeReaction(message: MessageID, reaction: string, creator: SocialID): Promise<void> {
|
||||
await this.remove(TableName.Reaction, {
|
||||
message_id: message,
|
||||
reaction: reaction,
|
||||
creator: creator
|
||||
})
|
||||
}
|
||||
|
||||
//Find messages
|
||||
async find(workspace: string, params: FindMessagesParams): Promise<Message[]> {
|
||||
//TODO: experiment with select to improve performance
|
||||
const select = `SELECT m.id,
|
||||
m.content,
|
||||
m.creator,
|
||||
m.created,
|
||||
${this.subSelectPatches()},
|
||||
${this.subSelectAttachments()},
|
||||
${this.subSelectReactions()}
|
||||
FROM ${TableName.Message} m
|
||||
INNER JOIN ${TableName.MessagePlace} mp ON m.id = mp.message_id`
|
||||
|
||||
const {where, values} = this.buildMessageWhere(workspace, params)
|
||||
const orderBy = params.sort ? `ORDER BY m.created ${params.sort === SortOrder.Asc ? 'ASC' : 'DESC'}` : ''
|
||||
const limit = params.limit ? ` LIMIT ${params.limit}` : ''
|
||||
const sql = [select, where, orderBy, limit].join(' ')
|
||||
|
||||
const result = await this.client.unsafe(sql, values)
|
||||
|
||||
return result.map(it => this.toMessage(it)) as Message[]
|
||||
}
|
||||
|
||||
buildMessageWhere(workspace: string, params: FindMessagesParams): { where: string, values: any[] } {
|
||||
const where: string[] = ['mp.workspace_id = $1']
|
||||
const values: any[] = [workspace]
|
||||
let index = 2
|
||||
for (const key of Object.keys(params)) {
|
||||
const value = (params as any)[key]
|
||||
switch (key) {
|
||||
case 'id': {
|
||||
where.push(`m.id = $${index++}`)
|
||||
values.push(value)
|
||||
break
|
||||
}
|
||||
case 'card': {
|
||||
where.push(`mp.card_id = $${index++}`)
|
||||
values.push(value)
|
||||
break
|
||||
}
|
||||
case 'from': {
|
||||
const exclude = params.excluded ?? false
|
||||
const direction = params.direction ?? Direction.Forward
|
||||
const getOperator = () => {
|
||||
if (exclude) {
|
||||
return direction === Direction.Forward ? '>' : '<'
|
||||
} else {
|
||||
return direction === Direction.Forward ? '>=' : '<='
|
||||
}
|
||||
}
|
||||
|
||||
where.push(`m.created ${getOperator()} $${index++}`)
|
||||
values.push(value)
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return {where: `WHERE ${where.join(' AND ')}`, values}
|
||||
}
|
||||
|
||||
subSelectPatches(): string {
|
||||
return `array(
|
||||
SELECT jsonb_build_object(
|
||||
'content', p.content,
|
||||
'creator', p.creator,
|
||||
'created', p.created
|
||||
)
|
||||
FROM ${TableName.Patch} p
|
||||
WHERE p.message_id = m.id
|
||||
) AS patches`
|
||||
}
|
||||
|
||||
subSelectAttachments(): string {
|
||||
return `array(
|
||||
SELECT jsonb_build_object(
|
||||
'card_id', a.card_id,
|
||||
'message_id', a.message_id,
|
||||
'creator', a.creator,
|
||||
'created', a.created
|
||||
)
|
||||
FROM ${TableName.Attachment} a
|
||||
WHERE a.message_id = m.id
|
||||
) AS attachments`
|
||||
}
|
||||
|
||||
subSelectReactions(): string {
|
||||
return `array(
|
||||
SELECT jsonb_build_object(
|
||||
'message_id', r.message_id,
|
||||
'reaction', r.reaction,
|
||||
'creator', r.creator,
|
||||
'created', r.created
|
||||
)
|
||||
FROM ${TableName.Reaction} r
|
||||
WHERE r.message_id = m.id
|
||||
) AS reactions`
|
||||
}
|
||||
|
||||
toMessage(row: any): Message {
|
||||
const lastPatch = row.patches?.[0]
|
||||
|
||||
return {
|
||||
id: row.id,
|
||||
content: lastPatch?.content ?? row.content,
|
||||
creator: row.creator,
|
||||
created: new Date(row.created),
|
||||
edited: new Date(lastPatch?.created ?? row.created),
|
||||
reactions: (row.reactions ?? []).map(this.toReaction),
|
||||
attachments: (row.attachments ?? []).map(this.toAttachment)
|
||||
}
|
||||
}
|
||||
|
||||
toReaction(row: any): Reaction {
|
||||
return {
|
||||
message: row.message_id,
|
||||
reaction: row.reaction,
|
||||
creator: row.creator,
|
||||
created: new Date(row.created)
|
||||
}
|
||||
}
|
||||
|
||||
toAttachment(row: any): Attachment {
|
||||
return {
|
||||
message: row.message_id,
|
||||
card: row.card_id,
|
||||
creator: row.creator,
|
||||
created: new Date(row.created)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,239 @@
|
||||
import {
|
||||
type MessageID,
|
||||
type ContextID,
|
||||
type CardID,
|
||||
type NotificationContext,
|
||||
type FindNotificationContextParams, SortOrder,
|
||||
type FindNotificationsParams, type Notification,
|
||||
type NotificationContextUpdate
|
||||
} from '@communication/types'
|
||||
|
||||
import {BaseDb} from './base.ts'
|
||||
import {TableName, type ContextDb, type NotificationDb} from './types.ts'
|
||||
|
||||
export class NotificationsDb extends BaseDb {
|
||||
async createNotification(message: MessageID, context: ContextID): Promise<void> {
|
||||
const dbData: NotificationDb = {
|
||||
message_id: message,
|
||||
context
|
||||
}
|
||||
await this.insert(TableName.Notification, dbData)
|
||||
}
|
||||
|
||||
async removeNotification(message: MessageID, context: ContextID): Promise<void> {
|
||||
await this.remove(TableName.Notification, {
|
||||
message_id: message,
|
||||
context
|
||||
})
|
||||
}
|
||||
|
||||
async createContext(workspace: string, card: CardID, personWorkspace: string, lastView?: Date, lastUpdate?: Date): Promise<ContextID> {
|
||||
const dbData: ContextDb = {
|
||||
workspace_id: workspace,
|
||||
card_id: card,
|
||||
person_workspace: personWorkspace,
|
||||
last_view: lastView,
|
||||
last_update: lastUpdate
|
||||
}
|
||||
return await this.insertWithReturn(TableName.NotificationContext, dbData, 'id') as ContextID
|
||||
}
|
||||
|
||||
async removeContext(context: ContextID): Promise<void> {
|
||||
await this.remove(TableName.NotificationContext, {
|
||||
id: context
|
||||
})
|
||||
}
|
||||
|
||||
async updateContext(context: ContextID, update: NotificationContextUpdate): Promise<void> {
|
||||
const dbData: Partial<ContextDb> = {}
|
||||
|
||||
if (update.archivedFrom != null) {
|
||||
dbData.archived_from = update.archivedFrom
|
||||
}
|
||||
if (update.lastView != null) {
|
||||
dbData.last_view = update.lastView
|
||||
}
|
||||
if (update.lastUpdate != null) {
|
||||
dbData.last_update = update.lastUpdate
|
||||
}
|
||||
|
||||
if (Object.keys(dbData).length === 0) {
|
||||
return
|
||||
}
|
||||
|
||||
const keys = Object.keys(dbData)
|
||||
const values = Object.values(dbData)
|
||||
|
||||
const sql = `UPDATE ${TableName.NotificationContext}
|
||||
SET ${keys.map((k, idx) => `"${k}" = $${idx + 1}`).join(', ')}
|
||||
WHERE id =$${keys.length + 1}`
|
||||
|
||||
await this.client.unsafe(sql, [values, context])
|
||||
}
|
||||
|
||||
async findContexts( params: FindNotificationContextParams, personWorkspaces: string[], workspace?: string,): Promise<NotificationContext[]> {
|
||||
const select = `
|
||||
SELECT nc.id, nc.card_id, nc.archived_from, nc.last_view, nc.last_update
|
||||
FROM ${TableName.NotificationContext} nc`;
|
||||
const {where, values} = this.buildContextWhere(params, personWorkspaces, workspace)
|
||||
// const orderSql = `ORDER BY nc.created ${params.sort === SortOrder.Asc ? 'ASC' : 'DESC'}`
|
||||
const limit = params.limit ? ` LIMIT ${params.limit}` : ''
|
||||
const sql = [select, where, limit].join(' ')
|
||||
|
||||
const result = await this.client.unsafe(sql, values);
|
||||
|
||||
return result.map(this.toNotificationContext);
|
||||
}
|
||||
|
||||
|
||||
async findNotifications(params: FindNotificationsParams, personWorkspace: string, workspace?: string): Promise<Notification[]> {
|
||||
//TODO: experiment with select to improve performance, should join with attachments and reactions?
|
||||
const select = `
|
||||
SELECT n.message_id,
|
||||
n.context,
|
||||
m.content AS message_content,
|
||||
m.creator AS message_creator,
|
||||
m.created AS message_created,
|
||||
nc.card_id,
|
||||
nc.archived_from,
|
||||
nc.last_view,
|
||||
nc.last_update,
|
||||
(SELECT json_agg(
|
||||
jsonb_build_object(
|
||||
'id', p.id,
|
||||
'content', p.content,
|
||||
'creator', p.creator,
|
||||
'created', p.created
|
||||
)
|
||||
)
|
||||
FROM ${TableName.Patch} p
|
||||
WHERE p.message_id = m.id) AS patches
|
||||
FROM ${TableName.Notification} n
|
||||
JOIN ${TableName.NotificationContext} nc ON n.context = nc.id
|
||||
JOIN ${TableName.Message} m ON n.message_id = m.id
|
||||
`;
|
||||
const {where, values} = this.buildNotificationWhere(params, personWorkspace, workspace)
|
||||
const orderBy = params.sort ? `ORDER BY m.created ${params.sort === SortOrder.Asc ? 'ASC' : 'DESC'}` : ''
|
||||
const limit = params.limit ? ` LIMIT ${params.limit}` : ''
|
||||
const sql = [select, where, orderBy, limit].join(' ')
|
||||
|
||||
const result = await this.client.unsafe(sql, values);
|
||||
|
||||
return result.map(this.toNotification);
|
||||
}
|
||||
|
||||
buildContextWhere(params: FindNotificationContextParams, personWorkspaces: string[], workspace?: string,): {
|
||||
where: string,
|
||||
values: any[]
|
||||
} {
|
||||
const where: string[] = []
|
||||
const values: any[] = []
|
||||
let index = 1
|
||||
|
||||
if(workspace != null) {
|
||||
where.push(`nc.workspace_id = $${index++}`)
|
||||
values.push(workspace)
|
||||
}
|
||||
|
||||
if(personWorkspaces.length > 0) {
|
||||
where.push(`nc.person_workspace IN (${personWorkspaces.map((it) => `$${index++}`).join(', ')})`)
|
||||
values.push(...personWorkspaces)
|
||||
}
|
||||
|
||||
for (const key of Object.keys(params)) {
|
||||
const value = (params as any)[key]
|
||||
switch (key) {
|
||||
case 'card': {
|
||||
where.push(`nc.card_id = $${index++}`)
|
||||
values.push(value)
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return {where: `WHERE ${where.join(' AND ')}`, values}
|
||||
}
|
||||
|
||||
buildNotificationWhere(params: FindNotificationsParams, personWorkspace: string, workspace?: string): {
|
||||
where: string,
|
||||
values: any[]
|
||||
} {
|
||||
const where: string[] = ['nc.person_workspace = $1']
|
||||
const values: any[] = [personWorkspace]
|
||||
let index = 2
|
||||
|
||||
if(workspace != null) {
|
||||
where.push(`nc.workspace_id = $${index++}`)
|
||||
values.push(workspace)
|
||||
}
|
||||
|
||||
for (const key of Object.keys(params)) {
|
||||
const value = (params as any)[key]
|
||||
switch (key) {
|
||||
case 'context': {
|
||||
where.push(`n.context = $${index++}`)
|
||||
values.push(value)
|
||||
break
|
||||
}
|
||||
case 'card': {
|
||||
where.push(`nc.card_id = $${index++}`)
|
||||
values.push(value)
|
||||
break
|
||||
}
|
||||
case 'read': {
|
||||
if (value === true) {
|
||||
where.push(`nc.last_view IS NOT NULL AND nc.last_view >= m.created`)
|
||||
} else if (value === false) {
|
||||
where.push(`(nc.last_view IS NULL OR nc.last_view > m.created)`)
|
||||
}
|
||||
break
|
||||
}
|
||||
case 'archived': {
|
||||
if (value === true) {
|
||||
where.push(`nc.archived_from IS NOT NULL AND nc.archived_from >= m.created`)
|
||||
} else if (value === false) {
|
||||
where.push(`(nc.archived_from IS NULL OR nc.archived_from > m.created)`)
|
||||
}
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return {where: `WHERE ${where.join(' AND ')}`, values}
|
||||
}
|
||||
|
||||
toNotificationContext(row: any): NotificationContext {
|
||||
return {
|
||||
id: row.id,
|
||||
card: row.card_id,
|
||||
workspace: row.workspace_id,
|
||||
personWorkspace: row.person_workspace,
|
||||
archivedFrom: row.archived_from ? new Date(row.archived_from) : undefined,
|
||||
lastView: row.last_view ? new Date(row.last_view) : undefined,
|
||||
lastUpdate: row.last_update ? new Date(row.last_update) : undefined
|
||||
}
|
||||
}
|
||||
|
||||
toNotification(row: any): Notification {
|
||||
const lastPatch = row.patches?.[0]
|
||||
const lastView = row.last_view ? new Date(row.last_view) : undefined
|
||||
const archivedFrom = row.archived_from ? new Date(row.archived_from) : undefined
|
||||
const created = new Date(row.message_created)
|
||||
|
||||
return {
|
||||
message: {
|
||||
id: row.id,
|
||||
content: lastPatch?.content ?? row.message_content,
|
||||
creator: row.message_creator,
|
||||
created,
|
||||
edited: new Date(lastPatch?.created ?? row.message_created),
|
||||
reactions: row.reactions ?? [],
|
||||
attachments: row.attachments ?? []
|
||||
},
|
||||
context: row.context,
|
||||
read: lastView != null && lastView >= created,
|
||||
archived: archivedFrom != null && archivedFrom >= created
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,59 @@
|
||||
import type {CardID, ContextID, MessageID, RichText, SocialID } from "@communication/types"
|
||||
|
||||
export enum TableName {
|
||||
Message = 'message',
|
||||
Patch = 'patch',
|
||||
MessagePlace = 'message_place',
|
||||
Attachment = 'attachment',
|
||||
Reaction = 'reaction',
|
||||
Notification = 'notification',
|
||||
NotificationContext = 'notification_context'
|
||||
}
|
||||
|
||||
export interface MessageDb {
|
||||
content: RichText,
|
||||
creator: SocialID,
|
||||
created: Date,
|
||||
}
|
||||
|
||||
export interface PatchDb {
|
||||
message_id: MessageID,
|
||||
content: RichText,
|
||||
creator: SocialID,
|
||||
created: Date,
|
||||
}
|
||||
|
||||
export interface MessagePlaceDb {
|
||||
workspace_id: string,
|
||||
card_id: CardID,
|
||||
message_id: MessageID
|
||||
}
|
||||
|
||||
export interface ReactionDb {
|
||||
message_id: MessageID,
|
||||
reaction: string,
|
||||
creator: SocialID
|
||||
created: Date
|
||||
}
|
||||
|
||||
export interface AttachmentDb {
|
||||
message_id: MessageID,
|
||||
card_id: CardID,
|
||||
creator: SocialID
|
||||
created: Date
|
||||
}
|
||||
|
||||
export interface NotificationDb {
|
||||
message_id: MessageID,
|
||||
context: ContextID
|
||||
}
|
||||
|
||||
export interface ContextDb {
|
||||
workspace_id: string
|
||||
card_id: CardID
|
||||
person_workspace: string
|
||||
|
||||
archived_from?: Date
|
||||
last_view?: Date
|
||||
last_update?: Date
|
||||
}
|
||||
@@ -0,0 +1 @@
|
||||
export * from './adapter.ts'
|
||||
@@ -1,19 +0,0 @@
|
||||
CREATE TABLE IF NOT EXISTS message
|
||||
(
|
||||
id INT8 NOT NULL DEFAULT unique_rowid(),
|
||||
content TEXT,
|
||||
version INTEGER NOT NULL,
|
||||
creator VARCHAR(255) NOT NULL,
|
||||
created TIMESTAMPTZ NOT NULL DEFAULT now(),
|
||||
|
||||
PRIMARY KEY (id, version)
|
||||
);
|
||||
|
||||
CREATE TABLE IF NOT EXISTS message_place
|
||||
(
|
||||
workspace_id UUID NOT NULL,
|
||||
card_id UUID NOT NULL,
|
||||
message_id INT8 NOT NULL,
|
||||
|
||||
PRIMARY KEY (workspace_id, card_id, message_id)
|
||||
);
|
||||
@@ -1,11 +0,0 @@
|
||||
CREATE TABLE IF NOT EXISTS reaction
|
||||
(
|
||||
message_id INT8 NOT NULL,
|
||||
reaction INTEGER NOT NULL,
|
||||
creator VARCHAR(255) NOT NULL,
|
||||
created TIMESTAMPTZ NOT NULL DEFAULT now(),
|
||||
|
||||
PRIMARY KEY (message_id, creator, reaction)
|
||||
);
|
||||
|
||||
CREATE INDEX IF NOT EXISTS reaction_message_idx ON reaction (message_id);
|
||||
@@ -1,7 +0,0 @@
|
||||
CREATE TABLE IF NOT EXISTS notification
|
||||
(
|
||||
social_id VARCHAR(255) NOT NULL,
|
||||
message_id INT8 NOT NULL,
|
||||
|
||||
PRIMARY KEY (social_id, message_id)
|
||||
);
|
||||
@@ -1,12 +0,0 @@
|
||||
CREATE TABLE IF NOT EXISTS notification_context
|
||||
(
|
||||
workspace_id UUID NOT NULL,
|
||||
card_id UUID NOT NULL,
|
||||
huly_id VARCHAR(255) NOT NULL, /* Or maybe account id or something else */
|
||||
|
||||
archived_from TIMESTAMPTZ,
|
||||
last_view TIMESTAMPTZ,
|
||||
last_update TIMESTAMPTZ,
|
||||
|
||||
PRIMARY KEY (workspace_id, card_id, huly_id)
|
||||
);
|
||||
@@ -0,0 +1,16 @@
|
||||
{
|
||||
"name": "@communication/sdk-types",
|
||||
"version": "0.1.0",
|
||||
"main": "src/index.ts",
|
||||
"module": "src/index.ts",
|
||||
"type": "module",
|
||||
"devDependencies": {
|
||||
"@types/bun": "^1.1.14"
|
||||
},
|
||||
"dependencies": {
|
||||
"@communication/types": "workspace:*"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"typescript": "^5.6.3"
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,54 @@
|
||||
import type {
|
||||
CardID,
|
||||
ContextID,
|
||||
FindMessagesParams,
|
||||
FindNotificationContextParams,
|
||||
FindNotificationsParams,
|
||||
Message,
|
||||
MessageID,
|
||||
NotificationContext,
|
||||
NotificationContextUpdate,
|
||||
RichText,
|
||||
SocialID,
|
||||
Notification
|
||||
} from '@communication/types'
|
||||
|
||||
export interface DbAdapter {
|
||||
createMessage(content: RichText, creator: SocialID, created: Date): Promise<MessageID>
|
||||
removeMessage(id: MessageID): Promise<void>
|
||||
|
||||
placeMessage(message: MessageID, card: CardID, workspace: string): Promise<void>
|
||||
createPatch(message: MessageID, content: RichText, creator: SocialID, created: Date): Promise<void>
|
||||
|
||||
createReaction(message: MessageID, reaction: string, creator: SocialID, created: Date): Promise<void>
|
||||
removeReaction(message: MessageID, reaction: string, creator: SocialID): Promise<void>
|
||||
|
||||
createAttachment(message: MessageID, card: CardID, creator: SocialID, created: Date): Promise<void>
|
||||
removeAttachment(message: MessageID, card: CardID): Promise<void>
|
||||
|
||||
findMessages(workspace: string, query: FindMessagesParams): Promise<Message[]>
|
||||
|
||||
createNotification(message: MessageID, context: ContextID): Promise<void>
|
||||
removeNotification(message: MessageID, context: ContextID): Promise<void>
|
||||
createContext(
|
||||
personWorkspace: string,
|
||||
workspace: string,
|
||||
card: CardID,
|
||||
lastView?: Date,
|
||||
lastUpdate?: Date
|
||||
): Promise<ContextID>
|
||||
updateContext(context: ContextID, update: NotificationContextUpdate): Promise<void>
|
||||
removeContext(context: ContextID): Promise<void>
|
||||
findContexts(
|
||||
params: FindNotificationContextParams,
|
||||
personWorkspaces: string[],
|
||||
workspace?: string
|
||||
): Promise<NotificationContext[]>
|
||||
findNotifications(
|
||||
params: FindNotificationsParams,
|
||||
personWorkspace: string,
|
||||
workspace?: string
|
||||
): Promise<Notification[]>
|
||||
|
||||
close(): void
|
||||
}
|
||||
@@ -0,0 +1 @@
|
||||
export * from './db'
|
||||
@@ -0,0 +1,8 @@
|
||||
{
|
||||
"extends": "../../tsconfig.json",
|
||||
"compilerOptions": {
|
||||
"outDir": "./dist",
|
||||
"rootDir": "./src"
|
||||
},
|
||||
"include": ["src"]
|
||||
}
|
||||
Reference in New Issue
Block a user