chore: combine context

This commit is contained in:
2024-12-20 18:30:50 +07:00
parent 461f2653e3
commit 776881f961
23 changed files with 532 additions and 694 deletions

View File

@@ -171,19 +171,16 @@ export class AnalyticSchema extends PothosSchema {
type: this.customerAnalytic(), type: this.customerAnalytic(),
description: 'Retrieve a single customer analytic.', description: 'Retrieve a single customer analytic.',
resolve: async (_parent, _args, ctx, _info) => { resolve: async (_parent, _args, ctx, _info) => {
if (ctx.isSubscription) { if (!ctx.me) {
throw new Error('Not allowed')
}
if (!ctx.http.me) {
throw new Error('Unauthorized') throw new Error('Unauthorized')
} }
if (ctx.http.me.role !== Role.CUSTOMER) { if (ctx.me.role !== Role.CUSTOMER) {
throw new Error('Only customers can access this data') throw new Error('Only customers can access this data')
} }
// calculate analytic // calculate analytic
const activeServiceCount = await this.prisma.order.count({ const activeServiceCount = await this.prisma.order.count({
where: { where: {
userId: ctx.http.me.id, userId: ctx.me.id,
status: OrderStatus.PAID, status: OrderStatus.PAID,
schedule: { schedule: {
dates: { dates: {
@@ -198,12 +195,12 @@ export class AnalyticSchema extends PothosSchema {
}) })
const totalServiceCount = await this.prisma.order.count({ const totalServiceCount = await this.prisma.order.count({
where: { where: {
userId: ctx.http.me.id, userId: ctx.me.id,
}, },
}) })
const totalSpent = await this.prisma.order.aggregate({ const totalSpent = await this.prisma.order.aggregate({
where: { where: {
userId: ctx.http.me.id, userId: ctx.me.id,
status: OrderStatus.PAID, status: OrderStatus.PAID,
}, },
_sum: { _sum: {
@@ -211,7 +208,7 @@ export class AnalyticSchema extends PothosSchema {
}, },
}) })
return { return {
userId: ctx.http.me.id, userId: ctx.me.id,
activeServiceCount: activeServiceCount, activeServiceCount: activeServiceCount,
totalServiceCount: totalServiceCount, totalServiceCount: totalServiceCount,
totalSpent: totalSpent._sum.total, totalSpent: totalSpent._sum.total,
@@ -223,18 +220,15 @@ export class AnalyticSchema extends PothosSchema {
type: this.mentorAnalytic(), type: this.mentorAnalytic(),
description: 'Retrieve a single mentor analytic.', description: 'Retrieve a single mentor analytic.',
resolve: async (_parent, _args, ctx, _info) => { resolve: async (_parent, _args, ctx, _info) => {
if (ctx.isSubscription) { if (!ctx.me) {
throw new Error('Not allowed')
}
if (!ctx.http.me) {
throw new Error('Unauthorized') throw new Error('Unauthorized')
} }
if (ctx.http.me.role !== Role.CENTER_MENTOR) { if (ctx.me.role !== Role.CENTER_MENTOR) {
throw new Error('Only center mentors can access this data') throw new Error('Only center mentors can access this data')
} }
// calculate analytic // calculate analytic
return { return {
userId: ctx.http.me.id, userId: ctx.me.id,
} }
}, },
}), }),
@@ -242,19 +236,16 @@ export class AnalyticSchema extends PothosSchema {
type: this.centerAnalytic(), type: this.centerAnalytic(),
description: 'Retrieve a single center analytic.', description: 'Retrieve a single center analytic.',
resolve: async (_parent, _args, ctx, _info) => { resolve: async (_parent, _args, ctx, _info) => {
if (ctx.isSubscription) { if (!ctx.me) {
throw new Error('Not allowed')
}
if (!ctx.http.me) {
throw new Error('Unauthorized') throw new Error('Unauthorized')
} }
if (ctx.http.me.role !== Role.CENTER_OWNER) { if (ctx.me.role !== Role.CENTER_OWNER) {
throw new Error('Only center owners can access this data') throw new Error('Only center owners can access this data')
} }
// get center by owner id // get center by owner id
const center = await this.prisma.center.findUnique({ const center = await this.prisma.center.findUnique({
where: { where: {
centerOwnerId: ctx.http.me.id, centerOwnerId: ctx.me.id,
}, },
}) })
if (!center) { if (!center) {
@@ -266,7 +257,7 @@ export class AnalyticSchema extends PothosSchema {
const activeMentorCount = await this.prisma.user.count({ const activeMentorCount = await this.prisma.user.count({
where: { where: {
center: { center: {
centerOwnerId: ctx.http.me.id, centerOwnerId: ctx.me.id,
}, },
banned: false, banned: false,
}, },
@@ -340,13 +331,10 @@ export class AnalyticSchema extends PothosSchema {
}, },
description: 'Retrieve a single platform analytic.', description: 'Retrieve a single platform analytic.',
resolve: async (_parent, args, ctx, _info) => { resolve: async (_parent, args, ctx, _info) => {
if (ctx.isSubscription) { if (!ctx.me) {
throw new Error('Not allowed')
}
if (!ctx.http.me) {
throw new Error('Unauthorized') throw new Error('Unauthorized')
} }
if (ctx.http.me.role !== Role.ADMIN && ctx.http.me.role !== Role.MODERATOR) { if (ctx.me.role !== Role.ADMIN && ctx.me.role !== Role.MODERATOR) {
throw new Error('Only admins and moderators can access this data') throw new Error('Only admins and moderators can access this data')
} }
// calculate analytic for services sorted by args.serviceSortBy and args.timeframes // calculate analytic for services sorted by args.serviceSortBy and args.timeframes

View File

@@ -154,16 +154,13 @@ export class CenterSchema extends PothosSchema {
}), }),
}, },
resolve: async (query, _root, args, ctx) => { resolve: async (query, _root, args, ctx) => {
if (ctx.isSubscription) { if (ctx.me?.role !== Role.CUSTOMER) {
throw new Error('Not allowed in subscription')
}
if (ctx.http.me?.role !== Role.CUSTOMER) {
throw new Error('Not allowed') throw new Error('Not allowed')
} }
// check if user has already created a center // check if user has already created a center
const existingCenter = await this.prisma.center.findFirst({ const existingCenter = await this.prisma.center.findFirst({
where: { where: {
centerOwnerId: ctx.http.me?.id, centerOwnerId: ctx.me?.id,
}, },
}) })
if (existingCenter) { if (existingCenter) {
@@ -239,10 +236,7 @@ export class CenterSchema extends PothosSchema {
}, },
resolve: async (query, _root, args, ctx) => { resolve: async (query, _root, args, ctx) => {
return await this.prisma.$transaction(async (prisma) => { return await this.prisma.$transaction(async (prisma) => {
if (ctx.isSubscription) { if (ctx.me?.role !== Role.ADMIN && ctx.me?.role !== Role.MODERATOR) {
throw new Error('Not allowed in subscription')
}
if (ctx.http.me?.role !== Role.ADMIN && ctx.http.me?.role !== Role.MODERATOR) {
throw new Error('Not allowed') throw new Error('Not allowed')
} }
const center = await prisma.center.findUnique({ const center = await prisma.center.findUnique({

View File

@@ -139,11 +139,11 @@ export class CenterMentorSchema extends PothosSchema {
}, },
resolve: async (_query, _root, args, ctx) => { resolve: async (_query, _root, args, ctx) => {
return this.prisma.$transaction(async (prisma) => { return this.prisma.$transaction(async (prisma) => {
if (ctx.isSubscription) { if (!ctx.me) {
throw new Error('Not allowed') throw new Error('User not found')
} }
// get centerId by user id from context // get centerId by user id from context
const userId = ctx.http.me?.id const userId = ctx.me.id
if (!userId) { if (!userId) {
throw new Error('User ID is required') throw new Error('User ID is required')
} }
@@ -205,8 +205,8 @@ export class CenterMentorSchema extends PothosSchema {
adminNote: t.arg({ type: 'String', required: false }), adminNote: t.arg({ type: 'String', required: false }),
}, },
resolve: async (_query, _root, args, ctx, _info) => { resolve: async (_query, _root, args, ctx, _info) => {
if (ctx.isSubscription) { if (!ctx.me) {
throw new Error('Not allowed') throw new Error('User not found')
} }
return this.prisma.$transaction(async (prisma) => { return this.prisma.$transaction(async (prisma) => {
// get mentor info // get mentor info
@@ -255,7 +255,7 @@ export class CenterMentorSchema extends PothosSchema {
data: { data: {
content: args.adminNote ?? '', content: args.adminNote ?? '',
mentorId: mentor.id, mentorId: mentor.id,
notedByUserId: ctx.http.me?.id ?? '', notedByUserId: ctx.me.id,
}, },
}) })
// update user role // update user role
@@ -310,7 +310,7 @@ export class CenterMentorSchema extends PothosSchema {
adminNote: { adminNote: {
create: { create: {
content: args.adminNote ?? '', content: args.adminNote ?? '',
notedByUserId: ctx.http.me?.id ?? '', notedByUserId: ctx.me.id,
updatedAt: new Date(), updatedAt: new Date(),
}, },
}, },

View File

@@ -1,88 +1,123 @@
import { Inject, Injectable } from '@nestjs/common' import { Inject, Injectable } from "@nestjs/common";
import { ChatRoomType } from '@prisma/client' import { ChatRoomType } from "@prisma/client";
import { Pothos, PothosRef, PothosSchema, SchemaBuilderToken } from '@smatch-corp/nestjs-pothos' import {
import { Builder } from '../Graphql/graphql.builder' Pothos,
import { PrismaService } from '../Prisma/prisma.service' PothosRef,
PothosSchema,
SchemaBuilderToken,
} from "@smatch-corp/nestjs-pothos";
import { Builder } from "../Graphql/graphql.builder";
import { PrismaService } from "../Prisma/prisma.service";
@Injectable() @Injectable()
export class ChatroomSchema extends PothosSchema { export class ChatroomSchema extends PothosSchema {
constructor( constructor(
@Inject(SchemaBuilderToken) private readonly builder: Builder, @Inject(SchemaBuilderToken) private readonly builder: Builder,
private readonly prisma: PrismaService, private readonly prisma: PrismaService
) { ) {
super() super();
} }
@PothosRef() @PothosRef()
chatRoom() { chatRoom() {
return this.builder.prismaObject('ChatRoom', { return this.builder.prismaObject("ChatRoom", {
description: 'A chat room in the system.', description: "A chat room in the system.",
fields: (t) => ({ fields: (t) => ({
id: t.exposeID('id', { id: t.exposeID("id", {
description: 'The ID of the chat room.', description: "The ID of the chat room.",
}), }),
type: t.expose('type', { type: t.expose("type", {
type: ChatRoomType, type: ChatRoomType,
description: 'The type of the chat room.', description: "The type of the chat room.",
}), }),
customerId: t.exposeID('customerId', { customerId: t.exposeID("customerId", {
description: 'The ID of the customer.', description: "The ID of the customer.",
}), }),
centerId: t.exposeID('centerId', { centerId: t.exposeID("centerId", {
description: 'The ID of the center.', description: "The ID of the center.",
}), }),
mentorId: t.exposeID('mentorId', { mentorId: t.exposeID("mentorId", {
description: 'The ID of the mentor.', description: "The ID of the mentor.",
}), }),
createdAt: t.expose('createdAt', { createdAt: t.expose("createdAt", {
type: 'DateTime', type: "DateTime",
description: 'The date and time the chat room was created.', description: "The date and time the chat room was created.",
}), }),
message: t.relation('message', { message: t.relation("message", {
description: 'The messages in the chat room.', description: "The messages in the chat room.",
}), }),
customer: t.relation('customer', { customer: t.relation("customer", {
description: 'The customer.', description: "The customer.",
}), }),
center: t.relation('center', { center: t.relation("center", {
description: 'The center.', description: "The center.",
}), }),
mentor: t.relation('mentor', { mentor: t.relation("mentor", {
description: 'The mentor.', description: "The mentor.",
}), }),
collaborationSession: t.relation('CollaborationSession', { collaborationSession: t.relation("CollaborationSession", {
description: 'The collaboration session.', description: "The collaboration session.",
}), }),
lastActivity: t.expose('lastActivity', { lastActivity: t.expose("lastActivity", {
type: 'DateTime', type: "DateTime",
description: 'The last activity date and time.', description: "The last activity date and time.",
}), }),
order: t.relation('Order', { order: t.relation("Order", {
description: 'The order.', description: "The order.",
}), }),
}), }),
}) });
} }
// @PothosRef()
// chatUser() {
// return this.builder.simpleObject("ChatUser", {
// description: "A user in a chat room.",
// fields: (t) => ({
// user: t.field({
// type: this.userSchema.user(),
// description: "The user.",
// }),
// chatRoom: t.field({
// type: this.chatRoom(),
// description: "The chat room.",
// }),
// lastMessage: t.field({
// type: this.messageSchema.message(),
// description: "The last message.",
// }),
// lastMessageAt: t.field({
// type: "DateTime",
// description: "The date and time of the last message.",
// }),
// unreadCount: t.field({
// type: "Int",
// description: "The number of unread messages.",
// }),
// }),
// });
// }
@Pothos() @Pothos()
init(): void { init(): void {
this.builder.queryFields((t) => ({ this.builder.queryFields((t) => ({
chatRoom: t.prismaField({ chatRoom: t.prismaField({
type: this.chatRoom(), type: this.chatRoom(),
description: 'Retrieve a single chat room by its unique identifier.', description: "Retrieve a single chat room by its unique identifier.",
args: this.builder.generator.findUniqueArgs('ChatRoom'), args: this.builder.generator.findUniqueArgs("ChatRoom"),
resolve: async (query, _root, args, _ctx, _info) => { resolve: async (query, _root, args, _ctx, _info) => {
return await this.prisma.chatRoom.findUnique({ return await this.prisma.chatRoom.findUnique({
...query, ...query,
where: args.where, where: args.where,
}) });
}, },
}), }),
chatRooms: t.prismaField({ chatRooms: t.prismaField({
type: [this.chatRoom()], type: [this.chatRoom()],
description: 'Retrieve a list of chat rooms with optional filtering, ordering, and pagination.', description:
args: this.builder.generator.findManyArgs('ChatRoom'), "Retrieve a list of chat rooms with optional filtering, ordering, and pagination.",
args: this.builder.generator.findManyArgs("ChatRoom"),
resolve: async (query, _root, args, _ctx, _info) => { resolve: async (query, _root, args, _ctx, _info) => {
return await this.prisma.chatRoom.findMany({ return await this.prisma.chatRoom.findMany({
...query, ...query,
@@ -90,9 +125,30 @@ export class ChatroomSchema extends PothosSchema {
take: args.take ?? undefined, take: args.take ?? undefined,
orderBy: args.orderBy ?? undefined, orderBy: args.orderBy ?? undefined,
where: args.filter ?? undefined, where: args.filter ?? undefined,
}) });
}, },
}), }),
})) // chatUsers: t.prismaField({
// type: [this.chatUser()],
// args: {
// chatRoomId: t.arg({
// type: "ID",
// description: "The ID of the chat room.",
// }),
// },
// description: "Retrieve a list of chat users.",
// args: this.builder.generator.findManyArgs("ChatUser"),
// resolve: async (query, _root, args, ctx, _info) => {
// if (ctx.isSubscription) {
// throw new Error("Not allowed");
// }
// if (!ctx.user) {
// throw new Error("Unauthorized");
// }
// // TODO: get chat users for the chat room
// return [];
// },
// }),
}));
} }
} }

View File

@@ -73,10 +73,7 @@ export class CollaborationSessionSchema extends PothosSchema {
}, },
description: 'Retrieve a single collaboration session by its unique identifier.', description: 'Retrieve a single collaboration session by its unique identifier.',
resolve: async (_query, _root, args, ctx, _info) => { resolve: async (_query, _root, args, ctx, _info) => {
if (ctx.isSubscription) { if (!ctx.me) {
throw new Error('Not allowed')
}
if (!ctx.http.me) {
throw new Error('Cannot get your info') throw new Error('Cannot get your info')
} }
const scheduleDate = await this.prisma.scheduleDate.findUnique({ const scheduleDate = await this.prisma.scheduleDate.findUnique({
@@ -94,9 +91,9 @@ export class CollaborationSessionSchema extends PothosSchema {
}, },
}) })
/* ---------- use case 1 : customer get collaboration session by id --------- */ /* ---------- use case 1 : customer get collaboration session by id --------- */
if (ctx.http.me?.role === Role.CUSTOMER && collaborationSession) { if (ctx.me?.role === Role.CUSTOMER && collaborationSession) {
// check if user is participant // check if user is participant
if (!collaborationSession.collaboratorsIds.includes(ctx.http.me.id)) { if (!collaborationSession.collaboratorsIds.includes(ctx.me.id)) {
throw new Error('User not allowed') throw new Error('User not allowed')
} }
// update schedule date status // update schedule date status
@@ -111,14 +108,14 @@ export class CollaborationSessionSchema extends PothosSchema {
return collaborationSession return collaborationSession
} }
/* ---------- use case 2 : center mentor get collaboration session by schedule date id --------- */ /* ---------- use case 2 : center mentor get collaboration session by schedule date id --------- */
if (ctx.http.me.role !== Role.CENTER_MENTOR && ctx.http.me.role !== Role.CENTER_OWNER) { if (ctx.me.role !== Role.CENTER_MENTOR && ctx.me.role !== Role.CENTER_OWNER) {
if (!collaborationSession) { if (!collaborationSession) {
throw new Error('Mentor does not created collaboration session yet') throw new Error('Mentor does not created collaboration session yet')
} }
throw new Error('User not allowed') throw new Error('User not allowed')
} }
// check if user is participant // check if user is participant
if (!scheduleDate.participantIds.includes(ctx.http.me.id)) { if (!scheduleDate.participantIds.includes(ctx.me.id)) {
throw new Error('User not allowed') throw new Error('User not allowed')
} }
// check if order is exist in schedule date // check if order is exist in schedule date
@@ -218,16 +215,13 @@ export class CollaborationSessionSchema extends PothosSchema {
liveKitToken: t.field({ liveKitToken: t.field({
type: 'String', type: 'String',
resolve: async (_, _args, ctx: SchemaContext) => { resolve: async (_, _args, ctx: SchemaContext) => {
if (ctx.isSubscription) { if (!ctx.me?.id) {
throw new Error('Not allowed')
}
if (!ctx.http?.me?.id) {
throw new Error('Unauthorized') throw new Error('Unauthorized')
} }
// check if participantId is in meetingRoomCollaborators // check if participantId is in meetingRoomCollaborators
const meetingRoomCollaborator = await this.prisma.meetingRoomCollaborator.findFirst({ const meetingRoomCollaborator = await this.prisma.meetingRoomCollaborator.findFirst({
where: { where: {
userId: ctx.http.me.id, userId: ctx.me.id,
}, },
}) })
if (!meetingRoomCollaborator) { if (!meetingRoomCollaborator) {
@@ -241,7 +235,7 @@ export class CollaborationSessionSchema extends PothosSchema {
if (!meetingRoom) { if (!meetingRoom) {
throw new Error('Meeting room not found') throw new Error('Meeting room not found')
} }
const token = await this.liveKitService.createToken(ctx.http.me, meetingRoom.id) const token = await this.liveKitService.createToken(ctx.me, meetingRoom.id)
return token return token
}, },
}), }),
@@ -263,10 +257,7 @@ export class CollaborationSessionSchema extends PothosSchema {
}, },
description: 'Update the active document ID for a collaboration session.', description: 'Update the active document ID for a collaboration session.',
resolve: async (_query, _root, args, ctx, _info) => { resolve: async (_query, _root, args, ctx, _info) => {
if (ctx.isSubscription) { if (!ctx.me) {
throw new Error('Not allowed')
}
if (!ctx.http.me) {
throw new Error('Cannot get your info') throw new Error('Cannot get your info')
} }
// check permission // check permission
@@ -281,7 +272,7 @@ export class CollaborationSessionSchema extends PothosSchema {
if (!collaborationSession) { if (!collaborationSession) {
throw new Error('Collaboration session not found') throw new Error('Collaboration session not found')
} }
if (!collaborationSession.scheduleDate.participantIds.includes(ctx.http.me.id)) { if (!collaborationSession.scheduleDate.participantIds.includes(ctx.me.id)) {
throw new Error('User not allowed') throw new Error('User not allowed')
} }
const updatedCollaborationSession = await this.prisma.collaborationSession.update({ const updatedCollaborationSession = await this.prisma.collaborationSession.update({
@@ -292,7 +283,7 @@ export class CollaborationSessionSchema extends PothosSchema {
activeDocumentId: args.activeDocumentId, activeDocumentId: args.activeDocumentId,
}, },
}) })
ctx.http.pubSub.publish(`collaborationSessionUpdated:${collaborationSession.id}`, updatedCollaborationSession) ctx.pubSub.publish(`collaborationSessionUpdated:${collaborationSession.id}`, updatedCollaborationSession)
Logger.log(`Collaboration session updated: ${updatedCollaborationSession.id}`, 'updateActiveDocumentId') Logger.log(`Collaboration session updated: ${updatedCollaborationSession.id}`, 'updateActiveDocumentId')
return updatedCollaborationSession return updatedCollaborationSession
}, },
@@ -310,10 +301,7 @@ export class CollaborationSessionSchema extends PothosSchema {
}), }),
}, },
subscribe: async (_parent, args, ctx) => { subscribe: async (_parent, args, ctx) => {
if (!ctx.isSubscription) { if (!ctx.me) {
throw new Error('Not allowed')
}
if (!ctx.websocket.me) {
throw new Error('Cannot get your info') throw new Error('Cannot get your info')
} }
const collaborationSession = await this.prisma.collaborationSession.findUnique({ const collaborationSession = await this.prisma.collaborationSession.findUnique({
@@ -324,7 +312,7 @@ export class CollaborationSessionSchema extends PothosSchema {
if (!collaborationSession) { if (!collaborationSession) {
throw new Error('Collaboration session not found') throw new Error('Collaboration session not found')
} }
return ctx.websocket.pubSub.asyncIterator([ return ctx.pubSub.asyncIterator([
`collaborationSessionUpdated:${collaborationSession.id}`, `collaborationSessionUpdated:${collaborationSession.id}`,
]) as unknown as AsyncIterable<CollaborationSession> ]) as unknown as AsyncIterable<CollaborationSession>
}, },

View File

@@ -206,10 +206,7 @@ export class DocumentSchema extends PothosSchema {
type: [this.document()], type: [this.document()],
args: this.builder.generator.findManyArgs("Document"), args: this.builder.generator.findManyArgs("Document"),
resolve: async (query, _parent, args, ctx: SchemaContext) => { resolve: async (query, _parent, args, ctx: SchemaContext) => {
if (ctx.isSubscription) { if (!ctx.me?.id) {
throw new Error("Not allowed");
}
if (!ctx.http?.me?.id) {
throw new Error("User not found"); throw new Error("User not found");
} }
return await this.prisma.document.findMany({ return await this.prisma.document.findMany({
@@ -217,8 +214,8 @@ export class DocumentSchema extends PothosSchema {
orderBy: args.orderBy ?? undefined, orderBy: args.orderBy ?? undefined,
where: { where: {
OR: [ OR: [
{ ownerId: ctx.http.me.id }, { ownerId: ctx.me.id },
{ collaborators: { some: { userId: ctx.http.me.id } } }, { collaborators: { some: { userId: ctx.me.id } } },
], ],
}, },
}); });
@@ -252,10 +249,7 @@ export class DocumentSchema extends PothosSchema {
type: this.document(), type: this.document(),
args: {}, args: {},
resolve: async (query, _args, ctx, _info) => { resolve: async (query, _args, ctx, _info) => {
if (ctx.isSubscription) { const userId = ctx.me?.id;
throw new Error("Not allowed");
}
const userId = ctx.http?.me?.id;
if (!userId) { if (!userId) {
throw new Error("User not found"); throw new Error("User not found");
} }
@@ -271,33 +265,6 @@ export class DocumentSchema extends PothosSchema {
}, },
}), }),
testCheckGrammar: t.field({
type: "Boolean",
args: {
documentId: t.arg({ type: "String", required: true }),
pageId: t.arg({ type: "Int", required: true }),
promptType: t.arg({
type: this.builder.enumType("PromptType", {
values: [
"CHECK_GRAMMAR",
"REWRITE_TEXT",
"SUMMARIZE",
"TRANSLATE",
"EXPAND_CONTENT",
] as const,
}),
required: true,
}),
},
resolve: async (_query, args, _ctx: SchemaContext) => {
await this.documentService.checkGrammarForPage(
args.documentId,
args.pageId,
args.promptType as PromptType
);
return true;
},
}),
// exportDocument: t.field({ // exportDocument: t.field({
// type: this.DocumentExportObject(), // type: this.DocumentExportObject(),
@@ -344,10 +311,7 @@ export class DocumentSchema extends PothosSchema {
pageIndex: t.arg({ type: "Int", required: true }), pageIndex: t.arg({ type: "Int", required: true }),
}, },
resolve: async (_, args, ctx: SchemaContext) => { resolve: async (_, args, ctx: SchemaContext) => {
if (ctx.isSubscription) { if (!ctx.me?.id) {
throw new Error("Not allowed");
}
if (!ctx.http?.me?.id) {
throw new Error("User not found"); throw new Error("User not found");
} }
if (!args.documentId) { if (!args.documentId) {
@@ -371,7 +335,7 @@ export class DocumentSchema extends PothosSchema {
pageIndex: args.pageIndex, pageIndex: args.pageIndex,
delta, delta,
totalPage, totalPage,
senderId: ctx.http?.me?.id, senderId: ctx.me?.id,
eventType: DocumentEvent.CLIENT_REQUEST_SYNC, eventType: DocumentEvent.CLIENT_REQUEST_SYNC,
}; };
}, },
@@ -398,10 +362,7 @@ export class DocumentSchema extends PothosSchema {
}), }),
}, },
resolve: async (query, _parent, args, ctx: SchemaContext) => { resolve: async (query, _parent, args, ctx: SchemaContext) => {
if (ctx.isSubscription) { const userId = ctx.me?.id;
throw new Error("Not allowed");
}
const userId = ctx.http?.me?.id;
if (!userId) { if (!userId) {
throw new Error("Unauthorized"); throw new Error("Unauthorized");
} }
@@ -430,13 +391,10 @@ export class DocumentSchema extends PothosSchema {
}), }),
}, },
resolve: async (_, args, ctx: SchemaContext) => { resolve: async (_, args, ctx: SchemaContext) => {
if (ctx.isSubscription) {
throw new Error("Not allowed");
}
const { const {
http: { pubSub }, pubSub,
} = ctx; } = ctx;
const senderId = ctx.http?.me?.id; const senderId = ctx.me?.id;
if (!senderId) { if (!senderId) {
throw new Error("User not found"); throw new Error("User not found");
} }
@@ -466,10 +424,7 @@ export class DocumentSchema extends PothosSchema {
data: t.arg({ type: this.documentDeltaInput(), required: true }), data: t.arg({ type: this.documentDeltaInput(), required: true }),
}, },
resolve: async (_, args, ctx: SchemaContext) => { resolve: async (_, args, ctx: SchemaContext) => {
if (ctx.isSubscription) { const senderId = ctx.me?.id;
throw new Error("Not allowed");
}
const senderId = ctx.http?.me?.id;
if (!args.data.documentId) { if (!args.data.documentId) {
throw new Error("Document id not found"); throw new Error("Document id not found");
} }
@@ -522,10 +477,7 @@ export class DocumentSchema extends PothosSchema {
}), }),
}, },
resolve: async (query, _parent, args, ctx: SchemaContext) => { resolve: async (query, _parent, args, ctx: SchemaContext) => {
if (ctx.isSubscription) { if (!ctx.me?.id) {
throw new Error("Not allowed");
}
if (!ctx.http?.me?.id) {
throw new Error("Unauthorized"); throw new Error("Unauthorized");
} }
// check if user is owner or collaborator // check if user is owner or collaborator
@@ -541,9 +493,9 @@ export class DocumentSchema extends PothosSchema {
if ( if (
!document.isPublic && !document.isPublic &&
!document.collaborators.some( !document.collaborators.some(
(c) => c.userId === ctx.http?.me?.id && c.writable (c) => c.userId === ctx.me?.id && c.writable
) && ) &&
document.ownerId !== ctx.http?.me?.id document.ownerId !== ctx.me?.id
) { ) {
throw new Error("User is not owner or collaborator of document"); throw new Error("User is not owner or collaborator of document");
} }
@@ -563,9 +515,6 @@ export class DocumentSchema extends PothosSchema {
writable: t.arg({ type: "Boolean", required: true }), writable: t.arg({ type: "Boolean", required: true }),
}, },
resolve: async (_, __, args, ctx: SchemaContext) => { resolve: async (_, __, args, ctx: SchemaContext) => {
if (ctx.isSubscription) {
throw new Error("Not allowed");
}
// check if ctx user is owner of document // check if ctx user is owner of document
const document = await this.prisma.document.findUnique({ const document = await this.prisma.document.findUnique({
where: { id: args.documentId }, where: { id: args.documentId },
@@ -573,7 +522,7 @@ export class DocumentSchema extends PothosSchema {
if (!document) { if (!document) {
throw new Error("Document not found"); throw new Error("Document not found");
} }
if (document.ownerId !== ctx.http?.me?.id) { if (document.ownerId !== ctx.me?.id) {
throw new Error("User is not owner of document"); throw new Error("User is not owner of document");
} }
return await this.prisma.documentCollaborator.create({ return await this.prisma.documentCollaborator.create({
@@ -593,9 +542,6 @@ export class DocumentSchema extends PothosSchema {
userId: t.arg({ type: "String", required: true }), userId: t.arg({ type: "String", required: true }),
}, },
resolve: async (_, __, args, ctx: SchemaContext) => { resolve: async (_, __, args, ctx: SchemaContext) => {
if (ctx.isSubscription) {
throw new Error("Not allowed");
}
// check if ctx user is owner of document // check if ctx user is owner of document
const document = await this.prisma.document.findUnique({ const document = await this.prisma.document.findUnique({
where: { id: args.documentId }, where: { id: args.documentId },
@@ -603,7 +549,7 @@ export class DocumentSchema extends PothosSchema {
if (!document) { if (!document) {
throw new Error("Document not found"); throw new Error("Document not found");
} }
if (document.ownerId !== ctx.http?.me?.id) { if (document.ownerId !== ctx.me?.id) {
throw new Error("User is not owner of document"); throw new Error("User is not owner of document");
} }
return await this.prisma.documentCollaborator.delete({ return await this.prisma.documentCollaborator.delete({
@@ -625,9 +571,6 @@ export class DocumentSchema extends PothosSchema {
writable: t.arg({ type: "Boolean", required: true }), writable: t.arg({ type: "Boolean", required: true }),
}, },
resolve: async (_, __, args, ctx: SchemaContext) => { resolve: async (_, __, args, ctx: SchemaContext) => {
if (ctx.isSubscription) {
throw new Error("Not allowed");
}
// check if ctx user is owner of document // check if ctx user is owner of document
const document = await this.prisma.document.findUnique({ const document = await this.prisma.document.findUnique({
where: { id: args.documentId }, where: { id: args.documentId },
@@ -635,7 +578,7 @@ export class DocumentSchema extends PothosSchema {
if (!document) { if (!document) {
throw new Error("Document not found"); throw new Error("Document not found");
} }
if (document.ownerId !== ctx.http?.me?.id) { if (document.ownerId !== ctx.me?.id) {
throw new Error("User is not owner of document"); throw new Error("User is not owner of document");
} }
return await this.prisma.documentCollaborator.update({ return await this.prisma.documentCollaborator.update({
@@ -656,9 +599,6 @@ export class DocumentSchema extends PothosSchema {
imageId: t.arg({ type: "String", required: true }), imageId: t.arg({ type: "String", required: true }),
}, },
resolve: async (query, _parent, args, ctx: SchemaContext) => { resolve: async (query, _parent, args, ctx: SchemaContext) => {
if (ctx.isSubscription) {
throw new Error("Not allowed");
}
const document = await this.prisma.document.findUnique({ const document = await this.prisma.document.findUnique({
where: { id: args.documentId }, where: { id: args.documentId },
include: { include: {
@@ -669,9 +609,9 @@ export class DocumentSchema extends PothosSchema {
throw new Error("Document not found"); throw new Error("Document not found");
} }
if ( if (
document.ownerId !== ctx.http?.me?.id && document.ownerId !== ctx.me?.id &&
!document.collaborators.some( !document.collaborators.some(
(c) => c.userId === ctx.http?.me?.id && (c.writable || c.readable) (c) => c.userId === ctx.me?.id && (c.writable || c.readable)
) )
) { ) {
throw new Error("User is not owner or collaborator of document"); throw new Error("User is not owner or collaborator of document");
@@ -702,9 +642,6 @@ export class DocumentSchema extends PothosSchema {
}), }),
}, },
subscribe: async (_, args, ctx: SchemaContext) => { subscribe: async (_, args, ctx: SchemaContext) => {
if (!ctx.isSubscription) {
throw new Error("Not allowed");
}
const documentId = args.documentId; const documentId = args.documentId;
// check user permission // check user permission
const document = await this.prisma.document.findUnique({ const document = await this.prisma.document.findUnique({
@@ -718,17 +655,17 @@ export class DocumentSchema extends PothosSchema {
} }
if (!document.isPublic) { if (!document.isPublic) {
if ( if (
document.ownerId !== ctx.websocket?.me?.id && document.ownerId !== ctx.me?.id &&
!document.collaborators.some( !document.collaborators.some(
(c) => (c) =>
c.userId === ctx.websocket?.me?.id && c.userId === ctx.me?.id &&
(c.writable || c.readable) (c.writable || c.readable)
) )
) { ) {
throw new Error("User is not owner or collaborator of document"); throw new Error("User is not owner or collaborator of document");
} }
} }
return ctx.websocket.pubSub.asyncIterator([ return ctx.pubSub.asyncIterator([
`${DocumentEvent.CHANGED}.${documentId}`, `${DocumentEvent.CHANGED}.${documentId}`,
`${DocumentEvent.DELETED}.${documentId}`, `${DocumentEvent.DELETED}.${documentId}`,
`${DocumentEvent.SAVED}.${documentId}`, `${DocumentEvent.SAVED}.${documentId}`,
@@ -739,11 +676,7 @@ export class DocumentSchema extends PothosSchema {
`${DocumentEvent.CURSOR_MOVED}.${documentId}`, `${DocumentEvent.CURSOR_MOVED}.${documentId}`,
]) as unknown as AsyncIterable<DocumentDelta>; ]) as unknown as AsyncIterable<DocumentDelta>;
}, },
resolve: async (payload: DocumentDelta, _args, ctx: SchemaContext) => { resolve: async (payload: DocumentDelta, _args, _ctx: SchemaContext) => {
if (!ctx.isSubscription) {
throw new Error("Not allowed");
}
// If there's an explicit sync request, pass it through immediately // If there's an explicit sync request, pass it through immediately
if (payload.requestSync) { if (payload.requestSync) {
return payload; return payload;
@@ -797,9 +730,6 @@ export class DocumentSchema extends PothosSchema {
pageIndex: number, pageIndex: number,
ctx: SchemaContext ctx: SchemaContext
): Promise<void> { ): Promise<void> {
if (!ctx.isSubscription) {
throw new Error("Not allowed");
}
try { try {
const syncKey = `document:sync:${documentId}:${pageIndex}`; const syncKey = `document:sync:${documentId}:${pageIndex}`;
const currentTime = DateTimeUtils.now().toMillis().toString(); const currentTime = DateTimeUtils.now().toMillis().toString();
@@ -823,7 +753,7 @@ export class DocumentSchema extends PothosSchema {
} }
// Optionally publish AI suggestion if needed // Optionally publish AI suggestion if needed
ctx.websocket.pubSub.publish( ctx.pubSub.publish(
`${DocumentEvent.AI_SUGGESTION}.${documentId}`, `${DocumentEvent.AI_SUGGESTION}.${documentId}`,
{ {
documentId, documentId,

View File

@@ -1,93 +1,84 @@
import { Injectable, Logger } from '@nestjs/common' import { Injectable, Logger } from "@nestjs/common";
import SchemaBuilder from '@pothos/core' import SchemaBuilder from "@pothos/core";
import AuthzPlugin from '@pothos/plugin-authz' import AuthzPlugin from "@pothos/plugin-authz";
import ErrorsPlugin from '@pothos/plugin-errors' import ErrorsPlugin from "@pothos/plugin-errors";
import PrismaPlugin, { PothosPrismaDatamodel, PrismaClient } from '@pothos/plugin-prisma' import PrismaPlugin, {
import PrismaUtils from '@pothos/plugin-prisma-utils' PothosPrismaDatamodel,
import RelayPlugin from '@pothos/plugin-relay' PrismaClient,
import SimpleObjectPlugin from '@pothos/plugin-simple-objects' } from "@pothos/plugin-prisma";
import SmartSubscriptionPlugin, { subscribeOptionsFromIterator } from '@pothos/plugin-smart-subscriptions' import PrismaUtils from "@pothos/plugin-prisma-utils";
import ZodPlugin from '@pothos/plugin-zod' import RelayPlugin from "@pothos/plugin-relay";
import { User } from '@prisma/client' import SimpleObjectPlugin from "@pothos/plugin-simple-objects";
import { JsonValue } from '@prisma/client/runtime/library' import SmartSubscriptionPlugin, {
import { Request, Response } from 'express' subscribeOptionsFromIterator,
import { Kind, ValueNode } from 'graphql' } from "@pothos/plugin-smart-subscriptions";
import { RedisPubSub } from 'graphql-redis-subscriptions' import ZodPlugin from "@pothos/plugin-zod";
import { JSONObjectResolver } from 'graphql-scalars' import { User } from "@prisma/client";
import { JsonValue } from "@prisma/client/runtime/library";
import { Request, Response } from "express";
import { Kind, ValueNode } from "graphql";
import { RedisPubSub } from "graphql-redis-subscriptions";
import { JSONObjectResolver } from "graphql-scalars";
// @ts-expect-error // @ts-expect-error
import GraphQLUpload from 'graphql-upload/GraphQLUpload.mjs' import GraphQLUpload from "graphql-upload/GraphQLUpload.mjs";
// @ts-expect-error // @ts-expect-error
import type { FileUpload } from 'graphql-upload/processRequest.mjs' import type { FileUpload } from "graphql-upload/processRequest.mjs";
import { DateTime } from 'luxon' import { DateTime } from "luxon";
import Delta from 'quill-delta' import Delta from "quill-delta";
import { DateTimeUtils } from '../common/utils/datetime.utils' import { DateTimeUtils } from "../common/utils/datetime.utils";
import type PrismaTypes from '../types/pothos.generated' import type PrismaTypes from "../types/pothos.generated";
import { getDatamodel } from '../types/pothos.generated' import { getDatamodel } from "../types/pothos.generated";
import { PrismaCrudGenerator } from './graphql.generator' import { PrismaCrudGenerator } from "./graphql.generator";
export type SchemaContext =
| {
isSubscription: true
websocket: {
req: Request
pubSub: RedisPubSub
sessionId: string
me: User
generator: PrismaCrudGenerator<BuilderTypes>
}
}
| {
isSubscription: false
http: {
req: Request
res: Response
me: User | null
pubSub: RedisPubSub
invalidateCache: () => Promise<void>
generator: PrismaCrudGenerator<BuilderTypes>
}
}
export type SchemaContext = {
req: Request;
pubSub: RedisPubSub;
sessionId: string;
me: User;
generator: PrismaCrudGenerator<BuilderTypes>;
res: Response;
invalidateCache: () => Promise<void>;
};
// extend prisma types to contain string type // extend prisma types to contain string type
export interface SchemaBuilderOption { export interface SchemaBuilderOption {
Context: SchemaContext Context: SchemaContext;
PrismaTypes: PrismaTypes PrismaTypes: PrismaTypes;
DataModel: PothosPrismaDatamodel DataModel: PothosPrismaDatamodel;
Connection: { Connection: {
totalCount: number | (() => number | Promise<number>) totalCount: number | (() => number | Promise<number>);
} };
// AuthZRule: keyof typeof rules; // AuthZRule: keyof typeof rules;
Scalars: { Scalars: {
DateTime: { DateTime: {
Input: string | DateTime | Date Input: string | DateTime | Date;
Output: string | DateTime | Date Output: string | DateTime | Date;
} };
Json: { Json: {
Input: JsonValue Input: JsonValue;
Output: JsonValue Output: JsonValue;
} };
Upload: { Upload: {
Input: FileUpload Input: FileUpload;
Output: FileUpload Output: FileUpload;
} };
Int: { Int: {
Input: number Input: number;
Output: number | bigint | string Output: number | bigint | string;
} };
Delta: { Delta: {
Input: Delta Input: Delta;
Output: Delta Output: Delta;
} };
JsonList: { JsonList: {
Input: JsonValue[] Input: JsonValue[];
Output: JsonValue[] Output: JsonValue[];
} };
} };
} }
@Injectable() @Injectable()
export class Builder extends SchemaBuilder<SchemaBuilderOption> { export class Builder extends SchemaBuilder<SchemaBuilderOption> {
public generator: PrismaCrudGenerator<BuilderTypes> public generator: PrismaCrudGenerator<BuilderTypes>;
constructor(private readonly prisma: PrismaClient) { constructor(private readonly prisma: PrismaClient) {
super({ super({
@@ -104,17 +95,15 @@ export class Builder extends SchemaBuilder<SchemaBuilderOption> {
smartSubscriptions: { smartSubscriptions: {
debounceDelay: 1000, debounceDelay: 1000,
...subscribeOptionsFromIterator((name, context) => { ...subscribeOptionsFromIterator((name, context) => {
return context.isSubscription return context.pubSub.asyncIterator(name);
? context.websocket.pubSub.asyncIterator(name)
: context.http.pubSub.asyncIterator(name)
}), }),
}, },
zod: { zod: {
// optionally customize how errors are formatted // optionally customize how errors are formatted
validationError: (zodError, _args, _context, _info) => { validationError: (zodError, _args, _context, _info) => {
// the default behavior is to just throw the zod error directly // the default behavior is to just throw the zod error directly
Logger.error(zodError.message, 'Zod Error') Logger.error(zodError.message, "Zod Error");
return zodError return zodError;
}, },
}, },
relay: {}, relay: {},
@@ -123,65 +112,70 @@ export class Builder extends SchemaBuilder<SchemaBuilderOption> {
exposeDescriptions: true, exposeDescriptions: true,
filterConnectionTotalCount: true, filterConnectionTotalCount: true,
onUnusedQuery: (info) => { onUnusedQuery: (info) => {
Logger.log(`Unused query: ${info.fieldName}`, 'GraphQL') Logger.log(`Unused query: ${info.fieldName}`, "GraphQL");
}, },
dmmf: getDatamodel(), dmmf: getDatamodel(),
}, },
errors: { errors: {
defaultTypes: [], defaultTypes: [],
}, },
}) });
this.generator = new PrismaCrudGenerator<BuilderTypes>(this) this.generator = new PrismaCrudGenerator<BuilderTypes>(this);
this.scalarType('DateTime', { this.scalarType("DateTime", {
serialize: (value) => { serialize: (value) => {
if (typeof value === 'string') { if (typeof value === "string") {
return value return value;
} }
if (typeof value === 'object' && value !== null && 'toISO' in value) { if (typeof value === "object" && value !== null && "toISO" in value) {
return value return value;
} }
if (value instanceof Date) { if (value instanceof Date) {
return DateTimeUtils.toIsoString(DateTimeUtils.fromDate(value)) return DateTimeUtils.toIsoString(DateTimeUtils.fromDate(value));
} }
throw new Error('Invalid DateTime') throw new Error("Invalid DateTime");
}, },
parseValue: (value) => { parseValue: (value) => {
if (typeof value === 'string') { if (typeof value === "string") {
return DateTimeUtils.fromIsoString(value) return DateTimeUtils.fromIsoString(value);
} }
throw new Error('Invalid DateTime') throw new Error("Invalid DateTime");
}, },
parseLiteral: (ast) => { parseLiteral: (ast) => {
if (ast.kind === Kind.STRING) { if (ast.kind === Kind.STRING) {
return DateTimeUtils.fromIsoString(ast.value) return DateTimeUtils.fromIsoString(ast.value);
} }
throw new Error('Invalid DateTime') throw new Error("Invalid DateTime");
}, },
}) });
this.scalarType('Delta', { this.scalarType("Delta", {
serialize: (value) => JSON.stringify(value), serialize: (value) => JSON.stringify(value),
parseValue: (value: unknown) => JSON.parse(value as string) as Delta, parseValue: (value: unknown) => JSON.parse(value as string) as Delta,
parseLiteral: (ast: ValueNode) => ast as unknown as Delta, parseLiteral: (ast: ValueNode) => ast as unknown as Delta,
}) });
this.scalarType('JsonList', { this.scalarType("JsonList", {
serialize: (value) => JSON.stringify(value), serialize: (value) => JSON.stringify(value),
parseValue: (value: unknown) => JSON.parse(value as string) as JsonValue[], parseValue: (value: unknown) =>
JSON.parse(value as string) as JsonValue[],
parseLiteral: (ast: ValueNode) => ast as unknown as JsonValue[], parseLiteral: (ast: ValueNode) => ast as unknown as JsonValue[],
}) });
this.addScalarType('Json', JSONObjectResolver) this.addScalarType("Json", JSONObjectResolver);
this.addScalarType('Upload', GraphQLUpload) this.addScalarType("Upload", GraphQLUpload);
this.queryType({}) this.queryType({});
this.mutationType({}) this.mutationType({});
this.subscriptionType({}) this.subscriptionType({});
this.globalConnectionField('totalCount', (t) => this.globalConnectionField("totalCount", (t) =>
t.int({ t.int({
nullable: true, nullable: true,
resolve: (parent) => (typeof parent.totalCount === 'function' ? parent.totalCount() : parent.totalCount), resolve: (parent) =>
}), typeof parent.totalCount === "function"
) ? parent.totalCount()
: parent.totalCount,
})
);
} }
} }
export type BuilderTypes = PothosSchemaTypes.ExtendDefaultTypes<SchemaBuilderOption> export type BuilderTypes =
PothosSchemaTypes.ExtendDefaultTypes<SchemaBuilderOption>;

View File

@@ -1,53 +1,53 @@
import { Global, Logger, Module } from '@nestjs/common' import { Global, Logger, Module } from "@nestjs/common";
import { ApolloServerPluginLandingPageLocalDefault } from '@apollo/server/plugin/landingPage/default' import { ApolloServerPluginLandingPageLocalDefault } from "@apollo/server/plugin/landingPage/default";
import { ApolloDriverConfig } from '@nestjs/apollo' import { ApolloDriverConfig } from "@nestjs/apollo";
import { ConfigModule } from '@nestjs/config' import { ConfigModule } from "@nestjs/config";
import { GraphQLModule } from '@nestjs/graphql' import { GraphQLModule } from "@nestjs/graphql";
import { initContextCache } from '@pothos/core' import { initContextCache } from "@pothos/core";
import { PothosModule } from '@smatch-corp/nestjs-pothos' import { PothosModule } from "@smatch-corp/nestjs-pothos";
import { PothosApolloDriver } from '@smatch-corp/nestjs-pothos-apollo-driver' import { PothosApolloDriver } from "@smatch-corp/nestjs-pothos-apollo-driver";
import { Request } from 'express' import { Request } from "express";
import { RedisPubSub } from 'graphql-redis-subscriptions' import { RedisPubSub } from "graphql-redis-subscriptions";
import { CloseCode, Context, WebSocket } from 'graphql-ws' import { CloseCode, Context, WebSocket } from "graphql-ws";
import { PersonalMilestoneModule } from 'src/PersonalMilestone/personalmilestone.module' import { PersonalMilestoneModule } from "src/PersonalMilestone/personalmilestone.module";
import { AdminNoteModule } from '../AdminNote/adminnote.module' import { AdminNoteModule } from "../AdminNote/adminnote.module";
import { AnalyticModule } from '../Analytic/analytic.module' import { AnalyticModule } from "../Analytic/analytic.module";
import { AppConfigModule } from '../AppConfig/appconfig.module' import { AppConfigModule } from "../AppConfig/appconfig.module";
import { CategoryModule } from '../Category/category.module' import { CategoryModule } from "../Category/category.module";
import { CenterModule } from '../Center/center.module' import { CenterModule } from "../Center/center.module";
import { CenterMentorModule } from '../CenterMentor/centermentor.module' import { CenterMentorModule } from "../CenterMentor/centermentor.module";
import { ChatroomModule } from '../ChatRoom/chatroom.module' import { ChatroomModule } from "../ChatRoom/chatroom.module";
import { CollaborationSessionModule } from '../CollaborationSession/collaborationsession.module' import { CollaborationSessionModule } from "../CollaborationSession/collaborationsession.module";
import { DocumentModule } from '../Document/document.module' import { DocumentModule } from "../Document/document.module";
import { ManagedServiceModule } from '../ManagedService/managedservice.module' import { ManagedServiceModule } from "../ManagedService/managedservice.module";
import { MeetingRoomModule } from '../MeetingRoom/meetingroom.module' import { MeetingRoomModule } from "../MeetingRoom/meetingroom.module";
import { MessageModule } from '../Message/message.module' import { MessageModule } from "../Message/message.module";
import { OrderModule } from '../Order/order.module' import { OrderModule } from "../Order/order.module";
import { PaymentModule } from '../Payment/payment.module' import { PaymentModule } from "../Payment/payment.module";
import { PrismaModule } from '../Prisma/prisma.module' import { PrismaModule } from "../Prisma/prisma.module";
import { PrismaService } from '../Prisma/prisma.service' import { PrismaService } from "../Prisma/prisma.service";
import { PubSubModule } from '../PubSub/pubsub.module' import { PubSubModule } from "../PubSub/pubsub.module";
import { PubSubService } from '../PubSub/pubsub.service' import { PubSubService } from "../PubSub/pubsub.service";
import { QuizModule } from '../Quiz/quiz.module' import { QuizModule } from "../Quiz/quiz.module";
import { RedisModule } from '../Redis/redis.module' import { RedisModule } from "../Redis/redis.module";
import { RedisService } from '../Redis/redis.service' import { RedisService } from "../Redis/redis.service";
import { RefundTicketModule } from '../RefundTicket/refundticket.module' import { RefundTicketModule } from "../RefundTicket/refundticket.module";
import { ResumeModule } from '../Resume/resume.module' import { ResumeModule } from "../Resume/resume.module";
import { ScheduleModule } from '../Schedule/schedule.module' import { ScheduleModule } from "../Schedule/schedule.module";
import { ServiceModule } from '../Service/service.module' import { ServiceModule } from "../Service/service.module";
import { ServiceAndCategoryModule } from '../ServiceAndCategory/serviceandcategory.module' import { ServiceAndCategoryModule } from "../ServiceAndCategory/serviceandcategory.module";
import { ServiceFeedbackModule } from '../ServiceFeedback/servicefeedback.module' import { ServiceFeedbackModule } from "../ServiceFeedback/servicefeedback.module";
import { UploadedFileModule } from '../UploadedFile/uploadedfile.module' import { UploadedFileModule } from "../UploadedFile/uploadedfile.module";
import { UserModule } from '../User/user.module' import { UserModule } from "../User/user.module";
import { WorkshopModule } from '../Workshop/workshop.module' import { WorkshopModule } from "../Workshop/workshop.module";
import { WorkshopMeetingRoomModule } from '../WorkshopMeetingRoom/workshopmeetingroom.module' import { WorkshopMeetingRoomModule } from "../WorkshopMeetingRoom/workshopmeetingroom.module";
import { WorkshopOrganizationModule } from '../WorkshopOrganization/workshoporganization.module' import { WorkshopOrganizationModule } from "../WorkshopOrganization/workshoporganization.module";
import { WorkshopSubscriptionModule } from '../WorkshopSubscription/workshopsubscription.module' import { WorkshopSubscriptionModule } from "../WorkshopSubscription/workshopsubscription.module";
import { CommonModule } from '../common/common.module' import { CommonModule } from "../common/common.module";
import { Builder } from './graphql.builder' import { Builder } from "./graphql.builder";
import { PrismaCrudGenerator } from './graphql.generator' import { PrismaCrudGenerator } from "./graphql.generator";
import { GraphqlService } from './graphql.service' import { GraphqlService } from "./graphql.service";
@Global() @Global()
@Module({ @Module({
imports: [ imports: [
@@ -94,36 +94,48 @@ import { GraphqlService } from './graphql.service'
}), }),
GraphQLModule.forRootAsync<ApolloDriverConfig>({ GraphQLModule.forRootAsync<ApolloDriverConfig>({
driver: PothosApolloDriver, driver: PothosApolloDriver,
inject: [GraphqlService, 'PUB_SUB_REDIS'], inject: [GraphqlService, "PUB_SUB_REDIS"],
useFactory: async (graphqlService: GraphqlService, pubsub: RedisPubSub) => ({ useFactory: async (
path: process.env.API_PATH + '/graphql', graphqlService: GraphqlService,
pubsub: RedisPubSub
) => ({
path: process.env.API_PATH + "/graphql",
debug: true, debug: true,
playground: false, playground: false,
allowBatchedHttpRequests: true, allowBatchedHttpRequests: true,
includeStacktraceInErrorResponses: false, includeStacktraceInErrorResponses: false,
introspection: process.env.NODE_ENV === 'development' || false, introspection: process.env.NODE_ENV === "development" || false,
logger: { logger: {
debug: (...args) => Logger.debug(...args, 'GraphqlModule'), debug: (...args) => Logger.debug(...args, "GraphqlModule"),
info: (...args) => Logger.log(...args, 'GraphqlModule'), info: (...args) => Logger.log(...args, "GraphqlModule"),
warn: (...args) => Logger.warn(...args, 'GraphqlModule'), warn: (...args) => Logger.warn(...args, "GraphqlModule"),
error: (...args) => Logger.error(...args, 'GraphqlModule'), error: (...args) => Logger.error(...args, "GraphqlModule"),
}, },
plugins: [ApolloServerPluginLandingPageLocalDefault()], plugins: [ApolloServerPluginLandingPageLocalDefault()],
installSubscriptionHandlers: true, installSubscriptionHandlers: true,
subscriptions: { subscriptions: {
'graphql-ws': { "graphql-ws": {
onSubscribe(ctx, message) { onSubscribe(ctx, message) {
console.log('onSubscribe', ctx, message) console.log("onSubscribe", ctx, message);
}, },
onConnect: (ctx: Context<Record<string, unknown>>) => { onConnect: (ctx: Context<Record<string, unknown>>) => {
if (!ctx.connectionParams) { if (!ctx.connectionParams) {
Logger.log('No connectionParams provided', 'GraphqlModule') Logger.log("No connectionParams provided", "GraphqlModule");
}
if (!ctx.connectionParams?.["x-session-id"]) {
Logger.log("No sessionId provided", "GraphqlModule");
} }
if (!ctx.extra) { if (!ctx.extra) {
Logger.log('No extra provided', 'GraphqlModule') Logger.log("No extra provided", "GraphqlModule");
} }
// @ts-expect-error: Request is not typed const sessionId: string = ctx.connectionParams?.[
ctx.extra.request.headers['x-session-id'] = ctx.connectionParams['x-session-id'] "x-session-id"
] as string;
if (!sessionId) {
Logger.log("No sessionId provided", "GraphqlModule");
}
// @ts-expect-error: extra is not typed
ctx.extra.request.headers["x-session-id"] = sessionId;
}, },
}, },
}, },
@@ -132,37 +144,34 @@ import { GraphqlService } from './graphql.service'
subscriptions, subscriptions,
extra, extra,
}: { }: {
req?: Request req?: Request;
subscriptions?: Record<string, never> subscriptions?: Record<string, never>;
extra?: Record<string, never> extra?: Record<string, never>;
}) => { }) => {
initContextCache() initContextCache();
if (subscriptions) { if (subscriptions) {
// @ts-expect-error: TODO // @ts-expect-error: TODO
if (!extra?.request?.headers['x-session-id']) { if (!extra?.request?.headers["x-session-id"]) {
throw new Error('No sessionId provided') throw new Error("No sessionId provided");
} }
return { return {
isSubscription: true,
websocket: {
req: extra?.request, req: extra?.request,
pubSub: pubsub, pubSub: pubsub,
me: await graphqlService.acquireContextFromSessionId( me: await graphqlService.acquireContextFromSessionId(
// @ts-expect-error: TODO // @ts-expect-error: TODO
extra.request.headers['x-session-id'], extra.request.headers["x-session-id"]
), ),
}, };
}
} }
return { return {
isSubscription: false,
http: {
req, req,
me: req ? await graphqlService.acquireContext(req) : null, me: req ? await graphqlService.acquireContext(req) : null,
pubSub: pubsub, pubSub: pubsub,
invalidateCache: () => graphqlService.invalidateCache(req?.headers['x-session-id'] as string), invalidateCache: () =>
}, graphqlService.invalidateCache(
} req?.headers["x-session-id"] as string
),
};
}, },
}), }),
}), }),
@@ -171,8 +180,9 @@ import { GraphqlService } from './graphql.service'
RedisService, RedisService,
{ {
provide: GraphqlService, provide: GraphqlService,
useFactory: (prisma: PrismaService, redis: RedisService) => new GraphqlService(prisma, redis), useFactory: (prisma: PrismaService, redis: RedisService) =>
inject: [PrismaService, 'REDIS_CLIENT'], new GraphqlService(prisma, redis),
inject: [PrismaService, "REDIS_CLIENT"],
}, },
{ {
provide: Builder, provide: Builder,

View File

@@ -75,10 +75,7 @@ export class MeetingRoomSchema extends PothosSchema {
required: true, required: true,
}), }),
}, },
resolve: async (_query, _parent, args, ctx: SchemaContext) => { resolve: async (_query, _parent, args, _ctx: SchemaContext) => {
if (ctx.isSubscription) {
throw new Error('Not allowed')
}
const collaborationSession = await this.prisma.collaborationSession.findUnique({ const collaborationSession = await this.prisma.collaborationSession.findUnique({
where: { where: {
scheduleDateId: args.scheduleDateId, scheduleDateId: args.scheduleDateId,
@@ -112,10 +109,7 @@ export class MeetingRoomSchema extends PothosSchema {
}), }),
}, },
resolve: async (_, args, ctx: SchemaContext) => { resolve: async (_, args, ctx: SchemaContext) => {
if (ctx.isSubscription) { if (!ctx.me) {
throw new Error('Not allowed')
}
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({
@@ -131,11 +125,11 @@ export class MeetingRoomSchema extends PothosSchema {
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.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.me, meetingRoom.id)
return { return {
id: meetingRoom.id, id: meetingRoom.id,
token, token,
@@ -151,13 +145,10 @@ export class MeetingRoomSchema extends PothosSchema {
}), }),
}, },
resolve: async (_, args, ctx: SchemaContext) => { resolve: async (_, args, ctx: SchemaContext) => {
if (ctx.isSubscription) { if (!ctx.me) {
throw new Error('Not allowed')
}
if (!ctx.http.me) {
throw new Error('Unauthorized') throw new Error('Unauthorized')
} }
const token = await this.livekitService.createToken(ctx.http.me, args.scheduleId) const token = await this.livekitService.createToken(ctx.me, args.scheduleId)
return { return {
id: args.scheduleId, id: args.scheduleId,
token, token,
@@ -181,10 +172,7 @@ export class MeetingRoomSchema extends PothosSchema {
}), }),
}, },
resolve: async (query, _parent, args, ctx: SchemaContext) => { resolve: async (query, _parent, args, ctx: SchemaContext) => {
if (ctx.isSubscription) { if (!ctx.me) {
throw new Error('Not allowed')
}
if (!ctx.http.me) {
throw new Error('Unauthorized') throw new Error('Unauthorized')
} }
return await this.prisma.meetingRoom.create({ return await this.prisma.meetingRoom.create({
@@ -207,10 +195,7 @@ export class MeetingRoomSchema extends PothosSchema {
}), }),
}, },
resolve: async (query, _parent, args, ctx: SchemaContext) => { resolve: async (query, _parent, args, ctx: SchemaContext) => {
if (ctx.isSubscription) { if (!ctx.me) {
throw new Error('Not allowed')
}
if (!ctx.http.me) {
throw new Error('Unauthorized') throw new Error('Unauthorized')
} }
return await this.prisma.meetingRoom.update({ return await this.prisma.meetingRoom.update({

View File

@@ -20,7 +20,7 @@ import { DateTimeUtils } from "../common/utils/datetime.utils";
export class MessageSchema extends PothosSchema { export class MessageSchema extends PothosSchema {
constructor( constructor(
@Inject(SchemaBuilderToken) private readonly builder: Builder, @Inject(SchemaBuilderToken) private readonly builder: Builder,
private readonly prisma: PrismaService private readonly prisma: PrismaService,
) { ) {
super(); super();
} }
@@ -92,9 +92,6 @@ export class MessageSchema extends PothosSchema {
"Retrieve a list of messages with optional filtering, ordering, and pagination.", "Retrieve a list of messages with optional filtering, ordering, and pagination.",
args: this.builder.generator.findManyArgs("Message"), args: this.builder.generator.findManyArgs("Message"),
resolve: async (query, _root, args, ctx, _info) => { resolve: async (query, _root, args, ctx, _info) => {
if (ctx.isSubscription) {
throw new Error("Not allowed");
}
if (args.filter?.context && typeof args.filter.context === "object") { if (args.filter?.context && typeof args.filter.context === "object") {
// if args.context is NOTIFICATION or SYSTEM, filter by recipientId // if args.context is NOTIFICATION or SYSTEM, filter by recipientId
if ( if (
@@ -105,7 +102,7 @@ export class MessageSchema extends PothosSchema {
?.toString() ?.toString()
.includes(MessageContextType.SYSTEM) .includes(MessageContextType.SYSTEM)
) { ) {
args.filter.recipientId = ctx.http.me?.id; args.filter.recipientId = ctx.me?.id;
} }
} }
return await this.prisma.message.findMany({ return await this.prisma.message.findMany({
@@ -151,14 +148,11 @@ export class MessageSchema extends PothosSchema {
}), }),
}, },
resolve: async (query, _root, args, ctx, _info) => { resolve: async (query, _root, args, ctx, _info) => {
if (ctx.isSubscription) {
throw new Error("Not allowed");
}
const messageContext = MessageContextType.CHAT; const messageContext = MessageContextType.CHAT;
// get the sender from the context and add it to the input // get the sender from the context and add it to the input
args.input.sender = { args.input.sender = {
connect: { connect: {
id: ctx.http.me?.id, id: ctx.me?.id,
}, },
}; };
if (!args.input.sender) { if (!args.input.sender) {
@@ -206,13 +200,13 @@ export class MessageSchema extends PothosSchema {
}); });
return message; return message;
}); });
ctx.http.pubSub.publish( ctx.pubSub.publish(
`${PubSubEvent.MESSAGE_SENT}.${message.chatRoomId}`, `${PubSubEvent.MESSAGE_SENT}.${message.chatRoomId}`,
message message
); );
// publish to new message subscribers // publish to new message subscribers
userIds.forEach((userId: string) => { userIds.forEach((userId: string) => {
ctx.http.pubSub.publish( ctx.pubSub.publish(
`${PubSubEvent.NEW_MESSAGE}.${userId}`, `${PubSubEvent.NEW_MESSAGE}.${userId}`,
message message
); );
@@ -233,10 +227,7 @@ export class MessageSchema extends PothosSchema {
}), }),
}, },
subscribe: (_, args, ctx: SchemaContext) => { subscribe: (_, args, ctx: SchemaContext) => {
if (!ctx.isSubscription) { return ctx.pubSub.asyncIterator([
throw new Error("Not allowed");
}
return ctx.websocket.pubSub.asyncIterator([
`${PubSubEvent.MESSAGE_SENT}.${args.chatRoomId}`, `${PubSubEvent.MESSAGE_SENT}.${args.chatRoomId}`,
]) as unknown as AsyncIterable<Message>; ]) as unknown as AsyncIterable<Message>;
}, },

View File

@@ -154,10 +154,7 @@ export class OrderSchema extends PothosSchema {
description: 'Retrieve a list of completed orders', description: 'Retrieve a list of completed orders',
args: this.builder.generator.findManyArgs('Order'), args: this.builder.generator.findManyArgs('Order'),
resolve: async (query, _root, args, ctx, _info) => { resolve: async (query, _root, args, ctx, _info) => {
if (ctx.isSubscription) { if (!ctx.me) {
throw new Error('Orders cannot be retrieved in subscription context')
}
if (!ctx.http.me) {
throw new Error('Unauthorized') throw new Error('Unauthorized')
} }
// return orders where user is the one who made the order and status is PAID and schedule.dates is in the past // return orders where user is the one who made the order and status is PAID and schedule.dates is in the past
@@ -167,7 +164,7 @@ export class OrderSchema extends PothosSchema {
AND: [ AND: [
...(args.filter ? [args.filter] : []), ...(args.filter ? [args.filter] : []),
{ {
userId: ctx.http.me.id, userId: ctx.me.id,
status: OrderStatus.PAID, status: OrderStatus.PAID,
schedule: { schedule: {
OR: [ OR: [
@@ -202,14 +199,11 @@ export class OrderSchema extends PothosSchema {
description: 'Retrieve a list of completed orders for moderator', description: 'Retrieve a list of completed orders for moderator',
args: this.builder.generator.findManyArgs('Order'), args: this.builder.generator.findManyArgs('Order'),
resolve: async (query, _root, args, ctx, _info) => { resolve: async (query, _root, args, ctx, _info) => {
if (ctx.isSubscription) { if (!ctx.me) {
throw new Error('Orders cannot be retrieved in subscription context')
}
if (!ctx.http.me) {
throw new Error('Unauthorized') throw new Error('Unauthorized')
} }
// only for role moderator // only for role moderator
if (ctx.http.me.role !== Role.MODERATOR) { if (ctx.me.role !== Role.MODERATOR) {
throw new Error('Unauthorized') throw new Error('Unauthorized')
} }
// return completed order list where schedule status is COMPLETED // return completed order list where schedule status is COMPLETED
@@ -239,13 +233,10 @@ export class OrderSchema extends PothosSchema {
}, },
description: 'Retrieve a list of completed orders details', description: 'Retrieve a list of completed orders details',
resolve: async (_query, args, ctx, _info) => { resolve: async (_query, args, ctx, _info) => {
if (ctx.isSubscription) { if (!ctx.me) {
throw new Error('Orders cannot be retrieved in subscription context')
}
if (!ctx.http.me) {
throw new Error('Unauthorized') throw new Error('Unauthorized')
} }
if (ctx.http.me.role !== Role.MODERATOR) { if (ctx.me.role !== Role.MODERATOR) {
throw new Error('Unauthorized') throw new Error('Unauthorized')
} }
// get order details // get order details
@@ -367,10 +358,7 @@ export class OrderSchema extends PothosSchema {
}), }),
}, },
resolve: async (query, _root, args, ctx, _info) => { resolve: async (query, _root, args, ctx, _info) => {
if (ctx.isSubscription) { if (!ctx.me) {
throw new Error('Subscription is not allowed')
}
if (!ctx.http.me) {
throw new Error('Unauthorized') throw new Error('Unauthorized')
} }
if (!args.data.service.connect?.id) { if (!args.data.service.connect?.id) {
@@ -388,7 +376,7 @@ export class OrderSchema extends PothosSchema {
where: { where: {
AND: [ AND: [
{ {
customerId: ctx.http.me?.id, customerId: ctx.me?.id,
}, },
{ {
managedService: { managedService: {
@@ -419,7 +407,7 @@ export class OrderSchema extends PothosSchema {
data: { data: {
status: OrderStatus.PENDING, status: OrderStatus.PENDING,
total: service.price, total: service.price,
userId: ctx.http.me?.id ?? '', userId: ctx.me?.id ?? '',
serviceId: service.id, serviceId: service.id,
scheduleId: args.data.schedule.connect?.id ?? '', scheduleId: args.data.schedule.connect?.id ?? '',
commission: service.commission ?? 0.0, commission: service.commission ?? 0.0,
@@ -460,8 +448,8 @@ export class OrderSchema extends PothosSchema {
orderCode: paymentCode, orderCode: paymentCode,
amount: service.price, amount: service.price,
description: _name, description: _name,
buyerName: ctx.http.me?.name ?? '', buyerName: ctx.me?.name ?? '',
buyerEmail: ctx.http.me?.email ?? '', buyerEmail: ctx.me?.email ?? '',
returnUrl: `${process.env.PAYOS_RETURN_URL}`.replace('<serviceId>', service.id), returnUrl: `${process.env.PAYOS_RETURN_URL}`.replace('<serviceId>', service.id),
cancelUrl: `${process.env.PAYOS_RETURN_URL}`.replace('<serviceId>', service.id), cancelUrl: `${process.env.PAYOS_RETURN_URL}`.replace('<serviceId>', service.id),
expiredAt: DateTimeUtils.now().plus({ minutes: 15 }).toUnixInteger(), expiredAt: DateTimeUtils.now().plus({ minutes: 15 }).toUnixInteger(),
@@ -519,13 +507,10 @@ export class OrderSchema extends PothosSchema {
}), }),
}, },
resolve: async (query, _root, args, ctx, _info) => { resolve: async (query, _root, args, ctx, _info) => {
if (ctx.isSubscription) { if (!ctx.me) {
throw new Error('Subscription is not allowed')
}
if (!ctx.http.me) {
throw new Error('Unauthorized') throw new Error('Unauthorized')
} }
if (ctx.http.me.role !== Role.MODERATOR) { if (ctx.me.role !== Role.MODERATOR) {
throw new Error('Unauthorized') throw new Error('Unauthorized')
} }
return await this.prisma.order.update({ return await this.prisma.order.update({

View File

@@ -65,10 +65,7 @@ export class PersonalMilestoneSchema extends PothosSchema {
args: this.builder.generator.findUniqueArgs('PersonalMilestone'), args: this.builder.generator.findUniqueArgs('PersonalMilestone'),
description: 'Retrieve a single personal milestone by its unique identifier.', description: 'Retrieve a single personal milestone by its unique identifier.',
resolve: async (_query, _root, args, ctx, _info) => { resolve: async (_query, _root, args, ctx, _info) => {
if (ctx.isSubscription) { if (!ctx.me) {
throw new Error('Not allowed')
}
if (!ctx.http.me) {
throw new Error('Cannot get your info') throw new Error('Cannot get your info')
} }
return this.prisma.personalMilestone.findUnique({ return this.prisma.personalMilestone.findUnique({
@@ -81,10 +78,7 @@ export class PersonalMilestoneSchema extends PothosSchema {
args: this.builder.generator.findManyArgs('PersonalMilestone'), args: this.builder.generator.findManyArgs('PersonalMilestone'),
description: 'Retrieve multiple personal milestones.', description: 'Retrieve multiple personal milestones.',
resolve: async (_query, _root, args, ctx, _info) => { resolve: async (_query, _root, args, ctx, _info) => {
if (ctx.isSubscription) { if (!ctx.me) {
throw new Error('Not allowed')
}
if (!ctx.http.me) {
throw new Error('Cannot get your info') throw new Error('Cannot get your info')
} }
return this.prisma.personalMilestone.findMany({ return this.prisma.personalMilestone.findMany({
@@ -119,13 +113,10 @@ export class PersonalMilestoneSchema extends PothosSchema {
}, },
description: 'Create multiple personal milestones.', description: 'Create multiple personal milestones.',
resolve: async (_query, _root, args, ctx, _info) => { resolve: async (_query, _root, args, ctx, _info) => {
if (ctx.isSubscription) { if (!ctx.me) {
throw new Error('Not allowed')
}
if (!ctx.http.me) {
throw new Error('Cannot get your info') throw new Error('Cannot get your info')
} }
const userId = ctx.http.me.id const userId = ctx.me.id
const result = await this.prisma.personalMilestone.createManyAndReturn({ const result = await this.prisma.personalMilestone.createManyAndReturn({
data: args.data.map((data) => ({ data: args.data.map((data) => ({
...data, ...data,
@@ -158,13 +149,10 @@ export class PersonalMilestoneSchema extends PothosSchema {
}, },
description: 'Update a personal milestone.', description: 'Update a personal milestone.',
resolve: async (_query, _root, args, ctx, _info) => { resolve: async (_query, _root, args, ctx, _info) => {
if (ctx.isSubscription) { if (!ctx.me) {
throw new Error('Not allowed')
}
if (!ctx.http.me) {
throw new Error('Cannot get your info') throw new Error('Cannot get your info')
} }
const userId = ctx.http.me.id const userId = ctx.me.id
return this.prisma.personalMilestone.update({ return this.prisma.personalMilestone.update({
where: { where: {
...args.where, ...args.where,
@@ -185,10 +173,7 @@ export class PersonalMilestoneSchema extends PothosSchema {
}, },
description: 'Delete a personal milestone.', description: 'Delete a personal milestone.',
resolve: async (_query, _root, args, ctx, _info) => { resolve: async (_query, _root, args, ctx, _info) => {
if (ctx.isSubscription) { if (!ctx.me) {
throw new Error('Not allowed')
}
if (!ctx.http.me) {
throw new Error('Cannot get your info') throw new Error('Cannot get your info')
} }
// check if the user is mentor of the schedule where the personal milestone belongs to // check if the user is mentor of the schedule where the personal milestone belongs to
@@ -202,7 +187,7 @@ export class PersonalMilestoneSchema extends PothosSchema {
const managedService = await this.prisma.managedService.findUnique({ const managedService = await this.prisma.managedService.findUnique({
where: { id: schedule.managedServiceId }, where: { id: schedule.managedServiceId },
}) })
if (managedService?.mentorId !== ctx.http.me.id) { if (managedService?.mentorId !== ctx.me.id) {
throw new Error('Unauthorized') throw new Error('Unauthorized')
} }
} }

View File

@@ -145,10 +145,7 @@ export class QuizSchema extends PothosSchema {
type: this.quiz(), type: this.quiz(),
args: this.builder.generator.findUniqueArgs('Quiz'), args: this.builder.generator.findUniqueArgs('Quiz'),
resolve: async (query, _root, args, ctx, _info) => { resolve: async (query, _root, args, ctx, _info) => {
if (ctx.isSubscription) { if (!ctx.me) {
throw new Error('Subscription is not allowed')
}
if (!ctx.http.me) {
throw new Error('Unauthorized') throw new Error('Unauthorized')
} }
return await this.prisma.quiz.findUnique({ ...query, where: { id: args.where.id } }) return await this.prisma.quiz.findUnique({ ...query, where: { id: args.where.id } })
@@ -167,17 +164,14 @@ export class QuizSchema extends PothosSchema {
}), }),
}, },
resolve: async (query, _root, args, ctx, _info) => { resolve: async (query, _root, args, ctx, _info) => {
if (ctx.isSubscription) { if (!ctx.me) {
throw new Error('Subscription is not allowed')
}
if (!ctx.http.me) {
throw new Error('Unauthorized') throw new Error('Unauthorized')
} }
// use case 1: customer // use case 1: customer
if (ctx.http.me.role === Role.CUSTOMER) { if (ctx.me.role === Role.CUSTOMER) {
// using pseudo random to get amount of quizzes based on userid as seed // using pseudo random to get amount of quizzes based on userid as seed
const random = getRandomWithSeed( const random = getRandomWithSeed(
parseInt(crypto.createHash('sha256').update(ctx.http.me.id).digest('hex'), 16), parseInt(crypto.createHash('sha256').update(ctx.me.id).digest('hex'), 16),
) )
if (!args.scheduleId) { if (!args.scheduleId) {
throw new Error('Schedule ID is required') throw new Error('Schedule ID is required')
@@ -192,7 +186,7 @@ export class QuizSchema extends PothosSchema {
if (!schedule) { if (!schedule) {
throw new Error('Schedule not found') throw new Error('Schedule not found')
} }
if (schedule.customerId !== ctx.http.me.id) { if (schedule.customerId !== ctx.me.id) {
throw new Error('Unauthorized') throw new Error('Unauthorized')
} }
// get centerMentorId from schedule // get centerMentorId from schedule
@@ -211,7 +205,7 @@ export class QuizSchema extends PothosSchema {
// check if user has already taken the quiz // check if user has already taken the quiz
const quizAttempt = await this.prisma.quizAttempt.findFirst({ const quizAttempt = await this.prisma.quizAttempt.findFirst({
where: { where: {
userId: ctx.http.me.id, userId: ctx.me.id,
quizId: quizzes[0]?.id, quizId: quizzes[0]?.id,
}, },
}) })
@@ -226,9 +220,9 @@ export class QuizSchema extends PothosSchema {
} }
// use case 2: center mentor or center owner // use case 2: center mentor or center owner
if (ctx.http.me.role === Role.CENTER_MENTOR || ctx.http.me.role === Role.CENTER_OWNER) { if (ctx.me.role === Role.CENTER_MENTOR || ctx.me.role === Role.CENTER_OWNER) {
const centerMentor = await this.prisma.centerMentor.findUnique({ const centerMentor = await this.prisma.centerMentor.findUnique({
where: { mentorId: ctx.http.me.id }, where: { mentorId: ctx.me.id },
}) })
if (!centerMentor) { if (!centerMentor) {
throw new Error('Center mentor not found') throw new Error('Center mentor not found')
@@ -252,10 +246,7 @@ export class QuizSchema extends PothosSchema {
}), }),
}, },
resolve: async (query, _root, args, ctx, _info) => { resolve: async (query, _root, args, ctx, _info) => {
if (ctx.isSubscription) { if (!ctx.me) {
throw new Error('Not allowed')
}
if (!ctx.http.me) {
throw new Error('Unauthorized') throw new Error('Unauthorized')
} }
if (!args.id) { if (!args.id) {
@@ -272,10 +263,10 @@ export class QuizSchema extends PothosSchema {
} }
// check if user is the owner of the quiz attempt // check if user is the owner of the quiz attempt
if ( if (
result.userId !== ctx.http.me.id && result.userId !== ctx.me.id &&
ctx.http.me.role !== Role.CENTER_OWNER && ctx.me.role !== Role.CENTER_OWNER &&
ctx.http.me.role !== Role.CENTER_MENTOR && ctx.me.role !== Role.CENTER_MENTOR &&
ctx.http.me.role !== Role.CUSTOMER ctx.me.role !== Role.CUSTOMER
) { ) {
throw new Error('Unauthorized') throw new Error('Unauthorized')
} }
@@ -299,16 +290,13 @@ export class QuizSchema extends PothosSchema {
}), }),
}, },
resolve: async (query, _root, args, ctx, _info) => { resolve: async (query, _root, args, ctx, _info) => {
if (ctx.isSubscription) { if (!ctx.me) {
throw new Error('Not allowed')
}
if (!ctx.http.me) {
throw new Error('Unauthorized') throw new Error('Unauthorized')
} }
// use case 1: center mentor or center owner // use case 1: center mentor or center owner
if (ctx.http.me.role === Role.CENTER_MENTOR || ctx.http.me.role === Role.CENTER_OWNER) { if (ctx.me.role === Role.CENTER_MENTOR || ctx.me.role === Role.CENTER_OWNER) {
const centerMentor = await this.prisma.centerMentor.findUnique({ const centerMentor = await this.prisma.centerMentor.findUnique({
where: { mentorId: ctx.http.me.id }, where: { mentorId: ctx.me.id },
}) })
if (!centerMentor) { if (!centerMentor) {
throw new Error('Center mentor not found') throw new Error('Center mentor not found')
@@ -331,11 +319,11 @@ export class QuizSchema extends PothosSchema {
}) })
} }
// use case 2: customer // use case 2: customer
if (ctx.http.me.role === Role.CUSTOMER) { if (ctx.me.role === Role.CUSTOMER) {
return await this.prisma.quizAttempt.findMany({ return await this.prisma.quizAttempt.findMany({
...query, ...query,
where: { where: {
userId: ctx.http.me.id, userId: ctx.me.id,
...(args.quizId ? [{ quizId: args.quizId }] : []), ...(args.quizId ? [{ quizId: args.quizId }] : []),
}, },
orderBy: { orderBy: {
@@ -356,10 +344,7 @@ export class QuizSchema extends PothosSchema {
}), }),
}, },
resolve: async (query, _root, args, ctx, _info) => { resolve: async (query, _root, args, ctx, _info) => {
if (ctx.isSubscription) { if (!ctx.me) {
throw new Error('Subscription is not allowed')
}
if (!ctx.http.me) {
throw new Error('Unauthorized') throw new Error('Unauthorized')
} }
if (!args.data) { if (!args.data) {
@@ -370,7 +355,7 @@ export class QuizSchema extends PothosSchema {
} }
args.data.centerMentor = { args.data.centerMentor = {
connect: { connect: {
mentorId: ctx.http.me.id, mentorId: ctx.me.id,
}, },
} }
return await this.prisma.quiz.create({ return await this.prisma.quiz.create({
@@ -394,10 +379,7 @@ export class QuizSchema extends PothosSchema {
}), }),
}, },
resolve: async (query, _root, args, ctx, _info) => { resolve: async (query, _root, args, ctx, _info) => {
if (ctx.isSubscription) { if (!ctx.me) {
throw new Error('Subscription is not allowed')
}
if (!ctx.http.me) {
throw new Error('Unauthorized') throw new Error('Unauthorized')
} }
return await this.prisma.quiz.update({ return await this.prisma.quiz.update({
@@ -422,10 +404,7 @@ export class QuizSchema extends PothosSchema {
}), }),
}, },
resolve: async (query, _root, args, ctx, _info) => { resolve: async (query, _root, args, ctx, _info) => {
if (ctx.isSubscription) { if (!ctx.me) {
throw new Error('Subscription is not allowed')
}
if (!ctx.http.me) {
throw new Error('Unauthorized') throw new Error('Unauthorized')
} }
if (!args.data) { if (!args.data) {
@@ -450,7 +429,7 @@ export class QuizSchema extends PothosSchema {
data: { data: {
...args.data, ...args.data,
quiz: { connect: { id: args.data.quiz.connect.id } }, quiz: { connect: { id: args.data.quiz.connect.id } },
user: { connect: { id: ctx.http.me.id } }, user: { connect: { id: ctx.me.id } },
}, },
}) })
// update schedule status to WAITING_INTERVIEW // update schedule status to WAITING_INTERVIEW

View File

@@ -91,10 +91,10 @@ export class RefundTicketSchema extends PothosSchema {
id: t.arg({ type: 'String', required: true }), id: t.arg({ type: 'String', required: true }),
}, },
resolve: async (query, _root, args, ctx, _info) => { resolve: async (query, _root, args, ctx, _info) => {
if (ctx.isSubscription) { if (!ctx.me) {
throw new Error('Subscription is not allowed') throw new Error('User not found')
} }
if (ctx.http.me?.role !== Role.MODERATOR) { if (ctx.me.role !== Role.MODERATOR) {
throw new Error('Only moderators can retrieve refund tickets') throw new Error('Only moderators can retrieve refund tickets')
} }
return await this.prisma.refundTicket.findUnique({ ...query, where: { id: args.id } }) return await this.prisma.refundTicket.findUnique({ ...query, where: { id: args.id } })
@@ -132,24 +132,21 @@ export class RefundTicketSchema extends PothosSchema {
}), }),
}, },
resolve: async (_query, _root, args, ctx, _info) => { resolve: async (_query, _root, args, ctx, _info) => {
if (ctx.isSubscription) { if (!ctx.me) {
throw new Error('Subscription is not allowed')
}
if (!ctx.http.me) {
throw new Error('Unauthorized') throw new Error('Unauthorized')
} }
// Check if the user is a customer or a center mentor // Check if the user is a customer or a center mentor
if ( if (
ctx.http.me?.role !== Role.CUSTOMER && ctx.me?.role !== Role.CUSTOMER &&
ctx.http.me?.role !== Role.CENTER_MENTOR && ctx.me?.role !== Role.CENTER_MENTOR &&
ctx.http.me?.role !== Role.CENTER_OWNER ctx.me?.role !== Role.CENTER_OWNER
) { ) {
throw new Error('Only customers and center mentors can request refund') throw new Error('Only customers and center mentors can request refund')
} }
// Check bank details for non-center mentors // Check bank details for non-center mentors
if (ctx.http.me?.role !== Role.CENTER_MENTOR) { if (ctx.me?.role !== Role.CENTER_MENTOR) {
if (!ctx.http.me?.bankBin || !ctx.http.me?.bankAccountNumber) { if (!ctx.me?.bankBin || !ctx.me?.bankAccountNumber) {
throw new Error('Bank bin and bank account number are required, please update your profile first') throw new Error('Bank bin and bank account number are required, please update your profile first')
} }
} }
@@ -188,7 +185,7 @@ export class RefundTicketSchema extends PothosSchema {
let refundAmount = 0 let refundAmount = 0
// Special handling for center mentors - full refund always allowed // Special handling for center mentors - full refund always allowed
if (ctx.http.me?.role === Role.CENTER_MENTOR) { if (ctx.me?.role === Role.CENTER_MENTOR) {
refundAmount = order.total refundAmount = order.total
} else { } else {
// Existing refund logic for customers // Existing refund logic for customers
@@ -204,12 +201,12 @@ export class RefundTicketSchema extends PothosSchema {
} }
// Prepare bank details // Prepare bank details
let bankBin = ctx.http.me?.bankBin let bankBin = ctx.me?.bankBin
let bankAccountNumber = ctx.http.me?.bankAccountNumber let bankAccountNumber = ctx.me?.bankAccountNumber
let bankName = '' let bankName = ''
// For center mentors, use a default or system bank account // For center mentors, use a default or system bank account
if (ctx.http.me?.role === Role.CENTER_MENTOR) { if (ctx.me?.role === Role.CENTER_MENTOR) {
// You might want to replace this with a specific system bank account // You might want to replace this with a specific system bank account
bankBin = 'SYSTEM_MENTOR_REFUND' bankBin = 'SYSTEM_MENTOR_REFUND'
bankAccountNumber = 'SYSTEM_MENTOR_ACCOUNT' bankAccountNumber = 'SYSTEM_MENTOR_ACCOUNT'
@@ -234,7 +231,7 @@ export class RefundTicketSchema extends PothosSchema {
bankBin: bankBin, bankBin: bankBin,
bankAccountNumber: bankAccountNumber, bankAccountNumber: bankAccountNumber,
bankName: bankName, bankName: bankName,
requesterId: ctx.http.me.id, requesterId: ctx.me.id,
}, },
}) })
// notify all Moderator // notify all Moderator
@@ -244,15 +241,15 @@ export class RefundTicketSchema extends PothosSchema {
for (const moderator of moderators) { for (const moderator of moderators) {
const message = await this.prisma.message.create({ const message = await this.prisma.message.create({
data: { data: {
senderId: ctx.http.me?.id ?? '', senderId: ctx.me.id,
recipientId: moderator.id, recipientId: moderator.id,
type: MessageType.TEXT, type: MessageType.TEXT,
content: `Có yêu cầu hoàn tiền mới từ ${ctx.http.me?.name}`, content: `Có yêu cầu hoàn tiền mới từ ${ctx.me.name}`,
sentAt: DateTimeUtils.nowAsJSDate(), sentAt: DateTimeUtils.nowAsJSDate(),
context: MessageContextType.NOTIFICATION, context: MessageContextType.NOTIFICATION,
}, },
}) })
ctx.http.pubSub.publish(`${PubSubEvent.NOTIFICATION}.${moderator.id}`, message) ctx.pubSub.publish(`${PubSubEvent.NOTIFICATION}.${moderator.id}`, message)
} }
return refundTicket return refundTicket
}, },
@@ -275,10 +272,10 @@ export class RefundTicketSchema extends PothosSchema {
}), }),
}, },
resolve: async (_query, _root, args, ctx, _info) => { resolve: async (_query, _root, args, ctx, _info) => {
if (ctx.isSubscription) { if (!ctx.me) {
throw new Error('Subscription is not allowed') throw new Error('User not found')
} }
if (ctx.http.me?.role !== Role.MODERATOR) { if (ctx.me.role !== Role.MODERATOR) {
throw new Error('Only moderators can process refund tickets') throw new Error('Only moderators can process refund tickets')
} }
// if action is REJECT, reason is required // if action is REJECT, reason is required
@@ -291,7 +288,7 @@ export class RefundTicketSchema extends PothosSchema {
data: { data: {
status: args.action === 'APPROVE' ? RefundTicketStatus.APPROVED : RefundTicketStatus.REJECTED, status: args.action === 'APPROVE' ? RefundTicketStatus.APPROVED : RefundTicketStatus.REJECTED,
rejectedReason: args.action === 'REJECT' ? args.reason : undefined, rejectedReason: args.action === 'REJECT' ? args.reason : undefined,
moderatorId: ctx.http.me.id, moderatorId: ctx.me.id,
}, },
include: { include: {
order: true, order: true,
@@ -332,7 +329,7 @@ export class RefundTicketSchema extends PothosSchema {
} }
const message = await this.prisma.message.create({ const message = await this.prisma.message.create({
data: { data: {
senderId: ctx.http.me.id, senderId: ctx.me.id,
recipientId: requester.id, recipientId: requester.id,
type: MessageType.TEXT, type: MessageType.TEXT,
content: `Yêu cầu hoàn tiền của bạn đã được ${args.action === 'APPROVE' ? 'chấp thuận' : 'từ chối'}`, content: `Yêu cầu hoàn tiền của bạn đã được ${args.action === 'APPROVE' ? 'chấp thuận' : 'từ chối'}`,
@@ -340,7 +337,7 @@ export class RefundTicketSchema extends PothosSchema {
context: MessageContextType.NOTIFICATION, context: MessageContextType.NOTIFICATION,
}, },
}) })
ctx.http.pubSub.publish(`${PubSubEvent.NOTIFICATION}.${requester.id}`, message) ctx.pubSub.publish(`${PubSubEvent.NOTIFICATION}.${requester.id}`, message)
return refundTicket return refundTicket
}, },
}), }),

View File

@@ -108,13 +108,13 @@ export class ResumeSchema extends PothosSchema {
}, },
resolve: async (query, _root, args, ctx, _info) => { resolve: async (query, _root, args, ctx, _info) => {
try { try {
if (ctx.isSubscription) { if (!ctx.me) {
throw new Error('Not allowed') throw new Error('User not found')
} }
const resumes = await this.prisma.resume.findMany({ const resumes = await this.prisma.resume.findMany({
...query, ...query,
where: { where: {
userId: ctx.http.me?.id ?? '', userId: ctx.me.id,
status: args.status ?? undefined, status: args.status ?? undefined,
}, },
}) })
@@ -205,10 +205,10 @@ export class ResumeSchema extends PothosSchema {
}), }),
}, },
resolve: async (_query, _root, args, ctx, _info) => { resolve: async (_query, _root, args, ctx, _info) => {
if (ctx.isSubscription) { if (!ctx.me) {
throw new Error('Not allowed') throw new Error('User not found')
} }
if (ctx.http.me?.role !== Role.CUSTOMER && ctx.http.me?.role !== Role.CENTER_OWNER) { if (ctx.me.role !== Role.CUSTOMER && ctx.me.role !== Role.CENTER_OWNER) {
throw new Error('Not allowed') throw new Error('Not allowed')
} }
const { resumeFile } = args const { resumeFile } = args
@@ -251,15 +251,15 @@ export class ResumeSchema extends PothosSchema {
for (const moderator of moderators) { for (const moderator of moderators) {
const message = await this.prisma.message.create({ const message = await this.prisma.message.create({
data: { data: {
senderId: ctx.http.me?.id ?? '', senderId: ctx.me?.id ?? '',
recipientId: moderator.id, recipientId: moderator.id,
type: MessageType.TEXT, type: MessageType.TEXT,
content: `Có yêu cầu hồ sơ mới từ ${ctx.http.me?.name}`, content: `Có yêu cầu hồ sơ mới từ ${ctx.me?.name}`,
sentAt: DateTimeUtils.nowAsJSDate(), sentAt: DateTimeUtils.nowAsJSDate(),
context: MessageContextType.NOTIFICATION, context: MessageContextType.NOTIFICATION,
}, },
}) })
ctx.http.pubSub.publish(`${PubSubEvent.NOTIFICATION}.${moderator.id}`, message) ctx.pubSub.publish(`${PubSubEvent.NOTIFICATION}.${moderator.id}`, message)
} }
return resume return resume
} }
@@ -278,15 +278,15 @@ export class ResumeSchema extends PothosSchema {
for (const moderator of moderators) { for (const moderator of moderators) {
const message = await this.prisma.message.create({ const message = await this.prisma.message.create({
data: { data: {
senderId: ctx.http.me?.id ?? '', senderId: ctx.me?.id ?? '',
recipientId: moderator.id, recipientId: moderator.id,
type: MessageType.TEXT, type: MessageType.TEXT,
content: `Có yêu cầu hồ sơ mới từ ${ctx.http.me?.name}`, content: `Có yêu cầu hồ sơ mới từ ${ctx.me?.name}`,
sentAt: DateTimeUtils.nowAsJSDate(), sentAt: DateTimeUtils.nowAsJSDate(),
context: MessageContextType.NOTIFICATION, context: MessageContextType.NOTIFICATION,
}, },
}) })
ctx.http.pubSub.publish(`${PubSubEvent.NOTIFICATION}.${moderator.id}`, message) ctx.pubSub.publish(`${PubSubEvent.NOTIFICATION}.${moderator.id}`, message)
} }
return resume return resume
}, },
@@ -310,10 +310,10 @@ export class ResumeSchema extends PothosSchema {
}), }),
}, },
resolve: async (_query, _root, args, ctx, _info) => { resolve: async (_query, _root, args, ctx, _info) => {
if (ctx.isSubscription) { if (!ctx.me) {
throw new Error('Not allowed') throw new Error('User not found')
} }
if (ctx.http.me?.role !== Role.MODERATOR) { if (ctx.me.role !== Role.MODERATOR) {
throw new Error('Not allowed') throw new Error('Not allowed')
} }
const { resumeId, status, adminNote } = args const { resumeId, status, adminNote } = args
@@ -348,7 +348,7 @@ export class ResumeSchema extends PothosSchema {
_adminNote = await tx.adminNote.create({ _adminNote = await tx.adminNote.create({
data: { data: {
content: adminNote, content: adminNote,
notedByUserId: ctx.http.me?.id ?? '', notedByUserId: ctx.me?.id ?? '',
resumeId, resumeId,
}, },
}) })

View File

@@ -225,10 +225,7 @@ export class ScheduleSchema extends PothosSchema {
description: 'Retrieve a single schedule by its unique identifier.', description: 'Retrieve a single schedule by its unique identifier.',
args: this.builder.generator.findUniqueArgs('Schedule'), args: this.builder.generator.findUniqueArgs('Schedule'),
resolve: async (query, _root, args, ctx, _info) => { resolve: async (query, _root, args, ctx, _info) => {
if (ctx.isSubscription) { if (!ctx.me) {
throw new Error('Cannot retrieve schedule in subscription')
}
if (!ctx.http?.me?.id) {
throw new Error('User not found') throw new Error('User not found')
} }
// only return schedule belong to center // only return schedule belong to center
@@ -236,7 +233,7 @@ export class ScheduleSchema extends PothosSchema {
const center = await this.prisma.center.findFirst({ const center = await this.prisma.center.findFirst({
where: { where: {
AND: [ AND: [
{ OR: [{ centerOwnerId: ctx.http.me.id }, { centerMentors: { some: { mentorId: ctx.http.me.id } } }] }, { OR: [{ centerOwnerId: ctx.me.id }, { centerMentors: { some: { mentorId: ctx.me.id } } }] },
{ centerStatus: CenterStatus.APPROVED }, { centerStatus: CenterStatus.APPROVED },
], ],
}, },
@@ -260,14 +257,11 @@ export class ScheduleSchema extends PothosSchema {
args: this.builder.generator.findManyArgs('Schedule'), args: this.builder.generator.findManyArgs('Schedule'),
description: 'Retrieve a list of schedules with optional filtering, ordering, and pagination.', description: 'Retrieve a list of schedules with optional filtering, ordering, and pagination.',
resolve: async (query, _root, args, ctx, _info) => { resolve: async (query, _root, args, ctx, _info) => {
if (ctx.isSubscription) { if (!ctx.me) {
throw new Error('Cannot retrieve schedules in subscription')
}
if (!ctx.http?.me?.id) {
throw new Error('User not found') throw new Error('User not found')
} }
// use case 1: customer query schedules where customer is participant // use case 1: customer query schedules where customer is participant
if (ctx.http.me.role === Role.CUSTOMER) { if (ctx.me.role === Role.CUSTOMER) {
const schedules = await this.prisma.schedule.findMany({ const schedules = await this.prisma.schedule.findMany({
...query, ...query,
orderBy: args.orderBy ?? undefined, orderBy: args.orderBy ?? undefined,
@@ -278,12 +272,12 @@ export class ScheduleSchema extends PothosSchema {
return schedules return schedules
} }
// use case 2: center mentor or center owner query schedules where center mentor or center owner is mentor // use case 2: center mentor or center owner query schedules where center mentor or center owner is mentor
if (ctx.http.me.role === Role.CENTER_MENTOR) { if (ctx.me.role === Role.CENTER_MENTOR) {
const center = await this.prisma.center.findFirst({ const center = await this.prisma.center.findFirst({
where: { where: {
centerMentors: { centerMentors: {
some: { some: {
mentorId: ctx.http.me.id, mentorId: ctx.me.id,
}, },
}, },
}, },
@@ -299,7 +293,7 @@ export class ScheduleSchema extends PothosSchema {
orderBy: args.orderBy ?? undefined, orderBy: args.orderBy ?? undefined,
where: { where: {
AND: [ AND: [
{ managedService: { service: { centerId: center.id }, mentorId: ctx.http.me.id } }, { managedService: { service: { centerId: center.id }, mentorId: ctx.me.id } },
...(args.filter ? [args.filter] : []), ...(args.filter ? [args.filter] : []),
], ],
}, },
@@ -307,9 +301,9 @@ export class ScheduleSchema extends PothosSchema {
return schedules return schedules
} }
// use case 3: Center owner query all schedules belong to center // use case 3: Center owner query all schedules belong to center
if (ctx.http.me.role === Role.CENTER_OWNER) { if (ctx.me.role === Role.CENTER_OWNER) {
const center = await this.prisma.center.findFirst({ const center = await this.prisma.center.findFirst({
where: { centerOwnerId: ctx.http.me.id }, where: { centerOwnerId: ctx.me.id },
}) })
if (!center) { if (!center) {
throw new Error('Center not found') throw new Error('Center not found')
@@ -333,10 +327,7 @@ export class ScheduleSchema extends PothosSchema {
description: 'Retrieve a list of schedule dates.', description: 'Retrieve a list of schedule dates.',
args: this.builder.generator.findManyArgs('ScheduleDate'), args: this.builder.generator.findManyArgs('ScheduleDate'),
resolve: async (query, _root, args, ctx, _info) => { resolve: async (query, _root, args, ctx, _info) => {
if (ctx.isSubscription) { if (!ctx.me) {
throw new Error('Cannot retrieve schedule dates in subscription')
}
if (!ctx.http?.me?.id) {
throw new Error('User not found') throw new Error('User not found')
} }
return await this.prisma.scheduleDate.findMany({ return await this.prisma.scheduleDate.findMany({
@@ -345,7 +336,7 @@ export class ScheduleSchema extends PothosSchema {
take: args.take ?? undefined, take: args.take ?? undefined,
orderBy: args.orderBy ?? undefined, orderBy: args.orderBy ?? undefined,
where: { where: {
AND: [{ participantIds: { has: ctx.http.me.id } }, ...(args.filter ? [args.filter] : [])], AND: [{ participantIds: { has: ctx.me.id } }, ...(args.filter ? [args.filter] : [])],
}, },
}) })
}, },
@@ -404,8 +395,8 @@ d72a864e-2f41-45ab-9c9b-bf0512a31883,e9be51fd-2382-4e43-9988-74e76fde4b56,2024-1
}), }),
}, },
resolve: async (query, _root, args, ctx, _info) => { resolve: async (query, _root, args, ctx, _info) => {
if (ctx.isSubscription) { if (!ctx.me) {
throw new Error('Cannot create schedule in subscription') throw new Error('User not found')
} }
Logger.log('args.schedule', args.schedule) Logger.log('args.schedule', args.schedule)
// reject schedule if start date is today or in the past // reject schedule if start date is today or in the past

View File

@@ -118,16 +118,13 @@ export class ServiceSchema extends PothosSchema {
description: 'Whether the user has already provided feedback for the service.', description: 'Whether the user has already provided feedback for the service.',
nullable: true, nullable: true,
resolve: async (service, _args, ctx) => { resolve: async (service, _args, ctx) => {
if (ctx.isSubscription) { if (!ctx.me) {
return null
}
if (!ctx.http.me) {
return false return false
} }
const serviceFeedbacks = await this.prisma.serviceFeedback.findMany({ const serviceFeedbacks = await this.prisma.serviceFeedback.findMany({
where: { where: {
serviceId: service.id, serviceId: service.id,
userId: ctx.http.me.id, userId: ctx.me.id,
}, },
}) })
return serviceFeedbacks.length > 0 return serviceFeedbacks.length > 0
@@ -164,18 +161,15 @@ export class ServiceSchema extends PothosSchema {
args: this.builder.generator.findManyArgs('Service'), args: this.builder.generator.findManyArgs('Service'),
resolve: async (query, _root, args, ctx, _info) => { resolve: async (query, _root, args, ctx, _info) => {
if (ctx.isSubscription) {
throw new Error('Not allowed')
}
// check role if user is mentor or center owner // check role if user is mentor or center owner
const role = ctx.http.me?.role const role = ctx.me?.role
if (role !== Role.CENTER_MENTOR && role !== Role.CENTER_OWNER) { if (role !== Role.CENTER_MENTOR && role !== Role.CENTER_OWNER) {
throw new Error('Not allowed') throw new Error('User not found')
} }
if (role === Role.CENTER_MENTOR) { if (role === Role.CENTER_MENTOR) {
// load only service belong to center of current user // load only service belong to center of current user
const managedServices = await this.prisma.managedService.findMany({ const managedServices = await this.prisma.managedService.findMany({
where: { mentorId: ctx.http.me?.id ?? '' }, where: { mentorId: ctx.me.id },
}) })
if (!managedServices) { if (!managedServices) {
throw new Error('Managed services not found') throw new Error('Managed services not found')
@@ -188,7 +182,7 @@ export class ServiceSchema extends PothosSchema {
// if role is center owner, load all services belong to center of current user // if role is center owner, load all services belong to center of current user
if (role === Role.CENTER_OWNER) { if (role === Role.CENTER_OWNER) {
const center = await this.prisma.center.findUnique({ const center = await this.prisma.center.findUnique({
where: { centerOwnerId: ctx.http.me?.id ?? '' }, where: { centerOwnerId: ctx.me.id },
}) })
if (!center) { if (!center) {
throw new Error('Center not found') throw new Error('Center not found')
@@ -239,11 +233,11 @@ export class ServiceSchema extends PothosSchema {
}), }),
}, },
resolve: async (query, _root, args, ctx, _info) => { resolve: async (query, _root, args, ctx, _info) => {
if (ctx.isSubscription) { if (!ctx.me) {
throw new Error('Not allowed') throw new Error('User not found')
} }
// replace userId with current user id // replace userId with current user id
args.input.user = { connect: { id: ctx.http.me?.id ?? '' } } args.input.user = { connect: { id: ctx.me.id } }
const service = await this.prisma.service.create({ const service = await this.prisma.service.create({
...query, ...query,
data: args.input, data: args.input,
@@ -279,7 +273,7 @@ export class ServiceSchema extends PothosSchema {
}) })
const messages = await this.prisma.message.createManyAndReturn({ const messages = await this.prisma.message.createManyAndReturn({
data: moderatorIds.map((moderator) => ({ data: moderatorIds.map((moderator) => ({
senderId: ctx.http.me?.id ?? '', senderId: ctx.me.id,
recipientId: moderator.id, recipientId: moderator.id,
type: MessageType.TEXT, type: MessageType.TEXT,
content: `Có một dịch vụ mới với tên ${service.name} được đăng tải bởi ${center.name}`, content: `Có một dịch vụ mới với tên ${service.name} được đăng tải bởi ${center.name}`,
@@ -288,7 +282,7 @@ export class ServiceSchema extends PothosSchema {
})), })),
}) })
messages.forEach((message) => { messages.forEach((message) => {
ctx.http.pubSub.publish(`${PubSubEvent.NOTIFICATION}.${message.recipientId}`, message) ctx.pubSub.publish(`${PubSubEvent.NOTIFICATION}.${message.recipientId}`, message)
}) })
return service return service
}, },
@@ -355,8 +349,8 @@ export class ServiceSchema extends PothosSchema {
}), }),
}, },
resolve: async (query, _root, args, ctx, _info) => { resolve: async (query, _root, args, ctx, _info) => {
if (ctx.isSubscription) { if (!ctx.me) {
throw new Error('Not allowed') throw new Error('User not found')
} }
return await this.prisma.$transaction(async (prisma) => { return await this.prisma.$transaction(async (prisma) => {
// check if service is already approved or rejected // check if service is already approved or rejected
@@ -382,7 +376,7 @@ export class ServiceSchema extends PothosSchema {
adminNote: { adminNote: {
create: { create: {
content: args.adminNote ?? '', content: args.adminNote ?? '',
notedByUserId: ctx.http.me?.id ?? '', notedByUserId: ctx.me.id,
}, },
}, },
commission: commission ?? 0, commission: commission ?? 0,
@@ -427,7 +421,7 @@ export class ServiceSchema extends PothosSchema {
// add message to database // add message to database
const message = await this.prisma.message.create({ const message = await this.prisma.message.create({
data: { data: {
senderId: ctx.http.me?.id ?? '', senderId: ctx.me.id,
recipientId: id, recipientId: id,
type: MessageType.TEXT, type: MessageType.TEXT,
content: `Dịch vụ ${service.name} của bạn đã được chấp thuận`, content: `Dịch vụ ${service.name} của bạn đã được chấp thuận`,
@@ -438,7 +432,7 @@ export class ServiceSchema extends PothosSchema {
}, },
}, },
}) })
ctx.http.pubSub.publish(`${PubSubEvent.NOTIFICATION}.${id}`, message) ctx.pubSub.publish(`${PubSubEvent.NOTIFICATION}.${id}`, message)
}) })
} else { } else {
await this.mailService.sendTemplateEmail(emails, 'Thông báo về trạng thái dịch vụ', 'ServiceRejected', { await this.mailService.sendTemplateEmail(emails, 'Thông báo về trạng thái dịch vụ', 'ServiceRejected', {
@@ -455,7 +449,7 @@ export class ServiceSchema extends PothosSchema {
// add message to database // add message to database
const message = await this.prisma.message.create({ const message = await this.prisma.message.create({
data: { data: {
senderId: ctx.http.me?.id ?? '', senderId: ctx.me.id,
recipientId: id, recipientId: id,
type: MessageType.TEXT, type: MessageType.TEXT,
content: `Dịch vụ ${service.name} của bạn đã bị từ chối`, content: `Dịch vụ ${service.name} của bạn đã bị từ chối`,
@@ -466,7 +460,7 @@ export class ServiceSchema extends PothosSchema {
}, },
}, },
}) })
ctx.http.pubSub.publish(`${PubSubEvent.NOTIFICATION}.${id}`, message) ctx.pubSub.publish(`${PubSubEvent.NOTIFICATION}.${id}`, message)
}) })
} }
return updatedService return updatedService

View File

@@ -90,21 +90,18 @@ export class ServiceFeedbackSchema extends PothosSchema {
}, },
description: 'Create a new service feedback.', description: 'Create a new service feedback.',
resolve: async (_, _root, args, ctx, _info) => { resolve: async (_, _root, args, ctx, _info) => {
if (ctx.isSubscription) { if (!ctx.me) {
throw new Error('Not allowed')
}
if (!ctx.http?.me) {
throw new Error('Unauthorized') throw new Error('Unauthorized')
} }
// allow only when user is CUSTOMER and order is completed // allow only when user is CUSTOMER and order is completed
if (ctx.http?.me?.role !== Role.CUSTOMER) { if (ctx.me.role !== Role.CUSTOMER) {
throw new Error('Unauthorized') throw new Error('Unauthorized')
} }
Logger.log(`args: ${JSON.stringify(args)}`) Logger.log(`args: ${JSON.stringify(args)}`)
const order = await this.prisma.order.findFirst({ const order = await this.prisma.order.findFirst({
where: { where: {
id: args.orderId, id: args.orderId,
userId: ctx.http?.me?.id, userId: ctx.me.id,
status: OrderStatus.PAID, status: OrderStatus.PAID,
schedule: { schedule: {
dates: { dates: {
@@ -118,7 +115,7 @@ export class ServiceFeedbackSchema extends PothosSchema {
if (!order) { if (!order) {
throw new Error('Order not found') throw new Error('Order not found')
} }
if (order.userId !== ctx.http?.me?.id) { if (order.userId !== ctx.me.id) {
throw new Error('Unauthorized') throw new Error('Unauthorized')
} }
if (order.status !== OrderStatus.PAID) { if (order.status !== OrderStatus.PAID) {
@@ -134,7 +131,7 @@ export class ServiceFeedbackSchema extends PothosSchema {
} }
return await this.prisma.serviceFeedback.create({ return await this.prisma.serviceFeedback.create({
data: { data: {
userId: ctx.http?.me?.id, userId: ctx.me.id,
serviceId: order.serviceId, serviceId: order.serviceId,
rating: args.rating, rating: args.rating,
comments: args.comments, comments: args.comments,

View File

@@ -1,10 +1,11 @@
import { Module } from '@nestjs/common' import { Module } from '@nestjs/common'
import { ChatroomModule } from '../ChatRoom/chatroom.module' import { ChatroomSchema } from 'src/ChatRoom/chatroom.schema'
import { MessageModule } from '../Message/message.module' import { MessageSchema } from 'src/Message/message.schema'
import { UserSchema } from './user.schema' import { UserSchema } from './user.schema'
@Module({ @Module({
imports: [MessageModule, ChatroomModule], providers: [UserSchema, ChatroomSchema, MessageSchema],
providers: [UserSchema],
exports: [UserSchema], exports: [UserSchema],
}) })
export class UserModule {} export class UserModule {}

View File

@@ -170,10 +170,10 @@ export class UserSchema extends PothosSchema {
description: 'Retrieve the current user in context.', description: 'Retrieve the current user in context.',
type: this.user(), type: this.user(),
resolve: async (_query, _root, _args, ctx) => { resolve: async (_query, _root, _args, ctx) => {
if (ctx.isSubscription) { if (!ctx.me) {
throw new Error('Not allowed') throw new Error('User not found')
} }
return ctx.http.me return ctx.me
}, },
}), }),
@@ -184,18 +184,13 @@ export class UserSchema extends PothosSchema {
take: t.arg({ type: 'Int', required: false }), take: t.arg({ type: 'Int', required: false }),
}, },
resolve: async (_parent, args, ctx) => { resolve: async (_parent, args, ctx) => {
if (ctx.isSubscription) { if (!ctx.me) {
throw new Error('Not allowed')
}
const me = ctx.http.me
if (!me) {
throw new Error('User not found') throw new Error('User not found')
} }
// get chat rooms that the user is a part of // get chat rooms that the user is a part of
const chatRooms = await this.prisma.chatRoom.findMany({ const chatRooms = await this.prisma.chatRoom.findMany({
where: { where: {
OR: [{ customerId: me.id }, { mentorId: me.id }], OR: [{ customerId: ctx.me.id }, { mentorId: ctx.me.id }],
}, },
orderBy: { orderBy: {
lastActivity: 'desc', lastActivity: 'desc',
@@ -357,10 +352,10 @@ export class UserSchema extends PothosSchema {
}), }),
}, },
resolve: async (_query, args, ctx, _info) => { resolve: async (_query, args, ctx, _info) => {
if (ctx.isSubscription) { if (!ctx.me) {
throw new Error('Not allowed') throw new Error('User not found')
} }
const id = ctx.http.me?.id const id = ctx.me.id
if (!id) { if (!id) {
throw new Error('User not found') throw new Error('User not found')
} }
@@ -420,7 +415,7 @@ export class UserSchema extends PothosSchema {
}) })
} }
// invalidate cache // invalidate cache
await ctx.http.invalidateCache() await ctx.invalidateCache()
return await this.prisma.user.findUniqueOrThrow({ return await this.prisma.user.findUniqueOrThrow({
where: { id: clerkUser.id }, where: { id: clerkUser.id },
}) })
@@ -434,12 +429,12 @@ export class UserSchema extends PothosSchema {
}, },
resolve: async (_parent, args, ctx) => { resolve: async (_parent, args, ctx) => {
// check context // check context
if (ctx.isSubscription) { if (!ctx.me) {
throw new Error('Not allowed') throw new Error('Not allowed')
} }
// check context is admin // check context is admin
if (ctx.http.me?.role !== Role.ADMIN) { if (ctx.me?.role !== Role.ADMIN) {
throw new Error(`Only admin can invite moderator`) throw new Error(`Only admin can invite moderator`)
} }
return this.prisma.$transaction(async (tx) => { return this.prisma.$transaction(async (tx) => {
@@ -477,11 +472,7 @@ export class UserSchema extends PothosSchema {
}), }),
}, },
resolve: async (_, args, ctx) => { resolve: async (_, args, ctx) => {
if (ctx.isSubscription) { if (!ctx.me) {
throw new Error('Not allowed')
}
const me = ctx.http.me
if (!me) {
throw new Error('User not found') throw new Error('User not found')
} }
// create message // create message
@@ -489,7 +480,7 @@ export class UserSchema extends PothosSchema {
data: { data: {
type: args.input.type, type: args.input.type,
content: args.input.content, content: args.input.content,
senderId: me.id, senderId: ctx.me.id,
recipientId: args.input.recipient?.connect?.id ?? null, recipientId: args.input.recipient?.connect?.id ?? null,
chatRoomId: args.input.chatRoom?.connect?.id ?? null, chatRoomId: args.input.chatRoom?.connect?.id ?? null,
sentAt: DateTimeUtils.nowAsJSDate(), sentAt: DateTimeUtils.nowAsJSDate(),
@@ -498,7 +489,7 @@ export class UserSchema extends PothosSchema {
}, },
}) })
// publish message // publish message
await ctx.http.pubSub.publish(`${PubSubEvent.NEW_MESSAGE}.${message.recipientId}`, message) await ctx.pubSub.publish(`${PubSubEvent.NEW_MESSAGE}.${message.recipientId}`, message)
return message return message
}, },
}), }),
@@ -508,13 +499,10 @@ export class UserSchema extends PothosSchema {
userId: t.arg({ type: 'String', required: true }), userId: t.arg({ type: 'String', required: true }),
}, },
resolve: async (_parent, args, ctx) => { resolve: async (_parent, args, ctx) => {
if (ctx.isSubscription) { if (ctx.me?.role !== Role.ADMIN && ctx.me?.role !== Role.MODERATOR) {
throw new Error('Not allowed')
}
if (ctx.http.me?.role !== Role.ADMIN && ctx.http.me?.role !== Role.MODERATOR) {
throw new Error(`Only admin or moderator can ban user`) throw new Error(`Only admin or moderator can ban user`)
} }
if (args.userId === ctx.http.me?.id) { if (args.userId === ctx.me?.id) {
throw new Error(`Cannot ban yourself`) throw new Error(`Cannot ban yourself`)
} }
// get banning user info // get banning user info
@@ -531,7 +519,7 @@ export class UserSchema extends PothosSchema {
// ban user from clerk // ban user from clerk
await clerkClient.users.banUser(args.userId) await clerkClient.users.banUser(args.userId)
// invalidate cache // invalidate cache
await ctx.http.invalidateCache() await ctx.invalidateCache()
// update user banned status // update user banned status
await this.prisma.user.update({ await this.prisma.user.update({
where: { id: args.userId }, where: { id: args.userId },
@@ -547,13 +535,13 @@ export class UserSchema extends PothosSchema {
userScopedMessage: t.field({ userScopedMessage: t.field({
type: this.messageSchema.message(), type: this.messageSchema.message(),
subscribe: async (_, _args, ctx) => { subscribe: async (_, _args, ctx) => {
if (!ctx.isSubscription) { if (!ctx.me) {
throw new Error('Not allowed') throw new Error('Not allowed')
} }
return ctx.websocket.pubSub.asyncIterator([ return ctx.pubSub.asyncIterator([
`${PubSubEvent.NEW_MESSAGE}.${ctx.websocket.me?.id}`, `${PubSubEvent.NEW_MESSAGE}.${ctx.me?.id}`,
`${PubSubEvent.NOTIFICATION}.${ctx.websocket.me?.id}`, `${PubSubEvent.NOTIFICATION}.${ctx.me?.id}`,
]) as unknown as AsyncIterable<Message> ]) as unknown as AsyncIterable<Message>
}, },
resolve: async (payload: Message) => payload, resolve: async (payload: Message) => payload,

View File

@@ -136,13 +136,10 @@ export class WorkshopSchema extends PothosSchema {
}, },
description: 'Create a new workshop.', description: 'Create a new workshop.',
resolve: async (query, _root, args, ctx, _info) => { resolve: async (query, _root, args, ctx, _info) => {
if (ctx.isSubscription) { if (!ctx.me) {
throw new Error('Workshops cannot be created in subscription context')
}
if (!ctx.http.me) {
throw new Error('User is not authenticated to create a workshop') throw new Error('User is not authenticated to create a workshop')
} }
if (ctx.http.me.role !== Role.CENTER_OWNER) { if (ctx.me.role !== Role.CENTER_OWNER) {
throw new Error('Only center owners can create workshops') throw new Error('Only center owners can create workshops')
} }
if (!args.input.service.connect) { if (!args.input.service.connect) {
@@ -186,7 +183,7 @@ export class WorkshopSchema extends PothosSchema {
for (const customer of customers) { for (const customer of customers) {
const message = await this.prisma.message.create({ const message = await this.prisma.message.create({
data: { data: {
senderId: ctx.http.me?.id ?? '', senderId: ctx.me?.id ?? '',
recipientId: customer.id, recipientId: customer.id,
type: MessageType.TEXT, type: MessageType.TEXT,
content: `Workshop ${workshop.title} đã được lên lịch do ${service.center.name} tổ chức. Nhanh tay đăng kí ngay!`, content: `Workshop ${workshop.title} đã được lên lịch do ${service.center.name} tổ chức. Nhanh tay đăng kí ngay!`,
@@ -194,7 +191,7 @@ export class WorkshopSchema extends PothosSchema {
context: MessageContextType.NOTIFICATION, context: MessageContextType.NOTIFICATION,
}, },
}) })
ctx.http.pubSub.publish(`${PubSubEvent.NOTIFICATION}.${customer.id}`, message) ctx.pubSub.publish(`${PubSubEvent.NOTIFICATION}.${customer.id}`, message)
} }
return workshop return workshop
}, },

View File

@@ -72,10 +72,7 @@ export class WorkshopMeetingRoomSchema extends PothosSchema {
}), }),
}, },
resolve: async (_, args, ctx) => { resolve: async (_, args, ctx) => {
if (ctx.isSubscription) { if (!ctx.me) {
throw new Error('Not allowed')
}
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({
@@ -96,7 +93,7 @@ export class WorkshopMeetingRoomSchema extends PothosSchema {
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.me, meetingRoom.id),
serverUrl, serverUrl,
chatRoomId: chatRoom?.id, chatRoomId: chatRoom?.id,
} }

View File

@@ -58,10 +58,7 @@ export class WorkshopSubscriptionSchema extends PothosSchema {
type: [this.workshopSubscription()], type: [this.workshopSubscription()],
args: this.builder.generator.findManyArgs('WorkshopSubscription'), args: this.builder.generator.findManyArgs('WorkshopSubscription'),
description: 'Retrieve a list of workshop subscriptions with optional filtering, ordering, and pagination.', description: 'Retrieve a list of workshop subscriptions with optional filtering, ordering, and pagination.',
resolve: async (query, _root, args, ctx) => { resolve: async (query, _root, args, _ctx) => {
if (ctx.isSubscription) {
throw new Error('Workshops cannot be retrieved in subscription context')
}
return await this.prisma.workshopSubscription.findMany({ return await this.prisma.workshopSubscription.findMany({
...query, ...query,
skip: args.skip ?? undefined, skip: args.skip ?? undefined,
@@ -75,16 +72,13 @@ export class WorkshopSubscriptionSchema extends PothosSchema {
type: [this.workshopSubscription()], type: [this.workshopSubscription()],
description: 'Retrieve a list of workshops that the current user is subscribed to.', description: 'Retrieve a list of workshops that the current user is subscribed to.',
resolve: async (query, _root, _args, ctx, _info) => { resolve: async (query, _root, _args, ctx, _info) => {
if (ctx.isSubscription) { if (!ctx.me) {
throw new Error('Workshops cannot be retrieved in subscription context')
}
if (!ctx.http.me) {
throw new Error('User is not authenticated') throw new Error('User is not authenticated')
} }
return await this.prisma.workshopSubscription.findMany({ return await this.prisma.workshopSubscription.findMany({
...query, ...query,
where: { where: {
userId: ctx.http.me.id, userId: ctx.me.id,
}, },
}) })
}, },
@@ -100,10 +94,7 @@ export class WorkshopSubscriptionSchema extends PothosSchema {
}), }),
}, },
resolve: async (_query, _root, args, ctx) => { resolve: async (_query, _root, args, ctx) => {
if (ctx.isSubscription) { const userId = ctx.me?.id
throw new Error('Not allowed in subscription')
}
const userId = ctx.http.me?.id
// retrieve the workshop // retrieve the workshop
const workshop = await this.prisma.workshop.findUnique({ const workshop = await this.prisma.workshop.findUnique({
where: { id: args.workshopId }, where: { id: args.workshopId },