feat: integrate LiveKit services into Collaboration and Meeting Room modules

- Added LiveKitModule to CollaborationSession and MeetingRoom modules for enhanced real-time collaboration features.
- Updated CollaborationSessionSchema to include LiveKit services for managing participant access and room permissions.
- Implemented a new cron job in CronService to disable services without schedules for over 30 days, improving service management.
- Enhanced GraphQL schema generation with improved filtering logic for better performance and readability.
- Refactored LiveKit services to streamline access token creation and room management functionalities.
This commit is contained in:
2024-12-01 19:18:20 +07:00
parent 111acacf2d
commit 561823225d
10 changed files with 206 additions and 96 deletions

View File

@@ -1,8 +1,12 @@
import { Module } from '@nestjs/common' import { Module } from '@nestjs/common'
import { CollaborationSessionSchema } from './collaborationsession.schema' import { CollaborationSessionSchema } from './collaborationsession.schema'
import { LiveKitModule } from 'src/LiveKit/livekit.module'
import { LiveKitService } from 'src/LiveKit/livekit.service'
import { LiveKitParticipantService } from 'src/LiveKit/livekit.participant.service'
import { LiveKitRoomService } from 'src/LiveKit/livekit.room.service'
@Module({ @Module({
providers: [CollaborationSessionSchema], imports: [LiveKitModule],
providers: [CollaborationSessionSchema, LiveKitService, LiveKitParticipantService, LiveKitRoomService],
exports: [CollaborationSessionSchema], exports: [CollaborationSessionSchema],
}) })
export class CollaborationSessionModule {} export class CollaborationSessionModule {}

View File

@@ -1,17 +1,20 @@
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 { Pothos, PothosRef, PothosSchema, SchemaBuilderToken } from '@smatch-corp/nestjs-pothos'
import { Builder } from '../Graphql/graphql.builder' import { Builder, SchemaContext } from '../Graphql/graphql.builder'
import { PrismaService } from '../Prisma/prisma.service' import { PrismaService } from '../Prisma/prisma.service'
// import { LiveKitRoomService } from 'src/LiveKit/livekit.room.service' // import { LiveKitRoomService } from 'src/LiveKit/livekit.room.service'
import { v4 as uuidv4 } from 'uuid' import { v4 as uuidv4 } from 'uuid'
import { CollaborationSession, Role, ScheduleDateStatus } from '@prisma/client' import { CollaborationSession, Role, ScheduleDateStatus } from '@prisma/client'
import { DateTimeUtils } from 'src/common/utils/datetime.utils' import { DateTimeUtils } from 'src/common/utils/datetime.utils'
import { LiveKitService } from 'src/LiveKit/livekit.service'
import { LiveKitParticipantService } from 'src/LiveKit/livekit.participant.service'
@Injectable() @Injectable()
export class CollaborationSessionSchema extends PothosSchema { export class CollaborationSessionSchema 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 liveKitRoomService: LiveKitRoomService, private readonly liveKitService: LiveKitService,
private readonly liveKitParticipantService: LiveKitParticipantService,
) { ) {
super() super()
} }
@@ -99,6 +102,7 @@ export class CollaborationSessionSchema extends PothosSchema {
// }, // },
// }) // })
// } // }
// check if user is participant // check if user is participant
if ( if (
!collaborationSession.collaboratorsIds.includes(ctx.http.me.id) || !collaborationSession.collaboratorsIds.includes(ctx.http.me.id) ||
@@ -182,6 +186,29 @@ export class CollaborationSessionSchema extends PothosSchema {
}) })
}, },
}), }),
liveKitToken: t.field({
type: 'String',
resolve: async (_, _args, ctx: SchemaContext) => {
if (ctx.isSubscription) throw new Error('Not allowed')
if (!ctx.http?.me?.id) throw new Error('User not found')
// check if participantId is in meetingRoomCollaborators
const meetingRoomCollaborator = await this.prisma.meetingRoomCollaborator.findFirst({
where: {
userId: ctx.http.me.id,
},
})
if (!meetingRoomCollaborator) throw new Error('Meeting room collaborator not found')
const meetingRoom = await this.prisma.meetingRoom.findUnique({
where: {
id: meetingRoomCollaborator.meetingRoomId,
},
})
if (!meetingRoom) throw new Error('Meeting room not found')
const token = await this.liveKitService.createAccessToken(ctx.http.me.id)
await this.liveKitParticipantService.grantRoomJoinPermission(token, meetingRoom.collaborationSessionId)
return await this.liveKitParticipantService.toJWT(token)
},
}),
})) }))
this.builder.mutationFields((t) => ({ this.builder.mutationFields((t) => ({

View File

@@ -1,7 +1,7 @@
import { Injectable, Logger } from '@nestjs/common' import { Injectable, Logger } from '@nestjs/common'
import { Cron } from '@nestjs/schedule' import { Cron } from '@nestjs/schedule'
import { CronExpression } from '@nestjs/schedule' import { CronExpression } from '@nestjs/schedule'
import { OrderStatus, PaymentStatus, ScheduleDateStatus, ScheduleStatus } from '@prisma/client' import { OrderStatus, PaymentStatus, ScheduleDateStatus, ScheduleStatus, ServiceStatus } from '@prisma/client'
import { DateTimeUtils } from 'src/common/utils/datetime.utils' import { DateTimeUtils } from 'src/common/utils/datetime.utils'
import { NotificationService } from 'src/Notification/notification.service' import { NotificationService } from 'src/Notification/notification.service'
import { PrismaService } from 'src/Prisma/prisma.service' import { PrismaService } from 'src/Prisma/prisma.service'
@@ -189,4 +189,34 @@ export class CronService {
} }
} }
} }
// cron every day to disable service without any schedule in the past 30 days
@Cron(CronExpression.EVERY_DAY_AT_1AM)
async taskDisableServiceWithoutSchedule() {
Logger.log('Disabling service without any schedule', 'taskDisableServiceWithoutSchedule')
const services = await this.prisma.managedService.findMany({
where: {
NOT: {
schedule: {
some: {
scheduleStart: { gte: DateTimeUtils.now().minus({ days: 30 }).toJSDate() },
},
},
},
},
})
for (const service of services) {
await this.prisma.managedService.update({
where: { id: service.id },
data: {
service: {
update: {
status: ServiceStatus.INACTIVE,
},
},
},
})
Logger.log(`Service ${service.id} has been disabled`, 'taskDisableServiceWithoutSchedule')
}
}
} }

View File

@@ -1,6 +1,13 @@
import { Inject, Injectable } from '@nestjs/common' import { Inject, Injectable } from '@nestjs/common'
import { type BaseEnum, type EnumRef, InputObjectRef, type InputType, type InputTypeParam, type SchemaTypes } from '@pothos/core' import {
type BaseEnum,
type EnumRef,
InputObjectRef,
type InputType,
type InputTypeParam,
type SchemaTypes,
} from '@pothos/core'
import { type PrismaModelTypes, getModel } from '@pothos/plugin-prisma' import { type PrismaModelTypes, getModel } from '@pothos/plugin-prisma'
import type { FilterOps } from '@pothos/plugin-prisma-utils' import type { FilterOps } from '@pothos/plugin-prisma-utils'
import * as Prisma from '@prisma/client' import * as Prisma from '@prisma/client'
@@ -76,18 +83,27 @@ export class PrismaCrudGenerator<Types extends SchemaTypes> {
const withoutFields = model.fields.filter((field) => without?.includes(field.name)) const withoutFields = model.fields.filter((field) => without?.includes(field.name))
model.fields model.fields
.filter((field) => !withoutFields.some((f) => f.name === field.name || f.relationFromFields?.includes(field.name))) .filter(
(field) =>
!withoutFields.some((f) => f.name === field.name || f.relationFromFields?.includes(field.name)),
)
.forEach((field) => { .forEach((field) => {
let type let type
switch (field.kind) { switch (field.kind) {
case 'scalar': case 'scalar':
type = field.isList ? this.getScalarListFilter(this.mapScalarType(field.type) as InputType<Types>) : this.getFilter(this.mapScalarType(field.type) as InputType<Types>) type = field.isList
? this.getScalarListFilter(this.mapScalarType(field.type) as InputType<Types>)
: this.getFilter(this.mapScalarType(field.type) as InputType<Types>)
break break
case 'enum': case 'enum':
type = field.isList ? this.getScalarListFilter(this.getEnum(field.type)) : this.getFilter(this.getEnum(field.type)) type = field.isList
? this.getScalarListFilter(this.getEnum(field.type))
: this.getFilter(this.getEnum(field.type))
break break
case 'object': case 'object':
type = field.isList ? this.getListFilter(this.getWhere(field.type as Name)) : this.getWhere(field.type as Name) type = field.isList
? this.getListFilter(this.getWhere(field.type as Name))
: this.getWhere(field.type as Name)
break break
case 'unsupported': case 'unsupported':
break break
@@ -117,7 +133,13 @@ export class PrismaCrudGenerator<Types extends SchemaTypes> {
const fields: Record<string, InputType<Types>> = {} const fields: Record<string, InputType<Types>> = {}
model.fields model.fields
.filter((field) => field.isUnique || field.isId || model.uniqueIndexes.some((index) => index.fields.includes(field.name)) || model.primaryKey?.fields.includes(field.name)) .filter(
(field) =>
field.isUnique ||
field.isId ||
model.uniqueIndexes.some((index) => index.fields.includes(field.name)) ||
model.primaryKey?.fields.includes(field.name),
)
.forEach((field) => { .forEach((field) => {
let type let type
switch (field.kind) { switch (field.kind) {
@@ -197,7 +219,11 @@ export class PrismaCrudGenerator<Types extends SchemaTypes> {
const relationIds = model.fields.flatMap((field) => field.relationFromFields ?? []) const relationIds = model.fields.flatMap((field) => field.relationFromFields ?? [])
model.fields model.fields
.filter((field) => !withoutFields.some((f) => f.name === field.name || f.relationFromFields?.includes(field.name)) && !relationIds.includes(field.name)) .filter(
(field) =>
!withoutFields.some((f) => f.name === field.name || f.relationFromFields?.includes(field.name)) &&
!relationIds.includes(field.name),
)
.forEach((field) => { .forEach((field) => {
let type let type
switch (field.kind) { switch (field.kind) {
@@ -227,17 +253,22 @@ export class PrismaCrudGenerator<Types extends SchemaTypes> {
}) })
} }
getCreateRelationInput<Name extends string & keyof Types['PrismaTypes'], Relation extends Model['RelationName'], Model extends PrismaModelTypes = Types['PrismaTypes'][Name] extends PrismaModelTypes ? Types['PrismaTypes'][Name] : never>( getCreateRelationInput<
modelName: Name, Name extends string & keyof Types['PrismaTypes'],
relation: Relation, Relation extends Model['RelationName'],
) { Model extends PrismaModelTypes = Types['PrismaTypes'][Name] extends PrismaModelTypes
? Types['PrismaTypes'][Name]
: never,
>(modelName: Name, relation: Relation) {
return this.getRef(`${modelName}${capitalize(relation)}`, 'CreateRelationInput', () => { return this.getRef(`${modelName}${capitalize(relation)}`, 'CreateRelationInput', () => {
const model = getModel(modelName, this.builder) const model = getModel(modelName, this.builder)
return this.builder.prismaCreateRelation(modelName, relation, { return this.builder.prismaCreateRelation(modelName, relation, {
fields: () => { fields: () => {
const relationField = model.fields.find((field) => field.name === relation)! const relationField = model.fields.find((field) => field.name === relation)!
const relatedModel = getModel(relationField.type, this.builder) const relatedModel = getModel(relationField.type, this.builder)
const relatedFieldName = relatedModel.fields.find((field) => field.relationName === relationField.relationName)! const relatedFieldName = relatedModel.fields.find(
(field) => field.relationName === relationField.relationName,
)!
return { return {
create: this.getCreateInput(relationField.type as Name, [relatedFieldName.name]), create: this.getCreateInput(relationField.type as Name, [relatedFieldName.name]),
@@ -263,7 +294,11 @@ export class PrismaCrudGenerator<Types extends SchemaTypes> {
const relationIds = model.fields.flatMap((field) => field.relationFromFields ?? []) const relationIds = model.fields.flatMap((field) => field.relationFromFields ?? [])
model.fields model.fields
.filter((field) => !withoutFields.some((f) => f.name === field.name || f.relationFromFields?.includes(field.name)) && !relationIds.includes(field.name)) .filter(
(field) =>
!withoutFields.some((f) => f.name === field.name || f.relationFromFields?.includes(field.name)) &&
!relationIds.includes(field.name),
)
.forEach((field) => { .forEach((field) => {
let type let type
switch (field.kind) { switch (field.kind) {
@@ -304,7 +339,11 @@ export class PrismaCrudGenerator<Types extends SchemaTypes> {
const relationIds = model.fields.flatMap((field) => field.relationFromFields ?? []) const relationIds = model.fields.flatMap((field) => field.relationFromFields ?? [])
model.fields model.fields
.filter((field) => !withoutFields.some((f) => f.name === field.name || f.relationFromFields?.includes(field.name)) && !relationIds.includes(field.name)) .filter(
(field) =>
!withoutFields.some((f) => f.name === field.name || f.relationFromFields?.includes(field.name)) &&
!relationIds.includes(field.name),
)
.forEach((field) => { .forEach((field) => {
let type let type
switch (field.kind) { switch (field.kind) {
@@ -333,17 +372,22 @@ export class PrismaCrudGenerator<Types extends SchemaTypes> {
}) as InputObjectRef<Types, (PrismaModelTypes & Types['PrismaTypes'][Name])['Update']> }) as InputObjectRef<Types, (PrismaModelTypes & Types['PrismaTypes'][Name])['Update']>
}) })
} }
getUpdateRelationInput<Name extends string & keyof Types['PrismaTypes'], Relation extends Model['RelationName'], Model extends PrismaModelTypes = Types['PrismaTypes'][Name] extends PrismaModelTypes ? Types['PrismaTypes'][Name] : never>( getUpdateRelationInput<
modelName: Name, Name extends string & keyof Types['PrismaTypes'],
relation: Relation, Relation extends Model['RelationName'],
) { Model extends PrismaModelTypes = Types['PrismaTypes'][Name] extends PrismaModelTypes
? Types['PrismaTypes'][Name]
: never,
>(modelName: Name, relation: Relation) {
return this.getRef(`${modelName}${capitalize(relation)}`, 'UpdateRelationInput', () => { return this.getRef(`${modelName}${capitalize(relation)}`, 'UpdateRelationInput', () => {
const model = getModel(modelName, this.builder) const model = getModel(modelName, this.builder)
return this.builder.prismaUpdateRelation(modelName, relation, { return this.builder.prismaUpdateRelation(modelName, relation, {
fields: () => { fields: () => {
const relationField = model.fields.find((field) => field.name === relation)! const relationField = model.fields.find((field) => field.name === relation)!
const relatedModel = getModel(relationField.type, this.builder) const relatedModel = getModel(relationField.type, this.builder)
const relatedFieldName = relatedModel.fields.find((field) => field.relationName === relationField.relationName)!.name const relatedFieldName = relatedModel.fields.find(
(field) => field.relationName === relationField.relationName,
)!.name
if (relationField.isList) { if (relationField.isList) {
return { return {
@@ -446,7 +490,11 @@ export class PrismaCrudGenerator<Types extends SchemaTypes> {
} }
} }
private getRef<T extends InputObjectRef<Types, unknown>>(key: InputType<Types> | string, name: string, create: () => T): T { private getRef<T extends InputObjectRef<Types, unknown>>(
key: InputType<Types> | string,
name: string,
create: () => T,
): T {
if (!this.refCache.has(key)) { if (!this.refCache.has(key)) {
this.refCache.set(key, new Map()) this.refCache.set(key, new Map())
} }

View File

@@ -1,7 +1,10 @@
// import { Module } from '@nestjs/common' import { Module } from '@nestjs/common'
// import { LiveKitService } from './livekit.service' import { LiveKitService } from './livekit.service'
// @Module({ import { LiveKitParticipantService } from './livekit.participant.service'
// providers: [LiveKitService], import { LiveKitRoomService } from './livekit.room.service'
// exports: [LiveKitService],
// }) @Module({
// export class LiveKitModule {} providers: [LiveKitService, LiveKitParticipantService, LiveKitRoomService],
exports: [LiveKitService],
})
export class LiveKitModule {}

View File

@@ -5,13 +5,9 @@ import { AccessToken } from 'livekit-server-sdk'
@Injectable() @Injectable()
export class LiveKitParticipantService { export class LiveKitParticipantService {
async createAccessToken(participantId: string) { async createAccessToken(participantId: string) {
return new AccessToken( return new AccessToken(process.env.LIVEKIT_API_KEY as string, process.env.LIVEKIT_API_SECRET as string, {
process.env.LIVEKIT_API_KEY, identity: participantId,
process.env.LIVEKIT_API_SECRET, })
{
identity: participantId,
},
)
} }
async grantRoomJoinPermission(token: AccessToken, roomName: string) { async grantRoomJoinPermission(token: AccessToken, roomName: string) {

View File

@@ -1,36 +1,35 @@
// import { import { v4 as uuidv4 } from 'uuid'
// Room,
// RoomServiceClient,
// RoomCompositeOptions,
// // @ts-ignore
// } from 'livekit-server-sdk'
// import { v4 as uuidv4 } from 'uuid'
// export class LiveKitRoomService { export class LiveKitRoomService {
// private roomServiceClient: RoomServiceClient // biome-ignore lint/suspicious/noExplicitAny: <explanation>
private roomServiceClient: any
constructor() {
this.initializeRoomServiceClient()
}
// constructor() { private async initializeRoomServiceClient() {
// this.roomServiceClient = new RoomServiceClient( const { RoomServiceClient } = await import('livekit-server-sdk')
// process.env.LIVEKIT_URL as string, this.roomServiceClient = new RoomServiceClient(
// process.env.LIVEKIT_API_KEY as string, process.env.LIVEKIT_URL as string,
// process.env.LIVEKIT_API_SECRET as string, process.env.LIVEKIT_API_KEY as string,
// ) process.env.LIVEKIT_API_SECRET as string,
// } )
}
// async createServiceMeetingRoom(chattingRoomId: string) { async createServiceMeetingRoom(chattingRoomId: string) {
// const room = await this.roomServiceClient.createRoom({ const room = await this.roomServiceClient.createRoom({
// maxParticipants: 3, maxParticipants: 3,
// name: chattingRoomId, name: chattingRoomId,
// }) })
// return room return room
// } }
// async createWorkshopMeetingRoom(workshopId: string, maxParticipants: number) { async createWorkshopMeetingRoom(workshopId: string, maxParticipants: number) {
// const room = await this.roomServiceClient.createRoom({ const room = await this.roomServiceClient.createRoom({
// maxParticipants: maxParticipants, maxParticipants: maxParticipants,
// name: workshopId, name: workshopId,
// }) })
// return room return room
// } }
// } }

View File

@@ -1,11 +1,15 @@
// import { Injectable, OnModuleInit } from '@nestjs/common' import { Injectable } from '@nestjs/common'
// import { LiveKitRoomService } from './livekit.room.service' import { LiveKitRoomService } from './livekit.room.service'
import { LiveKitParticipantService } from './livekit.participant.service'
// @Injectable() @Injectable()
// export class LiveKitService implements OnModuleInit { export class LiveKitService {
// private liveKitRoomService: LiveKitRoomService constructor(
// async onModuleInit() { private liveKitRoomService: LiveKitRoomService,
// // init livekit room service private liveKitParticipantService: LiveKitParticipantService,
// this.liveKitRoomService = new LiveKitRoomService() ) {}
// }
// } async createAccessToken(participantId: string) {
return await this.liveKitParticipantService.createAccessToken(participantId)
}
}

View File

@@ -1,7 +1,9 @@
import { Module } from '@nestjs/common' import { Module } from '@nestjs/common'
import { MeetingRoomSchema } from './meetingroom.schema' import { MeetingRoomSchema } from './meetingroom.schema'
import { LiveKitModule } from 'src/LiveKit/livekit.module'
@Module({ @Module({
imports: [LiveKitModule],
providers: [MeetingRoomSchema], providers: [MeetingRoomSchema],
exports: [MeetingRoomSchema], exports: [MeetingRoomSchema],
}) })

View File

@@ -1,13 +1,7 @@
import { Inject, Injectable } from '@nestjs/common' import { Inject, Injectable } from '@nestjs/common'
import { import { Pothos, PothosRef, PothosSchema, SchemaBuilderToken } from '@smatch-corp/nestjs-pothos'
Pothos,
PothosRef,
PothosSchema,
SchemaBuilderToken,
} from '@smatch-corp/nestjs-pothos'
import { Builder, SchemaContext } from 'src/Graphql/graphql.builder' import { Builder, SchemaContext } from 'src/Graphql/graphql.builder'
import { PrismaService } from 'src/Prisma/prisma.service' import { PrismaService } from 'src/Prisma/prisma.service'
@Injectable() @Injectable()
export class MeetingRoomSchema extends PothosSchema { export class MeetingRoomSchema extends PothosSchema {
constructor( constructor(
@@ -56,14 +50,12 @@ export class MeetingRoomSchema extends PothosSchema {
}, },
resolve: async (_query, _parent, args, ctx: SchemaContext) => { resolve: async (_query, _parent, args, ctx: SchemaContext) => {
if (ctx.isSubscription) throw new Error('Not allowed') if (ctx.isSubscription) throw new Error('Not allowed')
const collaborationSession = const collaborationSession = await this.prisma.collaborationSession.findUnique({
await this.prisma.collaborationSession.findUnique({ where: {
where: { scheduleDateId: args.scheduleDateId,
scheduleDateId: args.scheduleDateId, },
}, })
}) if (!collaborationSession) throw new Error('Collaboration session not found')
if (!collaborationSession)
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,
@@ -83,7 +75,12 @@ export class MeetingRoomSchema extends PothosSchema {
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',
'createdAt',
'updatedAt',
'collaborators',
]),
required: true, required: true,
}), }),
}, },