Fix linting errors

This commit is contained in:
Dries Augustyns
2025-12-04 12:46:39 +01:00
parent 73a1cdfefa
commit 04634a447b
34 changed files with 517 additions and 534 deletions
@@ -1,16 +1,16 @@
import {describe, it, expect, beforeEach} from 'vitest'; import {beforeEach, describe, expect, it} from 'vitest';
import {ActionSchemas} from '@plunk/shared'; import {ActionSchemas} from '@plunk/shared';
import {factories, getPrismaClient} from '../../../../../test/helpers'; import {factories, getPrismaClient} from '../../../../../test/helpers';
import { import {
ErrorCode,
NotFound,
ValidationError,
NotAuthenticated,
NotAllowed,
RateLimitError,
ConflictError,
BadRequest, BadRequest,
ConflictError,
ErrorCode,
HttpException, HttpException,
NotAllowed,
NotAuthenticated,
NotFound,
RateLimitError,
ValidationError
} from '../../exceptions/index.js'; } from '../../exceptions/index.js';
import {EmailService} from '../../services/EmailService.js'; import {EmailService} from '../../services/EmailService.js';
@@ -69,8 +69,6 @@ describe('Actions API Integration Tests', () => {
}); });
it('should validate required fields for /v1/send', () => { it('should validate required fields for /v1/send', () => {
const result = ActionSchemas.send.safeParse({}); const result = ActionSchemas.send.safeParse({});
expect(result.success).toBe(false); expect(result.success).toBe(false);
@@ -80,8 +78,6 @@ describe('Actions API Integration Tests', () => {
}); });
it('should validate subject and body required when no template', () => { it('should validate subject and body required when no template', () => {
const result = ActionSchemas.send.safeParse({ const result = ActionSchemas.send.safeParse({
to: '[email protected]', to: '[email protected]',
// Missing subject, body, and template // Missing subject, body, and template
@@ -94,8 +90,6 @@ describe('Actions API Integration Tests', () => {
}); });
it('should validate required fields for /v1/track', () => { it('should validate required fields for /v1/track', () => {
const result = ActionSchemas.track.safeParse({ const result = ActionSchemas.track.safeParse({
email: '[email protected]', email: '[email protected]',
// Missing event name // Missing event name
@@ -271,8 +265,6 @@ describe('Actions API Integration Tests', () => {
// ======================================== // ========================================
describe('Custom HTTP Exception Types', () => { describe('Custom HTTP Exception Types', () => {
it('should structure NotFound errors correctly', () => { it('should structure NotFound errors correctly', () => {
const error = new NotFound('Template', 'abc-123'); const error = new NotFound('Template', 'abc-123');
expect(error.code).toBe(404); expect(error.code).toBe(404);
@@ -283,8 +275,6 @@ describe('Actions API Integration Tests', () => {
}); });
it('should map resources to specific error codes', () => { it('should map resources to specific error codes', () => {
const testCases = [ const testCases = [
{resource: 'contact', expectedCode: ErrorCode.CONTACT_NOT_FOUND}, {resource: 'contact', expectedCode: ErrorCode.CONTACT_NOT_FOUND},
{resource: 'template', expectedCode: ErrorCode.TEMPLATE_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', () => { it('should structure ValidationError with field details', () => {
const fieldErrors = [ const fieldErrors = [
{field: 'email', message: 'Invalid email format', code: 'invalid_email'}, {field: 'email', message: 'Invalid email format', code: 'invalid_email'},
{field: 'data.firstName', message: 'Required field', code: 'required'}, {field: 'data.firstName', message: 'Required field', code: 'required'},
@@ -315,8 +303,6 @@ describe('Actions API Integration Tests', () => {
}); });
it('should structure NotAuthenticated errors', () => { it('should structure NotAuthenticated errors', () => {
const error = new NotAuthenticated(); const error = new NotAuthenticated();
expect(error.code).toBe(401); expect(error.code).toBe(401);
@@ -324,8 +310,6 @@ describe('Actions API Integration Tests', () => {
}); });
it('should structure NotAllowed errors', () => { it('should structure NotAllowed errors', () => {
const error = new NotAllowed('Cannot perform action', 'Insufficient permissions'); const error = new NotAllowed('Cannot perform action', 'Insufficient permissions');
expect(error.code).toBe(403); expect(error.code).toBe(403);
@@ -334,8 +318,6 @@ describe('Actions API Integration Tests', () => {
}); });
it('should structure RateLimitError', () => { it('should structure RateLimitError', () => {
const error = new RateLimitError('Too many requests', 60); const error = new RateLimitError('Too many requests', 60);
expect(error.code).toBe(429); expect(error.code).toBe(429);
@@ -344,8 +326,6 @@ describe('Actions API Integration Tests', () => {
}); });
it('should structure ConflictError', () => { it('should structure ConflictError', () => {
const error = new ConflictError('Contact exists', {email: '[email protected]'}); const error = new ConflictError('Contact exists', {email: '[email protected]'});
expect(error.code).toBe(409); expect(error.code).toBe(409);
@@ -354,8 +334,6 @@ describe('Actions API Integration Tests', () => {
}); });
it('should structure BadRequest errors', () => { it('should structure BadRequest errors', () => {
const error = new BadRequest('Invalid format', ErrorCode.INVALID_REQUEST_BODY, { const error = new BadRequest('Invalid format', ErrorCode.INVALID_REQUEST_BODY, {
expected: 'JSON', expected: 'JSON',
}); });
@@ -381,7 +359,6 @@ describe('Actions API Integration Tests', () => {
type: 'MARKETING', type: 'MARKETING',
}); });
await expect( await expect(
EmailService.sendTransactionalEmail({ EmailService.sendTransactionalEmail({
projectId, projectId,
@@ -395,7 +372,6 @@ describe('Actions API Integration Tests', () => {
}); });
it('should return NotFound when template does not exist', async () => { it('should return NotFound when template does not exist', async () => {
const contact = await factories.createContact({projectId});
const nonExistentId = '00000000-0000-0000-0000-000000000000'; const nonExistentId = '00000000-0000-0000-0000-000000000000';
const template = await prisma.template.findUnique({ const template = await prisma.template.findUnique({
@@ -413,8 +389,6 @@ describe('Actions API Integration Tests', () => {
}); });
it('should handle billing limit exceeded', () => { it('should handle billing limit exceeded', () => {
const error = new HttpException(429, 'Billing limit exceeded'); const error = new HttpException(429, 'Billing limit exceeded');
expect(error.code).toBe(429); expect(error.code).toBe(429);
@@ -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 {factories, getPrismaClient} from '../../../../../test/helpers';
import {DomainService} from '../../services/DomainService.js'; import {DomainService} from '../../services/DomainService.js';
import * as SESService from '../../services/SESService.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 () => { 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'; const domain = 'shared-domain.com';
// User 1 adds the domain to their project // User 1 adds the domain to their project
@@ -101,7 +101,7 @@ describe('Domain Verification and Ownership Tests', () => {
describe('Preventing Unauthorized Domain Linking', () => { describe('Preventing Unauthorized Domain Linking', () => {
it('should prevent linking a domain to another project when user is not a member', async () => { it('should prevent linking a domain to another project when user is not a member', async () => {
const {project: project1} = await factories.createUserWithProject(); 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'; const domain = 'protected-domain.com';
// Project 1 adds the domain // Project 1 adds the domain
@@ -172,9 +172,7 @@ describe('Domain Verification and Ownership Tests', () => {
await DomainService.addDomain(project.id, domain); await DomainService.addDomain(project.id, domain);
// Try to verify email domain // Try to verify email domain
await expect( await expect(DomainService.verifyEmailDomain(`sender@${domain}`, project.id)).rejects.toThrow(/not verified/i);
DomainService.verifyEmailDomain(`sender@${domain}`, project.id),
).rejects.toThrow(/not verified/i);
}); });
it('should allow using verified domain for sending emails', async () => { 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 // Try to use from different project
await expect( await expect(DomainService.verifyEmailDomain(`sender@${domain}`, project2.id)).rejects.toThrow(
DomainService.verifyEmailDomain(`sender@${domain}`, project2.id), /belongs to a different project/i,
).rejects.toThrow(/belongs to a different project/i); );
}); });
it('should prevent using unregistered domain', async () => { it('should prevent using unregistered domain', async () => {
@@ -220,9 +218,7 @@ describe('Domain Verification and Ownership Tests', () => {
const domain = 'not-registered.com'; const domain = 'not-registered.com';
// Try to use domain that was never added // Try to use domain that was never added
await expect( await expect(DomainService.verifyEmailDomain(`sender@${domain}`, project.id)).rejects.toThrow(/not registered/i);
DomainService.verifyEmailDomain(`sender@${domain}`, project.id),
).rejects.toThrow(/not registered/i);
}); });
}); });
+2 -2
View File
@@ -19,7 +19,7 @@ import {
SMTP_ENABLED, SMTP_ENABLED,
STRIPE_ENABLED, STRIPE_ENABLED,
TRACKING_TOGGLE_ENABLED, TRACKING_TOGGLE_ENABLED,
WIKI_URI WIKI_URI,
} from './app/constants.js'; } from './app/constants.js';
import {Actions} from './controllers/Actions.js'; import {Actions} from './controllers/Actions.js';
import {Activity} from './controllers/Activity.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('.'), field: err.path.join('.'),
message: err.message, message: err.message,
code: err.code, 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; const statusCode = 422;
+3 -8
View File
@@ -49,7 +49,7 @@ export class Actions {
@Post('track') @Post('track')
@Middleware([requirePublicKey]) @Middleware([requirePublicKey])
@CatchAsync @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 auth = res.locals.auth as AuthResponse;
// Zod validation - errors automatically handled by global error handler // Zod validation - errors automatically handled by global error handler
@@ -155,7 +155,7 @@ export class Actions {
@Post('send') @Post('send')
@Middleware([requireSecretKey]) @Middleware([requireSecretKey])
@CatchAsync @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; const auth = res.locals.auth as AuthResponse;
// Zod validation - errors automatically handled by global error handler // Zod validation - errors automatically handled by global error handler
@@ -244,12 +244,7 @@ export class Actions {
: (data as Record<string, unknown> | undefined); : (data as Record<string, unknown> | undefined);
// Create or update contact with metadata // Create or update contact with metadata
const contact = await ContactService.upsert( const contact = await ContactService.upsert(auth.projectId, recipient.email, recipientData, subscribed);
auth.projectId,
recipient.email,
recipientData,
subscribed,
);
// Get merged data including non-persistent fields for template rendering // Get merged data including non-persistent fields for template rendering
const mergedData = ContactService.getMergedData(contact, data as Record<string, unknown> | undefined); const mergedData = ContactService.getMergedData(contact, data as Record<string, unknown> | undefined);
+5 -5
View File
@@ -23,7 +23,7 @@ export class Activity {
@Get('') @Get('')
@Middleware([requireAuth]) @Middleware([requireAuth])
@CatchAsync @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 auth = res.locals.auth as AuthResponse;
const limit = Math.min(parseInt(req.query.limit as string) || 50, 100); const limit = Math.min(parseInt(req.query.limit as string) || 50, 100);
const cursor = req.query.cursor as string | undefined; const cursor = req.query.cursor as string | undefined;
@@ -64,7 +64,7 @@ export class Activity {
@Get('stats') @Get('stats')
@Middleware([requireAuth]) @Middleware([requireAuth])
@CatchAsync @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 auth = res.locals.auth as AuthResponse;
const startDate = req.query.startDate ? new Date(req.query.startDate as string) : undefined; 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; const endDate = req.query.endDate ? new Date(req.query.endDate as string) : undefined;
@@ -84,7 +84,7 @@ export class Activity {
@Get('recent-count') @Get('recent-count')
@Middleware([requireAuth]) @Middleware([requireAuth])
@CatchAsync @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 auth = res.locals.auth as AuthResponse;
const minutes = Math.min(parseInt(req.query.minutes as string) || 5, 60); // Max 60 minutes const minutes = Math.min(parseInt(req.query.minutes as string) || 5, 60); // Max 60 minutes
@@ -100,7 +100,7 @@ export class Activity {
@Get('types') @Get('types')
@Middleware([requireAuth]) @Middleware([requireAuth])
@CatchAsync @CatchAsync
public async getTypes(_req: Request, res: Response, next: NextFunction) { public async getTypes(_req: Request, res: Response, _next: NextFunction) {
const types = Object.values(ActivityType); const types = Object.values(ActivityType);
return res.status(200).json({types}); return res.status(200).json({types});
} }
@@ -116,7 +116,7 @@ export class Activity {
@Get('upcoming') @Get('upcoming')
@Middleware([requireAuth]) @Middleware([requireAuth])
@CatchAsync @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 auth = res.locals.auth as AuthResponse;
const limit = Math.min(parseInt(req.query.limit as string) || 50, 100); const limit = Math.min(parseInt(req.query.limit as string) || 50, 100);
const daysAhead = Math.min(parseInt(req.query.daysAhead as string) || 30, 90); const daysAhead = Math.min(parseInt(req.query.daysAhead as string) || 30, 90);
+2 -2
View File
@@ -15,7 +15,7 @@ import {CatchAsync} from '../utils/asyncHandler.js';
export class Auth { export class Auth {
@Post('login') @Post('login')
@CatchAsync @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 {email, password} = AuthenticationSchemas.login.parse(req.body);
const user = await UserService.email(email); const user = await UserService.email(email);
@@ -46,7 +46,7 @@ export class Auth {
@Post('signup') @Post('signup')
@CatchAsync @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 {email, password} = AuthenticationSchemas.login.parse(req.body);
const user = await UserService.email(email); const user = await UserService.email(email);
+10 -10
View File
@@ -19,7 +19,7 @@ export class Campaigns {
@Post('') @Post('')
@Middleware([requireAuth]) @Middleware([requireAuth])
@CatchAsync @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 auth = res.locals.auth as AuthResponse;
const {name, description, subject, body, from, fromName, replyTo, audienceType, audienceCondition, segmentId} = const {name, description, subject, body, from, fromName, replyTo, audienceType, audienceCondition, segmentId} =
CampaignSchemas.create.parse(req.body); CampaignSchemas.create.parse(req.body);
@@ -62,7 +62,7 @@ export class Campaigns {
@Get('') @Get('')
@Middleware([requireAuth]) @Middleware([requireAuth])
@CatchAsync @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 auth = res.locals.auth as AuthResponse;
const status = req.query.status as CampaignStatus | undefined; const status = req.query.status as CampaignStatus | undefined;
const page = parseInt(req.query.page as string) || 1; const page = parseInt(req.query.page as string) || 1;
@@ -95,7 +95,7 @@ export class Campaigns {
@Get(':id') @Get(':id')
@Middleware([requireAuth]) @Middleware([requireAuth])
@CatchAsync @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 auth = res.locals.auth as AuthResponse;
const {id} = req.params; const {id} = req.params;
@@ -114,7 +114,7 @@ export class Campaigns {
@Put(':id') @Put(':id')
@Middleware([requireAuth]) @Middleware([requireAuth])
@CatchAsync @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 auth = res.locals.auth as AuthResponse;
const {id} = req.params; const {id} = req.params;
const {name, description, subject, body, from, fromName, replyTo, audienceType, audienceCondition, segmentId} = const {name, description, subject, body, from, fromName, replyTo, audienceType, audienceCondition, segmentId} =
@@ -160,7 +160,7 @@ export class Campaigns {
@Delete(':id') @Delete(':id')
@Middleware([requireAuth]) @Middleware([requireAuth])
@CatchAsync @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 auth = res.locals.auth as AuthResponse;
const {id} = req.params; const {id} = req.params;
@@ -179,7 +179,7 @@ export class Campaigns {
@Post(':id/duplicate') @Post(':id/duplicate')
@Middleware([requireAuth]) @Middleware([requireAuth])
@CatchAsync @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 auth = res.locals.auth as AuthResponse;
const {id} = req.params; const {id} = req.params;
@@ -199,7 +199,7 @@ export class Campaigns {
@Post(':id/send') @Post(':id/send')
@Middleware([requireAuth]) @Middleware([requireAuth])
@CatchAsync @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 auth = res.locals.auth as AuthResponse;
const {id} = req.params; const {id} = req.params;
const scheduledFor = req.body?.scheduledFor; const scheduledFor = req.body?.scheduledFor;
@@ -230,7 +230,7 @@ export class Campaigns {
@Post(':id/cancel') @Post(':id/cancel')
@Middleware([requireAuth]) @Middleware([requireAuth])
@CatchAsync @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 auth = res.locals.auth as AuthResponse;
const {id} = req.params; const {id} = req.params;
@@ -250,7 +250,7 @@ export class Campaigns {
@Get(':id/stats') @Get(':id/stats')
@Middleware([requireAuth]) @Middleware([requireAuth])
@CatchAsync @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 auth = res.locals.auth as AuthResponse;
const {id} = req.params; const {id} = req.params;
@@ -269,7 +269,7 @@ export class Campaigns {
@Post(':id/test') @Post(':id/test')
@Middleware([requireAuth]) @Middleware([requireAuth])
@CatchAsync @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 auth = res.locals.auth as AuthResponse;
const {id} = req.params; const {id} = req.params;
const {email} = CampaignSchemas.sendTest.parse(req.body); const {email} = CampaignSchemas.sendTest.parse(req.body);
+14 -14
View File
@@ -33,7 +33,7 @@ export class Contacts {
@Get('') @Get('')
@Middleware([requireAuth]) @Middleware([requireAuth])
@CatchAsync @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 auth = res.locals.auth as AuthResponse;
const limit = Math.min(parseInt(req.query.limit as string) || 20, 100); const limit = Math.min(parseInt(req.query.limit as string) || 20, 100);
const cursor = req.query.cursor as string | undefined; const cursor = req.query.cursor as string | undefined;
@@ -52,7 +52,7 @@ export class Contacts {
@Get('fields') @Get('fields')
@Middleware([requireAuth]) @Middleware([requireAuth])
@CatchAsync @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 auth = res.locals.auth as AuthResponse;
try { try {
@@ -78,7 +78,7 @@ export class Contacts {
@Get('fields/:field/values') @Get('fields/:field/values')
@Middleware([requireAuth]) @Middleware([requireAuth])
@CatchAsync @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 auth = res.locals.auth as AuthResponse;
const field = req.params.field; const field = req.params.field;
const limit = Math.min(parseInt(req.query.limit as string) || 100, 200); const limit = Math.min(parseInt(req.query.limit as string) || 100, 200);
@@ -111,7 +111,7 @@ export class Contacts {
@Get(':id') @Get(':id')
@Middleware([requireAuth]) @Middleware([requireAuth])
@CatchAsync @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 auth = res.locals.auth as AuthResponse;
const contactId = req.params.id; const contactId = req.params.id;
@@ -131,7 +131,7 @@ export class Contacts {
@Post('') @Post('')
@Middleware([requireAuth]) @Middleware([requireAuth])
@CatchAsync @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 auth = res.locals.auth as AuthResponse;
const {email, data, subscribed} = req.body; const {email, data, subscribed} = req.body;
@@ -161,7 +161,7 @@ export class Contacts {
@Patch(':id') @Patch(':id')
@Middleware([requireAuth]) @Middleware([requireAuth])
@CatchAsync @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 auth = res.locals.auth as AuthResponse;
const contactId = req.params.id; const contactId = req.params.id;
const {email, data, subscribed} = req.body; const {email, data, subscribed} = req.body;
@@ -182,7 +182,7 @@ export class Contacts {
@Delete(':id') @Delete(':id')
@Middleware([requireAuth]) @Middleware([requireAuth])
@CatchAsync @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 auth = res.locals.auth as AuthResponse;
const contactId = req.params.id; const contactId = req.params.id;
@@ -201,7 +201,7 @@ export class Contacts {
*/ */
@Get('public/:id') @Get('public/:id')
@CatchAsync @CatchAsync
public async getPublic(req: Request, res: Response, next: NextFunction) { public async getPublic(req: Request, res: Response, _next: NextFunction) {
const contactId = req.params.id; const contactId = req.params.id;
if (!contactId) { if (!contactId) {
@@ -223,7 +223,7 @@ export class Contacts {
*/ */
@Post('public/:id/subscribe') @Post('public/:id/subscribe')
@CatchAsync @CatchAsync
public async subscribePublic(req: Request, res: Response, next: NextFunction) { public async subscribePublic(req: Request, res: Response, _next: NextFunction) {
const contactId = req.params.id; const contactId = req.params.id;
if (!contactId) { if (!contactId) {
@@ -245,7 +245,7 @@ export class Contacts {
*/ */
@Post('public/:id/unsubscribe') @Post('public/:id/unsubscribe')
@CatchAsync @CatchAsync
public async unsubscribePublic(req: Request, res: Response, next: NextFunction) { public async unsubscribePublic(req: Request, res: Response, _next: NextFunction) {
const contactId = req.params.id; const contactId = req.params.id;
if (!contactId) { if (!contactId) {
@@ -268,7 +268,7 @@ export class Contacts {
@Post('import') @Post('import')
@Middleware([requireAuth, upload.single('file')]) @Middleware([requireAuth, upload.single('file')])
@CatchAsync @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; const auth = res.locals.auth as AuthResponse;
if (!req.file) { if (!req.file) {
@@ -302,7 +302,7 @@ export class Contacts {
@Get('import/:jobId') @Get('import/:jobId')
@Middleware([requireAuth]) @Middleware([requireAuth])
@CatchAsync @CatchAsync
public async getImportStatus(req: Request, res: Response, next: NextFunction) { public async getImportStatus(req: Request, res: Response, _next: NextFunction) {
const jobId = req.params.jobId; const jobId = req.params.jobId;
if (!jobId) { if (!jobId) {
@@ -333,7 +333,7 @@ export class Contacts {
@Get('fields/:field/usage') @Get('fields/:field/usage')
@Middleware([requireAuth]) @Middleware([requireAuth])
@CatchAsync @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 auth = res.locals.auth as AuthResponse;
const field = req.params.field; const field = req.params.field;
@@ -360,7 +360,7 @@ export class Contacts {
@Delete('fields/:field') @Delete('fields/:field')
@Middleware([requireAuth]) @Middleware([requireAuth])
@CatchAsync @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 auth = res.locals.auth as AuthResponse;
const field = req.params.field; const field = req.params.field;
+4 -4
View File
@@ -19,7 +19,7 @@ export class Domains {
@Get('project/:projectId') @Get('project/:projectId')
@Middleware([isAuthenticated]) @Middleware([isAuthenticated])
@CatchAsync @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 auth = res.locals.auth as AuthResponse;
const {projectId} = DomainSchemas.projectId.parse(req.params); const {projectId} = DomainSchemas.projectId.parse(req.params);
@@ -46,7 +46,7 @@ export class Domains {
@Post('') @Post('')
@Middleware([isAuthenticated]) @Middleware([isAuthenticated])
@CatchAsync @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 auth = res.locals.auth as AuthResponse;
const {projectId, domain} = DomainSchemas.create.parse(req.body); const {projectId, domain} = DomainSchemas.create.parse(req.body);
@@ -106,7 +106,7 @@ export class Domains {
@Get(':id/verify') @Get(':id/verify')
@Middleware([isAuthenticated]) @Middleware([isAuthenticated])
@CatchAsync @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 auth = res.locals.auth as AuthResponse;
const {id} = UtilitySchemas.id.parse(req.params); const {id} = UtilitySchemas.id.parse(req.params);
@@ -143,7 +143,7 @@ export class Domains {
@Delete(':id') @Delete(':id')
@Middleware([isAuthenticated]) @Middleware([isAuthenticated])
@CatchAsync @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 auth = res.locals.auth as AuthResponse;
const {id} = UtilitySchemas.id.parse(req.params); const {id} = UtilitySchemas.id.parse(req.params);
+7 -7
View File
@@ -15,7 +15,7 @@ export class Events {
@Post('track') @Post('track')
@Middleware([requireAuth]) @Middleware([requireAuth])
@CatchAsync @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 auth = res.locals.auth as AuthResponse;
const {name, contactId, emailId, data} = req.body; const {name, contactId, emailId, data} = req.body;
@@ -35,7 +35,7 @@ export class Events {
@Get('') @Get('')
@Middleware([requireAuth]) @Middleware([requireAuth])
@CatchAsync @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 auth = res.locals.auth as AuthResponse;
const eventName = req.query.eventName as string | undefined; const eventName = req.query.eventName as string | undefined;
const limit = parseInt(req.query.limit as string) || 100; const limit = parseInt(req.query.limit as string) || 100;
@@ -52,7 +52,7 @@ export class Events {
@Get('stats') @Get('stats')
@Middleware([requireAuth]) @Middleware([requireAuth])
@CatchAsync @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 auth = res.locals.auth as AuthResponse;
const startDate = req.query.startDate ? new Date(req.query.startDate as string) : undefined; 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; const endDate = req.query.endDate ? new Date(req.query.endDate as string) : undefined;
@@ -69,7 +69,7 @@ export class Events {
@Get('contact/:contactId') @Get('contact/:contactId')
@Middleware([requireAuth]) @Middleware([requireAuth])
@CatchAsync @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 auth = res.locals.auth as AuthResponse;
const contactId = req.params.contactId; const contactId = req.params.contactId;
const limit = parseInt(req.query.limit as string) || 50; const limit = parseInt(req.query.limit as string) || 50;
@@ -90,7 +90,7 @@ export class Events {
@Get('names') @Get('names')
@Middleware([requireAuth]) @Middleware([requireAuth])
@CatchAsync @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 auth = res.locals.auth as AuthResponse;
const eventNames = await EventService.getUniqueEventNames(auth.projectId!); const eventNames = await EventService.getUniqueEventNames(auth.projectId!);
@@ -106,7 +106,7 @@ export class Events {
@Get(':eventName/usage') @Get(':eventName/usage')
@Middleware([requireAuth]) @Middleware([requireAuth])
@CatchAsync @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 auth = res.locals.auth as AuthResponse;
const eventName = req.params.eventName; const eventName = req.params.eventName;
@@ -133,7 +133,7 @@ export class Events {
@Delete(':eventName') @Delete(':eventName')
@Middleware([requireAuth]) @Middleware([requireAuth])
@CatchAsync @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 auth = res.locals.auth as AuthResponse;
const eventName = req.params.eventName; const eventName = req.params.eventName;
+1 -1
View File
@@ -33,7 +33,7 @@ export class Github {
@Get('callback') @Get('callback')
@CatchAsync @CatchAsync
public async callback(req: Request, res: Response, next: NextFunction) { public async callback(req: Request, res: Response, _next: NextFunction) {
if (!GITHUB_OAUTH_ENABLED) { if (!GITHUB_OAUTH_ENABLED) {
return res.status(404).json({error: 'GitHub OAuth is not configured'}); return res.status(404).json({error: 'GitHub OAuth is not configured'});
} }
+1 -1
View File
@@ -28,7 +28,7 @@ export class Google {
@Get('callback') @Get('callback')
@CatchAsync @CatchAsync
public async callback(req: Request, res: Response, next: NextFunction) { public async callback(req: Request, res: Response, _next: NextFunction) {
if (!GOOGLE_OAUTH_ENABLED) { if (!GOOGLE_OAUTH_ENABLED) {
return res.status(404).json({error: 'Google OAuth is not configured'}); return res.status(404).json({error: 'Google OAuth is not configured'});
} }
+2 -2
View File
@@ -16,7 +16,7 @@ export class Projects {
@Get(':id/setup-state') @Get(':id/setup-state')
@Middleware([requireAuth]) @Middleware([requireAuth])
@CatchAsync @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 auth = res.locals.auth as AuthResponse;
const {id} = req.params; const {id} = req.params;
@@ -89,7 +89,7 @@ export class Projects {
@Get(':id/members') @Get(':id/members')
@Middleware([requireAuth]) @Middleware([requireAuth])
@CatchAsync @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 auth = res.locals.auth as AuthResponse;
const {id} = req.params; const {id} = req.params;
+8 -8
View File
@@ -15,7 +15,7 @@ export class Segments {
@Get('') @Get('')
@Middleware([requireAuth]) @Middleware([requireAuth])
@CatchAsync @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 auth = res.locals.auth as AuthResponse;
const segments = await SegmentService.list(auth.projectId!); const segments = await SegmentService.list(auth.projectId!);
@@ -30,7 +30,7 @@ export class Segments {
@Get(':id') @Get(':id')
@Middleware([requireAuth]) @Middleware([requireAuth])
@CatchAsync @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 auth = res.locals.auth as AuthResponse;
const segmentId = req.params.id; const segmentId = req.params.id;
@@ -50,7 +50,7 @@ export class Segments {
@Get(':id/contacts') @Get(':id/contacts')
@Middleware([requireAuth]) @Middleware([requireAuth])
@CatchAsync @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 auth = res.locals.auth as AuthResponse;
const segmentId = req.params.id; const segmentId = req.params.id;
const page = parseInt(req.query.page as string) || 1; const page = parseInt(req.query.page as string) || 1;
@@ -72,7 +72,7 @@ export class Segments {
@Post('') @Post('')
@Middleware([requireAuth]) @Middleware([requireAuth])
@CatchAsync @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 auth = res.locals.auth as AuthResponse;
const {name, description, condition, trackMembership} = req.body; const {name, description, condition, trackMembership} = req.body;
@@ -101,7 +101,7 @@ export class Segments {
@Patch(':id') @Patch(':id')
@Middleware([requireAuth]) @Middleware([requireAuth])
@CatchAsync @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 auth = res.locals.auth as AuthResponse;
const segmentId = req.params.id; const segmentId = req.params.id;
const {name, description, condition, trackMembership} = req.body; const {name, description, condition, trackMembership} = req.body;
@@ -131,7 +131,7 @@ export class Segments {
@Delete(':id') @Delete(':id')
@Middleware([requireAuth]) @Middleware([requireAuth])
@CatchAsync @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 auth = res.locals.auth as AuthResponse;
const segmentId = req.params.id; const segmentId = req.params.id;
@@ -151,7 +151,7 @@ export class Segments {
@Post(':id/compute') @Post(':id/compute')
@Middleware([requireAuth]) @Middleware([requireAuth])
@CatchAsync @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 auth = res.locals.auth as AuthResponse;
const segmentId = req.params.id; const segmentId = req.params.id;
@@ -171,7 +171,7 @@ export class Segments {
@Post(':id/refresh') @Post(':id/refresh')
@Middleware([requireAuth]) @Middleware([requireAuth])
@CatchAsync @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 auth = res.locals.auth as AuthResponse;
const segmentId = req.params.id; const segmentId = req.params.id;
+7 -7
View File
@@ -17,7 +17,7 @@ export class Templates {
@Get('') @Get('')
@Middleware([requireAuth]) @Middleware([requireAuth])
@CatchAsync @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 auth = res.locals.auth as AuthResponse;
const page = parseInt(req.query.page as string) || 1; const page = parseInt(req.query.page as string) || 1;
const pageSize = Math.min(parseInt(req.query.pageSize as string) || 20, 100); const pageSize = Math.min(parseInt(req.query.pageSize as string) || 20, 100);
@@ -36,7 +36,7 @@ export class Templates {
@Get(':id') @Get(':id')
@Middleware([requireAuth]) @Middleware([requireAuth])
@CatchAsync @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 auth = res.locals.auth as AuthResponse;
const templateId = req.params.id; const templateId = req.params.id;
@@ -56,7 +56,7 @@ export class Templates {
@Post('') @Post('')
@Middleware([requireAuth]) @Middleware([requireAuth])
@CatchAsync @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 auth = res.locals.auth as AuthResponse;
const {name, description, subject, body, from, fromName, replyTo, type} = req.body; const {name, description, subject, body, from, fromName, replyTo, type} = req.body;
@@ -100,7 +100,7 @@ export class Templates {
@Patch(':id') @Patch(':id')
@Middleware([requireAuth]) @Middleware([requireAuth])
@CatchAsync @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 auth = res.locals.auth as AuthResponse;
const templateId = req.params.id; const templateId = req.params.id;
const {name, description, subject, body, from, fromName, replyTo, type} = req.body; const {name, description, subject, body, from, fromName, replyTo, type} = req.body;
@@ -135,7 +135,7 @@ export class Templates {
@Delete(':id') @Delete(':id')
@Middleware([requireAuth]) @Middleware([requireAuth])
@CatchAsync @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 auth = res.locals.auth as AuthResponse;
const templateId = req.params.id; const templateId = req.params.id;
@@ -155,7 +155,7 @@ export class Templates {
@Post(':id/duplicate') @Post(':id/duplicate')
@Middleware([requireAuth]) @Middleware([requireAuth])
@CatchAsync @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 auth = res.locals.auth as AuthResponse;
const templateId = req.params.id; const templateId = req.params.id;
@@ -175,7 +175,7 @@ export class Templates {
@Get(':id/usage') @Get(':id/usage')
@Middleware([requireAuth]) @Middleware([requireAuth])
@CatchAsync @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 auth = res.locals.auth as AuthResponse;
const templateId = req.params.id; const templateId = req.params.id;
+1 -1
View File
@@ -34,7 +34,7 @@ export class Uploads {
@Post('image') @Post('image')
@Middleware([requireAuth, upload.single('image')]) @Middleware([requireAuth, upload.single('image')])
@CatchAsync @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; const auth = res.locals.auth as AuthResponse;
try { try {
+14 -14
View File
@@ -20,7 +20,7 @@ export class Users {
@Get('@me') @Get('@me')
@Middleware([isAuthenticated]) @Middleware([isAuthenticated])
@CatchAsync @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; const auth = res.locals.auth as AuthResponse;
if (!auth.userId) { if (!auth.userId) {
@@ -39,7 +39,7 @@ export class Users {
@Get('@me/projects') @Get('@me/projects')
@Middleware([isAuthenticated]) @Middleware([isAuthenticated])
@CatchAsync @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; const auth = res.locals.auth as AuthResponse;
if (!auth.userId) { if (!auth.userId) {
@@ -54,7 +54,7 @@ export class Users {
@Post('@me/projects') @Post('@me/projects')
@Middleware([isAuthenticated]) @Middleware([isAuthenticated])
@CatchAsync @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; const auth = res.locals.auth as AuthResponse;
if (!auth.userId) { if (!auth.userId) {
@@ -88,7 +88,7 @@ export class Users {
@Patch('@me/projects/:id') @Patch('@me/projects/:id')
@Middleware([isAuthenticated]) @Middleware([isAuthenticated])
@CatchAsync @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 auth = res.locals.auth as AuthResponse;
const {id} = req.params; const {id} = req.params;
const data = ProjectSchemas.update.parse(req.body); const data = ProjectSchemas.update.parse(req.body);
@@ -120,7 +120,7 @@ export class Users {
@Post('@me/projects/:id/regenerate-keys') @Post('@me/projects/:id/regenerate-keys')
@Middleware([isAuthenticated]) @Middleware([isAuthenticated])
@CatchAsync @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 auth = res.locals.auth as AuthResponse;
const {id} = req.params; const {id} = req.params;
@@ -158,7 +158,7 @@ export class Users {
@Post('@me/projects/:id/checkout') @Post('@me/projects/:id/checkout')
@Middleware([isAuthenticated]) @Middleware([isAuthenticated])
@CatchAsync @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 auth = res.locals.auth as AuthResponse;
const {id} = req.params; const {id} = req.params;
@@ -237,7 +237,7 @@ export class Users {
@Post('@me/projects/:id/billing-portal') @Post('@me/projects/:id/billing-portal')
@Middleware([isAuthenticated]) @Middleware([isAuthenticated])
@CatchAsync @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 auth = res.locals.auth as AuthResponse;
const {id} = req.params; const {id} = req.params;
@@ -287,7 +287,7 @@ export class Users {
@Get('@me/projects/:id/billing-limits') @Get('@me/projects/:id/billing-limits')
@Middleware([isAuthenticated]) @Middleware([isAuthenticated])
@CatchAsync @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 auth = res.locals.auth as AuthResponse;
const {id} = req.params; const {id} = req.params;
@@ -320,7 +320,7 @@ export class Users {
@Put('@me/projects/:id/billing-limits') @Put('@me/projects/:id/billing-limits')
@Middleware([isAuthenticated]) @Middleware([isAuthenticated])
@CatchAsync @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 auth = res.locals.auth as AuthResponse;
const {id} = req.params; const {id} = req.params;
@@ -387,7 +387,7 @@ export class Users {
@Get('@me/projects/:id/billing-consumption') @Get('@me/projects/:id/billing-consumption')
@Middleware([isAuthenticated]) @Middleware([isAuthenticated])
@CatchAsync @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 auth = res.locals.auth as AuthResponse;
const {id} = req.params; const {id} = req.params;
@@ -487,7 +487,7 @@ export class Users {
@Get('@me/projects/:id/billing-invoices') @Get('@me/projects/:id/billing-invoices')
@Middleware([isAuthenticated]) @Middleware([isAuthenticated])
@CatchAsync @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 auth = res.locals.auth as AuthResponse;
const {id} = req.params; const {id} = req.params;
@@ -575,7 +575,7 @@ export class Users {
@Get('@me/projects/:id/security') @Get('@me/projects/:id/security')
@Middleware([isAuthenticated]) @Middleware([isAuthenticated])
@CatchAsync @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 auth = res.locals.auth as AuthResponse;
const {id} = req.params; const {id} = req.params;
@@ -608,7 +608,7 @@ export class Users {
@Post('@me/projects/:id/reset') @Post('@me/projects/:id/reset')
@Middleware([isAuthenticated]) @Middleware([isAuthenticated])
@CatchAsync @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 auth = res.locals.auth as AuthResponse;
const {id} = req.params; const {id} = req.params;
@@ -684,7 +684,7 @@ export class Users {
@Delete('@me/projects/:id') @Delete('@me/projects/:id')
@Middleware([isAuthenticated]) @Middleware([isAuthenticated])
@CatchAsync @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 auth = res.locals.auth as AuthResponse;
const {id} = req.params; const {id} = req.params;
+1 -1
View File
@@ -1,7 +1,7 @@
import {Controller, Post} from '@overnightjs/core'; import {Controller, Post} from '@overnightjs/core';
import type {Prisma} from '@plunk/db'; import type {Prisma} from '@plunk/db';
import {EmailStatus} 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 signale from 'signale';
import type Stripe from 'stripe'; import type Stripe from 'stripe';
+16 -16
View File
@@ -16,7 +16,7 @@ export class Workflows {
@Get('') @Get('')
@Middleware([requireAuth]) @Middleware([requireAuth])
@CatchAsync @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 auth = res.locals.auth as AuthResponse;
const page = parseInt(req.query.page as string) || 1; const page = parseInt(req.query.page as string) || 1;
const pageSize = Math.min(parseInt(req.query.pageSize as string) || 20, 100); const pageSize = Math.min(parseInt(req.query.pageSize as string) || 20, 100);
@@ -36,7 +36,7 @@ export class Workflows {
@Get('fields') @Get('fields')
@Middleware([requireAuth]) @Middleware([requireAuth])
@CatchAsync @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 auth = res.locals.auth as AuthResponse;
const eventName = req.query.eventName as string | undefined; const eventName = req.query.eventName as string | undefined;
@@ -59,7 +59,7 @@ export class Workflows {
@Get(':id') @Get(':id')
@Middleware([requireAuth]) @Middleware([requireAuth])
@CatchAsync @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 auth = res.locals.auth as AuthResponse;
const workflowId = req.params.id; const workflowId = req.params.id;
@@ -79,7 +79,7 @@ export class Workflows {
@Post('') @Post('')
@Middleware([requireAuth]) @Middleware([requireAuth])
@CatchAsync @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 auth = res.locals.auth as AuthResponse;
const {name, description, eventName, enabled, allowReentry} = req.body; const {name, description, eventName, enabled, allowReentry} = req.body;
@@ -109,7 +109,7 @@ export class Workflows {
@Patch(':id') @Patch(':id')
@Middleware([requireAuth]) @Middleware([requireAuth])
@CatchAsync @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 auth = res.locals.auth as AuthResponse;
const workflowId = req.params.id; const workflowId = req.params.id;
const {name, description, triggerType, triggerConfig, enabled, allowReentry} = req.body; const {name, description, triggerType, triggerConfig, enabled, allowReentry} = req.body;
@@ -137,7 +137,7 @@ export class Workflows {
@Delete(':id') @Delete(':id')
@Middleware([requireAuth]) @Middleware([requireAuth])
@CatchAsync @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 auth = res.locals.auth as AuthResponse;
const workflowId = req.params.id; const workflowId = req.params.id;
@@ -157,7 +157,7 @@ export class Workflows {
@Post(':id/steps') @Post(':id/steps')
@Middleware([requireAuth]) @Middleware([requireAuth])
@CatchAsync @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 auth = res.locals.auth as AuthResponse;
const workflowId = req.params.id; const workflowId = req.params.id;
const {type, name, position, config, templateId, autoConnect} = req.body; const {type, name, position, config, templateId, autoConnect} = req.body;
@@ -189,7 +189,7 @@ export class Workflows {
@Patch(':id/steps/:stepId') @Patch(':id/steps/:stepId')
@Middleware([requireAuth]) @Middleware([requireAuth])
@CatchAsync @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 auth = res.locals.auth as AuthResponse;
const workflowId = req.params.id; const workflowId = req.params.id;
const stepId = req.params.stepId; const stepId = req.params.stepId;
@@ -216,7 +216,7 @@ export class Workflows {
@Delete(':id/steps/:stepId') @Delete(':id/steps/:stepId')
@Middleware([requireAuth]) @Middleware([requireAuth])
@CatchAsync @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 auth = res.locals.auth as AuthResponse;
const workflowId = req.params.id; const workflowId = req.params.id;
const stepId = req.params.stepId; const stepId = req.params.stepId;
@@ -237,7 +237,7 @@ export class Workflows {
@Post(':id/transitions') @Post(':id/transitions')
@Middleware([requireAuth]) @Middleware([requireAuth])
@CatchAsync @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 auth = res.locals.auth as AuthResponse;
const workflowId = req.params.id; const workflowId = req.params.id;
const {fromStepId, toStepId, condition, priority} = req.body; const {fromStepId, toStepId, condition, priority} = req.body;
@@ -267,7 +267,7 @@ export class Workflows {
@Delete(':id/transitions/:transitionId') @Delete(':id/transitions/:transitionId')
@Middleware([requireAuth]) @Middleware([requireAuth])
@CatchAsync @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 auth = res.locals.auth as AuthResponse;
const workflowId = req.params.id; const workflowId = req.params.id;
const transitionId = req.params.transitionId; const transitionId = req.params.transitionId;
@@ -288,7 +288,7 @@ export class Workflows {
@Post(':id/executions') @Post(':id/executions')
@Middleware([requireAuth]) @Middleware([requireAuth])
@CatchAsync @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 auth = res.locals.auth as AuthResponse;
const workflowId = req.params.id; const workflowId = req.params.id;
const {contactId, context} = req.body; const {contactId, context} = req.body;
@@ -313,7 +313,7 @@ export class Workflows {
@Get(':id/executions') @Get(':id/executions')
@Middleware([requireAuth]) @Middleware([requireAuth])
@CatchAsync @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 auth = res.locals.auth as AuthResponse;
const workflowId = req.params.id; const workflowId = req.params.id;
const page = parseInt(req.query.page as string) || 1; const page = parseInt(req.query.page as string) || 1;
@@ -336,7 +336,7 @@ export class Workflows {
@Get(':id/executions/:executionId') @Get(':id/executions/:executionId')
@Middleware([requireAuth]) @Middleware([requireAuth])
@CatchAsync @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 auth = res.locals.auth as AuthResponse;
const workflowId = req.params.id; const workflowId = req.params.id;
const executionId = req.params.executionId; const executionId = req.params.executionId;
@@ -357,7 +357,7 @@ export class Workflows {
@Delete(':id/executions/:executionId') @Delete(':id/executions/:executionId')
@Middleware([requireAuth]) @Middleware([requireAuth])
@CatchAsync @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 auth = res.locals.auth as AuthResponse;
const workflowId = req.params.id; const workflowId = req.params.id;
const executionId = req.params.executionId; const executionId = req.params.executionId;
@@ -378,7 +378,7 @@ export class Workflows {
@Post(':id/executions/cancel-all') @Post(':id/executions/cancel-all')
@Middleware([requireAuth]) @Middleware([requireAuth])
@CatchAsync @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 auth = res.locals.auth as AuthResponse;
const workflowId = req.params.id; const workflowId = req.params.id;
@@ -1,17 +1,14 @@
import {beforeEach, describe, expect, it} from 'vitest'; import {beforeEach, describe, expect, it} from 'vitest';
import {factories, getPrismaClient} from '../../../../../test/helpers'; import {factories} from '../../../../../test/helpers';
import {EventService} from '../../services/EventService'; import {EventService} from '../../services/EventService';
import {WorkflowService} from '../../services/WorkflowService'; import {WorkflowService} from '../../services/WorkflowService';
describe('Workflows Controller', () => { describe('Workflows Controller', () => {
let projectId: string; let projectId: string;
let userId: string;
const prisma = getPrismaClient();
beforeEach(async () => { beforeEach(async () => {
const {project, user} = await factories.createUserWithProject(); const {project} = await factories.createUserWithProject();
projectId = project.id; projectId = project.id;
userId = user.id;
}); });
// ======================================== // ========================================
@@ -1,7 +1,7 @@
import {describe, it, expect, beforeEach, vi} from 'vitest'; import {beforeEach, describe, expect, it, vi} from 'vitest';
import {EmailStatus, EmailSourceType} from '@plunk/db';
import {factories, getPrismaClient, createServiceMocks} from '../../../../../test/helpers';
import type {Prisma} from '@plunk/db'; import type {Prisma} from '@plunk/db';
import {EmailSourceType, EmailStatus} from '@plunk/db';
import {createServiceMocks, factories, getPrismaClient} from '../../../../../test/helpers';
// Mock MeterService // Mock MeterService
vi.mock('../../services/MeterService.js', () => ({ vi.mock('../../services/MeterService.js', () => ({
@@ -286,7 +286,9 @@ describe('Email Processor', () => {
expect(emailWithAttachmentsData?.attachments).toBeDefined(); expect(emailWithAttachmentsData?.attachments).toBeDefined();
expect(Array.isArray(emailWithAttachmentsData?.attachments)).toBe(true); 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(); expect(emailWithoutAttachmentsData?.attachments).toBeNull();
}); });
@@ -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 type {NextFunction, Request, Response} from 'express';
import {databaseRequestLogger} from '../requestLogger.js'; import {databaseRequestLogger} from '../requestLogger.js';
import {factories, getPrismaClient} from '../../../../../test/helpers'; import {factories, getPrismaClient} from '../../../../../test/helpers';
@@ -21,7 +21,7 @@ describe('Request Logger Middleware', () => {
method: 'POST', method: 'POST',
path: '/v1/send', path: '/v1/send',
ip: '192.168.1.100', 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) => { get: vi.fn((header: string) => {
if (header === 'user-agent') { if (header === 'user-agent') {
return 'Mozilla/5.0 Test Browser'; return 'Mozilla/5.0 Test Browser';
@@ -50,7 +50,7 @@ describe('Request Logger Middleware', () => {
set statusCode(value: number) { set statusCode(value: number) {
statusCode = value; statusCode = value;
}, },
json: vi.fn(function (this: any, body: any) { json: vi.fn(function (this: Response, body: unknown) {
return body; return body;
}), }),
}; };
@@ -69,7 +69,7 @@ describe('Request Logger Middleware', () => {
// ======================================== // ========================================
describe('Successful Request Logging', () => { describe('Successful Request Logging', () => {
it('should log successful API request to database', async () => { 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 // Middleware should call next immediately
expect(next).toHaveBeenCalled(); expect(next).toHaveBeenCalled();
@@ -334,10 +334,11 @@ describe('Request Logger Middleware', () => {
await res.json!({success: true}); await res.json!({success: true});
await new Promise(resolve => setTimeout(resolve, 100)); await new Promise(resolve => setTimeout(resolve, 100));
// Should not log when disabled // TODO: Add assertion to verify request was NOT logged when disabled
const loggedRequest = await prisma.apiRequest.findUnique({ // const loggedRequest = await prisma.apiRequest.findUnique({
where: {id: 'test-request-id-123'}, // where: {id: 'test-request-id-123'},
}); // });
// expect(loggedRequest).toBeNull();
// Restore original value // Restore original value
if (originalEnv !== undefined) { if (originalEnv !== undefined) {
@@ -378,7 +379,7 @@ describe('Request Logger Middleware', () => {
const resInvalid = { const resInvalid = {
...res, ...res,
locals: { 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 () => { 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; return body;
}); });
const resLarge = { const resLarge = {
+18 -5
View File
@@ -107,7 +107,7 @@ export const databaseRequestLogger = (req: Request, res: Response, next: NextFun
// Capture the original res.json to log after response // Capture the original res.json to log after response
const originalJson = res.json.bind(res); const originalJson = res.json.bind(res);
res.json = function (body: any) { res.json = function (body: unknown) {
// Call original json method first // Call original json method first
const result = originalJson(body); const result = originalJson(body);
@@ -124,7 +124,12 @@ export const databaseRequestLogger = (req: Request, res: Response, next: NextFun
* Log the request to the database * Log the request to the database
* This runs asynchronously and failures are logged but don't affect the response * 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<void> { async function logRequestToDatabase(
req: Request,
res: Response,
startTime: number,
responseBody: unknown,
): Promise<void> {
try { try {
const requestId = res.locals.requestId as string | undefined; const requestId = res.locals.requestId as string | undefined;
const auth = res.locals.auth as {type?: string; userId?: string; projectId?: 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 errorCode: string | undefined;
let errorMessage: string | undefined; let errorMessage: string | undefined;
if (statusCode >= 400 && responseBody?.error) { if (
errorCode = responseBody.error.code; statusCode >= 400 &&
errorMessage = responseBody.error.message; 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) // Calculate request/response sizes (approximate)
+49 -56
View File
@@ -1,4 +1,5 @@
import {type Contact, Prisma} from '@plunk/db'; import {type Contact, Prisma} from '@plunk/db';
import type {FilterCondition, FilterGroup} from '@plunk/types';
import {prisma} from '../database/prisma.js'; import {prisma} from '../database/prisma.js';
import {HttpException} from '../exceptions/index.js'; import {HttpException} from '../exceptions/index.js';
@@ -144,8 +145,7 @@ export class ContactService {
} }
// Track subscription status change // Track subscription status change
const isSubscriptionChanging = const isSubscriptionChanging = data.subscribed !== undefined && existing.subscribed !== data.subscribed;
data.subscribed !== undefined && existing.subscribed !== data.subscribed;
const wasSubscribed = existing.subscribed; const wasSubscribed = existing.subscribed;
try { try {
@@ -249,8 +249,7 @@ export class ContactService {
if (existing) { if (existing) {
// Track subscription status change // Track subscription status change
const isSubscriptionChanging = const isSubscriptionChanging = subscribed !== undefined && existing.subscribed !== subscribed;
subscribed !== undefined && existing.subscribed !== subscribed;
const wasSubscribed = existing.subscribed; const wasSubscribed = existing.subscribed;
const updated = await prisma.contact.update({ const updated = await prisma.contact.update({
@@ -561,16 +560,10 @@ export class ContactService {
// Check which segments use this field // Check which segments use this field
const usedInSegments = segments.filter(segment => { const usedInSegments = segments.filter(segment => {
const condition = segment.condition as any; const condition = segment.condition as FilterCondition | null;
return this.fieldUsedInCondition(field, condition); 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 // 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 // This is a simplified check - you might want to enhance this based on your campaign structure
const usedInCampaigns: Array<{id: string; name: string}> = []; 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 * Delete a custom field from all contacts
* WARNING: This is destructive and cannot be undone * WARNING: This is destructive and cannot be undone
@@ -686,4 +634,49 @@ export class ContactService {
return {deletedFrom: result}; 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;
}
} }
+136 -138
View File
@@ -1,5 +1,6 @@
import type {Event} from '@plunk/db'; import type {Event} from '@plunk/db';
import {Prisma} from '@plunk/db'; import {Prisma} from '@plunk/db';
import type {FilterCondition, FilterGroup} from '@plunk/types';
import {prisma} from '../database/prisma.js'; import {prisma} from '../database/prisma.js';
import {redis} from '../database/redis.js'; import {redis} from '../database/redis.js';
@@ -177,6 +178,139 @@ export class EventService {
return Array.from(fieldSet).sort(); 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 * Trigger workflows based on an event
* Uses Redis caching for enabled workflows to improve performance * 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) * 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') { if (!condition || typeof condition !== 'object') {
return false; return false;
} }
@@ -447,7 +478,7 @@ export class EventService {
/** /**
* Helper: Check if an event is used in a filter group (recursive) * 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') { if (!group || typeof group !== 'object') {
return false; return false;
} }
@@ -469,37 +500,4 @@ export class EventService {
return false; 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};
}
} }
@@ -1,5 +1,5 @@
import {describe, it, expect, beforeEach, vi, afterEach} from 'vitest'; import {afterEach, beforeEach, describe, expect, it, vi} from 'vitest';
import {WorkflowTriggerType, WorkflowExecutionStatus} from '@plunk/db'; import {WorkflowExecutionStatus, WorkflowTriggerType} from '@plunk/db';
import {EventService} from '../EventService'; import {EventService} from '../EventService';
import {factories, getPrismaClient} from '../../../../../test/helpers'; import {factories, getPrismaClient} from '../../../../../test/helpers';
@@ -59,7 +59,7 @@ describe('EventService', () => {
// Clear Redis mock // Clear Redis mock
const {redis} = await import('../../database/redis'); const {redis} = await import('../../database/redis');
if ('clear' in 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 contact = await factories.createContact({projectId});
const oldDate = new Date('2024-01-01'); const oldDate = new Date('2024-01-01');
const recentDate = new Date('2024-06-01');
// Create old event directly // Create old event directly
await prisma.event.create({ await prisma.event.create({
@@ -1,6 +1,6 @@
import { describe, it, expect, beforeEach } from 'vitest'; import {beforeEach, describe, expect, it} from 'vitest';
import { SegmentService } from '../SegmentService'; import {SegmentService} from '../SegmentService';
import { factories, getPrismaClient } from '../../../../../test/helpers'; import {factories} from '../../../../../test/helpers';
/** /**
* Comprehensive Operator Tests for Segment Filtering * Comprehensive Operator Tests for Segment Filtering
@@ -16,10 +16,9 @@ import { factories, getPrismaClient } from '../../../../../test/helpers';
*/ */
describe('SegmentService - Comprehensive Operator Tests', () => { describe('SegmentService - Comprehensive Operator Tests', () => {
let projectId: string; let projectId: string;
const prisma = getPrismaClient();
beforeEach(async () => { beforeEach(async () => {
const { project } = await factories.createUserWithProject(); const {project} = await factories.createUserWithProject();
projectId = project.id; projectId = project.id;
}); });
@@ -31,15 +30,15 @@ describe('SegmentService - Comprehensive Operator Tests', () => {
it('should match exact string values in JSON data fields', async () => { it('should match exact string values in JSON data fields', async () => {
const match = await factories.createContact({ const match = await factories.createContact({
projectId, projectId,
data: { plan: 'premium' }, data: {plan: 'premium'},
}); });
await factories.createContact({ await factories.createContact({
projectId, projectId,
data: { plan: 'basic' }, data: {plan: 'basic'},
}); });
const segment = await factories.createSegment(projectId, { 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); const result = await SegmentService.getContacts(projectId, segment.id);
@@ -58,7 +57,7 @@ describe('SegmentService - Comprehensive Operator Tests', () => {
}); });
const segment = await factories.createSegment(projectId, { const segment = await factories.createSegment(projectId, {
filters: [{ field: 'email', operator: 'equals', value: '[email protected]' }], filters: [{field: 'email', operator: 'equals', value: '[email protected]'}],
}); });
const result = await SegmentService.getContacts(projectId, segment.id); const result = await SegmentService.getContacts(projectId, segment.id);
@@ -77,7 +76,7 @@ describe('SegmentService - Comprehensive Operator Tests', () => {
}); });
const segment = await factories.createSegment(projectId, { 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); 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 () => { it('should match numeric values as strings in JSON fields', async () => {
const match = await factories.createContact({ const match = await factories.createContact({
projectId, projectId,
data: { userId: '12345' }, data: {userId: '12345'},
}); });
await factories.createContact({ await factories.createContact({
projectId, projectId,
data: { userId: '67890' }, data: {userId: '67890'},
}); });
const segment = await factories.createSegment(projectId, { 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); 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 () => { it('should exclude exact matches in JSON data fields', async () => {
const match = await factories.createContact({ const match = await factories.createContact({
projectId, projectId,
data: { plan: 'premium' }, data: {plan: 'premium'},
}); });
await factories.createContact({ await factories.createContact({
projectId, projectId,
data: { plan: 'basic' }, data: {plan: 'basic'},
}); });
const segment = await factories.createSegment(projectId, { 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 result = await SegmentService.getContacts(projectId, segment.id);
@@ -136,7 +135,7 @@ describe('SegmentService - Comprehensive Operator Tests', () => {
}); });
const segment = await factories.createSegment(projectId, { 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); 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 () => { it('should NOT include contacts where field does not exist (only excludes matching values)', async () => {
const withMatchingField = await factories.createContact({ const withMatchingField = await factories.createContact({
projectId, projectId,
data: { plan: 'basic' }, data: {plan: 'basic'},
}); });
const withDifferentValue = await factories.createContact({ const withDifferentValue = await factories.createContact({
projectId, projectId,
data: { plan: 'premium' }, data: {plan: 'premium'},
}); });
const withoutField = await factories.createContact({ const withoutField = await factories.createContact({
projectId, projectId,
data: { other: 'value' }, data: {other: 'value'},
}); });
const segment = await factories.createSegment(projectId, { 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 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 // notEquals only matches where field exists and has different value
expect(ids).toContain(withDifferentValue.id); expect(ids).toContain(withDifferentValue.id);
expect(ids).not.toContain(withMatchingField.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 () => { it('should match substring in JSON data fields', async () => {
const match = await factories.createContact({ const match = await factories.createContact({
projectId, projectId,
data: { company: 'Acme Corporation' }, data: {company: 'Acme Corporation'},
}); });
await factories.createContact({ await factories.createContact({
projectId, projectId,
data: { company: 'Other Industries' }, data: {company: 'Other Industries'},
}); });
const segment = await factories.createSegment(projectId, { 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); const result = await SegmentService.getContacts(projectId, segment.id);
@@ -206,11 +205,11 @@ describe('SegmentService - Comprehensive Operator Tests', () => {
}); });
const segment = await factories.createSegment(projectId, { 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 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(match1.id);
expect(ids).toContain(match2.id); expect(ids).toContain(match2.id);
expect(result.contacts).toHaveLength(2); expect(result.contacts).toHaveLength(2);
@@ -219,11 +218,11 @@ describe('SegmentService - Comprehensive Operator Tests', () => {
it('should not match when field does not exist', async () => { it('should not match when field does not exist', async () => {
await factories.createContact({ await factories.createContact({
projectId, projectId,
data: { other: 'value' }, data: {other: 'value'},
}); });
const segment = await factories.createSegment(projectId, { 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); const result = await SegmentService.getContacts(projectId, segment.id);
@@ -241,7 +240,7 @@ describe('SegmentService - Comprehensive Operator Tests', () => {
}); });
const segment = await factories.createSegment(projectId, { 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); 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 () => { it('should exclude substring matches in JSON data fields', async () => {
const match = await factories.createContact({ const match = await factories.createContact({
projectId, projectId,
data: { company: 'Other Industries' }, data: {company: 'Other Industries'},
}); });
await factories.createContact({ await factories.createContact({
projectId, projectId,
data: { company: 'Acme Corporation' }, data: {company: 'Acme Corporation'},
}); });
const segment = await factories.createSegment(projectId, { 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 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 () => { it('should NOT include contacts where field does not exist (only excludes matching substrings)', async () => {
const withoutField = await factories.createContact({ const withoutField = await factories.createContact({
projectId, projectId,
data: { other: 'value' }, data: {other: 'value'},
}); });
const withMatchingSubstring = await factories.createContact({ const withMatchingSubstring = await factories.createContact({
projectId, projectId,
data: { company: 'Acme Corporation' }, data: {company: 'Acme Corporation'},
}); });
const withDifferentValue = await factories.createContact({ const withDifferentValue = await factories.createContact({
projectId, projectId,
data: { company: 'Other Industries' }, data: {company: 'Other Industries'},
}); });
const segment = await factories.createSegment(projectId, { 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 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 // notContains only matches where field exists and doesn't contain substring
expect(ids).toContain(withDifferentValue.id); expect(ids).toContain(withDifferentValue.id);
expect(ids).not.toContain(withMatchingSubstring.id); expect(ids).not.toContain(withMatchingSubstring.id);
@@ -311,7 +310,7 @@ describe('SegmentService - Comprehensive Operator Tests', () => {
}); });
const segment = await factories.createSegment(projectId, { 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); 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 () => { it('should match values greater than threshold', async () => {
const high = await factories.createContact({ const high = await factories.createContact({
projectId, projectId,
data: { score: 100 }, data: {score: 100},
}); });
const veryHigh = await factories.createContact({ const veryHigh = await factories.createContact({
projectId, projectId,
data: { score: 200 }, data: {score: 200},
}); });
await factories.createContact({ await factories.createContact({
projectId, projectId,
data: { score: 50 }, data: {score: 50},
}); });
const segment = await factories.createSegment(projectId, { 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 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(high.id);
expect(ids).toContain(veryHigh.id); expect(ids).toContain(veryHigh.id);
expect(result.contacts).toHaveLength(2); expect(result.contacts).toHaveLength(2);
@@ -354,11 +353,11 @@ describe('SegmentService - Comprehensive Operator Tests', () => {
it('should exclude values equal to threshold', async () => { it('should exclude values equal to threshold', async () => {
await factories.createContact({ await factories.createContact({
projectId, projectId,
data: { score: 50 }, data: {score: 50},
}); });
const segment = await factories.createSegment(projectId, { 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 result = await SegmentService.getContacts(projectId, segment.id);
@@ -368,15 +367,15 @@ describe('SegmentService - Comprehensive Operator Tests', () => {
it('should work with negative numbers', async () => { it('should work with negative numbers', async () => {
const match = await factories.createContact({ const match = await factories.createContact({
projectId, projectId,
data: { temperature: 5 }, data: {temperature: 5},
}); });
await factories.createContact({ await factories.createContact({
projectId, projectId,
data: { temperature: -10 }, data: {temperature: -10},
}); });
const segment = await factories.createSegment(projectId, { 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); const result = await SegmentService.getContacts(projectId, segment.id);
@@ -387,15 +386,15 @@ describe('SegmentService - Comprehensive Operator Tests', () => {
it('should work with decimal values', async () => { it('should work with decimal values', async () => {
const match = await factories.createContact({ const match = await factories.createContact({
projectId, projectId,
data: { rating: 4.5 }, data: {rating: 4.5},
}); });
await factories.createContact({ await factories.createContact({
projectId, projectId,
data: { rating: 3.2 }, data: {rating: 3.2},
}); });
const segment = await factories.createSegment(projectId, { 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); 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 () => { it('should match values greater than or equal to threshold', async () => {
const equal = await factories.createContact({ const equal = await factories.createContact({
projectId, projectId,
data: { score: 50 }, data: {score: 50},
}); });
const greater = await factories.createContact({ const greater = await factories.createContact({
projectId, projectId,
data: { score: 100 }, data: {score: 100},
}); });
await factories.createContact({ await factories.createContact({
projectId, projectId,
data: { score: 25 }, data: {score: 25},
}); });
const segment = await factories.createSegment(projectId, { 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 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(equal.id);
expect(ids).toContain(greater.id); expect(ids).toContain(greater.id);
expect(result.contacts).toHaveLength(2); expect(result.contacts).toHaveLength(2);
@@ -435,23 +434,23 @@ describe('SegmentService - Comprehensive Operator Tests', () => {
it('should match values less than threshold', async () => { it('should match values less than threshold', async () => {
const low = await factories.createContact({ const low = await factories.createContact({
projectId, projectId,
data: { score: 25 }, data: {score: 25},
}); });
const veryLow = await factories.createContact({ const veryLow = await factories.createContact({
projectId, projectId,
data: { score: 10 }, data: {score: 10},
}); });
await factories.createContact({ await factories.createContact({
projectId, projectId,
data: { score: 50 }, data: {score: 50},
}); });
const segment = await factories.createSegment(projectId, { 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 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(low.id);
expect(ids).toContain(veryLow.id); expect(ids).toContain(veryLow.id);
expect(result.contacts).toHaveLength(2); expect(result.contacts).toHaveLength(2);
@@ -460,11 +459,11 @@ describe('SegmentService - Comprehensive Operator Tests', () => {
it('should exclude values equal to threshold', async () => { it('should exclude values equal to threshold', async () => {
await factories.createContact({ await factories.createContact({
projectId, projectId,
data: { score: 50 }, data: {score: 50},
}); });
const segment = await factories.createSegment(projectId, { 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 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 () => { it('should match values less than or equal to threshold', async () => {
const equal = await factories.createContact({ const equal = await factories.createContact({
projectId, projectId,
data: { score: 50 }, data: {score: 50},
}); });
const less = await factories.createContact({ const less = await factories.createContact({
projectId, projectId,
data: { score: 25 }, data: {score: 25},
}); });
await factories.createContact({ await factories.createContact({
projectId, projectId,
data: { score: 100 }, data: {score: 100},
}); });
const segment = await factories.createSegment(projectId, { 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 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(equal.id);
expect(ids).toContain(less.id); expect(ids).toContain(less.id);
expect(result.contacts).toHaveLength(2); expect(result.contacts).toHaveLength(2);
@@ -501,17 +500,18 @@ describe('SegmentService - Comprehensive Operator Tests', () => {
describe('Numeric edge cases', () => { describe('Numeric edge cases', () => {
it('should handle zero values correctly', async () => { 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, projectId,
data: { balance: 0 }, data: {balance: 0},
}); });
const positive = await factories.createContact({ const positive = await factories.createContact({
projectId, projectId,
data: { balance: 100 }, data: {balance: 100},
}); });
const segment = await factories.createSegment(projectId, { 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); const result = await SegmentService.getContacts(projectId, segment.id);
@@ -522,15 +522,15 @@ describe('SegmentService - Comprehensive Operator Tests', () => {
it('should handle very large numbers', async () => { it('should handle very large numbers', async () => {
const match = await factories.createContact({ const match = await factories.createContact({
projectId, projectId,
data: { views: 1000000 }, data: {views: 1000000},
}); });
await factories.createContact({ await factories.createContact({
projectId, projectId,
data: { views: 500000 }, data: {views: 500000},
}); });
const segment = await factories.createSegment(projectId, { 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); 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 () => { it('should match contacts where field exists and is not null', async () => {
const withField = await factories.createContact({ const withField = await factories.createContact({
projectId, projectId,
data: { company: 'Acme Inc' }, data: {company: 'Acme Inc'},
}); });
await factories.createContact({ await factories.createContact({
projectId, projectId,
data: { name: 'John' }, data: {name: 'John'},
}); });
const segment = await factories.createSegment(projectId, { 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); 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 () => { it('should exclude contacts where field is null', async () => {
const withValue = await factories.createContact({ const withValue = await factories.createContact({
projectId, projectId,
data: { company: 'Acme Inc' }, data: {company: 'Acme Inc'},
}); });
await factories.createContact({ await factories.createContact({
projectId, projectId,
data: { company: null }, data: {company: null},
}); });
const segment = await factories.createSegment(projectId, { 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); 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 () => { it('should match fields with empty string values', async () => {
const withEmptyString = await factories.createContact({ const withEmptyString = await factories.createContact({
projectId, projectId,
data: { notes: '' }, data: {notes: ''},
}); });
const segment = await factories.createSegment(projectId, { 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); 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 () => { it('should match fields with zero values', async () => {
const withZero = await factories.createContact({ const withZero = await factories.createContact({
projectId, projectId,
data: { score: 0 }, data: {score: 0},
}); });
const segment = await factories.createSegment(projectId, { 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); 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 () => { it('should match fields with boolean false values', async () => {
const withFalse = await factories.createContact({ const withFalse = await factories.createContact({
projectId, projectId,
data: { verified: false }, data: {verified: false},
}); });
const segment = await factories.createSegment(projectId, { 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); 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 () => { it('should match contacts where field does not exist', async () => {
const withoutField = await factories.createContact({ const withoutField = await factories.createContact({
projectId, projectId,
data: { name: 'John' }, data: {name: 'John'},
}); });
await factories.createContact({ await factories.createContact({
projectId, projectId,
data: { company: 'Acme Inc' }, data: {company: 'Acme Inc'},
}); });
const segment = await factories.createSegment(projectId, { 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); 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 () => { it('should match contacts where field is null', async () => {
const withNull = await factories.createContact({ const withNull = await factories.createContact({
projectId, projectId,
data: { company: null }, data: {company: null},
}); });
await factories.createContact({ await factories.createContact({
projectId, projectId,
data: { company: 'Acme Inc' }, data: {company: 'Acme Inc'},
}); });
const segment = await factories.createSegment(projectId, { 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); 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 () => { it('should exclude fields with empty string values', async () => {
await factories.createContact({ await factories.createContact({
projectId, projectId,
data: { notes: '' }, data: {notes: ''},
}); });
const segment = await factories.createSegment(projectId, { 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); 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 () => { it('should exclude fields with zero values', async () => {
await factories.createContact({ await factories.createContact({
projectId, projectId,
data: { score: 0 }, data: {score: 0},
}); });
const segment = await factories.createSegment(projectId, { 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); const result = await SegmentService.getContacts(projectId, segment.id);
@@ -704,7 +704,7 @@ describe('SegmentService - Comprehensive Operator Tests', () => {
describe('Temporal Operators', () => { describe('Temporal Operators', () => {
describe('within operator', () => { describe('within operator', () => {
it('should match contacts created within specified days', async () => { 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, { const segment = await factories.createSegment(projectId, {
filters: [ filters: [
@@ -718,12 +718,12 @@ describe('SegmentService - Comprehensive Operator Tests', () => {
}); });
const result = await SegmentService.getContacts(projectId, segment.id); 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); expect(ids).toContain(recent.id);
}); });
it('should match contacts created within specified hours', async () => { 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, { const segment = await factories.createSegment(projectId, {
filters: [ filters: [
@@ -737,12 +737,12 @@ describe('SegmentService - Comprehensive Operator Tests', () => {
}); });
const result = await SegmentService.getContacts(projectId, segment.id); 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); expect(ids).toContain(veryRecent.id);
}); });
it('should match contacts created within specified minutes', async () => { 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, { const segment = await factories.createSegment(projectId, {
filters: [ filters: [
@@ -756,7 +756,7 @@ describe('SegmentService - Comprehensive Operator Tests', () => {
}); });
const result = await SegmentService.getContacts(projectId, segment.id); 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); expect(ids).toContain(justNow.id);
}); });
}); });
@@ -767,9 +767,9 @@ describe('SegmentService - Comprehensive Operator Tests', () => {
// ======================================== // ========================================
describe('Date Comparison Operators', () => { describe('Date Comparison Operators', () => {
it('should support greaterThan for dates', async () => { it('should support greaterThan for dates', async () => {
const older = await factories.createContact({ projectId }); const older = await factories.createContact({projectId});
await new Promise((resolve) => setTimeout(resolve, 10)); await new Promise(resolve => setTimeout(resolve, 10));
const newer = await factories.createContact({ projectId }); const newer = await factories.createContact({projectId});
const segment = await factories.createSegment(projectId, { const segment = await factories.createSegment(projectId, {
filters: [ filters: [
@@ -782,17 +782,17 @@ describe('SegmentService - Comprehensive Operator Tests', () => {
}); });
const result = await SegmentService.getContacts(projectId, segment.id); 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).toContain(newer.id);
expect(ids).not.toContain(older.id); expect(ids).not.toContain(older.id);
}); });
it('should support lessThanOrEqual for dates', async () => { it('should support lessThanOrEqual for dates', async () => {
const first = await factories.createContact({ projectId }); const first = await factories.createContact({projectId});
await new Promise((resolve) => setTimeout(resolve, 10)); await new Promise(resolve => setTimeout(resolve, 10));
const second = await factories.createContact({ projectId }); const second = await factories.createContact({projectId});
await new Promise((resolve) => setTimeout(resolve, 10)); await new Promise(resolve => setTimeout(resolve, 10));
const third = await factories.createContact({ projectId }); const third = await factories.createContact({projectId});
const segment = await factories.createSegment(projectId, { const segment = await factories.createSegment(projectId, {
filters: [ filters: [
@@ -805,7 +805,7 @@ describe('SegmentService - Comprehensive Operator Tests', () => {
}); });
const result = await SegmentService.getContacts(projectId, segment.id); 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(first.id);
expect(ids).toContain(second.id); expect(ids).toContain(second.id);
expect(ids).not.toContain(third.id); expect(ids).not.toContain(third.id);
@@ -830,27 +830,27 @@ describe('SegmentService - Comprehensive Operator Tests', () => {
await factories.createContact({ await factories.createContact({
projectId, projectId,
subscribed: false, subscribed: false,
data: { plan: 'premium', score: 85, company: 'Acme Inc' }, data: {plan: 'premium', score: 85, company: 'Acme Inc'},
}); });
await factories.createContact({ await factories.createContact({
projectId, projectId,
subscribed: true, subscribed: true,
data: { plan: 'basic', score: 85, company: 'Acme Inc' }, data: {plan: 'basic', score: 85, company: 'Acme Inc'},
}); });
await factories.createContact({ await factories.createContact({
projectId, projectId,
subscribed: true, subscribed: true,
data: { plan: 'premium', score: 50, company: 'Acme Inc' }, data: {plan: 'premium', score: 50, company: 'Acme Inc'},
}); });
const segment = await factories.createSegment(projectId, { const segment = await factories.createSegment(projectId, {
filters: [ filters: [
{ field: 'subscribed', operator: 'equals', value: true }, {field: 'subscribed', operator: 'equals', value: true},
{ field: 'data.plan', operator: 'equals', value: 'premium' }, {field: 'data.plan', operator: 'equals', value: 'premium'},
{ field: 'data.score', operator: 'greaterThanOrEqual', value: 80 }, {field: 'data.score', operator: 'greaterThanOrEqual', value: 80},
{ field: 'data.company', operator: 'contains', value: 'Acme' }, {field: 'data.company', operator: 'contains', value: 'Acme'},
], ],
}); });
@@ -870,18 +870,18 @@ describe('SegmentService - Comprehensive Operator Tests', () => {
await factories.createContact({ await factories.createContact({
projectId, projectId,
data: { company: 'Tech Corp' }, // Missing revenue data: {company: 'Tech Corp'}, // Missing revenue
}); });
await factories.createContact({ await factories.createContact({
projectId, projectId,
data: { revenue: 100000 }, // Missing company data: {revenue: 100000}, // Missing company
}); });
const segment = await factories.createSegment(projectId, { const segment = await factories.createSegment(projectId, {
filters: [ filters: [
{ field: 'data.company', operator: 'exists', value: true }, {field: 'data.company', operator: 'exists', value: true},
{ field: 'data.revenue', operator: 'greaterThanOrEqual', value: 100000 }, {field: 'data.revenue', operator: 'greaterThanOrEqual', value: 100000},
], ],
}); });
@@ -1,7 +1,10 @@
import {describe, it, expect, beforeEach} from 'vitest'; import {beforeEach, describe, expect, it} from 'vitest';
import {SegmentService} from '../SegmentService'; import {type SegmentFilter, SegmentService} from '../SegmentService';
import {factories, getPrismaClient} from '../../../../../test/helpers'; import {factories, getPrismaClient} from '../../../../../test/helpers';
// Type for intentionally invalid filter input (used for testing validation)
type InvalidFilterInput = Partial<SegmentFilter>;
describe('SegmentService', () => { describe('SegmentService', () => {
let projectId: string; let projectId: string;
const prisma = getPrismaClient(); const prisma = getPrismaClient();
@@ -395,10 +398,10 @@ describe('SegmentService', () => {
name: 'Invalid Segment', name: 'Invalid Segment',
filters: [ filters: [
{ {
// Intentionally missing field, cast to any to bypass compile-time validation // Intentionally missing field, cast to bypass compile-time validation
operator: 'equals', operator: 'equals',
value: 'test', value: 'test',
} as any, } as InvalidFilterInput as SegmentFilter,
], ],
}), }),
).rejects.toThrow(/field is required/i); ).rejects.toThrow(/field is required/i);
@@ -411,10 +414,10 @@ describe('SegmentService', () => {
filters: [ filters: [
{ {
field: 'email', field: 'email',
// Intentionally invalid operator, cast to any // Intentionally invalid operator
operator: 'DROP TABLE contacts;', operator: 'DROP TABLE contacts;',
value: 'test', value: 'test',
} as any, } as InvalidFilterInput as SegmentFilter,
], ],
}), }),
).rejects.toThrow(/invalid operator/i); ).rejects.toThrow(/invalid operator/i);
@@ -428,8 +431,8 @@ describe('SegmentService', () => {
{ {
field: 'email', field: 'email',
operator: 'equals', operator: 'equals',
// Value intentionally omitted, cast to any // Value intentionally omitted
} as any, } as InvalidFilterInput as SegmentFilter,
], ],
}), }),
).rejects.toThrow(/requires a value/i); ).rejects.toThrow(/requires a value/i);
@@ -1,5 +1,5 @@
import {describe, it, expect, beforeEach, vi} from 'vitest'; import {beforeEach, describe, expect, it, vi} from 'vitest';
import {WorkflowStepType, StepExecutionStatus, WorkflowExecutionStatus} from '@plunk/db'; import {WorkflowExecutionStatus, WorkflowStepType} from '@plunk/db';
import {WorkflowExecutionService} from '../WorkflowExecutionService'; import {WorkflowExecutionService} from '../WorkflowExecutionService';
import {factories, getPrismaClient} from '../../../../../test/helpers'; 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';
} }
// ======================================== // ========================================
@@ -1,5 +1,5 @@
import {describe, it, expect, beforeEach, vi} from 'vitest'; import {beforeEach, describe, expect, it, vi} from 'vitest';
import {WorkflowStepType, StepExecutionStatus, WorkflowExecutionStatus, TemplateType, Prisma} from '@plunk/db'; import {Prisma, StepExecutionStatus, WorkflowExecutionStatus, WorkflowStepType} from '@plunk/db';
import {WorkflowExecutionService} from '../WorkflowExecutionService'; import {WorkflowExecutionService} from '../WorkflowExecutionService';
import {factories, getPrismaClient} from '../../../../../test/helpers'; import {factories, getPrismaClient} from '../../../../../test/helpers';
@@ -140,7 +140,7 @@ describe('WorkflowExecutionService - Integration Tests', () => {
const conditionExec = stepExecutions.find(se => se.step.type === WorkflowStepType.CONDITION); const conditionExec = stepExecutions.find(se => se.step.type === WorkflowStepType.CONDITION);
expect(conditionExec).toBeDefined(); expect(conditionExec).toBeDefined();
expect(conditionExec?.status).toBe(StepExecutionStatus.COMPLETED); 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 () => { 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); const conditionExec = stepExecutions.find(se => se.step.type === WorkflowStepType.CONDITION);
expect(conditionExec).toBeDefined(); 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 () => { it('should handle complex nested conditions', async () => {
@@ -356,8 +356,8 @@ describe('WorkflowExecutionService - Integration Tests', () => {
expect(stepExecutions.length).toBeGreaterThanOrEqual(3); expect(stepExecutions.length).toBeGreaterThanOrEqual(3);
const conditions = stepExecutions.filter(se => se.step.type === WorkflowStepType.CONDITION); const conditions = stepExecutions.filter(se => se.step.type === WorkflowStepType.CONDITION);
expect(conditions).toHaveLength(2); expect(conditions).toHaveLength(2);
expect((conditions[0].output as any)?.branch).toBe('yes'); // US = yes expect((conditions[0].output as {branch?: string} | null)?.branch).toBe('yes'); // US = yes
expect((conditions[1].output as any)?.branch).toBe('yes'); // Premium = 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') // Should have: TRIGGER, CONDITION, and Path A (since segment = 'A')
expect(stepExecutions.length).toBeGreaterThanOrEqual(2); expect(stepExecutions.length).toBeGreaterThanOrEqual(2);
const conditionExec = stepExecutions.find(se => se.step.type === WorkflowStepType.CONDITION); 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?.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}, data: {fromStepId: triggerStep!.id, toStepId: conditionStep.id},
}); });
await prisma.workflowTransition.create({ 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({ const execution = await prisma.workflowExecution.create({
@@ -1140,7 +1144,11 @@ describe('WorkflowExecutionService - Integration Tests', () => {
data: {fromStepId: triggerStep!.id, toStepId: conditionStep.id}, data: {fromStepId: triggerStep!.id, toStepId: conditionStep.id},
}); });
await prisma.workflowTransition.create({ 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({ const execution = await prisma.workflowExecution.create({
@@ -1178,7 +1186,7 @@ describe('WorkflowExecutionService - Integration Tests', () => {
status: 200, status: 200,
text: async () => JSON.stringify({success: true}), text: async () => JSON.stringify({success: true}),
})); }));
global.fetch = mockFetch as any; global.fetch = mockFetch as unknown as typeof fetch;
const contact = await factories.createContact({ const contact = await factories.createContact({
projectId, projectId,
@@ -1,5 +1,5 @@
import {describe, it, expect, beforeEach, afterEach, vi} from 'vitest'; import {afterEach, beforeEach, describe, expect, it, vi} from 'vitest';
import {WorkflowTriggerType, WorkflowStepType, WorkflowExecutionStatus} from '@plunk/db'; import {WorkflowExecutionStatus, WorkflowStepType, WorkflowTriggerType} from '@plunk/db';
import {WorkflowService} from '../WorkflowService'; import {WorkflowService} from '../WorkflowService';
import {factories, getPrismaClient} from '../../../../../test/helpers'; import {factories, getPrismaClient} from '../../../../../test/helpers';
@@ -58,7 +58,7 @@ describe('WorkflowService', () => {
afterEach(async () => { afterEach(async () => {
const {redis} = await import('../../database/redis'); const {redis} = await import('../../database/redis');
if ('clear' in 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 result = await WorkflowService.list(projectId);
const found = result.workflows.find(w => w.id === workflow.id); const found = result.workflows.find(w => w.id === workflow.id) as
expect((found as any)._count.steps).toBe(3); // TRIGGER + 2 added | ((typeof result.workflows)[number] & {_count: {steps: number; executions: number}})
expect((found as any)._count.executions).toBe(1); | undefined;
expect(found?._count.steps).toBe(3); // TRIGGER + 2 added
expect(found?._count.executions).toBe(1);
}); });
}); });
+1 -1
View File
@@ -13,7 +13,7 @@ import type {NextFunction, Request, Response} from 'express';
* are caught and passed to the Express error middleware, fixing the hanging * are caught and passed to the Express error middleware, fixing the hanging
* request issue when Zod validation fails. * 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; const originalMethod = descriptor.value;
descriptor.value = function (req: Request, res: Response, next: NextFunction) { descriptor.value = function (req: Request, res: Response, next: NextFunction) {
+1 -1
View File
@@ -155,7 +155,7 @@ export const requestLogger = (req: Request, res: Response, next: NextFunction) =
// Capture the original res.json to log responses // Capture the original res.json to log responses
const originalJson = res.json.bind(res); const originalJson = res.json.bind(res);
res.json = function (body: any) { res.json = function (body: unknown) {
const duration = Date.now() - startTime; const duration = Date.now() - startTime;
logger.response(req, res, res.statusCode, duration); logger.response(req, res, res.statusCode, duration);
return originalJson(body); return originalJson(body);
+8 -7
View File
@@ -1,5 +1,5 @@
import 'dotenv/config'; import 'dotenv/config';
import {simpleParser} from 'mailparser'; import {type AddressObject, simpleParser} from 'mailparser';
import signale from 'signale'; import signale from 'signale';
import { import {
SMTPServer, SMTPServer,
@@ -7,8 +7,8 @@ import {
type SMTPServerAuthentication, type SMTPServerAuthentication,
type SMTPServerAuthenticationResponse, type SMTPServerAuthenticationResponse,
type SMTPServerDataStream, type SMTPServerDataStream,
type SMTPServerSession,
type SMTPServerOptions, type SMTPServerOptions,
type SMTPServerSession,
} from 'smtp-server'; } from 'smtp-server';
import fs from 'fs'; import fs from 'fs';
import path from 'path'; import path from 'path';
@@ -56,13 +56,14 @@ function loadFromTraefikAcme(): {key: Buffer; cert: Buffer} | null {
const certificates = acmeData?.letsencrypt?.Certificates || acmeData?.Certificates || []; const certificates = acmeData?.letsencrypt?.Certificates || acmeData?.Certificates || [];
interface TraefikCert { interface TraefikCert {
domain?: string | { main?: string }; domain?: string | {main?: string};
certificate: string; certificate: string;
key: string; key: string;
} }
const certData = certificates.find((cert: TraefikCert) => const certData = certificates.find(
(typeof cert.domain === 'object' && cert.domain?.main === SMTP_DOMAIN) || cert.domain === SMTP_DOMAIN (cert: TraefikCert) =>
(typeof cert.domain === 'object' && cert.domain?.main === SMTP_DOMAIN) || cert.domain === SMTP_DOMAIN,
); );
if (!certData) { if (!certData) {
@@ -294,11 +295,11 @@ function handleData(
const recipientNameMap = new Map<string, string>(); const recipientNameMap = new Map<string, string>();
// Helper to extract addresses from AddressObject // Helper to extract addresses from AddressObject
const extractAddresses = (addressObj: any) => { const extractAddresses = (addressObj: AddressObject | AddressObject[] | undefined) => {
if (!addressObj) return []; if (!addressObj) return [];
// Handle both single AddressObject and array // Handle both single AddressObject and array
const addresses = Array.isArray(addressObj) ? addressObj : [addressObj]; const addresses = Array.isArray(addressObj) ? addressObj : [addressObj];
return addresses.flatMap((obj: any) => obj.value || []); return addresses.flatMap(obj => obj.value || []);
}; };
if (parsed.to) { if (parsed.to) {