feat: enhance DocumentSchema and DocumentService with Redis integration and background processing

- Updated DocumentSchema to include RedisService for improved background processing of grammar checks.
- Refactored checkGrammarForPage method in DocumentService to utilize Promise.all for parallel processing and error handling.
- Introduced background grammar check functionality with a low probability trigger, enhancing performance and user experience.
- Added new utility methods for better time management and error logging.

These changes improve the efficiency and responsiveness of the grammar checking feature, leveraging Redis for state management and background processing.
This commit is contained in:
2024-12-14 21:54:43 +07:00
parent 114998ea7e
commit c886d9a02f
9 changed files with 340 additions and 163 deletions

View File

@@ -4,6 +4,8 @@ import { Pothos, PothosRef, PothosSchema, SchemaBuilderToken } from '@smatch-cor
import Delta from 'quill-delta' import Delta from 'quill-delta'
import { MinioService } from 'src/Minio/minio.service' import { MinioService } from 'src/Minio/minio.service'
import { PromptType } from 'src/OpenAI/openai.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 { Builder, SchemaContext } from '../Graphql/graphql.builder'
import { PrismaService } from '../Prisma/prisma.service' import { PrismaService } from '../Prisma/prisma.service'
import { DocumentEvent } from './document.event' import { DocumentEvent } from './document.event'
@@ -16,6 +18,7 @@ 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,
) { ) {
super() super()
} }
@@ -210,7 +213,7 @@ export class DocumentSchema extends PothosSchema {
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(args.documentId, args.pageId, args.promptType as PromptType)
return true return true
}, },
@@ -616,21 +619,81 @@ export class DocumentSchema extends PothosSchema {
if (!ctx.isSubscription) { if (!ctx.isSubscription) {
throw new Error('Not allowed') throw new Error('Not allowed')
} }
if (!payload.requestSync) {
// using randomize sync mechanism to avoid performance issue // If there's an explicit sync request, pass it through immediately
const random = Math.random() if (payload.requestSync) {
// 0.5% chance to request sync return payload
if (random <= 0.005) {
// check grammar too
// this.documentService.checkGrammarForPage(payload.documentId, payload.pageIndex)
Logger.log('request sync', 'request sync')
payload.requestSync = true
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 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()
// Only proceed if enough time has passed and we hit the random chance
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)
}
// biome-ignore lint/suspicious/noExplicitAny: error is any
} catch (error: any) {
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> {
if (!ctx.isSubscription) {
throw new Error('Not allowed')
}
try {
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)
Logger.log('Initiating background grammar check', {
documentId,
pageIndex,
})
// Perform grammar check
const delta = await this.documentService.checkGrammarForPage(documentId, pageIndex, PromptType.CHECK_GRAMMAR)
if (!delta) {
return
}
// Optionally publish AI suggestion if needed
ctx.websocket.pubSub.publish(`${DocumentEvent.AI_SUGGESTION}.${documentId}`, {
documentId,
pageIndex,
eventType: DocumentEvent.AI_SUGGESTION,
delta,
senderId: 'system',
})
// biome-ignore lint/suspicious/noExplicitAny: error is any
} catch (error: any) {
Logger.error('Background grammar check failed', {
documentId,
pageIndex,
error: error.message,
})
}
}
} }

View File

@@ -19,6 +19,7 @@ export type GrammarCheckResult = {
enum customAttributes { enum customAttributes {
AI_SUGGESTION = 'ai-suggestion', AI_SUGGESTION = 'ai-suggestion',
IGNORE = 'ignore',
} }
export const blacklist = [ export const blacklist = [
@@ -50,7 +51,6 @@ export const blacklist = [
'_', '_',
' ', ' ',
'\n', '\n',
'\n\n',
'\t', '\t',
'\r', '\r',
'\v', '\v',
@@ -67,58 +67,89 @@ export class DocumentService {
) {} ) {}
// check grammar for a page by parallely send each sentence to OpenAI service as text and get the result, after that return the result as delta and publish the result to the document // check grammar for a page by parallely send each sentence to OpenAI service as text and get the result, after that return the result as delta and publish the result to the document
async checkGrammarForPage(documentId: string, pageId: number , promptType: PromptType): Promise<void> { async checkGrammarForPage(documentId: string, pageId: number, promptType: PromptType): Promise<Delta | null> {
const content = await this.minio.getDocumentContent(documentId, pageId) try {
content.ops.forEach(async (op) => { // Fetch document content safely
if (typeof op.insert !== 'string') { const content = await this.minio.getDocumentContent(documentId, pageId)
return
} // Use Promise.all for parallel processing and proper error handling
if (!this.isSentence(op) && blacklist.includes(op.insert)) { const grammarCheckPromises = content.ops
return .filter(
} (op) =>
// check if the sentence is already corrected by checking the attributes typeof op.insert === 'string' &&
if (op.attributes?.[customAttributes.AI_SUGGESTION]) { this.isSentence(op) &&
return !blacklist.includes(op.insert) &&
} !op.attributes?.[customAttributes.AI_SUGGESTION],
const originalDelta = new Delta().push(op) )
Logger.log(op.insert, 'op.insert') .map(async (op) => {
Logger.log(promptType, 'promptType') try {
const grammarCheckResult = await this.openai.processText(op.insert, promptType) if (!op.insert || typeof op.insert !== 'string') {
if (!grammarCheckResult) { return null
return }
} // Process text with OpenAI
Logger.log(grammarCheckResult, 'grammarCheckResult') const grammarCheckResult = await this.openai.processText(op.insert, promptType)
// create new delta and maintain the original delta attributes
const newDelta = new Delta().push(op) if (!grammarCheckResult) {
newDelta.ops[0].attributes = originalDelta.ops[0].attributes return null
newDelta.ops[0].insert = grammarCheckResult }
// compose the original delta with the grammarCheckResult // Trim and normalize the comparison
// const correctedDelta = originalDelta.ops[0].insert = grammarCheckResult if (
// calculate where to insert the correctedDelta grammarCheckResult.trim() === op.insert.trim() ||
// const index = content.ops.findIndex((op) => op.insert === op.insert) // Optional: Add more sophisticated comparison if needed
// content.ops.splice(index, 0, newDelta) this.areSentencesEquivalent(grammarCheckResult, op.insert)
Logger.log(JSON.stringify(newDelta), 'newDelta') ) {
return null
}
// Create new delta with only corrected attribute
const newDelta = new Delta().push({
// publish the result to the subscriber ...op,
const payload: DocumentDelta = { insert: op.insert,
documentId, attributes: {
pageIndex: pageId, ...(op.attributes || {}),
eventType: DocumentEvent.AI_SUGGESTION, [customAttributes.AI_SUGGESTION]: true,
delta: newDelta, corrected: grammarCheckResult,
senderId: 'system', },
} })
await this.pubSub.publish(`${DocumentEvent.AI_SUGGESTION}.${documentId}`, payload) return newDelta
}) } catch (error) {
Logger.error(`Grammar check failed for op: ${op.insert}`, error)
return null
}
})
// Wait for all grammar checks to complete
const results = await Promise.all(grammarCheckPromises)
// Return a combined delta of all processed results
const combinedDelta = results.reduce((acc, result) => {
if (result) {
return (acc || new Delta()).compose(result)
}
return acc
}, new Delta())
return combinedDelta && combinedDelta.ops.length > 0 ? combinedDelta : null
} catch (error) {
Logger.error(`Grammar check for document ${documentId}, page ${pageId} failed`, error)
return null
}
} }
isSentence(op: Op) { isSentence(op: Op) {
if (typeof op.insert !== 'string') { if (typeof op.insert !== 'string') {
return false return false
} }
return op.insert?.match(/^[A-Z]/i) const match = op.insert?.match(/^[A-Z]/i)
Logger.log(`Match: ${match}`, 'DocumentService')
return match && match.index === 0
}
// Optional helper method for more nuanced comparison
private areSentencesEquivalent(s1: string, s2: string): boolean {
// Remove extra whitespace
const normalize = (s: string) => s.replace(/\s+/g, ' ').trim()
return normalize(s1) === normalize(s2)
} }
} }

View File

@@ -1,67 +1,72 @@
import { Inject, Injectable, Logger } from '@nestjs/common' import { Inject, Injectable, Logger } from "@nestjs/common";
import { Pothos, PothosRef, PothosSchema, SchemaBuilderToken } from '@smatch-corp/nestjs-pothos' import {
import { Builder, SchemaContext } from 'src/Graphql/graphql.builder' Pothos,
import { LiveKitService } from 'src/LiveKit/livekit.service' PothosRef,
import { MinioService } from 'src/Minio/minio.service' PothosSchema,
import { PrismaService } from 'src/Prisma/prisma.service' SchemaBuilderToken,
} from "@smatch-corp/nestjs-pothos";
import { Builder, SchemaContext } from "src/Graphql/graphql.builder";
import { LiveKitService } from "src/LiveKit/livekit.service";
import { MinioService } from "src/Minio/minio.service";
import { PrismaService } from "src/Prisma/prisma.service";
@Injectable() @Injectable()
export class MeetingRoomSchema extends PothosSchema { export class MeetingRoomSchema extends PothosSchema {
constructor( constructor(
@Inject(SchemaBuilderToken) private readonly builder: Builder, @Inject(SchemaBuilderToken) private readonly builder: Builder,
private readonly prisma: PrismaService, private readonly prisma: PrismaService,
private readonly livekitService: LiveKitService, private readonly livekitService: LiveKitService,
private readonly minioService: MinioService, private readonly minioService: MinioService
) { ) {
super() super();
} }
@PothosRef() @PothosRef()
meetingRoom() { meetingRoom() {
return this.builder.prismaObject('MeetingRoom', { return this.builder.prismaObject("MeetingRoom", {
fields: (t) => ({ fields: (t) => ({
id: t.exposeID('id'), id: t.exposeID("id"),
collaborationSessionId: t.exposeString('collaborationSessionId'), collaborationSessionId: t.exposeString("collaborationSessionId"),
collaborationSession: t.relation('collaborationSession'), collaborationSession: t.relation("collaborationSession"),
collaborators: t.relation('collaborators'), collaborators: t.relation("collaborators"),
createdAt: t.expose('createdAt', { type: 'DateTime' }), createdAt: t.expose("createdAt", { type: "DateTime" }),
updatedAt: t.expose('updatedAt', { type: 'DateTime' }), updatedAt: t.expose("updatedAt", { type: "DateTime" }),
recordUrl: t.string({ recordUrl: t.string({
nullable: true, nullable: true,
resolve: async (meetingRoom) => { resolve: async (meetingRoom) => {
return await this.minioService.getRoomRecordUrl(meetingRoom.id) return await this.minioService.getRoomRecordUrl(meetingRoom.id);
}, },
}), }),
}), }),
}) });
} }
@PothosRef() @PothosRef()
meetingRoomJoinInfo() { meetingRoomJoinInfo() {
return this.builder.simpleObject('MeetingRoomJoinInfo', { return this.builder.simpleObject("MeetingRoomJoinInfo", {
fields: (t) => ({ fields: (t) => ({
id: t.string({ id: t.string({
description: 'The ID of the meeting room.', description: "The ID of the meeting room.",
}), }),
token: t.string({ token: t.string({
description: 'The token to join the meeting room.', description: "The token to join the meeting room.",
}), }),
serverUrl: t.string({ serverUrl: t.string({
description: 'The URL of the server.', description: "The URL of the server.",
}), }),
}), }),
}) });
} }
@PothosRef() @PothosRef()
meetingRoomCollaborator() { meetingRoomCollaborator() {
return this.builder.prismaObject('MeetingRoomCollaborator', { return this.builder.prismaObject("MeetingRoomCollaborator", {
fields: (t) => ({ fields: (t) => ({
id: t.exposeID('id'), id: t.exposeID("id"),
meetingRoomId: t.exposeString('meetingRoomId'), meetingRoomId: t.exposeString("meetingRoomId"),
meetingRoom: t.relation('meetingRoom'), meetingRoom: t.relation("meetingRoom"),
userId: t.exposeString('userId'), userId: t.exposeString("userId"),
user: t.relation('user'), user: t.relation("user"),
}), }),
}) });
} }
@Pothos() @Pothos()
@@ -77,29 +82,30 @@ export class MeetingRoomSchema extends PothosSchema {
}, },
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 collaborationSession = await this.prisma.collaborationSession.findUnique({ const collaborationSession =
where: { await this.prisma.collaborationSession.findUnique({
scheduleDateId: args.scheduleDateId, where: {
}, scheduleDateId: args.scheduleDateId,
}) },
});
if (!collaborationSession) { if (!collaborationSession) {
throw new Error('Collaboration session not found') throw new Error("Collaboration session not found");
} }
const meetingRoom = await this.prisma.meetingRoom.findUnique({ const meetingRoom = await this.prisma.meetingRoom.findUnique({
where: { where: {
collaborationSessionId: collaborationSession.id, collaborationSessionId: collaborationSession.id,
}, },
}) });
if (meetingRoom) { if (meetingRoom) {
return meetingRoom return meetingRoom;
} }
return await this.prisma.meetingRoom.create({ return await this.prisma.meetingRoom.create({
data: { data: {
collaborationSessionId: collaborationSession.id, collaborationSessionId: collaborationSession.id,
}, },
}) });
}, },
}), }),
// get meeting room info by room id and check if user is collaborator of collaboration session then create new token and return it, // get meeting room info by room id and check if user is collaborator of collaboration session then create new token and return it,
@@ -113,62 +119,91 @@ export class MeetingRoomSchema 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");
} }
if (!ctx.http.me) { if (!ctx.http.me) {
throw new Error('Unauthorized') throw new Error("Unauthorized");
} }
const meetingRoom = await this.prisma.meetingRoom.findUnique({ const meetingRoom = await this.prisma.meetingRoom.findUnique({
where: { collaborationSessionId: args.collaborationSessionId }, where: { collaborationSessionId: args.collaborationSessionId },
}) });
if (!meetingRoom) { if (!meetingRoom) {
throw new Error('Meeting room not found') throw new Error("Meeting room not found");
} }
// check if user is collaborator of collaboration session // check if user is collaborator of collaboration session
const collaborationSession = await this.prisma.collaborationSession.findUnique({ const collaborationSession =
where: { id: meetingRoom.collaborationSessionId }, await this.prisma.collaborationSession.findUnique({
}) where: { id: meetingRoom.collaborationSessionId },
});
if (!collaborationSession) { if (!collaborationSession) {
throw new Error('Collaboration session not found') throw new Error("Collaboration session not found");
} }
if (!collaborationSession.collaboratorsIds.includes(ctx.http.me.id)) { if (!collaborationSession.collaboratorsIds.includes(ctx.http.me.id)) {
throw new Error('User is not collaborator') throw new Error("User is not collaborator");
} }
// create new token // create new token
const token = await this.livekitService.createToken(ctx.http.me, meetingRoom.id) const token = await this.livekitService.createToken(
ctx.http.me,
meetingRoom.id
);
return { return {
id: meetingRoom.id, id: meetingRoom.id,
token, token,
serverUrl: this.livekitService.getServerUrl(), serverUrl: this.livekitService.getServerUrl(),
} };
}, },
}), }),
})) interviewJoinInfo: t.field({
type: this.meetingRoomJoinInfo(),
args: {
scheduleId: t.arg.string({
required: true,
}),
},
resolve: async (_, args, ctx: SchemaContext) => {
if (ctx.isSubscription) {
throw new Error("Not allowed");
}
if (!ctx.http.me) {
throw new Error("Unauthorized");
}
const token = await this.livekitService.createToken(
ctx.http.me,
args.scheduleId
);
return {
id: args.scheduleId,
token,
serverUrl: this.livekitService.getServerUrl(),
};
},
}),
}));
this.builder.mutationFields((t) => ({ this.builder.mutationFields((t) => ({
createMeetingRoom: t.prismaField({ createMeetingRoom: t.prismaField({
type: this.meetingRoom(), type: this.meetingRoom(),
args: { args: {
input: t.arg({ input: t.arg({
type: this.builder.generator.getCreateInput('MeetingRoom', [ type: this.builder.generator.getCreateInput("MeetingRoom", [
'id', "id",
'createdAt', "createdAt",
'updatedAt', "updatedAt",
'collaborators', "collaborators",
]), ]),
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) { if (!ctx.http.me) {
throw new Error('Unauthorized') throw new Error("Unauthorized");
} }
return await this.prisma.meetingRoom.create({ return await this.prisma.meetingRoom.create({
...query, ...query,
data: args.input, data: args.input,
}) });
}, },
}), }),
updateMeetingRoomCollaborators: t.prismaField({ updateMeetingRoomCollaborators: t.prismaField({
@@ -186,10 +221,10 @@ export class MeetingRoomSchema extends PothosSchema {
}, },
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) { if (!ctx.http.me) {
throw new Error('Unauthorized') throw new Error("Unauthorized");
} }
return await this.prisma.meetingRoom.update({ return await this.prisma.meetingRoom.update({
...query, ...query,
@@ -199,7 +234,9 @@ export class MeetingRoomSchema extends PothosSchema {
data: { data: {
collaborators: { collaborators: {
createMany: { createMany: {
data: args.addCollaborators ? args.addCollaborators.map((id) => ({ userId: id })) : [], data: args.addCollaborators
? args.addCollaborators.map((id) => ({ userId: id }))
: [],
}, },
deleteMany: { deleteMany: {
userId: { userId: {
@@ -208,9 +245,9 @@ export class MeetingRoomSchema extends PothosSchema {
}, },
}, },
}, },
}) });
}, },
}), }),
})) }));
} }
} }

View File

@@ -5,6 +5,7 @@ import { OpenaiService } from './openai.service'
const openaiOptions: ClientOptions = { const openaiOptions: ClientOptions = {
apiKey: process.env.OPENAI_API_KEY, apiKey: process.env.OPENAI_API_KEY,
timeout: 5000,
baseURL: process.env.OPENAI_BASE_URL, baseURL: process.env.OPENAI_BASE_URL,
maxRetries: parseInt(process.env.OPENAI_MAX_RETRIES as string) ?? 3, maxRetries: parseInt(process.env.OPENAI_MAX_RETRIES as string) ?? 3,
dangerouslyAllowBrowser: true, dangerouslyAllowBrowser: true,

View File

@@ -17,6 +17,11 @@ export class RedisService {
async set(key: string, value: string, expireAt: number) { async set(key: string, value: string, expireAt: number) {
return await this.redis.set(key, value, 'EXAT', expireAt) return await this.redis.set(key, value, 'EXAT', expireAt)
} }
async setPX(key: string, value: string, expireAt: number) {
return await this.redis.set(key, value, 'PX', expireAt)
}
async setPermanent(key: string, value: string) { async setPermanent(key: string, value: string) {
return await this.redis.set(key, value) return await this.redis.set(key, value)
} }

View File

@@ -1,5 +1,5 @@
import { Inject, Injectable } from '@nestjs/common' import { Inject, Injectable } from '@nestjs/common'
import { MessageContextType, MessageType, Role } from '@prisma/client' import { ChatRoomType, MessageContextType, MessageType, Role } from '@prisma/client'
import { Pothos, PothosRef, PothosSchema, SchemaBuilderToken } from '@smatch-corp/nestjs-pothos' import { Pothos, PothosRef, PothosSchema, SchemaBuilderToken } from '@smatch-corp/nestjs-pothos'
import { MinioService } from 'src/Minio/minio.service' import { MinioService } from 'src/Minio/minio.service'
import { PubSubEvent } from 'src/common/pubsub/pubsub-event' import { PubSubEvent } from 'src/common/pubsub/pubsub-event'
@@ -169,6 +169,16 @@ export class WorkshopSchema extends PothosSchema {
workshopId: workshop.id, workshopId: workshop.id,
}, },
}) })
// create workshop chatroom
await this.prisma.chatRoom.create({
data: {
type: ChatRoomType.WORKSHOP,
active: true,
workshopId: workshop.id,
customerId: service.center.centerOwnerId ?? '',
centerId: service.center.id,
},
})
// notify all user has role CUSTOMER // notify all user has role CUSTOMER
const customers = await this.prisma.user.findMany({ const customers = await this.prisma.user.findMany({
where: { role: Role.CUSTOMER }, where: { role: Role.CUSTOMER },

View File

@@ -1,49 +1,58 @@
import { Inject, Injectable } from '@nestjs/common' import { Inject, Injectable } from "@nestjs/common";
import { Pothos, PothosRef, PothosSchema, SchemaBuilderToken } from '@smatch-corp/nestjs-pothos' import { ChatRoomType } from "@prisma/client";
import { Builder } from '../Graphql/graphql.builder' import {
import { LiveKitService } from '../LiveKit/livekit.service' Pothos,
import { PrismaService } from '../Prisma/prisma.service' PothosRef,
PothosSchema,
SchemaBuilderToken,
} from "@smatch-corp/nestjs-pothos";
import { Builder } from "../Graphql/graphql.builder";
import { LiveKitService } from "../LiveKit/livekit.service";
import { PrismaService } from "../Prisma/prisma.service";
@Injectable() @Injectable()
export class WorkshopMeetingRoomSchema extends PothosSchema { export class WorkshopMeetingRoomSchema extends PothosSchema {
constructor( constructor(
@Inject(SchemaBuilderToken) private readonly builder: Builder, @Inject(SchemaBuilderToken) private readonly builder: Builder,
private readonly prisma: PrismaService, private readonly prisma: PrismaService,
private readonly livekitService: LiveKitService, private readonly livekitService: LiveKitService
) { ) {
super() super();
} }
@PothosRef() @PothosRef()
workshopMeetingRoom() { workshopMeetingRoom() {
return this.builder.prismaObject('WorkshopMeetingRoom', { return this.builder.prismaObject("WorkshopMeetingRoom", {
fields: (t) => ({ fields: (t) => ({
id: t.exposeID('id', { id: t.exposeID("id", {
description: 'The ID of the workshop meeting room.', description: "The ID of the workshop meeting room.",
}), }),
workshopId: t.exposeID('workshopId', { workshopId: t.exposeID("workshopId", {
description: 'The ID of the workshop that the meeting room is for.', description: "The ID of the workshop that the meeting room is for.",
}), }),
workshop: t.relation('workshop', { workshop: t.relation("workshop", {
description: 'The workshop that the meeting room is for.', description: "The workshop that the meeting room is for.",
}), }),
}), }),
}) });
} }
@PothosRef() @PothosRef()
workshopMeetingRoomJoinInfo() { workshopMeetingRoomJoinInfo() {
return this.builder.simpleObject('WorkshopMeetingRoomJoinInfo', { return this.builder.simpleObject("WorkshopMeetingRoomJoinInfo", {
fields: (t) => ({ fields: (t) => ({
id: t.string({ id: t.string({
description: 'The ID of the workshop meeting room.', description: "The ID of the workshop meeting room.",
}), }),
token: t.string({ token: t.string({
description: 'The token to join the workshop meeting room.', description: "The token to join the workshop meeting room.",
}), }),
serverUrl: t.string({ serverUrl: t.string({
description: 'The URL of the server.', description: "The URL of the server.",
}),
chatRoomId: t.string({
description: "The ID of the chat room.",
}), }),
}), }),
}) });
} }
@Pothos() @Pothos()
@@ -51,48 +60,59 @@ export class WorkshopMeetingRoomSchema extends PothosSchema {
this.builder.queryFields((t) => ({ this.builder.queryFields((t) => ({
workshopMeetingRoom: t.prismaField({ workshopMeetingRoom: t.prismaField({
type: this.workshopMeetingRoom(), type: this.workshopMeetingRoom(),
args: this.builder.generator.findUniqueArgs('WorkshopMeetingRoom'), args: this.builder.generator.findUniqueArgs("WorkshopMeetingRoom"),
resolve: async (query, _root, args, _ctx, _info) => { resolve: async (query, _root, args, _ctx, _info) => {
return await this.prisma.workshopMeetingRoom.findUnique({ return await this.prisma.workshopMeetingRoom.findUnique({
...query, ...query,
where: args.where, where: args.where,
}) });
}, },
}), }),
workshopMeetingRoomJoinInfo: t.field({ workshopMeetingRoomJoinInfo: t.field({
type: this.workshopMeetingRoomJoinInfo(), type: this.workshopMeetingRoomJoinInfo(),
args: { args: {
workshopId: t.arg({ workshopId: t.arg({
type: 'String', type: "String",
required: true, required: true,
}), }),
}, },
resolve: async (_, args, ctx) => { resolve: async (_, args, ctx) => {
if (ctx.isSubscription) { if (ctx.isSubscription) {
throw new Error('Not allowed') throw new Error("Not allowed");
} }
if (!ctx.http?.me) { if (!ctx.http?.me) {
throw new Error('Unauthorized') throw new Error("Unauthorized");
} }
const meetingRoom = await this.prisma.workshopMeetingRoom.findUnique({ const meetingRoom = await this.prisma.workshopMeetingRoom.findUnique({
where: { where: {
workshopId: args.workshopId, workshopId: args.workshopId,
}, },
}) });
// query chat room
const chatRoom = await this.prisma.chatRoom.findFirst({
where: {
workshopId: args.workshopId,
type: ChatRoomType.WORKSHOP,
},
});
if (!meetingRoom) { if (!meetingRoom) {
throw new Error('Meeting room not found') throw new Error("Meeting room not found");
} }
const serverUrl = this.livekitService.getServerUrl() const serverUrl = this.livekitService.getServerUrl();
return { return {
id: meetingRoom.id, id: meetingRoom.id,
token: await this.livekitService.createToken(ctx.http?.me, meetingRoom.id), token: await this.livekitService.createToken(
ctx.http?.me,
meetingRoom.id
),
serverUrl, serverUrl,
} chatRoomId: chatRoom?.id,
};
}, },
}), }),
workshopMeetingRooms: t.prismaField({ workshopMeetingRooms: t.prismaField({
type: [this.workshopMeetingRoom()], type: [this.workshopMeetingRoom()],
args: this.builder.generator.findManyArgs('WorkshopMeetingRoom'), args: this.builder.generator.findManyArgs("WorkshopMeetingRoom"),
resolve: async (query, _root, args, _ctx, _info) => { resolve: async (query, _root, args, _ctx, _info) => {
return await this.prisma.workshopMeetingRoom.findMany({ return await this.prisma.workshopMeetingRoom.findMany({
...query, ...query,
@@ -101,9 +121,9 @@ export class WorkshopMeetingRoomSchema extends PothosSchema {
cursor: args.cursor ?? undefined, cursor: args.cursor ?? undefined,
take: args.take ?? undefined, take: args.take ?? undefined,
skip: args.skip ?? undefined, skip: args.skip ?? undefined,
}) });
}, },
}), }),
})) }));
} }
} }

File diff suppressed because one or more lines are too long