From c6ffd1b0f7429f394b172c30760e42ce3f4c92e4 Mon Sep 17 00:00:00 2001 From: Vasilica Olariu Date: Fri, 12 Sep 2025 11:07:21 +0300 Subject: [PATCH 1/8] PM-1897 - prisma middleware for audit fields & request context --- src/app.module.ts | 11 ++- src/shared/guards/tokenRoles.guard.ts | 12 +--- src/shared/modules/global/prisma.service.ts | 70 +++++++++++++++++++ .../request/createRequestStore.middleware.ts | 15 ++++ src/shared/request/index.ts | 2 + src/shared/request/requestStore.ts | 41 +++++++++++ .../tokenRequestValidator.middleware.ts | 43 ++++++++++++ 7 files changed, 182 insertions(+), 12 deletions(-) create mode 100644 src/shared/request/createRequestStore.middleware.ts create mode 100644 src/shared/request/index.ts create mode 100644 src/shared/request/requestStore.ts create mode 100644 src/shared/request/tokenRequestValidator.middleware.ts diff --git a/src/app.module.ts b/src/app.module.ts index 599aec8..7122bae 100644 --- a/src/app.module.ts +++ b/src/app.module.ts @@ -1,9 +1,16 @@ -import { Module } from '@nestjs/common'; +import { MiddlewareConsumer, Module } from '@nestjs/common'; import { ApiModule } from './api/api.module'; +import { CreateRequestStoreMiddleware } from './shared/request/createRequestStore.middleware'; +import { TokenValidatorMiddleware } from './shared/request/tokenRequestValidator.middleware'; @Module({ imports: [ApiModule], controllers: [], providers: [], }) -export class AppModule {} +export class AppModule { + configure(consumer: MiddlewareConsumer) { + consumer.apply(TokenValidatorMiddleware).forRoutes('*'); + consumer.apply(CreateRequestStoreMiddleware).forRoutes('*'); + } +} diff --git a/src/shared/guards/tokenRoles.guard.ts b/src/shared/guards/tokenRoles.guard.ts index 8ce26bc..94c4222 100644 --- a/src/shared/guards/tokenRoles.guard.ts +++ b/src/shared/guards/tokenRoles.guard.ts @@ -35,18 +35,10 @@ export class TokenRolesGuard implements CanActivate { } const request = context.switchToHttp().getRequest(); - const authHeader = request.headers['authorization']; - if (!authHeader || !authHeader.startsWith('Bearer ')) { - throw new UnauthorizedException('Invalid or missing token'); - } try { - const token = authHeader.split(' ')[1]; - const user = await this.jwtService.validateToken(token); - - // Add user to request for later use in controllers - request['user'] = user; - + const user = request['user'] ?? {}; + // Check role-based access for regular users if (user.roles && requiredRoles.length > 0) { const hasRole = requiredRoles.some((role) => diff --git a/src/shared/modules/global/prisma.service.ts b/src/shared/modules/global/prisma.service.ts index 25d3bea..e51ed6f 100644 --- a/src/shared/modules/global/prisma.service.ts +++ b/src/shared/modules/global/prisma.service.ts @@ -2,6 +2,73 @@ import { Injectable, OnModuleInit, OnModuleDestroy } from '@nestjs/common'; import { PrismaClient, Prisma } from '@prisma/client'; import { LoggerService } from './logger.service'; import { PrismaErrorService } from './prisma-error.service'; +import { getStore } from 'src/shared/request/requestStore'; + + +enum auditField { + createdBy = 'createdBy', + updatedBy = 'updatedBy', +} + +const modelHasField = (model: string, field: string) => { + const modelObj = Prisma.dmmf?.datamodel?.models?.find((x) => x.name === model); + return modelObj && modelObj.fields?.some((x) => x.name === field); +}; + +const addUserAuditField = (model: string, field: auditField, data: object | Array) => { + const userId = getStore()?.userId; + + if (userId && modelHasField(model, field)) { + if (Array.isArray(data)) { + data.forEach((x) => (x[field] = userId)); + } else { + data[field] = userId; + } + } +}; + +// Prisma client extension for automatically adding creator_id and modifier_id on insert and update operations +const auditFieldsExtension = Prisma.defineExtension({ + query: { + $allModels: { + async create({ model, args, query }) { + addUserAuditField(model, auditField.createdBy, args.data); + addUserAuditField(model, auditField.updatedBy, args.data); + return query(args); + }, + + async createMany({ model, args, query }) { + addUserAuditField(model, auditField.createdBy, args.data); + addUserAuditField(model, auditField.updatedBy, args.data); + return query(args); + }, + + async update({ model, args, query }) { + addUserAuditField(model, auditField.updatedBy, args.data); + return query(args); + }, + + async updateMany({ model, args, query }) { + addUserAuditField(model, auditField.updatedBy, args.data); + return query(args); + }, + + async upsert({ model, args, query }) { + if (args.create) { + addUserAuditField(model, auditField.createdBy, args.create); + addUserAuditField(model, auditField.updatedBy, args.create); + + } + + if (args.update) { + addUserAuditField(model, auditField.updatedBy, args.update); + } + + return query(args); + }, + }, + }, +}); @Injectable() export class PrismaService @@ -60,6 +127,9 @@ export class PrismaService this.$on('error' as never, (e: Prisma.LogEvent) => { this.logger.error(`Prisma Error: ${e.message}`, e.target); }); + + // Extend the client and replace this instance with the extended instance + Object.assign(this, this.$extends(auditFieldsExtension)); } async onModuleInit() { diff --git a/src/shared/request/createRequestStore.middleware.ts b/src/shared/request/createRequestStore.middleware.ts new file mode 100644 index 0000000..f4e84ee --- /dev/null +++ b/src/shared/request/createRequestStore.middleware.ts @@ -0,0 +1,15 @@ +import { Injectable, NestMiddleware } from '@nestjs/common'; +import { Response, NextFunction } from 'express'; +import { RequestMetadata, saveStore } from './requestStore'; + +@Injectable() +export class CreateRequestStoreMiddleware implements NestMiddleware { + constructor() {} + + use(req: any, res: Response, next: NextFunction) { + const requestUserId = req['user'] ? (req['user'].isMachine ? 'System' : `${req['user'].userId}`) : undefined; + const requestMetaData = new RequestMetadata({ userId: requestUserId }); + + saveStore(requestMetaData, next); + } +} diff --git a/src/shared/request/index.ts b/src/shared/request/index.ts new file mode 100644 index 0000000..a812f4f --- /dev/null +++ b/src/shared/request/index.ts @@ -0,0 +1,2 @@ +export * from './createRequestStore.middleware'; +export * from './requestStore'; diff --git a/src/shared/request/requestStore.ts b/src/shared/request/requestStore.ts new file mode 100644 index 0000000..91f741c --- /dev/null +++ b/src/shared/request/requestStore.ts @@ -0,0 +1,41 @@ +import { AsyncLocalStorage } from 'async_hooks'; +import { NextFunction } from 'express'; +import { nanoid } from 'nanoid'; + +// Class for storing request specific metadata +export class RequestMetadata { + requestId: string; + userId?: string; + + // eslint-disable-next-line @typescript-eslint/no-unused-vars, @typescript-eslint/no-empty-object-type + constructor(params: { requestId?: string, userId?: string }) { + this.requestId = params.requestId ?? nanoid(11); + this.userId = params.userId; + } +} + +// Create a AsyncLocalStorage of type RequestMetaData for storing request specific data +const asyncStorage = new AsyncLocalStorage(); + +// Gets the RequestMetadada object associated with the current request +export function getStore(): RequestMetadata { + let store = asyncStorage.getStore(); + if (store === undefined) { + store = new RequestMetadata({ + requestId: '', + }); + } + + return store; +} + +// For use in middleware +// Saves RequestMetadata for the current request +export function saveStore( + requestMetaData: RequestMetadata, + next: NextFunction, +) { + asyncStorage.run(requestMetaData, () => { + next(); + }); +} diff --git a/src/shared/request/tokenRequestValidator.middleware.ts b/src/shared/request/tokenRequestValidator.middleware.ts new file mode 100644 index 0000000..0bbdb45 --- /dev/null +++ b/src/shared/request/tokenRequestValidator.middleware.ts @@ -0,0 +1,43 @@ +import { + Injectable, + NestMiddleware, + UnauthorizedException, +} from '@nestjs/common'; +import { JwtService } from '../modules/global/jwt.service'; +import { LoggerService } from '../modules/global/logger.service'; + +@Injectable() +export class TokenValidatorMiddleware implements NestMiddleware { + private readonly logger: LoggerService; + + constructor(private jwtService: JwtService) { + this.logger = LoggerService.forRoot('Auth/TokenValidatorMiddleware'); + } + + async use(request: any, res: Response, next: (error?: any) => void) { + const authHeader = request.headers.authorization; + if (!authHeader) { + return next(); + } + + const [type, idToken] = request.headers.authorization.split(' ') ?? []; + + if (type !== 'Bearer' || !idToken) { + throw new UnauthorizedException('Invalid or missing JWT!'); + } + + let decoded: any; + try { + decoded = await this.jwtService.validateToken(idToken); + } catch (error) { + this.logger.error('Error verifying JWT', error); + throw new UnauthorizedException('Invalid or expired JWT!'); + } + + // Add user to request for later use in controllers + request['user'] = decoded; + request.idTokenVerified = true; + + return next(); + } +} From 665c7dd831758ad67186fe7cd96f41dbebb63125 Mon Sep 17 00:00:00 2001 From: Vasilica Olariu Date: Fri, 12 Sep 2025 11:17:20 +0300 Subject: [PATCH 2/8] PM-1897 - Update scorecard service: remove manual audit fields --- src/api/scorecard/scorecard.service.ts | 91 +++++++++++++++----------- src/dto/scorecard.dto.ts | 35 ---------- 2 files changed, 54 insertions(+), 72 deletions(-) diff --git a/src/api/scorecard/scorecard.service.ts b/src/api/scorecard/scorecard.service.ts index 3dd6453..442d3c3 100644 --- a/src/api/scorecard/scorecard.service.ts +++ b/src/api/scorecard/scorecard.service.ts @@ -10,7 +10,6 @@ import { scorecardSection, } from '@prisma/client'; import { - mapScorecardRequestForCreate, ScorecardGroupBaseDto, ScorecardPaginatedResponseDto, ScorecardQuestionBaseDto, @@ -67,25 +66,46 @@ export class ScoreCardService { user: JwtUser, ): Promise { try { - const data = await this.prisma.scorecard.create({ - data: { - ...(mapScorecardRequestForCreate({ - ...body, - createdBy: user.isMachine ? 'System' : (user.userId as string), - updatedBy: user.isMachine ? 'System' : (user.userId as string), - }) as any), - }, - include: { - scorecardGroups: { - include: { - sections: { - include: { - questions: true, - }, - }, - }, - }, - }, + const data = await this.prisma.$transaction(async (tx) => { + const scorecard = await tx.scorecard.create({ + data: { ...omit(body, ['id', 'scorecardGroups']) }, + }); + this.logger.log( + `[updateScorecard] Created empty scorecard: ${JSON.stringify(scorecard)}`, + ); + + const scorecardGroups = await Promise.all(body.scorecardGroups.map(async (scorecardGroupData) => { + const group = await tx.scorecardGroup.create({ + data: { + ...omit(scorecardGroupData, ['id', 'sections']), + scorecardId: scorecard.id, + } + }); + + const sections = await Promise.all(scorecardGroupData.sections.map(async (scorecardSectionData) => { + const section = await tx.scorecardSection.create({ + data: { + ...omit(scorecardSectionData, ['id', 'questions']), + scorecardGroupId: group.id, + } + }); + + const questions = await Promise.all(scorecardSectionData.questions.map(async (scorecardQuestionData) => { + return await tx.scorecardQuestion.create({ + data: { + ...omit(scorecardQuestionData, ['id', 'questions']), + scorecardSectionId: section.id, + } + }); + })); + + return { ...section, questions }; + })); + + return { ...group, sections }; + })); + + return { ...scorecard, scorecardGroups }; }); return data as unknown as ScorecardWithGroupResponseDto; @@ -130,7 +150,7 @@ export class ScoreCardService { `[updateScorecard] Updating scorecard with id: ${scorecardId}`, ); - return await this.prisma.$transaction(async (tx) => { + await this.prisma.$transaction(async (tx) => { // Update scorecard basic info const updatedScorecard = await tx.scorecard.update({ where: { id: scorecardId }, @@ -183,22 +203,22 @@ export class ScoreCardService { this.logger.log( `[updateScorecard] Finished syncing groups, sections, and questions for scorecard ${scorecardId}`, ); + }); - return this.prisma.scorecard.findUnique({ - where: { id: scorecardId }, - include: { - scorecardGroups: { - include: { - sections: { - include: { - questions: true, - }, + return this.prisma.scorecard.findUnique({ + where: { id: scorecardId }, + include: { + scorecardGroups: { + include: { + sections: { + include: { + questions: true, }, }, }, }, - }) as Promise; - }); + }, + }) as Promise; } catch (error) { if (error instanceof NotFoundException) { throw error; @@ -243,8 +263,7 @@ export class ScoreCardService { await model.update({ where: { id: child.id }, data: { - ...omit(child, ['sections', 'questions']), - updatedBy: userId, + ...omit(child, ['id', 'sections', 'questions']), }, }); } else { @@ -253,10 +272,8 @@ export class ScoreCardService { ); const created = await model.create({ data: { - ...omit(child, ['sections', 'questions']), + ...omit(child, ['id', 'sections', 'questions']), [parentField]: parentId, - createdBy: userId, - updatedBy: userId, }, }); child.id = created.id; // assign id back to input diff --git a/src/dto/scorecard.dto.ts b/src/dto/scorecard.dto.ts index 02679fa..7de6a59 100644 --- a/src/dto/scorecard.dto.ts +++ b/src/dto/scorecard.dto.ts @@ -388,38 +388,3 @@ export class ScorecardPaginatedResponseDto { }) scoreCards: ScorecardResponseDto[]; } - -export function mapScorecardRequestForCreate(request: ScorecardRequestDto) { - const userFields = { - ...(request.createdBy ? { createdBy: request.createdBy } : {}), - updatedBy: request.updatedBy, - }; - - return { - ...request, - id: undefined, - ...userFields, - scorecardGroups: { - create: request.scorecardGroups.map((group) => ({ - ...group, - id: undefined, - ...userFields, - sections: { - create: group.sections.map((section) => ({ - ...section, - id: undefined, - ...userFields, - questions: { - create: section.questions.map((question) => ({ - ...question, - id: undefined, - sortOrder: 1, - ...userFields, - })), - }, - })), - }, - })), - }, - }; -} From 12d9441937d4518ea6cf690b1a97af3f9f1685e5 Mon Sep 17 00:00:00 2001 From: Vasilica Olariu Date: Fri, 12 Sep 2025 11:43:44 +0300 Subject: [PATCH 3/8] PM-1897 - update submission service --- src/api/submission/submission.service.ts | 4 ---- 1 file changed, 4 deletions(-) diff --git a/src/api/submission/submission.service.ts b/src/api/submission/submission.service.ts index 05085b7..d8e45c9 100644 --- a/src/api/submission/submission.service.ts +++ b/src/api/submission/submission.service.ts @@ -131,8 +131,6 @@ export class SubmissionService { ...body, status: SubmissionStatus.ACTIVE, type: body.type as SubmissionType, - createdBy: String(authUser.userId) || '', - createdAt: new Date(), }, }); this.logger.log(`Submission created with ID: ${data.id}`); @@ -278,8 +276,6 @@ export class SubmissionService { data: { ...body, type: (body.type as SubmissionType) || existing.type, - updatedBy: String(authUser.userId) || '', - updatedAt: new Date(), }, }); this.logger.log(`Submission updated successfully: ${submissionId}`); From e6754fc33a92cf61026257456cb940142b9d031d Mon Sep 17 00:00:00 2001 From: Vasilica Olariu Date: Fri, 12 Sep 2025 16:14:17 +0300 Subject: [PATCH 4/8] PM-1897 - remove manual handling of created & updated by --- .../migration.sql | 81 +++++++++++++++++++ prisma/schema.prisma | 76 ++++++++--------- src/api/appeal/appeal.controller.ts | 5 +- src/api/contact/contactRequests.controller.ts | 3 +- .../reviewApplication.service.ts | 2 - .../reviewOpportunity.service.ts | 2 - .../review-summation.service.ts | 5 -- src/api/scorecard/scorecard.service.ts | 3 + src/dto/appeal.dto.ts | 18 ----- src/dto/contactRequest.dto.ts | 36 --------- src/dto/reviewSummation.dto.ts | 2 +- src/dto/scorecard.dto.ts | 8 -- 12 files changed, 126 insertions(+), 115 deletions(-) create mode 100644 prisma/migrations/20250912082808_make_audit_fields_optional/migration.sql diff --git a/prisma/migrations/20250912082808_make_audit_fields_optional/migration.sql b/prisma/migrations/20250912082808_make_audit_fields_optional/migration.sql new file mode 100644 index 0000000..d7475c7 --- /dev/null +++ b/prisma/migrations/20250912082808_make_audit_fields_optional/migration.sql @@ -0,0 +1,81 @@ +-- -- AlterTable +-- ALTER TABLE "aiWorkflow" ALTER COLUMN "createdBy" DROP NOT NULL, +-- ALTER COLUMN "updatedBy" DROP NOT NULL; + +-- -- AlterTable +-- ALTER TABLE "aiWorkflowRunItem" ALTER COLUMN "createdBy" DROP NOT NULL; + +-- -- AlterTable +-- ALTER TABLE "aiWorkflowRunItemComment" ALTER COLUMN "createdBy" DROP NOT NULL, +-- ALTER COLUMN "updatedBy" DROP NOT NULL; + +-- -- AlterTable +-- ALTER TABLE "appeal" ALTER COLUMN "createdBy" DROP NOT NULL, +-- ALTER COLUMN "updatedBy" DROP NOT NULL; + +-- -- AlterTable +-- ALTER TABLE "appealResponse" ALTER COLUMN "createdBy" DROP NOT NULL, +-- ALTER COLUMN "updatedBy" DROP NOT NULL; + +-- -- AlterTable +-- ALTER TABLE "challengeResult" ALTER COLUMN "createdBy" DROP NOT NULL, +-- ALTER COLUMN "updatedBy" DROP NOT NULL; + +-- -- AlterTable +-- ALTER TABLE "contactRequest" ALTER COLUMN "createdBy" DROP NOT NULL, +-- ALTER COLUMN "updatedBy" DROP NOT NULL; + +-- -- AlterTable +-- ALTER TABLE "llmModel" ALTER COLUMN "createdBy" DROP NOT NULL; + +-- -- AlterTable +-- ALTER TABLE "llmProvider" ALTER COLUMN "createdBy" DROP NOT NULL; + +-- -- AlterTable +-- ALTER TABLE "resourceSubmission" ALTER COLUMN "createdBy" DROP NOT NULL; + +-- -- AlterTable +-- ALTER TABLE "review" ALTER COLUMN "createdBy" DROP NOT NULL, +-- ALTER COLUMN "updatedBy" DROP NOT NULL; + +-- -- AlterTable +-- ALTER TABLE "reviewApplication" ALTER COLUMN "createdBy" DROP NOT NULL, +-- ALTER COLUMN "updatedBy" DROP NOT NULL; + +-- -- AlterTable +-- ALTER TABLE "reviewItem" ALTER COLUMN "createdBy" DROP NOT NULL, +-- ALTER COLUMN "updatedBy" DROP NOT NULL; + +-- -- AlterTable +-- ALTER TABLE "reviewItemComment" ALTER COLUMN "createdBy" DROP NOT NULL, +-- ALTER COLUMN "updatedBy" DROP NOT NULL; + +-- -- AlterTable +-- ALTER TABLE "reviewOpportunity" ALTER COLUMN "createdBy" DROP NOT NULL, +-- ALTER COLUMN "updatedBy" DROP NOT NULL; + +-- -- AlterTable +-- ALTER TABLE "reviewSummation" ALTER COLUMN "createdBy" DROP NOT NULL, +-- ALTER COLUMN "updatedBy" DROP NOT NULL; + +-- -- AlterTable +-- ALTER TABLE "scorecard" ALTER COLUMN "createdBy" DROP NOT NULL, +-- ALTER COLUMN "updatedBy" DROP NOT NULL; + +-- -- AlterTable +-- ALTER TABLE "scorecardGroup" ALTER COLUMN "createdBy" DROP NOT NULL, +-- ALTER COLUMN "updatedBy" DROP NOT NULL; + +-- -- AlterTable +-- ALTER TABLE "scorecardQuestion" ALTER COLUMN "createdBy" DROP NOT NULL, +-- ALTER COLUMN "updatedBy" DROP NOT NULL; + +-- -- AlterTable +-- ALTER TABLE "scorecardSection" ALTER COLUMN "createdBy" DROP NOT NULL, +-- ALTER COLUMN "updatedBy" DROP NOT NULL; + +-- -- AlterTable +-- ALTER TABLE "submission" ALTER COLUMN "createdBy" DROP NOT NULL; + +-- -- AlterTable +-- ALTER TABLE "upload" ALTER COLUMN "createdBy" DROP NOT NULL; diff --git a/prisma/schema.prisma b/prisma/schema.prisma index 656ea48..436dc6e 100644 --- a/prisma/schema.prisma +++ b/prisma/schema.prisma @@ -26,9 +26,9 @@ model scorecard { minScore Float maxScore Float createdAt DateTime @default(now()) - createdBy String + createdBy String? updatedAt DateTime @updatedAt - updatedBy String + updatedBy String? scorecardGroups scorecardGroup[] reviews review[] @@ -75,9 +75,9 @@ model scorecardGroup { weight Float sortOrder Int createdAt DateTime @default(now()) - createdBy String + createdBy String? updatedAt DateTime @updatedAt - updatedBy String + updatedBy String? scorecard scorecard @relation(fields: [scorecardId], references: [id], onDelete: Cascade) sections scorecardSection[] @@ -95,9 +95,9 @@ model scorecardSection { weight Float sortOrder Int createdAt DateTime @default(now()) - createdBy String + createdBy String? updatedAt DateTime @updatedAt - updatedBy String + updatedBy String? group scorecardGroup @relation(fields: [scorecardGroupId], references: [id], onDelete: Cascade) questions scorecardQuestion[] @@ -125,9 +125,9 @@ model scorecardQuestion { requiresUpload Boolean sortOrder Int createdAt DateTime @default(now()) - createdBy String + createdBy String? updatedAt DateTime @updatedAt - updatedBy String + updatedBy String? scaleMin Int? // Minimum value for scale (used when type is SCALE) scaleMax Int? // Maximum value for scale (used when type is SCALE) @@ -167,9 +167,9 @@ model review { status ReviewStatus? reviewDate DateTime? createdAt DateTime @default(now()) - createdBy String + createdBy String? updatedAt DateTime @updatedAt - updatedBy String + updatedBy String? scorecard scorecard @relation(fields: [scorecardId], references: [id], onDelete: Cascade) submission submission? @relation(fields: [submissionId], references: [id], onDelete: Cascade) @@ -193,9 +193,9 @@ model reviewItem { finalAnswer String? managerComment String? createdAt DateTime @default(now()) - createdBy String + createdBy String? updatedAt DateTime @updatedAt - updatedBy String + updatedBy String? review review @relation(fields: [reviewId], references: [id], onDelete: Cascade) reviewItemComments reviewItemComment[] @@ -214,9 +214,9 @@ model reviewItemComment { type ReviewItemCommentType sortOrder Int @default(0) createdAt DateTime @default(now()) - createdBy String + createdBy String? updatedAt DateTime @updatedAt - updatedBy String + updatedBy String? reviewItem reviewItem @relation(fields: [reviewItemId], references: [id], onDelete: Cascade) appeal appeal? @@ -249,9 +249,9 @@ model appeal { reviewItemCommentId String @unique // Ensure one-to-one relationship content String createdAt DateTime @default(now()) - createdBy String + createdBy String? updatedAt DateTime @updatedAt - updatedBy String + updatedBy String? reviewItemComment reviewItemComment @relation(fields: [reviewItemCommentId], references: [id], onDelete: Cascade) // Define fields and references here appealResponse appealResponse? @@ -269,9 +269,9 @@ model appealResponse { content String success Boolean createdAt DateTime @default(now()) - createdBy String + createdBy String? updatedAt DateTime @updatedAt - updatedBy String + updatedBy String? appeal appeal @relation(fields: [appealId], references: [id], onDelete: Cascade) @@ -297,9 +297,9 @@ model challengeResult { ratingOrder Int? createdAt DateTime @default(now()) - createdBy String + createdBy String? updatedAt DateTime @updatedAt - updatedBy String + updatedBy String? @@id([challengeId, userId]) @@index([challengeId]) // Index for filtering by challenge @@ -313,9 +313,9 @@ model contactRequest { challengeId String // Associated challenge message String // Markdown content createdAt DateTime @default(now()) - createdBy String + createdBy String? updatedAt DateTime @updatedAt - updatedBy String + updatedBy String? @@index([id]) // Index for direct ID lookups @@index([resourceId]) // Index for filtering by resource (requester) @@ -344,9 +344,9 @@ model reviewSummation { isFinal Boolean? reviewedDate DateTime? createdAt DateTime @default(now()) - createdBy String + createdBy String? updatedAt DateTime @updatedAt - updatedBy String + updatedBy String? submission submission @relation(fields: [submissionId], references: [id], onDelete: Cascade) @@ -383,7 +383,7 @@ model submission { submittedDate DateTime? createdAt DateTime @default(now()) - createdBy String + createdBy String? updatedAt DateTime? @updatedAt updatedBy String? @@ -449,9 +449,9 @@ model reviewOpportunity { applications reviewApplication[] createdAt DateTime @default(now()) - createdBy String + createdBy String? updatedAt DateTime @updatedAt - updatedBy String + updatedBy String? @@unique([challengeId, type]) @@index([id]) // Index for direct ID lookups @@ -469,9 +469,9 @@ model reviewApplication { opportunity reviewOpportunity @relation(fields: [opportunityId], references: [id], onDelete: Cascade) createdAt DateTime @default(now()) - createdBy String + createdBy String? updatedAt DateTime @updatedAt - updatedBy String + updatedBy String? @@unique([opportunityId, userId, role]) @@index([id]) // Index for direct ID lookups @@ -521,7 +521,7 @@ model upload { projectPhaseId String? createdAt DateTime @default(now()) - createdBy String + createdBy String? updatedAt DateTime? @updatedAt updatedBy String? @@ -538,7 +538,7 @@ model resourceSubmission { legacySubmissionId String? createdAt DateTime @default(now()) - createdBy String + createdBy String? updatedAt DateTime? @updatedAt updatedBy String? @@ -561,7 +561,7 @@ model llmProvider { id String @id @default(dbgenerated("nanoid()")) @db.VarChar(14) name String @unique @db.VarChar createdAt DateTime @db.Timestamp(3) - createdBy String @db.Text + createdBy String? @db.Text models llmModel[] } @@ -574,7 +574,7 @@ model llmModel { icon String? @db.VarChar url String? @db.VarChar createdAt DateTime @default(now()) @db.Timestamp(3) - createdBy String @db.Text + createdBy String? @db.Text provider llmProvider @relation(fields: [providerId], references: [id]) workflows aiWorkflow[] @@ -590,9 +590,9 @@ model aiWorkflow { gitOwner String @db.VarChar scorecardId String @db.VarChar(14) createdAt DateTime @default(now()) @db.Timestamp(3) - createdBy String @db.Text + createdBy String? @db.Text updatedAt DateTime @db.Timestamp(3) - updatedBy String @db.Text + updatedBy String? @db.Text llm llmModel @relation(fields: [llmId], references: [id]) scorecard scorecard @relation(fields: [scorecardId], references: [id]) @@ -623,7 +623,7 @@ model aiWorkflowRunItem { downVotes Int @default(0) questionScore Float? @db.DoublePrecision createdAt DateTime @db.Timestamp(3) - createdBy String @db.Text + createdBy String? @db.Text run aiWorkflowRun @relation(fields: [workflowRunId], references: [id]) question scorecardQuestion @relation(fields: [scorecardQuestionId], references: [id]) @@ -637,9 +637,9 @@ model aiWorkflowRunItemComment { content String @db.Text parentId String? @db.VarChar(14) createdAt DateTime @default(now()) @db.Timestamp(3) - createdBy String @db.Text + createdBy String? @db.Text updatedAt DateTime @db.Timestamp(3) - updatedBy String @db.Text + updatedBy String? @db.Text item aiWorkflowRunItem @relation(fields: [workflowRunItemId], references: [id]) parent aiWorkflowRunItemComment? @relation("CommentHierarchy", fields: [parentId], references: [id]) diff --git a/src/api/appeal/appeal.controller.ts b/src/api/appeal/appeal.controller.ts index a32ff02..b85f0ff 100644 --- a/src/api/appeal/appeal.controller.ts +++ b/src/api/appeal/appeal.controller.ts @@ -29,7 +29,6 @@ import { AppealResponseDto, AppealResponseRequestDto, AppealResponseResponseDto, - mapAppealRequestToDto, mapAppealResponseRequestToDto, } from 'src/dto/appeal.dto'; import { PrismaService } from '../../shared/modules/global/prisma.service'; @@ -103,7 +102,7 @@ export class AppealController { await this.challengeApiService.validateAppealSubmission(challengeId); const data = await this.prisma.appeal.create({ - data: mapAppealRequestToDto(body), + data: { ...body }, }); this.logger.log(`Appeal created with ID: ${data.id}`); return data as AppealResponseDto; @@ -156,7 +155,7 @@ export class AppealController { try { const data = await this.prisma.appeal.update({ where: { id: appealId }, - data: mapAppealRequestToDto(body), + data: { ...body }, }); this.logger.log(`Appeal updated successfully: ${appealId}`); return data as AppealResponseDto; diff --git a/src/api/contact/contactRequests.controller.ts b/src/api/contact/contactRequests.controller.ts index d258d09..acea9ac 100644 --- a/src/api/contact/contactRequests.controller.ts +++ b/src/api/contact/contactRequests.controller.ts @@ -19,7 +19,6 @@ import { Scope } from 'src/shared/enums/scopes.enum'; import { ContactRequestDto, ContactRequestResponseDto, - mapContactRequestToDto, } from 'src/dto/contactRequest.dto'; import { PrismaService } from '../../shared/modules/global/prisma.service'; import { ResourceApiService } from 'src/shared/modules/global/resource.service'; @@ -73,7 +72,7 @@ export class ContactRequestsController { ); const data = await this.prisma.contactRequest.create({ - data: mapContactRequestToDto(body), + data: { ...body }, }); this.logger.log(`Contact request created with ID: ${data.id}`); diff --git a/src/api/review-application/reviewApplication.service.ts b/src/api/review-application/reviewApplication.service.ts index 74e005c..f37fb57 100644 --- a/src/api/review-application/reviewApplication.service.ts +++ b/src/api/review-application/reviewApplication.service.ts @@ -83,8 +83,6 @@ export class ReviewApplicationService { status: ReviewApplicationStatus.PENDING, userId, handle, - createdBy: userId, - updatedBy: userId, }, }); return this.buildResponse(entity); diff --git a/src/api/review-opportunity/reviewOpportunity.service.ts b/src/api/review-opportunity/reviewOpportunity.service.ts index 34e14d0..1281f48 100644 --- a/src/api/review-opportunity/reviewOpportunity.service.ts +++ b/src/api/review-opportunity/reviewOpportunity.service.ts @@ -168,8 +168,6 @@ export class ReviewOpportunityService { const entity = await this.prisma.reviewOpportunity.create({ data: { ...dto, - createdBy: authUser.userId ?? '', - updatedBy: authUser.userId ?? '', }, }); return this.buildResponse(entity, challengeData); diff --git a/src/api/review-summation/review-summation.service.ts b/src/api/review-summation/review-summation.service.ts index b817be8..e9ee0c9 100644 --- a/src/api/review-summation/review-summation.service.ts +++ b/src/api/review-summation/review-summation.service.ts @@ -30,9 +30,6 @@ export class ReviewSummationService { const data = await this.prisma.reviewSummation.create({ data: { ...body, - createdBy: String(authUser.userId) || '', - createdAt: new Date(), - updatedBy: String(authUser.userId) || '', }, }); this.logger.log(`Review summation created with ID: ${data.id}`); @@ -163,8 +160,6 @@ export class ReviewSummationService { where: { id }, data: { ...body, - updatedBy: String(authUser.userId) || '', - updatedAt: new Date(), }, }); this.logger.log(`Review type updated successfully: ${id}`); diff --git a/src/api/scorecard/scorecard.service.ts b/src/api/scorecard/scorecard.service.ts index 442d3c3..80cfcc5 100644 --- a/src/api/scorecard/scorecard.service.ts +++ b/src/api/scorecard/scorecard.service.ts @@ -471,6 +471,9 @@ export class ScoreCardService { }); } + // DO NOT remove these! + // prisma middleware is not recuresively parsing the data object + // groups, sections & questions will not be automatically updated with audit fields const auditFields = { createdBy: user.isMachine ? 'System' : (user.userId as string), updatedBy: user.isMachine ? 'System' : (user.userId as string), diff --git a/src/dto/appeal.dto.ts b/src/dto/appeal.dto.ts index 049bf97..8d34840 100644 --- a/src/dto/appeal.dto.ts +++ b/src/dto/appeal.dto.ts @@ -128,32 +128,14 @@ export class AppealResponseDto extends AppealBaseDto { updatedAt?: Date; } -export function mapAppealRequestToDto(request: AppealRequestDto) { - const userFields = { - createdBy: '', - updatedBy: '', - }; - - return { - ...request, - ...userFields, - }; -} - export function mapAppealResponseRequestToDto( request: AppealResponseRequestDto, ) { - const userFields = { - createdBy: '', - updatedBy: '', - }; - // Remove appealId when creating through relationship - Prisma sets it automatically // eslint-disable-next-line @typescript-eslint/no-unused-vars const { appealId, ...requestWithoutAppealId } = request; return { ...requestWithoutAppealId, - ...userFields, }; } diff --git a/src/dto/contactRequest.dto.ts b/src/dto/contactRequest.dto.ts index ffa6f2d..de2d21d 100644 --- a/src/dto/contactRequest.dto.ts +++ b/src/dto/contactRequest.dto.ts @@ -29,40 +29,4 @@ export class ContactRequestResponseDto extends ContactRequestBaseDto { example: 'abc123', }) id: string; - - @ApiProperty({ - description: 'The user who created the request', - example: 'user123', - }) - createdBy: string; - - @ApiProperty({ - description: 'The timestamp when the request was created', - example: '2023-02-10T00:00:00Z', - }) - createdAt: Date; - - @ApiProperty({ - description: 'The timestamp when the request was last updated', - example: '2023-02-10T00:00:00Z', - }) - updatedAt: Date; - - @ApiProperty({ - description: 'The user who last updated the request', - example: 'user456', - }) - updatedBy: string; -} - -export function mapContactRequestToDto(request: ContactRequestDto) { - const userFields = { - createdBy: '', - updatedBy: '', - }; - - return { - ...request, - ...userFields, - }; } diff --git a/src/dto/reviewSummation.dto.ts b/src/dto/reviewSummation.dto.ts index 6527788..1f94eb5 100644 --- a/src/dto/reviewSummation.dto.ts +++ b/src/dto/reviewSummation.dto.ts @@ -211,7 +211,7 @@ export class ReviewSummationResponseDto { description: 'The user who created the review summation', example: 'user123', }) - createdBy: string; + createdBy: string | null; @ApiProperty({ description: 'The last update timestamp', diff --git a/src/dto/scorecard.dto.ts b/src/dto/scorecard.dto.ts index 7de6a59..00f4635 100644 --- a/src/dto/scorecard.dto.ts +++ b/src/dto/scorecard.dto.ts @@ -265,14 +265,6 @@ export class ScorecardBaseDto { @Max(100) @IsGreaterThan('minScore') maxScore: number; - - /** - * These shouldn't be editable via API - */ - createdAt: Date; - createdBy: string; - updatedAt: Date; - updatedBy: string; } export class ScorecardBaseWithGroupsDto extends ScorecardBaseDto { From c85d1a927f2fe1a9f0338cd42db7e8c1c9636dd7 Mon Sep 17 00:00:00 2001 From: Vasilica Olariu Date: Fri, 12 Sep 2025 16:20:21 +0300 Subject: [PATCH 5/8] lint --- src/shared/request/tokenRequestValidator.middleware.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/shared/request/tokenRequestValidator.middleware.ts b/src/shared/request/tokenRequestValidator.middleware.ts index 0bbdb45..24f7aae 100644 --- a/src/shared/request/tokenRequestValidator.middleware.ts +++ b/src/shared/request/tokenRequestValidator.middleware.ts @@ -17,7 +17,7 @@ export class TokenValidatorMiddleware implements NestMiddleware { async use(request: any, res: Response, next: (error?: any) => void) { const authHeader = request.headers.authorization; if (!authHeader) { - return next(); + return next(); } const [type, idToken] = request.headers.authorization.split(' ') ?? []; From 08c0ca21bfe2438b6f300930aa73e18c2c20e339 Mon Sep 17 00:00:00 2001 From: Vasilica Olariu Date: Fri, 12 Sep 2025 18:32:07 +0300 Subject: [PATCH 6/8] PM-1897 - recursively check update & create data objects to see if there are deeply nested objects, and add audit fields to those as well --- src/shared/modules/global/prisma.service.ts | 98 +++++++++++++++++++-- 1 file changed, 92 insertions(+), 6 deletions(-) diff --git a/src/shared/modules/global/prisma.service.ts b/src/shared/modules/global/prisma.service.ts index e51ed6f..15354b6 100644 --- a/src/shared/modules/global/prisma.service.ts +++ b/src/shared/modules/global/prisma.service.ts @@ -4,30 +4,117 @@ import { LoggerService } from './logger.service'; import { PrismaErrorService } from './prisma-error.service'; import { getStore } from 'src/shared/request/requestStore'; - enum auditField { createdBy = 'createdBy', updatedBy = 'updatedBy', } +/** + * Checks if a given Prisma model contains a specific field. + * + * @param model - The name of the Prisma model to search for. + * @param field - The name of the field to check within the model. + * @returns `true` if the model contains the specified field, otherwise `false`. + */ const modelHasField = (model: string, field: string) => { - const modelObj = Prisma.dmmf?.datamodel?.models?.find((x) => x.name === model); + const modelObj = Prisma.dmmf?.datamodel?.models?.find( + (x) => x.name === model, + ); return modelObj && modelObj.fields?.some((x) => x.name === field); }; -const addUserAuditField = (model: string, field: auditField, data: object | Array) => { +/** + * Retrieves the type of a specified field from a given Prisma model. + * + * @param model - The name of the Prisma model to search for. + * @param field - The name of the field within the model whose type is to be retrieved. + * @returns The type of the specified field as a string, or `undefined` if the model or field does not exist. + */ +const getFieldType = (model: string, field: string) => { + const modelObj = Prisma.dmmf?.datamodel?.models?.find( + (x) => x.name === model, + ); + return modelObj && modelObj.fields?.find((x) => x.name === field)?.type; +}; + +/** + * Checks an object's properties for nested 'update' or 'create' operations, + * and applies the `addUserAuditField` function to those operations. + * + * Iterates over the object's entries, filtering for values that are objects + * containing either an 'update' or 'create' key. For each matching entry, + * it determines the field type using `getFieldType`, and then calls + * `addUserAuditField` with the appropriate parameters. + * + * @param model - The name of the model being audited. + * @param field - The audit field information. + * @param obj - The object to inspect for nested update or create operations. + */ +const checkForNestedUpdateCreateOps = ( + model: string, + field: auditField, + obj: object, +) => { + return Object.entries(obj) + .filter( + ([key, value]) => + value && + typeof value === 'object' && + ('update' in value || 'create' in value) && + getFieldType(model, key), + ) + .forEach(([key, value]) => + addUserAuditField( + getFieldType(model, key)!, + field, + (value.create ?? value.update) as object, + ), + ); +}; + +/** + * Adds a user audit field to the provided data object(s) if the current user ID is available + * and the specified model contains the audit field. Handles both single objects and arrays of objects. + * Also checks for nested update/create operations within the data. + * + * @param model - The name of the model to check for the audit field. + * @param field - The audit field to add (e.g., createdBy, updatedBy). + * @param data - The object or array of objects to which the audit field should be added. + */ +const addUserAuditField = ( + model: string, + field: auditField, + data: object | Array, +) => { const userId = getStore()?.userId; if (userId && modelHasField(model, field)) { if (Array.isArray(data)) { - data.forEach((x) => (x[field] = userId)); + data.forEach((x) => { + x[field] = userId; + checkForNestedUpdateCreateOps(model, field, x); + }); } else { data[field] = userId; + checkForNestedUpdateCreateOps(model, field, data); } } }; -// Prisma client extension for automatically adding creator_id and modifier_id on insert and update operations +/** + * Prisma extension that automatically adds audit fields (`createdBy`, `updatedBy`) + * to all models during create, update, and upsert operations. + * + * - On `create` and `createMany`, both `createdBy` and `updatedBy` fields are set. + * - On `update` and `updateMany`, only the `updatedBy` field is set. + * - On `upsert`, sets `createdBy` and `updatedBy` on creation, and `updatedBy` on update. + * + * This extension relies on the `addUserAuditField` helper to inject audit information + * into the model's data payload before executing the query. + * + * @see addUserAuditField + * @see auditField + */ const auditFieldsExtension = Prisma.defineExtension({ query: { $allModels: { @@ -57,7 +144,6 @@ const auditFieldsExtension = Prisma.defineExtension({ if (args.create) { addUserAuditField(model, auditField.createdBy, args.create); addUserAuditField(model, auditField.updatedBy, args.create); - } if (args.update) { From 4248e5da64fd52ca5f756d61418e188b2cfe8abf Mon Sep 17 00:00:00 2001 From: Vasilica Olariu Date: Fri, 12 Sep 2025 18:33:29 +0300 Subject: [PATCH 7/8] Cleanup manual update for audit fields --- prisma/schema.prisma | 4 +- src/api/ai-workflow/ai-workflow.service.ts | 4 - .../reviewApplication.controller.ts | 18 ++--- .../reviewApplication.service.ts | 15 +--- .../reviewOpportunity.controller.ts | 4 +- .../reviewOpportunity.service.ts | 5 +- src/api/scorecard/scorecard.controller.ts | 10 +-- src/api/scorecard/scorecard.service.ts | 79 +++++-------------- src/dto/review.dto.ts | 23 ------ src/dto/scorecard.dto.ts | 26 ++++++ 10 files changed, 61 insertions(+), 127 deletions(-) diff --git a/prisma/schema.prisma b/prisma/schema.prisma index 436dc6e..e52368b 100644 --- a/prisma/schema.prisma +++ b/prisma/schema.prisma @@ -591,7 +591,7 @@ model aiWorkflow { scorecardId String @db.VarChar(14) createdAt DateTime @default(now()) @db.Timestamp(3) createdBy String? @db.Text - updatedAt DateTime @db.Timestamp(3) + updatedAt DateTime @updatedAt updatedBy String? @db.Text llm llmModel @relation(fields: [llmId], references: [id]) @@ -638,7 +638,7 @@ model aiWorkflowRunItemComment { parentId String? @db.VarChar(14) createdAt DateTime @default(now()) @db.Timestamp(3) createdBy String? @db.Text - updatedAt DateTime @db.Timestamp(3) + updatedAt DateTime @updatedAt updatedBy String? @db.Text item aiWorkflowRunItem @relation(fields: [workflowRunItemId], references: [id]) diff --git a/src/api/ai-workflow/ai-workflow.service.ts b/src/api/ai-workflow/ai-workflow.service.ts index 8fba6b1..e03de2e 100644 --- a/src/api/ai-workflow/ai-workflow.service.ts +++ b/src/api/ai-workflow/ai-workflow.service.ts @@ -61,10 +61,6 @@ export class AiWorkflowService { name, scorecardId, llmId, - // TODO: This has to be removed once the prisma middleware is implemented - createdBy: '', - updatedAt: new Date(), - updatedBy: '', }, }); } diff --git a/src/api/review-application/reviewApplication.controller.ts b/src/api/review-application/reviewApplication.controller.ts index ec9502a..9d54383 100644 --- a/src/api/review-application/reviewApplication.controller.ts +++ b/src/api/review-application/reviewApplication.controller.ts @@ -145,9 +145,8 @@ export class ReviewApplicationController { @ApiResponse({ status: 500, description: 'Internal Error' }) @Roles(UserRole.Admin) @Patch('/:id/accept') - async approveApplication(@Req() req: Request, @Param('id') id: string) { - const authUser: JwtUser = req['user'] as JwtUser; - await this.service.approve(authUser, id); + async approveApplication(@Param('id') id: string) { + await this.service.approve(id); return OkResponse({}); } @@ -170,9 +169,8 @@ export class ReviewApplicationController { @ApiResponse({ status: 500, description: 'Internal Error' }) @Roles(UserRole.Admin) @Patch('/:id/reject') - async rejectApplication(@Req() req: Request, @Param('id') id: string) { - const authUser: JwtUser = req['user'] as JwtUser; - await this.service.reject(authUser, id); + async rejectApplication(@Param('id') id: string) { + await this.service.reject(id); return OkResponse({}); } @@ -195,12 +193,8 @@ export class ReviewApplicationController { @ApiResponse({ status: 500, description: 'Internal Error' }) @Roles(UserRole.Admin) @Patch('/opportunity/:opportunityId/reject-all') - async rejectAllPending( - @Req() req: Request, - @Param('opportunityId') opportunityId: string, - ) { - const authUser: JwtUser = req['user'] as JwtUser; - await this.service.rejectAllPending(authUser, opportunityId); + async rejectAllPending(@Param('opportunityId') opportunityId: string) { + await this.service.rejectAllPending(opportunityId); return OkResponse({}); } } diff --git a/src/api/review-application/reviewApplication.service.ts b/src/api/review-application/reviewApplication.service.ts index f37fb57..68e538e 100644 --- a/src/api/review-application/reviewApplication.service.ts +++ b/src/api/review-application/reviewApplication.service.ts @@ -182,17 +182,15 @@ export class ReviewApplicationService { /** * Approve a review application. - * @param authUser auth user * @param id review application id */ - async approve(authUser: JwtUser, id: string): Promise { + async approve(id: string): Promise { try { const entity = await this.checkExists(id); await this.prisma.reviewApplication.update({ where: { id }, data: { status: ReviewApplicationStatus.APPROVED, - updatedBy: authUser.userId ?? '', }, }); // send email @@ -217,17 +215,15 @@ export class ReviewApplicationService { /** * Reject a review application. - * @param authUser auth user * @param id review application id */ - async reject(authUser: JwtUser, id: string): Promise { + async reject(id: string): Promise { try { const entity = await this.checkExists(id); await this.prisma.reviewApplication.update({ where: { id }, data: { status: ReviewApplicationStatus.REJECTED, - updatedBy: authUser.userId ?? '', }, }); // send email @@ -252,13 +248,9 @@ export class ReviewApplicationService { /** * Reject all pending applications of specific opportunity - * @param authUser auth user * @param opportunityId opportunity id */ - async rejectAllPending( - authUser: JwtUser, - opportunityId: string, - ): Promise { + async rejectAllPending(opportunityId: string): Promise { try { // select all pending const entityList = await this.prisma.reviewApplication.findMany({ @@ -270,7 +262,6 @@ export class ReviewApplicationService { where: { opportunityId, status: ReviewApplicationStatus.PENDING }, data: { status: ReviewApplicationStatus.REJECTED, - updatedBy: authUser.userId ?? '', }, }); // send emails to these users diff --git a/src/api/review-opportunity/reviewOpportunity.controller.ts b/src/api/review-opportunity/reviewOpportunity.controller.ts index 9f18ec2..ea055b5 100644 --- a/src/api/review-opportunity/reviewOpportunity.controller.ts +++ b/src/api/review-opportunity/reviewOpportunity.controller.ts @@ -226,12 +226,10 @@ export class ReviewOpportunityController { @Roles(UserRole.Admin, UserRole.Copilot) @Scopes(Scope.UpdateReviewOpportunity, Scope.AllReviewOpportunity) async updateById( - @Req() req: Request, @Param('id') id: string, @Body() dto: UpdateReviewOpportunityDto, ) { - const authUser: JwtUser = req['user'] as JwtUser; - return OkResponse(await this.service.update(authUser, id, dto)); + return OkResponse(await this.service.update(id, dto)); } @ApiOperation({ diff --git a/src/api/review-opportunity/reviewOpportunity.service.ts b/src/api/review-opportunity/reviewOpportunity.service.ts index 1281f48..d42cb71 100644 --- a/src/api/review-opportunity/reviewOpportunity.service.ts +++ b/src/api/review-opportunity/reviewOpportunity.service.ts @@ -221,19 +221,16 @@ export class ReviewOpportunityService { /** * Update review opportunity by id - * @param authUser auth user * @param id opportunity id * @param dto update dto */ - async update(authUser: JwtUser, id: string, dto: UpdateReviewOpportunityDto) { + async update(id: string, dto: UpdateReviewOpportunityDto) { try { - const updatedBy = authUser.userId ?? ''; await this.checkExists(id); const entity = await this.prisma.reviewOpportunity.update({ where: { id }, data: { ...dto, - updatedBy, }, }); return await this.assembleResult(entity); diff --git a/src/api/scorecard/scorecard.controller.ts b/src/api/scorecard/scorecard.controller.ts index fa8da8c..c1a84ae 100644 --- a/src/api/scorecard/scorecard.controller.ts +++ b/src/api/scorecard/scorecard.controller.ts @@ -59,9 +59,8 @@ export class ScorecardController { async addScorecard( @Body(new ValidationPipe({ whitelist: true, transform: true })) body: ScorecardRequestDto, - @User() user: JwtUser, ): Promise { - return await this.scorecardService.addScorecard(body, user); + return await this.scorecardService.addScorecard(body); } @Put('/:id') @@ -241,10 +240,7 @@ export class ScorecardController { }) @ApiResponse({ status: 403, description: 'Forbidden.' }) @ApiResponse({ status: 404, description: 'Scorecard not found.' }) - async cloneScorecard( - @Param('id') id: string, - @User() user: JwtUser, - ): Promise { - return this.scorecardService.cloneScorecard(id, user); + async cloneScorecard(@Param('id') id: string): Promise { + return this.scorecardService.cloneScorecard(id); } } diff --git a/src/api/scorecard/scorecard.service.ts b/src/api/scorecard/scorecard.service.ts index 80cfcc5..fee2e4b 100644 --- a/src/api/scorecard/scorecard.service.ts +++ b/src/api/scorecard/scorecard.service.ts @@ -10,6 +10,7 @@ import { scorecardSection, } from '@prisma/client'; import { + mapScorecardRequestForCreate, ScorecardGroupBaseDto, ScorecardPaginatedResponseDto, ScorecardQuestionBaseDto, @@ -63,49 +64,25 @@ export class ScoreCardService { */ async addScorecard( body: ScorecardRequestDto, - user: JwtUser, ): Promise { try { - const data = await this.prisma.$transaction(async (tx) => { - const scorecard = await tx.scorecard.create({ - data: { ...omit(body, ['id', 'scorecardGroups']) }, - }); - this.logger.log( - `[updateScorecard] Created empty scorecard: ${JSON.stringify(scorecard)}`, - ); - - const scorecardGroups = await Promise.all(body.scorecardGroups.map(async (scorecardGroupData) => { - const group = await tx.scorecardGroup.create({ - data: { - ...omit(scorecardGroupData, ['id', 'sections']), - scorecardId: scorecard.id, - } - }); - - const sections = await Promise.all(scorecardGroupData.sections.map(async (scorecardSectionData) => { - const section = await tx.scorecardSection.create({ - data: { - ...omit(scorecardSectionData, ['id', 'questions']), - scorecardGroupId: group.id, - } - }); - - const questions = await Promise.all(scorecardSectionData.questions.map(async (scorecardQuestionData) => { - return await tx.scorecardQuestion.create({ - data: { - ...omit(scorecardQuestionData, ['id', 'questions']), - scorecardSectionId: section.id, - } - }); - })); - - return { ...section, questions }; - })); - - return { ...group, sections }; - })); - - return { ...scorecard, scorecardGroups }; + const data = await this.prisma.scorecard.create({ + data: { + ...mapScorecardRequestForCreate({ + ...body, + }), + }, + include: { + scorecardGroups: { + include: { + sections: { + include: { + questions: true, + }, + }, + }, + }, + }, }); return data as unknown as ScorecardWithGroupResponseDto; @@ -242,7 +219,6 @@ export class ScoreCardService { incoming, existing, cascade, - userId, }: SyncChildrenParams): Promise { const incomingIds = incoming.filter((c) => c.id).map((c) => c.id); const existingIds = existing.map((e) => e.id); @@ -444,10 +420,7 @@ export class ScoreCardService { } } - async cloneScorecard( - id: string, - user: { userId?: string; isMachine: boolean }, - ): Promise { + async cloneScorecard(id: string): Promise { try { const original = await this.prisma.scorecard.findUnique({ where: { id }, @@ -471,35 +444,22 @@ export class ScoreCardService { }); } - // DO NOT remove these! - // prisma middleware is not recuresively parsing the data object - // groups, sections & questions will not be automatically updated with audit fields - const auditFields = { - createdBy: user.isMachine ? 'System' : (user.userId as string), - updatedBy: user.isMachine ? 'System' : (user.userId as string), - createdAt: undefined, - updatedAt: undefined, - }; - // Remove id fields from nested objects for cloning const cloneGroups = original.scorecardGroups.map( (group: ScorecardGroupBaseDto) => ({ ...group, id: undefined, - ...auditFields, scorecardId: undefined, sections: { create: group.sections.map((section: ScorecardSectionBaseDto) => ({ ...section, id: undefined, - ...auditFields, scorecardGroupId: undefined, questions: { create: section.questions.map( (question: ScorecardQuestionBaseDto) => ({ ...question, id: undefined, - ...auditFields, sectionId: undefined, scorecardSectionId: undefined, }), @@ -515,7 +475,6 @@ export class ScoreCardService { ...original, id: undefined, name: `${original.name} (Clone)`, - ...auditFields, scorecardGroups: { create: cloneGroups, }, diff --git a/src/dto/review.dto.ts b/src/dto/review.dto.ts index 9b1f4ac..fec7e45 100644 --- a/src/dto/review.dto.ts +++ b/src/dto/review.dto.ts @@ -396,8 +396,6 @@ type MappedReviewItemComment = { content: string; type: ReviewItemCommentType; sortOrder: number; - createdBy: string; - updatedBy: string; resourceId: string; }; @@ -406,8 +404,6 @@ type MappedReviewItem = { initialAnswer: string; finalAnswer?: string; managerComment?: string; - createdBy: string; - updatedBy: string; review?: { connect: { id: string } }; reviewItemComments?: { create?: MappedReviewItemComment[] }; }; @@ -415,15 +411,9 @@ type MappedReviewItem = { export function mapReviewRequestToDto( request: ReviewRequestDto | ReviewPatchRequestDto, ) { - const userFields = { - createdBy: '', - updatedBy: '', - }; - if (request instanceof ReviewRequestDto) { return { ...request, - ...userFields, reviewItems: { create: request.reviewItems?.map((item) => { // When creating review items nested within a review, don't include reviewId @@ -448,7 +438,6 @@ export function mapReviewRequestToDto( } else { return { ...request, - ...userFields, }; } } @@ -456,22 +445,15 @@ export function mapReviewRequestToDto( export function mapReviewItemRequestToDto( request: ReviewItemRequestDto, ): MappedReviewItem { - const userFields = { - createdBy: '', - updatedBy: '', - }; - const { reviewId, ...rest } = request as { reviewId?: string; } & ReviewItemRequestDto; const payload: MappedReviewItem = { ...rest, - ...userFields, reviewItemComments: { create: request.reviewItemComments?.map((comment) => ({ ...comment, - ...userFields, // resourceId is required on reviewItemComment; leave population to controller/service resourceId: '', })), @@ -490,10 +472,6 @@ export function mapReviewItemRequestToDto( export function mapReviewItemRequestForUpdate( request: ReviewItemRequestDto, ): Partial { - const userFields = { - updatedBy: '', - }; - // eslint-disable-next-line @typescript-eslint/no-unused-vars const { reviewId, reviewItemComments, ...rest } = request as { reviewId?: string; @@ -504,7 +482,6 @@ export function mapReviewItemRequestForUpdate( // Comments should be handled separately via dedicated comment endpoints const payload: Partial = { ...rest, - ...userFields, }; return payload; diff --git a/src/dto/scorecard.dto.ts b/src/dto/scorecard.dto.ts index 00f4635..228bf89 100644 --- a/src/dto/scorecard.dto.ts +++ b/src/dto/scorecard.dto.ts @@ -380,3 +380,29 @@ export class ScorecardPaginatedResponseDto { }) scoreCards: ScorecardResponseDto[]; } + +export function mapScorecardRequestForCreate(request: ScorecardRequestDto) { + return { + ...request, + id: undefined, + scorecardGroups: { + create: request.scorecardGroups.map((group) => ({ + ...group, + id: undefined, + sections: { + create: group.sections.map((section) => ({ + ...section, + id: undefined, + questions: { + create: section.questions.map((question) => ({ + ...question, + id: undefined, + sortOrder: 1, + })), + }, + })), + }, + })), + }, + }; +} From 55f30eef9cf5aae72a0dfa3172bc2b61f3f07118 Mon Sep 17 00:00:00 2001 From: Vasilica Olariu Date: Mon, 15 Sep 2025 10:31:39 +0300 Subject: [PATCH 8/8] lint fixes --- src/shared/guards/tokenRoles.guard.ts | 4 ++-- src/shared/request/createRequestStore.middleware.ts | 6 +++++- src/shared/request/requestStore.ts | 3 +-- 3 files changed, 8 insertions(+), 5 deletions(-) diff --git a/src/shared/guards/tokenRoles.guard.ts b/src/shared/guards/tokenRoles.guard.ts index 94c4222..d340733 100644 --- a/src/shared/guards/tokenRoles.guard.ts +++ b/src/shared/guards/tokenRoles.guard.ts @@ -21,7 +21,7 @@ export class TokenRolesGuard implements CanActivate { private jwtService: JwtService, ) {} - async canActivate(context: ExecutionContext): Promise { + canActivate(context: ExecutionContext): boolean { // Get required roles and scopes from decorators const requiredRoles = this.reflector.get(ROLES_KEY, context.getHandler()) || []; @@ -38,7 +38,7 @@ export class TokenRolesGuard implements CanActivate { try { const user = request['user'] ?? {}; - + // Check role-based access for regular users if (user.roles && requiredRoles.length > 0) { const hasRole = requiredRoles.some((role) => diff --git a/src/shared/request/createRequestStore.middleware.ts b/src/shared/request/createRequestStore.middleware.ts index f4e84ee..acc3b87 100644 --- a/src/shared/request/createRequestStore.middleware.ts +++ b/src/shared/request/createRequestStore.middleware.ts @@ -7,7 +7,11 @@ export class CreateRequestStoreMiddleware implements NestMiddleware { constructor() {} use(req: any, res: Response, next: NextFunction) { - const requestUserId = req['user'] ? (req['user'].isMachine ? 'System' : `${req['user'].userId}`) : undefined; + const requestUserId = req['user'] + ? req['user'].isMachine + ? 'System' + : `${req['user'].userId}` + : undefined; const requestMetaData = new RequestMetadata({ userId: requestUserId }); saveStore(requestMetaData, next); diff --git a/src/shared/request/requestStore.ts b/src/shared/request/requestStore.ts index 91f741c..135b2b0 100644 --- a/src/shared/request/requestStore.ts +++ b/src/shared/request/requestStore.ts @@ -7,8 +7,7 @@ export class RequestMetadata { requestId: string; userId?: string; - // eslint-disable-next-line @typescript-eslint/no-unused-vars, @typescript-eslint/no-empty-object-type - constructor(params: { requestId?: string, userId?: string }) { + constructor(params: { requestId?: string; userId?: string }) { this.requestId = params.requestId ?? nanoid(11); this.userId = params.userId; }