From 04634a447b2a1516a2713a0f1356ef920c25f427 Mon Sep 17 00:00:00 2001 From: Dries Augustyns Date: Thu, 4 Dec 2025 12:46:39 +0100 Subject: [PATCH] Fix linting errors --- .../src/__tests__/integration/actions.test.ts | 44 +-- .../src/__tests__/integration/domains.test.ts | 20 +- apps/api/src/app.ts | 4 +- apps/api/src/controllers/Actions.ts | 11 +- apps/api/src/controllers/Activity.ts | 10 +- apps/api/src/controllers/Auth.ts | 4 +- apps/api/src/controllers/Campaigns.ts | 20 +- apps/api/src/controllers/Contacts.ts | 28 +- apps/api/src/controllers/Domains.ts | 8 +- apps/api/src/controllers/Events.ts | 14 +- apps/api/src/controllers/Oauth/Github.ts | 2 +- apps/api/src/controllers/Oauth/Google.ts | 2 +- apps/api/src/controllers/Projects.ts | 4 +- apps/api/src/controllers/Segments.ts | 16 +- apps/api/src/controllers/Templates.ts | 14 +- apps/api/src/controllers/Uploads.ts | 2 +- apps/api/src/controllers/Users.ts | 28 +- apps/api/src/controllers/Webhooks.ts | 2 +- apps/api/src/controllers/Workflows.ts | 32 +- .../controllers/__tests__/Workflows.test.ts | 7 +- .../jobs/__tests__/email-processor.test.ts | 10 +- .../__tests__/requestLogger.test.ts | 21 +- apps/api/src/middleware/requestLogger.ts | 23 +- apps/api/src/services/ContactService.ts | 105 ++++--- apps/api/src/services/EventService.ts | 274 +++++++++--------- .../services/__tests__/EventService.test.ts | 7 +- .../SegmentService.operators.test.ts | 250 ++++++++-------- .../services/__tests__/SegmentService.test.ts | 19 +- .../WorkflowConditions.operators.test.ts | 7 +- ...rkflowExecutionService.integration.test.ts | 30 +- .../__tests__/WorkflowService.test.ts | 14 +- apps/api/src/utils/asyncHandler.ts | 2 +- apps/api/src/utils/logger.ts | 2 +- apps/smtp/src/server.ts | 15 +- 34 files changed, 517 insertions(+), 534 deletions(-) diff --git a/apps/api/src/__tests__/integration/actions.test.ts b/apps/api/src/__tests__/integration/actions.test.ts index b627c65..1b27e1d 100644 --- a/apps/api/src/__tests__/integration/actions.test.ts +++ b/apps/api/src/__tests__/integration/actions.test.ts @@ -1,16 +1,16 @@ -import {describe, it, expect, beforeEach} from 'vitest'; +import {beforeEach, describe, expect, it} from 'vitest'; import {ActionSchemas} from '@plunk/shared'; import {factories, getPrismaClient} from '../../../../../test/helpers'; import { - ErrorCode, - NotFound, - ValidationError, - NotAuthenticated, - NotAllowed, - RateLimitError, - ConflictError, BadRequest, + ConflictError, + ErrorCode, HttpException, + NotAllowed, + NotAuthenticated, + NotFound, + RateLimitError, + ValidationError } from '../../exceptions/index.js'; import {EmailService} from '../../services/EmailService.js'; @@ -69,8 +69,6 @@ describe('Actions API Integration Tests', () => { }); it('should validate required fields for /v1/send', () => { - - const result = ActionSchemas.send.safeParse({}); expect(result.success).toBe(false); @@ -80,8 +78,6 @@ describe('Actions API Integration Tests', () => { }); it('should validate subject and body required when no template', () => { - - const result = ActionSchemas.send.safeParse({ to: 'test@example.com', // Missing subject, body, and template @@ -94,8 +90,6 @@ describe('Actions API Integration Tests', () => { }); it('should validate required fields for /v1/track', () => { - - const result = ActionSchemas.track.safeParse({ email: 'test@example.com', // Missing event name @@ -271,8 +265,6 @@ describe('Actions API Integration Tests', () => { // ======================================== describe('Custom HTTP Exception Types', () => { it('should structure NotFound errors correctly', () => { - - const error = new NotFound('Template', 'abc-123'); expect(error.code).toBe(404); @@ -283,8 +275,6 @@ describe('Actions API Integration Tests', () => { }); it('should map resources to specific error codes', () => { - - const testCases = [ {resource: 'contact', expectedCode: ErrorCode.CONTACT_NOT_FOUND}, {resource: 'template', expectedCode: ErrorCode.TEMPLATE_NOT_FOUND}, @@ -300,8 +290,6 @@ describe('Actions API Integration Tests', () => { }); it('should structure ValidationError with field details', () => { - - const fieldErrors = [ {field: 'email', message: 'Invalid email format', code: 'invalid_email'}, {field: 'data.firstName', message: 'Required field', code: 'required'}, @@ -315,8 +303,6 @@ describe('Actions API Integration Tests', () => { }); it('should structure NotAuthenticated errors', () => { - - const error = new NotAuthenticated(); expect(error.code).toBe(401); @@ -324,8 +310,6 @@ describe('Actions API Integration Tests', () => { }); it('should structure NotAllowed errors', () => { - - const error = new NotAllowed('Cannot perform action', 'Insufficient permissions'); expect(error.code).toBe(403); @@ -334,8 +318,6 @@ describe('Actions API Integration Tests', () => { }); it('should structure RateLimitError', () => { - - const error = new RateLimitError('Too many requests', 60); expect(error.code).toBe(429); @@ -344,8 +326,6 @@ describe('Actions API Integration Tests', () => { }); it('should structure ConflictError', () => { - - const error = new ConflictError('Contact exists', {email: 'test@example.com'}); expect(error.code).toBe(409); @@ -354,8 +334,6 @@ describe('Actions API Integration Tests', () => { }); it('should structure BadRequest errors', () => { - - const error = new BadRequest('Invalid format', ErrorCode.INVALID_REQUEST_BODY, { expected: 'JSON', }); @@ -381,7 +359,6 @@ describe('Actions API Integration Tests', () => { type: 'MARKETING', }); - await expect( EmailService.sendTransactionalEmail({ projectId, @@ -395,7 +372,6 @@ describe('Actions API Integration Tests', () => { }); it('should return NotFound when template does not exist', async () => { - const contact = await factories.createContact({projectId}); const nonExistentId = '00000000-0000-0000-0000-000000000000'; const template = await prisma.template.findUnique({ @@ -405,7 +381,7 @@ describe('Actions API Integration Tests', () => { expect(template).toBeNull(); // In actual API, this would trigger NotFound exception - + const error = new NotFound('Template', nonExistentId); expect(error.code).toBe(404); @@ -413,8 +389,6 @@ describe('Actions API Integration Tests', () => { }); it('should handle billing limit exceeded', () => { - - const error = new HttpException(429, 'Billing limit exceeded'); expect(error.code).toBe(429); diff --git a/apps/api/src/__tests__/integration/domains.test.ts b/apps/api/src/__tests__/integration/domains.test.ts index f650429..8907ceb 100644 --- a/apps/api/src/__tests__/integration/domains.test.ts +++ b/apps/api/src/__tests__/integration/domains.test.ts @@ -1,4 +1,4 @@ -import {describe, it, expect, beforeEach, vi} from 'vitest'; +import {beforeEach, describe, expect, it, vi} from 'vitest'; import {factories, getPrismaClient} from '../../../../../test/helpers'; import {DomainService} from '../../services/DomainService.js'; import * as SESService from '../../services/SESService.js'; @@ -71,7 +71,7 @@ describe('Domain Verification and Ownership Tests', () => { }); it('should allow access when user is a member of the project that owns the domain', async () => { - const {user, project} = await factories.createUserWithProject(); + const {project} = await factories.createUserWithProject(); const domain = 'shared-domain.com'; // User 1 adds the domain to their project @@ -101,7 +101,7 @@ describe('Domain Verification and Ownership Tests', () => { describe('Preventing Unauthorized Domain Linking', () => { it('should prevent linking a domain to another project when user is not a member', async () => { const {project: project1} = await factories.createUserWithProject(); - const {user: user2, project: project2} = await factories.createUserWithProject(); + const {user: user2} = await factories.createUserWithProject(); const domain = 'protected-domain.com'; // Project 1 adds the domain @@ -172,9 +172,7 @@ describe('Domain Verification and Ownership Tests', () => { await DomainService.addDomain(project.id, domain); // Try to verify email domain - await expect( - DomainService.verifyEmailDomain(`sender@${domain}`, project.id), - ).rejects.toThrow(/not verified/i); + await expect(DomainService.verifyEmailDomain(`sender@${domain}`, project.id)).rejects.toThrow(/not verified/i); }); it('should allow using verified domain for sending emails', async () => { @@ -210,9 +208,9 @@ describe('Domain Verification and Ownership Tests', () => { }); // Try to use from different project - await expect( - DomainService.verifyEmailDomain(`sender@${domain}`, project2.id), - ).rejects.toThrow(/belongs to a different project/i); + await expect(DomainService.verifyEmailDomain(`sender@${domain}`, project2.id)).rejects.toThrow( + /belongs to a different project/i, + ); }); it('should prevent using unregistered domain', async () => { @@ -220,9 +218,7 @@ describe('Domain Verification and Ownership Tests', () => { const domain = 'not-registered.com'; // Try to use domain that was never added - await expect( - DomainService.verifyEmailDomain(`sender@${domain}`, project.id), - ).rejects.toThrow(/not registered/i); + await expect(DomainService.verifyEmailDomain(`sender@${domain}`, project.id)).rejects.toThrow(/not registered/i); }); }); diff --git a/apps/api/src/app.ts b/apps/api/src/app.ts index f7b2d33..e8deb01 100644 --- a/apps/api/src/app.ts +++ b/apps/api/src/app.ts @@ -19,7 +19,7 @@ import { SMTP_ENABLED, STRIPE_ENABLED, TRACKING_TOGGLE_ENABLED, - WIKI_URI + WIKI_URI, } from './app/constants.js'; import {Actions} from './controllers/Actions.js'; import {Activity} from './controllers/Activity.js'; @@ -159,7 +159,7 @@ server.app.use((error: Error, req: Request, res: Response, _next: NextFunction) field: err.path.join('.'), message: err.message, code: err.code, - received: err.code !== 'invalid_type' ? undefined : (err as any).received, + received: err.code !== 'invalid_type' ? undefined : 'received' in err ? err.received : undefined, })); const statusCode = 422; diff --git a/apps/api/src/controllers/Actions.ts b/apps/api/src/controllers/Actions.ts index 5b3272b..2a5713e 100644 --- a/apps/api/src/controllers/Actions.ts +++ b/apps/api/src/controllers/Actions.ts @@ -49,7 +49,7 @@ export class Actions { @Post('track') @Middleware([requirePublicKey]) @CatchAsync - public async track(req: Request, res: Response, next: NextFunction) { + public async track(req: Request, res: Response, _next: NextFunction) { const auth = res.locals.auth as AuthResponse; // Zod validation - errors automatically handled by global error handler @@ -155,7 +155,7 @@ export class Actions { @Post('send') @Middleware([requireSecretKey]) @CatchAsync - public async send(req: Request, res: Response, next: NextFunction) { + public async send(req: Request, res: Response, _next: NextFunction) { const auth = res.locals.auth as AuthResponse; // Zod validation - errors automatically handled by global error handler @@ -244,12 +244,7 @@ export class Actions { : (data as Record | undefined); // Create or update contact with metadata - const contact = await ContactService.upsert( - auth.projectId, - recipient.email, - recipientData, - subscribed, - ); + const contact = await ContactService.upsert(auth.projectId, recipient.email, recipientData, subscribed); // Get merged data including non-persistent fields for template rendering const mergedData = ContactService.getMergedData(contact, data as Record | undefined); diff --git a/apps/api/src/controllers/Activity.ts b/apps/api/src/controllers/Activity.ts index 2c0b61b..007607d 100644 --- a/apps/api/src/controllers/Activity.ts +++ b/apps/api/src/controllers/Activity.ts @@ -23,7 +23,7 @@ export class Activity { @Get('') @Middleware([requireAuth]) @CatchAsync - public async getActivities(req: Request, res: Response, next: NextFunction) { + public async getActivities(req: Request, res: Response, _next: NextFunction) { const auth = res.locals.auth as AuthResponse; const limit = Math.min(parseInt(req.query.limit as string) || 50, 100); const cursor = req.query.cursor as string | undefined; @@ -64,7 +64,7 @@ export class Activity { @Get('stats') @Middleware([requireAuth]) @CatchAsync - public async getStats(req: Request, res: Response, next: NextFunction) { + public async getStats(req: Request, res: Response, _next: NextFunction) { const auth = res.locals.auth as AuthResponse; const startDate = req.query.startDate ? new Date(req.query.startDate as string) : undefined; const endDate = req.query.endDate ? new Date(req.query.endDate as string) : undefined; @@ -84,7 +84,7 @@ export class Activity { @Get('recent-count') @Middleware([requireAuth]) @CatchAsync - public async getRecentCount(req: Request, res: Response, next: NextFunction) { + public async getRecentCount(req: Request, res: Response, _next: NextFunction) { const auth = res.locals.auth as AuthResponse; const minutes = Math.min(parseInt(req.query.minutes as string) || 5, 60); // Max 60 minutes @@ -100,7 +100,7 @@ export class Activity { @Get('types') @Middleware([requireAuth]) @CatchAsync - public async getTypes(_req: Request, res: Response, next: NextFunction) { + public async getTypes(_req: Request, res: Response, _next: NextFunction) { const types = Object.values(ActivityType); return res.status(200).json({types}); } @@ -116,7 +116,7 @@ export class Activity { @Get('upcoming') @Middleware([requireAuth]) @CatchAsync - public async getUpcoming(req: Request, res: Response, next: NextFunction) { + public async getUpcoming(req: Request, res: Response, _next: NextFunction) { const auth = res.locals.auth as AuthResponse; const limit = Math.min(parseInt(req.query.limit as string) || 50, 100); const daysAhead = Math.min(parseInt(req.query.daysAhead as string) || 30, 90); diff --git a/apps/api/src/controllers/Auth.ts b/apps/api/src/controllers/Auth.ts index b49c32b..e872e00 100644 --- a/apps/api/src/controllers/Auth.ts +++ b/apps/api/src/controllers/Auth.ts @@ -15,7 +15,7 @@ import {CatchAsync} from '../utils/asyncHandler.js'; export class Auth { @Post('login') @CatchAsync - public async login(req: Request, res: Response, next: NextFunction) { + public async login(req: Request, res: Response, _next: NextFunction) { const {email, password} = AuthenticationSchemas.login.parse(req.body); const user = await UserService.email(email); @@ -46,7 +46,7 @@ export class Auth { @Post('signup') @CatchAsync - public async signup(req: Request, res: Response, next: NextFunction) { + public async signup(req: Request, res: Response, _next: NextFunction) { const {email, password} = AuthenticationSchemas.login.parse(req.body); const user = await UserService.email(email); diff --git a/apps/api/src/controllers/Campaigns.ts b/apps/api/src/controllers/Campaigns.ts index a5472ae..5982e2b 100644 --- a/apps/api/src/controllers/Campaigns.ts +++ b/apps/api/src/controllers/Campaigns.ts @@ -19,7 +19,7 @@ export class Campaigns { @Post('') @Middleware([requireAuth]) @CatchAsync - private async create(req: Request, res: Response, next: NextFunction) { + private async create(req: Request, res: Response, _next: NextFunction) { const auth = res.locals.auth as AuthResponse; const {name, description, subject, body, from, fromName, replyTo, audienceType, audienceCondition, segmentId} = CampaignSchemas.create.parse(req.body); @@ -62,7 +62,7 @@ export class Campaigns { @Get('') @Middleware([requireAuth]) @CatchAsync - private async list(req: Request, res: Response, next: NextFunction) { + private async list(req: Request, res: Response, _next: NextFunction) { const auth = res.locals.auth as AuthResponse; const status = req.query.status as CampaignStatus | undefined; const page = parseInt(req.query.page as string) || 1; @@ -95,7 +95,7 @@ export class Campaigns { @Get(':id') @Middleware([requireAuth]) @CatchAsync - private async get(req: Request, res: Response, next: NextFunction) { + private async get(req: Request, res: Response, _next: NextFunction) { const auth = res.locals.auth as AuthResponse; const {id} = req.params; @@ -114,7 +114,7 @@ export class Campaigns { @Put(':id') @Middleware([requireAuth]) @CatchAsync - private async update(req: Request, res: Response, next: NextFunction) { + private async update(req: Request, res: Response, _next: NextFunction) { const auth = res.locals.auth as AuthResponse; const {id} = req.params; const {name, description, subject, body, from, fromName, replyTo, audienceType, audienceCondition, segmentId} = @@ -160,7 +160,7 @@ export class Campaigns { @Delete(':id') @Middleware([requireAuth]) @CatchAsync - private async delete(req: Request, res: Response, next: NextFunction) { + private async delete(req: Request, res: Response, _next: NextFunction) { const auth = res.locals.auth as AuthResponse; const {id} = req.params; @@ -179,7 +179,7 @@ export class Campaigns { @Post(':id/duplicate') @Middleware([requireAuth]) @CatchAsync - private async duplicate(req: Request, res: Response, next: NextFunction) { + private async duplicate(req: Request, res: Response, _next: NextFunction) { const auth = res.locals.auth as AuthResponse; const {id} = req.params; @@ -199,7 +199,7 @@ export class Campaigns { @Post(':id/send') @Middleware([requireAuth]) @CatchAsync - private async send(req: Request, res: Response, next: NextFunction) { + private async send(req: Request, res: Response, _next: NextFunction) { const auth = res.locals.auth as AuthResponse; const {id} = req.params; const scheduledFor = req.body?.scheduledFor; @@ -230,7 +230,7 @@ export class Campaigns { @Post(':id/cancel') @Middleware([requireAuth]) @CatchAsync - private async cancel(req: Request, res: Response, next: NextFunction) { + private async cancel(req: Request, res: Response, _next: NextFunction) { const auth = res.locals.auth as AuthResponse; const {id} = req.params; @@ -250,7 +250,7 @@ export class Campaigns { @Get(':id/stats') @Middleware([requireAuth]) @CatchAsync - private async stats(req: Request, res: Response, next: NextFunction) { + private async stats(req: Request, res: Response, _next: NextFunction) { const auth = res.locals.auth as AuthResponse; const {id} = req.params; @@ -269,7 +269,7 @@ export class Campaigns { @Post(':id/test') @Middleware([requireAuth]) @CatchAsync - private async sendTest(req: Request, res: Response, next: NextFunction) { + private async sendTest(req: Request, res: Response, _next: NextFunction) { const auth = res.locals.auth as AuthResponse; const {id} = req.params; const {email} = CampaignSchemas.sendTest.parse(req.body); diff --git a/apps/api/src/controllers/Contacts.ts b/apps/api/src/controllers/Contacts.ts index 10e9fa9..9dc684f 100644 --- a/apps/api/src/controllers/Contacts.ts +++ b/apps/api/src/controllers/Contacts.ts @@ -33,7 +33,7 @@ export class Contacts { @Get('') @Middleware([requireAuth]) @CatchAsync - public async list(req: Request, res: Response, next: NextFunction) { + public async list(req: Request, res: Response, _next: NextFunction) { const auth = res.locals.auth as AuthResponse; const limit = Math.min(parseInt(req.query.limit as string) || 20, 100); const cursor = req.query.cursor as string | undefined; @@ -52,7 +52,7 @@ export class Contacts { @Get('fields') @Middleware([requireAuth]) @CatchAsync - public async getAvailableFields(req: Request, res: Response, next: NextFunction) { + public async getAvailableFields(req: Request, res: Response, _next: NextFunction) { const auth = res.locals.auth as AuthResponse; try { @@ -78,7 +78,7 @@ export class Contacts { @Get('fields/:field/values') @Middleware([requireAuth]) @CatchAsync - public async getFieldValues(req: Request, res: Response, next: NextFunction) { + public async getFieldValues(req: Request, res: Response, _next: NextFunction) { const auth = res.locals.auth as AuthResponse; const field = req.params.field; const limit = Math.min(parseInt(req.query.limit as string) || 100, 200); @@ -111,7 +111,7 @@ export class Contacts { @Get(':id') @Middleware([requireAuth]) @CatchAsync - public async get(req: Request, res: Response, next: NextFunction) { + public async get(req: Request, res: Response, _next: NextFunction) { const auth = res.locals.auth as AuthResponse; const contactId = req.params.id; @@ -131,7 +131,7 @@ export class Contacts { @Post('') @Middleware([requireAuth]) @CatchAsync - public async create(req: Request, res: Response, next: NextFunction) { + public async create(req: Request, res: Response, _next: NextFunction) { const auth = res.locals.auth as AuthResponse; const {email, data, subscribed} = req.body; @@ -161,7 +161,7 @@ export class Contacts { @Patch(':id') @Middleware([requireAuth]) @CatchAsync - public async update(req: Request, res: Response, next: NextFunction) { + public async update(req: Request, res: Response, _next: NextFunction) { const auth = res.locals.auth as AuthResponse; const contactId = req.params.id; const {email, data, subscribed} = req.body; @@ -182,7 +182,7 @@ export class Contacts { @Delete(':id') @Middleware([requireAuth]) @CatchAsync - public async delete(req: Request, res: Response, next: NextFunction) { + public async delete(req: Request, res: Response, _next: NextFunction) { const auth = res.locals.auth as AuthResponse; const contactId = req.params.id; @@ -201,7 +201,7 @@ export class Contacts { */ @Get('public/:id') @CatchAsync - public async getPublic(req: Request, res: Response, next: NextFunction) { + public async getPublic(req: Request, res: Response, _next: NextFunction) { const contactId = req.params.id; if (!contactId) { @@ -223,7 +223,7 @@ export class Contacts { */ @Post('public/:id/subscribe') @CatchAsync - public async subscribePublic(req: Request, res: Response, next: NextFunction) { + public async subscribePublic(req: Request, res: Response, _next: NextFunction) { const contactId = req.params.id; if (!contactId) { @@ -245,7 +245,7 @@ export class Contacts { */ @Post('public/:id/unsubscribe') @CatchAsync - public async unsubscribePublic(req: Request, res: Response, next: NextFunction) { + public async unsubscribePublic(req: Request, res: Response, _next: NextFunction) { const contactId = req.params.id; if (!contactId) { @@ -268,7 +268,7 @@ export class Contacts { @Post('import') @Middleware([requireAuth, upload.single('file')]) @CatchAsync - public async importCsv(req: Request, res: Response, next: NextFunction) { + public async importCsv(req: Request, res: Response, _next: NextFunction) { const auth = res.locals.auth as AuthResponse; if (!req.file) { @@ -302,7 +302,7 @@ export class Contacts { @Get('import/:jobId') @Middleware([requireAuth]) @CatchAsync - public async getImportStatus(req: Request, res: Response, next: NextFunction) { + public async getImportStatus(req: Request, res: Response, _next: NextFunction) { const jobId = req.params.jobId; if (!jobId) { @@ -333,7 +333,7 @@ export class Contacts { @Get('fields/:field/usage') @Middleware([requireAuth]) @CatchAsync - public async getFieldUsage(req: Request, res: Response, next: NextFunction) { + public async getFieldUsage(req: Request, res: Response, _next: NextFunction) { const auth = res.locals.auth as AuthResponse; const field = req.params.field; @@ -360,7 +360,7 @@ export class Contacts { @Delete('fields/:field') @Middleware([requireAuth]) @CatchAsync - public async deleteField(req: Request, res: Response, next: NextFunction) { + public async deleteField(req: Request, res: Response, _next: NextFunction) { const auth = res.locals.auth as AuthResponse; const field = req.params.field; diff --git a/apps/api/src/controllers/Domains.ts b/apps/api/src/controllers/Domains.ts index 160881c..130ab48 100644 --- a/apps/api/src/controllers/Domains.ts +++ b/apps/api/src/controllers/Domains.ts @@ -19,7 +19,7 @@ export class Domains { @Get('project/:projectId') @Middleware([isAuthenticated]) @CatchAsync - public async getProjectDomains(req: Request, res: Response, next: NextFunction) { + public async getProjectDomains(req: Request, res: Response, _next: NextFunction) { const auth = res.locals.auth as AuthResponse; const {projectId} = DomainSchemas.projectId.parse(req.params); @@ -46,7 +46,7 @@ export class Domains { @Post('') @Middleware([isAuthenticated]) @CatchAsync - public async addDomain(req: Request, res: Response, next: NextFunction) { + public async addDomain(req: Request, res: Response, _next: NextFunction) { const auth = res.locals.auth as AuthResponse; const {projectId, domain} = DomainSchemas.create.parse(req.body); @@ -106,7 +106,7 @@ export class Domains { @Get(':id/verify') @Middleware([isAuthenticated]) @CatchAsync - public async checkVerification(req: Request, res: Response, next: NextFunction) { + public async checkVerification(req: Request, res: Response, _next: NextFunction) { const auth = res.locals.auth as AuthResponse; const {id} = UtilitySchemas.id.parse(req.params); @@ -143,7 +143,7 @@ export class Domains { @Delete(':id') @Middleware([isAuthenticated]) @CatchAsync - public async removeDomain(req: Request, res: Response, next: NextFunction) { + public async removeDomain(req: Request, res: Response, _next: NextFunction) { const auth = res.locals.auth as AuthResponse; const {id} = UtilitySchemas.id.parse(req.params); diff --git a/apps/api/src/controllers/Events.ts b/apps/api/src/controllers/Events.ts index f4f6cda..e23c7c0 100644 --- a/apps/api/src/controllers/Events.ts +++ b/apps/api/src/controllers/Events.ts @@ -15,7 +15,7 @@ export class Events { @Post('track') @Middleware([requireAuth]) @CatchAsync - public async track(req: Request, res: Response, next: NextFunction) { + public async track(req: Request, res: Response, _next: NextFunction) { const auth = res.locals.auth as AuthResponse; const {name, contactId, emailId, data} = req.body; @@ -35,7 +35,7 @@ export class Events { @Get('') @Middleware([requireAuth]) @CatchAsync - public async list(req: Request, res: Response, next: NextFunction) { + public async list(req: Request, res: Response, _next: NextFunction) { const auth = res.locals.auth as AuthResponse; const eventName = req.query.eventName as string | undefined; const limit = parseInt(req.query.limit as string) || 100; @@ -52,7 +52,7 @@ export class Events { @Get('stats') @Middleware([requireAuth]) @CatchAsync - public async stats(req: Request, res: Response, next: NextFunction) { + public async stats(req: Request, res: Response, _next: NextFunction) { const auth = res.locals.auth as AuthResponse; const startDate = req.query.startDate ? new Date(req.query.startDate as string) : undefined; const endDate = req.query.endDate ? new Date(req.query.endDate as string) : undefined; @@ -69,7 +69,7 @@ export class Events { @Get('contact/:contactId') @Middleware([requireAuth]) @CatchAsync - public async getContactEvents(req: Request, res: Response, next: NextFunction) { + public async getContactEvents(req: Request, res: Response, _next: NextFunction) { const auth = res.locals.auth as AuthResponse; const contactId = req.params.contactId; const limit = parseInt(req.query.limit as string) || 50; @@ -90,7 +90,7 @@ export class Events { @Get('names') @Middleware([requireAuth]) @CatchAsync - public async getEventNames(req: Request, res: Response, next: NextFunction) { + public async getEventNames(req: Request, res: Response, _next: NextFunction) { const auth = res.locals.auth as AuthResponse; const eventNames = await EventService.getUniqueEventNames(auth.projectId!); @@ -106,7 +106,7 @@ export class Events { @Get(':eventName/usage') @Middleware([requireAuth]) @CatchAsync - public async getEventUsage(req: Request, res: Response, next: NextFunction) { + public async getEventUsage(req: Request, res: Response, _next: NextFunction) { const auth = res.locals.auth as AuthResponse; const eventName = req.params.eventName; @@ -133,7 +133,7 @@ export class Events { @Delete(':eventName') @Middleware([requireAuth]) @CatchAsync - public async deleteEvent(req: Request, res: Response, next: NextFunction) { + public async deleteEvent(req: Request, res: Response, _next: NextFunction) { const auth = res.locals.auth as AuthResponse; const eventName = req.params.eventName; diff --git a/apps/api/src/controllers/Oauth/Github.ts b/apps/api/src/controllers/Oauth/Github.ts index 7867677..15eeda2 100644 --- a/apps/api/src/controllers/Oauth/Github.ts +++ b/apps/api/src/controllers/Oauth/Github.ts @@ -33,7 +33,7 @@ export class Github { @Get('callback') @CatchAsync - public async callback(req: Request, res: Response, next: NextFunction) { + public async callback(req: Request, res: Response, _next: NextFunction) { if (!GITHUB_OAUTH_ENABLED) { return res.status(404).json({error: 'GitHub OAuth is not configured'}); } diff --git a/apps/api/src/controllers/Oauth/Google.ts b/apps/api/src/controllers/Oauth/Google.ts index c57fe3b..6280c8e 100644 --- a/apps/api/src/controllers/Oauth/Google.ts +++ b/apps/api/src/controllers/Oauth/Google.ts @@ -28,7 +28,7 @@ export class Google { @Get('callback') @CatchAsync - public async callback(req: Request, res: Response, next: NextFunction) { + public async callback(req: Request, res: Response, _next: NextFunction) { if (!GOOGLE_OAUTH_ENABLED) { return res.status(404).json({error: 'Google OAuth is not configured'}); } diff --git a/apps/api/src/controllers/Projects.ts b/apps/api/src/controllers/Projects.ts index 9ec7ca7..1667138 100644 --- a/apps/api/src/controllers/Projects.ts +++ b/apps/api/src/controllers/Projects.ts @@ -16,7 +16,7 @@ export class Projects { @Get(':id/setup-state') @Middleware([requireAuth]) @CatchAsync - private async getSetupState(req: Request, res: Response, next: NextFunction) { + private async getSetupState(req: Request, res: Response, _next: NextFunction) { const auth = res.locals.auth as AuthResponse; const {id} = req.params; @@ -89,7 +89,7 @@ export class Projects { @Get(':id/members') @Middleware([requireAuth]) @CatchAsync - private async getMembers(req: Request, res: Response, next: NextFunction) { + private async getMembers(req: Request, res: Response, _next: NextFunction) { const auth = res.locals.auth as AuthResponse; const {id} = req.params; diff --git a/apps/api/src/controllers/Segments.ts b/apps/api/src/controllers/Segments.ts index 89fe9e1..cf78dc3 100644 --- a/apps/api/src/controllers/Segments.ts +++ b/apps/api/src/controllers/Segments.ts @@ -15,7 +15,7 @@ export class Segments { @Get('') @Middleware([requireAuth]) @CatchAsync - public async list(req: Request, res: Response, next: NextFunction) { + public async list(req: Request, res: Response, _next: NextFunction) { const auth = res.locals.auth as AuthResponse; const segments = await SegmentService.list(auth.projectId!); @@ -30,7 +30,7 @@ export class Segments { @Get(':id') @Middleware([requireAuth]) @CatchAsync - public async get(req: Request, res: Response, next: NextFunction) { + public async get(req: Request, res: Response, _next: NextFunction) { const auth = res.locals.auth as AuthResponse; const segmentId = req.params.id; @@ -50,7 +50,7 @@ export class Segments { @Get(':id/contacts') @Middleware([requireAuth]) @CatchAsync - public async getContacts(req: Request, res: Response, next: NextFunction) { + public async getContacts(req: Request, res: Response, _next: NextFunction) { const auth = res.locals.auth as AuthResponse; const segmentId = req.params.id; const page = parseInt(req.query.page as string) || 1; @@ -72,7 +72,7 @@ export class Segments { @Post('') @Middleware([requireAuth]) @CatchAsync - public async create(req: Request, res: Response, next: NextFunction) { + public async create(req: Request, res: Response, _next: NextFunction) { const auth = res.locals.auth as AuthResponse; const {name, description, condition, trackMembership} = req.body; @@ -101,7 +101,7 @@ export class Segments { @Patch(':id') @Middleware([requireAuth]) @CatchAsync - public async update(req: Request, res: Response, next: NextFunction) { + public async update(req: Request, res: Response, _next: NextFunction) { const auth = res.locals.auth as AuthResponse; const segmentId = req.params.id; const {name, description, condition, trackMembership} = req.body; @@ -131,7 +131,7 @@ export class Segments { @Delete(':id') @Middleware([requireAuth]) @CatchAsync - public async delete(req: Request, res: Response, next: NextFunction) { + public async delete(req: Request, res: Response, _next: NextFunction) { const auth = res.locals.auth as AuthResponse; const segmentId = req.params.id; @@ -151,7 +151,7 @@ export class Segments { @Post(':id/compute') @Middleware([requireAuth]) @CatchAsync - public async compute(req: Request, res: Response, next: NextFunction) { + public async compute(req: Request, res: Response, _next: NextFunction) { const auth = res.locals.auth as AuthResponse; const segmentId = req.params.id; @@ -171,7 +171,7 @@ export class Segments { @Post(':id/refresh') @Middleware([requireAuth]) @CatchAsync - public async refresh(req: Request, res: Response, next: NextFunction) { + public async refresh(req: Request, res: Response, _next: NextFunction) { const auth = res.locals.auth as AuthResponse; const segmentId = req.params.id; diff --git a/apps/api/src/controllers/Templates.ts b/apps/api/src/controllers/Templates.ts index 42b9c90..902d96e 100644 --- a/apps/api/src/controllers/Templates.ts +++ b/apps/api/src/controllers/Templates.ts @@ -17,7 +17,7 @@ export class Templates { @Get('') @Middleware([requireAuth]) @CatchAsync - public async list(req: Request, res: Response, next: NextFunction) { + public async list(req: Request, res: Response, _next: NextFunction) { const auth = res.locals.auth as AuthResponse; const page = parseInt(req.query.page as string) || 1; const pageSize = Math.min(parseInt(req.query.pageSize as string) || 20, 100); @@ -36,7 +36,7 @@ export class Templates { @Get(':id') @Middleware([requireAuth]) @CatchAsync - public async get(req: Request, res: Response, next: NextFunction) { + public async get(req: Request, res: Response, _next: NextFunction) { const auth = res.locals.auth as AuthResponse; const templateId = req.params.id; @@ -56,7 +56,7 @@ export class Templates { @Post('') @Middleware([requireAuth]) @CatchAsync - public async create(req: Request, res: Response, next: NextFunction) { + public async create(req: Request, res: Response, _next: NextFunction) { const auth = res.locals.auth as AuthResponse; const {name, description, subject, body, from, fromName, replyTo, type} = req.body; @@ -100,7 +100,7 @@ export class Templates { @Patch(':id') @Middleware([requireAuth]) @CatchAsync - public async update(req: Request, res: Response, next: NextFunction) { + public async update(req: Request, res: Response, _next: NextFunction) { const auth = res.locals.auth as AuthResponse; const templateId = req.params.id; const {name, description, subject, body, from, fromName, replyTo, type} = req.body; @@ -135,7 +135,7 @@ export class Templates { @Delete(':id') @Middleware([requireAuth]) @CatchAsync - public async delete(req: Request, res: Response, next: NextFunction) { + public async delete(req: Request, res: Response, _next: NextFunction) { const auth = res.locals.auth as AuthResponse; const templateId = req.params.id; @@ -155,7 +155,7 @@ export class Templates { @Post(':id/duplicate') @Middleware([requireAuth]) @CatchAsync - public async duplicate(req: Request, res: Response, next: NextFunction) { + public async duplicate(req: Request, res: Response, _next: NextFunction) { const auth = res.locals.auth as AuthResponse; const templateId = req.params.id; @@ -175,7 +175,7 @@ export class Templates { @Get(':id/usage') @Middleware([requireAuth]) @CatchAsync - public async getUsage(req: Request, res: Response, next: NextFunction) { + public async getUsage(req: Request, res: Response, _next: NextFunction) { const auth = res.locals.auth as AuthResponse; const templateId = req.params.id; diff --git a/apps/api/src/controllers/Uploads.ts b/apps/api/src/controllers/Uploads.ts index e01ec83..1a92d93 100644 --- a/apps/api/src/controllers/Uploads.ts +++ b/apps/api/src/controllers/Uploads.ts @@ -34,7 +34,7 @@ export class Uploads { @Post('image') @Middleware([requireAuth, upload.single('image')]) @CatchAsync - public async uploadImage(req: Request, res: Response, next: NextFunction) { + public async uploadImage(req: Request, res: Response, _next: NextFunction) { const auth = res.locals.auth as AuthResponse; try { diff --git a/apps/api/src/controllers/Users.ts b/apps/api/src/controllers/Users.ts index b945c57..63560e8 100644 --- a/apps/api/src/controllers/Users.ts +++ b/apps/api/src/controllers/Users.ts @@ -20,7 +20,7 @@ export class Users { @Get('@me') @Middleware([isAuthenticated]) @CatchAsync - public async me(req: Request, res: Response, next: NextFunction) { + public async me(req: Request, res: Response, _next: NextFunction) { const auth = res.locals.auth as AuthResponse; if (!auth.userId) { @@ -39,7 +39,7 @@ export class Users { @Get('@me/projects') @Middleware([isAuthenticated]) @CatchAsync - public async meProjects(req: Request, res: Response, next: NextFunction) { + public async meProjects(req: Request, res: Response, _next: NextFunction) { const auth = res.locals.auth as AuthResponse; if (!auth.userId) { @@ -54,7 +54,7 @@ export class Users { @Post('@me/projects') @Middleware([isAuthenticated]) @CatchAsync - public async createProject(req: Request, res: Response, next: NextFunction) { + public async createProject(req: Request, res: Response, _next: NextFunction) { const auth = res.locals.auth as AuthResponse; if (!auth.userId) { @@ -88,7 +88,7 @@ export class Users { @Patch('@me/projects/:id') @Middleware([isAuthenticated]) @CatchAsync - public async updateProject(req: Request, res: Response, next: NextFunction) { + public async updateProject(req: Request, res: Response, _next: NextFunction) { const auth = res.locals.auth as AuthResponse; const {id} = req.params; const data = ProjectSchemas.update.parse(req.body); @@ -120,7 +120,7 @@ export class Users { @Post('@me/projects/:id/regenerate-keys') @Middleware([isAuthenticated]) @CatchAsync - public async regenerateProjectKeys(req: Request, res: Response, next: NextFunction) { + public async regenerateProjectKeys(req: Request, res: Response, _next: NextFunction) { const auth = res.locals.auth as AuthResponse; const {id} = req.params; @@ -158,7 +158,7 @@ export class Users { @Post('@me/projects/:id/checkout') @Middleware([isAuthenticated]) @CatchAsync - public async createCheckoutSession(req: Request, res: Response, next: NextFunction) { + public async createCheckoutSession(req: Request, res: Response, _next: NextFunction) { const auth = res.locals.auth as AuthResponse; const {id} = req.params; @@ -237,7 +237,7 @@ export class Users { @Post('@me/projects/:id/billing-portal') @Middleware([isAuthenticated]) @CatchAsync - public async createBillingPortalSession(req: Request, res: Response, next: NextFunction) { + public async createBillingPortalSession(req: Request, res: Response, _next: NextFunction) { const auth = res.locals.auth as AuthResponse; const {id} = req.params; @@ -287,7 +287,7 @@ export class Users { @Get('@me/projects/:id/billing-limits') @Middleware([isAuthenticated]) @CatchAsync - public async getBillingLimits(req: Request, res: Response, next: NextFunction) { + public async getBillingLimits(req: Request, res: Response, _next: NextFunction) { const auth = res.locals.auth as AuthResponse; const {id} = req.params; @@ -320,7 +320,7 @@ export class Users { @Put('@me/projects/:id/billing-limits') @Middleware([isAuthenticated]) @CatchAsync - public async updateBillingLimits(req: Request, res: Response, next: NextFunction) { + public async updateBillingLimits(req: Request, res: Response, _next: NextFunction) { const auth = res.locals.auth as AuthResponse; const {id} = req.params; @@ -387,7 +387,7 @@ export class Users { @Get('@me/projects/:id/billing-consumption') @Middleware([isAuthenticated]) @CatchAsync - public async getBillingConsumption(req: Request, res: Response, next: NextFunction) { + public async getBillingConsumption(req: Request, res: Response, _next: NextFunction) { const auth = res.locals.auth as AuthResponse; const {id} = req.params; @@ -487,7 +487,7 @@ export class Users { @Get('@me/projects/:id/billing-invoices') @Middleware([isAuthenticated]) @CatchAsync - public async getBillingInvoices(req: Request, res: Response, next: NextFunction) { + public async getBillingInvoices(req: Request, res: Response, _next: NextFunction) { const auth = res.locals.auth as AuthResponse; const {id} = req.params; @@ -575,7 +575,7 @@ export class Users { @Get('@me/projects/:id/security') @Middleware([isAuthenticated]) @CatchAsync - public async getSecurityHealth(req: Request, res: Response, next: NextFunction) { + public async getSecurityHealth(req: Request, res: Response, _next: NextFunction) { const auth = res.locals.auth as AuthResponse; const {id} = req.params; @@ -608,7 +608,7 @@ export class Users { @Post('@me/projects/:id/reset') @Middleware([isAuthenticated]) @CatchAsync - public async resetProject(req: Request, res: Response, next: NextFunction) { + public async resetProject(req: Request, res: Response, _next: NextFunction) { const auth = res.locals.auth as AuthResponse; const {id} = req.params; @@ -684,7 +684,7 @@ export class Users { @Delete('@me/projects/:id') @Middleware([isAuthenticated]) @CatchAsync - public async deleteProject(req: Request, res: Response, next: NextFunction) { + public async deleteProject(req: Request, res: Response, _next: NextFunction) { const auth = res.locals.auth as AuthResponse; const {id} = req.params; diff --git a/apps/api/src/controllers/Webhooks.ts b/apps/api/src/controllers/Webhooks.ts index 628362f..f878e8c 100644 --- a/apps/api/src/controllers/Webhooks.ts +++ b/apps/api/src/controllers/Webhooks.ts @@ -1,7 +1,7 @@ import {Controller, Post} from '@overnightjs/core'; import type {Prisma} from '@plunk/db'; import {EmailStatus} from '@plunk/db'; -import type {NextFunction, Request, Response} from 'express'; +import type {Request, Response} from 'express'; import signale from 'signale'; import type Stripe from 'stripe'; diff --git a/apps/api/src/controllers/Workflows.ts b/apps/api/src/controllers/Workflows.ts index de6e44d..5caffb1 100644 --- a/apps/api/src/controllers/Workflows.ts +++ b/apps/api/src/controllers/Workflows.ts @@ -16,7 +16,7 @@ export class Workflows { @Get('') @Middleware([requireAuth]) @CatchAsync - public async list(req: Request, res: Response, next: NextFunction) { + public async list(req: Request, res: Response, _next: NextFunction) { const auth = res.locals.auth as AuthResponse; const page = parseInt(req.query.page as string) || 1; const pageSize = Math.min(parseInt(req.query.pageSize as string) || 20, 100); @@ -36,7 +36,7 @@ export class Workflows { @Get('fields') @Middleware([requireAuth]) @CatchAsync - public async getAvailableFields(req: Request, res: Response, next: NextFunction) { + public async getAvailableFields(req: Request, res: Response, _next: NextFunction) { const auth = res.locals.auth as AuthResponse; const eventName = req.query.eventName as string | undefined; @@ -59,7 +59,7 @@ export class Workflows { @Get(':id') @Middleware([requireAuth]) @CatchAsync - public async get(req: Request, res: Response, next: NextFunction) { + public async get(req: Request, res: Response, _next: NextFunction) { const auth = res.locals.auth as AuthResponse; const workflowId = req.params.id; @@ -79,7 +79,7 @@ export class Workflows { @Post('') @Middleware([requireAuth]) @CatchAsync - public async create(req: Request, res: Response, next: NextFunction) { + public async create(req: Request, res: Response, _next: NextFunction) { const auth = res.locals.auth as AuthResponse; const {name, description, eventName, enabled, allowReentry} = req.body; @@ -109,7 +109,7 @@ export class Workflows { @Patch(':id') @Middleware([requireAuth]) @CatchAsync - public async update(req: Request, res: Response, next: NextFunction) { + public async update(req: Request, res: Response, _next: NextFunction) { const auth = res.locals.auth as AuthResponse; const workflowId = req.params.id; const {name, description, triggerType, triggerConfig, enabled, allowReentry} = req.body; @@ -137,7 +137,7 @@ export class Workflows { @Delete(':id') @Middleware([requireAuth]) @CatchAsync - public async delete(req: Request, res: Response, next: NextFunction) { + public async delete(req: Request, res: Response, _next: NextFunction) { const auth = res.locals.auth as AuthResponse; const workflowId = req.params.id; @@ -157,7 +157,7 @@ export class Workflows { @Post(':id/steps') @Middleware([requireAuth]) @CatchAsync - public async addStep(req: Request, res: Response, next: NextFunction) { + public async addStep(req: Request, res: Response, _next: NextFunction) { const auth = res.locals.auth as AuthResponse; const workflowId = req.params.id; const {type, name, position, config, templateId, autoConnect} = req.body; @@ -189,7 +189,7 @@ export class Workflows { @Patch(':id/steps/:stepId') @Middleware([requireAuth]) @CatchAsync - public async updateStep(req: Request, res: Response, next: NextFunction) { + public async updateStep(req: Request, res: Response, _next: NextFunction) { const auth = res.locals.auth as AuthResponse; const workflowId = req.params.id; const stepId = req.params.stepId; @@ -216,7 +216,7 @@ export class Workflows { @Delete(':id/steps/:stepId') @Middleware([requireAuth]) @CatchAsync - public async deleteStep(req: Request, res: Response, next: NextFunction) { + public async deleteStep(req: Request, res: Response, _next: NextFunction) { const auth = res.locals.auth as AuthResponse; const workflowId = req.params.id; const stepId = req.params.stepId; @@ -237,7 +237,7 @@ export class Workflows { @Post(':id/transitions') @Middleware([requireAuth]) @CatchAsync - public async createTransition(req: Request, res: Response, next: NextFunction) { + public async createTransition(req: Request, res: Response, _next: NextFunction) { const auth = res.locals.auth as AuthResponse; const workflowId = req.params.id; const {fromStepId, toStepId, condition, priority} = req.body; @@ -267,7 +267,7 @@ export class Workflows { @Delete(':id/transitions/:transitionId') @Middleware([requireAuth]) @CatchAsync - public async deleteTransition(req: Request, res: Response, next: NextFunction) { + public async deleteTransition(req: Request, res: Response, _next: NextFunction) { const auth = res.locals.auth as AuthResponse; const workflowId = req.params.id; const transitionId = req.params.transitionId; @@ -288,7 +288,7 @@ export class Workflows { @Post(':id/executions') @Middleware([requireAuth]) @CatchAsync - public async startExecution(req: Request, res: Response, next: NextFunction) { + public async startExecution(req: Request, res: Response, _next: NextFunction) { const auth = res.locals.auth as AuthResponse; const workflowId = req.params.id; const {contactId, context} = req.body; @@ -313,7 +313,7 @@ export class Workflows { @Get(':id/executions') @Middleware([requireAuth]) @CatchAsync - public async listExecutions(req: Request, res: Response, next: NextFunction) { + public async listExecutions(req: Request, res: Response, _next: NextFunction) { const auth = res.locals.auth as AuthResponse; const workflowId = req.params.id; const page = parseInt(req.query.page as string) || 1; @@ -336,7 +336,7 @@ export class Workflows { @Get(':id/executions/:executionId') @Middleware([requireAuth]) @CatchAsync - public async getExecution(req: Request, res: Response, next: NextFunction) { + public async getExecution(req: Request, res: Response, _next: NextFunction) { const auth = res.locals.auth as AuthResponse; const workflowId = req.params.id; const executionId = req.params.executionId; @@ -357,7 +357,7 @@ export class Workflows { @Delete(':id/executions/:executionId') @Middleware([requireAuth]) @CatchAsync - public async cancelExecution(req: Request, res: Response, next: NextFunction) { + public async cancelExecution(req: Request, res: Response, _next: NextFunction) { const auth = res.locals.auth as AuthResponse; const workflowId = req.params.id; const executionId = req.params.executionId; @@ -378,7 +378,7 @@ export class Workflows { @Post(':id/executions/cancel-all') @Middleware([requireAuth]) @CatchAsync - public async cancelAllExecutions(req: Request, res: Response, next: NextFunction) { + public async cancelAllExecutions(req: Request, res: Response, _next: NextFunction) { const auth = res.locals.auth as AuthResponse; const workflowId = req.params.id; diff --git a/apps/api/src/controllers/__tests__/Workflows.test.ts b/apps/api/src/controllers/__tests__/Workflows.test.ts index 4398c5b..257de47 100644 --- a/apps/api/src/controllers/__tests__/Workflows.test.ts +++ b/apps/api/src/controllers/__tests__/Workflows.test.ts @@ -1,17 +1,14 @@ import {beforeEach, describe, expect, it} from 'vitest'; -import {factories, getPrismaClient} from '../../../../../test/helpers'; +import {factories} from '../../../../../test/helpers'; import {EventService} from '../../services/EventService'; import {WorkflowService} from '../../services/WorkflowService'; describe('Workflows Controller', () => { let projectId: string; - let userId: string; - const prisma = getPrismaClient(); beforeEach(async () => { - const {project, user} = await factories.createUserWithProject(); + const {project} = await factories.createUserWithProject(); projectId = project.id; - userId = user.id; }); // ======================================== diff --git a/apps/api/src/jobs/__tests__/email-processor.test.ts b/apps/api/src/jobs/__tests__/email-processor.test.ts index 3916256..633b735 100644 --- a/apps/api/src/jobs/__tests__/email-processor.test.ts +++ b/apps/api/src/jobs/__tests__/email-processor.test.ts @@ -1,7 +1,7 @@ -import {describe, it, expect, beforeEach, vi} from 'vitest'; -import {EmailStatus, EmailSourceType} from '@plunk/db'; -import {factories, getPrismaClient, createServiceMocks} from '../../../../../test/helpers'; +import {beforeEach, describe, expect, it, vi} from 'vitest'; import type {Prisma} from '@plunk/db'; +import {EmailSourceType, EmailStatus} from '@plunk/db'; +import {createServiceMocks, factories, getPrismaClient} from '../../../../../test/helpers'; // Mock MeterService vi.mock('../../services/MeterService.js', () => ({ @@ -286,7 +286,9 @@ describe('Email Processor', () => { expect(emailWithAttachmentsData?.attachments).toBeDefined(); expect(Array.isArray(emailWithAttachmentsData?.attachments)).toBe(true); - expect((emailWithAttachmentsData?.attachments as any[]).length).toBeGreaterThan(0); + expect( + Array.isArray(emailWithAttachmentsData?.attachments) ? emailWithAttachmentsData.attachments.length : 0, + ).toBeGreaterThan(0); expect(emailWithoutAttachmentsData?.attachments).toBeNull(); }); diff --git a/apps/api/src/middleware/__tests__/requestLogger.test.ts b/apps/api/src/middleware/__tests__/requestLogger.test.ts index ca77d60..bd48f36 100644 --- a/apps/api/src/middleware/__tests__/requestLogger.test.ts +++ b/apps/api/src/middleware/__tests__/requestLogger.test.ts @@ -1,4 +1,4 @@ -import {describe, it, expect, beforeEach, vi, afterEach} from 'vitest'; +import {afterEach, beforeEach, describe, expect, it, vi} from 'vitest'; import type {NextFunction, Request, Response} from 'express'; import {databaseRequestLogger} from '../requestLogger.js'; import {factories, getPrismaClient} from '../../../../../test/helpers'; @@ -21,7 +21,7 @@ describe('Request Logger Middleware', () => { method: 'POST', path: '/v1/send', ip: '192.168.1.100', - socket: {remoteAddress: '192.168.1.100'} as any, + socket: {remoteAddress: '192.168.1.100'} as Request['socket'], get: vi.fn((header: string) => { if (header === 'user-agent') { return 'Mozilla/5.0 Test Browser'; @@ -50,7 +50,7 @@ describe('Request Logger Middleware', () => { set statusCode(value: number) { statusCode = value; }, - json: vi.fn(function (this: any, body: any) { + json: vi.fn(function (this: Response, body: unknown) { return body; }), }; @@ -69,7 +69,7 @@ describe('Request Logger Middleware', () => { // ======================================== describe('Successful Request Logging', () => { it('should log successful API request to database', async () => { - const middleware = databaseRequestLogger(req as Request, res as Response, next); + databaseRequestLogger(req as Request, res as Response, next); // Middleware should call next immediately expect(next).toHaveBeenCalled(); @@ -334,10 +334,11 @@ describe('Request Logger Middleware', () => { await res.json!({success: true}); await new Promise(resolve => setTimeout(resolve, 100)); - // Should not log when disabled - const loggedRequest = await prisma.apiRequest.findUnique({ - where: {id: 'test-request-id-123'}, - }); + // TODO: Add assertion to verify request was NOT logged when disabled + // const loggedRequest = await prisma.apiRequest.findUnique({ + // where: {id: 'test-request-id-123'}, + // }); + // expect(loggedRequest).toBeNull(); // Restore original value if (originalEnv !== undefined) { @@ -378,7 +379,7 @@ describe('Request Logger Middleware', () => { const resInvalid = { ...res, locals: { - requestId: null as any, // Invalid request ID - will cause database error + requestId: null as unknown, // Invalid request ID - will cause database error }, }; @@ -460,7 +461,7 @@ describe('Request Logger Middleware', () => { }); it('should calculate response size from JSON body', async () => { - const jsonMock = vi.fn(function (this: any, body: any) { + const jsonMock = vi.fn(function (this: Response, body: unknown) { return body; }); const resLarge = { diff --git a/apps/api/src/middleware/requestLogger.ts b/apps/api/src/middleware/requestLogger.ts index e1e7b20..0fe0efb 100644 --- a/apps/api/src/middleware/requestLogger.ts +++ b/apps/api/src/middleware/requestLogger.ts @@ -107,7 +107,7 @@ export const databaseRequestLogger = (req: Request, res: Response, next: NextFun // Capture the original res.json to log after response const originalJson = res.json.bind(res); - res.json = function (body: any) { + res.json = function (body: unknown) { // Call original json method first const result = originalJson(body); @@ -124,7 +124,12 @@ export const databaseRequestLogger = (req: Request, res: Response, next: NextFun * Log the request to the database * This runs asynchronously and failures are logged but don't affect the response */ -async function logRequestToDatabase(req: Request, res: Response, startTime: number, responseBody: any): Promise { +async function logRequestToDatabase( + req: Request, + res: Response, + startTime: number, + responseBody: unknown, +): Promise { try { const requestId = res.locals.requestId as string | undefined; const auth = res.locals.auth as {type?: string; userId?: string; projectId?: string} | undefined; @@ -136,9 +141,17 @@ async function logRequestToDatabase(req: Request, res: Response, startTime: numb let errorCode: string | undefined; let errorMessage: string | undefined; - if (statusCode >= 400 && responseBody?.error) { - errorCode = responseBody.error.code; - errorMessage = responseBody.error.message; + if ( + statusCode >= 400 && + responseBody && + typeof responseBody === 'object' && + 'error' in responseBody && + responseBody.error && + typeof responseBody.error === 'object' + ) { + const error = responseBody.error as {code?: string; message?: string}; + errorCode = error.code; + errorMessage = error.message; } // Calculate request/response sizes (approximate) diff --git a/apps/api/src/services/ContactService.ts b/apps/api/src/services/ContactService.ts index b345c31..7b396ad 100644 --- a/apps/api/src/services/ContactService.ts +++ b/apps/api/src/services/ContactService.ts @@ -1,4 +1,5 @@ import {type Contact, Prisma} from '@plunk/db'; +import type {FilterCondition, FilterGroup} from '@plunk/types'; import {prisma} from '../database/prisma.js'; import {HttpException} from '../exceptions/index.js'; @@ -144,8 +145,7 @@ export class ContactService { } // Track subscription status change - const isSubscriptionChanging = - data.subscribed !== undefined && existing.subscribed !== data.subscribed; + const isSubscriptionChanging = data.subscribed !== undefined && existing.subscribed !== data.subscribed; const wasSubscribed = existing.subscribed; try { @@ -249,8 +249,7 @@ export class ContactService { if (existing) { // Track subscription status change - const isSubscriptionChanging = - subscribed !== undefined && existing.subscribed !== subscribed; + const isSubscriptionChanging = subscribed !== undefined && existing.subscribed !== subscribed; const wasSubscribed = existing.subscribed; const updated = await prisma.contact.update({ @@ -561,16 +560,10 @@ export class ContactService { // Check which segments use this field const usedInSegments = segments.filter(segment => { - const condition = segment.condition as any; + const condition = segment.condition as FilterCondition | null; return this.fieldUsedInCondition(field, condition); }); - // Get all campaigns for the project (emails) - const campaigns = await prisma.email.findMany({ - where: {projectId}, - select: {id: true, subject: true}, - }); - // For now, we'll check if campaigns use the field in their subject or body // This is a simplified check - you might want to enhance this based on your campaign structure const usedInCampaigns: Array<{id: string; name: string}> = []; @@ -604,51 +597,6 @@ export class ContactService { }; } - /** - * Helper: Check if a field is used in a filter condition (recursive) - */ - private static fieldUsedInCondition(field: string, condition: any): boolean { - if (!condition || typeof condition !== 'object') { - return false; - } - - // Check groups in the condition - if (Array.isArray(condition.groups)) { - for (const group of condition.groups) { - if (this.fieldUsedInGroup(field, group)) { - return true; - } - } - } - - return false; - } - - /** - * Helper: Check if a field is used in a filter group (recursive) - */ - private static fieldUsedInGroup(field: string, group: any): boolean { - if (!group || typeof group !== 'object') { - return false; - } - - // Check filters in the group - if (Array.isArray(group.filters)) { - for (const filter of group.filters) { - if (filter.field === field) { - return true; - } - } - } - - // Check nested conditions - if (group.conditions) { - return this.fieldUsedInCondition(field, group.conditions); - } - - return false; - } - /** * Delete a custom field from all contacts * WARNING: This is destructive and cannot be undone @@ -686,4 +634,49 @@ export class ContactService { return {deletedFrom: result}; } + + /** + * Helper: Check if a field is used in a filter condition (recursive) + */ + private static fieldUsedInCondition(field: string, condition: FilterCondition | null): boolean { + if (!condition || typeof condition !== 'object') { + return false; + } + + // Check groups in the condition + if (Array.isArray(condition.groups)) { + for (const group of condition.groups) { + if (this.fieldUsedInGroup(field, group)) { + return true; + } + } + } + + return false; + } + + /** + * Helper: Check if a field is used in a filter group (recursive) + */ + private static fieldUsedInGroup(field: string, group: FilterGroup): boolean { + if (!group || typeof group !== 'object') { + return false; + } + + // Check filters in the group + if (Array.isArray(group.filters)) { + for (const filter of group.filters) { + if (filter.field === field) { + return true; + } + } + } + + // Check nested conditions + if (group.conditions) { + return this.fieldUsedInCondition(field, group.conditions); + } + + return false; + } } diff --git a/apps/api/src/services/EventService.ts b/apps/api/src/services/EventService.ts index 83921f0..add3bd3 100644 --- a/apps/api/src/services/EventService.ts +++ b/apps/api/src/services/EventService.ts @@ -1,5 +1,6 @@ import type {Event} from '@plunk/db'; import {Prisma} from '@plunk/db'; +import type {FilterCondition, FilterGroup} from '@plunk/types'; import {prisma} from '../database/prisma.js'; import {redis} from '../database/redis.js'; @@ -177,6 +178,139 @@ export class EventService { return Array.from(fieldSet).sort(); } + /** + * Check if an event is used in any segments or workflows + * Returns usage information including which segments/workflows use the event + * + * @param projectId - The project ID + * @param eventName - The event name to check (e.g., "purchase.completed", "user.signup") + * @returns Usage information + */ + public static async getEventUsage( + projectId: string, + eventName: string, + ): Promise<{ + usedInSegments: Array<{id: string; name: string}>; + usedInWorkflows: Array<{id: string; name: string}>; + totalCount: number; + uniqueContacts: number; + canDelete: boolean; + }> { + // Get all segments for the project + const segments = await prisma.segment.findMany({ + where: {projectId}, + select: {id: true, name: true, condition: true}, + }); + + // Check which segments use this event + const usedInSegments = segments.filter(segment => { + const condition = segment.condition as FilterCondition | null; + return this.eventUsedInCondition(eventName, condition); + }); + + // Get workflows that use this event as a trigger or wait condition + const workflows = await prisma.workflow.findMany({ + where: { + projectId, + OR: [ + // Event as trigger + { + triggerType: 'EVENT', + triggerConfig: { + path: ['eventName'], + equals: eventName, + }, + }, + ], + }, + select: {id: true, name: true}, + }); + + // Also check workflow steps that wait for events + const workflowStepsWithEvent = await prisma.workflowStep.findMany({ + where: { + workflow: {projectId}, + type: 'WAIT_FOR_EVENT', + config: { + path: ['eventName'], + equals: eventName, + }, + }, + include: { + workflow: { + select: {id: true, name: true}, + }, + }, + }); + + const usedInWorkflows = [...workflows, ...workflowStepsWithEvent.map(step => step.workflow)].reduce( + (acc, workflow) => { + // Deduplicate by id + if (!acc.find((w: {id: string; name: string}) => w.id === workflow.id)) { + acc.push(workflow); + } + return acc; + }, + [] as Array<{id: string; name: string}>, + ); + + // Get event statistics + const [totalCount, uniqueContacts] = await Promise.all([ + prisma.event.count({ + where: {projectId, name: eventName}, + }), + prisma.event + .groupBy({ + by: ['contactId'], + where: {projectId, name: eventName, contactId: {not: null}}, + }) + .then(results => results.length), + ]); + + const canDelete = usedInSegments.length === 0 && usedInWorkflows.length === 0; + + return { + usedInSegments, + usedInWorkflows, + totalCount, + uniqueContacts, + canDelete, + }; + } + + /** + * Delete all events with a specific name + * WARNING: This is destructive and cannot be undone + * Should only be called after verifying the event is not in use + * + * @param projectId - The project ID + * @param eventName - The event name to delete + */ + public static async deleteEvent(projectId: string, eventName: string): Promise<{deletedCount: number}> { + // Prevent deletion of system events + if (eventName.startsWith('email.') || eventName.startsWith('segment.')) { + throw new Error('Cannot delete system events (email.* or segment.*)'); + } + + // Check if event is in use + const usage = await this.getEventUsage(projectId, eventName); + if (!usage.canDelete) { + throw new Error( + `Cannot delete event: used in ${usage.usedInSegments.length} segment(s) and ${usage.usedInWorkflows.length} workflow(s)`, + ); + } + + // Delete all events with this name + const result = await prisma.event.deleteMany({ + where: { + projectId, + name: eventName, + }, + }); + + return {deletedCount: result.count}; + } + /** * Trigger workflows based on an event * Uses Redis caching for enabled workflows to improve performance @@ -321,113 +455,10 @@ export class EventService { } } - /** - * Check if an event is used in any segments or workflows - * Returns usage information including which segments/workflows use the event - * - * @param projectId - The project ID - * @param eventName - The event name to check (e.g., "purchase.completed", "user.signup") - * @returns Usage information - */ - public static async getEventUsage( - projectId: string, - eventName: string, - ): Promise<{ - usedInSegments: Array<{id: string; name: string}>; - usedInWorkflows: Array<{id: string; name: string}>; - totalCount: number; - uniqueContacts: number; - canDelete: boolean; - }> { - // Get all segments for the project - const segments = await prisma.segment.findMany({ - where: {projectId}, - select: {id: true, name: true, condition: true}, - }); - - // Check which segments use this event - const usedInSegments = segments.filter(segment => { - const condition = segment.condition as any; - return this.eventUsedInCondition(eventName, condition); - }); - - // Get workflows that use this event as a trigger or wait condition - const workflows = await prisma.workflow.findMany({ - where: { - projectId, - OR: [ - // Event as trigger - { - triggerType: 'EVENT', - triggerConfig: { - path: ['eventName'], - equals: eventName, - }, - }, - ], - }, - select: {id: true, name: true}, - }); - - // Also check workflow steps that wait for events - const workflowStepsWithEvent = await prisma.workflowStep.findMany({ - where: { - workflow: {projectId}, - type: 'WAIT_FOR_EVENT', - config: { - path: ['eventName'], - equals: eventName, - }, - }, - include: { - workflow: { - select: {id: true, name: true}, - }, - }, - }); - - const usedInWorkflows = [ - ...workflows, - ...workflowStepsWithEvent.map(step => step.workflow), - ].reduce( - (acc, workflow) => { - // Deduplicate by id - if (!acc.find((w: {id: string; name: string}) => w.id === workflow.id)) { - acc.push(workflow); - } - return acc; - }, - [] as Array<{id: string; name: string}>, - ); - - // Get event statistics - const [totalCount, uniqueContacts] = await Promise.all([ - prisma.event.count({ - where: {projectId, name: eventName}, - }), - prisma.event - .groupBy({ - by: ['contactId'], - where: {projectId, name: eventName, contactId: {not: null}}, - }) - .then(results => results.length), - ]); - - const canDelete = usedInSegments.length === 0 && usedInWorkflows.length === 0; - - return { - usedInSegments, - usedInWorkflows, - totalCount, - uniqueContacts, - canDelete, - }; - } - /** * Helper: Check if an event is used in a filter condition (recursive) */ - private static eventUsedInCondition(eventName: string, condition: any): boolean { + private static eventUsedInCondition(eventName: string, condition: FilterCondition | null): boolean { if (!condition || typeof condition !== 'object') { return false; } @@ -447,7 +478,7 @@ export class EventService { /** * Helper: Check if an event is used in a filter group (recursive) */ - private static eventUsedInGroup(eventName: string, group: any): boolean { + private static eventUsedInGroup(eventName: string, group: FilterGroup): boolean { if (!group || typeof group !== 'object') { return false; } @@ -469,37 +500,4 @@ export class EventService { return false; } - - /** - * Delete all events with a specific name - * WARNING: This is destructive and cannot be undone - * Should only be called after verifying the event is not in use - * - * @param projectId - The project ID - * @param eventName - The event name to delete - */ - public static async deleteEvent(projectId: string, eventName: string): Promise<{deletedCount: number}> { - // Prevent deletion of system events - if (eventName.startsWith('email.') || eventName.startsWith('segment.')) { - throw new Error('Cannot delete system events (email.* or segment.*)'); - } - - // Check if event is in use - const usage = await this.getEventUsage(projectId, eventName); - if (!usage.canDelete) { - throw new Error( - `Cannot delete event: used in ${usage.usedInSegments.length} segment(s) and ${usage.usedInWorkflows.length} workflow(s)`, - ); - } - - // Delete all events with this name - const result = await prisma.event.deleteMany({ - where: { - projectId, - name: eventName, - }, - }); - - return {deletedCount: result.count}; - } } diff --git a/apps/api/src/services/__tests__/EventService.test.ts b/apps/api/src/services/__tests__/EventService.test.ts index af1b60b..2e102df 100644 --- a/apps/api/src/services/__tests__/EventService.test.ts +++ b/apps/api/src/services/__tests__/EventService.test.ts @@ -1,5 +1,5 @@ -import {describe, it, expect, beforeEach, vi, afterEach} from 'vitest'; -import {WorkflowTriggerType, WorkflowExecutionStatus} from '@plunk/db'; +import {afterEach, beforeEach, describe, expect, it, vi} from 'vitest'; +import {WorkflowExecutionStatus, WorkflowTriggerType} from '@plunk/db'; import {EventService} from '../EventService'; import {factories, getPrismaClient} from '../../../../../test/helpers'; @@ -59,7 +59,7 @@ describe('EventService', () => { // Clear Redis mock const {redis} = await import('../../database/redis'); if ('clear' in redis) { - (redis as any).clear(); + (redis as unknown as {clear: () => void}).clear(); } }); @@ -441,7 +441,6 @@ describe('EventService', () => { const contact = await factories.createContact({projectId}); const oldDate = new Date('2024-01-01'); - const recentDate = new Date('2024-06-01'); // Create old event directly await prisma.event.create({ diff --git a/apps/api/src/services/__tests__/SegmentService.operators.test.ts b/apps/api/src/services/__tests__/SegmentService.operators.test.ts index 0b7f10f..5a7d318 100644 --- a/apps/api/src/services/__tests__/SegmentService.operators.test.ts +++ b/apps/api/src/services/__tests__/SegmentService.operators.test.ts @@ -1,6 +1,6 @@ -import { describe, it, expect, beforeEach } from 'vitest'; -import { SegmentService } from '../SegmentService'; -import { factories, getPrismaClient } from '../../../../../test/helpers'; +import {beforeEach, describe, expect, it} from 'vitest'; +import {SegmentService} from '../SegmentService'; +import {factories} from '../../../../../test/helpers'; /** * Comprehensive Operator Tests for Segment Filtering @@ -16,10 +16,9 @@ import { factories, getPrismaClient } from '../../../../../test/helpers'; */ describe('SegmentService - Comprehensive Operator Tests', () => { let projectId: string; - const prisma = getPrismaClient(); beforeEach(async () => { - const { project } = await factories.createUserWithProject(); + const {project} = await factories.createUserWithProject(); projectId = project.id; }); @@ -31,15 +30,15 @@ describe('SegmentService - Comprehensive Operator Tests', () => { it('should match exact string values in JSON data fields', async () => { const match = await factories.createContact({ projectId, - data: { plan: 'premium' }, + data: {plan: 'premium'}, }); await factories.createContact({ projectId, - data: { plan: 'basic' }, + data: {plan: 'basic'}, }); const segment = await factories.createSegment(projectId, { - filters: [{ field: 'data.plan', operator: 'equals', value: 'premium' }], + filters: [{field: 'data.plan', operator: 'equals', value: 'premium'}], }); const result = await SegmentService.getContacts(projectId, segment.id); @@ -58,7 +57,7 @@ describe('SegmentService - Comprehensive Operator Tests', () => { }); const segment = await factories.createSegment(projectId, { - filters: [{ field: 'email', operator: 'equals', value: 'USER@EXAMPLE.COM' }], + filters: [{field: 'email', operator: 'equals', value: 'USER@EXAMPLE.COM'}], }); const result = await SegmentService.getContacts(projectId, segment.id); @@ -77,7 +76,7 @@ describe('SegmentService - Comprehensive Operator Tests', () => { }); const segment = await factories.createSegment(projectId, { - filters: [{ field: 'subscribed', operator: 'equals', value: true }], + filters: [{field: 'subscribed', operator: 'equals', value: true}], }); const result = await SegmentService.getContacts(projectId, segment.id); @@ -88,15 +87,15 @@ describe('SegmentService - Comprehensive Operator Tests', () => { it('should match numeric values as strings in JSON fields', async () => { const match = await factories.createContact({ projectId, - data: { userId: '12345' }, + data: {userId: '12345'}, }); await factories.createContact({ projectId, - data: { userId: '67890' }, + data: {userId: '67890'}, }); const segment = await factories.createSegment(projectId, { - filters: [{ field: 'data.userId', operator: 'equals', value: '12345' }], + filters: [{field: 'data.userId', operator: 'equals', value: '12345'}], }); const result = await SegmentService.getContacts(projectId, segment.id); @@ -109,15 +108,15 @@ describe('SegmentService - Comprehensive Operator Tests', () => { it('should exclude exact matches in JSON data fields', async () => { const match = await factories.createContact({ projectId, - data: { plan: 'premium' }, + data: {plan: 'premium'}, }); await factories.createContact({ projectId, - data: { plan: 'basic' }, + data: {plan: 'basic'}, }); const segment = await factories.createSegment(projectId, { - filters: [{ field: 'data.plan', operator: 'notEquals', value: 'basic' }], + filters: [{field: 'data.plan', operator: 'notEquals', value: 'basic'}], }); const result = await SegmentService.getContacts(projectId, segment.id); @@ -136,7 +135,7 @@ describe('SegmentService - Comprehensive Operator Tests', () => { }); const segment = await factories.createSegment(projectId, { - filters: [{ field: 'subscribed', operator: 'notEquals', value: false }], + filters: [{field: 'subscribed', operator: 'notEquals', value: false}], }); const result = await SegmentService.getContacts(projectId, segment.id); @@ -147,23 +146,23 @@ describe('SegmentService - Comprehensive Operator Tests', () => { it('should NOT include contacts where field does not exist (only excludes matching values)', async () => { const withMatchingField = await factories.createContact({ projectId, - data: { plan: 'basic' }, + data: {plan: 'basic'}, }); const withDifferentValue = await factories.createContact({ projectId, - data: { plan: 'premium' }, + data: {plan: 'premium'}, }); const withoutField = await factories.createContact({ projectId, - data: { other: 'value' }, + data: {other: 'value'}, }); const segment = await factories.createSegment(projectId, { - filters: [{ field: 'data.plan', operator: 'notEquals', value: 'basic' }], + filters: [{field: 'data.plan', operator: 'notEquals', value: 'basic'}], }); const result = await SegmentService.getContacts(projectId, segment.id); - const ids = result.contacts.map((c) => c.id); + const ids = result.contacts.map(c => c.id); // notEquals only matches where field exists and has different value expect(ids).toContain(withDifferentValue.id); expect(ids).not.toContain(withMatchingField.id); @@ -175,15 +174,15 @@ describe('SegmentService - Comprehensive Operator Tests', () => { it('should match substring in JSON data fields', async () => { const match = await factories.createContact({ projectId, - data: { company: 'Acme Corporation' }, + data: {company: 'Acme Corporation'}, }); await factories.createContact({ projectId, - data: { company: 'Other Industries' }, + data: {company: 'Other Industries'}, }); const segment = await factories.createSegment(projectId, { - filters: [{ field: 'data.company', operator: 'contains', value: 'Acme' }], + filters: [{field: 'data.company', operator: 'contains', value: 'Acme'}], }); const result = await SegmentService.getContacts(projectId, segment.id); @@ -206,11 +205,11 @@ describe('SegmentService - Comprehensive Operator Tests', () => { }); const segment = await factories.createSegment(projectId, { - filters: [{ field: 'email', operator: 'contains', value: 'company' }], + filters: [{field: 'email', operator: 'contains', value: 'company'}], }); const result = await SegmentService.getContacts(projectId, segment.id); - const ids = result.contacts.map((c) => c.id); + const ids = result.contacts.map(c => c.id); expect(ids).toContain(match1.id); expect(ids).toContain(match2.id); expect(result.contacts).toHaveLength(2); @@ -219,11 +218,11 @@ describe('SegmentService - Comprehensive Operator Tests', () => { it('should not match when field does not exist', async () => { await factories.createContact({ projectId, - data: { other: 'value' }, + data: {other: 'value'}, }); const segment = await factories.createSegment(projectId, { - filters: [{ field: 'data.company', operator: 'contains', value: 'Acme' }], + filters: [{field: 'data.company', operator: 'contains', value: 'Acme'}], }); const result = await SegmentService.getContacts(projectId, segment.id); @@ -241,7 +240,7 @@ describe('SegmentService - Comprehensive Operator Tests', () => { }); const segment = await factories.createSegment(projectId, { - filters: [{ field: 'email', operator: 'contains', value: 'gmail' }], + filters: [{field: 'email', operator: 'contains', value: 'gmail'}], }); const result = await SegmentService.getContacts(projectId, segment.id); @@ -254,15 +253,15 @@ describe('SegmentService - Comprehensive Operator Tests', () => { it('should exclude substring matches in JSON data fields', async () => { const match = await factories.createContact({ projectId, - data: { company: 'Other Industries' }, + data: {company: 'Other Industries'}, }); await factories.createContact({ projectId, - data: { company: 'Acme Corporation' }, + data: {company: 'Acme Corporation'}, }); const segment = await factories.createSegment(projectId, { - filters: [{ field: 'data.company', operator: 'notContains', value: 'Acme' }], + filters: [{field: 'data.company', operator: 'notContains', value: 'Acme'}], }); const result = await SegmentService.getContacts(projectId, segment.id); @@ -273,23 +272,23 @@ describe('SegmentService - Comprehensive Operator Tests', () => { it('should NOT include contacts where field does not exist (only excludes matching substrings)', async () => { const withoutField = await factories.createContact({ projectId, - data: { other: 'value' }, + data: {other: 'value'}, }); const withMatchingSubstring = await factories.createContact({ projectId, - data: { company: 'Acme Corporation' }, + data: {company: 'Acme Corporation'}, }); const withDifferentValue = await factories.createContact({ projectId, - data: { company: 'Other Industries' }, + data: {company: 'Other Industries'}, }); const segment = await factories.createSegment(projectId, { - filters: [{ field: 'data.company', operator: 'notContains', value: 'Acme' }], + filters: [{field: 'data.company', operator: 'notContains', value: 'Acme'}], }); const result = await SegmentService.getContacts(projectId, segment.id); - const ids = result.contacts.map((c) => c.id); + const ids = result.contacts.map(c => c.id); // notContains only matches where field exists and doesn't contain substring expect(ids).toContain(withDifferentValue.id); expect(ids).not.toContain(withMatchingSubstring.id); @@ -311,7 +310,7 @@ describe('SegmentService - Comprehensive Operator Tests', () => { }); const segment = await factories.createSegment(projectId, { - filters: [{ field: 'email', operator: 'notContains', value: 'gmail' }], + filters: [{field: 'email', operator: 'notContains', value: 'gmail'}], }); const result = await SegmentService.getContacts(projectId, segment.id); @@ -329,23 +328,23 @@ describe('SegmentService - Comprehensive Operator Tests', () => { it('should match values greater than threshold', async () => { const high = await factories.createContact({ projectId, - data: { score: 100 }, + data: {score: 100}, }); const veryHigh = await factories.createContact({ projectId, - data: { score: 200 }, + data: {score: 200}, }); await factories.createContact({ projectId, - data: { score: 50 }, + data: {score: 50}, }); const segment = await factories.createSegment(projectId, { - filters: [{ field: 'data.score', operator: 'greaterThan', value: 50 }], + filters: [{field: 'data.score', operator: 'greaterThan', value: 50}], }); const result = await SegmentService.getContacts(projectId, segment.id); - const ids = result.contacts.map((c) => c.id); + const ids = result.contacts.map(c => c.id); expect(ids).toContain(high.id); expect(ids).toContain(veryHigh.id); expect(result.contacts).toHaveLength(2); @@ -354,11 +353,11 @@ describe('SegmentService - Comprehensive Operator Tests', () => { it('should exclude values equal to threshold', async () => { await factories.createContact({ projectId, - data: { score: 50 }, + data: {score: 50}, }); const segment = await factories.createSegment(projectId, { - filters: [{ field: 'data.score', operator: 'greaterThan', value: 50 }], + filters: [{field: 'data.score', operator: 'greaterThan', value: 50}], }); const result = await SegmentService.getContacts(projectId, segment.id); @@ -368,15 +367,15 @@ describe('SegmentService - Comprehensive Operator Tests', () => { it('should work with negative numbers', async () => { const match = await factories.createContact({ projectId, - data: { temperature: 5 }, + data: {temperature: 5}, }); await factories.createContact({ projectId, - data: { temperature: -10 }, + data: {temperature: -10}, }); const segment = await factories.createSegment(projectId, { - filters: [{ field: 'data.temperature', operator: 'greaterThan', value: 0 }], + filters: [{field: 'data.temperature', operator: 'greaterThan', value: 0}], }); const result = await SegmentService.getContacts(projectId, segment.id); @@ -387,15 +386,15 @@ describe('SegmentService - Comprehensive Operator Tests', () => { it('should work with decimal values', async () => { const match = await factories.createContact({ projectId, - data: { rating: 4.5 }, + data: {rating: 4.5}, }); await factories.createContact({ projectId, - data: { rating: 3.2 }, + data: {rating: 3.2}, }); const segment = await factories.createSegment(projectId, { - filters: [{ field: 'data.rating', operator: 'greaterThan', value: 4.0 }], + filters: [{field: 'data.rating', operator: 'greaterThan', value: 4.0}], }); const result = await SegmentService.getContacts(projectId, segment.id); @@ -408,23 +407,23 @@ describe('SegmentService - Comprehensive Operator Tests', () => { it('should match values greater than or equal to threshold', async () => { const equal = await factories.createContact({ projectId, - data: { score: 50 }, + data: {score: 50}, }); const greater = await factories.createContact({ projectId, - data: { score: 100 }, + data: {score: 100}, }); await factories.createContact({ projectId, - data: { score: 25 }, + data: {score: 25}, }); const segment = await factories.createSegment(projectId, { - filters: [{ field: 'data.score', operator: 'greaterThanOrEqual', value: 50 }], + filters: [{field: 'data.score', operator: 'greaterThanOrEqual', value: 50}], }); const result = await SegmentService.getContacts(projectId, segment.id); - const ids = result.contacts.map((c) => c.id); + const ids = result.contacts.map(c => c.id); expect(ids).toContain(equal.id); expect(ids).toContain(greater.id); expect(result.contacts).toHaveLength(2); @@ -435,23 +434,23 @@ describe('SegmentService - Comprehensive Operator Tests', () => { it('should match values less than threshold', async () => { const low = await factories.createContact({ projectId, - data: { score: 25 }, + data: {score: 25}, }); const veryLow = await factories.createContact({ projectId, - data: { score: 10 }, + data: {score: 10}, }); await factories.createContact({ projectId, - data: { score: 50 }, + data: {score: 50}, }); const segment = await factories.createSegment(projectId, { - filters: [{ field: 'data.score', operator: 'lessThan', value: 50 }], + filters: [{field: 'data.score', operator: 'lessThan', value: 50}], }); const result = await SegmentService.getContacts(projectId, segment.id); - const ids = result.contacts.map((c) => c.id); + const ids = result.contacts.map(c => c.id); expect(ids).toContain(low.id); expect(ids).toContain(veryLow.id); expect(result.contacts).toHaveLength(2); @@ -460,11 +459,11 @@ describe('SegmentService - Comprehensive Operator Tests', () => { it('should exclude values equal to threshold', async () => { await factories.createContact({ projectId, - data: { score: 50 }, + data: {score: 50}, }); const segment = await factories.createSegment(projectId, { - filters: [{ field: 'data.score', operator: 'lessThan', value: 50 }], + filters: [{field: 'data.score', operator: 'lessThan', value: 50}], }); const result = await SegmentService.getContacts(projectId, segment.id); @@ -476,23 +475,23 @@ describe('SegmentService - Comprehensive Operator Tests', () => { it('should match values less than or equal to threshold', async () => { const equal = await factories.createContact({ projectId, - data: { score: 50 }, + data: {score: 50}, }); const less = await factories.createContact({ projectId, - data: { score: 25 }, + data: {score: 25}, }); await factories.createContact({ projectId, - data: { score: 100 }, + data: {score: 100}, }); const segment = await factories.createSegment(projectId, { - filters: [{ field: 'data.score', operator: 'lessThanOrEqual', value: 50 }], + filters: [{field: 'data.score', operator: 'lessThanOrEqual', value: 50}], }); const result = await SegmentService.getContacts(projectId, segment.id); - const ids = result.contacts.map((c) => c.id); + const ids = result.contacts.map(c => c.id); expect(ids).toContain(equal.id); expect(ids).toContain(less.id); expect(result.contacts).toHaveLength(2); @@ -501,17 +500,18 @@ describe('SegmentService - Comprehensive Operator Tests', () => { describe('Numeric edge cases', () => { it('should handle zero values correctly', async () => { - const zero = await factories.createContact({ + // Create a contact with balance 0 (should NOT match greaterThan 0) + await factories.createContact({ projectId, - data: { balance: 0 }, + data: {balance: 0}, }); const positive = await factories.createContact({ projectId, - data: { balance: 100 }, + data: {balance: 100}, }); const segment = await factories.createSegment(projectId, { - filters: [{ field: 'data.balance', operator: 'greaterThan', value: 0 }], + filters: [{field: 'data.balance', operator: 'greaterThan', value: 0}], }); const result = await SegmentService.getContacts(projectId, segment.id); @@ -522,15 +522,15 @@ describe('SegmentService - Comprehensive Operator Tests', () => { it('should handle very large numbers', async () => { const match = await factories.createContact({ projectId, - data: { views: 1000000 }, + data: {views: 1000000}, }); await factories.createContact({ projectId, - data: { views: 500000 }, + data: {views: 500000}, }); const segment = await factories.createSegment(projectId, { - filters: [{ field: 'data.views', operator: 'greaterThanOrEqual', value: 1000000 }], + filters: [{field: 'data.views', operator: 'greaterThanOrEqual', value: 1000000}], }); const result = await SegmentService.getContacts(projectId, segment.id); @@ -548,15 +548,15 @@ describe('SegmentService - Comprehensive Operator Tests', () => { it('should match contacts where field exists and is not null', async () => { const withField = await factories.createContact({ projectId, - data: { company: 'Acme Inc' }, + data: {company: 'Acme Inc'}, }); await factories.createContact({ projectId, - data: { name: 'John' }, + data: {name: 'John'}, }); const segment = await factories.createSegment(projectId, { - filters: [{ field: 'data.company', operator: 'exists', value: true }], + filters: [{field: 'data.company', operator: 'exists', value: true}], }); const result = await SegmentService.getContacts(projectId, segment.id); @@ -567,15 +567,15 @@ describe('SegmentService - Comprehensive Operator Tests', () => { it('should exclude contacts where field is null', async () => { const withValue = await factories.createContact({ projectId, - data: { company: 'Acme Inc' }, + data: {company: 'Acme Inc'}, }); await factories.createContact({ projectId, - data: { company: null }, + data: {company: null}, }); const segment = await factories.createSegment(projectId, { - filters: [{ field: 'data.company', operator: 'exists', value: true }], + filters: [{field: 'data.company', operator: 'exists', value: true}], }); const result = await SegmentService.getContacts(projectId, segment.id); @@ -586,11 +586,11 @@ describe('SegmentService - Comprehensive Operator Tests', () => { it('should match fields with empty string values', async () => { const withEmptyString = await factories.createContact({ projectId, - data: { notes: '' }, + data: {notes: ''}, }); const segment = await factories.createSegment(projectId, { - filters: [{ field: 'data.notes', operator: 'exists', value: true }], + filters: [{field: 'data.notes', operator: 'exists', value: true}], }); const result = await SegmentService.getContacts(projectId, segment.id); @@ -601,11 +601,11 @@ describe('SegmentService - Comprehensive Operator Tests', () => { it('should match fields with zero values', async () => { const withZero = await factories.createContact({ projectId, - data: { score: 0 }, + data: {score: 0}, }); const segment = await factories.createSegment(projectId, { - filters: [{ field: 'data.score', operator: 'exists', value: true }], + filters: [{field: 'data.score', operator: 'exists', value: true}], }); const result = await SegmentService.getContacts(projectId, segment.id); @@ -616,11 +616,11 @@ describe('SegmentService - Comprehensive Operator Tests', () => { it('should match fields with boolean false values', async () => { const withFalse = await factories.createContact({ projectId, - data: { verified: false }, + data: {verified: false}, }); const segment = await factories.createSegment(projectId, { - filters: [{ field: 'data.verified', operator: 'exists', value: true }], + filters: [{field: 'data.verified', operator: 'exists', value: true}], }); const result = await SegmentService.getContacts(projectId, segment.id); @@ -633,15 +633,15 @@ describe('SegmentService - Comprehensive Operator Tests', () => { it('should match contacts where field does not exist', async () => { const withoutField = await factories.createContact({ projectId, - data: { name: 'John' }, + data: {name: 'John'}, }); await factories.createContact({ projectId, - data: { company: 'Acme Inc' }, + data: {company: 'Acme Inc'}, }); const segment = await factories.createSegment(projectId, { - filters: [{ field: 'data.company', operator: 'notExists', value: true }], + filters: [{field: 'data.company', operator: 'notExists', value: true}], }); const result = await SegmentService.getContacts(projectId, segment.id); @@ -652,15 +652,15 @@ describe('SegmentService - Comprehensive Operator Tests', () => { it('should match contacts where field is null', async () => { const withNull = await factories.createContact({ projectId, - data: { company: null }, + data: {company: null}, }); await factories.createContact({ projectId, - data: { company: 'Acme Inc' }, + data: {company: 'Acme Inc'}, }); const segment = await factories.createSegment(projectId, { - filters: [{ field: 'data.company', operator: 'notExists', value: true }], + filters: [{field: 'data.company', operator: 'notExists', value: true}], }); const result = await SegmentService.getContacts(projectId, segment.id); @@ -671,11 +671,11 @@ describe('SegmentService - Comprehensive Operator Tests', () => { it('should exclude fields with empty string values', async () => { await factories.createContact({ projectId, - data: { notes: '' }, + data: {notes: ''}, }); const segment = await factories.createSegment(projectId, { - filters: [{ field: 'data.notes', operator: 'notExists', value: true }], + filters: [{field: 'data.notes', operator: 'notExists', value: true}], }); const result = await SegmentService.getContacts(projectId, segment.id); @@ -685,11 +685,11 @@ describe('SegmentService - Comprehensive Operator Tests', () => { it('should exclude fields with zero values', async () => { await factories.createContact({ projectId, - data: { score: 0 }, + data: {score: 0}, }); const segment = await factories.createSegment(projectId, { - filters: [{ field: 'data.score', operator: 'notExists', value: true }], + filters: [{field: 'data.score', operator: 'notExists', value: true}], }); const result = await SegmentService.getContacts(projectId, segment.id); @@ -704,7 +704,7 @@ describe('SegmentService - Comprehensive Operator Tests', () => { describe('Temporal Operators', () => { describe('within operator', () => { it('should match contacts created within specified days', async () => { - const recent = await factories.createContact({ projectId }); + const recent = await factories.createContact({projectId}); const segment = await factories.createSegment(projectId, { filters: [ @@ -718,12 +718,12 @@ describe('SegmentService - Comprehensive Operator Tests', () => { }); const result = await SegmentService.getContacts(projectId, segment.id); - const ids = result.contacts.map((c) => c.id); + const ids = result.contacts.map(c => c.id); expect(ids).toContain(recent.id); }); it('should match contacts created within specified hours', async () => { - const veryRecent = await factories.createContact({ projectId }); + const veryRecent = await factories.createContact({projectId}); const segment = await factories.createSegment(projectId, { filters: [ @@ -737,12 +737,12 @@ describe('SegmentService - Comprehensive Operator Tests', () => { }); const result = await SegmentService.getContacts(projectId, segment.id); - const ids = result.contacts.map((c) => c.id); + const ids = result.contacts.map(c => c.id); expect(ids).toContain(veryRecent.id); }); it('should match contacts created within specified minutes', async () => { - const justNow = await factories.createContact({ projectId }); + const justNow = await factories.createContact({projectId}); const segment = await factories.createSegment(projectId, { filters: [ @@ -756,7 +756,7 @@ describe('SegmentService - Comprehensive Operator Tests', () => { }); const result = await SegmentService.getContacts(projectId, segment.id); - const ids = result.contacts.map((c) => c.id); + const ids = result.contacts.map(c => c.id); expect(ids).toContain(justNow.id); }); }); @@ -767,9 +767,9 @@ describe('SegmentService - Comprehensive Operator Tests', () => { // ======================================== describe('Date Comparison Operators', () => { it('should support greaterThan for dates', async () => { - const older = await factories.createContact({ projectId }); - await new Promise((resolve) => setTimeout(resolve, 10)); - const newer = await factories.createContact({ projectId }); + const older = await factories.createContact({projectId}); + await new Promise(resolve => setTimeout(resolve, 10)); + const newer = await factories.createContact({projectId}); const segment = await factories.createSegment(projectId, { filters: [ @@ -782,17 +782,17 @@ describe('SegmentService - Comprehensive Operator Tests', () => { }); const result = await SegmentService.getContacts(projectId, segment.id); - const ids = result.contacts.map((c) => c.id); + const ids = result.contacts.map(c => c.id); expect(ids).toContain(newer.id); expect(ids).not.toContain(older.id); }); it('should support lessThanOrEqual for dates', async () => { - const first = await factories.createContact({ projectId }); - await new Promise((resolve) => setTimeout(resolve, 10)); - const second = await factories.createContact({ projectId }); - await new Promise((resolve) => setTimeout(resolve, 10)); - const third = await factories.createContact({ projectId }); + const first = await factories.createContact({projectId}); + await new Promise(resolve => setTimeout(resolve, 10)); + const second = await factories.createContact({projectId}); + await new Promise(resolve => setTimeout(resolve, 10)); + const third = await factories.createContact({projectId}); const segment = await factories.createSegment(projectId, { filters: [ @@ -805,7 +805,7 @@ describe('SegmentService - Comprehensive Operator Tests', () => { }); const result = await SegmentService.getContacts(projectId, segment.id); - const ids = result.contacts.map((c) => c.id); + const ids = result.contacts.map(c => c.id); expect(ids).toContain(first.id); expect(ids).toContain(second.id); expect(ids).not.toContain(third.id); @@ -830,27 +830,27 @@ describe('SegmentService - Comprehensive Operator Tests', () => { await factories.createContact({ projectId, subscribed: false, - data: { plan: 'premium', score: 85, company: 'Acme Inc' }, + data: {plan: 'premium', score: 85, company: 'Acme Inc'}, }); await factories.createContact({ projectId, subscribed: true, - data: { plan: 'basic', score: 85, company: 'Acme Inc' }, + data: {plan: 'basic', score: 85, company: 'Acme Inc'}, }); await factories.createContact({ projectId, subscribed: true, - data: { plan: 'premium', score: 50, company: 'Acme Inc' }, + data: {plan: 'premium', score: 50, company: 'Acme Inc'}, }); const segment = await factories.createSegment(projectId, { filters: [ - { field: 'subscribed', operator: 'equals', value: true }, - { field: 'data.plan', operator: 'equals', value: 'premium' }, - { field: 'data.score', operator: 'greaterThanOrEqual', value: 80 }, - { field: 'data.company', operator: 'contains', value: 'Acme' }, + {field: 'subscribed', operator: 'equals', value: true}, + {field: 'data.plan', operator: 'equals', value: 'premium'}, + {field: 'data.score', operator: 'greaterThanOrEqual', value: 80}, + {field: 'data.company', operator: 'contains', value: 'Acme'}, ], }); @@ -870,18 +870,18 @@ describe('SegmentService - Comprehensive Operator Tests', () => { await factories.createContact({ projectId, - data: { company: 'Tech Corp' }, // Missing revenue + data: {company: 'Tech Corp'}, // Missing revenue }); await factories.createContact({ projectId, - data: { revenue: 100000 }, // Missing company + data: {revenue: 100000}, // Missing company }); const segment = await factories.createSegment(projectId, { filters: [ - { field: 'data.company', operator: 'exists', value: true }, - { field: 'data.revenue', operator: 'greaterThanOrEqual', value: 100000 }, + {field: 'data.company', operator: 'exists', value: true}, + {field: 'data.revenue', operator: 'greaterThanOrEqual', value: 100000}, ], }); diff --git a/apps/api/src/services/__tests__/SegmentService.test.ts b/apps/api/src/services/__tests__/SegmentService.test.ts index f22dafa..3e01535 100644 --- a/apps/api/src/services/__tests__/SegmentService.test.ts +++ b/apps/api/src/services/__tests__/SegmentService.test.ts @@ -1,7 +1,10 @@ -import {describe, it, expect, beforeEach} from 'vitest'; -import {SegmentService} from '../SegmentService'; +import {beforeEach, describe, expect, it} from 'vitest'; +import {type SegmentFilter, SegmentService} from '../SegmentService'; import {factories, getPrismaClient} from '../../../../../test/helpers'; +// Type for intentionally invalid filter input (used for testing validation) +type InvalidFilterInput = Partial; + describe('SegmentService', () => { let projectId: string; const prisma = getPrismaClient(); @@ -395,10 +398,10 @@ describe('SegmentService', () => { name: 'Invalid Segment', filters: [ { - // Intentionally missing field, cast to any to bypass compile-time validation + // Intentionally missing field, cast to bypass compile-time validation operator: 'equals', value: 'test', - } as any, + } as InvalidFilterInput as SegmentFilter, ], }), ).rejects.toThrow(/field is required/i); @@ -411,10 +414,10 @@ describe('SegmentService', () => { filters: [ { field: 'email', - // Intentionally invalid operator, cast to any + // Intentionally invalid operator operator: 'DROP TABLE contacts;', value: 'test', - } as any, + } as InvalidFilterInput as SegmentFilter, ], }), ).rejects.toThrow(/invalid operator/i); @@ -428,8 +431,8 @@ describe('SegmentService', () => { { field: 'email', operator: 'equals', - // Value intentionally omitted, cast to any - } as any, + // Value intentionally omitted + } as InvalidFilterInput as SegmentFilter, ], }), ).rejects.toThrow(/requires a value/i); diff --git a/apps/api/src/services/__tests__/WorkflowConditions.operators.test.ts b/apps/api/src/services/__tests__/WorkflowConditions.operators.test.ts index 7a71ada..8d4fe29 100644 --- a/apps/api/src/services/__tests__/WorkflowConditions.operators.test.ts +++ b/apps/api/src/services/__tests__/WorkflowConditions.operators.test.ts @@ -1,5 +1,5 @@ -import {describe, it, expect, beforeEach, vi} from 'vitest'; -import {WorkflowStepType, StepExecutionStatus, WorkflowExecutionStatus} from '@plunk/db'; +import {beforeEach, describe, expect, it, vi} from 'vitest'; +import {WorkflowExecutionStatus, WorkflowStepType} from '@plunk/db'; import {WorkflowExecutionService} from '../WorkflowExecutionService'; import {factories, getPrismaClient} from '../../../../../test/helpers'; @@ -124,7 +124,8 @@ describe('Workflow CONDITION Step - Comprehensive Operator Tests', () => { }, }); - return (stepExecution?.output as any)?.branch || 'unknown'; + const output = stepExecution?.output as {branch?: string} | null; + return output?.branch || 'unknown'; } // ======================================== diff --git a/apps/api/src/services/__tests__/WorkflowExecutionService.integration.test.ts b/apps/api/src/services/__tests__/WorkflowExecutionService.integration.test.ts index 7d10564..e61b8e3 100644 --- a/apps/api/src/services/__tests__/WorkflowExecutionService.integration.test.ts +++ b/apps/api/src/services/__tests__/WorkflowExecutionService.integration.test.ts @@ -1,5 +1,5 @@ -import {describe, it, expect, beforeEach, vi} from 'vitest'; -import {WorkflowStepType, StepExecutionStatus, WorkflowExecutionStatus, TemplateType, Prisma} from '@plunk/db'; +import {beforeEach, describe, expect, it, vi} from 'vitest'; +import {Prisma, StepExecutionStatus, WorkflowExecutionStatus, WorkflowStepType} from '@plunk/db'; import {WorkflowExecutionService} from '../WorkflowExecutionService'; import {factories, getPrismaClient} from '../../../../../test/helpers'; @@ -140,7 +140,7 @@ describe('WorkflowExecutionService - Integration Tests', () => { const conditionExec = stepExecutions.find(se => se.step.type === WorkflowStepType.CONDITION); expect(conditionExec).toBeDefined(); expect(conditionExec?.status).toBe(StepExecutionStatus.COMPLETED); - expect((conditionExec?.output as any)?.branch).toBe('yes'); + expect((conditionExec?.output as {branch?: string} | null)?.branch).toBe('yes'); }); it('should follow NO branch when condition evaluates to false', async () => { @@ -229,7 +229,7 @@ describe('WorkflowExecutionService - Integration Tests', () => { const conditionExec = stepExecutions.find(se => se.step.type === WorkflowStepType.CONDITION); expect(conditionExec).toBeDefined(); - expect((conditionExec?.output as any)?.branch).toBe('no'); + expect((conditionExec?.output as {branch?: string} | null)?.branch).toBe('no'); }); it('should handle complex nested conditions', async () => { @@ -356,8 +356,8 @@ describe('WorkflowExecutionService - Integration Tests', () => { expect(stepExecutions.length).toBeGreaterThanOrEqual(3); const conditions = stepExecutions.filter(se => se.step.type === WorkflowStepType.CONDITION); expect(conditions).toHaveLength(2); - expect((conditions[0].output as any)?.branch).toBe('yes'); // US = yes - expect((conditions[1].output as any)?.branch).toBe('yes'); // Premium = yes + expect((conditions[0].output as {branch?: string} | null)?.branch).toBe('yes'); // US = yes + expect((conditions[1].output as {branch?: string} | null)?.branch).toBe('yes'); // Premium = yes }); }); @@ -695,7 +695,7 @@ describe('WorkflowExecutionService - Integration Tests', () => { // Should have: TRIGGER, CONDITION, and Path A (since segment = 'A') expect(stepExecutions.length).toBeGreaterThanOrEqual(2); const conditionExec = stepExecutions.find(se => se.step.type === WorkflowStepType.CONDITION); - expect((conditionExec?.output as any)?.branch).toBe('yes'); + expect((conditionExec?.output as {branch?: string} | null)?.branch).toBe('yes'); }); }); @@ -813,7 +813,7 @@ describe('WorkflowExecutionService - Integration Tests', () => { }); expect(conditionExec?.status).toBe(StepExecutionStatus.COMPLETED); - expect((conditionExec?.output as any)?.branch).toBe('no'); + expect((conditionExec?.output as {branch?: string} | null)?.branch).toBe('no'); }); }); @@ -1078,7 +1078,11 @@ describe('WorkflowExecutionService - Integration Tests', () => { data: {fromStepId: triggerStep!.id, toStepId: conditionStep.id}, }); await prisma.workflowTransition.create({ - data: {fromStepId: conditionStep.id, toStepId: exitStep.id, condition: {branch: 'yes'} as Prisma.InputJsonValue}, + data: { + fromStepId: conditionStep.id, + toStepId: exitStep.id, + condition: {branch: 'yes'} as Prisma.InputJsonValue, + }, }); const execution = await prisma.workflowExecution.create({ @@ -1140,7 +1144,11 @@ describe('WorkflowExecutionService - Integration Tests', () => { data: {fromStepId: triggerStep!.id, toStepId: conditionStep.id}, }); await prisma.workflowTransition.create({ - data: {fromStepId: conditionStep.id, toStepId: exitStep.id, condition: {branch: 'yes'} as Prisma.InputJsonValue}, + data: { + fromStepId: conditionStep.id, + toStepId: exitStep.id, + condition: {branch: 'yes'} as Prisma.InputJsonValue, + }, }); const execution = await prisma.workflowExecution.create({ @@ -1178,7 +1186,7 @@ describe('WorkflowExecutionService - Integration Tests', () => { status: 200, text: async () => JSON.stringify({success: true}), })); - global.fetch = mockFetch as any; + global.fetch = mockFetch as unknown as typeof fetch; const contact = await factories.createContact({ projectId, diff --git a/apps/api/src/services/__tests__/WorkflowService.test.ts b/apps/api/src/services/__tests__/WorkflowService.test.ts index aa5d9f9..2075a47 100644 --- a/apps/api/src/services/__tests__/WorkflowService.test.ts +++ b/apps/api/src/services/__tests__/WorkflowService.test.ts @@ -1,5 +1,5 @@ -import {describe, it, expect, beforeEach, afterEach, vi} from 'vitest'; -import {WorkflowTriggerType, WorkflowStepType, WorkflowExecutionStatus} from '@plunk/db'; +import {afterEach, beforeEach, describe, expect, it, vi} from 'vitest'; +import {WorkflowExecutionStatus, WorkflowStepType, WorkflowTriggerType} from '@plunk/db'; import {WorkflowService} from '../WorkflowService'; import {factories, getPrismaClient} from '../../../../../test/helpers'; @@ -58,7 +58,7 @@ describe('WorkflowService', () => { afterEach(async () => { const {redis} = await import('../../database/redis'); if ('clear' in redis) { - (redis as any).clear(); + (redis as unknown as {clear: () => void}).clear(); } }); @@ -233,9 +233,11 @@ describe('WorkflowService', () => { const result = await WorkflowService.list(projectId); - const found = result.workflows.find(w => w.id === workflow.id); - expect((found as any)._count.steps).toBe(3); // TRIGGER + 2 added - expect((found as any)._count.executions).toBe(1); + const found = result.workflows.find(w => w.id === workflow.id) as + | ((typeof result.workflows)[number] & {_count: {steps: number; executions: number}}) + | undefined; + expect(found?._count.steps).toBe(3); // TRIGGER + 2 added + expect(found?._count.executions).toBe(1); }); }); diff --git a/apps/api/src/utils/asyncHandler.ts b/apps/api/src/utils/asyncHandler.ts index b5b7b7f..bae6ff6 100644 --- a/apps/api/src/utils/asyncHandler.ts +++ b/apps/api/src/utils/asyncHandler.ts @@ -13,7 +13,7 @@ import type {NextFunction, Request, Response} from 'express'; * are caught and passed to the Express error middleware, fixing the hanging * request issue when Zod validation fails. */ -export function CatchAsync(target: any, propertyKey: string, descriptor: PropertyDescriptor) { +export function CatchAsync(target: object, propertyKey: string, descriptor: PropertyDescriptor) { const originalMethod = descriptor.value; descriptor.value = function (req: Request, res: Response, next: NextFunction) { diff --git a/apps/api/src/utils/logger.ts b/apps/api/src/utils/logger.ts index a796723..e18993c 100644 --- a/apps/api/src/utils/logger.ts +++ b/apps/api/src/utils/logger.ts @@ -155,7 +155,7 @@ export const requestLogger = (req: Request, res: Response, next: NextFunction) = // Capture the original res.json to log responses const originalJson = res.json.bind(res); - res.json = function (body: any) { + res.json = function (body: unknown) { const duration = Date.now() - startTime; logger.response(req, res, res.statusCode, duration); return originalJson(body); diff --git a/apps/smtp/src/server.ts b/apps/smtp/src/server.ts index 46b6d75..04889e7 100644 --- a/apps/smtp/src/server.ts +++ b/apps/smtp/src/server.ts @@ -1,5 +1,5 @@ import 'dotenv/config'; -import {simpleParser} from 'mailparser'; +import {type AddressObject, simpleParser} from 'mailparser'; import signale from 'signale'; import { SMTPServer, @@ -7,8 +7,8 @@ import { type SMTPServerAuthentication, type SMTPServerAuthenticationResponse, type SMTPServerDataStream, - type SMTPServerSession, type SMTPServerOptions, + type SMTPServerSession, } from 'smtp-server'; import fs from 'fs'; import path from 'path'; @@ -56,13 +56,14 @@ function loadFromTraefikAcme(): {key: Buffer; cert: Buffer} | null { const certificates = acmeData?.letsencrypt?.Certificates || acmeData?.Certificates || []; interface TraefikCert { - domain?: string | { main?: string }; + domain?: string | {main?: string}; certificate: string; key: string; } - const certData = certificates.find((cert: TraefikCert) => - (typeof cert.domain === 'object' && cert.domain?.main === SMTP_DOMAIN) || cert.domain === SMTP_DOMAIN + const certData = certificates.find( + (cert: TraefikCert) => + (typeof cert.domain === 'object' && cert.domain?.main === SMTP_DOMAIN) || cert.domain === SMTP_DOMAIN, ); if (!certData) { @@ -294,11 +295,11 @@ function handleData( const recipientNameMap = new Map(); // Helper to extract addresses from AddressObject - const extractAddresses = (addressObj: any) => { + const extractAddresses = (addressObj: AddressObject | AddressObject[] | undefined) => { if (!addressObj) return []; // Handle both single AddressObject and array const addresses = Array.isArray(addressObj) ? addressObj : [addressObj]; - return addresses.flatMap((obj: any) => obj.value || []); + return addresses.flatMap(obj => obj.value || []); }; if (parsed.to) {