types: Abstract inline interfaces to @plunk/types

This commit is contained in:
Dries Augustyns
2026-01-01 10:52:43 +01:00
parent 38da58e5e9
commit 85c992a9f9
95 changed files with 1370 additions and 1350 deletions
@@ -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);