update websocket

This commit is contained in:
2024-11-06 17:16:10 +07:00
parent ef5c753ce4
commit 57037a59ec
18 changed files with 129 additions and 71 deletions

View File

@@ -133,19 +133,13 @@
"ws": "^8.18.0" "ws": "^8.18.0"
}, },
"jest": { "jest": {
"moduleFileExtensions": [ "moduleFileExtensions": ["js", "json", "ts"],
"js",
"json",
"ts"
],
"rootDir": "src", "rootDir": "src",
"testRegex": ".*\\.spec\\.ts$", "testRegex": ".*\\.spec\\.ts$",
"transform": { "transform": {
"^.+\\.(t|j)s$": "ts-jest" "^.+\\.(t|j)s$": "ts-jest"
}, },
"collectCoverageFrom": [ "collectCoverageFrom": ["**/*.(t|j)s"],
"**/*.(t|j)s"
],
"coverageDirectory": "../coverage", "coverageDirectory": "../coverage",
"testEnvironment": "node" "testEnvironment": "node"
}, },

View File

@@ -155,7 +155,7 @@ export class CenterMentorSchema extends PothosSchema {
} }
// build signature // build signature
const token = this.jwtUtils.signTokenRS256( const token = this.jwtUtils.signTokenRS256(
{ centerId: center.id, email: args.email }, JSON.stringify({ centerId: center.id, email: args.email }),
'1d', '1d',
) )
// build invite url // build invite url
@@ -185,7 +185,7 @@ export class CenterMentorSchema extends PothosSchema {
return this.prisma.$transaction(async () => { return this.prisma.$transaction(async () => {
// sign token // sign token
const token = this.jwtUtils.signTokenRS256( const token = this.jwtUtils.signTokenRS256(
{ centerId: args.centerId, email: args.email }, JSON.stringify({ centerId: args.centerId, email: args.email }),
'1d', '1d',
) )
// build invite url // build invite url

View File

@@ -43,8 +43,10 @@ export class ClerkAuthGuard implements CanActivate {
request.user = session.user request.user = session.user
return true return true
} catch (error: any) { } catch (error: unknown) {
throw new UnauthorizedException(error.message) throw new UnauthorizedException(
error instanceof Error ? error.message : 'Unknown error',
)
} }
} }
} }

View File

@@ -18,7 +18,7 @@ export class ClerkController {
@Post('webhook') @Post('webhook')
@ApiOperation({ summary: 'Clerk Webhook' }) @ApiOperation({ summary: 'Clerk Webhook' })
@ApiResponse({ status: 200, description: 'Webhook created successfully' }) @ApiResponse({ status: 200, description: 'Webhook created successfully' })
webhook(@Headers() headers: any, @Body() body: any) { webhook(@Headers() headers: Headers, @Body() body: any) {
return this.clerkService.webhook(body) return this.clerkService.webhook(body)
} }
} }

View File

@@ -3,9 +3,7 @@ import { Injectable, Logger } from '@nestjs/common'
import { PrismaService } from '../Prisma/prisma.service' import { PrismaService } from '../Prisma/prisma.service'
import { clerkClient } from '@clerk/express' import { clerkClient } from '@clerk/express'
export interface ClerkResponse { export interface ClerkResponse {}
}
@Injectable() @Injectable()
export class ClerkService { export class ClerkService {
constructor(private readonly prisma: PrismaService) {} constructor(private readonly prisma: PrismaService) {}

View File

@@ -1,4 +1,4 @@
import { DateTimeResolver, JSONObjectResolver } from 'graphql-scalars' import { JSONObjectResolver } from 'graphql-scalars'
import PrismaPlugin, { import PrismaPlugin, {
PothosPrismaDatamodel, PothosPrismaDatamodel,
PrismaClient, PrismaClient,
@@ -25,8 +25,7 @@ import { getDatamodel } from '../types/pothos.generated'
import { DateTime } from 'luxon' import { DateTime } from 'luxon'
import { Kind } from 'graphql' import { Kind } from 'graphql'
import { DateTimeUtils } from '../common/utils/datetime.utils' import { DateTimeUtils } from '../common/utils/datetime.utils'
import { JsonValue } from '@prisma/client/runtime/library'
// import { rules } from '../common/graphql/common.graphql.auth-rule';
export type SchemaContext = export type SchemaContext =
| { | {
@@ -65,8 +64,8 @@ export interface SchemaBuilderOption {
Output: string | DateTime | Date Output: string | DateTime | Date
} }
Json: { Json: {
Input: JSON Input: JsonValue
Output: JSON Output: JsonValue
} }
Upload: { Upload: {
Input: FileUpload Input: FileUpload
@@ -161,8 +160,6 @@ export class Builder extends SchemaBuilder<SchemaBuilderOption> {
: parent.totalCount, : parent.totalCount,
}), }),
) )
// test print ManagedServiceWhereUniqueInput
} }
} }
export type BuilderTypes = export type BuilderTypes =

View File

@@ -41,6 +41,7 @@ import { WorkshopSubscriptionModule } from '../WorkshopSubscription/workshopsubs
import { initContextCache } from '@pothos/core' import { initContextCache } from '@pothos/core'
import { PubSub } from 'graphql-subscriptions' import { PubSub } from 'graphql-subscriptions'
import { isSubscription } from 'rxjs/internal/Subscription' import { isSubscription } from 'rxjs/internal/Subscription'
import { EventEmitter } from 'ws'
@Global() @Global()
@Module({ @Module({
@@ -147,7 +148,10 @@ import { isSubscription } from 'rxjs/internal/Subscription'
}, },
{ {
provide: 'PUB_SUB', provide: 'PUB_SUB',
useFactory: () => new PubSub(), useFactory: () =>
new PubSub({
eventEmitter: new EventEmitter({}),
}),
}, },
], ],
exports: [ exports: [

View File

@@ -5,8 +5,10 @@ import {
PothosSchema, PothosSchema,
SchemaBuilderToken, SchemaBuilderToken,
} from '@smatch-corp/nestjs-pothos' } 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 { Message, MessageContextType, MessageType } from '@prisma/client'
import { DateTimeUtils } from 'src/common/utils/datetime.utils'
@Injectable() @Injectable()
export class MessageSchema extends PothosSchema { export class MessageSchema extends PothosSchema {
@@ -31,15 +33,25 @@ export class MessageSchema extends PothosSchema {
chatRoomId: t.exposeID('chatRoomId', { chatRoomId: t.exposeID('chatRoomId', {
description: 'The ID of the chat room.', description: 'The ID of the chat room.',
}), }),
message: t.expose('message', { type: t.expose('type', {
// biome-ignore lint/suspicious/noExplicitAny: <explanation> type: MessageType,
type: 'Json' as any, description: 'The type of the message.',
}),
content: t.exposeString('content', {
description: 'The message content.', description: 'The message content.',
}), }),
sentAt: t.expose('sentAt', { sentAt: t.expose('sentAt', {
type: 'DateTime', type: 'DateTime',
description: 'The date and time the message was sent.', description: 'The date and time the message was sent.',
}), }),
context: t.expose('context', {
type: MessageContextType,
description: 'The context of the message.',
}),
metadata: t.expose('metadata', {
type: 'Json',
description: 'The metadata of the message.',
}),
sender: t.relation('sender', { sender: t.relation('sender', {
description: 'The sender of the message.', description: 'The sender of the message.',
}), }),
@@ -95,16 +107,34 @@ export class MessageSchema extends PothosSchema {
// mutations // mutations
this.builder.mutationFields((t) => ({ this.builder.mutationFields((t) => ({
testSendMessage: t.field({ testSendMessage: t.field({
type: 'String', type: this.message(),
description: 'Test sending a message.', description: 'Test sending a message.',
resolve: async (_, __, ctx) => { resolve: async (_root, _args, ctx) => {
if (ctx.isSubscription) { if (ctx.isSubscription) {
throw new Error('Not allowed') throw new Error('Not allowed')
} }
ctx.http.pubSub.publish('MESSAGE_SENT', { ctx.http.pubSub.publish('MESSAGE_SENT', {
message: 'Hello, world!', id: '1',
senderId: '1',
recipientId: '2',
chatRoomId: 'b86e840f-81d3-4043-b57a-f9adf719423c',
type: MessageType.TEXT,
content: 'Hello, world!',
context: MessageContextType.CHAT,
metadata: {},
sentAt: DateTimeUtils.now().toJSDate(),
}) })
return 'Message sent' return {
id: '1',
senderId: '1',
recipientId: '2',
chatRoomId: 'b86e840f-81d3-4043-b57a-f9adf719423c',
type: MessageType.TEXT,
content: 'Hello, world!',
context: MessageContextType.CHAT,
metadata: {},
sentAt: DateTimeUtils.now().toJSDate(),
}
}, },
}), }),
sendMessage: t.prismaField({ sendMessage: t.prismaField({
@@ -112,12 +142,29 @@ export class MessageSchema extends PothosSchema {
description: 'Send a message to a chat room.', description: 'Send a message to a chat room.',
args: { args: {
input: t.arg({ input: t.arg({
type: this.builder.generator.getCreateInput('Message'), type: this.builder.generator.getCreateInput('Message', [
'id',
'senderId',
'sender',
'sentAt',
]),
description: 'The message to send.', description: 'The message to send.',
required: true, required: true,
}), }),
}, },
resolve: async (query, _root, args, ctx, _info) => { resolve: async (query, _root, args, ctx, _info) => {
if (ctx.isSubscription) {
throw new Error('Not allowed')
}
// get the sender from the context and add it to the input
args.input.sender = {
connect: {
id: ctx.http.me?.id,
},
}
if (!args.input.sender) {
throw new Error('Cannot get sender from context')
}
const message = await this.prisma.message.create({ const message = await this.prisma.message.create({
...query, ...query,
data: args.input, data: args.input,
@@ -134,24 +181,17 @@ export class MessageSchema extends PothosSchema {
this.builder.subscriptionFields((t) => ({ this.builder.subscriptionFields((t) => ({
messageSent: t.field({ messageSent: t.field({
description: 'Subscribe to messages sent by users.', description: 'Subscribe to messages sent by users.',
args: {}, type: this.message(),
subscribe: async (_, __, ctx) => { subscribe: (_, __, ctx: SchemaContext) => {
if (!ctx.isSubscription) { if (!ctx.isSubscription) throw new Error('Not allowed')
throw new Error('Not allowed') const {
} websocket: { pubSub },
return (await ctx.websocket.pubSub.asyncIterator( } = ctx
return pubSub.asyncIterator(
'MESSAGE_SENT', 'MESSAGE_SENT',
)) as unknown as AsyncIterable<unknown> ) as unknown as AsyncIterable<Message>
},
type: this.message(), // Add the type property
resolve: (payload) =>
payload as {
message: 'Json'
id: string
senderId: string
chatRoomId: string
sentAt: Date
}, },
resolve: (payload: Message) => payload,
}), }),
})) }))
} }

View File

@@ -0,0 +1 @@

View File

@@ -57,7 +57,7 @@ export class RefundTicketSchema extends PothosSchema {
description: description:
'Retrieve a list of refund tickets with optional filtering, ordering, and pagination.', 'Retrieve a list of refund tickets with optional filtering, ordering, and pagination.',
args: this.builder.generator.findManyArgs('RefundTicket'), args: this.builder.generator.findManyArgs('RefundTicket'),
resolve: async (query, root, args, ctx, info) => { resolve: async (query, _root, args, _ctx, _info) => {
return await this.prisma.refundTicket.findMany({ return await this.prisma.refundTicket.findMany({
...query, ...query,
where: args.filter ?? undefined, where: args.filter ?? undefined,

View File

@@ -71,8 +71,11 @@ export class UserSchema extends PothosSchema {
files: t.relation('files', { files: t.relation('files', {
description: 'The files of the user.', description: 'The files of the user.',
}), }),
sendingMessage: t.relation('sendingMessage', { sentMessages: t.relation('sentMessages', {
description: 'The sending message of the user.', description: 'The sent messages of the user.',
}),
receivedMessages: t.relation('receivedMessages', {
description: 'The received messages of the user.',
}), }),
resume: t.relation('resume', { resume: t.relation('resume', {
description: 'The resume of the user.', description: 'The resume of the user.',

View File

@@ -40,7 +40,7 @@ export class WorkshopMeetingRoomSchema extends PothosSchema {
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,
@@ -50,7 +50,7 @@ export class WorkshopMeetingRoomSchema extends PothosSchema {
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,
where: args.filter ?? undefined, where: args.filter ?? undefined,

View File

@@ -50,7 +50,7 @@ export class WorkshopSubscriptionSchema extends PothosSchema {
args: this.builder.generator.findUniqueArgs('WorkshopSubscription'), args: this.builder.generator.findUniqueArgs('WorkshopSubscription'),
description: description:
'Retrieve a single workshop subscription by its unique identifier.', 'Retrieve a single workshop subscription by its unique identifier.',
resolve: async (query, root, args) => { resolve: async (query, _root, args) => {
return await this.prisma.workshopSubscription.findUnique({ return await this.prisma.workshopSubscription.findUnique({
...query, ...query,
where: args.where, where: args.where,
@@ -62,7 +62,7 @@ export class WorkshopSubscriptionSchema extends PothosSchema {
args: this.builder.generator.findManyArgs('WorkshopSubscription'), args: this.builder.generator.findManyArgs('WorkshopSubscription'),
description: description:
'Retrieve a list of workshop subscriptions with optional filtering, ordering, and pagination.', 'Retrieve a list of workshop subscriptions with optional filtering, ordering, and pagination.',
resolve: async (query, root, args) => { resolve: async (query, _root, args) => {
return await this.prisma.workshopSubscription.findMany({ return await this.prisma.workshopSubscription.findMany({
...query, ...query,
skip: args.skip ?? undefined, skip: args.skip ?? undefined,

View File

@@ -134,4 +134,8 @@ export class DateTimeUtils {
second: second as SecondNumbers, second: second as SecondNumbers,
} }
} }
static getTimeFromDateTime(dateTime: DateTime): TimeType {
return this.toTime(`${dateTime.hour}:${dateTime.minute}:${dateTime.second}`)
}
} }

View File

@@ -7,9 +7,11 @@ export class JwtUtils {
signToken(payload: string, expiresIn: string) { signToken(payload: string, expiresIn: string) {
return sign(payload, process.env.JWT_SECRET!, { expiresIn }) return sign(payload, process.env.JWT_SECRET!, { expiresIn })
} }
//eslint-disable-next-line @typescript-eslint/no-explicit-any signTokenRS256(payload: string, expiresIn: string) {
signTokenRS256(payload: any, expiresIn: string) { const privateKey = process.env.JWT_RS256_PRIVATE_KEY
const privateKey = process.env.JWT_RS256_PRIVATE_KEY! if (!privateKey) {
throw new Error('JWT_RS256_PRIVATE_KEY is not defined')
}
return sign(payload, privateKey, { return sign(payload, privateKey, {
algorithm: 'RS256', algorithm: 'RS256',
expiresIn, expiresIn,
@@ -17,7 +19,10 @@ export class JwtUtils {
} }
verifyTokenRS256(token: string) { verifyTokenRS256(token: string) {
const publicKey = process.env.JWT_RS256_PUBLIC_KEY! const publicKey = process.env.JWT_RS256_PUBLIC_KEY
if (!publicKey) {
throw new Error('JWT_RS256_PUBLIC_KEY is not defined')
}
return verify(token, publicKey, { return verify(token, publicKey, {
algorithms: ['RS256'], algorithms: ['RS256'],
}) })

File diff suppressed because one or more lines are too long