Add better segmenting

This commit is contained in:
Dries Augustyns
2025-12-02 09:03:05 +01:00
parent b521a405bb
commit fe2e038037
23 changed files with 2285 additions and 855 deletions
+8 -9
View File
@@ -8,7 +8,6 @@ import type {AuthResponse} from '../middleware/auth.js';
import {requireAuth} from '../middleware/auth.js';
import {CampaignService} from '../services/CampaignService.js';
import {DomainService} from '../services/DomainService.js';
import {type SegmentFilter} from '../services/SegmentService.js';
import {CatchAsync} from '../utils/asyncHandler.js';
@Controller('campaigns')
@@ -22,7 +21,7 @@ export class Campaigns {
@CatchAsync
private async create(req: Request, res: Response, next: NextFunction) {
const auth = res.locals.auth as AuthResponse;
const {name, description, subject, body, from, fromName, replyTo, audienceType, audienceFilter, segmentId} =
const {name, description, subject, body, from, fromName, replyTo, audienceType, audienceCondition, segmentId} =
CampaignSchemas.create.parse(req.body);
// Validate audience-specific fields
@@ -30,8 +29,8 @@ export class Campaigns {
throw new HttpException(400, 'Segment ID is required for SEGMENT audience type');
}
if (audienceType === CampaignAudienceType.FILTERED && !audienceFilter) {
throw new HttpException(400, 'Audience filter is required for FILTERED audience type');
if (audienceType === CampaignAudienceType.FILTERED && !audienceCondition) {
throw new HttpException(400, 'Audience condition is required for FILTERED audience type');
}
// Verify domain ownership and verification
@@ -46,7 +45,7 @@ export class Campaigns {
fromName,
replyTo,
audienceType,
audienceFilter: audienceFilter as SegmentFilter[] | undefined,
audienceCondition,
segmentId,
});
@@ -118,7 +117,7 @@ export class Campaigns {
private async update(req: Request, res: Response, next: NextFunction) {
const auth = res.locals.auth as AuthResponse;
const {id} = req.params;
const {name, description, subject, body, from, fromName, replyTo, audienceType, audienceFilter, segmentId} =
const {name, description, subject, body, from, fromName, replyTo, audienceType, audienceCondition, segmentId} =
req.body;
// Validate audience-specific fields if audienceType is being updated
@@ -126,8 +125,8 @@ export class Campaigns {
throw new HttpException(400, 'Segment ID is required for SEGMENT audience type');
}
if (audienceType === CampaignAudienceType.FILTERED && audienceFilter === undefined) {
throw new HttpException(400, 'Audience filter is required for FILTERED audience type');
if (audienceType === CampaignAudienceType.FILTERED && audienceCondition === undefined) {
throw new HttpException(400, 'Audience condition is required for FILTERED audience type');
}
// Verify domain ownership and verification if 'from' is being updated
@@ -144,7 +143,7 @@ export class Campaigns {
fromName,
replyTo,
audienceType,
audienceFilter,
audienceCondition,
segmentId,
});
+4 -3
View File
@@ -47,6 +47,7 @@ export class Contacts {
/**
* GET /contacts/fields
* Get all available contact fields (both standard and custom fields from data JSON)
* Returns field names with inferred types (string, number, boolean, date)
*/
@Get('fields')
@Middleware([requireAuth])
@@ -55,11 +56,11 @@ export class Contacts {
const auth = res.locals.auth as AuthResponse;
try {
const fields = await ContactService.getAvailableFields(auth.projectId!);
const fieldsWithTypes = await ContactService.getAvailableFields(auth.projectId!);
return res.status(200).json({
fields,
count: fields.length,
fields: fieldsWithTypes,
count: fieldsWithTypes.length,
});
} catch (error) {
console.error('[CONTACTS] Failed to get available fields:', error);
+8 -8
View File
@@ -74,20 +74,20 @@ export class Segments {
@CatchAsync
public async create(req: Request, res: Response, next: NextFunction) {
const auth = res.locals.auth as AuthResponse;
const {name, description, filters, trackMembership} = req.body;
const {name, description, condition, trackMembership} = req.body;
if (!name) {
return res.status(400).json({error: 'Name is required'});
}
if (!filters || !Array.isArray(filters)) {
return res.status(400).json({error: 'Filters must be an array'});
if (!condition || typeof condition !== 'object') {
return res.status(400).json({error: 'Condition is required and must be an object'});
}
const segment = await SegmentService.create(auth.projectId!, {
name,
description,
filters,
condition,
trackMembership,
});
@@ -104,20 +104,20 @@ export class Segments {
public async update(req: Request, res: Response, next: NextFunction) {
const auth = res.locals.auth as AuthResponse;
const segmentId = req.params.id;
const {name, description, filters, trackMembership} = req.body;
const {name, description, condition, trackMembership} = req.body;
if (!segmentId) {
return res.status(400).json({error: 'Segment ID is required'});
}
if (filters !== undefined && !Array.isArray(filters)) {
return res.status(400).json({error: 'Filters must be an array'});
if (condition !== undefined && typeof condition !== 'object') {
return res.status(400).json({error: 'Condition must be an object'});
}
const segment = await SegmentService.update(auth.projectId!, segmentId, {
name,
description,
filters,
condition,
trackMembership,
});
+20
View File
@@ -370,4 +370,24 @@ export class Workflows {
return res.status(200).json(execution);
}
/**
* POST /workflows/:id/executions/cancel-all
* Cancel all active executions for a workflow
*/
@Post(':id/executions/cancel-all')
@Middleware([requireAuth])
@CatchAsync
public async cancelAllExecutions(req: Request, res: Response, next: NextFunction) {
const auth = res.locals.auth as AuthResponse;
const workflowId = req.params.id;
if (!workflowId) {
return res.status(400).json({error: 'Workflow ID is required'});
}
const result = await WorkflowService.cancelAllExecutions(auth.projectId!, workflowId);
return res.status(200).json(result);
}
}