refactor: standardize formatting and enhance DocumentSchema
- Updated compose.yaml to use consistent quotation marks for port and labels. - Refactored package.json to improve readability of Jest configuration. - Enhanced DocumentSchema by adding a new event type for cursor movement and improving formatting for better clarity. - Consolidated import statements in document.schema.ts for consistency and readability. - Improved error handling and validation across various methods in DocumentSchema to enhance user experience. These changes improve code maintainability and readability, ensuring a more consistent and user-friendly experience in the Document management features.
This commit is contained in:
12
compose.yaml
12
compose.yaml
@@ -6,7 +6,7 @@ services:
|
||||
dockerfile: Dockerfile
|
||||
pull_policy: always
|
||||
ports:
|
||||
- '8888:3069'
|
||||
- "8888:3069"
|
||||
environment:
|
||||
- NODE_ENV=development
|
||||
- DISABLE_AUTH=false
|
||||
@@ -51,11 +51,11 @@ services:
|
||||
- LIVEKIT_CERT_FILE=/path/to/turn.crt
|
||||
- LIVEKIT_KEY_FILE=/path/to/turn.key
|
||||
labels:
|
||||
- 'traefik.enable=true'
|
||||
- 'traefik.http.routers.api.rule=Host(`api.epess.org`)'
|
||||
- 'traefik.http.routers.api.entrypoints=web'
|
||||
- 'traefik.http.routers.api.service=api'
|
||||
- 'traefik.http.services.api.loadbalancer.server.port=3069'
|
||||
- "traefik.enable=true"
|
||||
- "traefik.http.routers.api.rule=Host(`api.epess.org`)"
|
||||
- "traefik.http.routers.api.entrypoints=web"
|
||||
- "traefik.http.routers.api.service=api"
|
||||
- "traefik.http.services.api.loadbalancer.server.port=3069"
|
||||
networks:
|
||||
- epess-net
|
||||
restart: always
|
||||
|
||||
10
package.json
10
package.json
@@ -140,13 +140,19 @@
|
||||
"@css-inline/css-inline-linux-x64-musl": "^0.14.3"
|
||||
},
|
||||
"jest": {
|
||||
"moduleFileExtensions": ["js", "json", "ts"],
|
||||
"moduleFileExtensions": [
|
||||
"js",
|
||||
"json",
|
||||
"ts"
|
||||
],
|
||||
"rootDir": "src",
|
||||
"testRegex": ".*\\.spec\\.ts$",
|
||||
"transform": {
|
||||
"^.+\\.(t|j)s$": "ts-jest"
|
||||
},
|
||||
"collectCoverageFrom": ["**/*.(t|j)s"],
|
||||
"collectCoverageFrom": [
|
||||
"**/*.(t|j)s"
|
||||
],
|
||||
"coverageDirectory": "../coverage",
|
||||
"testEnvironment": "node"
|
||||
},
|
||||
|
||||
@@ -7,5 +7,6 @@ export enum DocumentEvent {
|
||||
ACTIVE_DOCUMENT_ID_CHANGED = 'document_active_document_id_changed',
|
||||
CLIENT_REQUEST_SYNC = 'document_client_request_sync',
|
||||
SERVER_REQUEST_SYNC = 'document_server_request_sync',
|
||||
CURSOR_MOVED = 'document_cursor_moved',
|
||||
AI_SUGGESTION = 'document_ai_suggestion',
|
||||
}
|
||||
|
||||
@@ -1,16 +1,22 @@
|
||||
import { Inject, Injectable, Logger } from '@nestjs/common'
|
||||
import { Document } from '@prisma/client'
|
||||
import { Pothos, PothosRef, PothosSchema, SchemaBuilderToken } from '@smatch-corp/nestjs-pothos'
|
||||
import Delta from 'quill-delta'
|
||||
import { MinioService } from 'src/Minio/minio.service'
|
||||
import { PromptType } from 'src/OpenAI/openai.service'
|
||||
import { RedisService } from 'src/Redis/redis.service'
|
||||
import { DateTimeUtils } from 'src/common/utils/datetime.utils'
|
||||
import { Builder, SchemaContext } from '../Graphql/graphql.builder'
|
||||
import { PrismaService } from '../Prisma/prisma.service'
|
||||
import { DocumentEvent } from './document.event'
|
||||
import { DocumentService } from './document.service'
|
||||
import { DocumentDelta } from './document.type'
|
||||
import { Inject, Injectable, Logger } from "@nestjs/common";
|
||||
import { Document } from "@prisma/client";
|
||||
import {
|
||||
Pothos,
|
||||
PothosRef,
|
||||
PothosSchema,
|
||||
SchemaBuilderToken,
|
||||
} from "@smatch-corp/nestjs-pothos";
|
||||
import Delta from "quill-delta";
|
||||
import { MinioService } from "src/Minio/minio.service";
|
||||
import { PromptType } from "src/OpenAI/openai.service";
|
||||
import { RedisService } from "src/Redis/redis.service";
|
||||
import { DateTimeUtils } from "src/common/utils/datetime.utils";
|
||||
import { Builder, SchemaContext } from "../Graphql/graphql.builder";
|
||||
import { PrismaService } from "../Prisma/prisma.service";
|
||||
import { DocumentEvent } from "./document.event";
|
||||
import { DocumentService } from "./document.service";
|
||||
import { DocumentDelta } from "./document.type";
|
||||
|
||||
@Injectable()
|
||||
export class DocumentSchema extends PothosSchema {
|
||||
constructor(
|
||||
@@ -18,67 +24,82 @@ export class DocumentSchema extends PothosSchema {
|
||||
private readonly prisma: PrismaService,
|
||||
private readonly minio: MinioService,
|
||||
private readonly documentService: DocumentService,
|
||||
private readonly redis: RedisService,
|
||||
private readonly redis: RedisService
|
||||
) {
|
||||
super()
|
||||
super();
|
||||
}
|
||||
|
||||
@PothosRef()
|
||||
document() {
|
||||
return this.builder.prismaObject('Document', {
|
||||
return this.builder.prismaObject("Document", {
|
||||
fields: (t) => ({
|
||||
id: t.exposeID('id', { description: 'The ID of the document.', nullable: false }),
|
||||
name: t.exposeString('name', { description: 'The name of the document.', nullable: false }),
|
||||
fileUrl: t.exposeString('fileUrl', { description: 'The file URL of the document.', nullable: false }),
|
||||
previewImage: t.relation('previewImage', {
|
||||
description: 'The preview image of the document.',
|
||||
nullable: true,
|
||||
}),
|
||||
owner: t.relation('owner', { description: 'The owner of the document.', nullable: false }),
|
||||
collaborators: t.relation('collaborators', {
|
||||
description: 'The collaborators of the document.',
|
||||
id: t.exposeID("id", {
|
||||
description: "The ID of the document.",
|
||||
nullable: false,
|
||||
}),
|
||||
isPublic: t.exposeBoolean('isPublic', { description: 'Whether the document is public.', nullable: false }),
|
||||
collaborationSession: t.relation('collaborationSession', {
|
||||
description: 'The collaboration session of the document.',
|
||||
nullable: true,
|
||||
}),
|
||||
collaborationSessionId: t.exposeID('collaborationSessionId', {
|
||||
description: 'The ID of the collaboration session of the document.',
|
||||
nullable: true,
|
||||
}),
|
||||
createdAt: t.expose('createdAt', {
|
||||
description: 'The creation time of the document.',
|
||||
type: 'DateTime',
|
||||
name: t.exposeString("name", {
|
||||
description: "The name of the document.",
|
||||
nullable: false,
|
||||
}),
|
||||
updatedAt: t.expose('updatedAt', {
|
||||
description: 'The update time of the document.',
|
||||
type: 'DateTime',
|
||||
fileUrl: t.exposeString("fileUrl", {
|
||||
description: "The file URL of the document.",
|
||||
nullable: false,
|
||||
}),
|
||||
previewImage: t.relation("previewImage", {
|
||||
description: "The preview image of the document.",
|
||||
nullable: true,
|
||||
}),
|
||||
owner: t.relation("owner", {
|
||||
description: "The owner of the document.",
|
||||
nullable: false,
|
||||
}),
|
||||
collaborators: t.relation("collaborators", {
|
||||
description: "The collaborators of the document.",
|
||||
nullable: false,
|
||||
}),
|
||||
isPublic: t.exposeBoolean("isPublic", {
|
||||
description: "Whether the document is public.",
|
||||
nullable: false,
|
||||
}),
|
||||
collaborationSession: t.relation("collaborationSession", {
|
||||
description: "The collaboration session of the document.",
|
||||
nullable: true,
|
||||
}),
|
||||
collaborationSessionId: t.exposeID("collaborationSessionId", {
|
||||
description: "The ID of the collaboration session of the document.",
|
||||
nullable: true,
|
||||
}),
|
||||
createdAt: t.expose("createdAt", {
|
||||
description: "The creation time of the document.",
|
||||
type: "DateTime",
|
||||
nullable: false,
|
||||
}),
|
||||
updatedAt: t.expose("updatedAt", {
|
||||
description: "The update time of the document.",
|
||||
type: "DateTime",
|
||||
nullable: false,
|
||||
}),
|
||||
}),
|
||||
})
|
||||
});
|
||||
}
|
||||
|
||||
@PothosRef()
|
||||
documentCollaborator() {
|
||||
return this.builder.prismaObject('DocumentCollaborator', {
|
||||
return this.builder.prismaObject("DocumentCollaborator", {
|
||||
fields: (t) => ({
|
||||
documentId: t.exposeID('documentId', { nullable: false }),
|
||||
userId: t.exposeID('userId', { nullable: false }),
|
||||
document: t.relation('document', { nullable: false }),
|
||||
user: t.relation('user', { nullable: false }),
|
||||
readable: t.exposeBoolean('readable', { nullable: false }),
|
||||
writable: t.exposeBoolean('writable', { nullable: false }),
|
||||
documentId: t.exposeID("documentId", { nullable: false }),
|
||||
userId: t.exposeID("userId", { nullable: false }),
|
||||
document: t.relation("document", { nullable: false }),
|
||||
user: t.relation("user", { nullable: false }),
|
||||
readable: t.exposeBoolean("readable", { nullable: false }),
|
||||
writable: t.exposeBoolean("writable", { nullable: false }),
|
||||
}),
|
||||
})
|
||||
});
|
||||
}
|
||||
|
||||
@PothosRef()
|
||||
documentDelta() {
|
||||
return this.builder.simpleObject('DocumentDelta', {
|
||||
return this.builder.simpleObject("DocumentDelta", {
|
||||
fields: (t) => ({
|
||||
eventType: t.string(),
|
||||
documentId: t.string({
|
||||
@@ -88,7 +109,7 @@ export class DocumentSchema extends PothosSchema {
|
||||
nullable: true,
|
||||
}),
|
||||
delta: t.field({
|
||||
type: 'Delta',
|
||||
type: "Delta",
|
||||
nullable: true,
|
||||
}),
|
||||
senderId: t.string({
|
||||
@@ -97,39 +118,85 @@ export class DocumentSchema extends PothosSchema {
|
||||
requestSync: t.boolean({
|
||||
nullable: true,
|
||||
}),
|
||||
cursor: t.field({
|
||||
type: this.cursor(),
|
||||
nullable: true,
|
||||
}),
|
||||
totalPage: t.int({
|
||||
nullable: true,
|
||||
}),
|
||||
}),
|
||||
})
|
||||
});
|
||||
}
|
||||
|
||||
@PothosRef()
|
||||
range() {
|
||||
return this.builder.simpleObject("Range", {
|
||||
fields: (t) => ({
|
||||
index: t.int(),
|
||||
length: t.int(),
|
||||
}),
|
||||
});
|
||||
}
|
||||
|
||||
@PothosRef()
|
||||
cursor() {
|
||||
return this.builder.simpleObject("Cursor", {
|
||||
fields: (t) => ({
|
||||
id: t.string(),
|
||||
name: t.string(),
|
||||
color: t.string(),
|
||||
range: t.field({
|
||||
type: this.range(),
|
||||
nullable: true,
|
||||
}),
|
||||
}),
|
||||
});
|
||||
}
|
||||
|
||||
@PothosRef()
|
||||
DocumentExportObject() {
|
||||
return this.builder.simpleObject('DocumentExportObject', {
|
||||
return this.builder.simpleObject("DocumentExportObject", {
|
||||
fields: (t) => ({
|
||||
documentId: t.string(),
|
||||
pageIndex: t.int(),
|
||||
type: t.field({
|
||||
type: this.builder.enumType('DocumentExportType', {
|
||||
values: ['PDF', 'DOCX'] as const,
|
||||
type: this.builder.enumType("DocumentExportType", {
|
||||
values: ["PDF", "DOCX"] as const,
|
||||
}),
|
||||
nullable: false,
|
||||
}),
|
||||
fileUrl: t.string(),
|
||||
}),
|
||||
})
|
||||
});
|
||||
}
|
||||
|
||||
@PothosRef()
|
||||
documentDeltaInput() {
|
||||
return this.builder.inputType('DocumentDeltaInput', {
|
||||
return this.builder.inputType("DocumentDeltaInput", {
|
||||
fields: (t) => ({
|
||||
documentId: t.string(),
|
||||
pageIndex: t.int(),
|
||||
delta: t.field({ type: 'Delta' }),
|
||||
delta: t.field({ type: "Delta" }),
|
||||
cursor: t.field({
|
||||
type: this.builder.inputType("CursorInput", {
|
||||
fields: (t) => ({
|
||||
id: t.string(),
|
||||
name: t.string(),
|
||||
color: t.string(),
|
||||
range: t.field({
|
||||
type: this.builder.inputType("RangeInput", {
|
||||
fields: (t) => ({
|
||||
index: t.int(),
|
||||
length: t.int(),
|
||||
}),
|
||||
})
|
||||
}),
|
||||
}),
|
||||
}),
|
||||
}),
|
||||
}),
|
||||
}),
|
||||
});
|
||||
}
|
||||
|
||||
@Pothos()
|
||||
@@ -137,37 +204,40 @@ export class DocumentSchema extends PothosSchema {
|
||||
this.builder.queryFields((t) => ({
|
||||
myDocuments: t.prismaField({
|
||||
type: [this.document()],
|
||||
args: this.builder.generator.findManyArgs('Document'),
|
||||
args: this.builder.generator.findManyArgs("Document"),
|
||||
resolve: async (query, _parent, args, ctx: SchemaContext) => {
|
||||
if (ctx.isSubscription) {
|
||||
throw new Error('Not allowed')
|
||||
throw new Error("Not allowed");
|
||||
}
|
||||
if (!ctx.http?.me?.id) {
|
||||
throw new Error('User not found')
|
||||
throw new Error("User not found");
|
||||
}
|
||||
return await this.prisma.document.findMany({
|
||||
...query,
|
||||
orderBy: args.orderBy ?? undefined,
|
||||
where: {
|
||||
OR: [{ ownerId: ctx.http.me.id }, { collaborators: { some: { userId: ctx.http.me.id } } }],
|
||||
OR: [
|
||||
{ ownerId: ctx.http.me.id },
|
||||
{ collaborators: { some: { userId: ctx.http.me.id } } },
|
||||
],
|
||||
},
|
||||
})
|
||||
});
|
||||
},
|
||||
}),
|
||||
document: t.prismaField({
|
||||
type: this.document(),
|
||||
args: this.builder.generator.findUniqueArgs('Document'),
|
||||
args: this.builder.generator.findUniqueArgs("Document"),
|
||||
resolve: async (query, _root, args) => {
|
||||
return await this.prisma.document.findUnique({
|
||||
...query,
|
||||
where: args.where,
|
||||
})
|
||||
});
|
||||
},
|
||||
}),
|
||||
|
||||
documents: t.prismaField({
|
||||
type: [this.document()],
|
||||
args: this.builder.generator.findManyArgs('Document'),
|
||||
args: this.builder.generator.findManyArgs("Document"),
|
||||
resolve: async (query, _root, args) => {
|
||||
return await this.prisma.document.findMany({
|
||||
...query,
|
||||
@@ -175,7 +245,7 @@ export class DocumentSchema extends PothosSchema {
|
||||
take: args.take ?? undefined,
|
||||
orderBy: args.orderBy ?? undefined,
|
||||
where: args.filter ?? undefined,
|
||||
})
|
||||
});
|
||||
},
|
||||
}),
|
||||
newDocument: t.field({
|
||||
@@ -183,39 +253,49 @@ export class DocumentSchema extends PothosSchema {
|
||||
args: {},
|
||||
resolve: async (query, _args, ctx, _info) => {
|
||||
if (ctx.isSubscription) {
|
||||
throw new Error('Not allowed')
|
||||
throw new Error("Not allowed");
|
||||
}
|
||||
const userId = ctx.http?.me?.id
|
||||
const userId = ctx.http?.me?.id;
|
||||
if (!userId) {
|
||||
throw new Error('User not found')
|
||||
throw new Error("User not found");
|
||||
}
|
||||
const document = await this.prisma.document.create({
|
||||
...query,
|
||||
data: {
|
||||
name: 'Untitled',
|
||||
fileUrl: '', // fileUrl,
|
||||
name: "Untitled",
|
||||
fileUrl: "", // fileUrl,
|
||||
ownerId: userId,
|
||||
},
|
||||
})
|
||||
return document
|
||||
});
|
||||
return document;
|
||||
},
|
||||
}),
|
||||
|
||||
testCheckGrammar: t.field({
|
||||
type: 'Boolean',
|
||||
type: "Boolean",
|
||||
args: {
|
||||
documentId: t.arg({ type: 'String', required: true }),
|
||||
pageId: t.arg({ type: 'Int', required: true }),
|
||||
documentId: t.arg({ type: "String", required: true }),
|
||||
pageId: t.arg({ type: "Int", required: true }),
|
||||
promptType: t.arg({
|
||||
type: this.builder.enumType('PromptType', {
|
||||
values: ['CHECK_GRAMMAR', 'REWRITE_TEXT', 'SUMMARIZE', 'TRANSLATE', 'EXPAND_CONTENT'] as const,
|
||||
type: this.builder.enumType("PromptType", {
|
||||
values: [
|
||||
"CHECK_GRAMMAR",
|
||||
"REWRITE_TEXT",
|
||||
"SUMMARIZE",
|
||||
"TRANSLATE",
|
||||
"EXPAND_CONTENT",
|
||||
] as const,
|
||||
}),
|
||||
required: true,
|
||||
}),
|
||||
},
|
||||
resolve: async (_query, args, _ctx: SchemaContext) => {
|
||||
await this.documentService.checkGrammarForPage(args.documentId, args.pageId, args.promptType as PromptType)
|
||||
return true
|
||||
await this.documentService.checkGrammarForPage(
|
||||
args.documentId,
|
||||
args.pageId,
|
||||
args.promptType as PromptType
|
||||
);
|
||||
return true;
|
||||
},
|
||||
}),
|
||||
|
||||
@@ -260,27 +340,32 @@ export class DocumentSchema extends PothosSchema {
|
||||
eventDocumentClientRequestSync: t.field({
|
||||
type: this.documentDelta(),
|
||||
args: {
|
||||
documentId: t.arg({ type: 'String', required: true }),
|
||||
pageIndex: t.arg({ type: 'Int', required: true }),
|
||||
documentId: t.arg({ type: "String", required: true }),
|
||||
pageIndex: t.arg({ type: "Int", required: true }),
|
||||
},
|
||||
resolve: async (_, args, ctx: SchemaContext) => {
|
||||
if (ctx.isSubscription) {
|
||||
throw new Error('Not allowed')
|
||||
throw new Error("Not allowed");
|
||||
}
|
||||
if (!ctx.http?.me?.id) {
|
||||
throw new Error('User not found')
|
||||
throw new Error("User not found");
|
||||
}
|
||||
if (!args.documentId) {
|
||||
throw new Error('Document id not found')
|
||||
throw new Error("Document id not found");
|
||||
}
|
||||
if (args.pageIndex === undefined || args.pageIndex === null) {
|
||||
throw new Error('Page index not found')
|
||||
throw new Error("Page index not found");
|
||||
}
|
||||
let delta = null
|
||||
let delta = null;
|
||||
try {
|
||||
delta = await this.minio.getDocumentPage(args.documentId, args.pageIndex)
|
||||
delta = await this.minio.getDocumentPage(
|
||||
args.documentId,
|
||||
args.pageIndex
|
||||
);
|
||||
} catch (_error) {}
|
||||
const totalPage = await this.minio.countDocumentPages(args.documentId)
|
||||
const totalPage = await this.minio.countDocumentPages(
|
||||
args.documentId
|
||||
);
|
||||
return {
|
||||
documentId: args.documentId,
|
||||
pageIndex: args.pageIndex,
|
||||
@@ -288,51 +373,51 @@ export class DocumentSchema extends PothosSchema {
|
||||
totalPage,
|
||||
senderId: ctx.http?.me?.id,
|
||||
eventType: DocumentEvent.CLIENT_REQUEST_SYNC,
|
||||
}
|
||||
};
|
||||
},
|
||||
}),
|
||||
}))
|
||||
}));
|
||||
|
||||
this.builder.mutationFields((t) => ({
|
||||
createDocument: t.prismaField({
|
||||
type: this.document(),
|
||||
args: {
|
||||
input: t.arg({
|
||||
type: this.builder.generator.getCreateInput('Document', [
|
||||
'id',
|
||||
'ownerId',
|
||||
'createdAt',
|
||||
'updatedAt',
|
||||
'collaborators',
|
||||
'owner',
|
||||
'fileUrl',
|
||||
'previewImageUrl',
|
||||
'name',
|
||||
type: this.builder.generator.getCreateInput("Document", [
|
||||
"id",
|
||||
"ownerId",
|
||||
"createdAt",
|
||||
"updatedAt",
|
||||
"collaborators",
|
||||
"owner",
|
||||
"fileUrl",
|
||||
"previewImageUrl",
|
||||
"name",
|
||||
]),
|
||||
required: false,
|
||||
}),
|
||||
},
|
||||
resolve: async (query, _parent, args, ctx: SchemaContext) => {
|
||||
if (ctx.isSubscription) {
|
||||
throw new Error('Not allowed')
|
||||
throw new Error("Not allowed");
|
||||
}
|
||||
const userId = ctx.http?.me?.id
|
||||
const userId = ctx.http?.me?.id;
|
||||
if (!userId) {
|
||||
throw new Error('Unauthorized')
|
||||
throw new Error("Unauthorized");
|
||||
}
|
||||
return await this.prisma.document.create({
|
||||
...query,
|
||||
data: {
|
||||
...args.input,
|
||||
name: args.input?.name ?? 'Untitled',
|
||||
fileUrl: '',
|
||||
name: args.input?.name ?? "Untitled",
|
||||
fileUrl: "",
|
||||
owner: {
|
||||
connect: {
|
||||
id: userId,
|
||||
},
|
||||
},
|
||||
},
|
||||
})
|
||||
});
|
||||
},
|
||||
}),
|
||||
|
||||
@@ -346,20 +431,32 @@ export class DocumentSchema extends PothosSchema {
|
||||
},
|
||||
resolve: async (_, args, ctx: SchemaContext) => {
|
||||
if (ctx.isSubscription) {
|
||||
throw new Error('Not allowed')
|
||||
throw new Error("Not allowed");
|
||||
}
|
||||
const {
|
||||
http: { pubSub },
|
||||
} = ctx
|
||||
const senderId = ctx.http?.me?.id
|
||||
} = ctx;
|
||||
const senderId = ctx.http?.me?.id;
|
||||
if (!senderId) {
|
||||
throw new Error('User not found')
|
||||
throw new Error("User not found");
|
||||
}
|
||||
if (args.data.cursor) {
|
||||
pubSub.publish(
|
||||
`${DocumentEvent.CURSOR_MOVED}.${args.data.documentId}`,
|
||||
{
|
||||
...args.data,
|
||||
senderId,
|
||||
eventType: DocumentEvent.CURSOR_MOVED,
|
||||
}
|
||||
);
|
||||
} else {
|
||||
pubSub.publish(`${DocumentEvent.CHANGED}.${args.data.documentId}`, {
|
||||
...args.data,
|
||||
senderId,
|
||||
})
|
||||
return args.data
|
||||
eventType: DocumentEvent.CHANGED,
|
||||
});
|
||||
}
|
||||
return args.data;
|
||||
},
|
||||
}),
|
||||
|
||||
@@ -370,58 +467,66 @@ export class DocumentSchema extends PothosSchema {
|
||||
},
|
||||
resolve: async (_, args, ctx: SchemaContext) => {
|
||||
if (ctx.isSubscription) {
|
||||
throw new Error('Not allowed')
|
||||
throw new Error("Not allowed");
|
||||
}
|
||||
const senderId = ctx.http?.me?.id
|
||||
const senderId = ctx.http?.me?.id;
|
||||
if (!args.data.documentId) {
|
||||
throw new Error('Document id not found')
|
||||
throw new Error("Document id not found");
|
||||
}
|
||||
if (!senderId) {
|
||||
throw new Error('User not found')
|
||||
throw new Error("User not found");
|
||||
}
|
||||
if (args.data.pageIndex === undefined || args.data.pageIndex === null) {
|
||||
throw new Error('Page index not found')
|
||||
if (
|
||||
args.data.pageIndex === undefined ||
|
||||
args.data.pageIndex === null
|
||||
) {
|
||||
throw new Error("Page index not found");
|
||||
}
|
||||
// save delta to minio
|
||||
const delta = args.data.delta
|
||||
if (!delta) {
|
||||
throw new Error('Delta not found')
|
||||
const delta = args.data.delta;
|
||||
if (delta) {
|
||||
await this.minio.upsertDocumentPage(
|
||||
args.data.documentId,
|
||||
args.data.pageIndex,
|
||||
delta
|
||||
);
|
||||
}
|
||||
await this.minio.upsertDocumentPage(args.data.documentId, args.data.pageIndex, delta)
|
||||
const totalPage = await this.minio.countDocumentPages(args.data.documentId)
|
||||
const totalPage = await this.minio.countDocumentPages(
|
||||
args.data.documentId
|
||||
);
|
||||
return {
|
||||
...args.data,
|
||||
totalPage,
|
||||
senderId: 'server',
|
||||
senderId: "server",
|
||||
eventType: DocumentEvent.SERVER_REQUEST_SYNC,
|
||||
}
|
||||
};
|
||||
},
|
||||
}),
|
||||
updateDocument: t.prismaField({
|
||||
type: this.document(),
|
||||
args: {
|
||||
documentId: t.arg({ type: 'String', required: true }),
|
||||
documentId: t.arg({ type: "String", required: true }),
|
||||
data: t.arg({
|
||||
type: this.builder.generator.getUpdateInput('Document', [
|
||||
'id',
|
||||
'ownerId',
|
||||
'createdAt',
|
||||
'updatedAt',
|
||||
'collaborationSessionId',
|
||||
'collaborationSession',
|
||||
'owner',
|
||||
'fileUrl',
|
||||
'previewImageUrl',
|
||||
type: this.builder.generator.getUpdateInput("Document", [
|
||||
"id",
|
||||
"ownerId",
|
||||
"createdAt",
|
||||
"updatedAt",
|
||||
"collaborationSessionId",
|
||||
"collaborationSession",
|
||||
"owner",
|
||||
"fileUrl",
|
||||
"previewImageUrl",
|
||||
]),
|
||||
required: true,
|
||||
}),
|
||||
},
|
||||
resolve: async (query, _parent, args, ctx: SchemaContext) => {
|
||||
if (ctx.isSubscription) {
|
||||
throw new Error('Not allowed')
|
||||
throw new Error("Not allowed");
|
||||
}
|
||||
if (!ctx.http?.me?.id) {
|
||||
throw new Error('Unauthorized')
|
||||
throw new Error("Unauthorized");
|
||||
}
|
||||
// check if user is owner or collaborator
|
||||
const document = await this.prisma.document.findUnique({
|
||||
@@ -429,45 +534,47 @@ export class DocumentSchema extends PothosSchema {
|
||||
include: {
|
||||
collaborators: true,
|
||||
},
|
||||
})
|
||||
});
|
||||
if (!document) {
|
||||
throw new Error('Document not found')
|
||||
throw new Error("Document not found");
|
||||
}
|
||||
if (
|
||||
!document.isPublic &&
|
||||
!document.collaborators.some((c) => c.userId === ctx.http?.me?.id && c.writable) &&
|
||||
!document.collaborators.some(
|
||||
(c) => c.userId === ctx.http?.me?.id && c.writable
|
||||
) &&
|
||||
document.ownerId !== ctx.http?.me?.id
|
||||
) {
|
||||
throw new Error('User is not owner or collaborator of document')
|
||||
throw new Error("User is not owner or collaborator of document");
|
||||
}
|
||||
return await this.prisma.document.update({
|
||||
...query,
|
||||
where: { id: args.documentId },
|
||||
data: args.data,
|
||||
})
|
||||
});
|
||||
},
|
||||
}),
|
||||
addCollaborator: t.prismaField({
|
||||
type: this.documentCollaborator(),
|
||||
args: {
|
||||
documentId: t.arg({ type: 'String', required: true }),
|
||||
userId: t.arg({ type: 'String', required: true }),
|
||||
readable: t.arg({ type: 'Boolean', required: true }),
|
||||
writable: t.arg({ type: 'Boolean', required: true }),
|
||||
documentId: t.arg({ type: "String", required: true }),
|
||||
userId: t.arg({ type: "String", required: true }),
|
||||
readable: t.arg({ type: "Boolean", required: true }),
|
||||
writable: t.arg({ type: "Boolean", required: true }),
|
||||
},
|
||||
resolve: async (_, __, args, ctx: SchemaContext) => {
|
||||
if (ctx.isSubscription) {
|
||||
throw new Error('Not allowed')
|
||||
throw new Error("Not allowed");
|
||||
}
|
||||
// check if ctx user is owner of document
|
||||
const document = await this.prisma.document.findUnique({
|
||||
where: { id: args.documentId },
|
||||
})
|
||||
});
|
||||
if (!document) {
|
||||
throw new Error('Document not found')
|
||||
throw new Error("Document not found");
|
||||
}
|
||||
if (document.ownerId !== ctx.http?.me?.id) {
|
||||
throw new Error('User is not owner of document')
|
||||
throw new Error("User is not owner of document");
|
||||
}
|
||||
return await this.prisma.documentCollaborator.create({
|
||||
data: {
|
||||
@@ -476,133 +583,149 @@ export class DocumentSchema extends PothosSchema {
|
||||
readable: args.readable,
|
||||
writable: args.writable,
|
||||
},
|
||||
})
|
||||
});
|
||||
},
|
||||
}),
|
||||
removeCollaborator: t.prismaField({
|
||||
type: this.documentCollaborator(),
|
||||
args: {
|
||||
documentId: t.arg({ type: 'String', required: true }),
|
||||
userId: t.arg({ type: 'String', required: true }),
|
||||
documentId: t.arg({ type: "String", required: true }),
|
||||
userId: t.arg({ type: "String", required: true }),
|
||||
},
|
||||
resolve: async (_, __, args, ctx: SchemaContext) => {
|
||||
if (ctx.isSubscription) {
|
||||
throw new Error('Not allowed')
|
||||
throw new Error("Not allowed");
|
||||
}
|
||||
// check if ctx user is owner of document
|
||||
const document = await this.prisma.document.findUnique({
|
||||
where: { id: args.documentId },
|
||||
})
|
||||
});
|
||||
if (!document) {
|
||||
throw new Error('Document not found')
|
||||
throw new Error("Document not found");
|
||||
}
|
||||
if (document.ownerId !== ctx.http?.me?.id) {
|
||||
throw new Error('User is not owner of document')
|
||||
throw new Error("User is not owner of document");
|
||||
}
|
||||
return await this.prisma.documentCollaborator.delete({
|
||||
where: { documentId_userId: { documentId: args.documentId, userId: args.userId } },
|
||||
})
|
||||
where: {
|
||||
documentId_userId: {
|
||||
documentId: args.documentId,
|
||||
userId: args.userId,
|
||||
},
|
||||
},
|
||||
});
|
||||
},
|
||||
}),
|
||||
editCollaboratorPermission: t.prismaField({
|
||||
type: this.documentCollaborator(),
|
||||
args: {
|
||||
documentId: t.arg({ type: 'String', required: true }),
|
||||
userId: t.arg({ type: 'String', required: true }),
|
||||
readable: t.arg({ type: 'Boolean', required: true }),
|
||||
writable: t.arg({ type: 'Boolean', required: true }),
|
||||
documentId: t.arg({ type: "String", required: true }),
|
||||
userId: t.arg({ type: "String", required: true }),
|
||||
readable: t.arg({ type: "Boolean", required: true }),
|
||||
writable: t.arg({ type: "Boolean", required: true }),
|
||||
},
|
||||
resolve: async (_, __, args, ctx: SchemaContext) => {
|
||||
if (ctx.isSubscription) {
|
||||
throw new Error('Not allowed')
|
||||
throw new Error("Not allowed");
|
||||
}
|
||||
// check if ctx user is owner of document
|
||||
const document = await this.prisma.document.findUnique({
|
||||
where: { id: args.documentId },
|
||||
})
|
||||
});
|
||||
if (!document) {
|
||||
throw new Error('Document not found')
|
||||
throw new Error("Document not found");
|
||||
}
|
||||
if (document.ownerId !== ctx.http?.me?.id) {
|
||||
throw new Error('User is not owner of document')
|
||||
throw new Error("User is not owner of document");
|
||||
}
|
||||
return await this.prisma.documentCollaborator.update({
|
||||
where: { documentId_userId: { documentId: args.documentId, userId: args.userId } },
|
||||
where: {
|
||||
documentId_userId: {
|
||||
documentId: args.documentId,
|
||||
userId: args.userId,
|
||||
},
|
||||
},
|
||||
data: { readable: args.readable, writable: args.writable },
|
||||
})
|
||||
});
|
||||
},
|
||||
}),
|
||||
updateDocumentPreviewImage: t.prismaField({
|
||||
type: this.document(),
|
||||
args: {
|
||||
documentId: t.arg({ type: 'String', required: true }),
|
||||
imageId: t.arg({ type: 'String', required: true }),
|
||||
documentId: t.arg({ type: "String", required: true }),
|
||||
imageId: t.arg({ type: "String", required: true }),
|
||||
},
|
||||
resolve: async (query, _parent, args, ctx: SchemaContext) => {
|
||||
if (ctx.isSubscription) {
|
||||
throw new Error('Not allowed')
|
||||
throw new Error("Not allowed");
|
||||
}
|
||||
const document = await this.prisma.document.findUnique({
|
||||
where: { id: args.documentId },
|
||||
include: {
|
||||
collaborators: true,
|
||||
},
|
||||
})
|
||||
});
|
||||
if (!document) {
|
||||
throw new Error('Document not found')
|
||||
throw new Error("Document not found");
|
||||
}
|
||||
if (
|
||||
document.ownerId !== ctx.http?.me?.id &&
|
||||
!document.collaborators.some((c) => c.userId === ctx.http?.me?.id && (c.writable || c.readable))
|
||||
!document.collaborators.some(
|
||||
(c) => c.userId === ctx.http?.me?.id && (c.writable || c.readable)
|
||||
)
|
||||
) {
|
||||
throw new Error('User is not owner or collaborator of document')
|
||||
throw new Error("User is not owner or collaborator of document");
|
||||
}
|
||||
// check if imageId is exist
|
||||
const image = await this.prisma.uploadedFile.findUnique({
|
||||
where: { id: args.imageId },
|
||||
})
|
||||
});
|
||||
if (!image) {
|
||||
throw new Error('Image not found')
|
||||
throw new Error("Image not found");
|
||||
}
|
||||
return await this.prisma.document.update({
|
||||
...query,
|
||||
where: { id: args.documentId },
|
||||
data: { previewImageId: args.imageId },
|
||||
})
|
||||
});
|
||||
},
|
||||
}),
|
||||
}))
|
||||
}));
|
||||
|
||||
this.builder.subscriptionFields((t) => ({
|
||||
document: t.field({
|
||||
type: this.documentDelta(),
|
||||
args: {
|
||||
documentId: t.arg({
|
||||
type: 'String',
|
||||
type: "String",
|
||||
required: true,
|
||||
}),
|
||||
},
|
||||
subscribe: async (_, args, ctx: SchemaContext) => {
|
||||
if (!ctx.isSubscription) {
|
||||
throw new Error('Not allowed')
|
||||
throw new Error("Not allowed");
|
||||
}
|
||||
const documentId = args.documentId
|
||||
const documentId = args.documentId;
|
||||
// check user permission
|
||||
const document = await this.prisma.document.findUnique({
|
||||
where: { id: documentId },
|
||||
include: {
|
||||
collaborators: true,
|
||||
},
|
||||
})
|
||||
});
|
||||
if (!document) {
|
||||
throw new Error('Document not found')
|
||||
throw new Error("Document not found");
|
||||
}
|
||||
if (!document.isPublic) {
|
||||
if (
|
||||
document.ownerId !== ctx.websocket?.me?.id &&
|
||||
!document.collaborators.some((c) => c.userId === ctx.websocket?.me?.id && (c.writable || c.readable))
|
||||
!document.collaborators.some(
|
||||
(c) =>
|
||||
c.userId === ctx.websocket?.me?.id &&
|
||||
(c.writable || c.readable)
|
||||
)
|
||||
) {
|
||||
throw new Error('User is not owner or collaborator of document')
|
||||
throw new Error("User is not owner or collaborator of document");
|
||||
}
|
||||
}
|
||||
return ctx.websocket.pubSub.asyncIterator([
|
||||
@@ -613,87 +736,108 @@ export class DocumentSchema extends PothosSchema {
|
||||
`${DocumentEvent.PAGE_DELETED}.${documentId}`,
|
||||
`${DocumentEvent.ACTIVE_DOCUMENT_ID_CHANGED}.${documentId}`,
|
||||
`${DocumentEvent.AI_SUGGESTION}.${documentId}`,
|
||||
]) as unknown as AsyncIterable<DocumentDelta>
|
||||
`${DocumentEvent.CURSOR_MOVED}.${documentId}`,
|
||||
]) as unknown as AsyncIterable<DocumentDelta>;
|
||||
},
|
||||
resolve: async (payload: DocumentDelta, _args, ctx: SchemaContext) => {
|
||||
if (!ctx.isSubscription) {
|
||||
throw new Error('Not allowed')
|
||||
throw new Error("Not allowed");
|
||||
}
|
||||
|
||||
// If there's an explicit sync request, pass it through immediately
|
||||
if (payload.requestSync) {
|
||||
return payload
|
||||
return payload;
|
||||
}
|
||||
|
||||
// Only perform background check with a very low probability
|
||||
const SYNC_PROBABILITY = 0.01 // 0.1% chance
|
||||
const SYNC_INTERVAL_MS = 15000 // 15 seconds minimum between syncs
|
||||
const SYNC_PROBABILITY = 0.01; // 0.1% chance
|
||||
const SYNC_INTERVAL_MS = 15000; // 15 seconds minimum between syncs
|
||||
|
||||
const syncKey = `document:sync:${payload.documentId}:${payload.pageIndex}`
|
||||
const syncKey = `document:sync:${payload.documentId}:${payload.pageIndex}`;
|
||||
|
||||
try {
|
||||
// Quick, non-blocking check for potential background processing
|
||||
const lastSyncTimeStr = await this.redis.get(syncKey)
|
||||
const lastSyncTime = lastSyncTimeStr ? parseInt(lastSyncTimeStr, 10) : 0
|
||||
const currentTime = DateTimeUtils.now().toMillis()
|
||||
const lastSyncTimeStr = await this.redis.get(syncKey);
|
||||
const lastSyncTime = lastSyncTimeStr
|
||||
? parseInt(lastSyncTimeStr, 10)
|
||||
: 0;
|
||||
const currentTime = DateTimeUtils.now().toMillis();
|
||||
|
||||
// Only proceed if enough time has passed and we hit the random chance
|
||||
if (currentTime - lastSyncTime >= SYNC_INTERVAL_MS && Math.random() <= SYNC_PROBABILITY) {
|
||||
if (
|
||||
currentTime - lastSyncTime >= SYNC_INTERVAL_MS &&
|
||||
Math.random() <= SYNC_PROBABILITY
|
||||
) {
|
||||
// Fire and forget - don't await or block
|
||||
this.backgroundGrammarCheck(payload.documentId, payload.pageIndex, ctx)
|
||||
this.backgroundGrammarCheck(
|
||||
payload.documentId,
|
||||
payload.pageIndex,
|
||||
ctx
|
||||
);
|
||||
}
|
||||
// biome-ignore lint/suspicious/noExplicitAny: error is any
|
||||
} catch (error: any) {
|
||||
Logger.error('Background sync check failed', {
|
||||
Logger.error("Background sync check failed", {
|
||||
documentId: payload.documentId,
|
||||
error: error.message,
|
||||
})
|
||||
});
|
||||
}
|
||||
// Always return payload immediately
|
||||
return payload
|
||||
return payload;
|
||||
},
|
||||
}),
|
||||
}))
|
||||
}));
|
||||
}
|
||||
|
||||
// Separate method for background processing
|
||||
private async backgroundGrammarCheck(documentId: string, pageIndex: number, ctx: SchemaContext): Promise<void> {
|
||||
private async backgroundGrammarCheck(
|
||||
documentId: string,
|
||||
pageIndex: number,
|
||||
ctx: SchemaContext
|
||||
): Promise<void> {
|
||||
if (!ctx.isSubscription) {
|
||||
throw new Error('Not allowed')
|
||||
throw new Error("Not allowed");
|
||||
}
|
||||
try {
|
||||
const syncKey = `document:sync:${documentId}:${pageIndex}`
|
||||
const currentTime = DateTimeUtils.now().toMillis().toString()
|
||||
const syncKey = `document:sync:${documentId}:${pageIndex}`;
|
||||
const currentTime = DateTimeUtils.now().toMillis().toString();
|
||||
|
||||
// Update sync time immediately to prevent multiple concurrent checks
|
||||
await this.redis.setPX(syncKey, currentTime, 60000)
|
||||
await this.redis.setPX(syncKey, currentTime, 60000);
|
||||
|
||||
Logger.log('Initiating background grammar check', {
|
||||
Logger.log("Initiating background grammar check", {
|
||||
documentId,
|
||||
pageIndex,
|
||||
})
|
||||
});
|
||||
|
||||
// Perform grammar check
|
||||
const delta = await this.documentService.checkGrammarForPage(documentId, pageIndex, PromptType.CHECK_GRAMMAR)
|
||||
const delta = await this.documentService.checkGrammarForPage(
|
||||
documentId,
|
||||
pageIndex,
|
||||
PromptType.CHECK_GRAMMAR
|
||||
);
|
||||
if (!delta) {
|
||||
return
|
||||
return;
|
||||
}
|
||||
|
||||
// Optionally publish AI suggestion if needed
|
||||
ctx.websocket.pubSub.publish(`${DocumentEvent.AI_SUGGESTION}.${documentId}`, {
|
||||
ctx.websocket.pubSub.publish(
|
||||
`${DocumentEvent.AI_SUGGESTION}.${documentId}`,
|
||||
{
|
||||
documentId,
|
||||
pageIndex,
|
||||
eventType: DocumentEvent.AI_SUGGESTION,
|
||||
delta,
|
||||
senderId: 'system',
|
||||
})
|
||||
senderId: "system",
|
||||
}
|
||||
);
|
||||
// biome-ignore lint/suspicious/noExplicitAny: error is any
|
||||
} catch (error: any) {
|
||||
Logger.error('Background grammar check failed', {
|
||||
Logger.error("Background grammar check failed", {
|
||||
documentId,
|
||||
pageIndex,
|
||||
error: error.message,
|
||||
})
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user