types: Abstract inline interfaces to @plunk/types
This commit is contained in:
@@ -10,7 +10,7 @@ import {
|
||||
NotAuthenticated,
|
||||
NotFound,
|
||||
RateLimitError,
|
||||
ValidationError
|
||||
ValidationError,
|
||||
} from '../../exceptions/index.js';
|
||||
import {EmailService} from '../../services/EmailService.js';
|
||||
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import {describe, it, expect, beforeEach, beforeAll} from 'vitest';
|
||||
import {CampaignStatus, CampaignAudienceType} from '@plunk/db';
|
||||
import {beforeAll, beforeEach, describe, expect, it} from 'vitest';
|
||||
import {CampaignAudienceType, CampaignStatus} from '@plunk/db';
|
||||
import {factories, getPrismaClient} from '../../../../../test/helpers';
|
||||
|
||||
// Note: To run these integration tests, you need to:
|
||||
|
||||
+1
-1
@@ -20,7 +20,7 @@ import {
|
||||
SMTP_ENABLED,
|
||||
STRIPE_ENABLED,
|
||||
TRACKING_TOGGLE_ENABLED,
|
||||
WIKI_URI
|
||||
WIKI_URI,
|
||||
} from './app/constants.js';
|
||||
import {Actions} from './controllers/Actions.js';
|
||||
import {Activity} from './controllers/Activity.js';
|
||||
|
||||
@@ -1,8 +1,6 @@
|
||||
import {Controller, Middleware, Post} from '@overnightjs/core';
|
||||
import {ActionSchemas} from '@plunk/shared';
|
||||
import type {NextFunction, Request, Response} from 'express';
|
||||
|
||||
import type {AuthResponse} from '../middleware/auth.js';
|
||||
import {requirePublicKey, requireSecretKey} from '../middleware/auth.js';
|
||||
import {prisma} from '../database/prisma.js';
|
||||
import {ContactService} from '../services/ContactService.js';
|
||||
@@ -52,7 +50,7 @@ export class Actions {
|
||||
@Middleware([requirePublicKey])
|
||||
@CatchAsync
|
||||
public async track(req: Request, res: Response, _next: NextFunction) {
|
||||
const auth = res.locals.auth as AuthResponse;
|
||||
const auth = res.locals.auth;
|
||||
|
||||
// Zod validation - errors automatically handled by global error handler
|
||||
const {event, email, subscribed, data} = ActionSchemas.track.parse(req.body);
|
||||
@@ -173,7 +171,7 @@ export class Actions {
|
||||
@Middleware([requireSecretKey])
|
||||
@CatchAsync
|
||||
public async send(req: Request, res: Response, _next: NextFunction) {
|
||||
const auth = res.locals.auth as AuthResponse;
|
||||
const auth = res.locals.auth;
|
||||
|
||||
// Zod validation - errors automatically handled by global error handler
|
||||
const {to, subject, body, subscribed, name, from, reply, headers, data, template, attachments} =
|
||||
|
||||
@@ -1,8 +1,6 @@
|
||||
import {Controller, Get, Middleware} from '@overnightjs/core';
|
||||
import type {NextFunction, Request, Response} from 'express';
|
||||
import {ActivityType} from '@plunk/types';
|
||||
|
||||
import type {AuthResponse} from '../middleware/auth.js';
|
||||
import {requireAuth, requireEmailVerified} from '../middleware/auth.js';
|
||||
import {ActivityService} from '../services/ActivityService.js';
|
||||
import {CatchAsync} from '../utils/asyncHandler.js';
|
||||
@@ -25,7 +23,7 @@ export class Activity {
|
||||
@Middleware([requireAuth, requireEmailVerified])
|
||||
@CatchAsync
|
||||
public async getActivities(req: Request, res: Response, _next: NextFunction) {
|
||||
const auth = res.locals.auth as AuthResponse;
|
||||
const auth = res.locals.auth;
|
||||
const limit = Math.min(parseInt(req.query.limit as string) || 50, 100);
|
||||
const cursor = req.query.cursor as string | undefined;
|
||||
const contactId = req.query.contactId as string | undefined;
|
||||
@@ -66,7 +64,7 @@ export class Activity {
|
||||
@Middleware([requireAuth, requireEmailVerified])
|
||||
@CatchAsync
|
||||
public async getStats(req: Request, res: Response, _next: NextFunction) {
|
||||
const auth = res.locals.auth as AuthResponse;
|
||||
const auth = res.locals.auth;
|
||||
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;
|
||||
|
||||
@@ -86,7 +84,7 @@ export class Activity {
|
||||
@Middleware([requireAuth, requireEmailVerified])
|
||||
@CatchAsync
|
||||
public async getRecentCount(req: Request, res: Response, _next: NextFunction) {
|
||||
const auth = res.locals.auth as AuthResponse;
|
||||
const auth = res.locals.auth;
|
||||
const minutes = Math.min(parseInt(req.query.minutes as string) || 5, 60); // Max 60 minutes
|
||||
|
||||
const count = await ActivityService.getRecentActivityCount(auth.projectId, minutes);
|
||||
@@ -118,7 +116,7 @@ export class Activity {
|
||||
@Middleware([requireAuth, requireEmailVerified])
|
||||
@CatchAsync
|
||||
public async getUpcoming(req: Request, res: Response, _next: NextFunction) {
|
||||
const auth = res.locals.auth as AuthResponse;
|
||||
const auth = res.locals.auth;
|
||||
const limit = Math.min(parseInt(req.query.limit as string) || 50, 100);
|
||||
const daysAhead = Math.min(parseInt(req.query.daysAhead as string) || 30, 90);
|
||||
|
||||
|
||||
@@ -1,7 +1,5 @@
|
||||
import {Controller, Get, Middleware} from '@overnightjs/core';
|
||||
import type {NextFunction, Request, Response} from 'express';
|
||||
|
||||
import type {AuthResponse} from '../middleware/auth.js';
|
||||
import {requireAuth, requireEmailVerified} from '../middleware/auth.js';
|
||||
import {AnalyticsService} from '../services/AnalyticsService.js';
|
||||
import {CatchAsync} from '../utils/asyncHandler.js';
|
||||
@@ -22,7 +20,7 @@ export class Analytics {
|
||||
@Middleware([requireAuth, requireEmailVerified])
|
||||
@CatchAsync
|
||||
public async getTimeSeries(req: Request, res: Response, _next: NextFunction) {
|
||||
const auth = res.locals.auth as AuthResponse;
|
||||
const auth = res.locals.auth;
|
||||
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;
|
||||
|
||||
@@ -44,7 +42,7 @@ export class Analytics {
|
||||
@Middleware([requireAuth, requireEmailVerified])
|
||||
@CatchAsync
|
||||
public async getTopCampaigns(req: Request, res: Response, _next: NextFunction) {
|
||||
const auth = res.locals.auth as AuthResponse;
|
||||
const auth = res.locals.auth;
|
||||
const limit = Math.min(parseInt(req.query.limit as string) || 10, 50);
|
||||
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;
|
||||
@@ -68,7 +66,7 @@ export class Analytics {
|
||||
@Middleware([requireAuth, requireEmailVerified])
|
||||
@CatchAsync
|
||||
public async getCampaignStats(req: Request, res: Response, _next: NextFunction) {
|
||||
const auth = res.locals.auth as AuthResponse;
|
||||
const auth = res.locals.auth;
|
||||
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;
|
||||
|
||||
@@ -92,7 +90,7 @@ export class Analytics {
|
||||
@Middleware([requireAuth, requireEmailVerified])
|
||||
@CatchAsync
|
||||
public async getTopEvents(req: Request, res: Response, _next: NextFunction) {
|
||||
const auth = res.locals.auth as AuthResponse;
|
||||
const auth = res.locals.auth;
|
||||
const limit = Math.min(parseInt(req.query.limit as string) || 5, 20);
|
||||
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;
|
||||
|
||||
@@ -4,7 +4,6 @@ import {CampaignSchemas, UtilitySchemas} from '@plunk/shared';
|
||||
import type {NextFunction, Request, Response} from 'express';
|
||||
|
||||
import {HttpException} from '../exceptions/index.js';
|
||||
import type {AuthResponse} from '../middleware/auth.js';
|
||||
import {requireAuth, requireEmailVerified} from '../middleware/auth.js';
|
||||
import {CampaignService} from '../services/CampaignService.js';
|
||||
import {DomainService} from '../services/DomainService.js';
|
||||
@@ -20,7 +19,7 @@ export class Campaigns {
|
||||
@Middleware([requireAuth, requireEmailVerified])
|
||||
@CatchAsync
|
||||
private async create(req: Request, res: Response, _next: NextFunction) {
|
||||
const auth = res.locals.auth as AuthResponse;
|
||||
const auth = res.locals.auth;
|
||||
const {name, description, subject, body, from, fromName, replyTo, audienceType, audienceCondition, segmentId} =
|
||||
CampaignSchemas.create.parse(req.body);
|
||||
|
||||
@@ -63,7 +62,7 @@ export class Campaigns {
|
||||
@Middleware([requireAuth, requireEmailVerified])
|
||||
@CatchAsync
|
||||
private async list(req: Request, res: Response, _next: NextFunction) {
|
||||
const auth = res.locals.auth as AuthResponse;
|
||||
const auth = res.locals.auth;
|
||||
const status = req.query.status as CampaignStatus | undefined;
|
||||
const page = parseInt(req.query.page as string) || 1;
|
||||
const pageSize = parseInt(req.query.pageSize as string) || 20;
|
||||
@@ -96,7 +95,7 @@ export class Campaigns {
|
||||
@Middleware([requireAuth, requireEmailVerified])
|
||||
@CatchAsync
|
||||
private async get(req: Request, res: Response, _next: NextFunction) {
|
||||
const auth = res.locals.auth as AuthResponse;
|
||||
const auth = res.locals.auth;
|
||||
const {id} = UtilitySchemas.id.parse(req.params);
|
||||
|
||||
const campaign = await CampaignService.get(auth.projectId, id!);
|
||||
@@ -115,7 +114,7 @@ export class Campaigns {
|
||||
@Middleware([requireAuth, requireEmailVerified])
|
||||
@CatchAsync
|
||||
private async update(req: Request, res: Response, _next: NextFunction) {
|
||||
const auth = res.locals.auth as AuthResponse;
|
||||
const auth = res.locals.auth;
|
||||
const {id} = UtilitySchemas.id.parse(req.params);
|
||||
const {name, description, subject, body, from, fromName, replyTo, audienceType, audienceCondition, segmentId} =
|
||||
req.body;
|
||||
@@ -161,7 +160,7 @@ export class Campaigns {
|
||||
@Middleware([requireAuth, requireEmailVerified])
|
||||
@CatchAsync
|
||||
private async delete(req: Request, res: Response, _next: NextFunction) {
|
||||
const auth = res.locals.auth as AuthResponse;
|
||||
const auth = res.locals.auth;
|
||||
const {id} = UtilitySchemas.id.parse(req.params);
|
||||
|
||||
await CampaignService.delete(auth.projectId, id!);
|
||||
@@ -180,7 +179,7 @@ export class Campaigns {
|
||||
@Middleware([requireAuth, requireEmailVerified])
|
||||
@CatchAsync
|
||||
private async duplicate(req: Request, res: Response, _next: NextFunction) {
|
||||
const auth = res.locals.auth as AuthResponse;
|
||||
const auth = res.locals.auth;
|
||||
const {id} = UtilitySchemas.id.parse(req.params);
|
||||
|
||||
const campaign = await CampaignService.duplicate(auth.projectId, id!);
|
||||
@@ -200,7 +199,7 @@ export class Campaigns {
|
||||
@Middleware([requireAuth, requireEmailVerified])
|
||||
@CatchAsync
|
||||
private async send(req: Request, res: Response, _next: NextFunction) {
|
||||
const auth = res.locals.auth as AuthResponse;
|
||||
const auth = res.locals.auth;
|
||||
const {id} = UtilitySchemas.id.parse(req.params);
|
||||
const scheduledFor = req.body?.scheduledFor;
|
||||
|
||||
@@ -231,7 +230,7 @@ export class Campaigns {
|
||||
@Middleware([requireAuth, requireEmailVerified])
|
||||
@CatchAsync
|
||||
private async cancel(req: Request, res: Response, _next: NextFunction) {
|
||||
const auth = res.locals.auth as AuthResponse;
|
||||
const auth = res.locals.auth;
|
||||
const {id} = UtilitySchemas.id.parse(req.params);
|
||||
|
||||
const campaign = await CampaignService.cancel(auth.projectId, id!);
|
||||
@@ -251,7 +250,7 @@ export class Campaigns {
|
||||
@Middleware([requireAuth, requireEmailVerified])
|
||||
@CatchAsync
|
||||
private async stats(req: Request, res: Response, _next: NextFunction) {
|
||||
const auth = res.locals.auth as AuthResponse;
|
||||
const auth = res.locals.auth;
|
||||
const {id} = UtilitySchemas.id.parse(req.params);
|
||||
|
||||
const stats = await CampaignService.getStats(auth.projectId, id!);
|
||||
@@ -270,7 +269,7 @@ export class Campaigns {
|
||||
@Middleware([requireAuth, requireEmailVerified])
|
||||
@CatchAsync
|
||||
private async sendTest(req: Request, res: Response, _next: NextFunction) {
|
||||
const auth = res.locals.auth as AuthResponse;
|
||||
const auth = res.locals.auth;
|
||||
const {id} = UtilitySchemas.id.parse(req.params);
|
||||
const {email} = CampaignSchemas.sendTest.parse(req.body);
|
||||
|
||||
|
||||
@@ -2,8 +2,6 @@ import {Controller, Delete, Get, Middleware, Patch, Post} from '@overnightjs/cor
|
||||
import type {NextFunction, Request, Response} from 'express';
|
||||
import multer from 'multer';
|
||||
import signale from 'signale';
|
||||
|
||||
import type {AuthResponse} from '../middleware/auth.js';
|
||||
import {requireAuth, requireEmailVerified} from '../middleware/auth.js';
|
||||
import {ContactService} from '../services/ContactService.js';
|
||||
import {QueueService} from '../services/QueueService.js';
|
||||
@@ -35,7 +33,7 @@ export class Contacts {
|
||||
@Middleware([requireAuth, requireEmailVerified])
|
||||
@CatchAsync
|
||||
public async list(req: Request, res: Response, _next: NextFunction) {
|
||||
const auth = res.locals.auth as AuthResponse;
|
||||
const auth = res.locals.auth;
|
||||
const limit = Math.min(parseInt(req.query.limit as string) || 20, 100);
|
||||
const cursor = req.query.cursor as string | undefined;
|
||||
const search = req.query.search as string | undefined;
|
||||
@@ -54,7 +52,7 @@ export class Contacts {
|
||||
@Middleware([requireAuth, requireEmailVerified])
|
||||
@CatchAsync
|
||||
public async getAvailableFields(req: Request, res: Response, _next: NextFunction) {
|
||||
const auth = res.locals.auth as AuthResponse;
|
||||
const auth = res.locals.auth;
|
||||
|
||||
try {
|
||||
const fieldsWithTypes = await ContactService.getAvailableFields(auth.projectId!);
|
||||
@@ -80,7 +78,7 @@ export class Contacts {
|
||||
@Middleware([requireAuth, requireEmailVerified])
|
||||
@CatchAsync
|
||||
public async getFieldValues(req: Request, res: Response, _next: NextFunction) {
|
||||
const auth = res.locals.auth as AuthResponse;
|
||||
const auth = res.locals.auth;
|
||||
const field = req.params.field;
|
||||
const limit = Math.min(parseInt(req.query.limit as string) || 100, 200);
|
||||
|
||||
@@ -113,7 +111,7 @@ export class Contacts {
|
||||
@Middleware([requireAuth, requireEmailVerified])
|
||||
@CatchAsync
|
||||
public async get(req: Request, res: Response, _next: NextFunction) {
|
||||
const auth = res.locals.auth as AuthResponse;
|
||||
const auth = res.locals.auth;
|
||||
const contactId = req.params.id;
|
||||
|
||||
if (!contactId) {
|
||||
@@ -133,7 +131,7 @@ export class Contacts {
|
||||
@Middleware([requireAuth, requireEmailVerified])
|
||||
@CatchAsync
|
||||
public async create(req: Request, res: Response, _next: NextFunction) {
|
||||
const auth = res.locals.auth as AuthResponse;
|
||||
const auth = res.locals.auth;
|
||||
const {email, data, subscribed} = req.body;
|
||||
|
||||
if (!email) {
|
||||
@@ -163,7 +161,7 @@ export class Contacts {
|
||||
@Middleware([requireAuth, requireEmailVerified])
|
||||
@CatchAsync
|
||||
public async update(req: Request, res: Response, _next: NextFunction) {
|
||||
const auth = res.locals.auth as AuthResponse;
|
||||
const auth = res.locals.auth;
|
||||
const contactId = req.params.id;
|
||||
const {email, data, subscribed} = req.body;
|
||||
|
||||
@@ -184,7 +182,7 @@ export class Contacts {
|
||||
@Middleware([requireAuth, requireEmailVerified])
|
||||
@CatchAsync
|
||||
public async delete(req: Request, res: Response, _next: NextFunction) {
|
||||
const auth = res.locals.auth as AuthResponse;
|
||||
const auth = res.locals.auth;
|
||||
const contactId = req.params.id;
|
||||
|
||||
if (!contactId) {
|
||||
@@ -284,7 +282,7 @@ export class Contacts {
|
||||
@Middleware([requireAuth, upload.single('file')])
|
||||
@CatchAsync
|
||||
public async importCsv(req: Request, res: Response, _next: NextFunction) {
|
||||
const auth = res.locals.auth as AuthResponse;
|
||||
const auth = res.locals.auth;
|
||||
|
||||
if (!req.file) {
|
||||
return res.status(400).json({error: 'CSV file is required'});
|
||||
@@ -349,7 +347,7 @@ export class Contacts {
|
||||
@Middleware([requireAuth, requireEmailVerified])
|
||||
@CatchAsync
|
||||
public async getFieldUsage(req: Request, res: Response, _next: NextFunction) {
|
||||
const auth = res.locals.auth as AuthResponse;
|
||||
const auth = res.locals.auth;
|
||||
const field = req.params.field;
|
||||
|
||||
if (!field) {
|
||||
@@ -376,7 +374,7 @@ export class Contacts {
|
||||
@Middleware([requireAuth, requireEmailVerified])
|
||||
@CatchAsync
|
||||
public async deleteField(req: Request, res: Response, _next: NextFunction) {
|
||||
const auth = res.locals.auth as AuthResponse;
|
||||
const auth = res.locals.auth;
|
||||
const field = req.params.field;
|
||||
|
||||
if (!field) {
|
||||
@@ -402,7 +400,7 @@ export class Contacts {
|
||||
@Middleware([requireAuth, requireEmailVerified])
|
||||
@CatchAsync
|
||||
public async bulkSubscribe(req: Request, res: Response, _next: NextFunction) {
|
||||
const auth = res.locals.auth as AuthResponse;
|
||||
const auth = res.locals.auth;
|
||||
const {contactIds} = req.body;
|
||||
|
||||
if (!Array.isArray(contactIds) || contactIds.length === 0) {
|
||||
@@ -437,7 +435,7 @@ export class Contacts {
|
||||
@Middleware([requireAuth, requireEmailVerified])
|
||||
@CatchAsync
|
||||
public async bulkUnsubscribe(req: Request, res: Response, _next: NextFunction) {
|
||||
const auth = res.locals.auth as AuthResponse;
|
||||
const auth = res.locals.auth;
|
||||
const {contactIds} = req.body;
|
||||
|
||||
if (!Array.isArray(contactIds) || contactIds.length === 0) {
|
||||
@@ -471,7 +469,7 @@ export class Contacts {
|
||||
@Middleware([requireAuth, requireEmailVerified])
|
||||
@CatchAsync
|
||||
public async bulkDelete(req: Request, res: Response, _next: NextFunction) {
|
||||
const auth = res.locals.auth as AuthResponse;
|
||||
const auth = res.locals.auth;
|
||||
const {contactIds} = req.body;
|
||||
|
||||
if (!Array.isArray(contactIds) || contactIds.length === 0) {
|
||||
|
||||
@@ -4,12 +4,10 @@ import type {NextFunction, Request, Response} from 'express';
|
||||
|
||||
import {redis} from '../database/redis.js';
|
||||
import {NotFound} from '../exceptions/index.js';
|
||||
import type {AuthResponse} from '../middleware/auth.js';
|
||||
import {isAuthenticated, requireEmailVerified} from '../middleware/auth.js';
|
||||
import {DomainService} from '../services/DomainService.js';
|
||||
import {Keys} from '../services/keys.js';
|
||||
import {MembershipService} from '../services/MembershipService.js';
|
||||
import {prisma} from '../database/prisma.js';
|
||||
import {CatchAsync} from '../utils/asyncHandler.js';
|
||||
|
||||
@Controller('domains')
|
||||
@@ -21,7 +19,7 @@ export class Domains {
|
||||
@Middleware([isAuthenticated, requireEmailVerified])
|
||||
@CatchAsync
|
||||
public async getProjectDomains(req: Request, res: Response, _next: NextFunction) {
|
||||
const auth = res.locals.auth as AuthResponse;
|
||||
const auth = res.locals.auth;
|
||||
const {projectId} = DomainSchemas.projectId.parse(req.params);
|
||||
|
||||
// Verify user has access to this project
|
||||
@@ -39,7 +37,7 @@ export class Domains {
|
||||
@Middleware([isAuthenticated, requireEmailVerified])
|
||||
@CatchAsync
|
||||
public async addDomain(req: Request, res: Response, _next: NextFunction) {
|
||||
const auth = res.locals.auth as AuthResponse;
|
||||
const auth = res.locals.auth;
|
||||
const {projectId, domain} = DomainSchemas.create.parse(req.body);
|
||||
|
||||
if (!auth.userId) {
|
||||
@@ -87,7 +85,7 @@ export class Domains {
|
||||
@Middleware([isAuthenticated, requireEmailVerified])
|
||||
@CatchAsync
|
||||
public async checkVerification(req: Request, res: Response, _next: NextFunction) {
|
||||
const auth = res.locals.auth as AuthResponse;
|
||||
const auth = res.locals.auth;
|
||||
const {id} = UtilitySchemas.id.parse(req.params);
|
||||
|
||||
const domain = await DomainService.id(id);
|
||||
@@ -115,7 +113,7 @@ export class Domains {
|
||||
@Middleware([isAuthenticated, requireEmailVerified])
|
||||
@CatchAsync
|
||||
public async removeDomain(req: Request, res: Response, _next: NextFunction) {
|
||||
const auth = res.locals.auth as AuthResponse;
|
||||
const auth = res.locals.auth;
|
||||
const {id} = UtilitySchemas.id.parse(req.params);
|
||||
|
||||
const domain = await DomainService.id(id);
|
||||
|
||||
@@ -1,8 +1,6 @@
|
||||
import {Controller, Delete, Get, Middleware, Post} from '@overnightjs/core';
|
||||
import type {NextFunction, Request, Response} from 'express';
|
||||
import signale from 'signale';
|
||||
|
||||
import type {AuthResponse} from '../middleware/auth.js';
|
||||
import {requireAuth, requireEmailVerified} from '../middleware/auth.js';
|
||||
import {EventService} from '../services/EventService.js';
|
||||
import {CatchAsync} from '../utils/asyncHandler.js';
|
||||
@@ -17,7 +15,7 @@ export class Events {
|
||||
@Middleware([requireAuth, requireEmailVerified])
|
||||
@CatchAsync
|
||||
public async track(req: Request, res: Response, _next: NextFunction) {
|
||||
const auth = res.locals.auth as AuthResponse;
|
||||
const auth = res.locals.auth;
|
||||
const {name, contactId, emailId, data} = req.body;
|
||||
|
||||
if (!name) {
|
||||
@@ -37,7 +35,7 @@ export class Events {
|
||||
@Middleware([requireAuth, requireEmailVerified])
|
||||
@CatchAsync
|
||||
public async list(req: Request, res: Response, _next: NextFunction) {
|
||||
const auth = res.locals.auth as AuthResponse;
|
||||
const auth = res.locals.auth;
|
||||
const eventName = req.query.eventName as string | undefined;
|
||||
const limit = parseInt(req.query.limit as string) || 100;
|
||||
|
||||
@@ -54,7 +52,7 @@ export class Events {
|
||||
@Middleware([requireAuth, requireEmailVerified])
|
||||
@CatchAsync
|
||||
public async stats(req: Request, res: Response, _next: NextFunction) {
|
||||
const auth = res.locals.auth as AuthResponse;
|
||||
const auth = res.locals.auth;
|
||||
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;
|
||||
|
||||
@@ -71,7 +69,7 @@ export class Events {
|
||||
@Middleware([requireAuth, requireEmailVerified])
|
||||
@CatchAsync
|
||||
public async getContactEvents(req: Request, res: Response, _next: NextFunction) {
|
||||
const auth = res.locals.auth as AuthResponse;
|
||||
const auth = res.locals.auth;
|
||||
const contactId = req.params.contactId;
|
||||
const limit = parseInt(req.query.limit as string) || 50;
|
||||
|
||||
@@ -92,7 +90,7 @@ export class Events {
|
||||
@Middleware([requireAuth, requireEmailVerified])
|
||||
@CatchAsync
|
||||
public async getEventNames(req: Request, res: Response, _next: NextFunction) {
|
||||
const auth = res.locals.auth as AuthResponse;
|
||||
const auth = res.locals.auth;
|
||||
|
||||
const eventNames = await EventService.getUniqueEventNames(auth.projectId!);
|
||||
|
||||
@@ -108,7 +106,7 @@ export class Events {
|
||||
@Middleware([requireAuth, requireEmailVerified])
|
||||
@CatchAsync
|
||||
public async getEventUsage(req: Request, res: Response, _next: NextFunction) {
|
||||
const auth = res.locals.auth as AuthResponse;
|
||||
const auth = res.locals.auth;
|
||||
const eventName = req.params.eventName;
|
||||
|
||||
if (!eventName) {
|
||||
@@ -135,7 +133,7 @@ export class Events {
|
||||
@Middleware([requireAuth, requireEmailVerified])
|
||||
@CatchAsync
|
||||
public async deleteEvent(req: Request, res: Response, _next: NextFunction) {
|
||||
const auth = res.locals.auth as AuthResponse;
|
||||
const auth = res.locals.auth;
|
||||
const eventName = req.params.eventName;
|
||||
|
||||
if (!eventName) {
|
||||
|
||||
@@ -4,7 +4,6 @@ import {MembershipSchemas, UtilitySchemas} from '@plunk/shared';
|
||||
|
||||
import {prisma} from '../database/prisma.js';
|
||||
import {HttpException} from '../exceptions/index.js';
|
||||
import type {AuthResponse} from '../middleware/auth.js';
|
||||
import {requireAuth, requireEmailVerified} from '../middleware/auth.js';
|
||||
import {MembershipService} from '../services/MembershipService.js';
|
||||
import {SecurityService} from '../services/SecurityService.js';
|
||||
@@ -20,7 +19,7 @@ export class Projects {
|
||||
@Middleware([requireAuth, requireEmailVerified])
|
||||
@CatchAsync
|
||||
private async getSetupState(req: Request, res: Response, _next: NextFunction) {
|
||||
const auth = res.locals.auth as AuthResponse;
|
||||
const auth = res.locals.auth;
|
||||
const {id} = UtilitySchemas.id.parse(req.params);
|
||||
|
||||
// Verify user has access to this project
|
||||
@@ -84,7 +83,7 @@ export class Projects {
|
||||
@Middleware([requireAuth, requireEmailVerified])
|
||||
@CatchAsync
|
||||
private async getSecurityMetrics(req: Request, res: Response, _next: NextFunction) {
|
||||
const auth = res.locals.auth as AuthResponse;
|
||||
const auth = res.locals.auth;
|
||||
const {id} = UtilitySchemas.id.parse(req.params);
|
||||
|
||||
// Verify user has access to this project
|
||||
@@ -107,7 +106,7 @@ export class Projects {
|
||||
@Middleware([requireAuth, requireEmailVerified])
|
||||
@CatchAsync
|
||||
private async getMembers(req: Request, res: Response, _next: NextFunction) {
|
||||
const auth = res.locals.auth as AuthResponse;
|
||||
const auth = res.locals.auth;
|
||||
const {id} = UtilitySchemas.id.parse(req.params);
|
||||
|
||||
// Verify user has access to this project
|
||||
@@ -131,7 +130,7 @@ export class Projects {
|
||||
@Middleware([requireAuth, requireEmailVerified])
|
||||
@CatchAsync
|
||||
private async addMember(req: Request, res: Response, _next: NextFunction) {
|
||||
const auth = res.locals.auth as AuthResponse;
|
||||
const auth = res.locals.auth;
|
||||
const {id} = UtilitySchemas.id.parse(req.params);
|
||||
|
||||
// Validate params
|
||||
@@ -182,7 +181,7 @@ export class Projects {
|
||||
@Middleware([requireAuth, requireEmailVerified])
|
||||
@CatchAsync
|
||||
private async updateMemberRole(req: Request, res: Response, _next: NextFunction) {
|
||||
const auth = res.locals.auth as AuthResponse;
|
||||
const auth = res.locals.auth;
|
||||
const {id, userId} = req.params;
|
||||
|
||||
// Validate params
|
||||
@@ -235,7 +234,7 @@ export class Projects {
|
||||
@Middleware([requireAuth, requireEmailVerified])
|
||||
@CatchAsync
|
||||
private async removeMember(req: Request, res: Response, _next: NextFunction) {
|
||||
const auth = res.locals.auth as AuthResponse;
|
||||
const auth = res.locals.auth;
|
||||
const {id, userId} = req.params;
|
||||
|
||||
// Validate params
|
||||
|
||||
@@ -1,7 +1,5 @@
|
||||
import {Controller, Delete, Get, Middleware, Patch, Post} from '@overnightjs/core';
|
||||
import type {NextFunction, Request, Response} from 'express';
|
||||
|
||||
import type {AuthResponse} from '../middleware/auth.js';
|
||||
import {requireAuth, requireEmailVerified} from '../middleware/auth.js';
|
||||
import {SegmentService} from '../services/SegmentService.js';
|
||||
import {CatchAsync} from '../utils/asyncHandler.js';
|
||||
@@ -16,7 +14,7 @@ export class Segments {
|
||||
@Middleware([requireAuth, requireEmailVerified])
|
||||
@CatchAsync
|
||||
public async list(req: Request, res: Response, _next: NextFunction) {
|
||||
const auth = res.locals.auth as AuthResponse;
|
||||
const auth = res.locals.auth;
|
||||
|
||||
const segments = await SegmentService.list(auth.projectId!);
|
||||
|
||||
@@ -31,7 +29,7 @@ export class Segments {
|
||||
@Middleware([requireAuth, requireEmailVerified])
|
||||
@CatchAsync
|
||||
public async get(req: Request, res: Response, _next: NextFunction) {
|
||||
const auth = res.locals.auth as AuthResponse;
|
||||
const auth = res.locals.auth;
|
||||
const segmentId = req.params.id;
|
||||
|
||||
if (!segmentId) {
|
||||
@@ -51,7 +49,7 @@ export class Segments {
|
||||
@Middleware([requireAuth, requireEmailVerified])
|
||||
@CatchAsync
|
||||
public async getContacts(req: Request, res: Response, _next: NextFunction) {
|
||||
const auth = res.locals.auth as AuthResponse;
|
||||
const auth = res.locals.auth;
|
||||
const segmentId = req.params.id;
|
||||
const page = parseInt(req.query.page as string) || 1;
|
||||
const pageSize = Math.min(parseInt(req.query.pageSize as string) || 20, 100);
|
||||
@@ -73,7 +71,7 @@ export class Segments {
|
||||
@Middleware([requireAuth, requireEmailVerified])
|
||||
@CatchAsync
|
||||
public async create(req: Request, res: Response, _next: NextFunction) {
|
||||
const auth = res.locals.auth as AuthResponse;
|
||||
const auth = res.locals.auth;
|
||||
const {name, description, condition, trackMembership} = req.body;
|
||||
|
||||
if (!name) {
|
||||
@@ -102,7 +100,7 @@ export class Segments {
|
||||
@Middleware([requireAuth, requireEmailVerified])
|
||||
@CatchAsync
|
||||
public async update(req: Request, res: Response, _next: NextFunction) {
|
||||
const auth = res.locals.auth as AuthResponse;
|
||||
const auth = res.locals.auth;
|
||||
const segmentId = req.params.id;
|
||||
const {name, description, condition, trackMembership} = req.body;
|
||||
|
||||
@@ -132,7 +130,7 @@ export class Segments {
|
||||
@Middleware([requireAuth, requireEmailVerified])
|
||||
@CatchAsync
|
||||
public async delete(req: Request, res: Response, _next: NextFunction) {
|
||||
const auth = res.locals.auth as AuthResponse;
|
||||
const auth = res.locals.auth;
|
||||
const segmentId = req.params.id;
|
||||
|
||||
if (!segmentId) {
|
||||
@@ -152,7 +150,7 @@ export class Segments {
|
||||
@Middleware([requireAuth, requireEmailVerified])
|
||||
@CatchAsync
|
||||
public async compute(req: Request, res: Response, _next: NextFunction) {
|
||||
const auth = res.locals.auth as AuthResponse;
|
||||
const auth = res.locals.auth;
|
||||
const segmentId = req.params.id;
|
||||
|
||||
if (!segmentId) {
|
||||
@@ -172,7 +170,7 @@ export class Segments {
|
||||
@Middleware([requireAuth, requireEmailVerified])
|
||||
@CatchAsync
|
||||
public async refresh(req: Request, res: Response, _next: NextFunction) {
|
||||
const auth = res.locals.auth as AuthResponse;
|
||||
const auth = res.locals.auth;
|
||||
const segmentId = req.params.id;
|
||||
|
||||
if (!segmentId) {
|
||||
|
||||
@@ -1,8 +1,6 @@
|
||||
import {Controller, Delete, Get, Middleware, Patch, Post} from '@overnightjs/core';
|
||||
import {TemplateType} from '@plunk/db';
|
||||
import type {NextFunction, Request, Response} from 'express';
|
||||
|
||||
import type {AuthResponse} from '../middleware/auth.js';
|
||||
import {requireAuth, requireEmailVerified} from '../middleware/auth.js';
|
||||
import {DomainService} from '../services/DomainService.js';
|
||||
import {TemplateService} from '../services/TemplateService.js';
|
||||
@@ -18,7 +16,7 @@ export class Templates {
|
||||
@Middleware([requireAuth, requireEmailVerified])
|
||||
@CatchAsync
|
||||
public async list(req: Request, res: Response, _next: NextFunction) {
|
||||
const auth = res.locals.auth as AuthResponse;
|
||||
const auth = res.locals.auth;
|
||||
const page = parseInt(req.query.page as string) || 1;
|
||||
const pageSize = Math.min(parseInt(req.query.pageSize as string) || 20, 100);
|
||||
const search = req.query.search as string | undefined;
|
||||
@@ -37,7 +35,7 @@ export class Templates {
|
||||
@Middleware([requireAuth, requireEmailVerified])
|
||||
@CatchAsync
|
||||
public async get(req: Request, res: Response, _next: NextFunction) {
|
||||
const auth = res.locals.auth as AuthResponse;
|
||||
const auth = res.locals.auth;
|
||||
const templateId = req.params.id;
|
||||
|
||||
if (!templateId) {
|
||||
@@ -57,7 +55,7 @@ export class Templates {
|
||||
@Middleware([requireAuth, requireEmailVerified])
|
||||
@CatchAsync
|
||||
public async create(req: Request, res: Response, _next: NextFunction) {
|
||||
const auth = res.locals.auth as AuthResponse;
|
||||
const auth = res.locals.auth;
|
||||
const {name, description, subject, body, from, fromName, replyTo, type} = req.body;
|
||||
|
||||
if (!name) {
|
||||
@@ -101,7 +99,7 @@ export class Templates {
|
||||
@Middleware([requireAuth, requireEmailVerified])
|
||||
@CatchAsync
|
||||
public async update(req: Request, res: Response, _next: NextFunction) {
|
||||
const auth = res.locals.auth as AuthResponse;
|
||||
const auth = res.locals.auth;
|
||||
const templateId = req.params.id;
|
||||
const {name, description, subject, body, from, fromName, replyTo, type} = req.body;
|
||||
|
||||
@@ -136,7 +134,7 @@ export class Templates {
|
||||
@Middleware([requireAuth, requireEmailVerified])
|
||||
@CatchAsync
|
||||
public async delete(req: Request, res: Response, _next: NextFunction) {
|
||||
const auth = res.locals.auth as AuthResponse;
|
||||
const auth = res.locals.auth;
|
||||
const templateId = req.params.id;
|
||||
|
||||
if (!templateId) {
|
||||
@@ -156,7 +154,7 @@ export class Templates {
|
||||
@Middleware([requireAuth, requireEmailVerified])
|
||||
@CatchAsync
|
||||
public async duplicate(req: Request, res: Response, _next: NextFunction) {
|
||||
const auth = res.locals.auth as AuthResponse;
|
||||
const auth = res.locals.auth;
|
||||
const templateId = req.params.id;
|
||||
|
||||
if (!templateId) {
|
||||
@@ -176,7 +174,7 @@ export class Templates {
|
||||
@Middleware([requireAuth, requireEmailVerified])
|
||||
@CatchAsync
|
||||
public async getUsage(req: Request, res: Response, _next: NextFunction) {
|
||||
const auth = res.locals.auth as AuthResponse;
|
||||
const auth = res.locals.auth;
|
||||
const templateId = req.params.id;
|
||||
|
||||
if (!templateId) {
|
||||
|
||||
@@ -2,8 +2,6 @@ import {Controller, Middleware, Post} from '@overnightjs/core';
|
||||
import type {NextFunction, Request, Response} from 'express';
|
||||
import multer from 'multer';
|
||||
import signale from 'signale';
|
||||
|
||||
import type {AuthResponse} from '../middleware/auth.js';
|
||||
import {requireAuth, requireEmailVerified} from '../middleware/auth.js';
|
||||
import * as S3Service from '../services/S3Service.js';
|
||||
import {CatchAsync} from '../utils/asyncHandler.js';
|
||||
@@ -36,7 +34,7 @@ export class Uploads {
|
||||
@Middleware([requireAuth, requireEmailVerified, upload.single('image')])
|
||||
@CatchAsync
|
||||
public async uploadImage(req: Request, res: Response, _next: NextFunction) {
|
||||
const auth = res.locals.auth as AuthResponse;
|
||||
const auth = res.locals.auth;
|
||||
|
||||
try {
|
||||
if (!S3Service.isS3Enabled()) {
|
||||
|
||||
@@ -8,7 +8,6 @@ import {DASHBOARD_URI, STRIPE_ENABLED, STRIPE_PRICE_EMAIL_USAGE, STRIPE_PRICE_ON
|
||||
import {stripe} from '../app/stripe.js';
|
||||
import {prisma} from '../database/prisma.js';
|
||||
import {ErrorCode, HttpException, NotAuthenticated, NotFound} from '../exceptions/index.js';
|
||||
import type {AuthResponse} from '../middleware/auth.js';
|
||||
import {isAuthenticated, requireEmailVerified} from '../middleware/auth.js';
|
||||
import {BillingLimitService} from '../services/BillingLimitService.js';
|
||||
import {MembershipService} from '../services/MembershipService.js';
|
||||
@@ -24,7 +23,7 @@ export class Users {
|
||||
@Middleware([isAuthenticated, requireEmailVerified])
|
||||
@CatchAsync
|
||||
public async me(req: Request, res: Response, _next: NextFunction) {
|
||||
const auth = res.locals.auth as AuthResponse;
|
||||
const auth = res.locals.auth;
|
||||
|
||||
if (!auth.userId) {
|
||||
throw new NotAuthenticated();
|
||||
@@ -43,7 +42,7 @@ export class Users {
|
||||
@Middleware([isAuthenticated, requireEmailVerified])
|
||||
@CatchAsync
|
||||
public async meProjects(req: Request, res: Response, _next: NextFunction) {
|
||||
const auth = res.locals.auth as AuthResponse;
|
||||
const auth = res.locals.auth;
|
||||
|
||||
if (!auth.userId) {
|
||||
throw new NotAuthenticated();
|
||||
@@ -58,7 +57,7 @@ export class Users {
|
||||
@Middleware([isAuthenticated, requireEmailVerified])
|
||||
@CatchAsync
|
||||
public async createProject(req: Request, res: Response, _next: NextFunction) {
|
||||
const auth = res.locals.auth as AuthResponse;
|
||||
const auth = res.locals.auth;
|
||||
|
||||
if (!auth.userId) {
|
||||
throw new NotAuthenticated();
|
||||
@@ -105,7 +104,7 @@ export class Users {
|
||||
@Middleware([isAuthenticated, requireEmailVerified])
|
||||
@CatchAsync
|
||||
public async updateProject(req: Request, res: Response, _next: NextFunction) {
|
||||
const auth = res.locals.auth as AuthResponse;
|
||||
const auth = res.locals.auth;
|
||||
const {id} = UtilitySchemas.id.parse(req.params);
|
||||
const data = ProjectSchemas.update.parse(req.body);
|
||||
|
||||
@@ -125,7 +124,7 @@ export class Users {
|
||||
@Middleware([isAuthenticated, requireEmailVerified])
|
||||
@CatchAsync
|
||||
public async regenerateProjectKeys(req: Request, res: Response, _next: NextFunction) {
|
||||
const auth = res.locals.auth as AuthResponse;
|
||||
const auth = res.locals.auth;
|
||||
const {id} = UtilitySchemas.id.parse(req.params);
|
||||
|
||||
// Verify user has admin/owner access to this project
|
||||
@@ -165,7 +164,7 @@ export class Users {
|
||||
@Middleware([isAuthenticated, requireEmailVerified])
|
||||
@CatchAsync
|
||||
public async createCheckoutSession(req: Request, res: Response, _next: NextFunction) {
|
||||
const auth = res.locals.auth as AuthResponse;
|
||||
const auth = res.locals.auth;
|
||||
const {id} = UtilitySchemas.id.parse(req.params);
|
||||
const {currency} = req.query;
|
||||
|
||||
@@ -245,7 +244,7 @@ export class Users {
|
||||
@Middleware([isAuthenticated, requireEmailVerified])
|
||||
@CatchAsync
|
||||
public async createBillingPortalSession(req: Request, res: Response, _next: NextFunction) {
|
||||
const auth = res.locals.auth as AuthResponse;
|
||||
const auth = res.locals.auth;
|
||||
const {id} = UtilitySchemas.id.parse(req.params);
|
||||
|
||||
// Check if billing is enabled
|
||||
@@ -283,7 +282,7 @@ export class Users {
|
||||
@Middleware([isAuthenticated, requireEmailVerified])
|
||||
@CatchAsync
|
||||
public async getBillingLimits(req: Request, res: Response, _next: NextFunction) {
|
||||
const auth = res.locals.auth as AuthResponse;
|
||||
const auth = res.locals.auth;
|
||||
const {id} = UtilitySchemas.id.parse(req.params);
|
||||
|
||||
if (!auth.userId) {
|
||||
@@ -307,7 +306,7 @@ export class Users {
|
||||
@Middleware([isAuthenticated, requireEmailVerified])
|
||||
@CatchAsync
|
||||
public async updateBillingLimits(req: Request, res: Response, _next: NextFunction) {
|
||||
const auth = res.locals.auth as AuthResponse;
|
||||
const auth = res.locals.auth;
|
||||
const {id} = UtilitySchemas.id.parse(req.params);
|
||||
|
||||
if (!auth.userId) {
|
||||
@@ -374,7 +373,7 @@ export class Users {
|
||||
@Middleware([isAuthenticated, requireEmailVerified])
|
||||
@CatchAsync
|
||||
public async getBillingConsumption(req: Request, res: Response, _next: NextFunction) {
|
||||
const auth = res.locals.auth as AuthResponse;
|
||||
const auth = res.locals.auth;
|
||||
const {id} = UtilitySchemas.id.parse(req.params);
|
||||
|
||||
// Check if billing is enabled
|
||||
@@ -491,7 +490,7 @@ export class Users {
|
||||
@Middleware([isAuthenticated, requireEmailVerified])
|
||||
@CatchAsync
|
||||
public async getBillingInvoices(req: Request, res: Response, _next: NextFunction) {
|
||||
const auth = res.locals.auth as AuthResponse;
|
||||
const auth = res.locals.auth;
|
||||
const {id} = UtilitySchemas.id.parse(req.params);
|
||||
|
||||
// Check if billing is enabled
|
||||
@@ -570,7 +569,7 @@ export class Users {
|
||||
@Middleware([isAuthenticated, requireEmailVerified])
|
||||
@CatchAsync
|
||||
public async getSecurityHealth(req: Request, res: Response, _next: NextFunction) {
|
||||
const auth = res.locals.auth as AuthResponse;
|
||||
const auth = res.locals.auth;
|
||||
const {id} = UtilitySchemas.id.parse(req.params);
|
||||
|
||||
if (!auth.userId) {
|
||||
@@ -594,7 +593,7 @@ export class Users {
|
||||
@Middleware([isAuthenticated, requireEmailVerified])
|
||||
@CatchAsync
|
||||
public async resetProject(req: Request, res: Response, _next: NextFunction) {
|
||||
const auth = res.locals.auth as AuthResponse;
|
||||
const auth = res.locals.auth;
|
||||
const {id} = UtilitySchemas.id.parse(req.params);
|
||||
|
||||
if (!auth.userId) {
|
||||
@@ -668,7 +667,7 @@ export class Users {
|
||||
@Middleware([isAuthenticated, requireEmailVerified])
|
||||
@CatchAsync
|
||||
public async deleteProject(req: Request, res: Response, _next: NextFunction) {
|
||||
const auth = res.locals.auth as AuthResponse;
|
||||
const auth = res.locals.auth;
|
||||
const {id} = UtilitySchemas.id.parse(req.params);
|
||||
|
||||
if (!auth.userId) {
|
||||
|
||||
@@ -2,8 +2,6 @@ import {Controller, Delete, Get, Middleware, Patch, Post} from '@overnightjs/cor
|
||||
import {WorkflowExecutionStatus} from '@plunk/db';
|
||||
import type {NextFunction, Request, Response} from 'express';
|
||||
import signale from 'signale';
|
||||
|
||||
import type {AuthResponse} from '../middleware/auth.js';
|
||||
import {requireAuth, requireEmailVerified} from '../middleware/auth.js';
|
||||
import {WorkflowService} from '../services/WorkflowService.js';
|
||||
import {CatchAsync} from '../utils/asyncHandler.js';
|
||||
@@ -18,7 +16,7 @@ export class Workflows {
|
||||
@Middleware([requireAuth, requireEmailVerified])
|
||||
@CatchAsync
|
||||
public async list(req: Request, res: Response, _next: NextFunction) {
|
||||
const auth = res.locals.auth as AuthResponse;
|
||||
const auth = res.locals.auth;
|
||||
const page = parseInt(req.query.page as string) || 1;
|
||||
const pageSize = Math.min(parseInt(req.query.pageSize as string) || 20, 100);
|
||||
const search = req.query.search as string | undefined;
|
||||
@@ -38,7 +36,7 @@ export class Workflows {
|
||||
@Middleware([requireAuth, requireEmailVerified])
|
||||
@CatchAsync
|
||||
public async getAvailableFields(req: Request, res: Response, _next: NextFunction) {
|
||||
const auth = res.locals.auth as AuthResponse;
|
||||
const auth = res.locals.auth;
|
||||
const eventName = req.query.eventName as string | undefined;
|
||||
|
||||
try {
|
||||
@@ -61,7 +59,7 @@ export class Workflows {
|
||||
@Middleware([requireAuth, requireEmailVerified])
|
||||
@CatchAsync
|
||||
public async get(req: Request, res: Response, _next: NextFunction) {
|
||||
const auth = res.locals.auth as AuthResponse;
|
||||
const auth = res.locals.auth;
|
||||
const workflowId = req.params.id;
|
||||
|
||||
if (!workflowId) {
|
||||
@@ -81,7 +79,7 @@ export class Workflows {
|
||||
@Middleware([requireAuth, requireEmailVerified])
|
||||
@CatchAsync
|
||||
public async create(req: Request, res: Response, _next: NextFunction) {
|
||||
const auth = res.locals.auth as AuthResponse;
|
||||
const auth = res.locals.auth;
|
||||
const {name, description, eventName, enabled, allowReentry} = req.body;
|
||||
|
||||
if (!name) {
|
||||
@@ -111,7 +109,7 @@ export class Workflows {
|
||||
@Middleware([requireAuth, requireEmailVerified])
|
||||
@CatchAsync
|
||||
public async update(req: Request, res: Response, _next: NextFunction) {
|
||||
const auth = res.locals.auth as AuthResponse;
|
||||
const auth = res.locals.auth;
|
||||
const workflowId = req.params.id;
|
||||
const {name, description, triggerType, triggerConfig, enabled, allowReentry} = req.body;
|
||||
|
||||
@@ -139,7 +137,7 @@ export class Workflows {
|
||||
@Middleware([requireAuth, requireEmailVerified])
|
||||
@CatchAsync
|
||||
public async delete(req: Request, res: Response, _next: NextFunction) {
|
||||
const auth = res.locals.auth as AuthResponse;
|
||||
const auth = res.locals.auth;
|
||||
const workflowId = req.params.id;
|
||||
|
||||
if (!workflowId) {
|
||||
@@ -159,7 +157,7 @@ export class Workflows {
|
||||
@Middleware([requireAuth, requireEmailVerified])
|
||||
@CatchAsync
|
||||
public async addStep(req: Request, res: Response, _next: NextFunction) {
|
||||
const auth = res.locals.auth as AuthResponse;
|
||||
const auth = res.locals.auth;
|
||||
const workflowId = req.params.id;
|
||||
const {type, name, position, config, templateId, autoConnect} = req.body;
|
||||
|
||||
@@ -191,7 +189,7 @@ export class Workflows {
|
||||
@Middleware([requireAuth, requireEmailVerified])
|
||||
@CatchAsync
|
||||
public async updateStep(req: Request, res: Response, _next: NextFunction) {
|
||||
const auth = res.locals.auth as AuthResponse;
|
||||
const auth = res.locals.auth;
|
||||
const workflowId = req.params.id;
|
||||
const stepId = req.params.stepId;
|
||||
const {name, position, config, templateId} = req.body;
|
||||
@@ -218,7 +216,7 @@ export class Workflows {
|
||||
@Middleware([requireAuth, requireEmailVerified])
|
||||
@CatchAsync
|
||||
public async deleteStep(req: Request, res: Response, _next: NextFunction) {
|
||||
const auth = res.locals.auth as AuthResponse;
|
||||
const auth = res.locals.auth;
|
||||
const workflowId = req.params.id;
|
||||
const stepId = req.params.stepId;
|
||||
|
||||
@@ -239,7 +237,7 @@ export class Workflows {
|
||||
@Middleware([requireAuth, requireEmailVerified])
|
||||
@CatchAsync
|
||||
public async createTransition(req: Request, res: Response, _next: NextFunction) {
|
||||
const auth = res.locals.auth as AuthResponse;
|
||||
const auth = res.locals.auth;
|
||||
const workflowId = req.params.id;
|
||||
const {fromStepId, toStepId, condition, priority} = req.body;
|
||||
|
||||
@@ -269,7 +267,7 @@ export class Workflows {
|
||||
@Middleware([requireAuth, requireEmailVerified])
|
||||
@CatchAsync
|
||||
public async deleteTransition(req: Request, res: Response, _next: NextFunction) {
|
||||
const auth = res.locals.auth as AuthResponse;
|
||||
const auth = res.locals.auth;
|
||||
const workflowId = req.params.id;
|
||||
const transitionId = req.params.transitionId;
|
||||
|
||||
@@ -290,7 +288,7 @@ export class Workflows {
|
||||
@Middleware([requireAuth, requireEmailVerified])
|
||||
@CatchAsync
|
||||
public async startExecution(req: Request, res: Response, _next: NextFunction) {
|
||||
const auth = res.locals.auth as AuthResponse;
|
||||
const auth = res.locals.auth;
|
||||
const workflowId = req.params.id;
|
||||
const {contactId, context} = req.body;
|
||||
|
||||
@@ -315,7 +313,7 @@ export class Workflows {
|
||||
@Middleware([requireAuth, requireEmailVerified])
|
||||
@CatchAsync
|
||||
public async listExecutions(req: Request, res: Response, _next: NextFunction) {
|
||||
const auth = res.locals.auth as AuthResponse;
|
||||
const auth = res.locals.auth;
|
||||
const workflowId = req.params.id;
|
||||
const page = parseInt(req.query.page as string) || 1;
|
||||
const pageSize = Math.min(parseInt(req.query.pageSize as string) || 20, 100);
|
||||
@@ -338,7 +336,7 @@ export class Workflows {
|
||||
@Middleware([requireAuth, requireEmailVerified])
|
||||
@CatchAsync
|
||||
public async getExecution(req: Request, res: Response, _next: NextFunction) {
|
||||
const auth = res.locals.auth as AuthResponse;
|
||||
const auth = res.locals.auth;
|
||||
const workflowId = req.params.id;
|
||||
const executionId = req.params.executionId;
|
||||
|
||||
@@ -359,7 +357,7 @@ export class Workflows {
|
||||
@Middleware([requireAuth, requireEmailVerified])
|
||||
@CatchAsync
|
||||
public async cancelExecution(req: Request, res: Response, _next: NextFunction) {
|
||||
const auth = res.locals.auth as AuthResponse;
|
||||
const auth = res.locals.auth;
|
||||
const workflowId = req.params.id;
|
||||
const executionId = req.params.executionId;
|
||||
|
||||
@@ -380,7 +378,7 @@ export class Workflows {
|
||||
@Middleware([requireAuth, requireEmailVerified])
|
||||
@CatchAsync
|
||||
public async cancelAllExecutions(req: Request, res: Response, _next: NextFunction) {
|
||||
const auth = res.locals.auth as AuthResponse;
|
||||
const auth = res.locals.auth;
|
||||
const workflowId = req.params.id;
|
||||
|
||||
if (!workflowId) {
|
||||
|
||||
@@ -74,9 +74,7 @@ export class NotFound extends HttpException {
|
||||
* @param id Optional resource identifier to include in the message
|
||||
*/
|
||||
public constructor(resource: string, id?: string) {
|
||||
const message = id
|
||||
? `${resource} with ID "${id}" was not found`
|
||||
: `That ${resource.toLowerCase()} was not found`;
|
||||
const message = id ? `${resource} with ID "${id}" was not found` : `That ${resource.toLowerCase()} was not found`;
|
||||
|
||||
// Map common resources to specific error codes
|
||||
const errorCodeMap: Record<string, ErrorCode> = {
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import {beforeEach, describe, expect, it, vi} from 'vitest';
|
||||
import type {Prisma} from '@plunk/db';
|
||||
import {EmailSourceType, EmailStatus, TrackingMode} from '@plunk/db';
|
||||
import {toPrismaJson} from '@plunk/types';
|
||||
import {createServiceMocks, factories, getPrismaClient} from '../../../../../test/helpers';
|
||||
|
||||
// Mock MeterService
|
||||
@@ -261,13 +261,13 @@ describe('Email Processor', () => {
|
||||
from: 'test@example.com',
|
||||
status: EmailStatus.PENDING,
|
||||
sourceType: EmailSourceType.TRANSACTIONAL,
|
||||
attachments: [
|
||||
attachments: toPrismaJson([
|
||||
{
|
||||
filename: 'document.pdf',
|
||||
content: 'base64encodedcontent',
|
||||
contentType: 'application/pdf',
|
||||
},
|
||||
] as unknown as Prisma.InputJsonValue,
|
||||
]),
|
||||
},
|
||||
});
|
||||
|
||||
@@ -306,9 +306,7 @@ describe('Email Processor', () => {
|
||||
from: 'test@example.com',
|
||||
status: EmailStatus.PENDING,
|
||||
sourceType: EmailSourceType.TRANSACTIONAL,
|
||||
attachments: [
|
||||
{filename: 'file.pdf', content: 'base64', contentType: 'application/pdf'},
|
||||
] as unknown as Prisma.InputJsonValue,
|
||||
attachments: toPrismaJson([{filename: 'file.pdf', content: 'base64', contentType: 'application/pdf'}]),
|
||||
},
|
||||
include: {
|
||||
project: true,
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import {describe, it, expect, beforeEach, afterEach} from 'vitest';
|
||||
import {afterEach, beforeEach, describe, expect, it} from 'vitest';
|
||||
import {CampaignStatus} from '@plunk/db';
|
||||
import {factories, getPrismaClient, createTimeControl} from '../../../../../test/helpers';
|
||||
import {createTimeControl, factories, getPrismaClient} from '../../../../../test/helpers';
|
||||
|
||||
describe('Scheduled Campaign Processor', () => {
|
||||
let projectId: string;
|
||||
|
||||
@@ -2,18 +2,14 @@ import dayjs from 'dayjs';
|
||||
import type {NextFunction, Request, Response} from 'express';
|
||||
import jsonwebtoken from 'jsonwebtoken';
|
||||
|
||||
import type {AuthResponse} from '@plunk/types';
|
||||
|
||||
import {JWT_SECRET, PLUNK_ENABLED} from '../app/constants.js';
|
||||
import {ErrorCode, HttpException, NotAuthenticated} from '../exceptions/index.js';
|
||||
import {MembershipService} from '../services/MembershipService.js';
|
||||
import {ProjectService} from '../services/ProjectService.js';
|
||||
import {UserService} from '../services/UserService.js';
|
||||
|
||||
export interface AuthResponse {
|
||||
type: 'jwt' | 'apiKey';
|
||||
userId?: string;
|
||||
projectId: string;
|
||||
}
|
||||
|
||||
/**
|
||||
* Middleware to check if this unsubscribe is authenticated on the dashboard
|
||||
* @param req
|
||||
@@ -129,7 +125,7 @@ export const requirePublicKey = async (req: Request, res: Response, next: NextFu
|
||||
res.locals.auth = {
|
||||
type: 'apiKey',
|
||||
projectId: project.id,
|
||||
} as AuthResponse;
|
||||
};
|
||||
|
||||
// Check if project is disabled - block write operations
|
||||
if (project.disabled) {
|
||||
@@ -198,7 +194,7 @@ export const requireSecretKey = async (req: Request, res: Response, next: NextFu
|
||||
res.locals.auth = {
|
||||
type: 'apiKey',
|
||||
projectId: project.id,
|
||||
} as AuthResponse;
|
||||
};
|
||||
|
||||
// Check if project is disabled - block write operations
|
||||
if (project.disabled) {
|
||||
@@ -309,7 +305,7 @@ export const requireAuth = async (req: Request, res: Response, next: NextFunctio
|
||||
type: 'jwt',
|
||||
userId,
|
||||
projectId,
|
||||
} as AuthResponse;
|
||||
};
|
||||
|
||||
// Check if project is disabled - block write operations
|
||||
if (project?.disabled) {
|
||||
@@ -340,7 +336,7 @@ export const requireAuth = async (req: Request, res: Response, next: NextFunctio
|
||||
*/
|
||||
export const requireEmailVerified = async (req: Request, res: Response, next: NextFunction) => {
|
||||
try {
|
||||
const auth = res.locals.auth as AuthResponse;
|
||||
const auth = res.locals.auth;
|
||||
|
||||
if (auth.type === 'apiKey') {
|
||||
return next();
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import type {Prisma} from '@plunk/db';
|
||||
import {ActivityType} from '@plunk/types';
|
||||
import type {Activity, ActivityStats, CursorPaginatedResponse} from '@plunk/types';
|
||||
import {ActivityType} from '@plunk/types';
|
||||
import signale from 'signale';
|
||||
|
||||
import {prisma} from '../database/prisma.js';
|
||||
|
||||
@@ -58,7 +58,11 @@ export class AnalyticsService {
|
||||
const limitedStartDate = effectiveStartDate < maxStartDate ? maxStartDate : effectiveStartDate;
|
||||
|
||||
// Check cache first
|
||||
const cacheKey = Keys.Analytics.timeseries(projectId, limitedStartDate.toISOString(), effectiveEndDate.toISOString());
|
||||
const cacheKey = Keys.Analytics.timeseries(
|
||||
projectId,
|
||||
limitedStartDate.toISOString(),
|
||||
effectiveEndDate.toISOString(),
|
||||
);
|
||||
const cached = await redis.get(cacheKey);
|
||||
if (cached) {
|
||||
return JSON.parse(cached);
|
||||
@@ -191,7 +195,11 @@ export class AnalyticsService {
|
||||
const effectiveEndDate = endDate || now;
|
||||
|
||||
// Check cache
|
||||
const cacheKey = Keys.Analytics.campaignStats(projectId, effectiveStartDate.toISOString(), effectiveEndDate.toISOString());
|
||||
const cacheKey = Keys.Analytics.campaignStats(
|
||||
projectId,
|
||||
effectiveStartDate.toISOString(),
|
||||
effectiveEndDate.toISOString(),
|
||||
);
|
||||
const cached = await redis.get(cacheKey);
|
||||
if (cached) {
|
||||
return JSON.parse(cached);
|
||||
@@ -295,7 +303,12 @@ export class AnalyticsService {
|
||||
const effectiveEndDate = endDate || now;
|
||||
|
||||
// Check cache
|
||||
const cacheKey = Keys.Analytics.topEvents(projectId, limit, effectiveStartDate.toISOString(), effectiveEndDate.toISOString());
|
||||
const cacheKey = Keys.Analytics.topEvents(
|
||||
projectId,
|
||||
limit,
|
||||
effectiveStartDate.toISOString(),
|
||||
effectiveEndDate.toISOString(),
|
||||
);
|
||||
const cached = await redis.get(cacheKey);
|
||||
if (cached) {
|
||||
return JSON.parse(cached);
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import {EmailSourceType} from '@plunk/db';
|
||||
import type {CategoryUsage, BillingLimitsResponse, LimitCheckResult} from '@plunk/types';
|
||||
import type {BillingLimitsResponse, CategoryUsage, LimitCheckResult} from '@plunk/types';
|
||||
import {BillingLimitExceededEmail, BillingLimitWarningEmail, sendPlatformEmail} from '@plunk/email';
|
||||
import React from 'react';
|
||||
import signale from 'signale';
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import type {Campaign, Contact, Prisma} from '@plunk/db';
|
||||
import {CampaignAudienceType, CampaignStatus, EmailSourceType} from '@plunk/db';
|
||||
import type {FilterCondition, CreateCampaignData, UpdateCampaignData} from '@plunk/types';
|
||||
import type {CreateCampaignData, FilterCondition, UpdateCampaignData} from '@plunk/types';
|
||||
import {fromPrismaJson, toPrismaJson} from '@plunk/types';
|
||||
import signale from 'signale';
|
||||
|
||||
import {prisma} from '../database/prisma.js';
|
||||
@@ -59,7 +60,7 @@ export class CampaignService {
|
||||
fromName: data.fromName,
|
||||
replyTo: data.replyTo,
|
||||
audienceType: data.audienceType,
|
||||
audienceCondition: (data.audienceCondition || null) as unknown as Prisma.InputJsonValue,
|
||||
audienceCondition: toPrismaJson(data.audienceCondition || null),
|
||||
segmentId: data.segmentId,
|
||||
status: CampaignStatus.DRAFT,
|
||||
totalRecipients: 0, // Will be updated below
|
||||
@@ -107,7 +108,7 @@ export class CampaignService {
|
||||
if (data.audienceCondition) {
|
||||
SegmentService.validateCondition(data.audienceCondition);
|
||||
}
|
||||
updateData.audienceCondition = (data.audienceCondition || null) as unknown as Prisma.InputJsonValue;
|
||||
updateData.audienceCondition = toPrismaJson(data.audienceCondition || null);
|
||||
}
|
||||
|
||||
if (data.segmentId !== undefined) {
|
||||
@@ -721,7 +722,7 @@ export class CampaignService {
|
||||
return this.buildSegmentWhereAsync(projectId, campaign.segmentId, baseWhere);
|
||||
|
||||
case CampaignAudienceType.FILTERED: {
|
||||
const condition = campaign.audienceCondition as unknown as FilterCondition;
|
||||
const condition = fromPrismaJson<FilterCondition>(campaign.audienceCondition);
|
||||
if (!condition) {
|
||||
throw new HttpException(400, 'Audience condition is required for FILTERED audience type');
|
||||
}
|
||||
@@ -757,7 +758,7 @@ export class CampaignService {
|
||||
throw new HttpException(404, 'Segment not found');
|
||||
}
|
||||
|
||||
const condition = segment.condition as unknown as FilterCondition;
|
||||
const condition = fromPrismaJson<FilterCondition>(segment.condition);
|
||||
const segmentWhere = SegmentService.buildConditionClause(condition);
|
||||
|
||||
return {
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import {type Contact, Prisma} from '@plunk/db';
|
||||
import {isValidLanguageCode} from '@plunk/shared';
|
||||
import type {FilterCondition, FilterGroup, CursorPaginatedResponse} from '@plunk/types';
|
||||
import type {CursorPaginatedResponse, FilterCondition, FilterGroup} from '@plunk/types';
|
||||
import {toPrismaJson} from '@plunk/types';
|
||||
|
||||
import {prisma} from '../database/prisma.js';
|
||||
import {HttpException} from '../exceptions/index.js';
|
||||
@@ -229,10 +230,7 @@ export class ContactService {
|
||||
if (key === 'locale') {
|
||||
if (typeof value === 'string') {
|
||||
if (!isValidLanguageCode(value)) {
|
||||
throw new HttpException(
|
||||
400,
|
||||
`Invalid locale code: ${value}. Must be one of: en, nl, fr, hi, de`,
|
||||
);
|
||||
throw new HttpException(400, `Invalid locale code: ${value}. Must be one of: en, nl, fr, hi, de`);
|
||||
}
|
||||
} else if (value !== null && value !== undefined) {
|
||||
throw new HttpException(400, 'Locale must be a string');
|
||||
@@ -265,7 +263,7 @@ export class ContactService {
|
||||
const updated = await prisma.contact.update({
|
||||
where: {id: existing.id},
|
||||
data: {
|
||||
data: Object.keys(mergedData).length > 0 ? (mergedData as Prisma.InputJsonValue) : Prisma.JsonNull,
|
||||
data: Object.keys(mergedData).length > 0 ? toPrismaJson(mergedData) : Prisma.JsonNull,
|
||||
...(subscribed !== undefined ? {subscribed} : {}),
|
||||
},
|
||||
});
|
||||
@@ -285,7 +283,7 @@ export class ContactService {
|
||||
data: {
|
||||
projectId,
|
||||
email,
|
||||
data: Object.keys(mergedData).length > 0 ? (mergedData as Prisma.InputJsonValue) : Prisma.JsonNull,
|
||||
data: Object.keys(mergedData).length > 0 ? toPrismaJson(mergedData) : Prisma.JsonNull,
|
||||
subscribed: subscribed ?? true,
|
||||
},
|
||||
});
|
||||
@@ -671,59 +669,11 @@ export class ContactService {
|
||||
return {deletedFrom: result};
|
||||
}
|
||||
|
||||
/**
|
||||
* Helper: Check if a field is used in a filter condition (recursive)
|
||||
*/
|
||||
private static fieldUsedInCondition(field: string, condition: FilterCondition | null): boolean {
|
||||
if (!condition || typeof condition !== 'object') {
|
||||
return false;
|
||||
}
|
||||
|
||||
// Check groups in the condition
|
||||
if (Array.isArray(condition.groups)) {
|
||||
for (const group of condition.groups) {
|
||||
if (this.fieldUsedInGroup(field, group)) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Helper: Check if a field is used in a filter group (recursive)
|
||||
*/
|
||||
private static fieldUsedInGroup(field: string, group: FilterGroup): boolean {
|
||||
if (!group || typeof group !== 'object') {
|
||||
return false;
|
||||
}
|
||||
|
||||
// Check filters in the group
|
||||
if (Array.isArray(group.filters)) {
|
||||
for (const filter of group.filters) {
|
||||
if (filter.field === field) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Check nested conditions
|
||||
if (group.conditions) {
|
||||
return this.fieldUsedInCondition(field, group.conditions);
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Bulk subscribe contacts
|
||||
* Updates multiple contacts to subscribed=true in batches
|
||||
*/
|
||||
public static async bulkSubscribe(
|
||||
projectId: string,
|
||||
contactIds: string[],
|
||||
): Promise<{updated: number}> {
|
||||
public static async bulkSubscribe(projectId: string, contactIds: string[]): Promise<{updated: number}> {
|
||||
// Verify all contacts belong to this project
|
||||
const contacts = await prisma.contact.findMany({
|
||||
where: {
|
||||
@@ -772,10 +722,7 @@ export class ContactService {
|
||||
/**
|
||||
* Bulk unsubscribe contacts
|
||||
*/
|
||||
public static async bulkUnsubscribe(
|
||||
projectId: string,
|
||||
contactIds: string[],
|
||||
): Promise<{updated: number}> {
|
||||
public static async bulkUnsubscribe(projectId: string, contactIds: string[]): Promise<{updated: number}> {
|
||||
const contacts = await prisma.contact.findMany({
|
||||
where: {
|
||||
id: {in: contactIds},
|
||||
@@ -822,10 +769,7 @@ export class ContactService {
|
||||
/**
|
||||
* Bulk delete contacts
|
||||
*/
|
||||
public static async bulkDelete(
|
||||
projectId: string,
|
||||
contactIds: string[],
|
||||
): Promise<{deleted: number}> {
|
||||
public static async bulkDelete(projectId: string, contactIds: string[]): Promise<{deleted: number}> {
|
||||
const result = await prisma.contact.deleteMany({
|
||||
where: {
|
||||
id: {in: contactIds},
|
||||
@@ -836,6 +780,51 @@ export class ContactService {
|
||||
return {deleted: result.count};
|
||||
}
|
||||
|
||||
/**
|
||||
* 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;
|
||||
}
|
||||
|
||||
/**
|
||||
* Track events sequentially to avoid database deadlocks
|
||||
* Processes events one at a time with error handling
|
||||
|
||||
@@ -1,11 +1,12 @@
|
||||
import type {Contact, Email, Prisma, Project} from '@plunk/db';
|
||||
import {EmailSourceType, EmailStatus, TrackingMode} from '@plunk/db';
|
||||
import {toPrismaJson} from '@plunk/types';
|
||||
import signale from 'signale';
|
||||
|
||||
import {DASHBOARD_URI, LANDING_URI, STRIPE_ENABLED} from '../app/constants.js';
|
||||
import {prisma} from '../database/prisma.js';
|
||||
import {HttpException} from '../exceptions/index.js';
|
||||
import {renderTemplate, createTranslatorSync} from '@plunk/shared';
|
||||
import {createTranslatorSync, renderTemplate} from '@plunk/shared';
|
||||
|
||||
import {BillingLimitService} from './BillingLimitService.js';
|
||||
import {DomainService} from './DomainService.js';
|
||||
@@ -91,8 +92,8 @@ export class EmailService {
|
||||
fromName: params.fromName,
|
||||
toName: params.toName,
|
||||
replyTo: params.replyTo,
|
||||
headers: params.headers ? (params.headers as Prisma.InputJsonValue) : undefined,
|
||||
attachments: params.attachments ? (params.attachments as unknown as Prisma.InputJsonValue) : undefined,
|
||||
headers: params.headers ? toPrismaJson(params.headers) : undefined,
|
||||
attachments: params.attachments ? toPrismaJson(params.attachments) : undefined,
|
||||
sourceType: EmailSourceType.TRANSACTIONAL,
|
||||
templateId: params.templateId,
|
||||
status: EmailStatus.PENDING,
|
||||
@@ -152,8 +153,8 @@ export class EmailService {
|
||||
from: params.from,
|
||||
fromName: params.fromName,
|
||||
replyTo: params.replyTo,
|
||||
headers: params.headers ? (params.headers as Prisma.InputJsonValue) : undefined,
|
||||
attachments: params.attachments ? (params.attachments as unknown as Prisma.InputJsonValue) : undefined,
|
||||
headers: params.headers ? toPrismaJson(params.headers) : undefined,
|
||||
attachments: params.attachments ? toPrismaJson(params.attachments) : undefined,
|
||||
sourceType,
|
||||
templateId: params.templateId,
|
||||
campaignId: params.campaignId,
|
||||
@@ -213,8 +214,8 @@ export class EmailService {
|
||||
from: params.from,
|
||||
fromName: params.fromName,
|
||||
replyTo: params.replyTo,
|
||||
headers: params.headers ? (params.headers as Prisma.InputJsonValue) : undefined,
|
||||
attachments: params.attachments ? (params.attachments as unknown as Prisma.InputJsonValue) : undefined,
|
||||
headers: params.headers ? toPrismaJson(params.headers) : undefined,
|
||||
attachments: params.attachments ? toPrismaJson(params.attachments) : undefined,
|
||||
sourceType,
|
||||
templateId: params.templateId,
|
||||
workflowExecutionId: params.workflowExecutionId,
|
||||
@@ -250,8 +251,8 @@ export class EmailService {
|
||||
from: params.from,
|
||||
fromName: params.fromName,
|
||||
replyTo: params.replyTo,
|
||||
headers: params.headers ? (params.headers as Prisma.InputJsonValue) : undefined,
|
||||
attachments: params.attachments ? (params.attachments as unknown as Prisma.InputJsonValue) : undefined,
|
||||
headers: params.headers ? toPrismaJson(params.headers) : undefined,
|
||||
attachments: params.attachments ? toPrismaJson(params.attachments) : undefined,
|
||||
sourceType,
|
||||
templateId: params.templateId,
|
||||
workflowExecutionId: params.workflowExecutionId,
|
||||
@@ -509,7 +510,7 @@ export class EmailService {
|
||||
contactId: email.contactId,
|
||||
emailId: email.id,
|
||||
name: `email.${eventType}`,
|
||||
data: metadata ? (metadata as Prisma.InputJsonValue) : undefined,
|
||||
data: metadata ? toPrismaJson(metadata) : undefined,
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
@@ -11,59 +11,6 @@ const CACHE_TTL_SECONDS = 24 * 60 * 60; // 24 hours (list updates daily)
|
||||
export class EmailVerificationService {
|
||||
private static disposableDomainsSet: Set<string> | null = null;
|
||||
|
||||
/**
|
||||
* Fetch and cache the disposable domains list from GitHub
|
||||
* Uses Redis for caching with 24-hour TTL
|
||||
* Falls back to in-memory cache if Redis fails
|
||||
*/
|
||||
private static async getDisposableDomains(): Promise<Set<string>> {
|
||||
// Return in-memory cache if available
|
||||
if (this.disposableDomainsSet) {
|
||||
return this.disposableDomainsSet;
|
||||
}
|
||||
|
||||
try {
|
||||
// Try to get from Redis cache first
|
||||
const cached = await redis.get(DISPOSABLE_DOMAINS_CACHE_KEY);
|
||||
if (cached) {
|
||||
const domains = JSON.parse(cached) as string[];
|
||||
this.disposableDomainsSet = new Set(domains);
|
||||
return this.disposableDomainsSet;
|
||||
}
|
||||
|
||||
// Fetch from GitHub if not in cache
|
||||
const response = await fetch(DISPOSABLE_DOMAINS_URL);
|
||||
if (!response.ok) {
|
||||
throw new Error(`Failed to fetch disposable domains: ${response.statusText}`);
|
||||
}
|
||||
|
||||
const text = await response.text();
|
||||
const domains = text
|
||||
.split('\n')
|
||||
.map(line => line.trim())
|
||||
.filter(line => line && !line.startsWith('#')); // Filter empty lines and comments
|
||||
|
||||
// Cache in Redis
|
||||
await redis.set(DISPOSABLE_DOMAINS_CACHE_KEY, JSON.stringify(domains), 'EX', CACHE_TTL_SECONDS);
|
||||
|
||||
// Cache in memory
|
||||
this.disposableDomainsSet = new Set(domains);
|
||||
return this.disposableDomainsSet;
|
||||
} catch (error) {
|
||||
console.error('Error fetching disposable domains:', error);
|
||||
// Return empty set as fallback - don't block email verification
|
||||
return new Set<string>();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if a domain is disposable
|
||||
*/
|
||||
private static async isDisposableDomain(domain: string): Promise<boolean> {
|
||||
const disposableDomains = await this.getDisposableDomains();
|
||||
return disposableDomains.has(domain.toLowerCase());
|
||||
}
|
||||
|
||||
/**
|
||||
* Verify an email address
|
||||
* - Checks if domain exists (DNS A/AAAA records)
|
||||
@@ -146,4 +93,57 @@ export class EmailVerificationService {
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
/**
|
||||
* Fetch and cache the disposable domains list from GitHub
|
||||
* Uses Redis for caching with 24-hour TTL
|
||||
* Falls back to in-memory cache if Redis fails
|
||||
*/
|
||||
private static async getDisposableDomains(): Promise<Set<string>> {
|
||||
// Return in-memory cache if available
|
||||
if (this.disposableDomainsSet) {
|
||||
return this.disposableDomainsSet;
|
||||
}
|
||||
|
||||
try {
|
||||
// Try to get from Redis cache first
|
||||
const cached = await redis.get(DISPOSABLE_DOMAINS_CACHE_KEY);
|
||||
if (cached) {
|
||||
const domains = JSON.parse(cached) as string[];
|
||||
this.disposableDomainsSet = new Set(domains);
|
||||
return this.disposableDomainsSet;
|
||||
}
|
||||
|
||||
// Fetch from GitHub if not in cache
|
||||
const response = await fetch(DISPOSABLE_DOMAINS_URL);
|
||||
if (!response.ok) {
|
||||
throw new Error(`Failed to fetch disposable domains: ${response.statusText}`);
|
||||
}
|
||||
|
||||
const text = await response.text();
|
||||
const domains = text
|
||||
.split('\n')
|
||||
.map(line => line.trim())
|
||||
.filter(line => line && !line.startsWith('#')); // Filter empty lines and comments
|
||||
|
||||
// Cache in Redis
|
||||
await redis.set(DISPOSABLE_DOMAINS_CACHE_KEY, JSON.stringify(domains), 'EX', CACHE_TTL_SECONDS);
|
||||
|
||||
// Cache in memory
|
||||
this.disposableDomainsSet = new Set(domains);
|
||||
return this.disposableDomainsSet;
|
||||
} catch (error) {
|
||||
console.error('Error fetching disposable domains:', error);
|
||||
// Return empty set as fallback - don't block email verification
|
||||
return new Set<string>();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if a domain is disposable
|
||||
*/
|
||||
private static async isDisposableDomain(domain: string): Promise<boolean> {
|
||||
const disposableDomains = await this.getDisposableDomains();
|
||||
return disposableDomains.has(domain.toLowerCase());
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import type {Event} from '@plunk/db';
|
||||
import {Prisma} from '@plunk/db';
|
||||
import type {FilterCondition, FilterGroup} from '@plunk/types';
|
||||
import {toPrismaJson} from '@plunk/types';
|
||||
import signale from 'signale';
|
||||
|
||||
import {prisma} from '../database/prisma.js';
|
||||
@@ -32,7 +33,7 @@ export class EventService {
|
||||
contactId,
|
||||
emailId,
|
||||
name: eventName,
|
||||
data: data ? (data as Prisma.InputJsonValue) : undefined,
|
||||
data: data ? toPrismaJson(data) : undefined,
|
||||
},
|
||||
});
|
||||
|
||||
@@ -472,7 +473,7 @@ export class EventService {
|
||||
contactId,
|
||||
status: 'RUNNING',
|
||||
currentStepId: triggerStep.id,
|
||||
context: context ? (context as Prisma.InputJsonValue) : undefined,
|
||||
context: context ? toPrismaJson(context) : undefined,
|
||||
},
|
||||
});
|
||||
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import type {Membership, Role} from '@plunk/db';
|
||||
import type {MemberWithEmail, OwnerInfo, DisabledProjectInfo} from '@plunk/types';
|
||||
import type {Membership} from '@plunk/db';
|
||||
import type {DisabledProjectInfo, MemberWithEmail, OwnerInfo} from '@plunk/types';
|
||||
|
||||
import {prisma} from '../database/prisma.js';
|
||||
import {redis, REDIS_ONE_MINUTE, wrapRedis} from '../database/redis.js';
|
||||
@@ -141,7 +141,7 @@ export class MembershipService {
|
||||
},
|
||||
});
|
||||
|
||||
return memberships.map((m) => ({
|
||||
return memberships.map(m => ({
|
||||
userId: m.userId,
|
||||
email: m.user.email,
|
||||
role: m.role,
|
||||
@@ -193,11 +193,7 @@ export class MembershipService {
|
||||
* Add a member to a project
|
||||
* Invalidates cache for the project
|
||||
*/
|
||||
public static async addMember(
|
||||
projectId: string,
|
||||
userId: string,
|
||||
role: 'ADMIN' | 'MEMBER',
|
||||
): Promise<Membership> {
|
||||
public static async addMember(projectId: string, userId: string, role: 'ADMIN' | 'MEMBER'): Promise<Membership> {
|
||||
// Check if membership already exists
|
||||
const existingMembership = await prisma.membership.findUnique({
|
||||
where: {
|
||||
@@ -232,11 +228,7 @@ export class MembershipService {
|
||||
* Throws if trying to change OWNER role
|
||||
* Invalidates cache
|
||||
*/
|
||||
public static async updateRole(
|
||||
projectId: string,
|
||||
userId: string,
|
||||
newRole: 'ADMIN' | 'MEMBER',
|
||||
): Promise<Membership> {
|
||||
public static async updateRole(projectId: string, userId: string, newRole: 'ADMIN' | 'MEMBER'): Promise<Membership> {
|
||||
// Get existing membership
|
||||
const existingMembership = await prisma.membership.findUnique({
|
||||
where: {
|
||||
@@ -341,7 +333,7 @@ export class MembershipService {
|
||||
|
||||
return {
|
||||
hasDisabledProject: disabledMemberships.length > 0,
|
||||
disabledProjectNames: disabledMemberships.map((m) => m.project.name),
|
||||
disabledProjectNames: disabledMemberships.map(m => m.project.name),
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import {NtfyPriority, NtfyTag, type NtfyNotification} from '@plunk/types';
|
||||
import {type NtfyNotification, NtfyPriority, NtfyTag} from '@plunk/types';
|
||||
import signale from 'signale';
|
||||
|
||||
/**
|
||||
|
||||
@@ -2,15 +2,15 @@ import {type Job, Queue} from 'bullmq';
|
||||
import type {RedisOptions} from 'ioredis';
|
||||
import signale from 'signale';
|
||||
import type {
|
||||
SendEmailJobData,
|
||||
CampaignBatchJobData,
|
||||
ScheduledCampaignJobData,
|
||||
WorkflowStepJobData,
|
||||
ContactImportJobData,
|
||||
BulkContactActionJobData,
|
||||
SegmentCountJobData,
|
||||
DomainVerificationJobData,
|
||||
ApiRequestCleanupJobData,
|
||||
BulkContactActionJobData,
|
||||
CampaignBatchJobData,
|
||||
ContactImportJobData,
|
||||
DomainVerificationJobData,
|
||||
ScheduledCampaignJobData,
|
||||
SegmentCountJobData,
|
||||
SendEmailJobData,
|
||||
WorkflowStepJobData,
|
||||
} from '@plunk/types';
|
||||
|
||||
import {REDIS_URL} from '../app/constants.js';
|
||||
|
||||
@@ -1,21 +1,21 @@
|
||||
import {
|
||||
S3Client,
|
||||
PutObjectCommand,
|
||||
CreateBucketCommand,
|
||||
HeadBucketCommand,
|
||||
PutBucketPolicyCommand,
|
||||
PutObjectCommand,
|
||||
S3Client,
|
||||
} from '@aws-sdk/client-s3';
|
||||
import crypto from 'crypto';
|
||||
import signale from 'signale';
|
||||
|
||||
import {
|
||||
S3_ENDPOINT,
|
||||
S3_ACCESS_KEY_ID,
|
||||
S3_ACCESS_KEY_SECRET,
|
||||
S3_BUCKET,
|
||||
S3_PUBLIC_URL,
|
||||
S3_FORCE_PATH_STYLE,
|
||||
S3_ENABLED,
|
||||
S3_ENDPOINT,
|
||||
S3_FORCE_PATH_STYLE,
|
||||
S3_PUBLIC_URL,
|
||||
} from '../app/constants.js';
|
||||
|
||||
/**
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import {type Contact, Prisma, type Segment} from '@plunk/db';
|
||||
import type {FilterCondition, FilterGroup, SegmentFilter, PaginatedResponse} from '@plunk/types';
|
||||
import type {FilterCondition, FilterGroup, PaginatedResponse, SegmentFilter} from '@plunk/types';
|
||||
import {fromPrismaJson, toPrismaJson} from '@plunk/types';
|
||||
import signale from 'signale';
|
||||
|
||||
import {prisma} from '../database/prisma.js';
|
||||
@@ -66,7 +67,7 @@ export class SegmentService {
|
||||
pageSize = 20,
|
||||
): Promise<PaginatedResponse<Contact>> {
|
||||
const segment = await this.get(projectId, segmentId);
|
||||
const condition = segment.condition as unknown as FilterCondition;
|
||||
const condition = fromPrismaJson<FilterCondition>(segment.condition);
|
||||
|
||||
const where = this.buildWhereClause(projectId, condition);
|
||||
const skip = (page - 1) * pageSize;
|
||||
@@ -114,7 +115,7 @@ export class SegmentService {
|
||||
projectId,
|
||||
name: data.name,
|
||||
description: data.description,
|
||||
condition: data.condition as unknown as Prisma.InputJsonValue,
|
||||
condition: toPrismaJson(data.condition),
|
||||
trackMembership: data.trackMembership ?? false,
|
||||
memberCount,
|
||||
},
|
||||
@@ -161,7 +162,7 @@ export class SegmentService {
|
||||
updateData.description = data.description;
|
||||
}
|
||||
if (data.condition !== undefined) {
|
||||
updateData.condition = data.condition as unknown as Prisma.InputJsonValue;
|
||||
updateData.condition = toPrismaJson(data.condition);
|
||||
|
||||
// Recompute member count when condition changes
|
||||
const where = this.buildWhereClause(projectId, data.condition);
|
||||
@@ -229,7 +230,7 @@ export class SegmentService {
|
||||
*/
|
||||
public static async refreshMemberCount(projectId: string, segmentId: string): Promise<number> {
|
||||
const segment = await this.get(projectId, segmentId);
|
||||
const condition = segment.condition as unknown as FilterCondition;
|
||||
const condition = fromPrismaJson<FilterCondition>(segment.condition);
|
||||
const where = this.buildWhereClause(projectId, condition);
|
||||
|
||||
const memberCount = await prisma.contact.count({where});
|
||||
@@ -260,7 +261,7 @@ export class SegmentService {
|
||||
await Promise.all(
|
||||
batch.map(async segment => {
|
||||
try {
|
||||
const condition = segment.condition as unknown as FilterCondition;
|
||||
const condition = fromPrismaJson<FilterCondition>(segment.condition);
|
||||
const where = this.buildWhereClause(projectId, condition);
|
||||
const memberCount = await prisma.contact.count({where});
|
||||
|
||||
@@ -290,7 +291,7 @@ export class SegmentService {
|
||||
throw new HttpException(400, 'Segment does not have membership tracking enabled');
|
||||
}
|
||||
|
||||
const condition = segment.condition as unknown as FilterCondition;
|
||||
const condition = fromPrismaJson<FilterCondition>(segment.condition);
|
||||
const where = this.buildWhereClause(projectId, condition);
|
||||
|
||||
// Get all matching contacts using cursor-based pagination to avoid memory issues
|
||||
@@ -488,6 +489,19 @@ export class SegmentService {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Build Prisma clause from filter condition (recursive)
|
||||
*/
|
||||
public static buildConditionClause(condition: FilterCondition): Prisma.ContactWhereInput {
|
||||
const groupClauses = condition.groups.map(group => this.buildGroupClause(group));
|
||||
|
||||
if (condition.logic === 'AND') {
|
||||
return {AND: groupClauses};
|
||||
} else {
|
||||
return {OR: groupClauses};
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Validate filter group (recursive)
|
||||
*/
|
||||
@@ -586,19 +600,6 @@ export class SegmentService {
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Build Prisma clause from filter condition (recursive)
|
||||
*/
|
||||
public static buildConditionClause(condition: FilterCondition): Prisma.ContactWhereInput {
|
||||
const groupClauses = condition.groups.map(group => this.buildGroupClause(group));
|
||||
|
||||
if (condition.logic === 'AND') {
|
||||
return {AND: groupClauses};
|
||||
} else {
|
||||
return {OR: groupClauses};
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Build Prisma clause from filter group (recursive)
|
||||
*/
|
||||
|
||||
@@ -1,14 +1,15 @@
|
||||
import type {
|
||||
Contact,
|
||||
Prisma,
|
||||
Template,
|
||||
Workflow,
|
||||
WorkflowExecution,
|
||||
WorkflowStep,
|
||||
WorkflowStepExecution,
|
||||
Template,
|
||||
Workflow,
|
||||
} from '@plunk/db';
|
||||
import {StepExecutionStatus, WorkflowExecutionStatus} from '@plunk/db';
|
||||
import {WorkflowStepConfigSchemas, renderTemplate} from '@plunk/shared';
|
||||
import {toPrismaJson} from '@plunk/types';
|
||||
import {renderTemplate, WorkflowStepConfigSchemas} from '@plunk/shared';
|
||||
import signale from 'signale';
|
||||
|
||||
import {prisma} from '../database/prisma.js';
|
||||
@@ -80,7 +81,9 @@ export class WorkflowExecutionService {
|
||||
signale.info(`[WORKFLOW] Execution ${executionId} is WAITING, resuming from delay`);
|
||||
// This is a delayed step - continue with execution
|
||||
} else if (initialExecution.status !== WorkflowExecutionStatus.RUNNING) {
|
||||
signale.info(`[WORKFLOW] Execution ${executionId} already completed or cancelled with status ${initialExecution.status}, skipping`);
|
||||
signale.info(
|
||||
`[WORKFLOW] Execution ${executionId} already completed or cancelled with status ${initialExecution.status}, skipping`,
|
||||
);
|
||||
return; // Already completed or cancelled
|
||||
}
|
||||
|
||||
@@ -241,7 +244,7 @@ export class WorkflowExecutionService {
|
||||
data: {
|
||||
status: StepExecutionStatus.COMPLETED,
|
||||
completedAt: new Date(),
|
||||
output: result ? (result as Prisma.InputJsonValue) : undefined,
|
||||
output: result ? toPrismaJson(result) : undefined,
|
||||
},
|
||||
});
|
||||
|
||||
@@ -440,11 +443,11 @@ export class WorkflowExecutionService {
|
||||
data: {
|
||||
status: StepExecutionStatus.COMPLETED,
|
||||
completedAt: new Date(),
|
||||
output: {
|
||||
output: toPrismaJson({
|
||||
eventName,
|
||||
eventData: data ? (data as Prisma.InputJsonValue) : undefined,
|
||||
eventData: data ? toPrismaJson(data) : undefined,
|
||||
receivedAt: new Date().toISOString(),
|
||||
} as Prisma.InputJsonValue,
|
||||
}),
|
||||
},
|
||||
});
|
||||
|
||||
@@ -866,7 +869,7 @@ export class WorkflowExecutionService {
|
||||
await prisma.contact.update({
|
||||
where: {id: contact.id},
|
||||
data: {
|
||||
data: newData ? (newData as Prisma.InputJsonValue) : undefined,
|
||||
data: newData ? toPrismaJson(newData) : undefined,
|
||||
},
|
||||
});
|
||||
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import type {Workflow, WorkflowExecution, WorkflowStep, WorkflowStepExecution, WorkflowTransition} from '@plunk/db';
|
||||
import type {Workflow, WorkflowExecution, WorkflowStep, WorkflowTransition} from '@plunk/db';
|
||||
import {Prisma, WorkflowExecutionStatus} from '@plunk/db';
|
||||
import type {PaginatedResponse, WorkflowWithDetails, WorkflowExecutionWithDetails} from '@plunk/types';
|
||||
import type {PaginatedResponse, WorkflowExecutionWithDetails, WorkflowWithDetails} from '@plunk/types';
|
||||
import {toPrismaJson} from '@plunk/types';
|
||||
import signale from 'signale';
|
||||
|
||||
import {prisma} from '../database/prisma.js';
|
||||
@@ -15,7 +16,12 @@ export class WorkflowService {
|
||||
/**
|
||||
* Get all workflows for a project with pagination
|
||||
*/
|
||||
public static async list(projectId: string, page = 1, pageSize = 20, search?: string): Promise<PaginatedResponse<Workflow>> {
|
||||
public static async list(
|
||||
projectId: string,
|
||||
page = 1,
|
||||
pageSize = 20,
|
||||
search?: string,
|
||||
): Promise<PaginatedResponse<Workflow>> {
|
||||
const skip = (page - 1) * pageSize;
|
||||
|
||||
const where: Prisma.WorkflowWhereInput = {
|
||||
@@ -299,8 +305,8 @@ export class WorkflowService {
|
||||
workflowId,
|
||||
type: data.type,
|
||||
name: data.name,
|
||||
position: data.position as Prisma.InputJsonValue,
|
||||
config: data.config as Prisma.InputJsonValue,
|
||||
position: toPrismaJson(data.position),
|
||||
config: toPrismaJson(data.config),
|
||||
templateId: data.templateId,
|
||||
},
|
||||
});
|
||||
@@ -381,8 +387,8 @@ export class WorkflowService {
|
||||
const updateData: Prisma.WorkflowStepUpdateInput = {};
|
||||
|
||||
if (data.name !== undefined) updateData.name = data.name;
|
||||
if (data.position !== undefined) updateData.position = data.position as Prisma.InputJsonValue;
|
||||
if (data.config !== undefined) updateData.config = data.config as Prisma.InputJsonValue;
|
||||
if (data.position !== undefined) updateData.position = toPrismaJson(data.position);
|
||||
if (data.config !== undefined) updateData.config = toPrismaJson(data.config);
|
||||
if (data.templateId !== undefined) {
|
||||
if (data.templateId === null) {
|
||||
updateData.template = {disconnect: true};
|
||||
@@ -574,7 +580,7 @@ export class WorkflowService {
|
||||
fromStepId: data.fromStepId,
|
||||
condition: {
|
||||
path: ['branch'],
|
||||
equals: conditionObj.branch as Prisma.InputJsonValue,
|
||||
equals: toPrismaJson(conditionObj.branch),
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import {describe, it, expect, beforeEach, vi} from 'vitest';
|
||||
import {beforeEach, describe, expect, it, vi} from 'vitest';
|
||||
import {EmailSourceType} from '@plunk/db';
|
||||
import {BillingLimitService} from '../BillingLimitService';
|
||||
import {EmailService} from '../EmailService';
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import {describe, it, expect, beforeEach, vi} from 'vitest';
|
||||
import {CampaignStatus, CampaignAudienceType} from '@plunk/db';
|
||||
import {beforeEach, describe, expect, it, vi} from 'vitest';
|
||||
import {CampaignAudienceType, CampaignStatus} from '@plunk/db';
|
||||
import {CampaignService} from '../CampaignService';
|
||||
import {factories, getPrismaClient} from '../../../../../test/helpers';
|
||||
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import {describe, it, expect, beforeEach} from 'vitest';
|
||||
import {beforeEach, describe, expect, it} from 'vitest';
|
||||
import {ContactService} from '../ContactService';
|
||||
import {factories, getPrismaClient} from '../../../../../test/helpers';
|
||||
|
||||
@@ -687,12 +687,7 @@ describe('ContactService - Duplicate Prevention & Data Merging', () => {
|
||||
const p2Contact1 = await factories.createContact({projectId: project2.id, subscribed: false});
|
||||
const p2Contact2 = await factories.createContact({projectId: project2.id, subscribed: false});
|
||||
|
||||
await ContactService.bulkSubscribe(project1.id, [
|
||||
p1Contact1.id,
|
||||
p1Contact2.id,
|
||||
p2Contact1.id,
|
||||
p2Contact2.id,
|
||||
]);
|
||||
await ContactService.bulkSubscribe(project1.id, [p1Contact1.id, p1Contact2.id, p2Contact1.id, p2Contact2.id]);
|
||||
|
||||
const p1ContactsAfter = await prisma.contact.findMany({
|
||||
where: {projectId: project1.id},
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import {describe, it, expect, beforeEach, vi} from 'vitest';
|
||||
import {beforeEach, describe, expect, it, vi} from 'vitest';
|
||||
import {factories, getPrismaClient} from '../../../../../test/helpers';
|
||||
import {DomainService} from '../DomainService.js';
|
||||
import {HttpException} from '../../exceptions/index.js';
|
||||
@@ -110,9 +110,7 @@ describe('DomainService', () => {
|
||||
it('should throw error for invalid email format', async () => {
|
||||
const {project} = await factories.createUserWithProject();
|
||||
|
||||
await expect(DomainService.verifyEmailDomain('invalid-email', project.id)).rejects.toThrow(
|
||||
HttpException,
|
||||
);
|
||||
await expect(DomainService.verifyEmailDomain('invalid-email', project.id)).rejects.toThrow(HttpException);
|
||||
|
||||
await expect(DomainService.verifyEmailDomain('invalid-email', project.id)).rejects.toThrow(
|
||||
/invalid email format/i,
|
||||
@@ -122,13 +120,13 @@ describe('DomainService', () => {
|
||||
it('should throw error when domain is not registered', async () => {
|
||||
const {project} = await factories.createUserWithProject();
|
||||
|
||||
await expect(
|
||||
DomainService.verifyEmailDomain('sender@unregistered.com', project.id),
|
||||
).rejects.toThrow(HttpException);
|
||||
await expect(DomainService.verifyEmailDomain('sender@unregistered.com', project.id)).rejects.toThrow(
|
||||
HttpException,
|
||||
);
|
||||
|
||||
await expect(
|
||||
DomainService.verifyEmailDomain('sender@unregistered.com', project.id),
|
||||
).rejects.toThrow(/not registered/i);
|
||||
await expect(DomainService.verifyEmailDomain('sender@unregistered.com', project.id)).rejects.toThrow(
|
||||
/not registered/i,
|
||||
);
|
||||
});
|
||||
|
||||
it('should throw error when domain belongs to different project', async () => {
|
||||
@@ -141,13 +139,11 @@ describe('DomainService', () => {
|
||||
data: {verified: true},
|
||||
});
|
||||
|
||||
await expect(
|
||||
DomainService.verifyEmailDomain('sender@project1.com', project2.id),
|
||||
).rejects.toThrow(HttpException);
|
||||
await expect(DomainService.verifyEmailDomain('sender@project1.com', project2.id)).rejects.toThrow(HttpException);
|
||||
|
||||
await expect(
|
||||
DomainService.verifyEmailDomain('sender@project1.com', project2.id),
|
||||
).rejects.toThrow(/belongs to a different project/i);
|
||||
await expect(DomainService.verifyEmailDomain('sender@project1.com', project2.id)).rejects.toThrow(
|
||||
/belongs to a different project/i,
|
||||
);
|
||||
});
|
||||
|
||||
it('should throw error when domain is not verified', async () => {
|
||||
@@ -155,13 +151,11 @@ describe('DomainService', () => {
|
||||
|
||||
await DomainService.addDomain(project.id, 'unverified.com');
|
||||
|
||||
await expect(
|
||||
DomainService.verifyEmailDomain('sender@unverified.com', project.id),
|
||||
).rejects.toThrow(HttpException);
|
||||
await expect(DomainService.verifyEmailDomain('sender@unverified.com', project.id)).rejects.toThrow(HttpException);
|
||||
|
||||
await expect(
|
||||
DomainService.verifyEmailDomain('sender@unverified.com', project.id),
|
||||
).rejects.toThrow(/not verified/i);
|
||||
await expect(DomainService.verifyEmailDomain('sender@unverified.com', project.id)).rejects.toThrow(
|
||||
/not verified/i,
|
||||
);
|
||||
});
|
||||
|
||||
it('should return domain when all checks pass', async () => {
|
||||
@@ -358,9 +352,9 @@ describe('DomainService', () => {
|
||||
});
|
||||
|
||||
it('should throw error for non-existent domain', async () => {
|
||||
await expect(
|
||||
DomainService.checkVerification('00000000-0000-0000-0000-000000000000'),
|
||||
).rejects.toThrow(/domain not found/i);
|
||||
await expect(DomainService.checkVerification('00000000-0000-0000-0000-000000000000')).rejects.toThrow(
|
||||
/domain not found/i,
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -391,9 +385,7 @@ describe('DomainService', () => {
|
||||
|
||||
await expect(DomainService.removeDomain(domain.id)).rejects.toThrow(HttpException);
|
||||
|
||||
await expect(DomainService.removeDomain(domain.id)).rejects.toThrow(
|
||||
/used in.*template/i,
|
||||
);
|
||||
await expect(DomainService.removeDomain(domain.id)).rejects.toThrow(/used in.*template/i);
|
||||
});
|
||||
|
||||
it('should throw error when domain is used in active campaigns', async () => {
|
||||
@@ -409,9 +401,7 @@ describe('DomainService', () => {
|
||||
|
||||
await expect(DomainService.removeDomain(domain.id)).rejects.toThrow(HttpException);
|
||||
|
||||
await expect(DomainService.removeDomain(domain.id)).rejects.toThrow(
|
||||
/used in.*campaign/i,
|
||||
);
|
||||
await expect(DomainService.removeDomain(domain.id)).rejects.toThrow(/used in.*campaign/i);
|
||||
});
|
||||
|
||||
it('should allow removal when campaign is SENT (completed)', async () => {
|
||||
@@ -433,9 +423,9 @@ describe('DomainService', () => {
|
||||
});
|
||||
|
||||
it('should throw error for non-existent domain', async () => {
|
||||
await expect(
|
||||
DomainService.removeDomain('00000000-0000-0000-0000-000000000000'),
|
||||
).rejects.toThrow(/domain not found/i);
|
||||
await expect(DomainService.removeDomain('00000000-0000-0000-0000-000000000000')).rejects.toThrow(
|
||||
/domain not found/i,
|
||||
);
|
||||
});
|
||||
|
||||
it('should check usage in multiple templates', async () => {
|
||||
@@ -492,17 +482,15 @@ describe('DomainService', () => {
|
||||
expect(result.domain).toBe('mail.example.com');
|
||||
|
||||
// Different subdomain should fail
|
||||
await expect(
|
||||
DomainService.verifyEmailDomain('sender@other.example.com', project.id),
|
||||
).rejects.toThrow(/not registered/i);
|
||||
await expect(DomainService.verifyEmailDomain('sender@other.example.com', project.id)).rejects.toThrow(
|
||||
/not registered/i,
|
||||
);
|
||||
});
|
||||
|
||||
it('should handle email with no @ sign', async () => {
|
||||
const {project} = await factories.createUserWithProject();
|
||||
|
||||
await expect(DomainService.verifyEmailDomain('nodomain', project.id)).rejects.toThrow(
|
||||
/invalid email format/i,
|
||||
);
|
||||
await expect(DomainService.verifyEmailDomain('nodomain', project.id)).rejects.toThrow(/invalid email format/i);
|
||||
});
|
||||
|
||||
it('should handle email with multiple @ signs', async () => {
|
||||
@@ -516,9 +504,7 @@ describe('DomainService', () => {
|
||||
it('should handle empty email string', async () => {
|
||||
const {project} = await factories.createUserWithProject();
|
||||
|
||||
await expect(DomainService.verifyEmailDomain('', project.id)).rejects.toThrow(
|
||||
/invalid email format/i,
|
||||
);
|
||||
await expect(DomainService.verifyEmailDomain('', project.id)).rejects.toThrow(/invalid email format/i);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -537,11 +523,7 @@ describe('DomainService', () => {
|
||||
]);
|
||||
|
||||
expect(results).toHaveLength(3);
|
||||
expect(results.map(d => d.domain).sort()).toEqual([
|
||||
'concurrent1.com',
|
||||
'concurrent2.com',
|
||||
'concurrent3.com',
|
||||
]);
|
||||
expect(results.map(d => d.domain).sort()).toEqual(['concurrent1.com', 'concurrent2.com', 'concurrent3.com']);
|
||||
});
|
||||
|
||||
it('should handle concurrent ownership checks', async () => {
|
||||
|
||||
@@ -42,8 +42,8 @@ describe('SegmentService - Comprehensive Operator Tests', () => {
|
||||
});
|
||||
|
||||
const result = await SegmentService.getContacts(projectId, segment.id);
|
||||
expect(result.contacts).toHaveLength(1);
|
||||
expect(result.contacts[0].id).toBe(match.id);
|
||||
expect(result.data).toHaveLength(1);
|
||||
expect(result.data[0].id).toBe(match.id);
|
||||
});
|
||||
|
||||
it('should match exact string values in standard fields (case-insensitive)', async () => {
|
||||
@@ -61,8 +61,8 @@ describe('SegmentService - Comprehensive Operator Tests', () => {
|
||||
});
|
||||
|
||||
const result = await SegmentService.getContacts(projectId, segment.id);
|
||||
expect(result.contacts).toHaveLength(1);
|
||||
expect(result.contacts[0].id).toBe(match.id);
|
||||
expect(result.data).toHaveLength(1);
|
||||
expect(result.data[0].id).toBe(match.id);
|
||||
});
|
||||
|
||||
it('should match boolean values', async () => {
|
||||
@@ -80,8 +80,8 @@ describe('SegmentService - Comprehensive Operator Tests', () => {
|
||||
});
|
||||
|
||||
const result = await SegmentService.getContacts(projectId, segment.id);
|
||||
expect(result.contacts).toHaveLength(1);
|
||||
expect(result.contacts[0].id).toBe(match.id);
|
||||
expect(result.data).toHaveLength(1);
|
||||
expect(result.data[0].id).toBe(match.id);
|
||||
});
|
||||
|
||||
it('should match numeric values as strings in JSON fields', async () => {
|
||||
@@ -99,8 +99,8 @@ describe('SegmentService - Comprehensive Operator Tests', () => {
|
||||
});
|
||||
|
||||
const result = await SegmentService.getContacts(projectId, segment.id);
|
||||
expect(result.contacts).toHaveLength(1);
|
||||
expect(result.contacts[0].id).toBe(match.id);
|
||||
expect(result.data).toHaveLength(1);
|
||||
expect(result.data[0].id).toBe(match.id);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -120,8 +120,8 @@ describe('SegmentService - Comprehensive Operator Tests', () => {
|
||||
});
|
||||
|
||||
const result = await SegmentService.getContacts(projectId, segment.id);
|
||||
expect(result.contacts).toHaveLength(1);
|
||||
expect(result.contacts[0].id).toBe(match.id);
|
||||
expect(result.data).toHaveLength(1);
|
||||
expect(result.data[0].id).toBe(match.id);
|
||||
});
|
||||
|
||||
it('should exclude boolean false values', async () => {
|
||||
@@ -139,8 +139,8 @@ describe('SegmentService - Comprehensive Operator Tests', () => {
|
||||
});
|
||||
|
||||
const result = await SegmentService.getContacts(projectId, segment.id);
|
||||
expect(result.contacts).toHaveLength(1);
|
||||
expect(result.contacts[0].id).toBe(match.id);
|
||||
expect(result.data).toHaveLength(1);
|
||||
expect(result.data[0].id).toBe(match.id);
|
||||
});
|
||||
|
||||
it('should NOT include contacts where field does not exist (only excludes matching values)', async () => {
|
||||
@@ -162,7 +162,7 @@ describe('SegmentService - Comprehensive Operator Tests', () => {
|
||||
});
|
||||
|
||||
const result = await SegmentService.getContacts(projectId, segment.id);
|
||||
const ids = result.contacts.map(c => c.id);
|
||||
const ids = result.data.map(c => c.id);
|
||||
// notEquals only matches where field exists and has different value
|
||||
expect(ids).toContain(withDifferentValue.id);
|
||||
expect(ids).not.toContain(withMatchingField.id);
|
||||
@@ -186,8 +186,8 @@ describe('SegmentService - Comprehensive Operator Tests', () => {
|
||||
});
|
||||
|
||||
const result = await SegmentService.getContacts(projectId, segment.id);
|
||||
expect(result.contacts).toHaveLength(1);
|
||||
expect(result.contacts[0].id).toBe(match.id);
|
||||
expect(result.data).toHaveLength(1);
|
||||
expect(result.data[0].id).toBe(match.id);
|
||||
});
|
||||
|
||||
it('should match substring in email field (case-insensitive)', async () => {
|
||||
@@ -209,10 +209,10 @@ describe('SegmentService - Comprehensive Operator Tests', () => {
|
||||
});
|
||||
|
||||
const result = await SegmentService.getContacts(projectId, segment.id);
|
||||
const ids = result.contacts.map(c => c.id);
|
||||
const ids = result.data.map(c => c.id);
|
||||
expect(ids).toContain(match1.id);
|
||||
expect(ids).toContain(match2.id);
|
||||
expect(result.contacts).toHaveLength(2);
|
||||
expect(result.data).toHaveLength(2);
|
||||
});
|
||||
|
||||
it('should not match when field does not exist', async () => {
|
||||
@@ -226,7 +226,7 @@ describe('SegmentService - Comprehensive Operator Tests', () => {
|
||||
});
|
||||
|
||||
const result = await SegmentService.getContacts(projectId, segment.id);
|
||||
expect(result.contacts).toHaveLength(0);
|
||||
expect(result.data).toHaveLength(0);
|
||||
});
|
||||
|
||||
it('should match partial domain in email', async () => {
|
||||
@@ -244,8 +244,8 @@ describe('SegmentService - Comprehensive Operator Tests', () => {
|
||||
});
|
||||
|
||||
const result = await SegmentService.getContacts(projectId, segment.id);
|
||||
expect(result.contacts).toHaveLength(1);
|
||||
expect(result.contacts[0].id).toBe(gmailUser.id);
|
||||
expect(result.data).toHaveLength(1);
|
||||
expect(result.data[0].id).toBe(gmailUser.id);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -265,8 +265,8 @@ describe('SegmentService - Comprehensive Operator Tests', () => {
|
||||
});
|
||||
|
||||
const result = await SegmentService.getContacts(projectId, segment.id);
|
||||
expect(result.contacts).toHaveLength(1);
|
||||
expect(result.contacts[0].id).toBe(match.id);
|
||||
expect(result.data).toHaveLength(1);
|
||||
expect(result.data[0].id).toBe(match.id);
|
||||
});
|
||||
|
||||
it('should NOT include contacts where field does not exist (only excludes matching substrings)', async () => {
|
||||
@@ -288,7 +288,7 @@ describe('SegmentService - Comprehensive Operator Tests', () => {
|
||||
});
|
||||
|
||||
const result = await SegmentService.getContacts(projectId, segment.id);
|
||||
const ids = result.contacts.map(c => c.id);
|
||||
const ids = result.data.map(c => c.id);
|
||||
// notContains only matches where field exists and doesn't contain substring
|
||||
expect(ids).toContain(withDifferentValue.id);
|
||||
expect(ids).not.toContain(withMatchingSubstring.id);
|
||||
@@ -314,8 +314,8 @@ describe('SegmentService - Comprehensive Operator Tests', () => {
|
||||
});
|
||||
|
||||
const result = await SegmentService.getContacts(projectId, segment.id);
|
||||
expect(result.contacts).toHaveLength(1);
|
||||
expect(result.contacts[0].id).toBe(match.id);
|
||||
expect(result.data).toHaveLength(1);
|
||||
expect(result.data[0].id).toBe(match.id);
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -344,10 +344,10 @@ describe('SegmentService - Comprehensive Operator Tests', () => {
|
||||
});
|
||||
|
||||
const result = await SegmentService.getContacts(projectId, segment.id);
|
||||
const ids = result.contacts.map(c => c.id);
|
||||
const ids = result.data.map(c => c.id);
|
||||
expect(ids).toContain(high.id);
|
||||
expect(ids).toContain(veryHigh.id);
|
||||
expect(result.contacts).toHaveLength(2);
|
||||
expect(result.data).toHaveLength(2);
|
||||
});
|
||||
|
||||
it('should exclude values equal to threshold', async () => {
|
||||
@@ -361,7 +361,7 @@ describe('SegmentService - Comprehensive Operator Tests', () => {
|
||||
});
|
||||
|
||||
const result = await SegmentService.getContacts(projectId, segment.id);
|
||||
expect(result.contacts).toHaveLength(0);
|
||||
expect(result.data).toHaveLength(0);
|
||||
});
|
||||
|
||||
it('should work with negative numbers', async () => {
|
||||
@@ -379,8 +379,8 @@ describe('SegmentService - Comprehensive Operator Tests', () => {
|
||||
});
|
||||
|
||||
const result = await SegmentService.getContacts(projectId, segment.id);
|
||||
expect(result.contacts).toHaveLength(1);
|
||||
expect(result.contacts[0].id).toBe(match.id);
|
||||
expect(result.data).toHaveLength(1);
|
||||
expect(result.data[0].id).toBe(match.id);
|
||||
});
|
||||
|
||||
it('should work with decimal values', async () => {
|
||||
@@ -398,8 +398,8 @@ describe('SegmentService - Comprehensive Operator Tests', () => {
|
||||
});
|
||||
|
||||
const result = await SegmentService.getContacts(projectId, segment.id);
|
||||
expect(result.contacts).toHaveLength(1);
|
||||
expect(result.contacts[0].id).toBe(match.id);
|
||||
expect(result.data).toHaveLength(1);
|
||||
expect(result.data[0].id).toBe(match.id);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -423,10 +423,10 @@ describe('SegmentService - Comprehensive Operator Tests', () => {
|
||||
});
|
||||
|
||||
const result = await SegmentService.getContacts(projectId, segment.id);
|
||||
const ids = result.contacts.map(c => c.id);
|
||||
const ids = result.data.map(c => c.id);
|
||||
expect(ids).toContain(equal.id);
|
||||
expect(ids).toContain(greater.id);
|
||||
expect(result.contacts).toHaveLength(2);
|
||||
expect(result.data).toHaveLength(2);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -450,10 +450,10 @@ describe('SegmentService - Comprehensive Operator Tests', () => {
|
||||
});
|
||||
|
||||
const result = await SegmentService.getContacts(projectId, segment.id);
|
||||
const ids = result.contacts.map(c => c.id);
|
||||
const ids = result.data.map(c => c.id);
|
||||
expect(ids).toContain(low.id);
|
||||
expect(ids).toContain(veryLow.id);
|
||||
expect(result.contacts).toHaveLength(2);
|
||||
expect(result.data).toHaveLength(2);
|
||||
});
|
||||
|
||||
it('should exclude values equal to threshold', async () => {
|
||||
@@ -467,7 +467,7 @@ describe('SegmentService - Comprehensive Operator Tests', () => {
|
||||
});
|
||||
|
||||
const result = await SegmentService.getContacts(projectId, segment.id);
|
||||
expect(result.contacts).toHaveLength(0);
|
||||
expect(result.data).toHaveLength(0);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -491,10 +491,10 @@ describe('SegmentService - Comprehensive Operator Tests', () => {
|
||||
});
|
||||
|
||||
const result = await SegmentService.getContacts(projectId, segment.id);
|
||||
const ids = result.contacts.map(c => c.id);
|
||||
const ids = result.data.map(c => c.id);
|
||||
expect(ids).toContain(equal.id);
|
||||
expect(ids).toContain(less.id);
|
||||
expect(result.contacts).toHaveLength(2);
|
||||
expect(result.data).toHaveLength(2);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -515,8 +515,8 @@ describe('SegmentService - Comprehensive Operator Tests', () => {
|
||||
});
|
||||
|
||||
const result = await SegmentService.getContacts(projectId, segment.id);
|
||||
expect(result.contacts).toHaveLength(1);
|
||||
expect(result.contacts[0].id).toBe(positive.id);
|
||||
expect(result.data).toHaveLength(1);
|
||||
expect(result.data[0].id).toBe(positive.id);
|
||||
});
|
||||
|
||||
it('should handle very large numbers', async () => {
|
||||
@@ -534,8 +534,8 @@ describe('SegmentService - Comprehensive Operator Tests', () => {
|
||||
});
|
||||
|
||||
const result = await SegmentService.getContacts(projectId, segment.id);
|
||||
expect(result.contacts).toHaveLength(1);
|
||||
expect(result.contacts[0].id).toBe(match.id);
|
||||
expect(result.data).toHaveLength(1);
|
||||
expect(result.data[0].id).toBe(match.id);
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -560,8 +560,8 @@ describe('SegmentService - Comprehensive Operator Tests', () => {
|
||||
});
|
||||
|
||||
const result = await SegmentService.getContacts(projectId, segment.id);
|
||||
expect(result.contacts).toHaveLength(1);
|
||||
expect(result.contacts[0].id).toBe(withField.id);
|
||||
expect(result.data).toHaveLength(1);
|
||||
expect(result.data[0].id).toBe(withField.id);
|
||||
});
|
||||
|
||||
it('should exclude contacts where field is null', async () => {
|
||||
@@ -579,8 +579,8 @@ describe('SegmentService - Comprehensive Operator Tests', () => {
|
||||
});
|
||||
|
||||
const result = await SegmentService.getContacts(projectId, segment.id);
|
||||
expect(result.contacts).toHaveLength(1);
|
||||
expect(result.contacts[0].id).toBe(withValue.id);
|
||||
expect(result.data).toHaveLength(1);
|
||||
expect(result.data[0].id).toBe(withValue.id);
|
||||
});
|
||||
|
||||
it('should match fields with empty string values', async () => {
|
||||
@@ -594,8 +594,8 @@ describe('SegmentService - Comprehensive Operator Tests', () => {
|
||||
});
|
||||
|
||||
const result = await SegmentService.getContacts(projectId, segment.id);
|
||||
expect(result.contacts).toHaveLength(1);
|
||||
expect(result.contacts[0].id).toBe(withEmptyString.id);
|
||||
expect(result.data).toHaveLength(1);
|
||||
expect(result.data[0].id).toBe(withEmptyString.id);
|
||||
});
|
||||
|
||||
it('should match fields with zero values', async () => {
|
||||
@@ -609,8 +609,8 @@ describe('SegmentService - Comprehensive Operator Tests', () => {
|
||||
});
|
||||
|
||||
const result = await SegmentService.getContacts(projectId, segment.id);
|
||||
expect(result.contacts).toHaveLength(1);
|
||||
expect(result.contacts[0].id).toBe(withZero.id);
|
||||
expect(result.data).toHaveLength(1);
|
||||
expect(result.data[0].id).toBe(withZero.id);
|
||||
});
|
||||
|
||||
it('should match fields with boolean false values', async () => {
|
||||
@@ -624,8 +624,8 @@ describe('SegmentService - Comprehensive Operator Tests', () => {
|
||||
});
|
||||
|
||||
const result = await SegmentService.getContacts(projectId, segment.id);
|
||||
expect(result.contacts).toHaveLength(1);
|
||||
expect(result.contacts[0].id).toBe(withFalse.id);
|
||||
expect(result.data).toHaveLength(1);
|
||||
expect(result.data[0].id).toBe(withFalse.id);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -645,8 +645,8 @@ describe('SegmentService - Comprehensive Operator Tests', () => {
|
||||
});
|
||||
|
||||
const result = await SegmentService.getContacts(projectId, segment.id);
|
||||
expect(result.contacts).toHaveLength(1);
|
||||
expect(result.contacts[0].id).toBe(withoutField.id);
|
||||
expect(result.data).toHaveLength(1);
|
||||
expect(result.data[0].id).toBe(withoutField.id);
|
||||
});
|
||||
|
||||
it('should match contacts where field is null', async () => {
|
||||
@@ -664,8 +664,8 @@ describe('SegmentService - Comprehensive Operator Tests', () => {
|
||||
});
|
||||
|
||||
const result = await SegmentService.getContacts(projectId, segment.id);
|
||||
expect(result.contacts).toHaveLength(1);
|
||||
expect(result.contacts[0].id).toBe(withNull.id);
|
||||
expect(result.data).toHaveLength(1);
|
||||
expect(result.data[0].id).toBe(withNull.id);
|
||||
});
|
||||
|
||||
it('should exclude fields with empty string values', async () => {
|
||||
@@ -679,7 +679,7 @@ describe('SegmentService - Comprehensive Operator Tests', () => {
|
||||
});
|
||||
|
||||
const result = await SegmentService.getContacts(projectId, segment.id);
|
||||
expect(result.contacts).toHaveLength(0);
|
||||
expect(result.data).toHaveLength(0);
|
||||
});
|
||||
|
||||
it('should exclude fields with zero values', async () => {
|
||||
@@ -693,7 +693,7 @@ describe('SegmentService - Comprehensive Operator Tests', () => {
|
||||
});
|
||||
|
||||
const result = await SegmentService.getContacts(projectId, segment.id);
|
||||
expect(result.contacts).toHaveLength(0);
|
||||
expect(result.data).toHaveLength(0);
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -718,7 +718,7 @@ describe('SegmentService - Comprehensive Operator Tests', () => {
|
||||
});
|
||||
|
||||
const result = await SegmentService.getContacts(projectId, segment.id);
|
||||
const ids = result.contacts.map(c => c.id);
|
||||
const ids = result.data.map(c => c.id);
|
||||
expect(ids).toContain(recent.id);
|
||||
});
|
||||
|
||||
@@ -737,7 +737,7 @@ describe('SegmentService - Comprehensive Operator Tests', () => {
|
||||
});
|
||||
|
||||
const result = await SegmentService.getContacts(projectId, segment.id);
|
||||
const ids = result.contacts.map(c => c.id);
|
||||
const ids = result.data.map(c => c.id);
|
||||
expect(ids).toContain(veryRecent.id);
|
||||
});
|
||||
|
||||
@@ -756,7 +756,7 @@ describe('SegmentService - Comprehensive Operator Tests', () => {
|
||||
});
|
||||
|
||||
const result = await SegmentService.getContacts(projectId, segment.id);
|
||||
const ids = result.contacts.map(c => c.id);
|
||||
const ids = result.data.map(c => c.id);
|
||||
expect(ids).toContain(justNow.id);
|
||||
});
|
||||
});
|
||||
@@ -794,8 +794,8 @@ describe('SegmentService - Comprehensive Operator Tests', () => {
|
||||
});
|
||||
|
||||
const result = await SegmentService.getContacts(projectId, segment.id);
|
||||
expect(result.contacts).toHaveLength(1);
|
||||
expect(result.contacts[0].id).toBe(match.id);
|
||||
expect(result.data).toHaveLength(1);
|
||||
expect(result.data[0].id).toBe(match.id);
|
||||
});
|
||||
|
||||
it('should match contacts with JSON date field within specified hours', async () => {
|
||||
@@ -819,8 +819,8 @@ describe('SegmentService - Comprehensive Operator Tests', () => {
|
||||
});
|
||||
|
||||
const result = await SegmentService.getContacts(projectId, segment.id);
|
||||
expect(result.contacts).toHaveLength(1);
|
||||
expect(result.contacts[0].id).toBe(match.id);
|
||||
expect(result.data).toHaveLength(1);
|
||||
expect(result.data[0].id).toBe(match.id);
|
||||
});
|
||||
|
||||
it('should match contacts with JSON date field within specified minutes', async () => {
|
||||
@@ -844,8 +844,8 @@ describe('SegmentService - Comprehensive Operator Tests', () => {
|
||||
});
|
||||
|
||||
const result = await SegmentService.getContacts(projectId, segment.id);
|
||||
expect(result.contacts).toHaveLength(1);
|
||||
expect(result.contacts[0].id).toBe(match.id);
|
||||
expect(result.data).toHaveLength(1);
|
||||
expect(result.data[0].id).toBe(match.id);
|
||||
});
|
||||
|
||||
it('should NOT match contacts with JSON date field outside the time range', async () => {
|
||||
@@ -870,7 +870,7 @@ describe('SegmentService - Comprehensive Operator Tests', () => {
|
||||
});
|
||||
|
||||
const result = await SegmentService.getContacts(projectId, segment.id);
|
||||
expect(result.contacts).toHaveLength(0);
|
||||
expect(result.data).toHaveLength(0);
|
||||
});
|
||||
|
||||
it('should NOT match contacts where JSON date field does not exist', async () => {
|
||||
@@ -893,7 +893,7 @@ describe('SegmentService - Comprehensive Operator Tests', () => {
|
||||
});
|
||||
|
||||
const result = await SegmentService.getContacts(projectId, segment.id);
|
||||
expect(result.contacts).toHaveLength(0);
|
||||
expect(result.data).toHaveLength(0);
|
||||
});
|
||||
|
||||
it('should handle null values in JSON date fields gracefully', async () => {
|
||||
@@ -916,7 +916,7 @@ describe('SegmentService - Comprehensive Operator Tests', () => {
|
||||
});
|
||||
|
||||
const result = await SegmentService.getContacts(projectId, segment.id);
|
||||
expect(result.contacts).toHaveLength(0);
|
||||
expect(result.data).toHaveLength(0);
|
||||
});
|
||||
|
||||
it('should work correctly with combined filters (AND logic)', async () => {
|
||||
@@ -953,8 +953,8 @@ describe('SegmentService - Comprehensive Operator Tests', () => {
|
||||
});
|
||||
|
||||
const result = await SegmentService.getContacts(projectId, segment.id);
|
||||
expect(result.contacts).toHaveLength(1);
|
||||
expect(result.contacts[0].id).toBe(match.id);
|
||||
expect(result.data).toHaveLength(1);
|
||||
expect(result.data[0].id).toBe(match.id);
|
||||
});
|
||||
|
||||
it('should require unit parameter for within operator', async () => {
|
||||
@@ -1002,7 +1002,7 @@ describe('SegmentService - Comprehensive Operator Tests', () => {
|
||||
});
|
||||
|
||||
const result = await SegmentService.getContacts(projectId, segment.id);
|
||||
const ids = result.contacts.map(c => c.id);
|
||||
const ids = result.data.map(c => c.id);
|
||||
expect(ids).toContain(newer.id);
|
||||
expect(ids).not.toContain(older.id);
|
||||
});
|
||||
@@ -1025,7 +1025,7 @@ describe('SegmentService - Comprehensive Operator Tests', () => {
|
||||
});
|
||||
|
||||
const result = await SegmentService.getContacts(projectId, segment.id);
|
||||
const ids = result.contacts.map(c => c.id);
|
||||
const ids = result.data.map(c => c.id);
|
||||
expect(ids).toContain(first.id);
|
||||
expect(ids).toContain(second.id);
|
||||
expect(ids).not.toContain(third.id);
|
||||
@@ -1069,7 +1069,7 @@ describe('SegmentService - Comprehensive Operator Tests', () => {
|
||||
});
|
||||
|
||||
const result = await SegmentService.getContacts(projectId, segment.id);
|
||||
const ids = result.contacts.map(c => c.id);
|
||||
const ids = result.data.map(c => c.id);
|
||||
expect(ids).toContain(match1.id);
|
||||
expect(ids).toContain(match2.id);
|
||||
expect(ids).not.toContain(noMatch.id);
|
||||
@@ -1107,7 +1107,7 @@ describe('SegmentService - Comprehensive Operator Tests', () => {
|
||||
});
|
||||
|
||||
const result = await SegmentService.getContacts(projectId, segment.id);
|
||||
const ids = result.contacts.map(c => c.id);
|
||||
const ids = result.data.map(c => c.id);
|
||||
expect(ids).toContain(match1.id);
|
||||
expect(ids).toContain(match2.id);
|
||||
expect(ids).not.toContain(noMatch.id);
|
||||
@@ -1133,7 +1133,7 @@ describe('SegmentService - Comprehensive Operator Tests', () => {
|
||||
});
|
||||
|
||||
const result = await SegmentService.getContacts(projectId, segment.id);
|
||||
const ids = result.contacts.map(c => c.id);
|
||||
const ids = result.data.map(c => c.id);
|
||||
expect(ids).toContain(match.id);
|
||||
});
|
||||
|
||||
@@ -1162,8 +1162,8 @@ describe('SegmentService - Comprehensive Operator Tests', () => {
|
||||
});
|
||||
|
||||
const result = await SegmentService.getContacts(projectId, segment.id);
|
||||
expect(result.contacts).toHaveLength(1);
|
||||
expect(result.contacts[0].id).toBe(match.id);
|
||||
expect(result.data).toHaveLength(1);
|
||||
expect(result.data[0].id).toBe(match.id);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -1205,7 +1205,7 @@ describe('SegmentService - Comprehensive Operator Tests', () => {
|
||||
});
|
||||
|
||||
const result = await SegmentService.getContacts(projectId, segment.id);
|
||||
const ids = result.contacts.map(c => c.id);
|
||||
const ids = result.data.map(c => c.id);
|
||||
expect(ids).toContain(match1.id);
|
||||
expect(ids).toContain(match2.id);
|
||||
expect(ids).not.toContain(noMatch.id);
|
||||
@@ -1247,7 +1247,7 @@ describe('SegmentService - Comprehensive Operator Tests', () => {
|
||||
});
|
||||
|
||||
const result = await SegmentService.getContacts(projectId, segment.id);
|
||||
const ids = result.contacts.map(c => c.id);
|
||||
const ids = result.data.map(c => c.id);
|
||||
expect(ids).toContain(match1.id);
|
||||
expect(ids).toContain(match2.id);
|
||||
expect(ids).not.toContain(noMatch.id);
|
||||
@@ -1298,8 +1298,8 @@ describe('SegmentService - Comprehensive Operator Tests', () => {
|
||||
});
|
||||
|
||||
const result = await SegmentService.getContacts(projectId, segment.id);
|
||||
expect(result.contacts).toHaveLength(1);
|
||||
expect(result.contacts[0].id).toBe(match.id);
|
||||
expect(result.data).toHaveLength(1);
|
||||
expect(result.data[0].id).toBe(match.id);
|
||||
});
|
||||
|
||||
it('should combine existence checks with value comparisons', async () => {
|
||||
@@ -1329,8 +1329,8 @@ describe('SegmentService - Comprehensive Operator Tests', () => {
|
||||
});
|
||||
|
||||
const result = await SegmentService.getContacts(projectId, segment.id);
|
||||
expect(result.contacts).toHaveLength(1);
|
||||
expect(result.contacts[0].id).toBe(match.id);
|
||||
expect(result.data).toHaveLength(1);
|
||||
expect(result.data[0].id).toBe(match.id);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -1357,8 +1357,8 @@ describe('SegmentService - Comprehensive Operator Tests', () => {
|
||||
});
|
||||
|
||||
const result = await SegmentService.getContacts(projectId, segment.id);
|
||||
expect(result.contacts).toHaveLength(1);
|
||||
expect(result.contacts[0].id).toBe(match.id);
|
||||
expect(result.data).toHaveLength(1);
|
||||
expect(result.data[0].id).toBe(match.id);
|
||||
});
|
||||
|
||||
it('should not match contacts who have not triggered the event', async () => {
|
||||
@@ -1379,7 +1379,7 @@ describe('SegmentService - Comprehensive Operator Tests', () => {
|
||||
});
|
||||
|
||||
const result = await SegmentService.getContacts(projectId, segment.id);
|
||||
expect(result.contacts).toHaveLength(0);
|
||||
expect(result.data).toHaveLength(0);
|
||||
});
|
||||
|
||||
it('should match contacts with multiple occurrences of the same event', async () => {
|
||||
@@ -1400,8 +1400,8 @@ describe('SegmentService - Comprehensive Operator Tests', () => {
|
||||
});
|
||||
|
||||
const result = await SegmentService.getContacts(projectId, segment.id);
|
||||
expect(result.contacts).toHaveLength(1);
|
||||
expect(result.contacts[0].id).toBe(match.id);
|
||||
expect(result.data).toHaveLength(1);
|
||||
expect(result.data[0].id).toBe(match.id);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -1440,8 +1440,8 @@ describe('SegmentService - Comprehensive Operator Tests', () => {
|
||||
});
|
||||
|
||||
const result = await SegmentService.getContacts(projectId, segment.id);
|
||||
expect(result.contacts).toHaveLength(1);
|
||||
expect(result.contacts[0].id).toBe(match.id);
|
||||
expect(result.data).toHaveLength(1);
|
||||
expect(result.data[0].id).toBe(match.id);
|
||||
});
|
||||
|
||||
it('should match contacts with event within time range (hours)', async () => {
|
||||
@@ -1476,8 +1476,8 @@ describe('SegmentService - Comprehensive Operator Tests', () => {
|
||||
});
|
||||
|
||||
const result = await SegmentService.getContacts(projectId, segment.id);
|
||||
expect(result.contacts).toHaveLength(1);
|
||||
expect(result.contacts[0].id).toBe(match.id);
|
||||
expect(result.data).toHaveLength(1);
|
||||
expect(result.data[0].id).toBe(match.id);
|
||||
});
|
||||
|
||||
it('should match contacts with event within time range (minutes)', async () => {
|
||||
@@ -1512,8 +1512,8 @@ describe('SegmentService - Comprehensive Operator Tests', () => {
|
||||
});
|
||||
|
||||
const result = await SegmentService.getContacts(projectId, segment.id);
|
||||
expect(result.contacts).toHaveLength(1);
|
||||
expect(result.contacts[0].id).toBe(match.id);
|
||||
expect(result.data).toHaveLength(1);
|
||||
expect(result.data[0].id).toBe(match.id);
|
||||
});
|
||||
|
||||
it('should handle events at exact boundary', async () => {
|
||||
@@ -1538,7 +1538,7 @@ describe('SegmentService - Comprehensive Operator Tests', () => {
|
||||
|
||||
const result = await SegmentService.getContacts(projectId, segment.id);
|
||||
// Should not match because events at exact boundary are excluded
|
||||
expect(result.contacts).toHaveLength(0);
|
||||
expect(result.data).toHaveLength(0);
|
||||
});
|
||||
|
||||
it('should not match contacts with event outside time range', async () => {
|
||||
@@ -1562,7 +1562,7 @@ describe('SegmentService - Comprehensive Operator Tests', () => {
|
||||
});
|
||||
|
||||
const result = await SegmentService.getContacts(projectId, segment.id);
|
||||
expect(result.contacts).toHaveLength(0);
|
||||
expect(result.data).toHaveLength(0);
|
||||
});
|
||||
|
||||
it('should match contact if any of their events is within range', async () => {
|
||||
@@ -1598,8 +1598,8 @@ describe('SegmentService - Comprehensive Operator Tests', () => {
|
||||
});
|
||||
|
||||
const result = await SegmentService.getContacts(projectId, segment.id);
|
||||
expect(result.contacts).toHaveLength(1);
|
||||
expect(result.contacts[0].id).toBe(match.id);
|
||||
expect(result.data).toHaveLength(1);
|
||||
expect(result.data[0].id).toBe(match.id);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -1623,8 +1623,8 @@ describe('SegmentService - Comprehensive Operator Tests', () => {
|
||||
});
|
||||
|
||||
const result = await SegmentService.getContacts(projectId, segment.id);
|
||||
expect(result.contacts).toHaveLength(1);
|
||||
expect(result.contacts[0].id).toBe(match.id);
|
||||
expect(result.data).toHaveLength(1);
|
||||
expect(result.data[0].id).toBe(match.id);
|
||||
});
|
||||
|
||||
it('should not match contacts who have triggered the event', async () => {
|
||||
@@ -1644,7 +1644,7 @@ describe('SegmentService - Comprehensive Operator Tests', () => {
|
||||
});
|
||||
|
||||
const result = await SegmentService.getContacts(projectId, segment.id);
|
||||
expect(result.contacts).toHaveLength(0);
|
||||
expect(result.data).toHaveLength(0);
|
||||
});
|
||||
|
||||
it('should match contacts with other events but not the target event', async () => {
|
||||
@@ -1664,8 +1664,8 @@ describe('SegmentService - Comprehensive Operator Tests', () => {
|
||||
});
|
||||
|
||||
const result = await SegmentService.getContacts(projectId, segment.id);
|
||||
expect(result.contacts).toHaveLength(1);
|
||||
expect(result.contacts[0].id).toBe(match.id);
|
||||
expect(result.data).toHaveLength(1);
|
||||
expect(result.data[0].id).toBe(match.id);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -1697,8 +1697,8 @@ describe('SegmentService - Comprehensive Operator Tests', () => {
|
||||
});
|
||||
|
||||
const result = await SegmentService.getContacts(projectId, segment.id);
|
||||
expect(result.contacts).toHaveLength(1);
|
||||
expect(result.contacts[0].id).toBe(match.id);
|
||||
expect(result.data).toHaveLength(1);
|
||||
expect(result.data[0].id).toBe(match.id);
|
||||
});
|
||||
|
||||
it('should combine multiple event filters', async () => {
|
||||
@@ -1727,8 +1727,8 @@ describe('SegmentService - Comprehensive Operator Tests', () => {
|
||||
});
|
||||
|
||||
const result = await SegmentService.getContacts(projectId, segment.id);
|
||||
expect(result.contacts).toHaveLength(1);
|
||||
expect(result.contacts[0].id).toBe(match.id);
|
||||
expect(result.data).toHaveLength(1);
|
||||
expect(result.data[0].id).toBe(match.id);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -32,8 +32,8 @@ describe('SegmentService', () => {
|
||||
|
||||
const result = await SegmentService.getContacts(projectId, segment.id);
|
||||
|
||||
expect(result.contacts).toHaveLength(1);
|
||||
expect(result.contacts[0].id).toBe(subscribed.id);
|
||||
expect(result.data).toHaveLength(1);
|
||||
expect(result.data[0].id).toBe(subscribed.id);
|
||||
});
|
||||
|
||||
it('should filter contacts by custom data fields', async () => {
|
||||
@@ -53,8 +53,8 @@ describe('SegmentService', () => {
|
||||
|
||||
const result = await SegmentService.getContacts(projectId, segment.id);
|
||||
|
||||
expect(result.contacts).toHaveLength(1);
|
||||
expect(result.contacts[0].id).toBe(proUser.id);
|
||||
expect(result.data).toHaveLength(1);
|
||||
expect(result.data[0].id).toBe(proUser.id);
|
||||
});
|
||||
|
||||
it('should filter contacts with multiple conditions', async () => {
|
||||
@@ -85,8 +85,8 @@ describe('SegmentService', () => {
|
||||
|
||||
const result = await SegmentService.getContacts(projectId, segment.id);
|
||||
|
||||
expect(result.contacts).toHaveLength(1);
|
||||
expect(result.contacts[0].id).toBe(target.id);
|
||||
expect(result.data).toHaveLength(1);
|
||||
expect(result.data[0].id).toBe(target.id);
|
||||
});
|
||||
|
||||
it('should support notEquals operator', async () => {
|
||||
@@ -106,8 +106,8 @@ describe('SegmentService', () => {
|
||||
|
||||
const result = await SegmentService.getContacts(projectId, segment.id);
|
||||
|
||||
expect(result.contacts).toHaveLength(1);
|
||||
expect(result.contacts[0].id).toBe(pro.id);
|
||||
expect(result.data).toHaveLength(1);
|
||||
expect(result.data[0].id).toBe(pro.id);
|
||||
});
|
||||
|
||||
it('should support contains operator for strings', async () => {
|
||||
@@ -127,8 +127,8 @@ describe('SegmentService', () => {
|
||||
|
||||
const result = await SegmentService.getContacts(projectId, segment.id);
|
||||
|
||||
expect(result.contacts).toHaveLength(1);
|
||||
expect(result.contacts[0].id).toBe(match.id);
|
||||
expect(result.data).toHaveLength(1);
|
||||
expect(result.data[0].id).toBe(match.id);
|
||||
});
|
||||
|
||||
it('should support exists operator for custom fields', async () => {
|
||||
@@ -148,8 +148,8 @@ describe('SegmentService', () => {
|
||||
|
||||
const result = await SegmentService.getContacts(projectId, segment.id);
|
||||
|
||||
expect(result.contacts).toHaveLength(1);
|
||||
expect(result.contacts[0].id).toBe(withField.id);
|
||||
expect(result.data).toHaveLength(1);
|
||||
expect(result.data[0].id).toBe(withField.id);
|
||||
});
|
||||
|
||||
it('should handle empty segments', async () => {
|
||||
@@ -165,7 +165,7 @@ describe('SegmentService', () => {
|
||||
|
||||
const result = await SegmentService.getContacts(projectId, segment.id);
|
||||
|
||||
expect(result.contacts).toHaveLength(0);
|
||||
expect(result.data).toHaveLength(0);
|
||||
expect(result.total).toBe(0);
|
||||
});
|
||||
});
|
||||
@@ -192,7 +192,7 @@ describe('SegmentService', () => {
|
||||
const result = await SegmentService.getContacts(projectId, segment.id);
|
||||
|
||||
expect(result.total).toBe(2);
|
||||
expect(result.contacts).toHaveLength(2);
|
||||
expect(result.data).toHaveLength(2);
|
||||
});
|
||||
|
||||
it('should support pagination', async () => {
|
||||
@@ -209,15 +209,15 @@ describe('SegmentService', () => {
|
||||
});
|
||||
|
||||
const page1 = await SegmentService.getContacts(projectId, segment.id, 1, 10);
|
||||
expect(page1.contacts).toHaveLength(10);
|
||||
expect(page1.data).toHaveLength(10);
|
||||
expect(page1.total).toBe(25);
|
||||
expect(page1.totalPages).toBe(3);
|
||||
|
||||
const page2 = await SegmentService.getContacts(projectId, segment.id, 2, 10);
|
||||
expect(page2.contacts).toHaveLength(10);
|
||||
expect(page2.data).toHaveLength(10);
|
||||
|
||||
const page3 = await SegmentService.getContacts(projectId, segment.id, 3, 10);
|
||||
expect(page3.contacts).toHaveLength(5);
|
||||
expect(page3.data).toHaveLength(5);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -278,7 +278,7 @@ describe('SegmentService', () => {
|
||||
|
||||
// Initially not in segment
|
||||
let result = await SegmentService.getContacts(projectId, proSegment.id);
|
||||
expect(result.contacts).toHaveLength(0);
|
||||
expect(result.data).toHaveLength(0);
|
||||
|
||||
// Update contact to pro plan
|
||||
await prisma.contact.update({
|
||||
@@ -288,8 +288,8 @@ describe('SegmentService', () => {
|
||||
|
||||
// Should now be in segment
|
||||
result = await SegmentService.getContacts(projectId, proSegment.id);
|
||||
expect(result.contacts).toHaveLength(1);
|
||||
expect(result.contacts[0].id).toBe(contact.id);
|
||||
expect(result.data).toHaveLength(1);
|
||||
expect(result.data[0].id).toBe(contact.id);
|
||||
});
|
||||
|
||||
it('should be removed from segment when criteria no longer met', async () => {
|
||||
@@ -304,7 +304,7 @@ describe('SegmentService', () => {
|
||||
|
||||
// Initially in segment
|
||||
let result = await SegmentService.getContacts(projectId, segment.id);
|
||||
expect(result.contacts).toHaveLength(1);
|
||||
expect(result.data).toHaveLength(1);
|
||||
|
||||
// Unsubscribe contact
|
||||
await prisma.contact.update({
|
||||
@@ -314,7 +314,7 @@ describe('SegmentService', () => {
|
||||
|
||||
// Should no longer be in segment
|
||||
result = await SegmentService.getContacts(projectId, segment.id);
|
||||
expect(result.contacts).toHaveLength(0);
|
||||
expect(result.data).toHaveLength(0);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -506,8 +506,8 @@ describe('SegmentService', () => {
|
||||
|
||||
const result = await SegmentService.getContacts(projectId, segment.id);
|
||||
|
||||
expect(result.contacts.map(c => c.id).sort()).toEqual([other.id].sort());
|
||||
expect(result.contacts.map(c => c.id)).not.toContain(match.id);
|
||||
expect(result.data.map(c => c.id).sort()).toEqual([other.id].sort());
|
||||
expect(result.data.map(c => c.id)).not.toContain(match.id);
|
||||
});
|
||||
|
||||
it('should support case-insensitive equals/contains for email strings', async () => {
|
||||
@@ -526,7 +526,7 @@ describe('SegmentService', () => {
|
||||
});
|
||||
|
||||
const equalsResult = await SegmentService.getContacts(projectId, equalsSegment.id);
|
||||
const equalsIds = equalsResult.contacts.map(c => c.id);
|
||||
const equalsIds = equalsResult.data.map(c => c.id);
|
||||
expect(equalsIds).toContain(lower.id);
|
||||
expect(equalsIds).toContain(upper.id);
|
||||
|
||||
@@ -536,7 +536,7 @@ describe('SegmentService', () => {
|
||||
});
|
||||
|
||||
const containsResult = await SegmentService.getContacts(projectId, containsSegment.id);
|
||||
const containsIds = containsResult.contacts.map(c => c.id);
|
||||
const containsIds = containsResult.data.map(c => c.id);
|
||||
expect(containsIds).toContain(lower.id);
|
||||
expect(containsIds).toContain(upper.id);
|
||||
});
|
||||
@@ -557,7 +557,7 @@ describe('SegmentService', () => {
|
||||
});
|
||||
|
||||
const result = await SegmentService.getContacts(projectId, segment.id);
|
||||
const ids = result.contacts.map(c => c.id);
|
||||
const ids = result.data.map(c => c.id);
|
||||
|
||||
expect(ids).toContain(unsubscribed.id);
|
||||
expect(ids).not.toContain(subscribed.id);
|
||||
@@ -579,7 +579,7 @@ describe('SegmentService', () => {
|
||||
});
|
||||
|
||||
const notContainsResult = await SegmentService.getContacts(projectId, notContainsSegment.id);
|
||||
const notContainsIds = notContainsResult.contacts.map(c => c.id);
|
||||
const notContainsIds = notContainsResult.data.map(c => c.id);
|
||||
expect(notContainsIds).toContain(other.id);
|
||||
expect(notContainsIds).not.toContain(acme.id);
|
||||
|
||||
@@ -589,7 +589,7 @@ describe('SegmentService', () => {
|
||||
});
|
||||
|
||||
const notEqualsResult = await SegmentService.getContacts(projectId, notEqualsSegment.id);
|
||||
const notEqualsIds = notEqualsResult.contacts.map(c => c.id);
|
||||
const notEqualsIds = notEqualsResult.data.map(c => c.id);
|
||||
expect(notEqualsIds).toContain(other.id);
|
||||
expect(notEqualsIds).not.toContain(acme.id);
|
||||
});
|
||||
@@ -610,7 +610,7 @@ describe('SegmentService', () => {
|
||||
});
|
||||
|
||||
const existsResult = await SegmentService.getContacts(projectId, existsSegment.id);
|
||||
const existsIds = new Set(existsResult.contacts.map(c => c.id));
|
||||
const existsIds = new Set(existsResult.data.map(c => c.id));
|
||||
expect(existsIds.has(withCompany.id)).toBe(true);
|
||||
expect(existsIds.has(withNullCompany.id)).toBe(false);
|
||||
|
||||
@@ -620,7 +620,7 @@ describe('SegmentService', () => {
|
||||
});
|
||||
|
||||
const notExistsResult = await SegmentService.getContacts(projectId, notExistsSegment.id);
|
||||
const notExistsIds = new Set(notExistsResult.contacts.map(c => c.id));
|
||||
const notExistsIds = new Set(notExistsResult.data.map(c => c.id));
|
||||
expect(notExistsIds.has(withCompany.id)).toBe(false);
|
||||
expect(notExistsIds.has(withNullCompany.id)).toBe(true);
|
||||
});
|
||||
@@ -645,7 +645,7 @@ describe('SegmentService', () => {
|
||||
});
|
||||
|
||||
const greaterThanResult = await SegmentService.getContacts(projectId, greaterThanSegment.id);
|
||||
const gtIds = greaterThanResult.contacts.map(c => c.id);
|
||||
const gtIds = greaterThanResult.data.map(c => c.id);
|
||||
expect(gtIds).toContain(mid.id);
|
||||
expect(gtIds).toContain(high.id);
|
||||
expect(gtIds).not.toContain(low.id);
|
||||
@@ -656,7 +656,7 @@ describe('SegmentService', () => {
|
||||
});
|
||||
|
||||
const lteResult = await SegmentService.getContacts(projectId, lessThanOrEqualSegment.id);
|
||||
const lteIds = lteResult.contacts.map(c => c.id);
|
||||
const lteIds = lteResult.data.map(c => c.id);
|
||||
expect(lteIds).toContain(low.id);
|
||||
expect(lteIds).toContain(mid.id);
|
||||
expect(lteIds).not.toContain(high.id);
|
||||
@@ -674,7 +674,7 @@ describe('SegmentService', () => {
|
||||
});
|
||||
|
||||
const gtResult = await SegmentService.getContacts(projectId, gtSegment.id);
|
||||
const gtIds = gtResult.contacts.map(c => c.id);
|
||||
const gtIds = gtResult.data.map(c => c.id);
|
||||
expect(gtIds).toContain(newer.id);
|
||||
expect(gtIds).not.toContain(older.id);
|
||||
|
||||
@@ -684,7 +684,7 @@ describe('SegmentService', () => {
|
||||
});
|
||||
|
||||
const lteResult = await SegmentService.getContacts(projectId, lteSegment.id);
|
||||
const lteIds = lteResult.contacts.map(c => c.id);
|
||||
const lteIds = lteResult.data.map(c => c.id);
|
||||
expect(lteIds).toContain(older.id);
|
||||
expect(lteIds).toContain(newer.id);
|
||||
});
|
||||
@@ -705,7 +705,7 @@ describe('SegmentService', () => {
|
||||
});
|
||||
|
||||
const result = await SegmentService.getContacts(projectId, segment.id);
|
||||
const ids = result.contacts.map(c => c.id);
|
||||
const ids = result.data.map(c => c.id);
|
||||
expect(ids).toContain(recent.id);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import {describe, it, expect, beforeEach} from 'vitest';
|
||||
import {beforeEach, describe, expect, it} from 'vitest';
|
||||
import {TemplateType} from '@plunk/db';
|
||||
import {TemplateService} from '../TemplateService';
|
||||
import {factories, getPrismaClient} from '../../../../../test/helpers';
|
||||
@@ -104,18 +104,18 @@ describe('TemplateService', () => {
|
||||
}
|
||||
|
||||
const page1 = await TemplateService.list(projectId, 1, 10);
|
||||
expect(page1.templates).toHaveLength(10);
|
||||
expect(page1.data).toHaveLength(10);
|
||||
expect(page1.total).toBe(25);
|
||||
expect(page1.page).toBe(1);
|
||||
expect(page1.pageSize).toBe(10);
|
||||
expect(page1.totalPages).toBe(3);
|
||||
|
||||
const page2 = await TemplateService.list(projectId, 2, 10);
|
||||
expect(page2.templates).toHaveLength(10);
|
||||
expect(page2.data).toHaveLength(10);
|
||||
expect(page2.page).toBe(2);
|
||||
|
||||
const page3 = await TemplateService.list(projectId, 3, 10);
|
||||
expect(page3.templates).toHaveLength(5);
|
||||
expect(page3.data).toHaveLength(5);
|
||||
expect(page3.page).toBe(3);
|
||||
});
|
||||
|
||||
@@ -127,7 +127,7 @@ describe('TemplateService', () => {
|
||||
const result = await TemplateService.list(projectId, 1, 20, 'welcome');
|
||||
|
||||
expect(result.total).toBe(2);
|
||||
expect(result.templates.every(t => t.name.toLowerCase().includes('welcome'))).toBe(true);
|
||||
expect(result.data.every(t => t.name.toLowerCase().includes('welcome'))).toBe(true);
|
||||
});
|
||||
|
||||
it('should filter templates by search query (description)', async () => {
|
||||
@@ -165,7 +165,7 @@ describe('TemplateService', () => {
|
||||
const result = await TemplateService.list(projectId, 1, 20, 'new');
|
||||
|
||||
expect(result.total).toBe(2);
|
||||
expect(result.templates.map(t => t.description)).toEqual(
|
||||
expect(result.data.map(t => t.description)).toEqual(
|
||||
expect.arrayContaining([expect.stringContaining('new')]),
|
||||
);
|
||||
});
|
||||
@@ -196,11 +196,11 @@ describe('TemplateService', () => {
|
||||
|
||||
const marketingResult = await TemplateService.list(projectId, 1, 20, undefined, TemplateType.MARKETING);
|
||||
expect(marketingResult.total).toBe(2);
|
||||
expect(marketingResult.templates.every(t => t.type === TemplateType.MARKETING)).toBe(true);
|
||||
expect(marketingResult.data.every(t => t.type === TemplateType.MARKETING)).toBe(true);
|
||||
|
||||
const transactionalResult = await TemplateService.list(projectId, 1, 20, undefined, TemplateType.TRANSACTIONAL);
|
||||
expect(transactionalResult.total).toBe(1);
|
||||
expect(transactionalResult.templates[0].type).toBe(TemplateType.TRANSACTIONAL);
|
||||
expect(transactionalResult.data[0].type).toBe(TemplateType.TRANSACTIONAL);
|
||||
});
|
||||
|
||||
it('should combine search and type filters', async () => {
|
||||
@@ -223,7 +223,7 @@ describe('TemplateService', () => {
|
||||
const result = await TemplateService.list(projectId, 1, 20, 'welcome', TemplateType.MARKETING);
|
||||
|
||||
expect(result.total).toBe(1);
|
||||
expect(result.templates[0].name).toBe('Welcome Email');
|
||||
expect(result.data[0].name).toBe('Welcome Email');
|
||||
});
|
||||
|
||||
it('should return templates ordered by creation date (newest first)', async () => {
|
||||
@@ -236,9 +236,9 @@ describe('TemplateService', () => {
|
||||
|
||||
const result = await TemplateService.list(projectId, 1, 20);
|
||||
|
||||
expect(result.templates[0].id).toBe(template3.id); // Newest
|
||||
expect(result.templates[1].id).toBe(template2.id);
|
||||
expect(result.templates[2].id).toBe(template1.id); // Oldest
|
||||
expect(result.data[0].id).toBe(template3.id); // Newest
|
||||
expect(result.data[1].id).toBe(template2.id);
|
||||
expect(result.data[2].id).toBe(template1.id); // Oldest
|
||||
});
|
||||
|
||||
it('should only return templates for the specified project', async () => {
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import {beforeEach, describe, expect, it, vi} from 'vitest';
|
||||
import {Prisma, StepExecutionStatus, WorkflowExecutionStatus, WorkflowStepType} from '@plunk/db';
|
||||
import {StepExecutionStatus, WorkflowExecutionStatus, WorkflowStepType} from '@plunk/db';
|
||||
import {toPrismaJson} from '@plunk/types';
|
||||
import {WorkflowExecutionService} from '../WorkflowExecutionService';
|
||||
import {factories, getPrismaClient} from '../../../../../test/helpers';
|
||||
|
||||
@@ -59,11 +60,11 @@ describe('WorkflowExecutionService - Integration Tests', () => {
|
||||
type: WorkflowStepType.CONDITION,
|
||||
name: 'Check Premium Status',
|
||||
position: {x: 100, y: 0},
|
||||
config: {
|
||||
config: toPrismaJson({
|
||||
field: 'data.isPremium',
|
||||
operator: 'equals',
|
||||
value: true,
|
||||
},
|
||||
}),
|
||||
},
|
||||
});
|
||||
|
||||
@@ -74,7 +75,7 @@ describe('WorkflowExecutionService - Integration Tests', () => {
|
||||
type: WorkflowStepType.EXIT,
|
||||
name: 'Premium Path',
|
||||
position: {x: 200, y: -50},
|
||||
config: {reason: 'Premium customer'},
|
||||
config: toPrismaJson({reason: 'Premium customer'}),
|
||||
},
|
||||
});
|
||||
|
||||
@@ -84,7 +85,7 @@ describe('WorkflowExecutionService - Integration Tests', () => {
|
||||
type: WorkflowStepType.EXIT,
|
||||
name: 'Standard Path',
|
||||
position: {x: 200, y: 50},
|
||||
config: {reason: 'Standard customer'},
|
||||
config: toPrismaJson({reason: 'Standard customer'}),
|
||||
},
|
||||
});
|
||||
|
||||
@@ -97,7 +98,7 @@ describe('WorkflowExecutionService - Integration Tests', () => {
|
||||
data: {
|
||||
fromStepId: conditionStep.id,
|
||||
toStepId: yesStep.id,
|
||||
condition: {branch: 'yes'},
|
||||
condition: toPrismaJson({branch: 'yes'}),
|
||||
priority: 1,
|
||||
},
|
||||
});
|
||||
@@ -106,7 +107,7 @@ describe('WorkflowExecutionService - Integration Tests', () => {
|
||||
data: {
|
||||
fromStepId: conditionStep.id,
|
||||
toStepId: noStep.id,
|
||||
condition: {branch: 'no'},
|
||||
condition: toPrismaJson({branch: 'no'}),
|
||||
priority: 2,
|
||||
},
|
||||
});
|
||||
@@ -118,7 +119,7 @@ describe('WorkflowExecutionService - Integration Tests', () => {
|
||||
contactId: contact.id,
|
||||
status: WorkflowExecutionStatus.RUNNING,
|
||||
currentStepId: triggerStep.id,
|
||||
context: {},
|
||||
context: toPrismaJson({}),
|
||||
},
|
||||
});
|
||||
|
||||
@@ -160,11 +161,11 @@ describe('WorkflowExecutionService - Integration Tests', () => {
|
||||
type: WorkflowStepType.CONDITION,
|
||||
name: 'Check Premium',
|
||||
position: {x: 100, y: 0},
|
||||
config: {
|
||||
config: toPrismaJson({
|
||||
field: 'data.isPremium',
|
||||
operator: 'equals',
|
||||
value: true,
|
||||
},
|
||||
}),
|
||||
},
|
||||
});
|
||||
|
||||
@@ -174,7 +175,7 @@ describe('WorkflowExecutionService - Integration Tests', () => {
|
||||
type: WorkflowStepType.EXIT,
|
||||
name: 'Premium',
|
||||
position: {x: 200, y: -50},
|
||||
config: {},
|
||||
config: toPrismaJson({}),
|
||||
},
|
||||
});
|
||||
|
||||
@@ -184,7 +185,7 @@ describe('WorkflowExecutionService - Integration Tests', () => {
|
||||
type: WorkflowStepType.EXIT,
|
||||
name: 'Standard',
|
||||
position: {x: 200, y: 50},
|
||||
config: {},
|
||||
config: toPrismaJson({}),
|
||||
},
|
||||
});
|
||||
|
||||
@@ -196,7 +197,7 @@ describe('WorkflowExecutionService - Integration Tests', () => {
|
||||
data: {
|
||||
fromStepId: conditionStep.id,
|
||||
toStepId: yesStep.id,
|
||||
condition: {branch: 'yes'},
|
||||
condition: toPrismaJson({branch: 'yes'}),
|
||||
},
|
||||
});
|
||||
|
||||
@@ -204,7 +205,7 @@ describe('WorkflowExecutionService - Integration Tests', () => {
|
||||
data: {
|
||||
fromStepId: conditionStep.id,
|
||||
toStepId: noStep.id,
|
||||
condition: {branch: 'no'},
|
||||
condition: toPrismaJson({branch: 'no'}),
|
||||
},
|
||||
});
|
||||
|
||||
@@ -214,7 +215,7 @@ describe('WorkflowExecutionService - Integration Tests', () => {
|
||||
contactId: contact.id,
|
||||
status: WorkflowExecutionStatus.RUNNING,
|
||||
currentStepId: triggerStep.id,
|
||||
context: {},
|
||||
context: toPrismaJson({}),
|
||||
},
|
||||
});
|
||||
|
||||
@@ -250,7 +251,7 @@ describe('WorkflowExecutionService - Integration Tests', () => {
|
||||
type: WorkflowStepType.CONDITION,
|
||||
name: 'Check Country',
|
||||
position: {x: 100, y: 0},
|
||||
config: {field: 'data.country', operator: 'equals', value: 'US'},
|
||||
config: toPrismaJson({field: 'data.country', operator: 'equals', value: 'US'}),
|
||||
},
|
||||
});
|
||||
|
||||
@@ -261,7 +262,7 @@ describe('WorkflowExecutionService - Integration Tests', () => {
|
||||
type: WorkflowStepType.CONDITION,
|
||||
name: 'Check Premium (US)',
|
||||
position: {x: 200, y: -50},
|
||||
config: {field: 'data.isPremium', operator: 'equals', value: true},
|
||||
config: toPrismaJson({field: 'data.isPremium', operator: 'equals', value: true}),
|
||||
},
|
||||
});
|
||||
|
||||
@@ -271,7 +272,7 @@ describe('WorkflowExecutionService - Integration Tests', () => {
|
||||
type: WorkflowStepType.EXIT,
|
||||
name: 'US Premium',
|
||||
position: {x: 300, y: -75},
|
||||
config: {},
|
||||
config: toPrismaJson({}),
|
||||
},
|
||||
});
|
||||
|
||||
@@ -281,7 +282,7 @@ describe('WorkflowExecutionService - Integration Tests', () => {
|
||||
type: WorkflowStepType.EXIT,
|
||||
name: 'US Standard',
|
||||
position: {x: 300, y: -25},
|
||||
config: {},
|
||||
config: toPrismaJson({}),
|
||||
},
|
||||
});
|
||||
|
||||
@@ -291,7 +292,7 @@ describe('WorkflowExecutionService - Integration Tests', () => {
|
||||
type: WorkflowStepType.EXIT,
|
||||
name: 'Non-US',
|
||||
position: {x: 200, y: 50},
|
||||
config: {},
|
||||
config: toPrismaJson({}),
|
||||
},
|
||||
});
|
||||
|
||||
@@ -304,7 +305,7 @@ describe('WorkflowExecutionService - Integration Tests', () => {
|
||||
data: {
|
||||
fromStepId: condition1.id,
|
||||
toStepId: condition2.id,
|
||||
condition: {branch: 'yes'},
|
||||
condition: toPrismaJson({branch: 'yes'}),
|
||||
},
|
||||
});
|
||||
|
||||
@@ -312,7 +313,7 @@ describe('WorkflowExecutionService - Integration Tests', () => {
|
||||
data: {
|
||||
fromStepId: condition1.id,
|
||||
toStepId: nonUsExit.id,
|
||||
condition: {branch: 'no'},
|
||||
condition: toPrismaJson({branch: 'no'}),
|
||||
},
|
||||
});
|
||||
|
||||
@@ -320,7 +321,7 @@ describe('WorkflowExecutionService - Integration Tests', () => {
|
||||
data: {
|
||||
fromStepId: condition2.id,
|
||||
toStepId: usPremiumExit.id,
|
||||
condition: {branch: 'yes'},
|
||||
condition: toPrismaJson({branch: 'yes'}),
|
||||
},
|
||||
});
|
||||
|
||||
@@ -328,7 +329,7 @@ describe('WorkflowExecutionService - Integration Tests', () => {
|
||||
data: {
|
||||
fromStepId: condition2.id,
|
||||
toStepId: usStandardExit.id,
|
||||
condition: {branch: 'no'},
|
||||
condition: toPrismaJson({branch: 'no'}),
|
||||
},
|
||||
});
|
||||
|
||||
@@ -338,7 +339,7 @@ describe('WorkflowExecutionService - Integration Tests', () => {
|
||||
contactId: contact.id,
|
||||
status: WorkflowExecutionStatus.RUNNING,
|
||||
currentStepId: triggerStep.id,
|
||||
context: {},
|
||||
context: toPrismaJson({}),
|
||||
},
|
||||
});
|
||||
|
||||
@@ -378,10 +379,10 @@ describe('WorkflowExecutionService - Integration Tests', () => {
|
||||
type: WorkflowStepType.WAIT_FOR_EVENT,
|
||||
name: 'Wait for Purchase',
|
||||
position: {x: 100, y: 0},
|
||||
config: {
|
||||
config: toPrismaJson({
|
||||
eventName: 'purchase.completed',
|
||||
timeout: 3600, // 1 hour
|
||||
},
|
||||
}),
|
||||
},
|
||||
});
|
||||
|
||||
@@ -391,7 +392,7 @@ describe('WorkflowExecutionService - Integration Tests', () => {
|
||||
type: WorkflowStepType.EXIT,
|
||||
name: 'Complete',
|
||||
position: {x: 200, y: 0},
|
||||
config: {},
|
||||
config: toPrismaJson({}),
|
||||
},
|
||||
});
|
||||
|
||||
@@ -409,7 +410,7 @@ describe('WorkflowExecutionService - Integration Tests', () => {
|
||||
contactId: contact.id,
|
||||
status: WorkflowExecutionStatus.RUNNING,
|
||||
currentStepId: triggerStep.id,
|
||||
context: {},
|
||||
context: toPrismaJson({}),
|
||||
},
|
||||
});
|
||||
|
||||
@@ -447,10 +448,10 @@ describe('WorkflowExecutionService - Integration Tests', () => {
|
||||
type: WorkflowStepType.WAIT_FOR_EVENT,
|
||||
name: 'Wait for Event',
|
||||
position: {x: 100, y: 0},
|
||||
config: {
|
||||
config: toPrismaJson({
|
||||
eventName: 'user.verified',
|
||||
timeout: 3600,
|
||||
},
|
||||
}),
|
||||
},
|
||||
});
|
||||
|
||||
@@ -460,7 +461,7 @@ describe('WorkflowExecutionService - Integration Tests', () => {
|
||||
type: WorkflowStepType.EXIT,
|
||||
name: 'Done',
|
||||
position: {x: 200, y: 0},
|
||||
config: {},
|
||||
config: toPrismaJson({}),
|
||||
},
|
||||
});
|
||||
|
||||
@@ -478,7 +479,7 @@ describe('WorkflowExecutionService - Integration Tests', () => {
|
||||
contactId: contact.id,
|
||||
status: WorkflowExecutionStatus.RUNNING,
|
||||
currentStepId: triggerStep.id,
|
||||
context: {},
|
||||
context: toPrismaJson({}),
|
||||
},
|
||||
});
|
||||
|
||||
@@ -526,7 +527,7 @@ describe('WorkflowExecutionService - Integration Tests', () => {
|
||||
type: WorkflowStepType.DELAY,
|
||||
name: 'Wait 1 day',
|
||||
position: {x: 100, y: 0},
|
||||
config: {amount: 1, unit: 'days'},
|
||||
config: toPrismaJson({amount: 1, unit: 'days'}),
|
||||
},
|
||||
});
|
||||
|
||||
@@ -536,7 +537,7 @@ describe('WorkflowExecutionService - Integration Tests', () => {
|
||||
type: WorkflowStepType.CONDITION,
|
||||
name: 'Check Status',
|
||||
position: {x: 200, y: 0},
|
||||
config: {field: 'contact.subscribed', operator: 'equals', value: true},
|
||||
config: toPrismaJson({field: 'contact.subscribed', operator: 'equals', value: true}),
|
||||
},
|
||||
});
|
||||
|
||||
@@ -546,7 +547,7 @@ describe('WorkflowExecutionService - Integration Tests', () => {
|
||||
type: WorkflowStepType.EXIT,
|
||||
name: 'Complete',
|
||||
position: {x: 300, y: 0},
|
||||
config: {},
|
||||
config: toPrismaJson({}),
|
||||
},
|
||||
});
|
||||
|
||||
@@ -561,7 +562,7 @@ describe('WorkflowExecutionService - Integration Tests', () => {
|
||||
data: {
|
||||
fromStepId: condition.id,
|
||||
toStepId: exit.id,
|
||||
condition: {branch: 'yes'},
|
||||
condition: toPrismaJson({branch: 'yes'}),
|
||||
},
|
||||
});
|
||||
|
||||
@@ -571,7 +572,7 @@ describe('WorkflowExecutionService - Integration Tests', () => {
|
||||
contactId: contact.id,
|
||||
status: WorkflowExecutionStatus.RUNNING,
|
||||
currentStepId: triggerStep.id,
|
||||
context: {},
|
||||
context: toPrismaJson({}),
|
||||
},
|
||||
});
|
||||
|
||||
@@ -611,7 +612,7 @@ describe('WorkflowExecutionService - Integration Tests', () => {
|
||||
type: WorkflowStepType.CONDITION,
|
||||
name: 'A/B Split',
|
||||
position: {x: 100, y: 0},
|
||||
config: {field: 'data.segment', operator: 'equals', value: 'A'},
|
||||
config: toPrismaJson({field: 'data.segment', operator: 'equals', value: 'A'}),
|
||||
},
|
||||
});
|
||||
|
||||
@@ -621,7 +622,7 @@ describe('WorkflowExecutionService - Integration Tests', () => {
|
||||
type: WorkflowStepType.DELAY,
|
||||
name: 'Path A Delay',
|
||||
position: {x: 200, y: -50},
|
||||
config: {amount: 1, unit: 'hours'},
|
||||
config: toPrismaJson({amount: 1, unit: 'hours'}),
|
||||
},
|
||||
});
|
||||
|
||||
@@ -631,7 +632,7 @@ describe('WorkflowExecutionService - Integration Tests', () => {
|
||||
type: WorkflowStepType.DELAY,
|
||||
name: 'Path B Delay',
|
||||
position: {x: 200, y: 50},
|
||||
config: {amount: 2, unit: 'hours'},
|
||||
config: toPrismaJson({amount: 2, unit: 'hours'}),
|
||||
},
|
||||
});
|
||||
|
||||
@@ -641,7 +642,7 @@ describe('WorkflowExecutionService - Integration Tests', () => {
|
||||
type: WorkflowStepType.EXIT,
|
||||
name: 'Merge Point',
|
||||
position: {x: 300, y: 0},
|
||||
config: {},
|
||||
config: toPrismaJson({}),
|
||||
},
|
||||
});
|
||||
|
||||
@@ -653,7 +654,7 @@ describe('WorkflowExecutionService - Integration Tests', () => {
|
||||
data: {
|
||||
fromStepId: condition.id,
|
||||
toStepId: pathA.id,
|
||||
condition: {branch: 'yes'},
|
||||
condition: toPrismaJson({branch: 'yes'}),
|
||||
},
|
||||
});
|
||||
|
||||
@@ -661,7 +662,7 @@ describe('WorkflowExecutionService - Integration Tests', () => {
|
||||
data: {
|
||||
fromStepId: condition.id,
|
||||
toStepId: pathB.id,
|
||||
condition: {branch: 'no'},
|
||||
condition: toPrismaJson({branch: 'no'}),
|
||||
},
|
||||
});
|
||||
|
||||
@@ -679,7 +680,7 @@ describe('WorkflowExecutionService - Integration Tests', () => {
|
||||
contactId: contact.id,
|
||||
status: WorkflowExecutionStatus.RUNNING,
|
||||
currentStepId: triggerStep.id,
|
||||
context: {},
|
||||
context: toPrismaJson({}),
|
||||
},
|
||||
});
|
||||
|
||||
@@ -717,7 +718,7 @@ describe('WorkflowExecutionService - Integration Tests', () => {
|
||||
type: WorkflowStepType.CONDITION,
|
||||
name: 'Bad Condition',
|
||||
position: {x: 100, y: 0},
|
||||
config: {}, // Invalid - missing required fields
|
||||
config: toPrismaJson({}), // Invalid - missing required fields
|
||||
},
|
||||
});
|
||||
|
||||
@@ -731,7 +732,7 @@ describe('WorkflowExecutionService - Integration Tests', () => {
|
||||
contactId: contact.id,
|
||||
status: WorkflowExecutionStatus.RUNNING,
|
||||
currentStepId: triggerStep.id,
|
||||
context: {},
|
||||
context: toPrismaJson({}),
|
||||
},
|
||||
});
|
||||
|
||||
@@ -764,11 +765,11 @@ describe('WorkflowExecutionService - Integration Tests', () => {
|
||||
type: WorkflowStepType.CONDITION,
|
||||
name: 'Check Missing Field',
|
||||
position: {x: 100, y: 0},
|
||||
config: {
|
||||
config: toPrismaJson({
|
||||
field: 'data.nonExistentField',
|
||||
operator: 'equals',
|
||||
value: 'something',
|
||||
},
|
||||
}),
|
||||
},
|
||||
});
|
||||
|
||||
@@ -778,7 +779,7 @@ describe('WorkflowExecutionService - Integration Tests', () => {
|
||||
type: WorkflowStepType.EXIT,
|
||||
name: 'Exit',
|
||||
position: {x: 200, y: 0},
|
||||
config: {},
|
||||
config: toPrismaJson({}),
|
||||
},
|
||||
});
|
||||
|
||||
@@ -790,7 +791,7 @@ describe('WorkflowExecutionService - Integration Tests', () => {
|
||||
data: {
|
||||
fromStepId: condition.id,
|
||||
toStepId: noStep.id,
|
||||
condition: {branch: 'no'},
|
||||
condition: toPrismaJson({branch: 'no'}),
|
||||
},
|
||||
});
|
||||
|
||||
@@ -800,7 +801,7 @@ describe('WorkflowExecutionService - Integration Tests', () => {
|
||||
contactId: contact.id,
|
||||
status: WorkflowExecutionStatus.RUNNING,
|
||||
currentStepId: triggerStep.id,
|
||||
context: {},
|
||||
context: toPrismaJson({}),
|
||||
},
|
||||
});
|
||||
|
||||
@@ -834,7 +835,7 @@ describe('WorkflowExecutionService - Integration Tests', () => {
|
||||
type: WorkflowStepType.EXIT,
|
||||
name: 'Early Exit',
|
||||
position: {x: 100, y: 0},
|
||||
config: {reason: 'User already converted'},
|
||||
config: toPrismaJson({reason: 'User already converted'}),
|
||||
},
|
||||
});
|
||||
|
||||
@@ -848,7 +849,7 @@ describe('WorkflowExecutionService - Integration Tests', () => {
|
||||
contactId: contact.id,
|
||||
status: WorkflowExecutionStatus.RUNNING,
|
||||
currentStepId: triggerStep.id,
|
||||
context: {},
|
||||
context: toPrismaJson({}),
|
||||
},
|
||||
});
|
||||
|
||||
@@ -882,7 +883,7 @@ describe('WorkflowExecutionService - Integration Tests', () => {
|
||||
type: WorkflowStepType.TRIGGER,
|
||||
name: 'Start',
|
||||
position: {x: 0, y: 0},
|
||||
config: {},
|
||||
config: toPrismaJson({}),
|
||||
});
|
||||
|
||||
const exitStep = await factories.createWorkflowStep({
|
||||
@@ -890,7 +891,7 @@ describe('WorkflowExecutionService - Integration Tests', () => {
|
||||
type: WorkflowStepType.EXIT,
|
||||
name: 'End',
|
||||
position: {x: 100, y: 0},
|
||||
config: {},
|
||||
config: toPrismaJson({}),
|
||||
});
|
||||
|
||||
await prisma.workflowTransition.create({
|
||||
@@ -910,7 +911,7 @@ describe('WorkflowExecutionService - Integration Tests', () => {
|
||||
contactId: contact.id,
|
||||
status: WorkflowExecutionStatus.RUNNING,
|
||||
currentStepId: triggerStep.id,
|
||||
context: contextData as Prisma.InputJsonValue,
|
||||
context: toPrismaJson(contextData),
|
||||
},
|
||||
});
|
||||
|
||||
@@ -974,11 +975,11 @@ describe('WorkflowExecutionService - Integration Tests', () => {
|
||||
type: WorkflowStepType.CONDITION,
|
||||
name: 'Check if first open',
|
||||
position: {x: 100, y: 0},
|
||||
config: {
|
||||
config: toPrismaJson({
|
||||
field: 'event.isFirstOpen',
|
||||
operator: 'equals',
|
||||
value: true, // Use boolean, not string
|
||||
} as Prisma.InputJsonValue,
|
||||
}),
|
||||
},
|
||||
});
|
||||
|
||||
@@ -988,7 +989,7 @@ describe('WorkflowExecutionService - Integration Tests', () => {
|
||||
type: WorkflowStepType.EXIT,
|
||||
name: 'First Open',
|
||||
position: {x: 200, y: 0},
|
||||
config: {reason: 'first_open'} as Prisma.InputJsonValue,
|
||||
config: toPrismaJson({reason: 'first_open'}),
|
||||
},
|
||||
});
|
||||
|
||||
@@ -998,7 +999,7 @@ describe('WorkflowExecutionService - Integration Tests', () => {
|
||||
type: WorkflowStepType.EXIT,
|
||||
name: 'Not First Open',
|
||||
position: {x: 200, y: 100},
|
||||
config: {reason: 'not_first_open'} as Prisma.InputJsonValue,
|
||||
config: toPrismaJson({reason: 'not_first_open'}),
|
||||
},
|
||||
});
|
||||
|
||||
@@ -1007,10 +1008,10 @@ describe('WorkflowExecutionService - Integration Tests', () => {
|
||||
data: {fromStepId: triggerStep!.id, toStepId: conditionStep.id},
|
||||
});
|
||||
await prisma.workflowTransition.create({
|
||||
data: {fromStepId: conditionStep.id, toStepId: yesStep.id, condition: {branch: 'yes'} as Prisma.InputJsonValue},
|
||||
data: {fromStepId: conditionStep.id, toStepId: yesStep.id, condition: toPrismaJson({branch: 'yes'})},
|
||||
});
|
||||
await prisma.workflowTransition.create({
|
||||
data: {fromStepId: conditionStep.id, toStepId: noStep.id, condition: {branch: 'no'} as Prisma.InputJsonValue},
|
||||
data: {fromStepId: conditionStep.id, toStepId: noStep.id, condition: toPrismaJson({branch: 'no'})},
|
||||
});
|
||||
|
||||
// Create execution with event data
|
||||
@@ -1020,12 +1021,12 @@ describe('WorkflowExecutionService - Integration Tests', () => {
|
||||
contactId: contact.id,
|
||||
status: WorkflowExecutionStatus.RUNNING,
|
||||
currentStepId: triggerStep!.id,
|
||||
context: {
|
||||
context: toPrismaJson({
|
||||
subject: 'Welcome Email',
|
||||
from: 'hello@example.com',
|
||||
isFirstOpen: true,
|
||||
openedAt: new Date().toISOString(),
|
||||
} as Prisma.InputJsonValue,
|
||||
}),
|
||||
},
|
||||
});
|
||||
|
||||
@@ -1056,11 +1057,11 @@ describe('WorkflowExecutionService - Integration Tests', () => {
|
||||
type: WorkflowStepType.CONDITION,
|
||||
name: 'Check subject',
|
||||
position: {x: 100, y: 0},
|
||||
config: {
|
||||
config: toPrismaJson({
|
||||
field: 'event.subject',
|
||||
operator: 'contains',
|
||||
value: 'Welcome',
|
||||
} as Prisma.InputJsonValue,
|
||||
}),
|
||||
},
|
||||
});
|
||||
|
||||
@@ -1070,7 +1071,7 @@ describe('WorkflowExecutionService - Integration Tests', () => {
|
||||
type: WorkflowStepType.EXIT,
|
||||
name: 'Done',
|
||||
position: {x: 200, y: 0},
|
||||
config: {reason: 'matched'} as Prisma.InputJsonValue,
|
||||
config: toPrismaJson({reason: 'matched'}),
|
||||
},
|
||||
});
|
||||
|
||||
@@ -1081,7 +1082,7 @@ describe('WorkflowExecutionService - Integration Tests', () => {
|
||||
data: {
|
||||
fromStepId: conditionStep.id,
|
||||
toStepId: exitStep.id,
|
||||
condition: {branch: 'yes'} as Prisma.InputJsonValue,
|
||||
condition: toPrismaJson({branch: 'yes'}),
|
||||
},
|
||||
});
|
||||
|
||||
@@ -1091,10 +1092,10 @@ describe('WorkflowExecutionService - Integration Tests', () => {
|
||||
contactId: contact.id,
|
||||
status: WorkflowExecutionStatus.RUNNING,
|
||||
currentStepId: triggerStep!.id,
|
||||
context: {
|
||||
context: toPrismaJson({
|
||||
subject: 'Welcome to Plunk!',
|
||||
from: 'team@plunk.com',
|
||||
} as Prisma.InputJsonValue,
|
||||
}),
|
||||
},
|
||||
});
|
||||
|
||||
@@ -1122,11 +1123,11 @@ describe('WorkflowExecutionService - Integration Tests', () => {
|
||||
type: WorkflowStepType.CONDITION,
|
||||
name: 'Check opens count',
|
||||
position: {x: 100, y: 0},
|
||||
config: {
|
||||
config: toPrismaJson({
|
||||
field: 'event.opens',
|
||||
operator: 'greaterThan',
|
||||
value: '3',
|
||||
} as Prisma.InputJsonValue,
|
||||
}),
|
||||
},
|
||||
});
|
||||
|
||||
@@ -1136,7 +1137,7 @@ describe('WorkflowExecutionService - Integration Tests', () => {
|
||||
type: WorkflowStepType.EXIT,
|
||||
name: 'Done',
|
||||
position: {x: 200, y: 0},
|
||||
config: {reason: 'engaged'} as Prisma.InputJsonValue,
|
||||
config: toPrismaJson({reason: 'engaged'}),
|
||||
},
|
||||
});
|
||||
|
||||
@@ -1147,7 +1148,7 @@ describe('WorkflowExecutionService - Integration Tests', () => {
|
||||
data: {
|
||||
fromStepId: conditionStep.id,
|
||||
toStepId: exitStep.id,
|
||||
condition: {branch: 'yes'} as Prisma.InputJsonValue,
|
||||
condition: toPrismaJson({branch: 'yes'}),
|
||||
},
|
||||
});
|
||||
|
||||
@@ -1157,10 +1158,10 @@ describe('WorkflowExecutionService - Integration Tests', () => {
|
||||
contactId: contact.id,
|
||||
status: WorkflowExecutionStatus.RUNNING,
|
||||
currentStepId: triggerStep!.id,
|
||||
context: {
|
||||
context: toPrismaJson({
|
||||
subject: 'Newsletter',
|
||||
opens: 5,
|
||||
} as Prisma.InputJsonValue,
|
||||
}),
|
||||
},
|
||||
});
|
||||
|
||||
@@ -1204,10 +1205,10 @@ describe('WorkflowExecutionService - Integration Tests', () => {
|
||||
type: WorkflowStepType.WEBHOOK,
|
||||
name: 'Send Webhook',
|
||||
position: {x: 100, y: 0},
|
||||
config: {
|
||||
config: toPrismaJson({
|
||||
url: 'https://webhook.example.com/test',
|
||||
method: 'POST',
|
||||
} as Prisma.InputJsonValue,
|
||||
}),
|
||||
},
|
||||
});
|
||||
|
||||
@@ -1221,13 +1222,13 @@ describe('WorkflowExecutionService - Integration Tests', () => {
|
||||
contactId: contact.id,
|
||||
status: WorkflowExecutionStatus.RUNNING,
|
||||
currentStepId: triggerStep!.id,
|
||||
context: {
|
||||
context: toPrismaJson({
|
||||
subject: 'Welcome Email',
|
||||
from: 'hello@example.com',
|
||||
messageId: 'msg-123',
|
||||
isFirstOpen: true,
|
||||
openedAt: '2024-01-15T10:00:00Z',
|
||||
} as Prisma.InputJsonValue,
|
||||
}),
|
||||
},
|
||||
});
|
||||
|
||||
|
||||
@@ -1,9 +1,9 @@
|
||||
import {describe, it, expect, beforeEach} from 'vitest';
|
||||
import {beforeEach, describe, expect, it} from 'vitest';
|
||||
import {
|
||||
WorkflowStepType,
|
||||
StepExecutionStatus,
|
||||
WorkflowExecutionStatus,
|
||||
TemplateType,
|
||||
WorkflowExecutionStatus,
|
||||
WorkflowStepType,
|
||||
WorkflowTriggerType,
|
||||
} from '@plunk/db';
|
||||
import {WorkflowExecutionService} from '../WorkflowExecutionService';
|
||||
|
||||
@@ -205,7 +205,7 @@ describe('WorkflowService', () => {
|
||||
|
||||
const page1 = await WorkflowService.list(projectId, 1, 10);
|
||||
|
||||
expect(page1.workflows).toHaveLength(10);
|
||||
expect(page1.data).toHaveLength(10);
|
||||
expect(page1.total).toBe(25);
|
||||
expect(page1.totalPages).toBe(3);
|
||||
});
|
||||
@@ -218,7 +218,7 @@ describe('WorkflowService', () => {
|
||||
const result = await WorkflowService.list(projectId, 1, 20, 'welcome');
|
||||
|
||||
expect(result.total).toBe(2);
|
||||
expect(result.workflows.every(w => w.name.toLowerCase().includes('welcome'))).toBe(true);
|
||||
expect(result.data.every(w => w.name.toLowerCase().includes('welcome'))).toBe(true);
|
||||
});
|
||||
|
||||
it('should include step and execution counts', async () => {
|
||||
@@ -234,8 +234,8 @@ describe('WorkflowService', () => {
|
||||
|
||||
const result = await WorkflowService.list(projectId);
|
||||
|
||||
const found = result.workflows.find(w => w.id === workflow.id) as
|
||||
| ((typeof result.workflows)[number] & {_count: {steps: number; executions: number}})
|
||||
const found = result.data.find(w => w.id === workflow.id) as
|
||||
| ((typeof result.data)[number] & {_count: {steps: number; executions: number}})
|
||||
| undefined;
|
||||
expect(found?._count.steps).toBe(3); // TRIGGER + 2 added
|
||||
expect(found?._count.executions).toBe(1);
|
||||
|
||||
Reference in New Issue
Block a user