fix time geneate logic and replace default datetime by luxon

This commit is contained in:
2024-11-01 17:27:25 +07:00
parent 24a49d9412
commit ec77f07de1
14 changed files with 253 additions and 52 deletions

18
package-lock.json generated
View File

@@ -50,6 +50,7 @@
"graphql-ws": "^5.16.0", "graphql-ws": "^5.16.0",
"ioredis": "^5.4.1", "ioredis": "^5.4.1",
"jsonwebtoken": "^9.0.2", "jsonwebtoken": "^9.0.2",
"luxon": "^3.5.0",
"minio": "^8.0.1", "minio": "^8.0.1",
"nestjs-minio": "^2.6.2", "nestjs-minio": "^2.6.2",
"nodemailer": "^6.9.15", "nodemailer": "^6.9.15",
@@ -78,6 +79,7 @@
"@types/express": "^4.17.17", "@types/express": "^4.17.17",
"@types/jest": "^29.5.2", "@types/jest": "^29.5.2",
"@types/lodash": "^4.17.13", "@types/lodash": "^4.17.13",
"@types/luxon": "^3.4.2",
"@types/node": "^20.3.1", "@types/node": "^20.3.1",
"@types/nodemailer": "^6.4.16", "@types/nodemailer": "^6.4.16",
"@types/passport-jwt": "^4.0.1", "@types/passport-jwt": "^4.0.1",
@@ -5548,6 +5550,13 @@
"integrity": "sha512-MqTGEo5bj5t157U6fA/BiDynNkn0YknVdh48CMPkTSpFTVmvao5UQmm7uEF6xBEo7qIMAlY/JSleYaE6VOdpaA==", "integrity": "sha512-MqTGEo5bj5t157U6fA/BiDynNkn0YknVdh48CMPkTSpFTVmvao5UQmm7uEF6xBEo7qIMAlY/JSleYaE6VOdpaA==",
"license": "MIT" "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": { "node_modules/@types/methods": {
"version": "1.1.4", "version": "1.1.4",
"resolved": "https://registry.npmjs.org/@types/methods/-/methods-1.1.4.tgz", "resolved": "https://registry.npmjs.org/@types/methods/-/methods-1.1.4.tgz",
@@ -12883,6 +12892,15 @@
"node": ">=12" "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": { "node_modules/magic-string": {
"version": "0.30.8", "version": "0.30.8",
"resolved": "https://registry.npmjs.org/magic-string/-/magic-string-0.30.8.tgz", "resolved": "https://registry.npmjs.org/magic-string/-/magic-string-0.30.8.tgz",

View File

@@ -72,6 +72,7 @@
"graphql-ws": "^5.16.0", "graphql-ws": "^5.16.0",
"ioredis": "^5.4.1", "ioredis": "^5.4.1",
"jsonwebtoken": "^9.0.2", "jsonwebtoken": "^9.0.2",
"luxon": "^3.5.0",
"minio": "^8.0.1", "minio": "^8.0.1",
"nestjs-minio": "^2.6.2", "nestjs-minio": "^2.6.2",
"nodemailer": "^6.9.15", "nodemailer": "^6.9.15",
@@ -100,6 +101,7 @@
"@types/express": "^4.17.17", "@types/express": "^4.17.17",
"@types/jest": "^29.5.2", "@types/jest": "^29.5.2",
"@types/lodash": "^4.17.13", "@types/lodash": "^4.17.13",
"@types/luxon": "^3.4.2",
"@types/node": "^20.3.1", "@types/node": "^20.3.1",
"@types/nodemailer": "^6.4.16", "@types/nodemailer": "^6.4.16",
"@types/passport-jwt": "^4.0.1", "@types/passport-jwt": "^4.0.1",

View File

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

View File

@@ -41,6 +41,7 @@ export type SchemaContext =
res: Response res: Response
me: User me: User
pubSub: PubSub pubSub: PubSub
invalidateCache: () => Promise<void>
generator: PrismaCrudGenerator<BuilderTypes> generator: PrismaCrudGenerator<BuilderTypes>
} }
} }

View File

@@ -95,7 +95,12 @@ import { initContextCache } from '@pothos/core'
...initContextCache(), ...initContextCache(),
isSubscription: false, isSubscription: false,
http: { http: {
req,
me: await graphqlService.acquireContext(req), me: await graphqlService.acquireContext(req),
invalidateCache: () =>
graphqlService.invalidateCache(
req.headers['x-session-id'] as string,
),
}, },
}), }),
}), }),

View File

@@ -37,6 +37,7 @@ export class GraphqlService {
// redis context cache // redis context cache
const cachedUser = await this.redis.getUser(sessionId) const cachedUser = await this.redis.getUser(sessionId)
if (cachedUser) { if (cachedUser) {
Logger.log(`Cache hit for sessionId: ${sessionId}`)
return cachedUser return cachedUser
} }
// check if the token is valid // check if the token is valid
@@ -53,4 +54,10 @@ export class GraphqlService {
await this.redis.setUser(sessionId, user, session.expireAt) await this.redis.setUser(sessionId, user, session.expireAt)
return user return user
} }
async invalidateCache(sessionId: string) {
// invalidate redis cache for sessionId
await this.redis.del(sessionId)
Logger.log(`Invalidated cache for sessionId: ${sessionId}`)
}
} }

View File

@@ -32,7 +32,7 @@ export class MessageSchema extends PothosSchema {
description: 'The ID of the chat room.', description: 'The ID of the chat room.',
}), }),
message: t.expose('message', { message: t.expose('message', {
// eslint-disable-next-line @typescript-eslint/no-explicit-any // biome-ignore lint/suspicious/noExplicitAny: <explanation>
type: 'Json' as any, type: 'Json' as any,
description: 'The message content.', description: 'The message content.',
}), }),

View File

@@ -81,7 +81,7 @@ export class OrderSchema extends PothosSchema {
description: description:
'Retrieve a list of orders with optional filtering, ordering, and pagination.', 'Retrieve a list of orders with optional filtering, ordering, and pagination.',
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) => {
return await this.prisma.order.findMany({ return await this.prisma.order.findMany({
...query, ...query,
take: args.take ?? undefined, take: args.take ?? undefined,
@@ -95,7 +95,7 @@ export class OrderSchema extends PothosSchema {
type: this.order(), type: this.order(),
args: this.builder.generator.findUniqueArgs('Order'), args: this.builder.generator.findUniqueArgs('Order'),
description: 'Retrieve a single order by its unique identifier.', 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({ return await this.prisma.order.findUnique({
...query, ...query,
where: args.where, where: args.where,
@@ -124,7 +124,7 @@ export class OrderSchema extends PothosSchema {
required: true, required: true,
}), }),
}, },
resolve: async (query, root, args, ctx, info) => { resolve: async (query, _root, args, _ctx, _info) => {
return this.prisma.$transaction(async (prisma) => { return this.prisma.$transaction(async (prisma) => {
const order = await prisma.order.create({ const order = await prisma.order.create({
...query, ...query,
@@ -162,7 +162,7 @@ export class OrderSchema extends PothosSchema {
required: true, required: true,
}), }),
}, },
resolve: async (query, root, args, ctx, info) => { resolve: async (query, _root, args, _ctx, _info) => {
return await this.prisma.order.delete({ return await this.prisma.order.delete({
...query, ...query,
where: args.where, where: args.where,
@@ -185,7 +185,7 @@ export class OrderSchema extends PothosSchema {
required: true, required: true,
}), }),
}, },
resolve: async (query, root, args, ctx, info) => { resolve: async (query, _root, args, _ctx, _info) => {
return await this.prisma.order.update({ return await this.prisma.order.update({
...query, ...query,
data: args.data, data: args.data,

View File

@@ -23,6 +23,16 @@ export type ScheduleConfigType =
| null | null
| undefined | 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 = { export type ScheduleSlotType = {
slot: string slot: string
start: 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() @Pothos()
init(): void { init(): void {
this.builder.queryFields((t) => ({ 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({ adminPreviewSchedule: t.field({
type: this.previewSchedule(), type: this.previewSchedule(),
description: 'Preview a schedule for admin.', description: 'Preview a schedule for admin.',

View File

@@ -7,11 +7,21 @@ import { AppConfigService } from 'src/AppConfig/appconfig.service'
import { import {
PreviewScheduleType, PreviewScheduleType,
ScheduleConfigType, ScheduleConfigType,
ScheduleConfigTypeForCenter,
ScheduleSlotType, ScheduleSlotType,
} from './schedule.schema' } from './schedule.schema'
import { Config } from '@prisma/client' import { Config } from '@prisma/client'
import { DateTime, Settings, Zone } from 'luxon'
import * as _ from 'lodash' import * as _ from 'lodash'
Settings.defaultLocale = 'en-US'
Settings.defaultZone = 'utc'
// Settings.defaultWeekSettings = {
// firstDay: 2,
// minimalDays: 1,
// weekend: [6, 7],
// }
@Injectable() @Injectable()
export class ScheduleService { export class ScheduleService {
constructor( constructor(
@@ -38,6 +48,18 @@ export class ScheduleService {
} }
} }
// async createSchedulePreviewForCenter(
// scheduleConfig: ScheduleConfigTypeForCenter,
// ): Promise<PreviewScheduleType> {
// 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[] { generateSlots(scheduleConfigFilled: ScheduleConfigType): ScheduleSlotType[] {
Logger.log(`Generating slots with config: ${scheduleConfigFilled}`) Logger.log(`Generating slots with config: ${scheduleConfigFilled}`)
const slots: ScheduleSlotType[] = [] const slots: ScheduleSlotType[] = []
@@ -66,11 +88,11 @@ export class ScheduleService {
!this.isOverLapping( !this.isOverLapping(
startTime, startTime,
endTime, endTime,
this.getSpecificDateWithTime( DateTime.fromISO(
// @ts-ignore // @ts-ignore
scheduleConfigFilled?.midDayBreakTimeStart, scheduleConfigFilled?.midDayBreakTimeStart,
), ),
this.getSpecificDateWithTime( DateTime.fromISO(
// @ts-ignore // @ts-ignore
scheduleConfigFilled?.midDayBreakTimeEnd, scheduleConfigFilled?.midDayBreakTimeEnd,
), ),
@@ -78,8 +100,8 @@ export class ScheduleService {
) { ) {
slots.push({ slots.push({
slot: i.toString(), slot: i.toString(),
start: startTime.toISOString(), start: startTime.toString(),
end: endTime.toISOString(), end: endTime.toString(),
}) })
} }
} }
@@ -87,14 +109,14 @@ export class ScheduleService {
} }
isOverLapping( isOverLapping(
startTime1: Date, startTime1: DateTime,
endTime1: Date, endTime1: DateTime,
startTime2: Date, startTime2: DateTime,
endTime2: Date, endTime2: DateTime,
) { ) {
return ( return (
Math.max(startTime1.getTime(), startTime2.getTime()) < Math.max(startTime1.toMillis(), startTime2.toMillis()) <
Math.min(endTime1.getTime(), endTime2.getTime()) Math.min(endTime1.toMillis(), endTime2.toMillis())
) )
} }
@@ -104,10 +126,11 @@ export class ScheduleService {
slotDuration: string, slotDuration: string,
slotBreakDuration: string, slotBreakDuration: string,
) { ) {
const startDate = new Date(startTime) const startDate = DateTime.fromISO(startTime)
const endDate = new Date(endTime) 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( const numberOfSlots = Math.floor(
totalMinutes / (parseInt(slotDuration) + parseInt(slotBreakDuration)), totalMinutes / (parseInt(slotDuration) + parseInt(slotBreakDuration)),
) )
@@ -120,13 +143,14 @@ export class ScheduleService {
slotBreakDuration: string, slotBreakDuration: string,
slotStartTime: string, slotStartTime: string,
) { ) {
const startTime = new Date(slotStartTime) const durationInMinutes = parseInt(slotDuration);
startTime.setUTCMinutes( const breakDurationInMinutes = parseInt(slotBreakDuration);
startTime.getUTCMinutes() +
slotNumber * (parseInt(slotDuration) + parseInt(slotBreakDuration)), const startTime = DateTime.fromISO(slotStartTime).plus({
) minutes: (slotNumber - 1) * (durationInMinutes + breakDurationInMinutes),
const endTime = new Date(startTime) });
endTime.setUTCMinutes(endTime.getUTCMinutes() + parseInt(slotDuration))
const endTime = startTime.plus({ minutes: durationInMinutes })
return { startTime, endTime } return { startTime, endTime }
} }
@@ -169,26 +193,23 @@ export class ScheduleService {
return _.camelCase(str.toLowerCase()) return _.camelCase(str.toLowerCase())
} }
getTodayWithTime(date: Date) { getTodayWithTime(date: DateTime) {
const today = new Date() let today = DateTime.now()
today.setUTCHours( today = today.set({
date.getUTCHours(), hour: date.hour,
date.getUTCMinutes(), minute: date.minute,
date.getUTCSeconds(), second: date.second,
0, })
)
return today return today
} }
getSpecificDateWithTime(date: Date) { getSpecificDateWithTime(date: DateTime) {
const specificDate = new Date(date) let specificDate = DateTime.now()
date = new Date(date) specificDate = specificDate.set({
specificDate.setUTCHours( hour: date.hour,
date.getUTCHours(), minute: date.minute,
date.getUTCMinutes(), second: date.second,
date.getUTCSeconds(), })
0,
)
return specificDate return specificDate
} }
} }

View File

@@ -43,7 +43,7 @@ export class ServiceMeetingRoomSchema extends PothosSchema {
args: this.builder.generator.findUniqueArgs('ServiceMeetingRoom'), args: this.builder.generator.findUniqueArgs('ServiceMeetingRoom'),
description: description:
'Retrieve a single service meeting room by its unique identifier.', '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({ return await this.prisma.serviceMeetingRoom.findUnique({
...query, ...query,
where: args.where, where: args.where,
@@ -55,7 +55,7 @@ export class ServiceMeetingRoomSchema extends PothosSchema {
args: this.builder.generator.findManyArgs('ServiceMeetingRoom'), args: this.builder.generator.findManyArgs('ServiceMeetingRoom'),
description: description:
'Retrieve a list of service meeting rooms with optional filtering, ordering, and pagination.', '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({ return await this.prisma.serviceMeetingRoom.findMany({
...query, ...query,
skip: args.skip ?? undefined, skip: args.skip ?? undefined,

View File

@@ -73,7 +73,7 @@ export class UploadedFileSchema extends PothosSchema {
'Retrieve a single uploaded file by its unique identifier.', 'Retrieve a single uploaded file by its unique identifier.',
type: this.uploadedFile(), type: this.uploadedFile(),
args: this.builder.generator.findUniqueArgs('UploadedFile'), args: this.builder.generator.findUniqueArgs('UploadedFile'),
resolve: async (query, root, args) => { resolve: async (query, _root, args) => {
const file = await this.prisma.uploadedFile.findUnique({ const file = await this.prisma.uploadedFile.findUnique({
...query, ...query,
where: args.where, where: args.where,
@@ -94,7 +94,7 @@ export class UploadedFileSchema extends PothosSchema {
'Retrieve a list of uploaded files with optional filtering, ordering, and pagination.', 'Retrieve a list of uploaded files with optional filtering, ordering, and pagination.',
type: [this.uploadedFile()], type: [this.uploadedFile()],
args: this.builder.generator.findManyArgs('UploadedFile'), args: this.builder.generator.findManyArgs('UploadedFile'),
resolve: async (query, root, args) => { resolve: async (query, _root, args) => {
const files = await this.prisma.uploadedFile.findMany({ const files = await this.prisma.uploadedFile.findMany({
...query, ...query,
skip: args.skip ?? undefined, skip: args.skip ?? undefined,
@@ -132,7 +132,7 @@ export class UploadedFileSchema extends PothosSchema {
required: true, required: true,
}), }),
}, },
resolve: async (query, root, args) => { resolve: async (_query, _root, args) => {
const user = await this.prisma.user.findUnique({ const user = await this.prisma.user.findUnique({
where: { where: {
id: args.userId, id: args.userId,
@@ -182,7 +182,7 @@ export class UploadedFileSchema extends PothosSchema {
required: true, required: true,
}), }),
}, },
resolve: async (query, root, args) => { resolve: async (_query, _root, args) => {
const user = await this.prisma.user.findUnique({ const user = await this.prisma.user.findUnique({
where: { where: {
id: args.userId, id: args.userId,
@@ -230,7 +230,7 @@ export class UploadedFileSchema extends PothosSchema {
required: true, required: true,
}), }),
}, },
resolve: async (query, root, args) => { resolve: async (_query, _root, args) => {
const file = await this.prisma.uploadedFile.findUnique({ const file = await this.prisma.uploadedFile.findUnique({
where: { where: {
id: args.id, id: args.id,
@@ -259,7 +259,7 @@ export class UploadedFileSchema extends PothosSchema {
required: true, required: true,
}), }),
}, },
resolve: async (query, root, args) => { resolve: async (_query, _root, args) => {
const files = await this.prisma.uploadedFile.findMany({ const files = await this.prisma.uploadedFile.findMany({
where: { where: {
id: { id: {

View File

@@ -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({ inviteModerator: t.field({
type: 'String', type: 'String',
args: { args: {