types: Abstract inline interfaces to @plunk/types
This commit is contained in:
@@ -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
|
||||||
|
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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
|
||||||
|
|
||||||
|
|||||||
@@ -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';
|
||||||
|
|
||||||
|
|||||||
@@ -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)
|
||||||
|
|||||||
@@ -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
|
||||||
|
|
||||||
|
|||||||
@@ -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>(
|
||||||
|
|||||||
@@ -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() {
|
||||||
|
|||||||
@@ -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,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,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
|
||||||
|
|||||||
@@ -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,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,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
|
||||||
|
|||||||
@@ -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
|
||||||
*/
|
*/
|
||||||
|
|||||||
@@ -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,
|
||||||
|
|||||||
@@ -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,
|
||||||
|
|||||||
@@ -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,
|
||||||
|
|||||||
@@ -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);
|
||||||
|
|||||||
@@ -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,
|
||||||
|
|||||||
@@ -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,
|
||||||
|
|||||||
@@ -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;
|
||||||
|
|||||||
@@ -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,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),
|
||||||
|
|||||||
@@ -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;
|
||||||
|
}
|
||||||
@@ -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;
|
||||||
|
}
|
||||||
@@ -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;
|
||||||
|
}
|
||||||
@@ -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';
|
||||||
@@ -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[];
|
||||||
|
}
|
||||||
@@ -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[];
|
||||||
|
}
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
export * from './pagination.js';
|
||||||
@@ -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
@@ -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;
|
|
||||||
}
|
|
||||||
|
|||||||
@@ -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;
|
||||||
|
}
|
||||||
@@ -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;
|
||||||
|
}
|
||||||
@@ -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';
|
||||||
|
}
|
||||||
@@ -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';
|
||||||
@@ -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
|
||||||
|
}
|
||||||
@@ -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';
|
||||||
@@ -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[];
|
||||||
|
}
|
||||||
@@ -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>;
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
export * from './extended.js';
|
||||||
@@ -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;
|
||||||
|
}
|
||||||
@@ -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;
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user