feat(server): add comment-attachment model

This commit is contained in:
fengmk2 2025-06-24 13:33:44 +08:00
parent 03391670d9
commit 9612f8ff74
No known key found for this signature in database
GPG Key ID: 8D8D804739EF5781
5 changed files with 242 additions and 0 deletions

View File

@ -0,0 +1,23 @@
-- CreateTable
CREATE TABLE "comment_attachments" (
"sid" INT GENERATED BY DEFAULT AS IDENTITY,
"workspace_id" VARCHAR NOT NULL,
"doc_id" VARCHAR NOT NULL,
"key" VARCHAR NOT NULL,
"size" INTEGER NOT NULL,
"mime" VARCHAR NOT NULL,
"name" VARCHAR NOT NULL,
"created_at" TIMESTAMPTZ(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
"created_by" VARCHAR,
CONSTRAINT "comment_attachments_pkey" PRIMARY KEY ("workspace_id","doc_id","key")
);
-- CreateIndex
CREATE UNIQUE INDEX "comment_attachments_sid_key" ON "comment_attachments"("sid");
-- AddForeignKey
ALTER TABLE "comment_attachments" ADD CONSTRAINT "comment_attachments_workspace_id_fkey" FOREIGN KEY ("workspace_id") REFERENCES "workspaces"("id") ON DELETE CASCADE ON UPDATE CASCADE;
-- AddForeignKey
ALTER TABLE "comment_attachments" ADD CONSTRAINT "comment_attachments_created_by_fkey" FOREIGN KEY ("created_by") REFERENCES "users"("id") ON DELETE SET NULL ON UPDATE CASCADE;

View File

@ -48,6 +48,7 @@ model User {
settings UserSettings?
comments Comment[]
replies Reply[]
commentAttachments CommentAttachment[] @relation("createdCommentAttachments")
@@index([email])
@@map("users")
@ -129,6 +130,7 @@ model Workspace {
ignoredDocs AiWorkspaceIgnoredDocs[]
embedFiles AiWorkspaceFiles[]
comments Comment[]
commentAttachments CommentAttachment[]
@@map("workspaces")
}
@ -906,3 +908,22 @@ model Reply {
@@index([userId])
@@map("replies")
}
model CommentAttachment {
// NOTE: manually set this column type to identity in migration file
sid Int @unique @default(autoincrement())
workspaceId String @map("workspace_id") @db.VarChar
docId String @map("doc_id") @db.VarChar
key String @db.VarChar
size Int @db.Integer
mime String @db.VarChar
name String @db.VarChar
createdAt DateTime @default(now()) @map("created_at") @db.Timestamptz(3)
createdBy String? @map("created_by") @db.VarChar
workspace Workspace @relation(fields: [workspaceId], references: [id], onDelete: Cascade)
// will delete creator record if creator's account is deleted
createdByUser User? @relation(name: "createdCommentAttachments", fields: [createdBy], references: [id], onDelete: SetNull)
@@id([workspaceId, docId, key])
@@map("comment_attachments")
}

View File

@ -0,0 +1,125 @@
import test from 'ava';
import { createModule } from '../../__tests__/create-module';
import { Mockers } from '../../__tests__/mocks';
import { Models } from '..';
const module = await createModule();
const models = module.get(Models);
test.after.always(async () => {
await module.close();
});
test('should upsert comment attachment', async t => {
const workspace = await module.create(Mockers.Workspace);
// add
const item = await models.commentAttachment.upsert({
workspaceId: workspace.id,
docId: 'test-doc-id',
key: 'test-key',
name: 'test-name',
mime: 'text/plain',
size: 100,
});
t.is(item.workspaceId, workspace.id);
t.is(item.docId, 'test-doc-id');
t.is(item.key, 'test-key');
t.is(item.mime, 'text/plain');
t.is(item.size, 100);
t.truthy(item.createdAt);
// update
const item2 = await models.commentAttachment.upsert({
workspaceId: workspace.id,
docId: 'test-doc-id',
name: 'test-name',
key: 'test-key',
mime: 'text/html',
size: 200,
});
t.is(item2.workspaceId, workspace.id);
t.is(item2.docId, 'test-doc-id');
t.is(item2.key, 'test-key');
t.is(item2.mime, 'text/html');
t.is(item2.size, 200);
// make sure only one blob is created
const items = await models.commentAttachment.list(workspace.id);
t.is(items.length, 1);
t.deepEqual(items[0], item2);
});
test('should delete comment attachment', async t => {
const workspace = await module.create(Mockers.Workspace);
const item = await models.commentAttachment.upsert({
workspaceId: workspace.id,
docId: 'test-doc-id',
key: 'test-key',
name: 'test-name',
mime: 'text/plain',
size: 100,
});
await models.commentAttachment.delete(workspace.id, item.docId, item.key);
const item2 = await models.commentAttachment.get(
workspace.id,
item.docId,
item.key
);
t.is(item2, null);
});
test('should list comment attachments', async t => {
const workspace = await module.create(Mockers.Workspace);
const item1 = await models.commentAttachment.upsert({
workspaceId: workspace.id,
docId: 'test-doc-id',
name: 'test-name',
key: 'test-key',
mime: 'text/plain',
size: 100,
});
const item2 = await models.commentAttachment.upsert({
workspaceId: workspace.id,
docId: 'test-doc-id2',
name: 'test-name2',
key: 'test-key2',
mime: 'text/plain',
size: 200,
});
const items = await models.commentAttachment.list(workspace.id);
t.is(items.length, 2);
items.sort((a, b) => a.key.localeCompare(b.key));
t.is(items[0].key, item1.key);
t.is(items[1].key, item2.key);
});
test('should get comment attachment', async t => {
const workspace = await module.create(Mockers.Workspace);
const item = await models.commentAttachment.upsert({
workspaceId: workspace.id,
docId: 'test-doc-id',
name: 'test-name',
key: 'test-key',
mime: 'text/plain',
size: 100,
});
const item2 = await models.commentAttachment.get(
workspace.id,
item.docId,
item.key
);
t.truthy(item2);
t.is(item2?.key, item.key);
});

View File

@ -0,0 +1,70 @@
import { Injectable } from '@nestjs/common';
import { Prisma } from '@prisma/client';
import { BaseModel } from './base';
export type CreateCommentAttachmentInput =
Prisma.CommentAttachmentUncheckedCreateInput;
/**
* Comment Attachment Model
*/
@Injectable()
export class CommentAttachmentModel extends BaseModel {
async upsert(input: CreateCommentAttachmentInput) {
return await this.db.commentAttachment.upsert({
where: {
workspaceId_docId_key: {
workspaceId: input.workspaceId,
docId: input.docId,
key: input.key,
},
},
update: {
name: input.name,
mime: input.mime,
size: input.size,
},
create: {
workspaceId: input.workspaceId,
docId: input.docId,
key: input.key,
name: input.name,
mime: input.mime,
size: input.size,
},
});
}
async delete(workspaceId: string, docId: string, key: string) {
await this.db.commentAttachment.deleteMany({
where: {
workspaceId,
docId,
key,
},
});
this.logger.log(`deleted comment attachment ${workspaceId}/${key}`);
}
async get(workspaceId: string, docId: string, key: string) {
return await this.db.commentAttachment.findUnique({
where: {
workspaceId_docId_key: {
workspaceId,
docId,
key,
},
},
});
}
async list(workspaceId: string, docId?: string) {
return await this.db.commentAttachment.findMany({
where: {
workspaceId,
docId,
},
});
}
}

View File

@ -8,6 +8,7 @@ import { ModuleRef } from '@nestjs/core';
import { ApplyType } from '../base';
import { CommentModel } from './comment';
import { CommentAttachmentModel } from './comment-attachment';
import { AppConfigModel } from './config';
import { CopilotContextModel } from './copilot-context';
import { CopilotJobModel } from './copilot-job';
@ -50,6 +51,7 @@ const MODELS = {
copilotJob: CopilotJobModel,
appConfig: AppConfigModel,
comment: CommentModel,
commentAttachment: CommentAttachmentModel,
};
type ModelsType = {
@ -102,6 +104,7 @@ const ModelsSymbolProvider: ExistingProvider = {
export class ModelsModule {}
export * from './comment';
export * from './comment-attachment';
export * from './common';
export * from './copilot-context';
export * from './copilot-job';