types: Abstract inline interfaces to @plunk/types
This commit is contained in:
@@ -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')
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
@@ -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<CampaignBatchJobData>(
|
||||
|
||||
@@ -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';
|
||||
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
@@ -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<ScheduledCampaignJobData>(
|
||||
|
||||
@@ -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';
|
||||
|
||||
/**
|
||||
|
||||
@@ -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() {
|
||||
|
||||
@@ -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<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
|
||||
*
|
||||
@@ -107,7 +58,7 @@ export class ActivityService {
|
||||
contactId?: string,
|
||||
startDate?: Date,
|
||||
endDate?: Date,
|
||||
): Promise<PaginatedActivities> {
|
||||
): Promise<CursorPaginatedResponse<Activity>> {
|
||||
// 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,
|
||||
};
|
||||
}
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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<PaginatedContacts> {
|
||||
): Promise<CursorPaginatedResponse<Contact>> {
|
||||
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,
|
||||
|
||||
@@ -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';
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
*/
|
||||
|
||||
@@ -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<PaginatedContacts> {
|
||||
): Promise<PaginatedResponse<Contact>> {
|
||||
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,
|
||||
|
||||
@@ -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<PaginatedTemplates> {
|
||||
): Promise<PaginatedResponse<Template>> {
|
||||
const skip = (page - 1) * pageSize;
|
||||
|
||||
const where: Prisma.TemplateWhereInput = {
|
||||
@@ -51,7 +44,7 @@ export class TemplateService {
|
||||
]);
|
||||
|
||||
return {
|
||||
templates,
|
||||
data: templates,
|
||||
total,
|
||||
page,
|
||||
pageSize,
|
||||
|
||||
@@ -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<PaginatedWorkflows> {
|
||||
public static async list(projectId: string, page = 1, pageSize = 20, search?: string): Promise<PaginatedResponse<Workflow>> {
|
||||
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,
|
||||
|
||||
@@ -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,
|
||||
},
|
||||
|
||||
@@ -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<string, unknown> | 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<string, unknown> | null || {}),
|
||||
};
|
||||
|
||||
return replaceVariables(subject, contactData);
|
||||
|
||||
@@ -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<BillingLimitsData>(
|
||||
const {data, error, mutate, isLoading} = useSWR<BillingLimitsResponse>(
|
||||
projectId && billingEnabled ? `/users/@me/projects/${projectId}/billing-limits` : null,
|
||||
{
|
||||
revalidateOnFocus: false,
|
||||
|
||||
@@ -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<string, unknown>;
|
||||
}
|
||||
|
||||
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<ContactsResponse>(
|
||||
const {data, error, mutate, isLoading} = useSWR<CursorPaginatedResponse<Contact>>(
|
||||
`/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,
|
||||
|
||||
@@ -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<unknown>;
|
||||
type CampaignsResponse = PaginatedResponse<unknown>;
|
||||
|
||||
export interface DashboardStats {
|
||||
totalContacts: number;
|
||||
@@ -46,7 +26,7 @@ export function useDashboardStats(): DashboardStats {
|
||||
const {data: contactsData, error: contactsError, isLoading: isLoadingContacts} = useSWR<ContactsResponse>('/contacts?limit=1');
|
||||
|
||||
// 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
|
||||
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 {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[];
|
||||
|
||||
Reference in New Issue
Block a user