From 3351219b8911b9320db1a477cb34f67b0fd1e94a Mon Sep 17 00:00:00 2001 From: EzeanoroEbuka Date: Thu, 26 Feb 2026 17:21:34 +0100 Subject: [PATCH 1/3] Logging service has been implemented and is saving to prisma database --- Dockerfile | 7 +- docker-compose.yml | 2 +- .../20260226015842_add_app_logs/migration.sql | 15 ++++ prisma/schema.prisma | 20 ++++- src/app.module.ts | 18 ++++- src/common/filters/http-exception.filter.ts | 46 ++++++++--- src/common/guards/jwt-auth.guard.ts | 32 ++++++++ .../interceptors/logging.interceptor.ts | 21 ++++- src/logging/logging.interceptor.ts | 63 +++++++++++++++ src/logging/logging.module.ts | 13 ++++ src/logging/logging.service.spec.ts | 18 +++++ src/logging/logging.service.ts | 78 +++++++++++++++++++ src/main.ts | 8 +- src/types/express.d.ts | 11 +++ tsconfig.json | 10 ++- 15 files changed, 337 insertions(+), 25 deletions(-) create mode 100644 prisma/migrations/20260226015842_add_app_logs/migration.sql create mode 100644 src/common/guards/jwt-auth.guard.ts create mode 100644 src/logging/logging.interceptor.ts create mode 100644 src/logging/logging.module.ts create mode 100644 src/logging/logging.service.spec.ts create mode 100644 src/logging/logging.service.ts create mode 100644 src/types/express.d.ts diff --git a/Dockerfile b/Dockerfile index b34cc21..a35c949 100644 --- a/Dockerfile +++ b/Dockerfile @@ -25,14 +25,17 @@ FROM node:20-alpine WORKDIR /app COPY package*.json ./ -RUN npm install +RUN npm config set registry https://registry.npmjs.org/ && \ + npm ci --prefer-offline || npm install --fetch-retry-mintimeout 20000 \ + --fetch-retry-maxtimeout 120000 \ + --fetch-retries 5 COPY . . # Generate Prisma client RUN npx prisma generate -EXPOSE 3000 +EXPOSE 3001 # CMD ["sh", "-c", "npx prisma migrate deploy && npm run start:dev"] CMD ["npm", "run", "start:dev"] diff --git a/docker-compose.yml b/docker-compose.yml index ae6c03b..1e0163f 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -5,7 +5,7 @@ services: context: . dockerfile: Dockerfile ports: - - "3000:3000" + - "3001:3001" - "5555:5555" volumes: - .:/app diff --git a/prisma/migrations/20260226015842_add_app_logs/migration.sql b/prisma/migrations/20260226015842_add_app_logs/migration.sql new file mode 100644 index 0000000..a089ef4 --- /dev/null +++ b/prisma/migrations/20260226015842_add_app_logs/migration.sql @@ -0,0 +1,15 @@ +-- CreateEnum +CREATE TYPE "LogLevel" AS ENUM ('INFO', 'WARN', 'ERROR'); + +-- CreateTable +CREATE TABLE "AppLog" ( + "id" TEXT NOT NULL, + "level" "LogLevel" NOT NULL, + "action" TEXT NOT NULL, + "message" TEXT NOT NULL, + "userId" TEXT, + "metadata" JSONB, + "createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP, + + CONSTRAINT "AppLog_pkey" PRIMARY KEY ("id") +); diff --git a/prisma/schema.prisma b/prisma/schema.prisma index 62ac152..097d76c 100644 --- a/prisma/schema.prisma +++ b/prisma/schema.prisma @@ -84,6 +84,11 @@ enum EventType { TRUST_SCORE_UPDATED } +enum LogLevel { + INFO + WARN + ERROR +} // ─── Models ────────────────────────────────────────────── model User { @@ -226,6 +231,18 @@ model Escrow { @@map("escrows") } + +model AppLog { + id String @id @default(uuid()) + level LogLevel + action String + message String + userId String? + metadata Json? + createdAt DateTime @default(now()) +} + + model EventLog { id String @id @default(uuid()) entityType EventEntityType @map("entity_type") @@ -247,4 +264,5 @@ model EventLog { @@index([eventType]) @@index([createdAt]) @@map("event_logs") -} \ No newline at end of file +} + diff --git a/src/app.module.ts b/src/app.module.ts index af05dbe..df5ab83 100644 --- a/src/app.module.ts +++ b/src/app.module.ts @@ -11,6 +11,10 @@ import { EventsModule } from './events/events.module'; import { StellarModule } from './stellar/stellar.module'; import { AuthModule } from './auth/auth.module'; import { HealthModule } from './health/health.module'; +import { LoggingModule } from './logging/logging.module'; +import { HttpExceptionFilter } from './common/filters/http-exception.filter'; +import { APP_INTERCEPTOR } from '@nestjs/core'; +import { LoggingInterceptor } from './logging/logging.interceptor'; @Module({ imports: [ @@ -23,9 +27,19 @@ import { HealthModule } from './health/health.module'; EventsModule, StellarModule, AuthModule, - HealthModule + HealthModule, + LoggingModule, + ], + controllers: [AppController], - providers: [AppService], + providers: [ + { + provide: APP_INTERCEPTOR, + useClass: LoggingInterceptor, + }, + AppService, HttpExceptionFilter], + }) + export class AppModule { } diff --git a/src/common/filters/http-exception.filter.ts b/src/common/filters/http-exception.filter.ts index 4d0757e..92ba7e9 100644 --- a/src/common/filters/http-exception.filter.ts +++ b/src/common/filters/http-exception.filter.ts @@ -6,10 +6,15 @@ import { HttpStatus, } from '@nestjs/common'; import { Request, Response } from 'express'; +import { LoggingService } from '../../logging/logging.service'; @Catch() export class HttpExceptionFilter implements ExceptionFilter { - catch(exception: unknown, host: ArgumentsHost): void { + constructor( + private readonly loggingService: LoggingService, + ) {} + + async catch(exception: unknown, host: ArgumentsHost): Promise { const ctx = host.switchToHttp(); const response = ctx.getResponse(); const request = ctx.getRequest(); @@ -18,15 +23,27 @@ export class HttpExceptionFilter implements ExceptionFilter { let message: string | object = 'Internal server error'; if (exception instanceof HttpException) { - const httpException = exception as HttpException; - status = httpException.getStatus(); - message = httpException.getResponse(); - } - else if (exception instanceof Error) { - const error = exception as Error; - message = error.message; + status = exception.getStatus(); + message = exception.getResponse(); + } else if (exception instanceof Error) { + message = exception.message; } + await this.loggingService.log({ + level: 'ERROR', + action: 'HTTP_EXCEPTION', + message: + typeof message === 'string' + ? message + : JSON.stringify(message), + userId: (request as any).user?.sub, + metadata: { + statusCode: status, + path: request.url, + method: request.method, + }, + }); + response.status(status).json({ success: false, statusCode: status, @@ -35,4 +52,15 @@ export class HttpExceptionFilter implements ExceptionFilter { message, }); } -} \ No newline at end of file +} + + + + + + + + + + + diff --git a/src/common/guards/jwt-auth.guard.ts b/src/common/guards/jwt-auth.guard.ts new file mode 100644 index 0000000..a310097 --- /dev/null +++ b/src/common/guards/jwt-auth.guard.ts @@ -0,0 +1,32 @@ +import { + Injectable, + CanActivate, + ExecutionContext, + UnauthorizedException, +} from '@nestjs/common'; +import { JwtService } from '@nestjs/jwt'; +import { Request } from 'express'; + +@Injectable() +export class JwtAuthGuard implements CanActivate { + constructor(private readonly jwtService: JwtService) {} + + async canActivate(context: ExecutionContext): Promise { + const request = context.switchToHttp().getRequest(); + const authHeader = request.headers.authorization; + + if (!authHeader || !authHeader.startsWith('Bearer ')) { + throw new UnauthorizedException('Missing or invalid token'); + } + + const token = authHeader.split(' ')[1]; + + try { + const payload = await this.jwtService.verifyAsync(token); + request.user = payload; // 👈 attach user + return true; + } catch { + throw new UnauthorizedException('Invalid or expired token'); + } + } +} \ No newline at end of file diff --git a/src/common/interceptors/logging.interceptor.ts b/src/common/interceptors/logging.interceptor.ts index 8ee2e18..acb566e 100644 --- a/src/common/interceptors/logging.interceptor.ts +++ b/src/common/interceptors/logging.interceptor.ts @@ -1,3 +1,4 @@ + import { Injectable, NestInterceptor, @@ -5,20 +6,32 @@ import { CallHandler, } from '@nestjs/common'; import { Observable, tap } from 'rxjs'; +import { LoggingService } from '../../logging/logging.service'; + + @Injectable() export class LoggingInterceptor implements NestInterceptor { + constructor(private readonly loggingService: LoggingService) {} + intercept(context: ExecutionContext, next: CallHandler): Observable { const request = context.switchToHttp().getRequest(); - const { method, url } = request; + const { method, url, user } = request; const now = Date.now(); return next.handle().pipe( - tap(() => + tap(async () => { console.log( `${method} ${url} - ${Date.now() - now}ms`, ), - ), + await this.loggingService.log({ + level: 'INFO', + action: `${method} ${url}`, + message: 'Request completed successfully', + userId: user?.sub, + }); + }), ); } -} \ No newline at end of file +} + diff --git a/src/logging/logging.interceptor.ts b/src/logging/logging.interceptor.ts new file mode 100644 index 0000000..bc28462 --- /dev/null +++ b/src/logging/logging.interceptor.ts @@ -0,0 +1,63 @@ +import { + Injectable, + NestInterceptor, + ExecutionContext, + CallHandler, +} from '@nestjs/common'; +import { Observable, tap, catchError } from 'rxjs'; +import { LoggingService } from './logging.service'; +import { Request, Response } from 'express'; + +@Injectable() +export class LoggingInterceptor implements NestInterceptor { + constructor(private readonly loggingService: LoggingService) {} + + intercept( + context: ExecutionContext, + next: CallHandler, + ): Observable { + const ctx = context.switchToHttp(); + const request = ctx.getRequest(); + const response = ctx.getResponse(); + + const { method, originalUrl } = request; + const startTime = Date.now(); + + return next.handle().pipe( + tap(() => { + const duration = Date.now() - startTime; + + this.loggingService.log({ + level: 'INFO', + action: 'HTTP_REQUEST', + message: `${method} ${originalUrl} ${response.statusCode}`, + userId: request.user?.sub, + metadata: { + method, + path: originalUrl, + statusCode: response.statusCode, + duration, + }, + }); + }), + + catchError((error) => { + const duration = Date.now() - startTime; + + this.loggingService.log({ + level: 'ERROR', + action: 'HTTP_ERROR', + message: error.message, + userId: request.user?.sub, + metadata: { + method, + path: originalUrl, + duration, + }, + }); + + throw error; + }), + ); + } +} \ No newline at end of file diff --git a/src/logging/logging.module.ts b/src/logging/logging.module.ts new file mode 100644 index 0000000..3171e1c --- /dev/null +++ b/src/logging/logging.module.ts @@ -0,0 +1,13 @@ + +import { Module } from '@nestjs/common'; +import { LoggingService } from './logging.service'; +import { LoggingInterceptor } from './logging.interceptor'; +import { PrismaModule } from '../prisma/prisma.module'; + + +@Module({ + imports: [PrismaModule], + providers: [LoggingService, LoggingInterceptor], + exports: [LoggingService, LoggingInterceptor], +}) +export class LoggingModule {} \ No newline at end of file diff --git a/src/logging/logging.service.spec.ts b/src/logging/logging.service.spec.ts new file mode 100644 index 0000000..35b8391 --- /dev/null +++ b/src/logging/logging.service.spec.ts @@ -0,0 +1,18 @@ +import { Test, TestingModule } from '@nestjs/testing'; +import { LoggingService } from './logging.service'; + +describe('LoggingService', () => { + let service: LoggingService; + + beforeEach(async () => { + const module: TestingModule = await Test.createTestingModule({ + providers: [LoggingService], + }).compile(); + + service = module.get(LoggingService); + }); + + it('should be defined', () => { + expect(service).toBeDefined(); + }); +}); diff --git a/src/logging/logging.service.ts b/src/logging/logging.service.ts new file mode 100644 index 0000000..64916df --- /dev/null +++ b/src/logging/logging.service.ts @@ -0,0 +1,78 @@ + +// import { Injectable, InternalServerErrorException } from '@nestjs/common'; +// import { PrismaService } from '../../src/prisma/prisma.service'; + +// export type LogLevel = 'INFO' | 'WARN' | 'ERROR'; + +// @Injectable() +// export class LoggingService { +// constructor(private readonly prisma: PrismaService) {} + +// async log(params: { +// level: LogLevel; +// action: string; +// message: string; +// userId?: string; +// metadata?: Record; +// }) { + +// try { +// return await this.prisma.appLog.create({ +// data: { +// level: params.level, +// action: params.action, +// message: params.message, +// userId: params.userId, +// metadata: params.metadata, +// }, +// }); +// } catch (error) { +// // ❗ Acceptance Criteria: no silent failures +// throw new InternalServerErrorException( +// 'Failed to record application log', +// ); +// } +// } +// } + + + +import { Injectable, Logger } from '@nestjs/common'; +import { PrismaService } from '../../src/prisma/prisma.service'; + +export type LogLevel = 'INFO' | 'WARN' | 'ERROR'; + +@Injectable() +export class LoggingService { + private readonly logger = new Logger(LoggingService.name); + + constructor(private readonly prisma: PrismaService) {} + + async log(params: { + level: LogLevel; + action: string; + message: string; + userId?: string; + metadata?: Record; + }) { + try { + return await this.prisma.appLog.create({ + data: { + level: params.level, + action: params.action, + message: params.message, + userId: params.userId, + metadata: params.metadata, + }, + }); + } catch (error) { + // Surface the failure visibly without crashing the caller + this.logger.error( + `Failed to record application log | action: ${params.action} | ${error?.message}`, + error?.stack, + ); + // Return null so callers can optionally handle it + return null; + } + } +} \ No newline at end of file diff --git a/src/main.ts b/src/main.ts index 61f14b1..f2bd218 100644 --- a/src/main.ts +++ b/src/main.ts @@ -20,9 +20,13 @@ async function bootstrap() { }), ); - app.useGlobalFilters(new HttpExceptionFilter()); +app.useGlobalFilters( + app.get(HttpExceptionFilter), +); - app.useGlobalInterceptors(new LoggingInterceptor()); +// const loggingInterceptor = app.get(LoggingInterceptor); +// app.useGlobalInterceptors(loggingInterceptor); + app.useLogger(new AppLogger()); diff --git a/src/types/express.d.ts b/src/types/express.d.ts new file mode 100644 index 0000000..86ebcab --- /dev/null +++ b/src/types/express.d.ts @@ -0,0 +1,11 @@ +import { JwtPayload } from 'jsonwebtoken'; + +declare global { + namespace Express { + interface Request { + user?: JwtPayload | any; + } + } +} + +export {}; \ No newline at end of file diff --git a/tsconfig.json b/tsconfig.json index 8cbe5b0..7763cbe 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -20,12 +20,14 @@ "forceConsistentCasingInFileNames": true, "noImplicitAny": false, "strictBindCallApply": false, - "noFallthroughCasesInSwitch": false - + "noFallthroughCasesInSwitch": false, + "typeRoots": ["./node_modules/@types", "./src/types"] + }, "watchOptions": { "watchFile": "fixedPollingInterval" } - - + } + + From f6a94029e8dfe19e0188eb0ceea625093bfc6e9e Mon Sep 17 00:00:00 2001 From: EzeanoroEbuka Date: Thu, 26 Feb 2026 17:40:26 +0100 Subject: [PATCH 2/3] Logger Wiston commit --- src/prisma/prisma.service.spec.ts | 4 ++++ src/types/express.d.ts | 2 ++ 2 files changed, 6 insertions(+) diff --git a/src/prisma/prisma.service.spec.ts b/src/prisma/prisma.service.spec.ts index 8062d68..081d417 100644 --- a/src/prisma/prisma.service.spec.ts +++ b/src/prisma/prisma.service.spec.ts @@ -5,6 +5,7 @@ describe('PrismaService', () => { let service: PrismaService; beforeEach(async () => { + const module: TestingModule = await Test.createTestingModule({ providers: [PrismaService], }).compile(); @@ -15,15 +16,18 @@ describe('PrismaService', () => { afterEach(async () => { await service.$disconnect(); // Ensure proper teardown }); + it('should be defined', () => { expect(service).toBeDefined(); }); + it('should be defined as PrismaService', () => { expect(service.constructor.name).toBe('PrismaService'); }); + it('should have onModuleInit method', () => { expect(typeof service.onModuleInit).toBe('function'); }); diff --git a/src/types/express.d.ts b/src/types/express.d.ts index 86ebcab..1d0e76a 100644 --- a/src/types/express.d.ts +++ b/src/types/express.d.ts @@ -1,5 +1,7 @@ import { JwtPayload } from 'jsonwebtoken'; + + declare global { namespace Express { interface Request { From 91729b053729055355b080afd77727d8ff789084 Mon Sep 17 00:00:00 2001 From: EzeanoroEbuka Date: Fri, 27 Feb 2026 06:45:11 +0100 Subject: [PATCH 3/3] Fixed Document model and Pipeline errors --- .../migration.sql | 26 ++++++++++++ .../migration.sql | 21 ++++++++++ prisma/schema.prisma | 21 +++++++++- .../interceptors/logging.interceptor.ts | 2 +- src/custody/custody.service.ts | 3 +- src/logging/logging.service.spec.ts | 42 ++++++++++++++++++- tsconfig.json | 4 +- 7 files changed, 113 insertions(+), 6 deletions(-) create mode 100644 prisma/migrations/20260226233649_add_custody_status/migration.sql create mode 100644 prisma/migrations/20260227054055_add_document_model/migration.sql diff --git a/prisma/migrations/20260226233649_add_custody_status/migration.sql b/prisma/migrations/20260226233649_add_custody_status/migration.sql new file mode 100644 index 0000000..49c840f --- /dev/null +++ b/prisma/migrations/20260226233649_add_custody_status/migration.sql @@ -0,0 +1,26 @@ +/* + Warnings: + + - You are about to drop the column `gender` on the `pets` table. All the data in the column will be lost. + - You are about to drop the column `size` on the `pets` table. All the data in the column will be lost. + - You are about to drop the `documents` table. If the table is not empty, all the data it contains will be lost. + +*/ +-- DropForeignKey +ALTER TABLE "documents" DROP CONSTRAINT "documents_adoption_id_fkey"; + +-- DropForeignKey +ALTER TABLE "documents" DROP CONSTRAINT "documents_uploaded_by_id_fkey"; + +-- AlterTable +ALTER TABLE "pets" DROP COLUMN "gender", +DROP COLUMN "size"; + +-- DropTable +DROP TABLE "documents"; + +-- DropEnum +DROP TYPE "PetGender"; + +-- DropEnum +DROP TYPE "PetSize"; diff --git a/prisma/migrations/20260227054055_add_document_model/migration.sql b/prisma/migrations/20260227054055_add_document_model/migration.sql new file mode 100644 index 0000000..5e4b5c1 --- /dev/null +++ b/prisma/migrations/20260227054055_add_document_model/migration.sql @@ -0,0 +1,21 @@ +-- CreateTable +CREATE TABLE "Document" ( + "id" TEXT NOT NULL, + "fileName" TEXT NOT NULL, + "fileUrl" TEXT NOT NULL, + "publicId" TEXT NOT NULL, + "mimeType" TEXT NOT NULL, + "size" INTEGER NOT NULL, + "uploadedById" TEXT NOT NULL, + "adoptionId" TEXT NOT NULL, + "createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP, + "updatedAt" TIMESTAMP(3) NOT NULL, + + CONSTRAINT "Document_pkey" PRIMARY KEY ("id") +); + +-- AddForeignKey +ALTER TABLE "Document" ADD CONSTRAINT "Document_uploadedById_fkey" FOREIGN KEY ("uploadedById") REFERENCES "users"("id") ON DELETE RESTRICT ON UPDATE CASCADE; + +-- AddForeignKey +ALTER TABLE "Document" ADD CONSTRAINT "Document_adoptionId_fkey" FOREIGN KEY ("adoptionId") REFERENCES "adoptions"("id") ON DELETE RESTRICT ON UPDATE CASCADE; diff --git a/prisma/schema.prisma b/prisma/schema.prisma index 097d76c..422860f 100644 --- a/prisma/schema.prisma +++ b/prisma/schema.prisma @@ -42,6 +42,7 @@ enum CustodyStatus { RETURNED CANCELLED VIOLATION + PENDING } enum CustodyType { @@ -116,6 +117,8 @@ model User { events EventLog[] @relation("ActorEvents") + documents Document[] + @@map("users") } @@ -162,7 +165,7 @@ model Adoption { escrowId String? @unique @map("escrow_id") escrow Escrow? @relation(fields: [escrowId], references: [id]) - + documents Document[] createdAt DateTime @default(now()) @map("created_at") updatedAt DateTime @updatedAt @map("updated_at") @@ -266,3 +269,19 @@ model EventLog { @@map("event_logs") } +model Document { + id String @id @default(cuid()) + fileName String + fileUrl String + publicId String + mimeType String + size Int + uploadedById String + adoptionId String + createdAt DateTime @default(now()) + updatedAt DateTime @updatedAt + + uploadedBy User @relation(fields: [uploadedById], references: [id]) + adoption Adoption @relation(fields: [adoptionId], references: [id]) +} + diff --git a/src/common/interceptors/logging.interceptor.ts b/src/common/interceptors/logging.interceptor.ts index bc28462..b9400f2 100644 --- a/src/common/interceptors/logging.interceptor.ts +++ b/src/common/interceptors/logging.interceptor.ts @@ -5,7 +5,7 @@ import { CallHandler, } from '@nestjs/common'; import { Observable, tap, catchError } from 'rxjs'; -import { LoggingService } from './logging.service'; +import { LoggingService } from '../../logging/logging.service'; import { Request, Response } from 'express'; @Injectable() diff --git a/src/custody/custody.service.ts b/src/custody/custody.service.ts index 12d2622..7775f71 100644 --- a/src/custody/custody.service.ts +++ b/src/custody/custody.service.ts @@ -8,6 +8,7 @@ import { EventsService } from '../events/events.service'; import { EscrowService } from '../escrow/escrow.service'; import { CreateCustodyDto } from './dto/create-custody.dto'; import { CustodyResponseDto } from './dto/custody-response.dto'; +import { CustodyStatus } from '@prisma/client'; @Injectable() export class CustodyService { @@ -113,7 +114,7 @@ export class CustodyService { // Create custody record const custodyRecord = await tx.custody.create({ data: { - status: 'PENDING', + status: CustodyStatus.PENDING, type: 'TEMPORARY', holderId: userId, petId, diff --git a/src/logging/logging.service.spec.ts b/src/logging/logging.service.spec.ts index 35b8391..9c1c0c0 100644 --- a/src/logging/logging.service.spec.ts +++ b/src/logging/logging.service.spec.ts @@ -1,12 +1,31 @@ + import { Test, TestingModule } from '@nestjs/testing'; import { LoggingService } from './logging.service'; +import { PrismaService } from '../prisma/prisma.service'; describe('LoggingService', () => { let service: LoggingService; beforeEach(async () => { const module: TestingModule = await Test.createTestingModule({ - providers: [LoggingService], + providers: [ + LoggingService, + { + provide: PrismaService, + useValue: { + appLog: { + create: jest.fn().mockResolvedValue({ + id: '1', + level: 'INFO', + action: 'TEST', + message: 'Test log', + userId: null, + metadata: null, + }), + }, + }, + }, + ], }).compile(); service = module.get(LoggingService); @@ -15,4 +34,25 @@ describe('LoggingService', () => { it('should be defined', () => { expect(service).toBeDefined(); }); + + it('should create a log entry', async () => { + const result = await service.log({ + level: 'INFO', + action: 'TEST', + message: 'Test log', + }); + expect(result).toBeDefined(); + }); + + it('should return null and not throw when db fails', async () => { + jest.spyOn(service['prisma'].appLog, 'create').mockRejectedValueOnce( + new Error('DB down'), + ); + const result = await service.log({ + level: 'ERROR', + action: 'TEST_FAIL', + message: 'Should not throw', + }); + expect(result).toBeNull(); + }); }); diff --git a/tsconfig.json b/tsconfig.json index 7763cbe..77950b5 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -21,7 +21,8 @@ "noImplicitAny": false, "strictBindCallApply": false, "noFallthroughCasesInSwitch": false, - "typeRoots": ["./node_modules/@types", "./src/types"] + "typeRoots": ["./node_modules/@types", "./src/types"], + "types": ["multer"] }, "watchOptions": { @@ -30,4 +31,3 @@ } -