diff --git a/CLAUDE.md b/CLAUDE.md index b937dce..ce313fb 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -108,6 +108,7 @@ between groups. - Consistent type imports preferred: `import type { ... }` - Unused vars allowed with `_` prefix - Strict type checking enabled across all packages +- Try to avoid inline types in favor of shared types in `@plunk/types` ### Component Structure diff --git a/apps/api/src/controllers/Activity.ts b/apps/api/src/controllers/Activity.ts index dc39639..570eef0 100644 --- a/apps/api/src/controllers/Activity.ts +++ b/apps/api/src/controllers/Activity.ts @@ -1,9 +1,10 @@ import {Controller, Get, Middleware} from '@overnightjs/core'; import type {NextFunction, Request, Response} from 'express'; +import {ActivityType} from '@plunk/types'; import type {AuthResponse} 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'; @Controller('activity') diff --git a/apps/api/src/jobs/api-request-cleanup-processor.ts b/apps/api/src/jobs/api-request-cleanup-processor.ts index 603a5fe..e819866 100644 --- a/apps/api/src/jobs/api-request-cleanup-processor.ts +++ b/apps/api/src/jobs/api-request-cleanup-processor.ts @@ -1,3 +1,4 @@ +import type {ApiRequestCleanupJobData} from '@plunk/types'; import type {Job} from 'bullmq'; import {Worker} from 'bullmq'; import type {RedisOptions} from 'ioredis'; @@ -5,7 +6,6 @@ import signale from 'signale'; import {REDIS_URL} from '../app/constants.js'; import {prisma} from '../database/prisma.js'; -import type {ApiRequestCleanupJobData} from '../services/QueueService.js'; /** * API Request Cleanup Worker diff --git a/apps/api/src/jobs/bulk-contact-processor.ts b/apps/api/src/jobs/bulk-contact-processor.ts index aec45e0..280df01 100644 --- a/apps/api/src/jobs/bulk-contact-processor.ts +++ b/apps/api/src/jobs/bulk-contact-processor.ts @@ -3,11 +3,12 @@ * Processes bulk subscribe, unsubscribe, and delete operations */ +import type {BulkContactActionJobData} from '@plunk/types'; import {type Job, Worker} from 'bullmq'; import signale from 'signale'; 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 diff --git a/apps/api/src/jobs/campaign-processor.ts b/apps/api/src/jobs/campaign-processor.ts index c581e89..58eada9 100644 --- a/apps/api/src/jobs/campaign-processor.ts +++ b/apps/api/src/jobs/campaign-processor.ts @@ -3,11 +3,12 @@ * Processes campaign batches (queues emails for each contact in the batch) */ +import type {CampaignBatchJobData} from '@plunk/types'; import {type Job, Worker} from 'bullmq'; import signale from 'signale'; import {CampaignService} from '../services/CampaignService.js'; -import {type CampaignBatchJobData, campaignQueue} from '../services/QueueService.js'; +import {campaignQueue} from '../services/QueueService.js'; export function createCampaignWorker() { const worker = new Worker( diff --git a/apps/api/src/jobs/domain-verification-processor.ts b/apps/api/src/jobs/domain-verification-processor.ts index 9c51ef0..dfd2d84 100644 --- a/apps/api/src/jobs/domain-verification-processor.ts +++ b/apps/api/src/jobs/domain-verification-processor.ts @@ -3,10 +3,11 @@ * Processes domain verification jobs from the BullMQ queue */ +import type {DomainVerificationJobData} from '@plunk/types'; import {type Job, Worker} from 'bullmq'; import signale from 'signale'; -import {type DomainVerificationJobData, domainVerificationQueue} from '../services/QueueService.js'; +import {domainVerificationQueue} from '../services/QueueService.js'; import {checkDomainVerifications} from './domain-verification.js'; diff --git a/apps/api/src/jobs/email-processor.ts b/apps/api/src/jobs/email-processor.ts index e929b8c..537dfc8 100644 --- a/apps/api/src/jobs/email-processor.ts +++ b/apps/api/src/jobs/email-processor.ts @@ -4,16 +4,17 @@ */ import {CampaignStatus, EmailSourceType, EmailStatus} from '@plunk/db'; +import type {SendEmailJobData} from '@plunk/types'; import {type Job, Worker} from 'bullmq'; import signale from 'signale'; +import {DASHBOARD_URI, EMAIL_RATE_LIMIT_PER_SECOND} from '../app/constants.js'; import {prisma} from '../database/prisma.js'; import {EmailService} from '../services/EmailService.js'; import {EventService} from '../services/EventService.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 {DASHBOARD_URI, EMAIL_RATE_LIMIT_PER_SECOND} from '../app/constants.js'; /** * Determine the email sending rate limit (emails per second) diff --git a/apps/api/src/jobs/import-processor.ts b/apps/api/src/jobs/import-processor.ts index a1ccfc5..f98ccea 100644 --- a/apps/api/src/jobs/import-processor.ts +++ b/apps/api/src/jobs/import-processor.ts @@ -3,6 +3,7 @@ * Processes CSV contact imports with validation and batch processing */ +import type {ContactImportJobData} from '@plunk/types'; import {type Job, Worker} from 'bullmq'; import {parse} from 'csv-parse/sync'; import signale from 'signale'; @@ -10,7 +11,7 @@ import signale from 'signale'; import {prisma} from '../database/prisma.js'; import {ContactService} from '../services/ContactService.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 diff --git a/apps/api/src/jobs/scheduled-processor.ts b/apps/api/src/jobs/scheduled-processor.ts index 1aba2c5..0e94d42 100644 --- a/apps/api/src/jobs/scheduled-processor.ts +++ b/apps/api/src/jobs/scheduled-processor.ts @@ -4,12 +4,13 @@ */ import {CampaignStatus} from '@plunk/db'; +import type {ScheduledCampaignJobData} from '@plunk/types'; import {type Job, Worker} from 'bullmq'; import signale from 'signale'; import {prisma} from '../database/prisma.js'; import {CampaignService} from '../services/CampaignService.js'; -import {type ScheduledCampaignJobData, scheduledQueue} from '../services/QueueService.js'; +import {scheduledQueue} from '../services/QueueService.js'; export function createScheduledCampaignWorker() { const worker = new Worker( diff --git a/apps/api/src/jobs/segment-count-processor.ts b/apps/api/src/jobs/segment-count-processor.ts index 03e8d98..836c941 100644 --- a/apps/api/src/jobs/segment-count-processor.ts +++ b/apps/api/src/jobs/segment-count-processor.ts @@ -3,12 +3,13 @@ * Processes segment count update jobs from the BullMQ queue */ +import type {SegmentCountJobData} from '@plunk/types'; import {type Job, Worker} from 'bullmq'; import signale from 'signale'; import {prisma} from '../database/prisma.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'; /** diff --git a/apps/api/src/jobs/workflow-processor-queue.ts b/apps/api/src/jobs/workflow-processor-queue.ts index 338ab37..36fcfc3 100644 --- a/apps/api/src/jobs/workflow-processor-queue.ts +++ b/apps/api/src/jobs/workflow-processor-queue.ts @@ -3,10 +3,11 @@ * Processes workflow steps from the queue (for delayed steps) */ +import type {WorkflowStepJobData} from '@plunk/types'; import {type Job, Worker} from 'bullmq'; import signale from 'signale'; -import {workflowQueue, type WorkflowStepJobData} from '../services/QueueService.js'; +import {workflowQueue} from '../services/QueueService.js'; import {WorkflowExecutionService} from '../services/WorkflowExecutionService.js'; export function createWorkflowWorker() { diff --git a/apps/api/src/services/ActivityService.ts b/apps/api/src/services/ActivityService.ts index 6e3ef61..2fe1703 100644 --- a/apps/api/src/services/ActivityService.ts +++ b/apps/api/src/services/ActivityService.ts @@ -1,61 +1,12 @@ import type {Prisma} from '@plunk/db'; +import {ActivityType} from '@plunk/types'; +import type {Activity, ActivityStats, CursorPaginatedResponse} from '@plunk/types'; import signale from 'signale'; import {prisma} from '../database/prisma.js'; import {redis} from '../database/redis.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; -} - -/** - * 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 * @@ -107,7 +58,7 @@ export class ActivityService { contactId?: string, startDate?: Date, endDate?: Date, - ): Promise { + ): Promise> { // Cap limit to prevent abuse 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; return { - activities: results, - nextCursor, + data: results, + cursor: nextCursor, hasMore, }; } diff --git a/apps/api/src/services/BillingLimitService.ts b/apps/api/src/services/BillingLimitService.ts index 6155d5f..4c11f02 100644 --- a/apps/api/src/services/BillingLimitService.ts +++ b/apps/api/src/services/BillingLimitService.ts @@ -1,4 +1,5 @@ import {EmailSourceType} from '@plunk/db'; +import type {CategoryUsage, BillingLimitsResponse, LimitCheckResult} from '@plunk/types'; import {BillingLimitExceededEmail, BillingLimitWarningEmail, sendPlatformEmail} from '@plunk/email'; import React from 'react'; import signale from 'signale'; @@ -11,39 +12,6 @@ import {Keys} from './keys.js'; import {MembershipService} from './MembershipService.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 * Handles usage tracking and enforcement of billing limits per email category diff --git a/apps/api/src/services/CampaignService.ts b/apps/api/src/services/CampaignService.ts index 65b21bf..8815654 100644 --- a/apps/api/src/services/CampaignService.ts +++ b/apps/api/src/services/CampaignService.ts @@ -1,6 +1,6 @@ import type {Campaign, Contact, Prisma} 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 {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) -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 { /** * Create a new campaign diff --git a/apps/api/src/services/ContactService.ts b/apps/api/src/services/ContactService.ts index e3b792d..34233de 100644 --- a/apps/api/src/services/ContactService.ts +++ b/apps/api/src/services/ContactService.ts @@ -1,18 +1,11 @@ import {type Contact, Prisma} from '@plunk/db'; 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 {HttpException} from '../exceptions/index.js'; import {EventService} from './EventService.js'; -export interface PaginatedContacts { - contacts: Contact[]; - total: number; - cursor?: string; - hasMore: boolean; -} - export class ContactService { /** * Get all contacts for a project with cursor-based pagination @@ -23,7 +16,7 @@ export class ContactService { limit = 20, cursor?: string, search?: string, - ): Promise { + ): Promise> { const where: Prisma.ContactWhereInput = { projectId, ...(search @@ -58,7 +51,7 @@ export class ContactService { const total = !cursor ? await prisma.contact.count({where}) : 0; return { - contacts: results, + data: results, total, cursor: nextCursor, hasMore, diff --git a/apps/api/src/services/EmailVerificationService.ts b/apps/api/src/services/EmailVerificationService.ts index 71f8547..a271662 100644 --- a/apps/api/src/services/EmailVerificationService.ts +++ b/apps/api/src/services/EmailVerificationService.ts @@ -1,19 +1,8 @@ import {promises as dns} from 'dns'; import {run} from '@zootools/email-spell-checker'; +import type {EmailVerificationResult} from '@plunk/types'; 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 = 'https://raw.githubusercontent.com/disposable-email-domains/disposable-email-domains/main/disposable_email_blocklist.conf'; const DISPOSABLE_DOMAINS_CACHE_KEY = 'email:disposable_domains'; diff --git a/apps/api/src/services/MembershipService.ts b/apps/api/src/services/MembershipService.ts index 13b7f03..d8d91f1 100644 --- a/apps/api/src/services/MembershipService.ts +++ b/apps/api/src/services/MembershipService.ts @@ -1,4 +1,5 @@ import type {Membership, Role} from '@plunk/db'; +import type {MemberWithEmail, OwnerInfo, DisabledProjectInfo} from '@plunk/types'; import {prisma} from '../database/prisma.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; -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 * Centralizes all membership-related database queries with caching diff --git a/apps/api/src/services/NtfyService.ts b/apps/api/src/services/NtfyService.ts index f4238b8..cf00d99 100644 --- a/apps/api/src/services/NtfyService.ts +++ b/apps/api/src/services/NtfyService.ts @@ -1,40 +1,6 @@ +import {NtfyPriority, NtfyTag, type NtfyNotification} from '@plunk/types'; 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 * Supports configurable ntfy server URL via NTFY_URL environment variable diff --git a/apps/api/src/services/QueueService.ts b/apps/api/src/services/QueueService.ts index d1560dc..0dd0787 100644 --- a/apps/api/src/services/QueueService.ts +++ b/apps/api/src/services/QueueService.ts @@ -1,63 +1,21 @@ import {type Job, Queue} from 'bullmq'; import type {RedisOptions} from 'ioredis'; 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 {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 */ diff --git a/apps/api/src/services/SegmentService.ts b/apps/api/src/services/SegmentService.ts index fdcaf2a..aa4f235 100644 --- a/apps/api/src/services/SegmentService.ts +++ b/apps/api/src/services/SegmentService.ts @@ -1,5 +1,5 @@ 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 {prisma} from '../database/prisma.js'; @@ -11,14 +11,6 @@ import {NtfyService} from './NtfyService.js'; // Re-export types for use in other services 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 * Example: "VIP Customers" -> "vip-customers" @@ -72,7 +64,7 @@ export class SegmentService { segmentId: string, page = 1, pageSize = 20, - ): Promise { + ): Promise> { const segment = await this.get(projectId, segmentId); const condition = segment.condition as unknown as FilterCondition; @@ -90,7 +82,7 @@ export class SegmentService { ]); return { - contacts, + data: contacts, total, page, pageSize, diff --git a/apps/api/src/services/TemplateService.ts b/apps/api/src/services/TemplateService.ts index 5b7d544..5c7af7f 100644 --- a/apps/api/src/services/TemplateService.ts +++ b/apps/api/src/services/TemplateService.ts @@ -1,18 +1,11 @@ import type {Template} from '@plunk/db'; import {Prisma} from '@plunk/db'; +import type {PaginatedResponse} from '@plunk/types'; import {prisma} from '../database/prisma.js'; import {HttpException} from '../exceptions/index.js'; import {buildEmailFieldsUpdate} from '../utils/modelUpdate.js'; -export interface PaginatedTemplates { - templates: Template[]; - total: number; - page: number; - pageSize: number; - totalPages: number; -} - export class TemplateService { /** * Get all templates for a project with pagination @@ -23,7 +16,7 @@ export class TemplateService { pageSize = 20, search?: string, type?: Template['type'], - ): Promise { + ): Promise> { const skip = (page - 1) * pageSize; const where: Prisma.TemplateWhereInput = { @@ -51,7 +44,7 @@ export class TemplateService { ]); return { - templates, + data: templates, total, page, pageSize, diff --git a/apps/api/src/services/WorkflowService.ts b/apps/api/src/services/WorkflowService.ts index 4890ce1..87c759b 100644 --- a/apps/api/src/services/WorkflowService.ts +++ b/apps/api/src/services/WorkflowService.ts @@ -1,5 +1,6 @@ import type {Workflow, WorkflowExecution, WorkflowStep, WorkflowStepExecution, WorkflowTransition} from '@plunk/db'; import {Prisma, WorkflowExecutionStatus} from '@plunk/db'; +import type {PaginatedResponse, WorkflowWithDetails, WorkflowExecutionWithDetails} from '@plunk/types'; import signale from 'signale'; import {prisma} from '../database/prisma.js'; @@ -10,34 +11,11 @@ import {EventService} from './EventService.js'; import {NtfyService} from './NtfyService.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 { /** * Get all workflows for a project with pagination */ - public static async list(projectId: string, page = 1, pageSize = 20, search?: string): Promise { + public static async list(projectId: string, page = 1, pageSize = 20, search?: string): Promise> { const skip = (page - 1) * pageSize; const where: Prisma.WorkflowWhereInput = { @@ -71,7 +49,7 @@ export class WorkflowService { ]); return { - workflows: workflows as Workflow[], + data: workflows as Workflow[], total, page, pageSize, diff --git a/apps/web/src/components/DomainsSettings.tsx b/apps/web/src/components/DomainsSettings.tsx index 07a1351..f24fe42 100644 --- a/apps/web/src/components/DomainsSettings.tsx +++ b/apps/web/src/components/DomainsSettings.tsx @@ -107,7 +107,7 @@ export function DomainsSettings({projectId}: DomainsSettingsProps) { setVerificationStatus(prev => ({ ...prev, [newDomain.id]: { - tokens: newDomain.dkimTokens, + tokens: newDomain.dkimTokens as string[] | null, status: 'Pending', verified: false, }, diff --git a/apps/web/src/components/EmailEditor/EmailEditor.tsx b/apps/web/src/components/EmailEditor/EmailEditor.tsx index f5b3de4..b445a89 100644 --- a/apps/web/src/components/EmailEditor/EmailEditor.tsx +++ b/apps/web/src/components/EmailEditor/EmailEditor.tsx @@ -298,7 +298,7 @@ export function EmailEditor({value, onChange, placeholder, subject, from, replyT subscribeUrl: `${window.location.origin}/subscribe/${contact.id}`, manageUrl: `${window.location.origin}/manage/${contact.id}`, data: contact.data || {}, - ...contact.data, + ...(contact.data as Record | null || {}), }; return replaceVariables(currentHtml, contactData); @@ -317,7 +317,7 @@ export function EmailEditor({value, onChange, placeholder, subject, from, replyT subscribeUrl: `${window.location.origin}/subscribe/${contact.id}`, manageUrl: `${window.location.origin}/manage/${contact.id}`, data: contact.data || {}, - ...contact.data, + ...(contact.data as Record | null || {}), }; return replaceVariables(subject, contactData); diff --git a/apps/web/src/lib/hooks/useBillingLimits.ts b/apps/web/src/lib/hooks/useBillingLimits.ts index 023cbb7..f9b7113 100644 --- a/apps/web/src/lib/hooks/useBillingLimits.ts +++ b/apps/web/src/lib/hooks/useBillingLimits.ts @@ -1,19 +1,9 @@ +import type {BillingLimitsResponse, CategoryUsage} from '@plunk/types'; import useSWR from 'swr'; -export interface CategoryLimit { - usage: number; - limit: number | null; - percentage: number; - isWarning: boolean; - isBlocked: boolean; -} - -export interface BillingLimitsData { - workflows: CategoryLimit; - campaigns: CategoryLimit; - transactional: CategoryLimit; - currency: string | null; -} +// Re-export for backward compatibility +export type CategoryLimit = CategoryUsage; +export type BillingLimitsData = BillingLimitsResponse; /** * 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 */ export function useBillingLimits(projectId: string | undefined, billingEnabled: boolean) { - const {data, error, mutate, isLoading} = useSWR( + const {data, error, mutate, isLoading} = useSWR( projectId && billingEnabled ? `/users/@me/projects/${projectId}/billing-limits` : null, { revalidateOnFocus: false, diff --git a/apps/web/src/lib/hooks/useContacts.ts b/apps/web/src/lib/hooks/useContacts.ts index 16a8ac9..9864442 100644 --- a/apps/web/src/lib/hooks/useContacts.ts +++ b/apps/web/src/lib/hooks/useContacts.ts @@ -1,16 +1,7 @@ +import type {Contact} from '@plunk/db'; +import type {CursorPaginatedResponse} from '@plunk/types'; import useSWR from 'swr'; -export interface Contact { - id: string; - email: string; - data?: Record; -} - -export interface ContactsResponse { - contacts: Contact[]; - total: number; -} - interface UseContactsOptions { limit?: number; search?: string; @@ -28,7 +19,7 @@ export function useContacts(options: UseContactsOptions = {}) { params.set('search', search); } - const {data, error, mutate, isLoading} = useSWR( + const {data, error, mutate, isLoading} = useSWR>( `/contacts?${params.toString()}`, { revalidateOnFocus: false, @@ -37,7 +28,7 @@ export function useContacts(options: UseContactsOptions = {}) { ); return { - contacts: data?.contacts || [], + contacts: data?.data || [], total: data?.total || 0, error, isLoading, diff --git a/apps/web/src/lib/hooks/useDashboardStats.ts b/apps/web/src/lib/hooks/useDashboardStats.ts index bfc35b7..56737c8 100644 --- a/apps/web/src/lib/hooks/useDashboardStats.ts +++ b/apps/web/src/lib/hooks/useDashboardStats.ts @@ -1,29 +1,9 @@ +import type {ActivityStats, CursorPaginatedResponse, PaginatedResponse} from '@plunk/types'; import useSWR from 'swr'; -export interface ActivityStats { - totalEvents: number; - totalEmailsSent: number; - 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; -} +// Specific response types for dashboard (using unknown[] since we only need counts) +type ContactsResponse = CursorPaginatedResponse; +type CampaignsResponse = PaginatedResponse; export interface DashboardStats { totalContacts: number; @@ -46,7 +26,7 @@ export function useDashboardStats(): DashboardStats { const {data: contactsData, error: contactsError, isLoading: isLoadingContacts} = useSWR('/contacts?limit=1'); // Fetch campaigns (only need the total count) - const {data: campaignsData, error: campaignsError, isLoading: isLoadingCampaigns} = useSWR('/campaigns?pageSize=1'); + const {data: campaignsData, error: campaignsError, isLoading: isLoadingCampaigns} = useSWR('/campaigns?page=1&limit=1'); // Still loading if ANY of the requests are still in progress const isLoading = isLoadingActivity || isLoadingContacts || isLoadingCampaigns; diff --git a/apps/web/src/lib/hooks/useDomains.ts b/apps/web/src/lib/hooks/useDomains.ts index 7a4e960..9011d6b 100644 --- a/apps/web/src/lib/hooks/useDomains.ts +++ b/apps/web/src/lib/hooks/useDomains.ts @@ -1,18 +1,9 @@ +import type {Domain} from '@plunk/db'; +import {DomainSchemas} from '@plunk/shared'; import useSWR from 'swr'; -import {DomainSchemas} from '@plunk/shared'; 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 { domain: string; tokens: string[]; diff --git a/packages/shared/src/schemas/index.ts b/packages/shared/src/schemas/index.ts index 2d5f715..650fe23 100644 --- a/packages/shared/src/schemas/index.ts +++ b/packages/shared/src/schemas/index.ts @@ -1,4 +1,5 @@ import {CampaignAudienceType, TemplateType, TrackingMode, WorkflowStepType, WorkflowTriggerType} from '@plunk/db'; +import type {FilterCondition, FilterGroup} from '@plunk/types'; import {z} from 'zod'; 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(), }); -type FilterGroup = { - filters: z.infer[]; - conditions?: FilterCondition; -}; - -type FilterCondition = { - logic: 'AND' | 'OR'; - groups: FilterGroup[]; -}; - const filterGroupSchema: z.ZodType = z.lazy(() => z.object({ filters: z.array(segmentFilterSchema), diff --git a/packages/types/src/api/activity.ts b/packages/types/src/api/activity.ts new file mode 100644 index 0000000..48717f8 --- /dev/null +++ b/packages/types/src/api/activity.ts @@ -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; +} + +/** + * Activity statistics for dashboard + */ +export interface ActivityStats { + totalEvents: number; + totalEmailsSent: number; + totalEmailsOpened: number; + totalEmailsClicked: number; + totalWorkflowsStarted: number; + openRate: number; + clickRate: number; +} diff --git a/packages/types/src/api/billing.ts b/packages/types/src/api/billing.ts new file mode 100644 index 0000000..9dc62b5 --- /dev/null +++ b/packages/types/src/api/billing.ts @@ -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; +} diff --git a/packages/types/src/api/campaign.ts b/packages/types/src/api/campaign.ts new file mode 100644 index 0000000..67f561d --- /dev/null +++ b/packages/types/src/api/campaign.ts @@ -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; +} diff --git a/packages/types/src/api/index.ts b/packages/types/src/api/index.ts new file mode 100644 index 0000000..105f489 --- /dev/null +++ b/packages/types/src/api/index.ts @@ -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'; diff --git a/packages/types/src/api/membership.ts b/packages/types/src/api/membership.ts new file mode 100644 index 0000000..df74517 --- /dev/null +++ b/packages/types/src/api/membership.ts @@ -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[]; +} diff --git a/packages/types/src/api/verification.ts b/packages/types/src/api/verification.ts new file mode 100644 index 0000000..cbccdb7 --- /dev/null +++ b/packages/types/src/api/verification.ts @@ -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[]; +} diff --git a/packages/types/src/common/index.ts b/packages/types/src/common/index.ts new file mode 100644 index 0000000..3897672 --- /dev/null +++ b/packages/types/src/common/index.ts @@ -0,0 +1 @@ +export * from './pagination.js'; diff --git a/packages/types/src/common/pagination.ts b/packages/types/src/common/pagination.ts new file mode 100644 index 0000000..1e83827 --- /dev/null +++ b/packages/types/src/common/pagination.ts @@ -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 { + 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 { + data: T[]; + cursor?: string; + hasMore: boolean; + total?: number; // Optional: computed on first page only for performance +} diff --git a/packages/types/src/index.ts b/packages/types/src/index.ts index e52410a..560698b 100644 --- a/packages/types/src/index.ts +++ b/packages/types/src/index.ts @@ -1,94 +1,25 @@ -// 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 +/** + * @plunk/types + * Centralized type definitions for the Plunk platform + */ -export type SegmentFilterLogic = 'AND' | 'OR'; +// Common utility types +export * from './common/index.js'; -export interface SegmentFilter { - field: string; - operator: SegmentFilterOperator; - value?: any; - unit?: 'days' | 'hours' | 'minutes'; -} +// Job queue types +export * from './jobs/index.js'; -export interface FilterGroup { - filters: SegmentFilter[]; - conditions?: FilterCondition; -} +// API service types +export * from './api/index.js'; -export interface FilterCondition { - logic: SegmentFilterLogic; - groups: FilterGroup[]; -} +// Notification types +export * from './notifications/index.js'; -export interface CreateSegmentData { - name: string; - description?: string; - condition: FilterCondition; - trackMembership?: boolean; -} +// Extended Prisma types +export * from './prisma/index.js'; -export interface UpdateSegmentData { - name?: string; - description?: string; - condition?: FilterCondition; - trackMembership?: boolean; -} +// Segment and filter types +export * from './segments/index.js'; -export interface SegmentMembershipComputeResult { - added: number; - 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; -} +// Security types +export * from './security/index.js'; diff --git a/packages/types/src/jobs/campaign.ts b/packages/types/src/jobs/campaign.ts new file mode 100644 index 0000000..6e0e0e2 --- /dev/null +++ b/packages/types/src/jobs/campaign.ts @@ -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; +} diff --git a/packages/types/src/jobs/email.ts b/packages/types/src/jobs/email.ts new file mode 100644 index 0000000..0bfd20b --- /dev/null +++ b/packages/types/src/jobs/email.ts @@ -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; +} diff --git a/packages/types/src/jobs/import.ts b/packages/types/src/jobs/import.ts new file mode 100644 index 0000000..6b4eba1 --- /dev/null +++ b/packages/types/src/jobs/import.ts @@ -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'; +} diff --git a/packages/types/src/jobs/index.ts b/packages/types/src/jobs/index.ts new file mode 100644 index 0000000..fd74b8a --- /dev/null +++ b/packages/types/src/jobs/index.ts @@ -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'; diff --git a/packages/types/src/jobs/maintenance.ts b/packages/types/src/jobs/maintenance.ts new file mode 100644 index 0000000..d1b6f6b --- /dev/null +++ b/packages/types/src/jobs/maintenance.ts @@ -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 +} diff --git a/packages/types/src/jobs/workflow.ts b/packages/types/src/jobs/workflow.ts new file mode 100644 index 0000000..a171f93 --- /dev/null +++ b/packages/types/src/jobs/workflow.ts @@ -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 +} diff --git a/packages/types/src/notifications/index.ts b/packages/types/src/notifications/index.ts new file mode 100644 index 0000000..1f2dc42 --- /dev/null +++ b/packages/types/src/notifications/index.ts @@ -0,0 +1 @@ +export * from './ntfy.js'; diff --git a/packages/types/src/notifications/ntfy.ts b/packages/types/src/notifications/ntfy.ts new file mode 100644 index 0000000..0351696 --- /dev/null +++ b/packages/types/src/notifications/ntfy.ts @@ -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[]; +} diff --git a/packages/types/src/prisma/extended.ts b/packages/types/src/prisma/extended.ts new file mode 100644 index 0000000..40e0cd6 --- /dev/null +++ b/packages/types/src/prisma/extended.ts @@ -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; diff --git a/packages/types/src/prisma/index.ts b/packages/types/src/prisma/index.ts new file mode 100644 index 0000000..0e05251 --- /dev/null +++ b/packages/types/src/prisma/index.ts @@ -0,0 +1 @@ +export * from './extended.js'; diff --git a/packages/types/src/security/index.ts b/packages/types/src/security/index.ts new file mode 100644 index 0000000..92b3e7a --- /dev/null +++ b/packages/types/src/security/index.ts @@ -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; +} diff --git a/packages/types/src/segments/index.ts b/packages/types/src/segments/index.ts new file mode 100644 index 0000000..6375670 --- /dev/null +++ b/packages/types/src/segments/index.ts @@ -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; +}