Enhance NestJS application with SWC builder configuration, add @nestjs/devtools-integration for development support, and refactor various components for improved readability. Update package dependencies and streamline import statements across multiple files.

This commit is contained in:
2024-11-26 04:26:55 +07:00
parent c4e302387f
commit a1ca5c62fb
12 changed files with 1646 additions and 235 deletions

View File

@@ -6,6 +6,14 @@
"compilerOptions": {
"watchAssets": true,
"deleteOutDir": true,
"builder": {
"type": "swc",
"options": {
"copyFiles": true,
"includeDotfiles": true
}
},
"typeCheck": true,
"assets": [
{
"include": "**/*.pug",

1561
package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@@ -44,6 +44,7 @@
"@nestjs/common": "^10.0.0",
"@nestjs/config": "^3.2.3",
"@nestjs/core": "^10.0.0",
"@nestjs/devtools-integration": "^0.1.6",
"@nestjs/event-emitter": "^2.1.1",
"@nestjs/graphql": "^12.2.0",
"@nestjs/jwt": "^10.2.0",
@@ -141,19 +142,13 @@
"ws": "^8.18.0"
},
"jest": {
"moduleFileExtensions": [
"js",
"json",
"ts"
],
"moduleFileExtensions": ["js", "json", "ts"],
"rootDir": "src",
"testRegex": ".*\\.spec\\.ts$",
"transform": {
"^.+\\.(t|j)s$": "ts-jest"
},
"collectCoverageFrom": [
"**/*.(t|j)s"
],
"collectCoverageFrom": ["**/*.(t|j)s"],
"coverageDirectory": "../coverage",
"testEnvironment": "node"
},

View File

@@ -1,13 +1,4 @@
import {
Controller,
Get,
Post,
Put,
Delete,
Param,
Body,
Headers,
} from '@nestjs/common'
import { Controller, Get, Post, Put, Delete, Param, Body, Headers } from '@nestjs/common'
import { ClerkService } from './clerk.service'
import { ApiTags, ApiOperation, ApiResponse } from '@nestjs/swagger'
@ApiTags('Clerk')
@@ -18,7 +9,7 @@ export class ClerkController {
@Post('webhook')
@ApiOperation({ summary: 'Clerk Webhook' })
@ApiResponse({ status: 200, description: 'Webhook created successfully' })
webhook(@Headers() headers: Headers, @Body() body: any) {
webhook(@Body() body: any) {
return this.clerkService.webhook(body)
}
}

View File

@@ -1,10 +1,8 @@
export enum DocumentEvent {
CREATED = 'document_created',
CHANGED = 'document_changed',
DELETED = 'document_deleted',
SAVED = 'document_saved',
PAGE_CREATED = 'document_page_created',
PAGE_DELETED = 'document_page_deleted',
DOCUMENT_CREATED = 'document_created',
ACTIVE_DOCUMENT_ID_CHANGED = 'document_active_document_id_changed',
}

View File

@@ -1,10 +1,5 @@
import { Inject, Injectable, Logger } from '@nestjs/common'
import {
Pothos,
PothosRef,
PothosSchema,
SchemaBuilderToken,
} from '@smatch-corp/nestjs-pothos'
import { Pothos, PothosRef, PothosSchema, SchemaBuilderToken } from '@smatch-corp/nestjs-pothos'
import { Builder, SchemaContext } from '../Graphql/graphql.builder'
import { PrismaService } from '../Prisma/prisma.service'
import { DocumentEvent } from './document.event'
@@ -99,10 +94,7 @@ export class DocumentSchema extends PothosSchema {
...query,
orderBy: args.orderBy ?? undefined,
where: {
OR: [
{ ownerId: ctx.http.me.id },
{ collaborators: { some: { userId: ctx.http.me.id } } },
],
OR: [{ ownerId: ctx.http.me.id }, { collaborators: { some: { userId: ctx.http.me.id } } }],
},
})
},
@@ -138,11 +130,7 @@ export class DocumentSchema extends PothosSchema {
if (ctx.isSubscription) throw new Error('Not allowed')
const userId = ctx.http?.me?.id
if (!userId) throw new Error('User not found')
const fileUrl = await this.minio.getFileUrl(
'document',
'document',
'document',
)
const fileUrl = await this.minio.getFileUrl('document', 'document', 'document')
if (!fileUrl) throw new Error('File not found')
const document = await this.prisma.document.create({
...query,
@@ -211,10 +199,7 @@ export class DocumentSchema extends PothosSchema {
delta,
senderId: ctx.http?.me?.id,
}
ctx.http.pubSub.publish(
`${DocumentEvent.CHANGED}.${args.documentId}`,
documentDelta,
)
ctx.http.pubSub.publish(`${DocumentEvent.CHANGED}.${args.documentId}`, documentDelta)
return documentDelta
},
}),
@@ -275,9 +260,7 @@ export class DocumentSchema extends PothosSchema {
if (
document.ownerId !== ctx.http?.me?.id &&
!document.isPublic &&
!document.collaborators.some(
(c) => c.userId === ctx.http?.me?.id && c.writable,
)
!document.collaborators.some((c) => c.userId === ctx.http?.me?.id && c.writable)
)
throw new Error('User is not owner or collaborator of document')
return await this.prisma.document.update({
@@ -302,8 +285,7 @@ export class DocumentSchema extends PothosSchema {
where: { id: args.documentId },
})
if (!document) throw new Error('Document not found')
if (document.ownerId !== ctx.http?.me?.id)
throw new Error('User is not owner of document')
if (document.ownerId !== ctx.http?.me?.id) throw new Error('User is not owner of document')
return await this.prisma.documentCollaborator.create({
data: {
documentId: args.documentId,
@@ -342,17 +324,16 @@ export class DocumentSchema extends PothosSchema {
if (!document.isPublic) {
if (
document.ownerId !== ctx.websocket?.me?.id &&
!document.collaborators.some(
(c) => c.userId === ctx.websocket?.me?.id && c.writable,
)
!document.collaborators.some((c) => c.userId === ctx.websocket?.me?.id && c.writable)
)
throw new Error('User is not owner or collaborator of document')
}
return pubSub.asyncIterator([
`${DocumentEvent.CHANGED}.${documentId}`,
`${DocumentEvent.CREATED}.${documentId}`,
`${DocumentEvent.DELETED}.${documentId}`,
`${DocumentEvent.SAVED}.${documentId}`,
`${DocumentEvent.PAGE_CREATED}.${documentId}`,
`${DocumentEvent.PAGE_DELETED}.${documentId}`,
`${DocumentEvent.ACTIVE_DOCUMENT_ID_CHANGED}.${documentId}`,
]) as unknown as AsyncIterable<DocumentDelta>
},

View File

@@ -3,64 +3,12 @@ import { forwardRef, Inject, Injectable } from '@nestjs/common'
import Delta, { Op } from 'quill-delta'
import { MinioService } from '../Minio/minio.service'
import { DocumentDelta } from './document.type'
import { JSDOM } from 'jsdom'
import { Logger } from '@nestjs/common'
import { PrismaService } from 'src/Prisma/prisma.service'
@Injectable()
export class DocumentService {
// biome-ignore lint/suspicious/noExplicitAny: <explanation>
private quill: any
constructor(
private readonly prisma: PrismaService,
private readonly minio: MinioService,
public document: JSDOM,
) {
;(async () => {
await this.loadQuill()
})()
}
private async loadQuill() {
if (typeof window === 'undefined' || typeof document === 'undefined') {
const { window } = new JSDOM('<!doctype html><html><body></body></html>')
const { navigator } = window
const { Node } = window
global.window = window
global.document = window.document
global.navigator = navigator
global.Node = Node
}
const { default: Quill } = await import('quill')
this.quill = Quill
}
// TODO: maybe never do :)
async handleOnChange(documentId: string, delta: DocumentDelta) {}
async handleOnSave(documentId: string) {}
async handleOnSync(documentId: string) {}
async requestSync(documentId: string, page?: number) {
// using pubsub to broadcast to all clients
// this.pubSub.publish(`document:sync:${documentId}`, {
// documentId,
// page,
// })
}
async clientRequestSave(documentId: string) {
// using pubsub to broadcast to all clients
// this.pubSub.publish(`document:save:${documentId}`, {
// documentId,
// })
}
async serverRequestSave(documentId: string) {}
async generatePreviewImage(documentId: string) {}
async allPageToDelta(documentId: string) {}
async toDocx(documentId: string) {}
) {}
}
// epess/documents/<id>/<page>

View File

@@ -36,10 +36,7 @@ export class MinioService {
}
let url = null
// check if presignUrl is provided and not expired else get new presignUrl
if (
presignUrl &&
!DateTimeUtils.isExpired(presignUrl.split('&X-Amz-Date=')[1])
) {
if (presignUrl && !DateTimeUtils.isExpired(presignUrl.split('&X-Amz-Date=')[1])) {
url = presignUrl
} else {
try {
@@ -81,14 +78,30 @@ export class MinioService {
}
async deleteFile(id: string, category: string) {
return await this.minioClient.removeObject(
this.configService.get('BUCKET_NAME') ?? 'epess',
`${category}/${id}`,
)
return await this.minioClient.removeObject(this.configService.get('BUCKET_NAME') ?? 'epess', `${category}/${id}`)
}
// create a folder for a document pattern epess/documents/<id>/<page>
async createDocumentFolder(id: string) {
return await this.minioClient.putObject(this.configService.get('BUCKET_NAME') ?? 'epess', `documents/${id}`, '')
}
async streamFile(id: string, category: string) {}
async upsertDocumentFolder(id: string, page: string) {
return await this.minioClient.putObject(
this.configService.get('BUCKET_NAME') ?? 'epess',
`documents/${id}/${page}`,
'',
)
}
// export document to docx format by get all pages and convert to docx
async exportDocument(id: string) {
// get all pages
const pages = await this.minioClient.listObjects(
this.configService.get('BUCKET_NAME') ?? 'epess',
`documents/${id}`,
)
// convert to docx
// const docx = await this.convertToDocx(pages)
}
fileName() {
// generate a unique file name using uuid
return uuidv4()

View File

@@ -4,15 +4,10 @@ import { Injectable, Logger } from '@nestjs/common'
import { PrismaService } from 'src/Prisma/prisma.service'
import { AppConfigService } from 'src/AppConfig/appconfig.service'
import {
PreviewScheduleType,
ScheduleConfigType,
ScheduleConfigTypeForCenter,
ScheduleSlotType,
} from './schedule.d'
import { PreviewScheduleType, ScheduleConfigType, ScheduleConfigTypeForCenter, ScheduleSlotType } from './schedule.d'
import { Config, Schedule, ScheduleDate } from '@prisma/client'
import { DateTime, Settings, Zone } from 'luxon'
import * as _ from 'lodash'
import _ from 'lodash'
import { ScheduleDateInput } from './schedule'
@Injectable()
@@ -22,9 +17,7 @@ export class ScheduleService {
private readonly appConfigService: AppConfigService,
) {}
async createSchedulePreviewForSingleDay(
scheduleConfig: ScheduleConfigType,
): Promise<PreviewScheduleType> {
async createSchedulePreviewForSingleDay(scheduleConfig: ScheduleConfigType): Promise<PreviewScheduleType> {
// generate Slot By config
const slots = this.generateSlots(scheduleConfig)
@@ -35,12 +28,8 @@ export class ScheduleService {
}
// create preview for center require scheduleConfigInput: { startDate: "2024-11-02T00:00:00.000Z", endDate: "2024-11-22T00:00:00.000Z", slots: [1, 3], days: [2, 5] }
async createSchedulePreviewForCenter(
scheduleConfig: ScheduleConfigTypeForCenter,
): Promise<PreviewScheduleType> {
const config: ScheduleConfigType = (
await this.appConfigService.getVisibleConfigs()
).reduce((acc, curr) => {
async createSchedulePreviewForCenter(scheduleConfig: ScheduleConfigTypeForCenter): Promise<PreviewScheduleType> {
const config: ScheduleConfigType = (await this.appConfigService.getVisibleConfigs()).reduce((acc, curr) => {
// @ts-ignore
acc[curr.key] = curr.value
return acc
@@ -54,9 +43,7 @@ export class ScheduleService {
async generateScheduleDates(schedule: Schedule): Promise<ScheduleDate[]> {
// generate schedule dates based on data and config
const config: ScheduleConfigType = (
await this.appConfigService.getVisibleConfigs()
).reduce((acc, curr) => {
const config: ScheduleConfigType = (await this.appConfigService.getVisibleConfigs()).reduce((acc, curr) => {
// @ts-ignore
acc[curr.key] = curr.value
return acc
@@ -72,11 +59,7 @@ export class ScheduleService {
const scheduleDates: ScheduleDateInput[] = []
// loop each day from scheduleStart to scheduleEnd
for (
let date = scheduleStart;
date <= scheduleEnd;
date = date.plus({ days: 1 })
) {
for (let date = scheduleStart; date <= scheduleEnd; date = date.plus({ days: 1 })) {
// Check if the current date matches one of the specified days of the week
if (daysOfWeeks.includes(date.weekday)) {
// loop through slots
@@ -85,10 +68,7 @@ export class ScheduleService {
slot,
slotDuration.toString(),
slotBreakDuration.toString(),
DateTimeUtils.getATimeWithDateB(
DateTime.fromISO(config.dayStartTime),
date,
).toISO() ?? '',
DateTimeUtils.getATimeWithDateB(DateTime.fromISO(config.dayStartTime), date).toISO() ?? '',
)
scheduleDates.push({
scheduleId: schedule.id,
@@ -103,10 +83,9 @@ export class ScheduleService {
}
}
}
const scheduleDatesCreated =
await this.prisma.scheduleDate.createManyAndReturn({
data: scheduleDates,
})
const scheduleDatesCreated = await this.prisma.scheduleDate.createManyAndReturn({
data: scheduleDates,
})
return scheduleDatesCreated
}
@@ -137,11 +116,7 @@ query CenterPreviewSchedule {
const scheduleStart = DateTime.fromISO(_scheduleConfig.startDate)
const scheduleEnd = DateTime.fromISO(_scheduleConfig.endDate)
// loop each day from scheduleStart to scheduleEnd
for (
let date = scheduleStart;
date <= scheduleEnd;
date = date.plus({ days: 1 })
) {
for (let date = scheduleStart; date <= scheduleEnd; date = date.plus({ days: 1 })) {
// Check if the current date matches one of the specified days of the week
if (daysOfWeeks.includes(date.weekday)) {
// loop through slots
@@ -151,10 +126,7 @@ query CenterPreviewSchedule {
slot,
_config.slotDuration,
_config.slotBreakDuration,
DateTimeUtils.getATimeWithDateB(
DateTime.fromISO(_config.dayStartTime),
date,
).toISO() ?? '',
DateTimeUtils.getATimeWithDateB(DateTime.fromISO(_config.dayStartTime), date).toISO() ?? '',
)
// if the slot is not overlapping with mid day break time, add it to the slots
if (
@@ -213,24 +185,11 @@ query CenterPreviewSchedule {
return slots
}
isOverLapping(
startTime1: DateTime,
endTime1: DateTime,
startTime2: DateTime,
endTime2: DateTime,
) {
return (
Math.max(startTime1.toMillis(), startTime2.toMillis()) <
Math.min(endTime1.toMillis(), endTime2.toMillis())
)
isOverLapping(startTime1: DateTime, endTime1: DateTime, startTime2: DateTime, endTime2: DateTime) {
return Math.max(startTime1.toMillis(), startTime2.toMillis()) < Math.min(endTime1.toMillis(), endTime2.toMillis())
}
calculateNumberOfSlots(
startTime: string,
endTime: string,
slotDuration: string,
slotBreakDuration: string,
) {
calculateNumberOfSlots(startTime: string, endTime: string, slotDuration: string, slotBreakDuration: string) {
const _startTime = DateTimeUtils.toTime(startTime)
const _endTime = DateTimeUtils.toTime(endTime)
const _slotDuration = parseInt(slotDuration) // minutes
@@ -247,20 +206,12 @@ query CenterPreviewSchedule {
second: _endTime.second,
})
const totalMinutes =
(endDate.toMillis() - startDate.toMillis()) / (60 * 1000)
const numberOfSlots = Math.floor(
totalMinutes / (_slotDuration + _slotBreakDuration),
)
const totalMinutes = (endDate.toMillis() - startDate.toMillis()) / (60 * 1000)
const numberOfSlots = Math.floor(totalMinutes / (_slotDuration + _slotBreakDuration))
return numberOfSlots
}
getSlotStartAndEndTime(
slotNumber: number,
slotDuration: string,
slotBreakDuration: string,
dayStartTime: string,
) {
getSlotStartAndEndTime(slotNumber: number, slotDuration: string, slotBreakDuration: string, dayStartTime: string) {
const durationInMinutes = parseInt(slotDuration)
const breakDurationInMinutes = parseInt(slotBreakDuration)
const initialStartTime = DateTime.fromISO(dayStartTime)

View File

@@ -1,3 +1,4 @@
import { DevtoolsModule } from '@nestjs/devtools-integration'
import { ClerkModule } from './Clerk/clerk.module'
import { ConfigModule } from '@nestjs/config'
import { GraphqlModule } from './Graphql/graphql.module'
@@ -9,6 +10,10 @@ import { CronModule } from './Cron/cron.module'
@Module({
imports: [
DevtoolsModule.register({
http: process.env.NODE_ENV !== 'production',
port: 8000,
}),
ConfigModule.forRoot({
isGlobal: true,
}),

View File

@@ -1,14 +1,6 @@
import { Injectable } from '@nestjs/common'
import * as _ from 'lodash'
import {
DateTime,
Settings,
HourNumbers,
MinuteNumbers,
SecondNumbers,
DayNumbers,
WeekdayNumbers,
} from 'luxon'
import _ from 'lodash'
import { DateTime, Settings, HourNumbers, MinuteNumbers, SecondNumbers, DayNumbers, WeekdayNumbers } from 'luxon'
Settings.defaultLocale = 'en-US'
Settings.defaultZone = 'utc'
@@ -52,22 +44,11 @@ export class DateTimeUtils {
return DateTime.fromISO(expires) < DateTime.now()
}
static isOverlap(
startA: DateTime,
endA: DateTime,
startB: DateTime,
endB: DateTime,
): boolean {
return (
this.getOverlapRange(startA, endA, startB, endB).start <
this.getOverlapRange(startA, endA, startB, endB).end
)
static isOverlap(startA: DateTime, endA: DateTime, startB: DateTime, endB: DateTime): boolean {
return this.getOverlapRange(startA, endA, startB, endB).start < this.getOverlapRange(startA, endA, startB, endB).end
}
static isOverlaps(
listA: { start: DateTime; end: DateTime }[],
listB: { start: DateTime; end: DateTime }[],
): boolean {
static isOverlaps(listA: { start: DateTime; end: DateTime }[], listB: { start: DateTime; end: DateTime }[]): boolean {
for (const a of listA) {
for (const b of listB) {
if (this.isOverlap(a.start, a.end, b.start, b.end)) {

View File

@@ -10,39 +10,22 @@ import { readFileSync } from 'node:fs'
import { json } from 'express'
async function bootstrap() {
const app = await NestFactory.create(AppModule)
const app = await NestFactory.create(AppModule, {})
// load private key and public key
const privateKey = readFileSync(
path.join(__dirname, 'KeyStore', 'private_key.pem'),
'utf8',
)
const publicKey = readFileSync(
path.join(__dirname, 'KeyStore', 'public_key.pem'),
'utf8',
)
const privateKey = readFileSync(path.join(__dirname, 'KeyStore', 'private_key.pem'), 'utf8')
const publicKey = readFileSync(path.join(__dirname, 'KeyStore', 'public_key.pem'), 'utf8')
// set private key and public key to env
process.env.JWT_RS256_PRIVATE_KEY = privateKey
process.env.JWT_RS256_PUBLIC_KEY = publicKey
Logger.log(
`Private key: ${privateKey.slice(0, 10).replace(/\n/g, '')}...`,
'Bootstrap',
)
Logger.log(
`Public key: ${publicKey.slice(0, 10).replace(/\n/g, '')}...`,
'Bootstrap',
)
Logger.log(`Private key: ${privateKey.slice(0, 10).replace(/\n/g, '')}...`, 'Bootstrap')
Logger.log(`Public key: ${publicKey.slice(0, 10).replace(/\n/g, '')}...`, 'Bootstrap')
const corsOrigin = (process.env.CORS_ORIGIN ?? '').split(',') // split by comma to array
app.enableCors({
origin: corsOrigin,
methods: ['GET', 'POST', 'PUT', 'DELETE', 'PATCH', 'OPTIONS'],
allowedHeaders: [
'Content-Type',
'*',
'x-apollo-operation-name',
'x-session-id',
],
allowedHeaders: ['Content-Type', '*', 'x-apollo-operation-name', 'x-session-id'],
credentials: true,
})
@@ -63,8 +46,7 @@ async function bootstrap() {
get: {
tags: ['GraphQL'],
summary: 'GraphQL Playground',
description:
'Access the GraphQL Playground to interact with the GraphQL API.',
description: 'Access the GraphQL Playground to interact with the GraphQL API.',
responses: {
'200': {
description: 'GraphQL Playground',
@@ -90,10 +72,7 @@ async function bootstrap() {
)
// biome-ignore lint/suspicious/noExplicitAny: <explanation>
} catch (error: any) {
Logger.error(
`Error in file upload middleware: ${error.message}`,
'Bootstrap',
)
Logger.error(`Error in file upload middleware: ${error.message}`, 'Bootstrap')
// Optionally, you can handle the error further or rethrow it
}