import { Inject, Injectable, Logger } from '@nestjs/common' import { Pothos, PothosRef, PothosSchema, SchemaBuilderToken } from '@smatch-corp/nestjs-pothos' import { Builder, SchemaContext } from '../Graphql/graphql.builder' import { PrismaService } from '../Prisma/prisma.service' import { DocumentEvent } from './document.event' import { Document } from '@prisma/client' import { DocumentDelta } from './document.type' import Delta from 'quill-delta' import { MinioService } from 'src/Minio/minio.service' @Injectable() export class DocumentSchema extends PothosSchema { constructor( @Inject(SchemaBuilderToken) private readonly builder: Builder, private readonly prisma: PrismaService, private readonly minio: MinioService, ) { super() } @PothosRef() document() { return this.builder.prismaObject('Document', { fields: (t) => ({ id: t.exposeID('id'), name: t.exposeString('name'), fileUrl: t.exposeString('fileUrl'), createdAt: t.expose('createdAt', { type: 'DateTime' }), updatedAt: t.expose('updatedAt', { type: 'DateTime' }), owner: t.relation('owner'), ownerId: t.exposeID('ownerId'), collaborators: t.relation('collaborators'), isPublic: t.exposeBoolean('isPublic'), }), }) } @PothosRef() 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 }), }), }) } @PothosRef() documentDelta() { return this.builder.simpleObject('DocumentDelta', { fields: (t) => ({ eventType: t.string(), documentId: t.string({ nullable: true, }), pageIndex: t.int({ nullable: true, }), delta: t.field({ type: 'Delta', nullable: true, }), senderId: t.string({ nullable: true, }), }), }) } @PothosRef() documentDeltaInput() { return this.builder.inputType('DocumentDeltaInput', { fields: (t) => ({ documentId: t.string(), pageIndex: t.int(), delta: t.field({ type: 'Delta' }), }), }) } @Pothos() init(): void { this.builder.queryFields((t) => ({ myDocuments: t.prismaField({ type: [this.document()], args: this.builder.generator.findManyArgs('Document'), resolve: async (query, _parent, args, ctx: SchemaContext) => { if (ctx.isSubscription) throw new Error('Not allowed') if (!ctx.http?.me?.id) 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 } } }], }, }) }, }), document: t.prismaField({ type: this.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'), resolve: async (query, _root, args) => { return await this.prisma.document.findMany({ ...query, skip: args.skip ?? undefined, take: args.take ?? undefined, orderBy: args.orderBy ?? undefined, where: args.filter ?? undefined, }) }, }), newDocument: t.field({ type: this.document(), args: {}, resolve: async (query, _args, ctx, _info) => { if (ctx.isSubscription) throw new Error('Not allowed') const userId = ctx.http?.me?.id if (!userId) throw new Error('User not found') const fileUrl = await this.minio.getFileUrl('document', 'document', 'document') if (!fileUrl) throw new Error('File not found') const document = await this.prisma.document.create({ ...query, data: { name: 'Untitled', fileUrl, ownerId: userId, }, }) return document }, }), })) 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', ]), required: false, }), }, resolve: async (query, _parent, args, ctx: SchemaContext) => { if (ctx.isSubscription) throw new Error('Not allowed') const userId = ctx.http?.me?.id if (!userId) throw new Error('User not found') return await this.prisma.document.create({ ...query, data: { ...args.input, name: args.input?.name ?? 'Untitled', fileUrl: '', owner: { connect: { id: userId, }, }, }, }) }, }), testUpdateDocument: t.field({ type: this.documentDelta(), args: { documentId: t.arg({ type: 'String', required: true }), pageIndex: t.arg({ type: 'Int', required: true }), }, resolve: async (_root, args, ctx: SchemaContext) => { if (ctx.isSubscription) throw new Error('Not allowed') const delta = new Delta().insert('test') const documentDelta = { documentId: args.documentId, pageIndex: args.pageIndex, delta, senderId: ctx.http?.me?.id, } ctx.http.pubSub.publish(`${DocumentEvent.CHANGED}.${args.documentId}`, documentDelta) return documentDelta }, }), eventUpdateDocument: t.field({ type: this.documentDelta(), args: { data: t.arg({ type: this.documentDeltaInput(), required: true, }), }, resolve: async (_, args, ctx: SchemaContext) => { if (ctx.isSubscription) throw new Error('Not allowed') const { http: { pubSub }, } = ctx const senderId = ctx.http?.me?.id if (!senderId) throw new Error('User not found') pubSub.publish(`${DocumentEvent.CHANGED}.${args.data.documentId}`, { ...args.data, senderId, }) return args.data }, }), updateDocument: t.prismaField({ type: this.document(), args: { 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', ]), required: true, }), }, resolve: async (query, _parent, args, ctx: SchemaContext) => { if (ctx.isSubscription) throw new Error('Not allowed') if (!ctx.http?.me?.id) throw new Error('User not found') // check if user is owner or collaborator const document = await this.prisma.document.findUnique({ where: { id: args.documentId }, include: { collaborators: true, }, }) if (!document) throw new Error('Document not found') if ( document.ownerId !== ctx.http?.me?.id && !document.isPublic && !document.collaborators.some((c) => c.userId === ctx.http?.me?.id && c.writable) ) 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 }), }, resolve: async (_, __, args, ctx: SchemaContext) => { if (ctx.isSubscription) 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') if (document.ownerId !== ctx.http?.me?.id) throw new Error('User is not owner of document') return await this.prisma.documentCollaborator.create({ data: { documentId: args.documentId, userId: args.userId, readable: args.readable, writable: args.writable, }, }) }, }), })) this.builder.subscriptionFields((t) => ({ document: t.field({ type: this.documentDelta(), args: { documentId: t.arg({ type: 'String', required: true, }), }, subscribe: async (_, args, ctx: SchemaContext) => { if (!ctx.isSubscription) throw new Error('Not allowed') const { websocket: { pubSub }, } = ctx 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') if (!document.isPublic) { if ( document.ownerId !== ctx.websocket?.me?.id && !document.collaborators.some((c) => c.userId === ctx.websocket?.me?.id && c.writable) ) throw new Error('User is not owner or collaborator of document') } return pubSub.asyncIterator([ `${DocumentEvent.CHANGED}.${documentId}`, `${DocumentEvent.DELETED}.${documentId}`, `${DocumentEvent.SAVED}.${documentId}`, `${DocumentEvent.PAGE_CREATED}.${documentId}`, `${DocumentEvent.PAGE_DELETED}.${documentId}`, `${DocumentEvent.ACTIVE_DOCUMENT_ID_CHANGED}.${documentId}`, ]) as unknown as AsyncIterable }, resolve: async (payload: DocumentDelta, _args, ctx: SchemaContext) => { if (!ctx.isSubscription) throw new Error('Not allowed') if (payload.senderId === ctx.websocket?.me?.id) return return payload }, }), })) } }