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