types: Abstract inline interfaces to @plunk/types

This commit is contained in:
Dries Augustyns
2026-01-01 09:09:03 +01:00
parent 519b131792
commit 38da58e5e9
50 changed files with 639 additions and 462 deletions
+1
View File
@@ -108,6 +108,7 @@ between groups.
- Consistent type imports preferred: `import type { ... }` - Consistent type imports preferred: `import type { ... }`
- Unused vars allowed with `_` prefix - Unused vars allowed with `_` prefix
- Strict type checking enabled across all packages - Strict type checking enabled across all packages
- Try to avoid inline types in favor of shared types in `@plunk/types`
### Component Structure ### Component Structure
+2 -1
View File
@@ -1,9 +1,10 @@
import {Controller, Get, Middleware} from '@overnightjs/core'; import {Controller, Get, Middleware} from '@overnightjs/core';
import type {NextFunction, Request, Response} from 'express'; import type {NextFunction, Request, Response} from 'express';
import {ActivityType} from '@plunk/types';
import type {AuthResponse} from '../middleware/auth.js'; import type {AuthResponse} from '../middleware/auth.js';
import {requireAuth, requireEmailVerified} from '../middleware/auth.js'; import {requireAuth, requireEmailVerified} from '../middleware/auth.js';
import {ActivityService, ActivityType} from '../services/ActivityService.js'; import {ActivityService} from '../services/ActivityService.js';
import {CatchAsync} from '../utils/asyncHandler.js'; import {CatchAsync} from '../utils/asyncHandler.js';
@Controller('activity') @Controller('activity')
@@ -1,3 +1,4 @@
import type {ApiRequestCleanupJobData} from '@plunk/types';
import type {Job} from 'bullmq'; import type {Job} from 'bullmq';
import {Worker} from 'bullmq'; import {Worker} from 'bullmq';
import type {RedisOptions} from 'ioredis'; import type {RedisOptions} from 'ioredis';
@@ -5,7 +6,6 @@ import signale from 'signale';
import {REDIS_URL} from '../app/constants.js'; import {REDIS_URL} from '../app/constants.js';
import {prisma} from '../database/prisma.js'; import {prisma} from '../database/prisma.js';
import type {ApiRequestCleanupJobData} from '../services/QueueService.js';
/** /**
* API Request Cleanup Worker * API Request Cleanup Worker
+2 -1
View File
@@ -3,11 +3,12 @@
* Processes bulk subscribe, unsubscribe, and delete operations * Processes bulk subscribe, unsubscribe, and delete operations
*/ */
import type {BulkContactActionJobData} from '@plunk/types';
import {type Job, Worker} from 'bullmq'; import {type Job, Worker} from 'bullmq';
import signale from 'signale'; import signale from 'signale';
import {ContactService} from '../services/ContactService.js'; import {ContactService} from '../services/ContactService.js';
import {type BulkContactActionJobData, bulkContactQueue} from '../services/QueueService.js'; import {bulkContactQueue} from '../services/QueueService.js';
const BATCH_SIZE = 100; // Process contacts in batches of 100 const BATCH_SIZE = 100; // Process contacts in batches of 100
+2 -1
View File
@@ -3,11 +3,12 @@
* Processes campaign batches (queues emails for each contact in the batch) * Processes campaign batches (queues emails for each contact in the batch)
*/ */
import type {CampaignBatchJobData} from '@plunk/types';
import {type Job, Worker} from 'bullmq'; import {type Job, Worker} from 'bullmq';
import signale from 'signale'; import signale from 'signale';
import {CampaignService} from '../services/CampaignService.js'; import {CampaignService} from '../services/CampaignService.js';
import {type CampaignBatchJobData, campaignQueue} from '../services/QueueService.js'; import {campaignQueue} from '../services/QueueService.js';
export function createCampaignWorker() { export function createCampaignWorker() {
const worker = new Worker<CampaignBatchJobData>( const worker = new Worker<CampaignBatchJobData>(
@@ -3,10 +3,11 @@
* Processes domain verification jobs from the BullMQ queue * Processes domain verification jobs from the BullMQ queue
*/ */
import type {DomainVerificationJobData} from '@plunk/types';
import {type Job, Worker} from 'bullmq'; import {type Job, Worker} from 'bullmq';
import signale from 'signale'; import signale from 'signale';
import {type DomainVerificationJobData, domainVerificationQueue} from '../services/QueueService.js'; import {domainVerificationQueue} from '../services/QueueService.js';
import {checkDomainVerifications} from './domain-verification.js'; import {checkDomainVerifications} from './domain-verification.js';
+3 -2
View File
@@ -4,16 +4,17 @@
*/ */
import {CampaignStatus, EmailSourceType, EmailStatus} from '@plunk/db'; import {CampaignStatus, EmailSourceType, EmailStatus} from '@plunk/db';
import type {SendEmailJobData} from '@plunk/types';
import {type Job, Worker} from 'bullmq'; import {type Job, Worker} from 'bullmq';
import signale from 'signale'; import signale from 'signale';
import {DASHBOARD_URI, EMAIL_RATE_LIMIT_PER_SECOND} from '../app/constants.js';
import {prisma} from '../database/prisma.js'; import {prisma} from '../database/prisma.js';
import {EmailService} from '../services/EmailService.js'; import {EmailService} from '../services/EmailService.js';
import {EventService} from '../services/EventService.js'; import {EventService} from '../services/EventService.js';
import {MeterService} from '../services/MeterService.js'; import {MeterService} from '../services/MeterService.js';
import {emailQueue, type SendEmailJobData} from '../services/QueueService.js'; import {emailQueue} from '../services/QueueService.js';
import {getSendingQuota, sendRawEmail} from '../services/SESService.js'; import {getSendingQuota, sendRawEmail} from '../services/SESService.js';
import {DASHBOARD_URI, EMAIL_RATE_LIMIT_PER_SECOND} from '../app/constants.js';
/** /**
* Determine the email sending rate limit (emails per second) * Determine the email sending rate limit (emails per second)
+2 -1
View File
@@ -3,6 +3,7 @@
* Processes CSV contact imports with validation and batch processing * Processes CSV contact imports with validation and batch processing
*/ */
import type {ContactImportJobData} from '@plunk/types';
import {type Job, Worker} from 'bullmq'; import {type Job, Worker} from 'bullmq';
import {parse} from 'csv-parse/sync'; import {parse} from 'csv-parse/sync';
import signale from 'signale'; import signale from 'signale';
@@ -10,7 +11,7 @@ import signale from 'signale';
import {prisma} from '../database/prisma.js'; import {prisma} from '../database/prisma.js';
import {ContactService} from '../services/ContactService.js'; import {ContactService} from '../services/ContactService.js';
import {NtfyService} from '../services/NtfyService.js'; import {NtfyService} from '../services/NtfyService.js';
import {type ContactImportJobData, importQueue} from '../services/QueueService.js'; import {importQueue} from '../services/QueueService.js';
const BATCH_SIZE = 100; // Process contacts in batches of 100 const BATCH_SIZE = 100; // Process contacts in batches of 100
+2 -1
View File
@@ -4,12 +4,13 @@
*/ */
import {CampaignStatus} from '@plunk/db'; import {CampaignStatus} from '@plunk/db';
import type {ScheduledCampaignJobData} from '@plunk/types';
import {type Job, Worker} from 'bullmq'; import {type Job, Worker} from 'bullmq';
import signale from 'signale'; import signale from 'signale';
import {prisma} from '../database/prisma.js'; import {prisma} from '../database/prisma.js';
import {CampaignService} from '../services/CampaignService.js'; import {CampaignService} from '../services/CampaignService.js';
import {type ScheduledCampaignJobData, scheduledQueue} from '../services/QueueService.js'; import {scheduledQueue} from '../services/QueueService.js';
export function createScheduledCampaignWorker() { export function createScheduledCampaignWorker() {
const worker = new Worker<ScheduledCampaignJobData>( const worker = new Worker<ScheduledCampaignJobData>(
+2 -1
View File
@@ -3,12 +3,13 @@
* Processes segment count update jobs from the BullMQ queue * Processes segment count update jobs from the BullMQ queue
*/ */
import type {SegmentCountJobData} from '@plunk/types';
import {type Job, Worker} from 'bullmq'; import {type Job, Worker} from 'bullmq';
import signale from 'signale'; import signale from 'signale';
import {prisma} from '../database/prisma.js'; import {prisma} from '../database/prisma.js';
import {NtfyService} from '../services/NtfyService.js'; import {NtfyService} from '../services/NtfyService.js';
import {type SegmentCountJobData, segmentCountQueue} from '../services/QueueService.js'; import {segmentCountQueue} from '../services/QueueService.js';
import {SegmentService} from '../services/SegmentService.js'; import {SegmentService} from '../services/SegmentService.js';
/** /**
@@ -3,10 +3,11 @@
* Processes workflow steps from the queue (for delayed steps) * Processes workflow steps from the queue (for delayed steps)
*/ */
import type {WorkflowStepJobData} from '@plunk/types';
import {type Job, Worker} from 'bullmq'; import {type Job, Worker} from 'bullmq';
import signale from 'signale'; import signale from 'signale';
import {workflowQueue, type WorkflowStepJobData} from '../services/QueueService.js'; import {workflowQueue} from '../services/QueueService.js';
import {WorkflowExecutionService} from '../services/WorkflowExecutionService.js'; import {WorkflowExecutionService} from '../services/WorkflowExecutionService.js';
export function createWorkflowWorker() { export function createWorkflowWorker() {
+5 -54
View File
@@ -1,61 +1,12 @@
import type {Prisma} from '@plunk/db'; import type {Prisma} from '@plunk/db';
import {ActivityType} from '@plunk/types';
import type {Activity, ActivityStats, CursorPaginatedResponse} from '@plunk/types';
import signale from 'signale'; import signale from 'signale';
import {prisma} from '../database/prisma.js'; import {prisma} from '../database/prisma.js';
import {redis} from '../database/redis.js'; import {redis} from '../database/redis.js';
import {Keys} from './keys.js'; import {Keys} from './keys.js';
/**
* Activity types that can be tracked
*/
export enum ActivityType {
EVENT_TRIGGERED = 'event.triggered',
EMAIL_SENT = 'email.sent',
EMAIL_DELIVERED = 'email.delivered',
EMAIL_OPENED = 'email.opened',
EMAIL_CLICKED = 'email.clicked',
EMAIL_BOUNCED = 'email.bounced',
CAMPAIGN_SENT = 'campaign.sent',
CAMPAIGN_SCHEDULED = 'campaign.scheduled',
WORKFLOW_STARTED = 'workflow.started',
WORKFLOW_COMPLETED = 'workflow.completed',
WORKFLOW_EMAIL_SCHEDULED = 'workflow.email.scheduled',
}
/**
* Unified activity item
*/
export interface Activity {
id: string;
type: ActivityType;
timestamp: Date;
contactEmail?: string;
contactId?: string;
metadata: Record<string, unknown>;
}
/**
* Paginated activity response
*/
export interface PaginatedActivities {
activities: Activity[];
nextCursor?: string;
hasMore: boolean;
}
/**
* Activity stats for dashboard
*/
export interface ActivityStats {
totalEvents: number;
totalEmailsSent: number;
totalEmailsOpened: number;
totalEmailsClicked: number;
totalWorkflowsStarted: number;
openRate: number;
clickRate: number;
}
/** /**
* Activity Service * Activity Service
* *
@@ -107,7 +58,7 @@ export class ActivityService {
contactId?: string, contactId?: string,
startDate?: Date, startDate?: Date,
endDate?: Date, endDate?: Date,
): Promise<PaginatedActivities> { ): Promise<CursorPaginatedResponse<Activity>> {
// Cap limit to prevent abuse // Cap limit to prevent abuse
const effectiveLimit = Math.min(limit, this.MAX_LIMIT); const effectiveLimit = Math.min(limit, this.MAX_LIMIT);
@@ -158,8 +109,8 @@ export class ActivityService {
const nextCursor = hasMore && lastActivity ? `${lastActivity.timestamp.getTime()}_${lastActivity.id}` : undefined; const nextCursor = hasMore && lastActivity ? `${lastActivity.timestamp.getTime()}_${lastActivity.id}` : undefined;
return { return {
activities: results, data: results,
nextCursor, cursor: nextCursor,
hasMore, hasMore,
}; };
} }
+1 -33
View File
@@ -1,4 +1,5 @@
import {EmailSourceType} from '@plunk/db'; import {EmailSourceType} from '@plunk/db';
import type {CategoryUsage, BillingLimitsResponse, LimitCheckResult} from '@plunk/types';
import {BillingLimitExceededEmail, BillingLimitWarningEmail, sendPlatformEmail} from '@plunk/email'; import {BillingLimitExceededEmail, BillingLimitWarningEmail, sendPlatformEmail} from '@plunk/email';
import React from 'react'; import React from 'react';
import signale from 'signale'; import signale from 'signale';
@@ -11,39 +12,6 @@ import {Keys} from './keys.js';
import {MembershipService} from './MembershipService.js'; import {MembershipService} from './MembershipService.js';
import {NtfyService} from './NtfyService.js'; import {NtfyService} from './NtfyService.js';
/**
* Usage information for a specific email category
*/
export interface CategoryUsage {
limit: number | null; // null = unlimited
usage: number;
percentage: number; // 0-100
isWarning: boolean; // true if >= 80%
isBlocked: boolean; // true if >= 100%
}
/**
* Complete billing limits and usage for a project
*/
export interface BillingLimitsResponse {
workflows: CategoryUsage;
campaigns: CategoryUsage;
transactional: CategoryUsage;
currency: string | null;
}
/**
* Result of limit check
*/
export interface LimitCheckResult {
allowed: boolean;
warning: boolean; // true if >= 80% but < 100%
usage: number;
limit: number | null;
percentage: number;
message?: string;
}
/** /**
* Billing Limit Service * Billing Limit Service
* Handles usage tracking and enforcement of billing limits per email category * Handles usage tracking and enforcement of billing limits per email category
+1 -27
View File
@@ -1,6 +1,6 @@
import type {Campaign, Contact, Prisma} from '@plunk/db'; import type {Campaign, Contact, Prisma} from '@plunk/db';
import {CampaignAudienceType, CampaignStatus, EmailSourceType} from '@plunk/db'; import {CampaignAudienceType, CampaignStatus, EmailSourceType} from '@plunk/db';
import type {FilterCondition} from '@plunk/types'; import type {FilterCondition, CreateCampaignData, UpdateCampaignData} from '@plunk/types';
import signale from 'signale'; import signale from 'signale';
import {prisma} from '../database/prisma.js'; import {prisma} from '../database/prisma.js';
@@ -18,32 +18,6 @@ import {sendRawEmail} from './SESService.js';
const BATCH_SIZE = 500; // Number of emails to process per batch (increased for better performance) const BATCH_SIZE = 500; // Number of emails to process per batch (increased for better performance)
export interface CreateCampaignData {
name: string;
description?: string;
subject: string;
body: string;
from: string;
fromName?: string | null;
replyTo?: string | null;
audienceType: CampaignAudienceType;
audienceCondition?: FilterCondition;
segmentId?: string;
}
export interface UpdateCampaignData {
name?: string;
description?: string;
subject?: string;
body?: string;
from?: string;
fromName?: string | null;
replyTo?: string | null;
audienceType?: CampaignAudienceType;
audienceCondition?: FilterCondition;
segmentId?: string;
}
export class CampaignService { export class CampaignService {
/** /**
* Create a new campaign * Create a new campaign
+3 -10
View File
@@ -1,18 +1,11 @@
import {type Contact, Prisma} from '@plunk/db'; import {type Contact, Prisma} from '@plunk/db';
import {isValidLanguageCode} from '@plunk/shared'; import {isValidLanguageCode} from '@plunk/shared';
import type {FilterCondition, FilterGroup} from '@plunk/types'; import type {FilterCondition, FilterGroup, CursorPaginatedResponse} from '@plunk/types';
import {prisma} from '../database/prisma.js'; import {prisma} from '../database/prisma.js';
import {HttpException} from '../exceptions/index.js'; import {HttpException} from '../exceptions/index.js';
import {EventService} from './EventService.js'; import {EventService} from './EventService.js';
export interface PaginatedContacts {
contacts: Contact[];
total: number;
cursor?: string;
hasMore: boolean;
}
export class ContactService { export class ContactService {
/** /**
* Get all contacts for a project with cursor-based pagination * Get all contacts for a project with cursor-based pagination
@@ -23,7 +16,7 @@ export class ContactService {
limit = 20, limit = 20,
cursor?: string, cursor?: string,
search?: string, search?: string,
): Promise<PaginatedContacts> { ): Promise<CursorPaginatedResponse<Contact>> {
const where: Prisma.ContactWhereInput = { const where: Prisma.ContactWhereInput = {
projectId, projectId,
...(search ...(search
@@ -58,7 +51,7 @@ export class ContactService {
const total = !cursor ? await prisma.contact.count({where}) : 0; const total = !cursor ? await prisma.contact.count({where}) : 0;
return { return {
contacts: results, data: results,
total, total,
cursor: nextCursor, cursor: nextCursor,
hasMore, hasMore,
@@ -1,19 +1,8 @@
import {promises as dns} from 'dns'; import {promises as dns} from 'dns';
import {run} from '@zootools/email-spell-checker'; import {run} from '@zootools/email-spell-checker';
import type {EmailVerificationResult} from '@plunk/types';
import {redis} from '../database/redis.js'; import {redis} from '../database/redis.js';
export interface EmailVerificationResult {
email: string;
valid: boolean;
isDisposable: boolean;
isTypo: boolean;
isPlusAddressed: boolean;
domainExists: boolean;
hasMxRecords: boolean;
suggestedEmail?: string;
reasons: string[];
}
const DISPOSABLE_DOMAINS_URL = const DISPOSABLE_DOMAINS_URL =
'https://raw.githubusercontent.com/disposable-email-domains/disposable-email-domains/main/disposable_email_blocklist.conf'; 'https://raw.githubusercontent.com/disposable-email-domains/disposable-email-domains/main/disposable_email_blocklist.conf';
const DISPOSABLE_DOMAINS_CACHE_KEY = 'email:disposable_domains'; const DISPOSABLE_DOMAINS_CACHE_KEY = 'email:disposable_domains';
+1 -17
View File
@@ -1,4 +1,5 @@
import type {Membership, Role} from '@plunk/db'; import type {Membership, Role} from '@plunk/db';
import type {MemberWithEmail, OwnerInfo, DisabledProjectInfo} from '@plunk/types';
import {prisma} from '../database/prisma.js'; import {prisma} from '../database/prisma.js';
import {redis, REDIS_ONE_MINUTE, wrapRedis} from '../database/redis.js'; import {redis, REDIS_ONE_MINUTE, wrapRedis} from '../database/redis.js';
@@ -7,23 +8,6 @@ import {Keys} from './keys.js';
const FIVE_MINUTES_IN_SECONDS = 5 * 60; const FIVE_MINUTES_IN_SECONDS = 5 * 60;
export interface MemberWithEmail {
userId: string;
email: string;
role: Role;
createdAt: Date;
}
export interface OwnerInfo {
userId: string;
email: string;
}
export interface DisabledProjectInfo {
hasDisabledProject: boolean;
disabledProjectNames: string[];
}
/** /**
* Service for managing project memberships * Service for managing project memberships
* Centralizes all membership-related database queries with caching * Centralizes all membership-related database queries with caching
+1 -35
View File
@@ -1,40 +1,6 @@
import {NtfyPriority, NtfyTag, type NtfyNotification} from '@plunk/types';
import signale from 'signale'; import signale from 'signale';
/**
* Priority levels for ntfy notifications
* Based on ntfy.sh documentation
*/
export enum NtfyPriority {
MIN = 1, // No vibration/sound, relegated to "Other notifications"
LOW = 2, // No vibration/sound, hidden until drawer opened
DEFAULT = 3, // Short vibration and sound (standard)
HIGH = 4, // Long vibration, pop-over notification
MAX = 5, // Long vibration bursts, pop-over notification
}
/**
* Tags for ntfy notifications (emoji shortcuts)
*/
export enum NtfyTag {
WARNING = 'warning',
ERROR = 'rotating_light',
SUCCESS = 'white_check_mark',
MONEY = 'money_with_wings',
SHIELD = 'shield',
ROCKET = 'rocket',
BELL = 'bell',
CHART = 'chart_with_upwards_trend',
SKULL = 'skull',
INFO = 'information_source',
}
export interface NtfyNotification {
title: string;
message: string;
priority?: NtfyPriority;
tags?: NtfyTag[];
}
/** /**
* Service for sending notifications via ntfy.sh * Service for sending notifications via ntfy.sh
* Supports configurable ntfy server URL via NTFY_URL environment variable * Supports configurable ntfy server URL via NTFY_URL environment variable
+11 -53
View File
@@ -1,63 +1,21 @@
import {type Job, Queue} from 'bullmq'; import {type Job, Queue} from 'bullmq';
import type {RedisOptions} from 'ioredis'; import type {RedisOptions} from 'ioredis';
import signale from 'signale'; import signale from 'signale';
import type {
SendEmailJobData,
CampaignBatchJobData,
ScheduledCampaignJobData,
WorkflowStepJobData,
ContactImportJobData,
BulkContactActionJobData,
SegmentCountJobData,
DomainVerificationJobData,
ApiRequestCleanupJobData,
} from '@plunk/types';
import {REDIS_URL} from '../app/constants.js'; import {REDIS_URL} from '../app/constants.js';
import {prisma} from '../database/prisma.js'; import {prisma} from '../database/prisma.js';
/**
* Queue Job Data Types
*/
export interface SendEmailJobData {
emailId: string;
}
export interface CampaignBatchJobData {
campaignId: string;
batchNumber: number;
offset: number;
limit: number;
cursor?: string; // For cursor-based pagination
}
export interface WorkflowStepJobData {
executionId: string;
stepId: string;
type?: 'process-step' | 'timeout'; // Job type for different handling
stepExecutionId?: string; // For timeout jobs, reference to the step execution
}
export interface ScheduledCampaignJobData {
campaignId: string;
}
export interface ContactImportJobData {
projectId: string;
csvData: string; // Base64 encoded CSV content
filename: string;
}
export interface SegmentCountJobData {
projectId?: string; // Optional: if provided, only update this project's segments
}
// eslint-disable-next-line @typescript-eslint/no-empty-object-type
export interface DomainVerificationJobData {
// Empty for now - processes all domains
}
// eslint-disable-next-line @typescript-eslint/no-empty-object-type
export interface ApiRequestCleanupJobData {
// Empty - cleans up old API request logs
}
export interface BulkContactActionJobData {
projectId: string;
contactIds: string[];
operation: 'subscribe' | 'unsubscribe' | 'delete';
}
/** /**
* Queue Configuration * Queue Configuration
*/ */
+3 -11
View File
@@ -1,5 +1,5 @@
import {type Contact, Prisma, type Segment} from '@plunk/db'; import {type Contact, Prisma, type Segment} from '@plunk/db';
import type {FilterCondition, FilterGroup, SegmentFilter} from '@plunk/types'; import type {FilterCondition, FilterGroup, SegmentFilter, PaginatedResponse} from '@plunk/types';
import signale from 'signale'; import signale from 'signale';
import {prisma} from '../database/prisma.js'; import {prisma} from '../database/prisma.js';
@@ -11,14 +11,6 @@ import {NtfyService} from './NtfyService.js';
// Re-export types for use in other services // Re-export types for use in other services
export type {FilterCondition, FilterGroup, SegmentFilter} from '@plunk/types'; export type {FilterCondition, FilterGroup, SegmentFilter} from '@plunk/types';
export interface PaginatedContacts {
contacts: Contact[];
total: number;
page: number;
pageSize: number;
totalPages: number;
}
/** /**
* Convert segment name to a URL-safe slug for event names * Convert segment name to a URL-safe slug for event names
* Example: "VIP Customers" -> "vip-customers" * Example: "VIP Customers" -> "vip-customers"
@@ -72,7 +64,7 @@ export class SegmentService {
segmentId: string, segmentId: string,
page = 1, page = 1,
pageSize = 20, pageSize = 20,
): Promise<PaginatedContacts> { ): Promise<PaginatedResponse<Contact>> {
const segment = await this.get(projectId, segmentId); const segment = await this.get(projectId, segmentId);
const condition = segment.condition as unknown as FilterCondition; const condition = segment.condition as unknown as FilterCondition;
@@ -90,7 +82,7 @@ export class SegmentService {
]); ]);
return { return {
contacts, data: contacts,
total, total,
page, page,
pageSize, pageSize,
+3 -10
View File
@@ -1,18 +1,11 @@
import type {Template} from '@plunk/db'; import type {Template} from '@plunk/db';
import {Prisma} from '@plunk/db'; import {Prisma} from '@plunk/db';
import type {PaginatedResponse} from '@plunk/types';
import {prisma} from '../database/prisma.js'; import {prisma} from '../database/prisma.js';
import {HttpException} from '../exceptions/index.js'; import {HttpException} from '../exceptions/index.js';
import {buildEmailFieldsUpdate} from '../utils/modelUpdate.js'; import {buildEmailFieldsUpdate} from '../utils/modelUpdate.js';
export interface PaginatedTemplates {
templates: Template[];
total: number;
page: number;
pageSize: number;
totalPages: number;
}
export class TemplateService { export class TemplateService {
/** /**
* Get all templates for a project with pagination * Get all templates for a project with pagination
@@ -23,7 +16,7 @@ export class TemplateService {
pageSize = 20, pageSize = 20,
search?: string, search?: string,
type?: Template['type'], type?: Template['type'],
): Promise<PaginatedTemplates> { ): Promise<PaginatedResponse<Template>> {
const skip = (page - 1) * pageSize; const skip = (page - 1) * pageSize;
const where: Prisma.TemplateWhereInput = { const where: Prisma.TemplateWhereInput = {
@@ -51,7 +44,7 @@ export class TemplateService {
]); ]);
return { return {
templates, data: templates,
total, total,
page, page,
pageSize, pageSize,
+3 -25
View File
@@ -1,5 +1,6 @@
import type {Workflow, WorkflowExecution, WorkflowStep, WorkflowStepExecution, WorkflowTransition} from '@plunk/db'; import type {Workflow, WorkflowExecution, WorkflowStep, WorkflowStepExecution, WorkflowTransition} from '@plunk/db';
import {Prisma, WorkflowExecutionStatus} from '@plunk/db'; import {Prisma, WorkflowExecutionStatus} from '@plunk/db';
import type {PaginatedResponse, WorkflowWithDetails, WorkflowExecutionWithDetails} from '@plunk/types';
import signale from 'signale'; import signale from 'signale';
import {prisma} from '../database/prisma.js'; import {prisma} from '../database/prisma.js';
@@ -10,34 +11,11 @@ import {EventService} from './EventService.js';
import {NtfyService} from './NtfyService.js'; import {NtfyService} from './NtfyService.js';
import {WorkflowExecutionService} from './WorkflowExecutionService.js'; import {WorkflowExecutionService} from './WorkflowExecutionService.js';
export interface PaginatedWorkflows {
workflows: Workflow[];
total: number;
page: number;
pageSize: number;
totalPages: number;
}
export interface WorkflowWithDetails extends Workflow {
steps: (WorkflowStep & {
template?: {id: string; name: string} | null;
outgoingTransitions: WorkflowTransition[];
incomingTransitions: WorkflowTransition[];
})[];
}
export interface WorkflowExecutionWithDetails extends WorkflowExecution {
workflow: Workflow;
contact: {id: string; email: string};
currentStep?: WorkflowStep | null;
stepExecutions: WorkflowStepExecution[];
}
export class WorkflowService { export class WorkflowService {
/** /**
* Get all workflows for a project with pagination * Get all workflows for a project with pagination
*/ */
public static async list(projectId: string, page = 1, pageSize = 20, search?: string): Promise<PaginatedWorkflows> { public static async list(projectId: string, page = 1, pageSize = 20, search?: string): Promise<PaginatedResponse<Workflow>> {
const skip = (page - 1) * pageSize; const skip = (page - 1) * pageSize;
const where: Prisma.WorkflowWhereInput = { const where: Prisma.WorkflowWhereInput = {
@@ -71,7 +49,7 @@ export class WorkflowService {
]); ]);
return { return {
workflows: workflows as Workflow[], data: workflows as Workflow[],
total, total,
page, page,
pageSize, pageSize,
+1 -1
View File
@@ -107,7 +107,7 @@ export function DomainsSettings({projectId}: DomainsSettingsProps) {
setVerificationStatus(prev => ({ setVerificationStatus(prev => ({
...prev, ...prev,
[newDomain.id]: { [newDomain.id]: {
tokens: newDomain.dkimTokens, tokens: newDomain.dkimTokens as string[] | null,
status: 'Pending', status: 'Pending',
verified: false, verified: false,
}, },
@@ -298,7 +298,7 @@ export function EmailEditor({value, onChange, placeholder, subject, from, replyT
subscribeUrl: `${window.location.origin}/subscribe/${contact.id}`, subscribeUrl: `${window.location.origin}/subscribe/${contact.id}`,
manageUrl: `${window.location.origin}/manage/${contact.id}`, manageUrl: `${window.location.origin}/manage/${contact.id}`,
data: contact.data || {}, data: contact.data || {},
...contact.data, ...(contact.data as Record<string, unknown> | null || {}),
}; };
return replaceVariables(currentHtml, contactData); return replaceVariables(currentHtml, contactData);
@@ -317,7 +317,7 @@ export function EmailEditor({value, onChange, placeholder, subject, from, replyT
subscribeUrl: `${window.location.origin}/subscribe/${contact.id}`, subscribeUrl: `${window.location.origin}/subscribe/${contact.id}`,
manageUrl: `${window.location.origin}/manage/${contact.id}`, manageUrl: `${window.location.origin}/manage/${contact.id}`,
data: contact.data || {}, data: contact.data || {},
...contact.data, ...(contact.data as Record<string, unknown> | null || {}),
}; };
return replaceVariables(subject, contactData); return replaceVariables(subject, contactData);
+5 -15
View File
@@ -1,19 +1,9 @@
import type {BillingLimitsResponse, CategoryUsage} from '@plunk/types';
import useSWR from 'swr'; import useSWR from 'swr';
export interface CategoryLimit { // Re-export for backward compatibility
usage: number; export type CategoryLimit = CategoryUsage;
limit: number | null; export type BillingLimitsData = BillingLimitsResponse;
percentage: number;
isWarning: boolean;
isBlocked: boolean;
}
export interface BillingLimitsData {
workflows: CategoryLimit;
campaigns: CategoryLimit;
transactional: CategoryLimit;
currency: string | null;
}
/** /**
* Hook to fetch billing limits for a project * Hook to fetch billing limits for a project
@@ -22,7 +12,7 @@ export interface BillingLimitsData {
* Paid tier projects (with subscription): Shows per-category usage with custom limits * Paid tier projects (with subscription): Shows per-category usage with custom limits
*/ */
export function useBillingLimits(projectId: string | undefined, billingEnabled: boolean) { export function useBillingLimits(projectId: string | undefined, billingEnabled: boolean) {
const {data, error, mutate, isLoading} = useSWR<BillingLimitsData>( const {data, error, mutate, isLoading} = useSWR<BillingLimitsResponse>(
projectId && billingEnabled ? `/users/@me/projects/${projectId}/billing-limits` : null, projectId && billingEnabled ? `/users/@me/projects/${projectId}/billing-limits` : null,
{ {
revalidateOnFocus: false, revalidateOnFocus: false,
+4 -13
View File
@@ -1,16 +1,7 @@
import type {Contact} from '@plunk/db';
import type {CursorPaginatedResponse} from '@plunk/types';
import useSWR from 'swr'; import useSWR from 'swr';
export interface Contact {
id: string;
email: string;
data?: Record<string, unknown>;
}
export interface ContactsResponse {
contacts: Contact[];
total: number;
}
interface UseContactsOptions { interface UseContactsOptions {
limit?: number; limit?: number;
search?: string; search?: string;
@@ -28,7 +19,7 @@ export function useContacts(options: UseContactsOptions = {}) {
params.set('search', search); params.set('search', search);
} }
const {data, error, mutate, isLoading} = useSWR<ContactsResponse>( const {data, error, mutate, isLoading} = useSWR<CursorPaginatedResponse<Contact>>(
`/contacts?${params.toString()}`, `/contacts?${params.toString()}`,
{ {
revalidateOnFocus: false, revalidateOnFocus: false,
@@ -37,7 +28,7 @@ export function useContacts(options: UseContactsOptions = {}) {
); );
return { return {
contacts: data?.contacts || [], contacts: data?.data || [],
total: data?.total || 0, total: data?.total || 0,
error, error,
isLoading, isLoading,
+5 -25
View File
@@ -1,29 +1,9 @@
import type {ActivityStats, CursorPaginatedResponse, PaginatedResponse} from '@plunk/types';
import useSWR from 'swr'; import useSWR from 'swr';
export interface ActivityStats { // Specific response types for dashboard (using unknown[] since we only need counts)
totalEvents: number; type ContactsResponse = CursorPaginatedResponse<unknown>;
totalEmailsSent: number; type CampaignsResponse = PaginatedResponse<unknown>;
totalEmailsOpened: number;
totalEmailsClicked: number;
totalWorkflowsStarted: number;
openRate: number;
clickRate: number;
}
export interface ContactsResponse {
contacts: unknown[];
total: number;
cursor?: string;
hasMore: boolean;
}
export interface CampaignsResponse {
campaigns: unknown[];
total: number;
page: number;
pageSize: number;
totalPages: number;
}
export interface DashboardStats { export interface DashboardStats {
totalContacts: number; totalContacts: number;
@@ -46,7 +26,7 @@ export function useDashboardStats(): DashboardStats {
const {data: contactsData, error: contactsError, isLoading: isLoadingContacts} = useSWR<ContactsResponse>('/contacts?limit=1'); const {data: contactsData, error: contactsError, isLoading: isLoadingContacts} = useSWR<ContactsResponse>('/contacts?limit=1');
// Fetch campaigns (only need the total count) // Fetch campaigns (only need the total count)
const {data: campaignsData, error: campaignsError, isLoading: isLoadingCampaigns} = useSWR<CampaignsResponse>('/campaigns?pageSize=1'); const {data: campaignsData, error: campaignsError, isLoading: isLoadingCampaigns} = useSWR<CampaignsResponse>('/campaigns?page=1&limit=1');
// Still loading if ANY of the requests are still in progress // Still loading if ANY of the requests are still in progress
const isLoading = isLoadingActivity || isLoadingContacts || isLoadingCampaigns; const isLoading = isLoadingActivity || isLoadingContacts || isLoadingCampaigns;
+2 -11
View File
@@ -1,18 +1,9 @@
import type {Domain} from '@plunk/db';
import {DomainSchemas} from '@plunk/shared';
import useSWR from 'swr'; import useSWR from 'swr';
import {DomainSchemas} from '@plunk/shared';
import {network} from '../network'; import {network} from '../network';
export interface Domain {
id: string;
domain: string;
verified: boolean;
dkimTokens: string[] | null;
projectId: string;
createdAt: string;
updatedAt: string;
}
export interface DomainVerificationStatus { export interface DomainVerificationStatus {
domain: string; domain: string;
tokens: string[]; tokens: string[];
+1 -10
View File
@@ -1,4 +1,5 @@
import {CampaignAudienceType, TemplateType, TrackingMode, WorkflowStepType, WorkflowTriggerType} from '@plunk/db'; import {CampaignAudienceType, TemplateType, TrackingMode, WorkflowStepType, WorkflowTriggerType} from '@plunk/db';
import type {FilterCondition, FilterGroup} from '@plunk/types';
import {z} from 'zod'; import {z} from 'zod';
const literalSchema = z.union([z.string(), z.number(), z.boolean(), z.null(), z.date()]); const literalSchema = z.union([z.string(), z.number(), z.boolean(), z.null(), z.date()]);
@@ -104,16 +105,6 @@ const segmentFilterSchema = z.object({
unit: z.enum(['days', 'hours', 'minutes']).optional(), unit: z.enum(['days', 'hours', 'minutes']).optional(),
}); });
type FilterGroup = {
filters: z.infer<typeof segmentFilterSchema>[];
conditions?: FilterCondition;
};
type FilterCondition = {
logic: 'AND' | 'OR';
groups: FilterGroup[];
};
const filterGroupSchema: z.ZodType<FilterGroup> = z.lazy(() => const filterGroupSchema: z.ZodType<FilterGroup> = z.lazy(() =>
z.object({ z.object({
filters: z.array(segmentFilterSchema), filters: z.array(segmentFilterSchema),
+45
View File
@@ -0,0 +1,45 @@
/**
* Activity tracking types
*/
/**
* Activity types that can be tracked
*/
export enum ActivityType {
EVENT_TRIGGERED = 'event.triggered',
EMAIL_SENT = 'email.sent',
EMAIL_DELIVERED = 'email.delivered',
EMAIL_OPENED = 'email.opened',
EMAIL_CLICKED = 'email.clicked',
EMAIL_BOUNCED = 'email.bounced',
CAMPAIGN_SENT = 'campaign.sent',
CAMPAIGN_SCHEDULED = 'campaign.scheduled',
WORKFLOW_STARTED = 'workflow.started',
WORKFLOW_COMPLETED = 'workflow.completed',
WORKFLOW_EMAIL_SCHEDULED = 'workflow.email.scheduled',
}
/**
* Unified activity item
*/
export interface Activity {
id: string;
type: ActivityType;
timestamp: Date;
contactEmail?: string;
contactId?: string;
metadata: Record<string, unknown>;
}
/**
* Activity statistics for dashboard
*/
export interface ActivityStats {
totalEvents: number;
totalEmailsSent: number;
totalEmailsOpened: number;
totalEmailsClicked: number;
totalWorkflowsStarted: number;
openRate: number;
clickRate: number;
}
+36
View File
@@ -0,0 +1,36 @@
/**
* Billing and usage limit types
*/
/**
* Usage information for a specific email category
*/
export interface CategoryUsage {
limit: number | null; // null = unlimited
usage: number;
percentage: number; // 0-100
isWarning: boolean; // true if >= 80%
isBlocked: boolean; // true if >= 100%
}
/**
* Complete billing limits and usage for a project
*/
export interface BillingLimitsResponse {
workflows: CategoryUsage;
campaigns: CategoryUsage;
transactional: CategoryUsage;
currency: string | null;
}
/**
* Result of limit check
*/
export interface LimitCheckResult {
allowed: boolean;
warning: boolean; // true if >= 80% but < 100%
usage: number;
limit: number | null;
percentage: number;
message?: string;
}
+38
View File
@@ -0,0 +1,38 @@
/**
* Campaign service types
*/
import type {CampaignAudienceType} from '@plunk/db';
import type {FilterCondition} from '../segments/index.js';
/**
* Data for creating a new campaign
*/
export interface CreateCampaignData {
name: string;
description?: string;
subject: string;
body: string;
from: string;
fromName?: string | null;
replyTo?: string | null;
audienceType: CampaignAudienceType;
audienceCondition?: FilterCondition;
segmentId?: string;
}
/**
* Data for updating an existing campaign
*/
export interface UpdateCampaignData {
name?: string;
description?: string;
subject?: string;
body?: string;
from?: string;
fromName?: string | null;
replyTo?: string | null;
audienceType?: CampaignAudienceType;
audienceCondition?: FilterCondition;
segmentId?: string;
}
+10
View File
@@ -0,0 +1,10 @@
/**
* API service types
* Response and data structures for API endpoints
*/
export * from './activity.js';
export * from './billing.js';
export * from './campaign.js';
export * from './membership.js';
export * from './verification.js';
+31
View File
@@ -0,0 +1,31 @@
/**
* Project membership types
*/
import type {Role} from '@plunk/db';
/**
* Project member with user email
*/
export interface MemberWithEmail {
userId: string;
email: string;
role: Role;
createdAt: Date;
}
/**
* Project owner information
*/
export interface OwnerInfo {
userId: string;
email: string;
}
/**
* Information about disabled projects for a user
*/
export interface DisabledProjectInfo {
hasDisabledProject: boolean;
disabledProjectNames: string[];
}
+18
View File
@@ -0,0 +1,18 @@
/**
* Email verification types
*/
/**
* Result of email address verification
*/
export interface EmailVerificationResult {
email: string;
valid: boolean;
isDisposable: boolean;
isTypo: boolean;
isPlusAddressed: boolean;
domainExists: boolean;
hasMxRecords: boolean;
suggestedEmail?: string;
reasons: string[];
}
+1
View File
@@ -0,0 +1 @@
export * from './pagination.js';
+33
View File
@@ -0,0 +1,33 @@
/**
* Generic pagination types for the Plunk platform
*/
/**
* Offset-based pagination response
* Used for: Templates, Workflows, and other paginated lists with fixed page sizes
*
* @template T - The type of items in the paginated response
*/
export interface PaginatedResponse<T> {
data: T[];
total: number;
page: number;
pageSize: number;
totalPages: number;
}
/**
* Cursor-based pagination response
* Used for: Contacts, Activities, and other large datasets requiring efficient pagination
*
* Cursor pagination is more efficient for large datasets (1M+ rows) as it doesn't
* require offset calculations and provides stable pagination when data changes.
*
* @template T - The type of items in the paginated response
*/
export interface CursorPaginatedResponse<T> {
data: T[];
cursor?: string;
hasMore: boolean;
total?: number; // Optional: computed on first page only for performance
}
+18 -87
View File
@@ -1,94 +1,25 @@
// Segment filter types /**
export type SegmentFilterOperator = * @plunk/types
// Standard operators (for contact fields) * Centralized type definitions for the Plunk platform
| 'equals' */
| 'notEquals'
| 'contains'
| 'notContains'
| 'greaterThan'
| 'lessThan'
| 'greaterThanOrEqual'
| 'lessThanOrEqual'
| 'exists'
| 'notExists'
| 'within'
// Event-based operators
| 'triggered' // Event/email activity occurred (any time)
| 'triggeredWithin' // Event/email activity occurred within timeframe
| 'notTriggered'; // Event/email activity never occurred
export type SegmentFilterLogic = 'AND' | 'OR'; // Common utility types
export * from './common/index.js';
export interface SegmentFilter { // Job queue types
field: string; export * from './jobs/index.js';
operator: SegmentFilterOperator;
value?: any;
unit?: 'days' | 'hours' | 'minutes';
}
export interface FilterGroup { // API service types
filters: SegmentFilter[]; export * from './api/index.js';
conditions?: FilterCondition;
}
export interface FilterCondition { // Notification types
logic: SegmentFilterLogic; export * from './notifications/index.js';
groups: FilterGroup[];
}
export interface CreateSegmentData { // Extended Prisma types
name: string; export * from './prisma/index.js';
description?: string;
condition: FilterCondition;
trackMembership?: boolean;
}
export interface UpdateSegmentData { // Segment and filter types
name?: string; export * from './segments/index.js';
description?: string;
condition?: FilterCondition;
trackMembership?: boolean;
}
export interface SegmentMembershipComputeResult { // Security types
added: number; export * from './security/index.js';
removed: number;
total: number;
}
// Security status types
export interface SecurityRateData {
total: number;
bounces: number;
complaints: number;
bounceRate: number;
complaintRate: number;
}
export interface SecurityStatus {
projectId: string;
isHealthy: boolean;
shouldDisable: boolean;
sevenDay: SecurityRateData;
allTime: SecurityRateData;
violations: string[];
warnings: string[];
}
export interface SecurityThresholds {
MIN_EMAILS_FOR_ENFORCEMENT: number;
BOUNCE_7DAY_WARNING: number;
BOUNCE_7DAY_CRITICAL: number;
BOUNCE_ALLTIME_WARNING: number;
BOUNCE_ALLTIME_CRITICAL: number;
COMPLAINT_7DAY_WARNING: number;
COMPLAINT_7DAY_CRITICAL: number;
COMPLAINT_ALLTIME_WARNING: number;
COMPLAINT_ALLTIME_CRITICAL: number;
}
export interface ProjectSecurityMetrics {
status: SecurityStatus;
thresholds: SecurityThresholds;
isDisabled: boolean;
}
+23
View File
@@ -0,0 +1,23 @@
/**
* Campaign queue job data types
*/
/**
* Job data for processing a batch of campaign recipients
* Used by: campaignQueue worker
*/
export interface CampaignBatchJobData {
campaignId: string;
batchNumber: number;
offset: number;
limit: number;
cursor?: string; // For cursor-based pagination
}
/**
* Job data for sending a scheduled campaign
* Used by: scheduledQueue worker
*/
export interface ScheduledCampaignJobData {
campaignId: string;
}
+11
View File
@@ -0,0 +1,11 @@
/**
* Email queue job data types
*/
/**
* Job data for sending a single email
* Used by: emailQueue worker
*/
export interface SendEmailJobData {
emailId: string;
}
+23
View File
@@ -0,0 +1,23 @@
/**
* Import and bulk operation queue job data types
*/
/**
* Job data for importing contacts from CSV
* Used by: importQueue worker
*/
export interface ContactImportJobData {
projectId: string;
csvData: string; // Base64 encoded CSV content
filename: string;
}
/**
* Job data for bulk contact actions (subscribe, unsubscribe, delete)
* Used by: bulkContactQueue worker
*/
export interface BulkContactActionJobData {
projectId: string;
contactIds: string[];
operation: 'subscribe' | 'unsubscribe' | 'delete';
}
+10
View File
@@ -0,0 +1,10 @@
/**
* Queue job data types
* Centralized type definitions for all BullMQ job payloads
*/
export * from './email.js';
export * from './campaign.js';
export * from './workflow.js';
export * from './import.js';
export * from './maintenance.js';
+29
View File
@@ -0,0 +1,29 @@
/**
* Maintenance and background task queue job data types
*/
/**
* Job data for updating segment membership counts
* Used by: segmentCountQueue worker
*/
export interface SegmentCountJobData {
projectId?: string; // Optional: if provided, only update this project's segments
}
/**
* Job data for domain verification checks
* Used by: domainVerificationQueue worker
*/
// eslint-disable-next-line @typescript-eslint/no-empty-object-type
export interface DomainVerificationJobData {
// Empty for now - processes all domains
}
/**
* Job data for cleaning up old API request logs
* Used by: apiRequestCleanupQueue worker
*/
// eslint-disable-next-line @typescript-eslint/no-empty-object-type
export interface ApiRequestCleanupJobData {
// Empty - cleans up old API request logs
}
+14
View File
@@ -0,0 +1,14 @@
/**
* Workflow queue job data types
*/
/**
* Job data for executing a workflow step
* Used by: workflowQueue worker
*/
export interface WorkflowStepJobData {
executionId: string;
stepId: string;
type?: 'process-step' | 'timeout'; // Job type for different handling
stepExecutionId?: string; // For timeout jobs, reference to the step execution
}
@@ -0,0 +1 @@
export * from './ntfy.js';
+41
View File
@@ -0,0 +1,41 @@
/**
* Notification types for ntfy.sh integration
*/
/**
* Priority levels for ntfy notifications
* Based on ntfy.sh documentation
*/
export enum NtfyPriority {
MIN = 1, // No vibration/sound, relegated to "Other notifications"
LOW = 2, // No vibration/sound, hidden until drawer opened
DEFAULT = 3, // Short vibration and sound (standard)
HIGH = 4, // Long vibration, pop-over notification
MAX = 5, // Long vibration bursts, pop-over notification
}
/**
* Tags for ntfy notifications (emoji shortcuts)
*/
export enum NtfyTag {
WARNING = 'warning',
ERROR = 'rotating_light',
SUCCESS = 'white_check_mark',
MONEY = 'money_with_wings',
SHIELD = 'shield',
ROCKET = 'rocket',
BELL = 'bell',
CHART = 'chart_with_upwards_trend',
SKULL = 'skull',
INFO = 'information_source',
}
/**
* Notification payload for ntfy
*/
export interface NtfyNotification {
title: string;
message: string;
priority?: NtfyPriority;
tags?: NtfyTag[];
}
+82
View File
@@ -0,0 +1,82 @@
/**
* Extended Prisma types
* Types that extend Prisma models with additional relations or computed fields
*/
import type {
Workflow,
WorkflowExecution,
WorkflowStep,
WorkflowStepExecution,
WorkflowTransition,
Template,
Contact,
Prisma,
} from '@plunk/db';
/**
* Workflow with all steps, transitions, and template details
* Used for workflow editor and detailed workflow views
*/
export interface WorkflowWithDetails extends Workflow {
steps: Array<
WorkflowStep & {
template?: {id: string; name: string} | null;
outgoingTransitions: WorkflowTransition[];
incomingTransitions: WorkflowTransition[];
}
>;
}
/**
* Workflow execution with full context
* Used for execution details and monitoring
*/
export interface WorkflowExecutionWithDetails extends WorkflowExecution {
workflow: Workflow;
contact: {id: string; email: string};
currentStep?: WorkflowStep | null;
stepExecutions: WorkflowStepExecution[];
}
/**
* Workflow execution with all relations loaded
* Used internally by workflow execution engine
*/
export type WorkflowExecutionWithRelations = WorkflowExecution & {
contact: Contact;
workflow: Workflow;
};
/**
* Workflow step with optional template
* Used by step execution logic
*/
export type WorkflowStepWithTemplate = WorkflowStep & {
template?: Template | null;
};
/**
* Workflow step with outgoing transitions loaded
* Used for flow control and navigation
*/
export type WorkflowStepWithTransitions = WorkflowStep & {
outgoingTransitions?: Array<{
id: string;
condition: Prisma.JsonValue;
priority: number;
toStep: WorkflowStep;
}>;
};
/**
* Step configuration (JSON value)
* Type-safe alias for workflow step config
*/
export type StepConfig = Prisma.JsonValue;
/**
* Step execution result
* Generic key-value result from step execution
*/
export type StepResult = Record<string, unknown>;
+1
View File
@@ -0,0 +1 @@
export * from './extended.js';
+39
View File
@@ -0,0 +1,39 @@
/**
* Security and project health types
*/
export interface SecurityRateData {
total: number;
bounces: number;
complaints: number;
bounceRate: number;
complaintRate: number;
}
export interface SecurityStatus {
projectId: string;
isHealthy: boolean;
shouldDisable: boolean;
sevenDay: SecurityRateData;
allTime: SecurityRateData;
violations: string[];
warnings: string[];
}
export interface SecurityThresholds {
MIN_EMAILS_FOR_ENFORCEMENT: number;
BOUNCE_7DAY_WARNING: number;
BOUNCE_7DAY_CRITICAL: number;
BOUNCE_ALLTIME_WARNING: number;
BOUNCE_ALLTIME_CRITICAL: number;
COMPLAINT_7DAY_WARNING: number;
COMPLAINT_7DAY_CRITICAL: number;
COMPLAINT_ALLTIME_WARNING: number;
COMPLAINT_ALLTIME_CRITICAL: number;
}
export interface ProjectSecurityMetrics {
status: SecurityStatus;
thresholds: SecurityThresholds;
isDisabled: boolean;
}
+61
View File
@@ -0,0 +1,61 @@
/**
* Segment and filter types
*/
// Segment filter types
export type SegmentFilterOperator =
// Standard operators (for contact fields)
| 'equals'
| 'notEquals'
| 'contains'
| 'notContains'
| 'greaterThan'
| 'lessThan'
| 'greaterThanOrEqual'
| 'lessThanOrEqual'
| 'exists'
| 'notExists'
| 'within'
// Event-based operators
| 'triggered' // Event/email activity occurred (any time)
| 'triggeredWithin' // Event/email activity occurred within timeframe
| 'notTriggered'; // Event/email activity never occurred
export type SegmentFilterLogic = 'AND' | 'OR';
export interface SegmentFilter {
field: string;
operator: SegmentFilterOperator;
value?: any;
unit?: 'days' | 'hours' | 'minutes';
}
export interface FilterGroup {
filters: SegmentFilter[];
conditions?: FilterCondition;
}
export interface FilterCondition {
logic: SegmentFilterLogic;
groups: FilterGroup[];
}
export interface CreateSegmentData {
name: string;
description?: string;
condition: FilterCondition;
trackMembership?: boolean;
}
export interface UpdateSegmentData {
name?: string;
description?: string;
condition?: FilterCondition;
trackMembership?: boolean;
}
export interface SegmentMembershipComputeResult {
added: number;
removed: number;
total: number;
}