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:
Submodule epess-database updated: 8341513355...d9e6be63fc
@@ -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,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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
|
|
||||||
}
|
|
||||||
if (!this.isSentence(op) && blacklist.includes(op.insert)) {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
// check if the sentence is already corrected by checking the attributes
|
|
||||||
if (op.attributes?.[customAttributes.AI_SUGGESTION]) {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
const originalDelta = new Delta().push(op)
|
|
||||||
Logger.log(op.insert, 'op.insert')
|
|
||||||
Logger.log(promptType, 'promptType')
|
|
||||||
const grammarCheckResult = await this.openai.processText(op.insert, promptType)
|
|
||||||
if (!grammarCheckResult) {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
Logger.log(grammarCheckResult, 'grammarCheckResult')
|
|
||||||
// create new delta and maintain the original delta attributes
|
|
||||||
const newDelta = new Delta().push(op)
|
|
||||||
newDelta.ops[0].attributes = originalDelta.ops[0].attributes
|
|
||||||
newDelta.ops[0].insert = grammarCheckResult
|
|
||||||
// compose the original delta with the grammarCheckResult
|
|
||||||
// const correctedDelta = originalDelta.ops[0].insert = grammarCheckResult
|
|
||||||
// calculate where to insert the correctedDelta
|
|
||||||
// const index = content.ops.findIndex((op) => op.insert === op.insert)
|
|
||||||
// content.ops.splice(index, 0, newDelta)
|
|
||||||
Logger.log(JSON.stringify(newDelta), 'newDelta')
|
|
||||||
|
|
||||||
|
// Use Promise.all for parallel processing and proper error handling
|
||||||
|
const grammarCheckPromises = content.ops
|
||||||
|
.filter(
|
||||||
|
(op) =>
|
||||||
|
typeof op.insert === 'string' &&
|
||||||
|
this.isSentence(op) &&
|
||||||
|
!blacklist.includes(op.insert) &&
|
||||||
|
!op.attributes?.[customAttributes.AI_SUGGESTION],
|
||||||
|
)
|
||||||
|
.map(async (op) => {
|
||||||
|
try {
|
||||||
|
if (!op.insert || typeof op.insert !== 'string') {
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
// Process text with OpenAI
|
||||||
|
const grammarCheckResult = await this.openai.processText(op.insert, promptType)
|
||||||
|
|
||||||
|
if (!grammarCheckResult) {
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
// Trim and normalize the comparison
|
||||||
|
if (
|
||||||
|
grammarCheckResult.trim() === op.insert.trim() ||
|
||||||
|
// Optional: Add more sophisticated comparison if needed
|
||||||
|
this.areSentencesEquivalent(grammarCheckResult, op.insert)
|
||||||
|
) {
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
|
||||||
|
// Create new delta with only corrected attribute
|
||||||
|
const newDelta = new Delta().push({
|
||||||
|
...op,
|
||||||
|
insert: op.insert,
|
||||||
|
attributes: {
|
||||||
|
...(op.attributes || {}),
|
||||||
|
[customAttributes.AI_SUGGESTION]: true,
|
||||||
|
corrected: grammarCheckResult,
|
||||||
|
},
|
||||||
|
})
|
||||||
|
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)
|
||||||
|
|
||||||
// publish the result to the subscriber
|
// Return a combined delta of all processed results
|
||||||
const payload: DocumentDelta = {
|
const combinedDelta = results.reduce((acc, result) => {
|
||||||
documentId,
|
if (result) {
|
||||||
pageIndex: pageId,
|
return (acc || new Delta()).compose(result)
|
||||||
eventType: DocumentEvent.AI_SUGGESTION,
|
}
|
||||||
delta: newDelta,
|
return acc
|
||||||
senderId: 'system',
|
}, new Delta())
|
||||||
}
|
|
||||||
await this.pubSub.publish(`${DocumentEvent.AI_SUGGESTION}.${documentId}`, payload)
|
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)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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 {
|
|||||||
},
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
})
|
});
|
||||||
},
|
},
|
||||||
}),
|
}),
|
||||||
}))
|
}));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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,
|
||||||
|
|||||||
@@ -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)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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 },
|
||||||
|
|||||||
@@ -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
Reference in New Issue
Block a user