Fix linting errors
This commit is contained in:
@@ -1,4 +1,5 @@
|
||||
import {type Contact, Prisma} from '@plunk/db';
|
||||
import type {FilterCondition, FilterGroup} from '@plunk/types';
|
||||
|
||||
import {prisma} from '../database/prisma.js';
|
||||
import {HttpException} from '../exceptions/index.js';
|
||||
@@ -144,8 +145,7 @@ export class ContactService {
|
||||
}
|
||||
|
||||
// Track subscription status change
|
||||
const isSubscriptionChanging =
|
||||
data.subscribed !== undefined && existing.subscribed !== data.subscribed;
|
||||
const isSubscriptionChanging = data.subscribed !== undefined && existing.subscribed !== data.subscribed;
|
||||
const wasSubscribed = existing.subscribed;
|
||||
|
||||
try {
|
||||
@@ -249,8 +249,7 @@ export class ContactService {
|
||||
|
||||
if (existing) {
|
||||
// Track subscription status change
|
||||
const isSubscriptionChanging =
|
||||
subscribed !== undefined && existing.subscribed !== subscribed;
|
||||
const isSubscriptionChanging = subscribed !== undefined && existing.subscribed !== subscribed;
|
||||
const wasSubscribed = existing.subscribed;
|
||||
|
||||
const updated = await prisma.contact.update({
|
||||
@@ -561,16 +560,10 @@ export class ContactService {
|
||||
|
||||
// Check which segments use this field
|
||||
const usedInSegments = segments.filter(segment => {
|
||||
const condition = segment.condition as any;
|
||||
const condition = segment.condition as FilterCondition | null;
|
||||
return this.fieldUsedInCondition(field, condition);
|
||||
});
|
||||
|
||||
// Get all campaigns for the project (emails)
|
||||
const campaigns = await prisma.email.findMany({
|
||||
where: {projectId},
|
||||
select: {id: true, subject: true},
|
||||
});
|
||||
|
||||
// For now, we'll check if campaigns use the field in their subject or body
|
||||
// This is a simplified check - you might want to enhance this based on your campaign structure
|
||||
const usedInCampaigns: Array<{id: string; name: string}> = [];
|
||||
@@ -604,51 +597,6 @@ export class ContactService {
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Helper: Check if a field is used in a filter condition (recursive)
|
||||
*/
|
||||
private static fieldUsedInCondition(field: string, condition: any): boolean {
|
||||
if (!condition || typeof condition !== 'object') {
|
||||
return false;
|
||||
}
|
||||
|
||||
// Check groups in the condition
|
||||
if (Array.isArray(condition.groups)) {
|
||||
for (const group of condition.groups) {
|
||||
if (this.fieldUsedInGroup(field, group)) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Helper: Check if a field is used in a filter group (recursive)
|
||||
*/
|
||||
private static fieldUsedInGroup(field: string, group: any): boolean {
|
||||
if (!group || typeof group !== 'object') {
|
||||
return false;
|
||||
}
|
||||
|
||||
// Check filters in the group
|
||||
if (Array.isArray(group.filters)) {
|
||||
for (const filter of group.filters) {
|
||||
if (filter.field === field) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Check nested conditions
|
||||
if (group.conditions) {
|
||||
return this.fieldUsedInCondition(field, group.conditions);
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Delete a custom field from all contacts
|
||||
* WARNING: This is destructive and cannot be undone
|
||||
@@ -686,4 +634,49 @@ export class ContactService {
|
||||
|
||||
return {deletedFrom: result};
|
||||
}
|
||||
|
||||
/**
|
||||
* Helper: Check if a field is used in a filter condition (recursive)
|
||||
*/
|
||||
private static fieldUsedInCondition(field: string, condition: FilterCondition | null): boolean {
|
||||
if (!condition || typeof condition !== 'object') {
|
||||
return false;
|
||||
}
|
||||
|
||||
// Check groups in the condition
|
||||
if (Array.isArray(condition.groups)) {
|
||||
for (const group of condition.groups) {
|
||||
if (this.fieldUsedInGroup(field, group)) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Helper: Check if a field is used in a filter group (recursive)
|
||||
*/
|
||||
private static fieldUsedInGroup(field: string, group: FilterGroup): boolean {
|
||||
if (!group || typeof group !== 'object') {
|
||||
return false;
|
||||
}
|
||||
|
||||
// Check filters in the group
|
||||
if (Array.isArray(group.filters)) {
|
||||
for (const filter of group.filters) {
|
||||
if (filter.field === field) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Check nested conditions
|
||||
if (group.conditions) {
|
||||
return this.fieldUsedInCondition(field, group.conditions);
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import type {Event} from '@plunk/db';
|
||||
import {Prisma} from '@plunk/db';
|
||||
import type {FilterCondition, FilterGroup} from '@plunk/types';
|
||||
|
||||
import {prisma} from '../database/prisma.js';
|
||||
import {redis} from '../database/redis.js';
|
||||
@@ -177,6 +178,139 @@ export class EventService {
|
||||
return Array.from(fieldSet).sort();
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if an event is used in any segments or workflows
|
||||
* Returns usage information including which segments/workflows use the event
|
||||
*
|
||||
* @param projectId - The project ID
|
||||
* @param eventName - The event name to check (e.g., "purchase.completed", "user.signup")
|
||||
* @returns Usage information
|
||||
*/
|
||||
public static async getEventUsage(
|
||||
projectId: string,
|
||||
eventName: string,
|
||||
): Promise<{
|
||||
usedInSegments: Array<{id: string; name: string}>;
|
||||
usedInWorkflows: Array<{id: string; name: string}>;
|
||||
totalCount: number;
|
||||
uniqueContacts: number;
|
||||
canDelete: boolean;
|
||||
}> {
|
||||
// Get all segments for the project
|
||||
const segments = await prisma.segment.findMany({
|
||||
where: {projectId},
|
||||
select: {id: true, name: true, condition: true},
|
||||
});
|
||||
|
||||
// Check which segments use this event
|
||||
const usedInSegments = segments.filter(segment => {
|
||||
const condition = segment.condition as FilterCondition | null;
|
||||
return this.eventUsedInCondition(eventName, condition);
|
||||
});
|
||||
|
||||
// Get workflows that use this event as a trigger or wait condition
|
||||
const workflows = await prisma.workflow.findMany({
|
||||
where: {
|
||||
projectId,
|
||||
OR: [
|
||||
// Event as trigger
|
||||
{
|
||||
triggerType: 'EVENT',
|
||||
triggerConfig: {
|
||||
path: ['eventName'],
|
||||
equals: eventName,
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
select: {id: true, name: true},
|
||||
});
|
||||
|
||||
// Also check workflow steps that wait for events
|
||||
const workflowStepsWithEvent = await prisma.workflowStep.findMany({
|
||||
where: {
|
||||
workflow: {projectId},
|
||||
type: 'WAIT_FOR_EVENT',
|
||||
config: {
|
||||
path: ['eventName'],
|
||||
equals: eventName,
|
||||
},
|
||||
},
|
||||
include: {
|
||||
workflow: {
|
||||
select: {id: true, name: true},
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
const usedInWorkflows = [...workflows, ...workflowStepsWithEvent.map(step => step.workflow)].reduce(
|
||||
(acc, workflow) => {
|
||||
// Deduplicate by id
|
||||
if (!acc.find((w: {id: string; name: string}) => w.id === workflow.id)) {
|
||||
acc.push(workflow);
|
||||
}
|
||||
return acc;
|
||||
},
|
||||
[] as Array<{id: string; name: string}>,
|
||||
);
|
||||
|
||||
// Get event statistics
|
||||
const [totalCount, uniqueContacts] = await Promise.all([
|
||||
prisma.event.count({
|
||||
where: {projectId, name: eventName},
|
||||
}),
|
||||
prisma.event
|
||||
.groupBy({
|
||||
by: ['contactId'],
|
||||
where: {projectId, name: eventName, contactId: {not: null}},
|
||||
})
|
||||
.then(results => results.length),
|
||||
]);
|
||||
|
||||
const canDelete = usedInSegments.length === 0 && usedInWorkflows.length === 0;
|
||||
|
||||
return {
|
||||
usedInSegments,
|
||||
usedInWorkflows,
|
||||
totalCount,
|
||||
uniqueContacts,
|
||||
canDelete,
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Delete all events with a specific name
|
||||
* WARNING: This is destructive and cannot be undone
|
||||
* Should only be called after verifying the event is not in use
|
||||
*
|
||||
* @param projectId - The project ID
|
||||
* @param eventName - The event name to delete
|
||||
*/
|
||||
public static async deleteEvent(projectId: string, eventName: string): Promise<{deletedCount: number}> {
|
||||
// Prevent deletion of system events
|
||||
if (eventName.startsWith('email.') || eventName.startsWith('segment.')) {
|
||||
throw new Error('Cannot delete system events (email.* or segment.*)');
|
||||
}
|
||||
|
||||
// Check if event is in use
|
||||
const usage = await this.getEventUsage(projectId, eventName);
|
||||
if (!usage.canDelete) {
|
||||
throw new Error(
|
||||
`Cannot delete event: used in ${usage.usedInSegments.length} segment(s) and ${usage.usedInWorkflows.length} workflow(s)`,
|
||||
);
|
||||
}
|
||||
|
||||
// Delete all events with this name
|
||||
const result = await prisma.event.deleteMany({
|
||||
where: {
|
||||
projectId,
|
||||
name: eventName,
|
||||
},
|
||||
});
|
||||
|
||||
return {deletedCount: result.count};
|
||||
}
|
||||
|
||||
/**
|
||||
* Trigger workflows based on an event
|
||||
* Uses Redis caching for enabled workflows to improve performance
|
||||
@@ -321,113 +455,10 @@ export class EventService {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if an event is used in any segments or workflows
|
||||
* Returns usage information including which segments/workflows use the event
|
||||
*
|
||||
* @param projectId - The project ID
|
||||
* @param eventName - The event name to check (e.g., "purchase.completed", "user.signup")
|
||||
* @returns Usage information
|
||||
*/
|
||||
public static async getEventUsage(
|
||||
projectId: string,
|
||||
eventName: string,
|
||||
): Promise<{
|
||||
usedInSegments: Array<{id: string; name: string}>;
|
||||
usedInWorkflows: Array<{id: string; name: string}>;
|
||||
totalCount: number;
|
||||
uniqueContacts: number;
|
||||
canDelete: boolean;
|
||||
}> {
|
||||
// Get all segments for the project
|
||||
const segments = await prisma.segment.findMany({
|
||||
where: {projectId},
|
||||
select: {id: true, name: true, condition: true},
|
||||
});
|
||||
|
||||
// Check which segments use this event
|
||||
const usedInSegments = segments.filter(segment => {
|
||||
const condition = segment.condition as any;
|
||||
return this.eventUsedInCondition(eventName, condition);
|
||||
});
|
||||
|
||||
// Get workflows that use this event as a trigger or wait condition
|
||||
const workflows = await prisma.workflow.findMany({
|
||||
where: {
|
||||
projectId,
|
||||
OR: [
|
||||
// Event as trigger
|
||||
{
|
||||
triggerType: 'EVENT',
|
||||
triggerConfig: {
|
||||
path: ['eventName'],
|
||||
equals: eventName,
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
select: {id: true, name: true},
|
||||
});
|
||||
|
||||
// Also check workflow steps that wait for events
|
||||
const workflowStepsWithEvent = await prisma.workflowStep.findMany({
|
||||
where: {
|
||||
workflow: {projectId},
|
||||
type: 'WAIT_FOR_EVENT',
|
||||
config: {
|
||||
path: ['eventName'],
|
||||
equals: eventName,
|
||||
},
|
||||
},
|
||||
include: {
|
||||
workflow: {
|
||||
select: {id: true, name: true},
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
const usedInWorkflows = [
|
||||
...workflows,
|
||||
...workflowStepsWithEvent.map(step => step.workflow),
|
||||
].reduce(
|
||||
(acc, workflow) => {
|
||||
// Deduplicate by id
|
||||
if (!acc.find((w: {id: string; name: string}) => w.id === workflow.id)) {
|
||||
acc.push(workflow);
|
||||
}
|
||||
return acc;
|
||||
},
|
||||
[] as Array<{id: string; name: string}>,
|
||||
);
|
||||
|
||||
// Get event statistics
|
||||
const [totalCount, uniqueContacts] = await Promise.all([
|
||||
prisma.event.count({
|
||||
where: {projectId, name: eventName},
|
||||
}),
|
||||
prisma.event
|
||||
.groupBy({
|
||||
by: ['contactId'],
|
||||
where: {projectId, name: eventName, contactId: {not: null}},
|
||||
})
|
||||
.then(results => results.length),
|
||||
]);
|
||||
|
||||
const canDelete = usedInSegments.length === 0 && usedInWorkflows.length === 0;
|
||||
|
||||
return {
|
||||
usedInSegments,
|
||||
usedInWorkflows,
|
||||
totalCount,
|
||||
uniqueContacts,
|
||||
canDelete,
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Helper: Check if an event is used in a filter condition (recursive)
|
||||
*/
|
||||
private static eventUsedInCondition(eventName: string, condition: any): boolean {
|
||||
private static eventUsedInCondition(eventName: string, condition: FilterCondition | null): boolean {
|
||||
if (!condition || typeof condition !== 'object') {
|
||||
return false;
|
||||
}
|
||||
@@ -447,7 +478,7 @@ export class EventService {
|
||||
/**
|
||||
* Helper: Check if an event is used in a filter group (recursive)
|
||||
*/
|
||||
private static eventUsedInGroup(eventName: string, group: any): boolean {
|
||||
private static eventUsedInGroup(eventName: string, group: FilterGroup): boolean {
|
||||
if (!group || typeof group !== 'object') {
|
||||
return false;
|
||||
}
|
||||
@@ -469,37 +500,4 @@ export class EventService {
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Delete all events with a specific name
|
||||
* WARNING: This is destructive and cannot be undone
|
||||
* Should only be called after verifying the event is not in use
|
||||
*
|
||||
* @param projectId - The project ID
|
||||
* @param eventName - The event name to delete
|
||||
*/
|
||||
public static async deleteEvent(projectId: string, eventName: string): Promise<{deletedCount: number}> {
|
||||
// Prevent deletion of system events
|
||||
if (eventName.startsWith('email.') || eventName.startsWith('segment.')) {
|
||||
throw new Error('Cannot delete system events (email.* or segment.*)');
|
||||
}
|
||||
|
||||
// Check if event is in use
|
||||
const usage = await this.getEventUsage(projectId, eventName);
|
||||
if (!usage.canDelete) {
|
||||
throw new Error(
|
||||
`Cannot delete event: used in ${usage.usedInSegments.length} segment(s) and ${usage.usedInWorkflows.length} workflow(s)`,
|
||||
);
|
||||
}
|
||||
|
||||
// Delete all events with this name
|
||||
const result = await prisma.event.deleteMany({
|
||||
where: {
|
||||
projectId,
|
||||
name: eventName,
|
||||
},
|
||||
});
|
||||
|
||||
return {deletedCount: result.count};
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import {describe, it, expect, beforeEach, vi, afterEach} from 'vitest';
|
||||
import {WorkflowTriggerType, WorkflowExecutionStatus} from '@plunk/db';
|
||||
import {afterEach, beforeEach, describe, expect, it, vi} from 'vitest';
|
||||
import {WorkflowExecutionStatus, WorkflowTriggerType} from '@plunk/db';
|
||||
import {EventService} from '../EventService';
|
||||
import {factories, getPrismaClient} from '../../../../../test/helpers';
|
||||
|
||||
@@ -59,7 +59,7 @@ describe('EventService', () => {
|
||||
// Clear Redis mock
|
||||
const {redis} = await import('../../database/redis');
|
||||
if ('clear' in redis) {
|
||||
(redis as any).clear();
|
||||
(redis as unknown as {clear: () => void}).clear();
|
||||
}
|
||||
});
|
||||
|
||||
@@ -441,7 +441,6 @@ describe('EventService', () => {
|
||||
const contact = await factories.createContact({projectId});
|
||||
|
||||
const oldDate = new Date('2024-01-01');
|
||||
const recentDate = new Date('2024-06-01');
|
||||
|
||||
// Create old event directly
|
||||
await prisma.event.create({
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { describe, it, expect, beforeEach } from 'vitest';
|
||||
import { SegmentService } from '../SegmentService';
|
||||
import { factories, getPrismaClient } from '../../../../../test/helpers';
|
||||
import {beforeEach, describe, expect, it} from 'vitest';
|
||||
import {SegmentService} from '../SegmentService';
|
||||
import {factories} from '../../../../../test/helpers';
|
||||
|
||||
/**
|
||||
* Comprehensive Operator Tests for Segment Filtering
|
||||
@@ -16,10 +16,9 @@ import { factories, getPrismaClient } from '../../../../../test/helpers';
|
||||
*/
|
||||
describe('SegmentService - Comprehensive Operator Tests', () => {
|
||||
let projectId: string;
|
||||
const prisma = getPrismaClient();
|
||||
|
||||
beforeEach(async () => {
|
||||
const { project } = await factories.createUserWithProject();
|
||||
const {project} = await factories.createUserWithProject();
|
||||
projectId = project.id;
|
||||
});
|
||||
|
||||
@@ -31,15 +30,15 @@ describe('SegmentService - Comprehensive Operator Tests', () => {
|
||||
it('should match exact string values in JSON data fields', async () => {
|
||||
const match = await factories.createContact({
|
||||
projectId,
|
||||
data: { plan: 'premium' },
|
||||
data: {plan: 'premium'},
|
||||
});
|
||||
await factories.createContact({
|
||||
projectId,
|
||||
data: { plan: 'basic' },
|
||||
data: {plan: 'basic'},
|
||||
});
|
||||
|
||||
const segment = await factories.createSegment(projectId, {
|
||||
filters: [{ field: 'data.plan', operator: 'equals', value: 'premium' }],
|
||||
filters: [{field: 'data.plan', operator: 'equals', value: 'premium'}],
|
||||
});
|
||||
|
||||
const result = await SegmentService.getContacts(projectId, segment.id);
|
||||
@@ -58,7 +57,7 @@ describe('SegmentService - Comprehensive Operator Tests', () => {
|
||||
});
|
||||
|
||||
const segment = await factories.createSegment(projectId, {
|
||||
filters: [{ field: 'email', operator: 'equals', value: '[email protected]' }],
|
||||
filters: [{field: 'email', operator: 'equals', value: '[email protected]'}],
|
||||
});
|
||||
|
||||
const result = await SegmentService.getContacts(projectId, segment.id);
|
||||
@@ -77,7 +76,7 @@ describe('SegmentService - Comprehensive Operator Tests', () => {
|
||||
});
|
||||
|
||||
const segment = await factories.createSegment(projectId, {
|
||||
filters: [{ field: 'subscribed', operator: 'equals', value: true }],
|
||||
filters: [{field: 'subscribed', operator: 'equals', value: true}],
|
||||
});
|
||||
|
||||
const result = await SegmentService.getContacts(projectId, segment.id);
|
||||
@@ -88,15 +87,15 @@ describe('SegmentService - Comprehensive Operator Tests', () => {
|
||||
it('should match numeric values as strings in JSON fields', async () => {
|
||||
const match = await factories.createContact({
|
||||
projectId,
|
||||
data: { userId: '12345' },
|
||||
data: {userId: '12345'},
|
||||
});
|
||||
await factories.createContact({
|
||||
projectId,
|
||||
data: { userId: '67890' },
|
||||
data: {userId: '67890'},
|
||||
});
|
||||
|
||||
const segment = await factories.createSegment(projectId, {
|
||||
filters: [{ field: 'data.userId', operator: 'equals', value: '12345' }],
|
||||
filters: [{field: 'data.userId', operator: 'equals', value: '12345'}],
|
||||
});
|
||||
|
||||
const result = await SegmentService.getContacts(projectId, segment.id);
|
||||
@@ -109,15 +108,15 @@ describe('SegmentService - Comprehensive Operator Tests', () => {
|
||||
it('should exclude exact matches in JSON data fields', async () => {
|
||||
const match = await factories.createContact({
|
||||
projectId,
|
||||
data: { plan: 'premium' },
|
||||
data: {plan: 'premium'},
|
||||
});
|
||||
await factories.createContact({
|
||||
projectId,
|
||||
data: { plan: 'basic' },
|
||||
data: {plan: 'basic'},
|
||||
});
|
||||
|
||||
const segment = await factories.createSegment(projectId, {
|
||||
filters: [{ field: 'data.plan', operator: 'notEquals', value: 'basic' }],
|
||||
filters: [{field: 'data.plan', operator: 'notEquals', value: 'basic'}],
|
||||
});
|
||||
|
||||
const result = await SegmentService.getContacts(projectId, segment.id);
|
||||
@@ -136,7 +135,7 @@ describe('SegmentService - Comprehensive Operator Tests', () => {
|
||||
});
|
||||
|
||||
const segment = await factories.createSegment(projectId, {
|
||||
filters: [{ field: 'subscribed', operator: 'notEquals', value: false }],
|
||||
filters: [{field: 'subscribed', operator: 'notEquals', value: false}],
|
||||
});
|
||||
|
||||
const result = await SegmentService.getContacts(projectId, segment.id);
|
||||
@@ -147,23 +146,23 @@ describe('SegmentService - Comprehensive Operator Tests', () => {
|
||||
it('should NOT include contacts where field does not exist (only excludes matching values)', async () => {
|
||||
const withMatchingField = await factories.createContact({
|
||||
projectId,
|
||||
data: { plan: 'basic' },
|
||||
data: {plan: 'basic'},
|
||||
});
|
||||
const withDifferentValue = await factories.createContact({
|
||||
projectId,
|
||||
data: { plan: 'premium' },
|
||||
data: {plan: 'premium'},
|
||||
});
|
||||
const withoutField = await factories.createContact({
|
||||
projectId,
|
||||
data: { other: 'value' },
|
||||
data: {other: 'value'},
|
||||
});
|
||||
|
||||
const segment = await factories.createSegment(projectId, {
|
||||
filters: [{ field: 'data.plan', operator: 'notEquals', value: 'basic' }],
|
||||
filters: [{field: 'data.plan', operator: 'notEquals', value: 'basic'}],
|
||||
});
|
||||
|
||||
const result = await SegmentService.getContacts(projectId, segment.id);
|
||||
const ids = result.contacts.map((c) => c.id);
|
||||
const ids = result.contacts.map(c => c.id);
|
||||
// notEquals only matches where field exists and has different value
|
||||
expect(ids).toContain(withDifferentValue.id);
|
||||
expect(ids).not.toContain(withMatchingField.id);
|
||||
@@ -175,15 +174,15 @@ describe('SegmentService - Comprehensive Operator Tests', () => {
|
||||
it('should match substring in JSON data fields', async () => {
|
||||
const match = await factories.createContact({
|
||||
projectId,
|
||||
data: { company: 'Acme Corporation' },
|
||||
data: {company: 'Acme Corporation'},
|
||||
});
|
||||
await factories.createContact({
|
||||
projectId,
|
||||
data: { company: 'Other Industries' },
|
||||
data: {company: 'Other Industries'},
|
||||
});
|
||||
|
||||
const segment = await factories.createSegment(projectId, {
|
||||
filters: [{ field: 'data.company', operator: 'contains', value: 'Acme' }],
|
||||
filters: [{field: 'data.company', operator: 'contains', value: 'Acme'}],
|
||||
});
|
||||
|
||||
const result = await SegmentService.getContacts(projectId, segment.id);
|
||||
@@ -206,11 +205,11 @@ describe('SegmentService - Comprehensive Operator Tests', () => {
|
||||
});
|
||||
|
||||
const segment = await factories.createSegment(projectId, {
|
||||
filters: [{ field: 'email', operator: 'contains', value: 'company' }],
|
||||
filters: [{field: 'email', operator: 'contains', value: 'company'}],
|
||||
});
|
||||
|
||||
const result = await SegmentService.getContacts(projectId, segment.id);
|
||||
const ids = result.contacts.map((c) => c.id);
|
||||
const ids = result.contacts.map(c => c.id);
|
||||
expect(ids).toContain(match1.id);
|
||||
expect(ids).toContain(match2.id);
|
||||
expect(result.contacts).toHaveLength(2);
|
||||
@@ -219,11 +218,11 @@ describe('SegmentService - Comprehensive Operator Tests', () => {
|
||||
it('should not match when field does not exist', async () => {
|
||||
await factories.createContact({
|
||||
projectId,
|
||||
data: { other: 'value' },
|
||||
data: {other: 'value'},
|
||||
});
|
||||
|
||||
const segment = await factories.createSegment(projectId, {
|
||||
filters: [{ field: 'data.company', operator: 'contains', value: 'Acme' }],
|
||||
filters: [{field: 'data.company', operator: 'contains', value: 'Acme'}],
|
||||
});
|
||||
|
||||
const result = await SegmentService.getContacts(projectId, segment.id);
|
||||
@@ -241,7 +240,7 @@ describe('SegmentService - Comprehensive Operator Tests', () => {
|
||||
});
|
||||
|
||||
const segment = await factories.createSegment(projectId, {
|
||||
filters: [{ field: 'email', operator: 'contains', value: 'gmail' }],
|
||||
filters: [{field: 'email', operator: 'contains', value: 'gmail'}],
|
||||
});
|
||||
|
||||
const result = await SegmentService.getContacts(projectId, segment.id);
|
||||
@@ -254,15 +253,15 @@ describe('SegmentService - Comprehensive Operator Tests', () => {
|
||||
it('should exclude substring matches in JSON data fields', async () => {
|
||||
const match = await factories.createContact({
|
||||
projectId,
|
||||
data: { company: 'Other Industries' },
|
||||
data: {company: 'Other Industries'},
|
||||
});
|
||||
await factories.createContact({
|
||||
projectId,
|
||||
data: { company: 'Acme Corporation' },
|
||||
data: {company: 'Acme Corporation'},
|
||||
});
|
||||
|
||||
const segment = await factories.createSegment(projectId, {
|
||||
filters: [{ field: 'data.company', operator: 'notContains', value: 'Acme' }],
|
||||
filters: [{field: 'data.company', operator: 'notContains', value: 'Acme'}],
|
||||
});
|
||||
|
||||
const result = await SegmentService.getContacts(projectId, segment.id);
|
||||
@@ -273,23 +272,23 @@ describe('SegmentService - Comprehensive Operator Tests', () => {
|
||||
it('should NOT include contacts where field does not exist (only excludes matching substrings)', async () => {
|
||||
const withoutField = await factories.createContact({
|
||||
projectId,
|
||||
data: { other: 'value' },
|
||||
data: {other: 'value'},
|
||||
});
|
||||
const withMatchingSubstring = await factories.createContact({
|
||||
projectId,
|
||||
data: { company: 'Acme Corporation' },
|
||||
data: {company: 'Acme Corporation'},
|
||||
});
|
||||
const withDifferentValue = await factories.createContact({
|
||||
projectId,
|
||||
data: { company: 'Other Industries' },
|
||||
data: {company: 'Other Industries'},
|
||||
});
|
||||
|
||||
const segment = await factories.createSegment(projectId, {
|
||||
filters: [{ field: 'data.company', operator: 'notContains', value: 'Acme' }],
|
||||
filters: [{field: 'data.company', operator: 'notContains', value: 'Acme'}],
|
||||
});
|
||||
|
||||
const result = await SegmentService.getContacts(projectId, segment.id);
|
||||
const ids = result.contacts.map((c) => c.id);
|
||||
const ids = result.contacts.map(c => c.id);
|
||||
// notContains only matches where field exists and doesn't contain substring
|
||||
expect(ids).toContain(withDifferentValue.id);
|
||||
expect(ids).not.toContain(withMatchingSubstring.id);
|
||||
@@ -311,7 +310,7 @@ describe('SegmentService - Comprehensive Operator Tests', () => {
|
||||
});
|
||||
|
||||
const segment = await factories.createSegment(projectId, {
|
||||
filters: [{ field: 'email', operator: 'notContains', value: 'gmail' }],
|
||||
filters: [{field: 'email', operator: 'notContains', value: 'gmail'}],
|
||||
});
|
||||
|
||||
const result = await SegmentService.getContacts(projectId, segment.id);
|
||||
@@ -329,23 +328,23 @@ describe('SegmentService - Comprehensive Operator Tests', () => {
|
||||
it('should match values greater than threshold', async () => {
|
||||
const high = await factories.createContact({
|
||||
projectId,
|
||||
data: { score: 100 },
|
||||
data: {score: 100},
|
||||
});
|
||||
const veryHigh = await factories.createContact({
|
||||
projectId,
|
||||
data: { score: 200 },
|
||||
data: {score: 200},
|
||||
});
|
||||
await factories.createContact({
|
||||
projectId,
|
||||
data: { score: 50 },
|
||||
data: {score: 50},
|
||||
});
|
||||
|
||||
const segment = await factories.createSegment(projectId, {
|
||||
filters: [{ field: 'data.score', operator: 'greaterThan', value: 50 }],
|
||||
filters: [{field: 'data.score', operator: 'greaterThan', value: 50}],
|
||||
});
|
||||
|
||||
const result = await SegmentService.getContacts(projectId, segment.id);
|
||||
const ids = result.contacts.map((c) => c.id);
|
||||
const ids = result.contacts.map(c => c.id);
|
||||
expect(ids).toContain(high.id);
|
||||
expect(ids).toContain(veryHigh.id);
|
||||
expect(result.contacts).toHaveLength(2);
|
||||
@@ -354,11 +353,11 @@ describe('SegmentService - Comprehensive Operator Tests', () => {
|
||||
it('should exclude values equal to threshold', async () => {
|
||||
await factories.createContact({
|
||||
projectId,
|
||||
data: { score: 50 },
|
||||
data: {score: 50},
|
||||
});
|
||||
|
||||
const segment = await factories.createSegment(projectId, {
|
||||
filters: [{ field: 'data.score', operator: 'greaterThan', value: 50 }],
|
||||
filters: [{field: 'data.score', operator: 'greaterThan', value: 50}],
|
||||
});
|
||||
|
||||
const result = await SegmentService.getContacts(projectId, segment.id);
|
||||
@@ -368,15 +367,15 @@ describe('SegmentService - Comprehensive Operator Tests', () => {
|
||||
it('should work with negative numbers', async () => {
|
||||
const match = await factories.createContact({
|
||||
projectId,
|
||||
data: { temperature: 5 },
|
||||
data: {temperature: 5},
|
||||
});
|
||||
await factories.createContact({
|
||||
projectId,
|
||||
data: { temperature: -10 },
|
||||
data: {temperature: -10},
|
||||
});
|
||||
|
||||
const segment = await factories.createSegment(projectId, {
|
||||
filters: [{ field: 'data.temperature', operator: 'greaterThan', value: 0 }],
|
||||
filters: [{field: 'data.temperature', operator: 'greaterThan', value: 0}],
|
||||
});
|
||||
|
||||
const result = await SegmentService.getContacts(projectId, segment.id);
|
||||
@@ -387,15 +386,15 @@ describe('SegmentService - Comprehensive Operator Tests', () => {
|
||||
it('should work with decimal values', async () => {
|
||||
const match = await factories.createContact({
|
||||
projectId,
|
||||
data: { rating: 4.5 },
|
||||
data: {rating: 4.5},
|
||||
});
|
||||
await factories.createContact({
|
||||
projectId,
|
||||
data: { rating: 3.2 },
|
||||
data: {rating: 3.2},
|
||||
});
|
||||
|
||||
const segment = await factories.createSegment(projectId, {
|
||||
filters: [{ field: 'data.rating', operator: 'greaterThan', value: 4.0 }],
|
||||
filters: [{field: 'data.rating', operator: 'greaterThan', value: 4.0}],
|
||||
});
|
||||
|
||||
const result = await SegmentService.getContacts(projectId, segment.id);
|
||||
@@ -408,23 +407,23 @@ describe('SegmentService - Comprehensive Operator Tests', () => {
|
||||
it('should match values greater than or equal to threshold', async () => {
|
||||
const equal = await factories.createContact({
|
||||
projectId,
|
||||
data: { score: 50 },
|
||||
data: {score: 50},
|
||||
});
|
||||
const greater = await factories.createContact({
|
||||
projectId,
|
||||
data: { score: 100 },
|
||||
data: {score: 100},
|
||||
});
|
||||
await factories.createContact({
|
||||
projectId,
|
||||
data: { score: 25 },
|
||||
data: {score: 25},
|
||||
});
|
||||
|
||||
const segment = await factories.createSegment(projectId, {
|
||||
filters: [{ field: 'data.score', operator: 'greaterThanOrEqual', value: 50 }],
|
||||
filters: [{field: 'data.score', operator: 'greaterThanOrEqual', value: 50}],
|
||||
});
|
||||
|
||||
const result = await SegmentService.getContacts(projectId, segment.id);
|
||||
const ids = result.contacts.map((c) => c.id);
|
||||
const ids = result.contacts.map(c => c.id);
|
||||
expect(ids).toContain(equal.id);
|
||||
expect(ids).toContain(greater.id);
|
||||
expect(result.contacts).toHaveLength(2);
|
||||
@@ -435,23 +434,23 @@ describe('SegmentService - Comprehensive Operator Tests', () => {
|
||||
it('should match values less than threshold', async () => {
|
||||
const low = await factories.createContact({
|
||||
projectId,
|
||||
data: { score: 25 },
|
||||
data: {score: 25},
|
||||
});
|
||||
const veryLow = await factories.createContact({
|
||||
projectId,
|
||||
data: { score: 10 },
|
||||
data: {score: 10},
|
||||
});
|
||||
await factories.createContact({
|
||||
projectId,
|
||||
data: { score: 50 },
|
||||
data: {score: 50},
|
||||
});
|
||||
|
||||
const segment = await factories.createSegment(projectId, {
|
||||
filters: [{ field: 'data.score', operator: 'lessThan', value: 50 }],
|
||||
filters: [{field: 'data.score', operator: 'lessThan', value: 50}],
|
||||
});
|
||||
|
||||
const result = await SegmentService.getContacts(projectId, segment.id);
|
||||
const ids = result.contacts.map((c) => c.id);
|
||||
const ids = result.contacts.map(c => c.id);
|
||||
expect(ids).toContain(low.id);
|
||||
expect(ids).toContain(veryLow.id);
|
||||
expect(result.contacts).toHaveLength(2);
|
||||
@@ -460,11 +459,11 @@ describe('SegmentService - Comprehensive Operator Tests', () => {
|
||||
it('should exclude values equal to threshold', async () => {
|
||||
await factories.createContact({
|
||||
projectId,
|
||||
data: { score: 50 },
|
||||
data: {score: 50},
|
||||
});
|
||||
|
||||
const segment = await factories.createSegment(projectId, {
|
||||
filters: [{ field: 'data.score', operator: 'lessThan', value: 50 }],
|
||||
filters: [{field: 'data.score', operator: 'lessThan', value: 50}],
|
||||
});
|
||||
|
||||
const result = await SegmentService.getContacts(projectId, segment.id);
|
||||
@@ -476,23 +475,23 @@ describe('SegmentService - Comprehensive Operator Tests', () => {
|
||||
it('should match values less than or equal to threshold', async () => {
|
||||
const equal = await factories.createContact({
|
||||
projectId,
|
||||
data: { score: 50 },
|
||||
data: {score: 50},
|
||||
});
|
||||
const less = await factories.createContact({
|
||||
projectId,
|
||||
data: { score: 25 },
|
||||
data: {score: 25},
|
||||
});
|
||||
await factories.createContact({
|
||||
projectId,
|
||||
data: { score: 100 },
|
||||
data: {score: 100},
|
||||
});
|
||||
|
||||
const segment = await factories.createSegment(projectId, {
|
||||
filters: [{ field: 'data.score', operator: 'lessThanOrEqual', value: 50 }],
|
||||
filters: [{field: 'data.score', operator: 'lessThanOrEqual', value: 50}],
|
||||
});
|
||||
|
||||
const result = await SegmentService.getContacts(projectId, segment.id);
|
||||
const ids = result.contacts.map((c) => c.id);
|
||||
const ids = result.contacts.map(c => c.id);
|
||||
expect(ids).toContain(equal.id);
|
||||
expect(ids).toContain(less.id);
|
||||
expect(result.contacts).toHaveLength(2);
|
||||
@@ -501,17 +500,18 @@ describe('SegmentService - Comprehensive Operator Tests', () => {
|
||||
|
||||
describe('Numeric edge cases', () => {
|
||||
it('should handle zero values correctly', async () => {
|
||||
const zero = await factories.createContact({
|
||||
// Create a contact with balance 0 (should NOT match greaterThan 0)
|
||||
await factories.createContact({
|
||||
projectId,
|
||||
data: { balance: 0 },
|
||||
data: {balance: 0},
|
||||
});
|
||||
const positive = await factories.createContact({
|
||||
projectId,
|
||||
data: { balance: 100 },
|
||||
data: {balance: 100},
|
||||
});
|
||||
|
||||
const segment = await factories.createSegment(projectId, {
|
||||
filters: [{ field: 'data.balance', operator: 'greaterThan', value: 0 }],
|
||||
filters: [{field: 'data.balance', operator: 'greaterThan', value: 0}],
|
||||
});
|
||||
|
||||
const result = await SegmentService.getContacts(projectId, segment.id);
|
||||
@@ -522,15 +522,15 @@ describe('SegmentService - Comprehensive Operator Tests', () => {
|
||||
it('should handle very large numbers', async () => {
|
||||
const match = await factories.createContact({
|
||||
projectId,
|
||||
data: { views: 1000000 },
|
||||
data: {views: 1000000},
|
||||
});
|
||||
await factories.createContact({
|
||||
projectId,
|
||||
data: { views: 500000 },
|
||||
data: {views: 500000},
|
||||
});
|
||||
|
||||
const segment = await factories.createSegment(projectId, {
|
||||
filters: [{ field: 'data.views', operator: 'greaterThanOrEqual', value: 1000000 }],
|
||||
filters: [{field: 'data.views', operator: 'greaterThanOrEqual', value: 1000000}],
|
||||
});
|
||||
|
||||
const result = await SegmentService.getContacts(projectId, segment.id);
|
||||
@@ -548,15 +548,15 @@ describe('SegmentService - Comprehensive Operator Tests', () => {
|
||||
it('should match contacts where field exists and is not null', async () => {
|
||||
const withField = await factories.createContact({
|
||||
projectId,
|
||||
data: { company: 'Acme Inc' },
|
||||
data: {company: 'Acme Inc'},
|
||||
});
|
||||
await factories.createContact({
|
||||
projectId,
|
||||
data: { name: 'John' },
|
||||
data: {name: 'John'},
|
||||
});
|
||||
|
||||
const segment = await factories.createSegment(projectId, {
|
||||
filters: [{ field: 'data.company', operator: 'exists', value: true }],
|
||||
filters: [{field: 'data.company', operator: 'exists', value: true}],
|
||||
});
|
||||
|
||||
const result = await SegmentService.getContacts(projectId, segment.id);
|
||||
@@ -567,15 +567,15 @@ describe('SegmentService - Comprehensive Operator Tests', () => {
|
||||
it('should exclude contacts where field is null', async () => {
|
||||
const withValue = await factories.createContact({
|
||||
projectId,
|
||||
data: { company: 'Acme Inc' },
|
||||
data: {company: 'Acme Inc'},
|
||||
});
|
||||
await factories.createContact({
|
||||
projectId,
|
||||
data: { company: null },
|
||||
data: {company: null},
|
||||
});
|
||||
|
||||
const segment = await factories.createSegment(projectId, {
|
||||
filters: [{ field: 'data.company', operator: 'exists', value: true }],
|
||||
filters: [{field: 'data.company', operator: 'exists', value: true}],
|
||||
});
|
||||
|
||||
const result = await SegmentService.getContacts(projectId, segment.id);
|
||||
@@ -586,11 +586,11 @@ describe('SegmentService - Comprehensive Operator Tests', () => {
|
||||
it('should match fields with empty string values', async () => {
|
||||
const withEmptyString = await factories.createContact({
|
||||
projectId,
|
||||
data: { notes: '' },
|
||||
data: {notes: ''},
|
||||
});
|
||||
|
||||
const segment = await factories.createSegment(projectId, {
|
||||
filters: [{ field: 'data.notes', operator: 'exists', value: true }],
|
||||
filters: [{field: 'data.notes', operator: 'exists', value: true}],
|
||||
});
|
||||
|
||||
const result = await SegmentService.getContacts(projectId, segment.id);
|
||||
@@ -601,11 +601,11 @@ describe('SegmentService - Comprehensive Operator Tests', () => {
|
||||
it('should match fields with zero values', async () => {
|
||||
const withZero = await factories.createContact({
|
||||
projectId,
|
||||
data: { score: 0 },
|
||||
data: {score: 0},
|
||||
});
|
||||
|
||||
const segment = await factories.createSegment(projectId, {
|
||||
filters: [{ field: 'data.score', operator: 'exists', value: true }],
|
||||
filters: [{field: 'data.score', operator: 'exists', value: true}],
|
||||
});
|
||||
|
||||
const result = await SegmentService.getContacts(projectId, segment.id);
|
||||
@@ -616,11 +616,11 @@ describe('SegmentService - Comprehensive Operator Tests', () => {
|
||||
it('should match fields with boolean false values', async () => {
|
||||
const withFalse = await factories.createContact({
|
||||
projectId,
|
||||
data: { verified: false },
|
||||
data: {verified: false},
|
||||
});
|
||||
|
||||
const segment = await factories.createSegment(projectId, {
|
||||
filters: [{ field: 'data.verified', operator: 'exists', value: true }],
|
||||
filters: [{field: 'data.verified', operator: 'exists', value: true}],
|
||||
});
|
||||
|
||||
const result = await SegmentService.getContacts(projectId, segment.id);
|
||||
@@ -633,15 +633,15 @@ describe('SegmentService - Comprehensive Operator Tests', () => {
|
||||
it('should match contacts where field does not exist', async () => {
|
||||
const withoutField = await factories.createContact({
|
||||
projectId,
|
||||
data: { name: 'John' },
|
||||
data: {name: 'John'},
|
||||
});
|
||||
await factories.createContact({
|
||||
projectId,
|
||||
data: { company: 'Acme Inc' },
|
||||
data: {company: 'Acme Inc'},
|
||||
});
|
||||
|
||||
const segment = await factories.createSegment(projectId, {
|
||||
filters: [{ field: 'data.company', operator: 'notExists', value: true }],
|
||||
filters: [{field: 'data.company', operator: 'notExists', value: true}],
|
||||
});
|
||||
|
||||
const result = await SegmentService.getContacts(projectId, segment.id);
|
||||
@@ -652,15 +652,15 @@ describe('SegmentService - Comprehensive Operator Tests', () => {
|
||||
it('should match contacts where field is null', async () => {
|
||||
const withNull = await factories.createContact({
|
||||
projectId,
|
||||
data: { company: null },
|
||||
data: {company: null},
|
||||
});
|
||||
await factories.createContact({
|
||||
projectId,
|
||||
data: { company: 'Acme Inc' },
|
||||
data: {company: 'Acme Inc'},
|
||||
});
|
||||
|
||||
const segment = await factories.createSegment(projectId, {
|
||||
filters: [{ field: 'data.company', operator: 'notExists', value: true }],
|
||||
filters: [{field: 'data.company', operator: 'notExists', value: true}],
|
||||
});
|
||||
|
||||
const result = await SegmentService.getContacts(projectId, segment.id);
|
||||
@@ -671,11 +671,11 @@ describe('SegmentService - Comprehensive Operator Tests', () => {
|
||||
it('should exclude fields with empty string values', async () => {
|
||||
await factories.createContact({
|
||||
projectId,
|
||||
data: { notes: '' },
|
||||
data: {notes: ''},
|
||||
});
|
||||
|
||||
const segment = await factories.createSegment(projectId, {
|
||||
filters: [{ field: 'data.notes', operator: 'notExists', value: true }],
|
||||
filters: [{field: 'data.notes', operator: 'notExists', value: true}],
|
||||
});
|
||||
|
||||
const result = await SegmentService.getContacts(projectId, segment.id);
|
||||
@@ -685,11 +685,11 @@ describe('SegmentService - Comprehensive Operator Tests', () => {
|
||||
it('should exclude fields with zero values', async () => {
|
||||
await factories.createContact({
|
||||
projectId,
|
||||
data: { score: 0 },
|
||||
data: {score: 0},
|
||||
});
|
||||
|
||||
const segment = await factories.createSegment(projectId, {
|
||||
filters: [{ field: 'data.score', operator: 'notExists', value: true }],
|
||||
filters: [{field: 'data.score', operator: 'notExists', value: true}],
|
||||
});
|
||||
|
||||
const result = await SegmentService.getContacts(projectId, segment.id);
|
||||
@@ -704,7 +704,7 @@ describe('SegmentService - Comprehensive Operator Tests', () => {
|
||||
describe('Temporal Operators', () => {
|
||||
describe('within operator', () => {
|
||||
it('should match contacts created within specified days', async () => {
|
||||
const recent = await factories.createContact({ projectId });
|
||||
const recent = await factories.createContact({projectId});
|
||||
|
||||
const segment = await factories.createSegment(projectId, {
|
||||
filters: [
|
||||
@@ -718,12 +718,12 @@ describe('SegmentService - Comprehensive Operator Tests', () => {
|
||||
});
|
||||
|
||||
const result = await SegmentService.getContacts(projectId, segment.id);
|
||||
const ids = result.contacts.map((c) => c.id);
|
||||
const ids = result.contacts.map(c => c.id);
|
||||
expect(ids).toContain(recent.id);
|
||||
});
|
||||
|
||||
it('should match contacts created within specified hours', async () => {
|
||||
const veryRecent = await factories.createContact({ projectId });
|
||||
const veryRecent = await factories.createContact({projectId});
|
||||
|
||||
const segment = await factories.createSegment(projectId, {
|
||||
filters: [
|
||||
@@ -737,12 +737,12 @@ describe('SegmentService - Comprehensive Operator Tests', () => {
|
||||
});
|
||||
|
||||
const result = await SegmentService.getContacts(projectId, segment.id);
|
||||
const ids = result.contacts.map((c) => c.id);
|
||||
const ids = result.contacts.map(c => c.id);
|
||||
expect(ids).toContain(veryRecent.id);
|
||||
});
|
||||
|
||||
it('should match contacts created within specified minutes', async () => {
|
||||
const justNow = await factories.createContact({ projectId });
|
||||
const justNow = await factories.createContact({projectId});
|
||||
|
||||
const segment = await factories.createSegment(projectId, {
|
||||
filters: [
|
||||
@@ -756,7 +756,7 @@ describe('SegmentService - Comprehensive Operator Tests', () => {
|
||||
});
|
||||
|
||||
const result = await SegmentService.getContacts(projectId, segment.id);
|
||||
const ids = result.contacts.map((c) => c.id);
|
||||
const ids = result.contacts.map(c => c.id);
|
||||
expect(ids).toContain(justNow.id);
|
||||
});
|
||||
});
|
||||
@@ -767,9 +767,9 @@ describe('SegmentService - Comprehensive Operator Tests', () => {
|
||||
// ========================================
|
||||
describe('Date Comparison Operators', () => {
|
||||
it('should support greaterThan for dates', async () => {
|
||||
const older = await factories.createContact({ projectId });
|
||||
await new Promise((resolve) => setTimeout(resolve, 10));
|
||||
const newer = await factories.createContact({ projectId });
|
||||
const older = await factories.createContact({projectId});
|
||||
await new Promise(resolve => setTimeout(resolve, 10));
|
||||
const newer = await factories.createContact({projectId});
|
||||
|
||||
const segment = await factories.createSegment(projectId, {
|
||||
filters: [
|
||||
@@ -782,17 +782,17 @@ describe('SegmentService - Comprehensive Operator Tests', () => {
|
||||
});
|
||||
|
||||
const result = await SegmentService.getContacts(projectId, segment.id);
|
||||
const ids = result.contacts.map((c) => c.id);
|
||||
const ids = result.contacts.map(c => c.id);
|
||||
expect(ids).toContain(newer.id);
|
||||
expect(ids).not.toContain(older.id);
|
||||
});
|
||||
|
||||
it('should support lessThanOrEqual for dates', async () => {
|
||||
const first = await factories.createContact({ projectId });
|
||||
await new Promise((resolve) => setTimeout(resolve, 10));
|
||||
const second = await factories.createContact({ projectId });
|
||||
await new Promise((resolve) => setTimeout(resolve, 10));
|
||||
const third = await factories.createContact({ projectId });
|
||||
const first = await factories.createContact({projectId});
|
||||
await new Promise(resolve => setTimeout(resolve, 10));
|
||||
const second = await factories.createContact({projectId});
|
||||
await new Promise(resolve => setTimeout(resolve, 10));
|
||||
const third = await factories.createContact({projectId});
|
||||
|
||||
const segment = await factories.createSegment(projectId, {
|
||||
filters: [
|
||||
@@ -805,7 +805,7 @@ describe('SegmentService - Comprehensive Operator Tests', () => {
|
||||
});
|
||||
|
||||
const result = await SegmentService.getContacts(projectId, segment.id);
|
||||
const ids = result.contacts.map((c) => c.id);
|
||||
const ids = result.contacts.map(c => c.id);
|
||||
expect(ids).toContain(first.id);
|
||||
expect(ids).toContain(second.id);
|
||||
expect(ids).not.toContain(third.id);
|
||||
@@ -830,27 +830,27 @@ describe('SegmentService - Comprehensive Operator Tests', () => {
|
||||
await factories.createContact({
|
||||
projectId,
|
||||
subscribed: false,
|
||||
data: { plan: 'premium', score: 85, company: 'Acme Inc' },
|
||||
data: {plan: 'premium', score: 85, company: 'Acme Inc'},
|
||||
});
|
||||
|
||||
await factories.createContact({
|
||||
projectId,
|
||||
subscribed: true,
|
||||
data: { plan: 'basic', score: 85, company: 'Acme Inc' },
|
||||
data: {plan: 'basic', score: 85, company: 'Acme Inc'},
|
||||
});
|
||||
|
||||
await factories.createContact({
|
||||
projectId,
|
||||
subscribed: true,
|
||||
data: { plan: 'premium', score: 50, company: 'Acme Inc' },
|
||||
data: {plan: 'premium', score: 50, company: 'Acme Inc'},
|
||||
});
|
||||
|
||||
const segment = await factories.createSegment(projectId, {
|
||||
filters: [
|
||||
{ field: 'subscribed', operator: 'equals', value: true },
|
||||
{ field: 'data.plan', operator: 'equals', value: 'premium' },
|
||||
{ field: 'data.score', operator: 'greaterThanOrEqual', value: 80 },
|
||||
{ field: 'data.company', operator: 'contains', value: 'Acme' },
|
||||
{field: 'subscribed', operator: 'equals', value: true},
|
||||
{field: 'data.plan', operator: 'equals', value: 'premium'},
|
||||
{field: 'data.score', operator: 'greaterThanOrEqual', value: 80},
|
||||
{field: 'data.company', operator: 'contains', value: 'Acme'},
|
||||
],
|
||||
});
|
||||
|
||||
@@ -870,18 +870,18 @@ describe('SegmentService - Comprehensive Operator Tests', () => {
|
||||
|
||||
await factories.createContact({
|
||||
projectId,
|
||||
data: { company: 'Tech Corp' }, // Missing revenue
|
||||
data: {company: 'Tech Corp'}, // Missing revenue
|
||||
});
|
||||
|
||||
await factories.createContact({
|
||||
projectId,
|
||||
data: { revenue: 100000 }, // Missing company
|
||||
data: {revenue: 100000}, // Missing company
|
||||
});
|
||||
|
||||
const segment = await factories.createSegment(projectId, {
|
||||
filters: [
|
||||
{ field: 'data.company', operator: 'exists', value: true },
|
||||
{ field: 'data.revenue', operator: 'greaterThanOrEqual', value: 100000 },
|
||||
{field: 'data.company', operator: 'exists', value: true},
|
||||
{field: 'data.revenue', operator: 'greaterThanOrEqual', value: 100000},
|
||||
],
|
||||
});
|
||||
|
||||
|
||||
@@ -1,7 +1,10 @@
|
||||
import {describe, it, expect, beforeEach} from 'vitest';
|
||||
import {SegmentService} from '../SegmentService';
|
||||
import {beforeEach, describe, expect, it} from 'vitest';
|
||||
import {type SegmentFilter, SegmentService} from '../SegmentService';
|
||||
import {factories, getPrismaClient} from '../../../../../test/helpers';
|
||||
|
||||
// Type for intentionally invalid filter input (used for testing validation)
|
||||
type InvalidFilterInput = Partial<SegmentFilter>;
|
||||
|
||||
describe('SegmentService', () => {
|
||||
let projectId: string;
|
||||
const prisma = getPrismaClient();
|
||||
@@ -395,10 +398,10 @@ describe('SegmentService', () => {
|
||||
name: 'Invalid Segment',
|
||||
filters: [
|
||||
{
|
||||
// Intentionally missing field, cast to any to bypass compile-time validation
|
||||
// Intentionally missing field, cast to bypass compile-time validation
|
||||
operator: 'equals',
|
||||
value: 'test',
|
||||
} as any,
|
||||
} as InvalidFilterInput as SegmentFilter,
|
||||
],
|
||||
}),
|
||||
).rejects.toThrow(/field is required/i);
|
||||
@@ -411,10 +414,10 @@ describe('SegmentService', () => {
|
||||
filters: [
|
||||
{
|
||||
field: 'email',
|
||||
// Intentionally invalid operator, cast to any
|
||||
// Intentionally invalid operator
|
||||
operator: 'DROP TABLE contacts;',
|
||||
value: 'test',
|
||||
} as any,
|
||||
} as InvalidFilterInput as SegmentFilter,
|
||||
],
|
||||
}),
|
||||
).rejects.toThrow(/invalid operator/i);
|
||||
@@ -428,8 +431,8 @@ describe('SegmentService', () => {
|
||||
{
|
||||
field: 'email',
|
||||
operator: 'equals',
|
||||
// Value intentionally omitted, cast to any
|
||||
} as any,
|
||||
// Value intentionally omitted
|
||||
} as InvalidFilterInput as SegmentFilter,
|
||||
],
|
||||
}),
|
||||
).rejects.toThrow(/requires a value/i);
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import {describe, it, expect, beforeEach, vi} from 'vitest';
|
||||
import {WorkflowStepType, StepExecutionStatus, WorkflowExecutionStatus} from '@plunk/db';
|
||||
import {beforeEach, describe, expect, it, vi} from 'vitest';
|
||||
import {WorkflowExecutionStatus, WorkflowStepType} from '@plunk/db';
|
||||
import {WorkflowExecutionService} from '../WorkflowExecutionService';
|
||||
import {factories, getPrismaClient} from '../../../../../test/helpers';
|
||||
|
||||
@@ -124,7 +124,8 @@ describe('Workflow CONDITION Step - Comprehensive Operator Tests', () => {
|
||||
},
|
||||
});
|
||||
|
||||
return (stepExecution?.output as any)?.branch || 'unknown';
|
||||
const output = stepExecution?.output as {branch?: string} | null;
|
||||
return output?.branch || 'unknown';
|
||||
}
|
||||
|
||||
// ========================================
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import {describe, it, expect, beforeEach, vi} from 'vitest';
|
||||
import {WorkflowStepType, StepExecutionStatus, WorkflowExecutionStatus, TemplateType, Prisma} from '@plunk/db';
|
||||
import {beforeEach, describe, expect, it, vi} from 'vitest';
|
||||
import {Prisma, StepExecutionStatus, WorkflowExecutionStatus, WorkflowStepType} from '@plunk/db';
|
||||
import {WorkflowExecutionService} from '../WorkflowExecutionService';
|
||||
import {factories, getPrismaClient} from '../../../../../test/helpers';
|
||||
|
||||
@@ -140,7 +140,7 @@ describe('WorkflowExecutionService - Integration Tests', () => {
|
||||
const conditionExec = stepExecutions.find(se => se.step.type === WorkflowStepType.CONDITION);
|
||||
expect(conditionExec).toBeDefined();
|
||||
expect(conditionExec?.status).toBe(StepExecutionStatus.COMPLETED);
|
||||
expect((conditionExec?.output as any)?.branch).toBe('yes');
|
||||
expect((conditionExec?.output as {branch?: string} | null)?.branch).toBe('yes');
|
||||
});
|
||||
|
||||
it('should follow NO branch when condition evaluates to false', async () => {
|
||||
@@ -229,7 +229,7 @@ describe('WorkflowExecutionService - Integration Tests', () => {
|
||||
|
||||
const conditionExec = stepExecutions.find(se => se.step.type === WorkflowStepType.CONDITION);
|
||||
expect(conditionExec).toBeDefined();
|
||||
expect((conditionExec?.output as any)?.branch).toBe('no');
|
||||
expect((conditionExec?.output as {branch?: string} | null)?.branch).toBe('no');
|
||||
});
|
||||
|
||||
it('should handle complex nested conditions', async () => {
|
||||
@@ -356,8 +356,8 @@ describe('WorkflowExecutionService - Integration Tests', () => {
|
||||
expect(stepExecutions.length).toBeGreaterThanOrEqual(3);
|
||||
const conditions = stepExecutions.filter(se => se.step.type === WorkflowStepType.CONDITION);
|
||||
expect(conditions).toHaveLength(2);
|
||||
expect((conditions[0].output as any)?.branch).toBe('yes'); // US = yes
|
||||
expect((conditions[1].output as any)?.branch).toBe('yes'); // Premium = yes
|
||||
expect((conditions[0].output as {branch?: string} | null)?.branch).toBe('yes'); // US = yes
|
||||
expect((conditions[1].output as {branch?: string} | null)?.branch).toBe('yes'); // Premium = yes
|
||||
});
|
||||
});
|
||||
|
||||
@@ -695,7 +695,7 @@ describe('WorkflowExecutionService - Integration Tests', () => {
|
||||
// Should have: TRIGGER, CONDITION, and Path A (since segment = 'A')
|
||||
expect(stepExecutions.length).toBeGreaterThanOrEqual(2);
|
||||
const conditionExec = stepExecutions.find(se => se.step.type === WorkflowStepType.CONDITION);
|
||||
expect((conditionExec?.output as any)?.branch).toBe('yes');
|
||||
expect((conditionExec?.output as {branch?: string} | null)?.branch).toBe('yes');
|
||||
});
|
||||
});
|
||||
|
||||
@@ -813,7 +813,7 @@ describe('WorkflowExecutionService - Integration Tests', () => {
|
||||
});
|
||||
|
||||
expect(conditionExec?.status).toBe(StepExecutionStatus.COMPLETED);
|
||||
expect((conditionExec?.output as any)?.branch).toBe('no');
|
||||
expect((conditionExec?.output as {branch?: string} | null)?.branch).toBe('no');
|
||||
});
|
||||
});
|
||||
|
||||
@@ -1078,7 +1078,11 @@ describe('WorkflowExecutionService - Integration Tests', () => {
|
||||
data: {fromStepId: triggerStep!.id, toStepId: conditionStep.id},
|
||||
});
|
||||
await prisma.workflowTransition.create({
|
||||
data: {fromStepId: conditionStep.id, toStepId: exitStep.id, condition: {branch: 'yes'} as Prisma.InputJsonValue},
|
||||
data: {
|
||||
fromStepId: conditionStep.id,
|
||||
toStepId: exitStep.id,
|
||||
condition: {branch: 'yes'} as Prisma.InputJsonValue,
|
||||
},
|
||||
});
|
||||
|
||||
const execution = await prisma.workflowExecution.create({
|
||||
@@ -1140,7 +1144,11 @@ describe('WorkflowExecutionService - Integration Tests', () => {
|
||||
data: {fromStepId: triggerStep!.id, toStepId: conditionStep.id},
|
||||
});
|
||||
await prisma.workflowTransition.create({
|
||||
data: {fromStepId: conditionStep.id, toStepId: exitStep.id, condition: {branch: 'yes'} as Prisma.InputJsonValue},
|
||||
data: {
|
||||
fromStepId: conditionStep.id,
|
||||
toStepId: exitStep.id,
|
||||
condition: {branch: 'yes'} as Prisma.InputJsonValue,
|
||||
},
|
||||
});
|
||||
|
||||
const execution = await prisma.workflowExecution.create({
|
||||
@@ -1178,7 +1186,7 @@ describe('WorkflowExecutionService - Integration Tests', () => {
|
||||
status: 200,
|
||||
text: async () => JSON.stringify({success: true}),
|
||||
}));
|
||||
global.fetch = mockFetch as any;
|
||||
global.fetch = mockFetch as unknown as typeof fetch;
|
||||
|
||||
const contact = await factories.createContact({
|
||||
projectId,
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import {describe, it, expect, beforeEach, afterEach, vi} from 'vitest';
|
||||
import {WorkflowTriggerType, WorkflowStepType, WorkflowExecutionStatus} from '@plunk/db';
|
||||
import {afterEach, beforeEach, describe, expect, it, vi} from 'vitest';
|
||||
import {WorkflowExecutionStatus, WorkflowStepType, WorkflowTriggerType} from '@plunk/db';
|
||||
import {WorkflowService} from '../WorkflowService';
|
||||
import {factories, getPrismaClient} from '../../../../../test/helpers';
|
||||
|
||||
@@ -58,7 +58,7 @@ describe('WorkflowService', () => {
|
||||
afterEach(async () => {
|
||||
const {redis} = await import('../../database/redis');
|
||||
if ('clear' in redis) {
|
||||
(redis as any).clear();
|
||||
(redis as unknown as {clear: () => void}).clear();
|
||||
}
|
||||
});
|
||||
|
||||
@@ -233,9 +233,11 @@ describe('WorkflowService', () => {
|
||||
|
||||
const result = await WorkflowService.list(projectId);
|
||||
|
||||
const found = result.workflows.find(w => w.id === workflow.id);
|
||||
expect((found as any)._count.steps).toBe(3); // TRIGGER + 2 added
|
||||
expect((found as any)._count.executions).toBe(1);
|
||||
const found = result.workflows.find(w => w.id === workflow.id) as
|
||||
| ((typeof result.workflows)[number] & {_count: {steps: number; executions: number}})
|
||||
| undefined;
|
||||
expect(found?._count.steps).toBe(3); // TRIGGER + 2 added
|
||||
expect(found?._count.executions).toBe(1);
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
Reference in New Issue
Block a user