diff --git a/epess-database b/epess-database index eb5e274..6d55331 160000 --- a/epess-database +++ b/epess-database @@ -1 +1 @@ -Subproject commit eb5e274bd060b01f2a5e56aa7b4893165113f341 +Subproject commit 6d55331650cf82b416bbd3a2616e3480b9731a28 diff --git a/package-lock.json b/package-lock.json index b6ad627..4e4d79f 100644 --- a/package-lock.json +++ b/package-lock.json @@ -50,6 +50,7 @@ "graphql-ws": "^5.16.0", "ioredis": "^5.4.1", "jsonwebtoken": "^9.0.2", + "luxon": "^3.5.0", "minio": "^8.0.1", "nestjs-minio": "^2.6.2", "nodemailer": "^6.9.15", @@ -78,6 +79,7 @@ "@types/express": "^4.17.17", "@types/jest": "^29.5.2", "@types/lodash": "^4.17.13", + "@types/luxon": "^3.4.2", "@types/node": "^20.3.1", "@types/nodemailer": "^6.4.16", "@types/passport-jwt": "^4.0.1", @@ -5548,6 +5550,13 @@ "integrity": "sha512-MqTGEo5bj5t157U6fA/BiDynNkn0YknVdh48CMPkTSpFTVmvao5UQmm7uEF6xBEo7qIMAlY/JSleYaE6VOdpaA==", "license": "MIT" }, + "node_modules/@types/luxon": { + "version": "3.4.2", + "resolved": "https://registry.npmjs.org/@types/luxon/-/luxon-3.4.2.tgz", + "integrity": "sha512-TifLZlFudklWlMBfhubvgqTXRzLDI5pCbGa4P8a3wPyUQSW+1xQ5eDsreP9DWHX3tjq1ke96uYG/nwundroWcA==", + "dev": true, + "license": "MIT" + }, "node_modules/@types/methods": { "version": "1.1.4", "resolved": "https://registry.npmjs.org/@types/methods/-/methods-1.1.4.tgz", @@ -12883,6 +12892,15 @@ "node": ">=12" } }, + "node_modules/luxon": { + "version": "3.5.0", + "resolved": "https://registry.npmjs.org/luxon/-/luxon-3.5.0.tgz", + "integrity": "sha512-rh+Zjr6DNfUYR3bPwJEnuwDdqMbxZW7LOQfUN4B54+Cl+0o5zaU9RJ6bcidfDtC1cWCZXQ+nvX8bf6bAji37QQ==", + "license": "MIT", + "engines": { + "node": ">=12" + } + }, "node_modules/magic-string": { "version": "0.30.8", "resolved": "https://registry.npmjs.org/magic-string/-/magic-string-0.30.8.tgz", diff --git a/package.json b/package.json index f921d3c..3d7fb02 100644 --- a/package.json +++ b/package.json @@ -72,6 +72,7 @@ "graphql-ws": "^5.16.0", "ioredis": "^5.4.1", "jsonwebtoken": "^9.0.2", + "luxon": "^3.5.0", "minio": "^8.0.1", "nestjs-minio": "^2.6.2", "nodemailer": "^6.9.15", @@ -100,6 +101,7 @@ "@types/express": "^4.17.17", "@types/jest": "^29.5.2", "@types/lodash": "^4.17.13", + "@types/luxon": "^3.4.2", "@types/node": "^20.3.1", "@types/nodemailer": "^6.4.16", "@types/passport-jwt": "^4.0.1", diff --git a/src/Clerk/clerk.service.ts b/src/Clerk/clerk.service.ts index e533076..dd2e8bc 100644 --- a/src/Clerk/clerk.service.ts +++ b/src/Clerk/clerk.service.ts @@ -3,6 +3,9 @@ import { Injectable, Logger } from '@nestjs/common' import { PrismaService } from '../Prisma/prisma.service' import { clerkClient } from '@clerk/express' +export interface ClerkResponse { + +} @Injectable() export class ClerkService { constructor(private readonly prisma: PrismaService) {} diff --git a/src/Graphql/graphql.builder.ts b/src/Graphql/graphql.builder.ts index 561ba9f..4589752 100644 --- a/src/Graphql/graphql.builder.ts +++ b/src/Graphql/graphql.builder.ts @@ -41,6 +41,7 @@ export type SchemaContext = res: Response me: User pubSub: PubSub + invalidateCache: () => Promise generator: PrismaCrudGenerator } } diff --git a/src/Graphql/graphql.module.ts b/src/Graphql/graphql.module.ts index 9e110a9..b01058b 100644 --- a/src/Graphql/graphql.module.ts +++ b/src/Graphql/graphql.module.ts @@ -95,7 +95,12 @@ import { initContextCache } from '@pothos/core' ...initContextCache(), isSubscription: false, http: { + req, me: await graphqlService.acquireContext(req), + invalidateCache: () => + graphqlService.invalidateCache( + req.headers['x-session-id'] as string, + ), }, }), }), diff --git a/src/Graphql/graphql.service.ts b/src/Graphql/graphql.service.ts index c1f8cde..df63a02 100644 --- a/src/Graphql/graphql.service.ts +++ b/src/Graphql/graphql.service.ts @@ -37,6 +37,7 @@ export class GraphqlService { // redis context cache const cachedUser = await this.redis.getUser(sessionId) if (cachedUser) { + Logger.log(`Cache hit for sessionId: ${sessionId}`) return cachedUser } // check if the token is valid @@ -53,4 +54,10 @@ export class GraphqlService { await this.redis.setUser(sessionId, user, session.expireAt) return user } + + async invalidateCache(sessionId: string) { + // invalidate redis cache for sessionId + await this.redis.del(sessionId) + Logger.log(`Invalidated cache for sessionId: ${sessionId}`) + } } diff --git a/src/Message/message.schema.ts b/src/Message/message.schema.ts index 49bf493..889a01c 100644 --- a/src/Message/message.schema.ts +++ b/src/Message/message.schema.ts @@ -32,7 +32,7 @@ export class MessageSchema extends PothosSchema { description: 'The ID of the chat room.', }), message: t.expose('message', { - // eslint-disable-next-line @typescript-eslint/no-explicit-any + // biome-ignore lint/suspicious/noExplicitAny: type: 'Json' as any, description: 'The message content.', }), diff --git a/src/Order/order.schema.ts b/src/Order/order.schema.ts index 9726185..7191d7f 100644 --- a/src/Order/order.schema.ts +++ b/src/Order/order.schema.ts @@ -81,7 +81,7 @@ export class OrderSchema extends PothosSchema { description: 'Retrieve a list of orders with optional filtering, ordering, and pagination.', args: this.builder.generator.findManyArgs('Order'), - resolve: async (query, root, args, ctx, info) => { + resolve: async (query, _root, args, _ctx, _info) => { return await this.prisma.order.findMany({ ...query, take: args.take ?? undefined, @@ -95,7 +95,7 @@ export class OrderSchema extends PothosSchema { type: this.order(), args: this.builder.generator.findUniqueArgs('Order'), description: 'Retrieve a single order by its unique identifier.', - resolve: async (query, root, args, ctx, info) => { + resolve: async (query, _root, args, _ctx, _info) => { return await this.prisma.order.findUnique({ ...query, where: args.where, @@ -124,7 +124,7 @@ export class OrderSchema extends PothosSchema { required: true, }), }, - resolve: async (query, root, args, ctx, info) => { + resolve: async (query, _root, args, _ctx, _info) => { return this.prisma.$transaction(async (prisma) => { const order = await prisma.order.create({ ...query, @@ -162,7 +162,7 @@ export class OrderSchema extends PothosSchema { required: true, }), }, - resolve: async (query, root, args, ctx, info) => { + resolve: async (query, _root, args, _ctx, _info) => { return await this.prisma.order.delete({ ...query, where: args.where, @@ -185,7 +185,7 @@ export class OrderSchema extends PothosSchema { required: true, }), }, - resolve: async (query, root, args, ctx, info) => { + resolve: async (query, _root, args, _ctx, _info) => { return await this.prisma.order.update({ ...query, data: args.data, diff --git a/src/Schedule/schedule.schema.ts b/src/Schedule/schedule.schema.ts index 0d7d41a..0c4403c 100644 --- a/src/Schedule/schedule.schema.ts +++ b/src/Schedule/schedule.schema.ts @@ -23,6 +23,16 @@ export type ScheduleConfigType = | null | undefined +export type ScheduleConfigTypeForCenter = + | { + startDate?: string | null | undefined + endDate?: string | null | undefined + slots?: number[] | null | undefined + days?: number[] | null | undefined + } + | null + | undefined + export type ScheduleSlotType = { slot: string start: string @@ -144,6 +154,18 @@ export class ScheduleSchema extends PothosSchema { }) } + @PothosRef() + scheduleConfigInputForCenter() { + return this.builder.inputType('ScheduleConfigInputForCenter', { + fields: (t) => ({ + startDate: t.string(), + endDate: t.string(), + slots: t.intList(), + days: t.intList(), + }), + }) + } + @Pothos() init(): void { this.builder.queryFields((t) => ({ @@ -175,6 +197,21 @@ export class ScheduleSchema extends PothosSchema { }, }), + // centerPreviewSchedule: t.field({ + // type: this.previewSchedule(), + // description: 'Preview a schedule for center mentor.', + // args: { + // scheduleConfig: t.arg({ + // type: this.scheduleConfigInputForCenter(), + // }), + // }, + // resolve: async (_parent, args, _context, _info) => { + // return await this.scheduleService.createSchedulePreviewForCenter( + // args.scheduleConfig, + // ) + // }, + // }), + adminPreviewSchedule: t.field({ type: this.previewSchedule(), description: 'Preview a schedule for admin.', diff --git a/src/Schedule/schedule.service.ts b/src/Schedule/schedule.service.ts index ade5ae6..1276ff9 100644 --- a/src/Schedule/schedule.service.ts +++ b/src/Schedule/schedule.service.ts @@ -7,11 +7,21 @@ import { AppConfigService } from 'src/AppConfig/appconfig.service' import { PreviewScheduleType, ScheduleConfigType, + ScheduleConfigTypeForCenter, ScheduleSlotType, } from './schedule.schema' import { Config } from '@prisma/client' +import { DateTime, Settings, Zone } from 'luxon' import * as _ from 'lodash' +Settings.defaultLocale = 'en-US' +Settings.defaultZone = 'utc' +// Settings.defaultWeekSettings = { +// firstDay: 2, +// minimalDays: 1, +// weekend: [6, 7], +// } + @Injectable() export class ScheduleService { constructor( @@ -38,6 +48,18 @@ export class ScheduleService { } } + // async createSchedulePreviewForCenter( + // scheduleConfig: ScheduleConfigTypeForCenter, + // ): Promise { + // const config: Config[] = await this.appConfigService.getVisibleConfigs() + // Logger.log(config) + // // process scheduleConfig input by filling with default values from config + // const scheduleConfigFilled = this.processScheduleConfig( + // scheduleConfig, + // config, + // ) + // } + generateSlots(scheduleConfigFilled: ScheduleConfigType): ScheduleSlotType[] { Logger.log(`Generating slots with config: ${scheduleConfigFilled}`) const slots: ScheduleSlotType[] = [] @@ -66,11 +88,11 @@ export class ScheduleService { !this.isOverLapping( startTime, endTime, - this.getSpecificDateWithTime( + DateTime.fromISO( // @ts-ignore scheduleConfigFilled?.midDayBreakTimeStart, ), - this.getSpecificDateWithTime( + DateTime.fromISO( // @ts-ignore scheduleConfigFilled?.midDayBreakTimeEnd, ), @@ -78,8 +100,8 @@ export class ScheduleService { ) { slots.push({ slot: i.toString(), - start: startTime.toISOString(), - end: endTime.toISOString(), + start: startTime.toString(), + end: endTime.toString(), }) } } @@ -87,14 +109,14 @@ export class ScheduleService { } isOverLapping( - startTime1: Date, - endTime1: Date, - startTime2: Date, - endTime2: Date, + startTime1: DateTime, + endTime1: DateTime, + startTime2: DateTime, + endTime2: DateTime, ) { return ( - Math.max(startTime1.getTime(), startTime2.getTime()) < - Math.min(endTime1.getTime(), endTime2.getTime()) + Math.max(startTime1.toMillis(), startTime2.toMillis()) < + Math.min(endTime1.toMillis(), endTime2.toMillis()) ) } @@ -104,10 +126,11 @@ export class ScheduleService { slotDuration: string, slotBreakDuration: string, ) { - const startDate = new Date(startTime) - const endDate = new Date(endTime) + const startDate = DateTime.fromISO(startTime) + const endDate = DateTime.fromISO(endTime) - const totalMinutes = (endDate.getTime() - startDate.getTime()) / (60 * 1000) + const totalMinutes = + (endDate.toMillis() - startDate.toMillis()) / (60 * 1000) const numberOfSlots = Math.floor( totalMinutes / (parseInt(slotDuration) + parseInt(slotBreakDuration)), ) @@ -120,13 +143,14 @@ export class ScheduleService { slotBreakDuration: string, slotStartTime: string, ) { - const startTime = new Date(slotStartTime) - startTime.setUTCMinutes( - startTime.getUTCMinutes() + - slotNumber * (parseInt(slotDuration) + parseInt(slotBreakDuration)), - ) - const endTime = new Date(startTime) - endTime.setUTCMinutes(endTime.getUTCMinutes() + parseInt(slotDuration)) + const durationInMinutes = parseInt(slotDuration); + const breakDurationInMinutes = parseInt(slotBreakDuration); + + const startTime = DateTime.fromISO(slotStartTime).plus({ + minutes: (slotNumber - 1) * (durationInMinutes + breakDurationInMinutes), + }); + + const endTime = startTime.plus({ minutes: durationInMinutes }) return { startTime, endTime } } @@ -169,26 +193,23 @@ export class ScheduleService { return _.camelCase(str.toLowerCase()) } - getTodayWithTime(date: Date) { - const today = new Date() - today.setUTCHours( - date.getUTCHours(), - date.getUTCMinutes(), - date.getUTCSeconds(), - 0, - ) + getTodayWithTime(date: DateTime) { + let today = DateTime.now() + today = today.set({ + hour: date.hour, + minute: date.minute, + second: date.second, + }) return today } - getSpecificDateWithTime(date: Date) { - const specificDate = new Date(date) - date = new Date(date) - specificDate.setUTCHours( - date.getUTCHours(), - date.getUTCMinutes(), - date.getUTCSeconds(), - 0, - ) + getSpecificDateWithTime(date: DateTime) { + let specificDate = DateTime.now() + specificDate = specificDate.set({ + hour: date.hour, + minute: date.minute, + second: date.second, + }) return specificDate } } diff --git a/src/ServiceMeetingRoom/servicemeetingroom.schema.ts b/src/ServiceMeetingRoom/servicemeetingroom.schema.ts index 891cee5..5ab0216 100644 --- a/src/ServiceMeetingRoom/servicemeetingroom.schema.ts +++ b/src/ServiceMeetingRoom/servicemeetingroom.schema.ts @@ -43,7 +43,7 @@ export class ServiceMeetingRoomSchema extends PothosSchema { args: this.builder.generator.findUniqueArgs('ServiceMeetingRoom'), description: 'Retrieve a single service meeting room by its unique identifier.', - resolve: async (query, root, args, ctx, info) => { + resolve: async (query, _root, args, _ctx, _info) => { return await this.prisma.serviceMeetingRoom.findUnique({ ...query, where: args.where, @@ -55,7 +55,7 @@ export class ServiceMeetingRoomSchema extends PothosSchema { args: this.builder.generator.findManyArgs('ServiceMeetingRoom'), description: 'Retrieve a list of service meeting rooms with optional filtering, ordering, and pagination.', - resolve: async (query, root, args, ctx, info) => { + resolve: async (query, _root, args, _ctx, _info) => { return await this.prisma.serviceMeetingRoom.findMany({ ...query, skip: args.skip ?? undefined, diff --git a/src/UploadedFile/uploadedfile.schema.ts b/src/UploadedFile/uploadedfile.schema.ts index e965e3b..bfee21a 100644 --- a/src/UploadedFile/uploadedfile.schema.ts +++ b/src/UploadedFile/uploadedfile.schema.ts @@ -73,7 +73,7 @@ export class UploadedFileSchema extends PothosSchema { 'Retrieve a single uploaded file by its unique identifier.', type: this.uploadedFile(), args: this.builder.generator.findUniqueArgs('UploadedFile'), - resolve: async (query, root, args) => { + resolve: async (query, _root, args) => { const file = await this.prisma.uploadedFile.findUnique({ ...query, where: args.where, @@ -94,7 +94,7 @@ export class UploadedFileSchema extends PothosSchema { 'Retrieve a list of uploaded files with optional filtering, ordering, and pagination.', type: [this.uploadedFile()], args: this.builder.generator.findManyArgs('UploadedFile'), - resolve: async (query, root, args) => { + resolve: async (query, _root, args) => { const files = await this.prisma.uploadedFile.findMany({ ...query, skip: args.skip ?? undefined, @@ -132,7 +132,7 @@ export class UploadedFileSchema extends PothosSchema { required: true, }), }, - resolve: async (query, root, args) => { + resolve: async (_query, _root, args) => { const user = await this.prisma.user.findUnique({ where: { id: args.userId, @@ -182,7 +182,7 @@ export class UploadedFileSchema extends PothosSchema { required: true, }), }, - resolve: async (query, root, args) => { + resolve: async (_query, _root, args) => { const user = await this.prisma.user.findUnique({ where: { id: args.userId, @@ -230,7 +230,7 @@ export class UploadedFileSchema extends PothosSchema { required: true, }), }, - resolve: async (query, root, args) => { + resolve: async (_query, _root, args) => { const file = await this.prisma.uploadedFile.findUnique({ where: { id: args.id, @@ -259,7 +259,7 @@ export class UploadedFileSchema extends PothosSchema { required: true, }), }, - resolve: async (query, root, args) => { + resolve: async (_query, _root, args) => { const files = await this.prisma.uploadedFile.findMany({ where: { id: { diff --git a/src/User/user.schema.ts b/src/User/user.schema.ts index 5f4835f..ab3cee2 100644 --- a/src/User/user.schema.ts +++ b/src/User/user.schema.ts @@ -214,6 +214,113 @@ export class UserSchema extends PothosSchema { }, }), + updateMe: t.field({ + type: this.user(), + description: 'Update the current user in context.', + args: { + input: t.arg({ + type: this.builder.generator.getUpdateInput('User', [ + 'id', + 'adminNote', + 'center', + 'customerChatRoom', + 'avatarUrl', + // 'bankAccountNumber', + // 'bankBin', + 'email', + 'name', + 'phoneNumber', + 'role', + 'createdAt', + 'updatedAt', + 'files', + 'orders', + 'sendingMessage', + 'mentor', + 'mentorChatRoom', + 'resume', + 'service', + 'serviceFeedbacks', + 'workshopSubscription', + ]), + required: false, + }), + imageBlob: t.arg({ + type: 'Upload', + required: false, + }), + firstName: t.arg({ + type: 'String', + required: false, + }), + lastName: t.arg({ + type: 'String', + required: false, + }), + }, + resolve: async (_query, args, ctx, _info) => { + if (ctx.isSubscription) { + throw new Error('Not allowed') + } + const id = ctx.http.me.id + if (args.imageBlob) { + const { mimetype, createReadStream } = await args.imageBlob + if (mimetype && createReadStream) { + const stream = createReadStream() + const chunks: Uint8Array[] = [] + + for await (const chunk of stream) { + chunks.push(chunk) + } + + const buffer = Buffer.concat(chunks) + await clerkClient.users.updateUserProfileImage(id, { + file: new Blob([buffer]), + }) + } + } + + // update info to clerk + const clerkUser = await clerkClient.users.updateUser(id, { + firstName: args.firstName as string, + lastName: args.lastName as string, + }) + Logger.log(clerkUser, 'Clerk User') + // update bank account number and bank bin to database + if (args.input?.bankAccountNumber) { + await this.prisma.user.update({ + where: { id: clerkUser.id }, + data: { + bankAccountNumber: args.input.bankAccountNumber, + }, + }) + } + + if (args.input?.bankBin) { + await this.prisma.user.update({ + where: { id: clerkUser.id }, + data: { + bankBin: args.input.bankBin, + }, + }) + } + + if (args.firstName || args.lastName) { + await this.prisma.user.update({ + where: { id: clerkUser.id }, + data: { + name: `${args.firstName || ''} ${args.lastName || ''}`.trim(), + }, + }) + } + // invalidate cache + await ctx.http.invalidateCache() + return await this.prisma.user.findUniqueOrThrow({ + where: { id: clerkUser.id }, + }) + }, + }), + inviteModerator: t.field({ type: 'String', args: {