Initial push of Plunk Next

This commit is contained in:
Dries Augustyns
2025-12-01 09:56:56 +01:00
parent 07cea20262
commit ff1876d580
566 changed files with 89036 additions and 28423 deletions
+2
View File
@@ -0,0 +1,2 @@
DATABASE_URL=postgresql://postgres:postgres@localhost:55432/postgres
DIRECT_DATABASE_URL=postgresql://postgres:postgres@localhost:55432/postgres
+21
View File
@@ -0,0 +1,21 @@
{
"name": "@plunk/db",
"version": "0.0.0",
"type": "module",
"main": "./dist/index.js",
"types": "./dist/index.d.ts",
"scripts": {
"db:generate": "prisma generate",
"migrate:dev": "prisma migrate dev",
"migrate:prod": "prisma migrate deploy",
"clean": "rimraf node_modules .turbo dist",
"build": "tsc && prisma generate"
},
"dependencies": {
"@prisma/client": "^6.19.0",
"prisma": "^6.19.0"
},
"devDependencies": {
"ts-node": "^10.9.2"
}
}
@@ -0,0 +1,570 @@
-- CreateEnum
CREATE TYPE "AuthMethod" AS ENUM ('PASSWORD', 'GOOGLE_OAUTH', 'GITHUB_OAUTH');
-- CreateEnum
CREATE TYPE "Role" AS ENUM ('OWNER', 'ADMIN', 'MEMBER');
-- CreateEnum
CREATE TYPE "TemplateType" AS ENUM ('TRANSACTIONAL', 'MARKETING');
-- CreateEnum
CREATE TYPE "CampaignStatus" AS ENUM ('DRAFT', 'SCHEDULED', 'SENDING', 'SENT', 'CANCELLED');
-- CreateEnum
CREATE TYPE "CampaignAudienceType" AS ENUM ('ALL', 'FILTERED', 'SEGMENT');
-- CreateEnum
CREATE TYPE "WorkflowTriggerType" AS ENUM ('EVENT', 'MANUAL', 'SCHEDULE');
-- CreateEnum
CREATE TYPE "WorkflowStepType" AS ENUM ('TRIGGER', 'SEND_EMAIL', 'DELAY', 'WAIT_FOR_EVENT', 'CONDITION', 'EXIT', 'WEBHOOK', 'UPDATE_CONTACT');
-- CreateEnum
CREATE TYPE "WorkflowExecutionStatus" AS ENUM ('RUNNING', 'WAITING', 'COMPLETED', 'EXITED', 'FAILED', 'CANCELLED');
-- CreateEnum
CREATE TYPE "StepExecutionStatus" AS ENUM ('PENDING', 'SCHEDULED', 'WAITING', 'RUNNING', 'COMPLETED', 'SKIPPED', 'FAILED');
-- CreateEnum
CREATE TYPE "EmailSourceType" AS ENUM ('TRANSACTIONAL', 'CAMPAIGN', 'WORKFLOW');
-- CreateEnum
CREATE TYPE "EmailStatus" AS ENUM ('PENDING', 'SENDING', 'SENT', 'DELIVERED', 'OPENED', 'CLICKED', 'BOUNCED', 'COMPLAINED', 'FAILED');
-- CreateTable
CREATE TABLE "users" (
"id" TEXT NOT NULL,
"email" TEXT NOT NULL,
"password" TEXT,
"type" "AuthMethod" NOT NULL,
"createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
"updatedAt" TIMESTAMP(3) NOT NULL,
CONSTRAINT "users_pkey" PRIMARY KEY ("id")
);
-- CreateTable
CREATE TABLE "projects" (
"id" TEXT NOT NULL,
"name" TEXT NOT NULL,
"public" TEXT NOT NULL,
"secret" TEXT NOT NULL,
"disabled" BOOLEAN NOT NULL DEFAULT false,
"customer" TEXT,
"subscription" TEXT,
"billingLimitWorkflows" INTEGER,
"billingLimitCampaigns" INTEGER,
"billingLimitTransactional" INTEGER,
"trackingEnabled" BOOLEAN NOT NULL DEFAULT true,
"createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
"updatedAt" TIMESTAMP(3) NOT NULL,
CONSTRAINT "projects_pkey" PRIMARY KEY ("id")
);
-- CreateTable
CREATE TABLE "memberships" (
"userId" TEXT NOT NULL,
"projectId" TEXT NOT NULL,
"role" "Role" NOT NULL DEFAULT 'MEMBER',
"createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
"updatedAt" TIMESTAMP(3) NOT NULL,
CONSTRAINT "memberships_pkey" PRIMARY KEY ("userId","projectId")
);
-- CreateTable
CREATE TABLE "domains" (
"id" TEXT NOT NULL,
"domain" TEXT NOT NULL,
"verified" BOOLEAN NOT NULL DEFAULT false,
"dkimTokens" JSONB,
"projectId" TEXT NOT NULL,
"createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
"updatedAt" TIMESTAMP(3) NOT NULL,
CONSTRAINT "domains_pkey" PRIMARY KEY ("id")
);
-- CreateTable
CREATE TABLE "contacts" (
"id" TEXT NOT NULL,
"email" TEXT NOT NULL,
"data" JSONB,
"subscribed" BOOLEAN NOT NULL DEFAULT true,
"projectId" TEXT NOT NULL,
"createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
"updatedAt" TIMESTAMP(3) NOT NULL,
CONSTRAINT "contacts_pkey" PRIMARY KEY ("id")
);
-- CreateTable
CREATE TABLE "templates" (
"id" TEXT NOT NULL,
"name" TEXT NOT NULL,
"description" TEXT,
"subject" TEXT NOT NULL,
"body" TEXT NOT NULL,
"from" TEXT NOT NULL,
"fromName" TEXT,
"replyTo" TEXT,
"type" "TemplateType" NOT NULL DEFAULT 'MARKETING',
"projectId" TEXT NOT NULL,
"createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
"updatedAt" TIMESTAMP(3) NOT NULL,
CONSTRAINT "templates_pkey" PRIMARY KEY ("id")
);
-- CreateTable
CREATE TABLE "segments" (
"id" TEXT NOT NULL,
"name" TEXT NOT NULL,
"description" TEXT,
"filters" JSONB NOT NULL,
"trackMembership" BOOLEAN NOT NULL DEFAULT false,
"memberCount" INTEGER NOT NULL DEFAULT 0,
"projectId" TEXT NOT NULL,
"createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
"updatedAt" TIMESTAMP(3) NOT NULL,
CONSTRAINT "segments_pkey" PRIMARY KEY ("id")
);
-- CreateTable
CREATE TABLE "segment_memberships" (
"contactId" TEXT NOT NULL,
"segmentId" TEXT NOT NULL,
"enteredAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
"exitedAt" TIMESTAMP(3),
"createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
"updatedAt" TIMESTAMP(3) NOT NULL,
CONSTRAINT "segment_memberships_pkey" PRIMARY KEY ("contactId","segmentId")
);
-- CreateTable
CREATE TABLE "campaigns" (
"id" TEXT NOT NULL,
"name" TEXT NOT NULL,
"description" TEXT,
"status" "CampaignStatus" NOT NULL DEFAULT 'DRAFT',
"subject" TEXT NOT NULL,
"body" TEXT NOT NULL,
"from" TEXT NOT NULL,
"fromName" TEXT,
"replyTo" TEXT,
"audienceType" "CampaignAudienceType" NOT NULL DEFAULT 'ALL',
"audienceFilter" JSONB,
"segmentId" TEXT,
"scheduledFor" TIMESTAMP(3),
"totalRecipients" INTEGER NOT NULL DEFAULT 0,
"sentCount" INTEGER NOT NULL DEFAULT 0,
"deliveredCount" INTEGER NOT NULL DEFAULT 0,
"openedCount" INTEGER NOT NULL DEFAULT 0,
"clickedCount" INTEGER NOT NULL DEFAULT 0,
"bouncedCount" INTEGER NOT NULL DEFAULT 0,
"projectId" TEXT NOT NULL,
"sentAt" TIMESTAMP(3),
"createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
"updatedAt" TIMESTAMP(3) NOT NULL,
CONSTRAINT "campaigns_pkey" PRIMARY KEY ("id")
);
-- CreateTable
CREATE TABLE "workflows" (
"id" TEXT NOT NULL,
"name" TEXT NOT NULL,
"description" TEXT,
"enabled" BOOLEAN NOT NULL DEFAULT false,
"triggerType" "WorkflowTriggerType" NOT NULL,
"triggerConfig" JSONB,
"allowReentry" BOOLEAN NOT NULL DEFAULT false,
"projectId" TEXT NOT NULL,
"createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
"updatedAt" TIMESTAMP(3) NOT NULL,
CONSTRAINT "workflows_pkey" PRIMARY KEY ("id")
);
-- CreateTable
CREATE TABLE "workflow_steps" (
"id" TEXT NOT NULL,
"type" "WorkflowStepType" NOT NULL,
"name" TEXT NOT NULL,
"position" JSONB NOT NULL,
"config" JSONB NOT NULL,
"workflowId" TEXT NOT NULL,
"templateId" TEXT,
"createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
"updatedAt" TIMESTAMP(3) NOT NULL,
CONSTRAINT "workflow_steps_pkey" PRIMARY KEY ("id")
);
-- CreateTable
CREATE TABLE "workflow_transitions" (
"id" TEXT NOT NULL,
"fromStepId" TEXT NOT NULL,
"toStepId" TEXT NOT NULL,
"condition" JSONB,
"priority" INTEGER NOT NULL DEFAULT 0,
"createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
"updatedAt" TIMESTAMP(3) NOT NULL,
CONSTRAINT "workflow_transitions_pkey" PRIMARY KEY ("id")
);
-- CreateTable
CREATE TABLE "workflow_executions" (
"id" TEXT NOT NULL,
"workflowId" TEXT NOT NULL,
"contactId" TEXT NOT NULL,
"status" "WorkflowExecutionStatus" NOT NULL DEFAULT 'RUNNING',
"currentStepId" TEXT,
"exitReason" TEXT,
"context" JSONB,
"startedAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
"completedAt" TIMESTAMP(3),
"createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
"updatedAt" TIMESTAMP(3) NOT NULL,
CONSTRAINT "workflow_executions_pkey" PRIMARY KEY ("id")
);
-- CreateTable
CREATE TABLE "workflow_step_executions" (
"id" TEXT NOT NULL,
"executionId" TEXT NOT NULL,
"stepId" TEXT NOT NULL,
"status" "StepExecutionStatus" NOT NULL DEFAULT 'PENDING',
"scheduledFor" TIMESTAMP(3),
"executeAfter" TIMESTAMP(3),
"output" JSONB,
"error" TEXT,
"startedAt" TIMESTAMP(3),
"completedAt" TIMESTAMP(3),
"createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
"updatedAt" TIMESTAMP(3) NOT NULL,
CONSTRAINT "workflow_step_executions_pkey" PRIMARY KEY ("id")
);
-- CreateTable
CREATE TABLE "emails" (
"id" TEXT NOT NULL,
"contactId" TEXT NOT NULL,
"subject" TEXT NOT NULL,
"body" TEXT NOT NULL,
"from" TEXT NOT NULL,
"fromName" TEXT,
"replyTo" TEXT,
"headers" JSONB,
"attachments" JSONB,
"messageId" TEXT,
"sourceType" "EmailSourceType" NOT NULL,
"templateId" TEXT,
"campaignId" TEXT,
"workflowExecutionId" TEXT,
"workflowStepExecutionId" TEXT,
"status" "EmailStatus" NOT NULL DEFAULT 'PENDING',
"sentAt" TIMESTAMP(3),
"deliveredAt" TIMESTAMP(3),
"openedAt" TIMESTAMP(3),
"clickedAt" TIMESTAMP(3),
"bouncedAt" TIMESTAMP(3),
"complainedAt" TIMESTAMP(3),
"opens" INTEGER NOT NULL DEFAULT 0,
"clicks" INTEGER NOT NULL DEFAULT 0,
"error" TEXT,
"projectId" TEXT NOT NULL,
"createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
"updatedAt" TIMESTAMP(3) NOT NULL,
CONSTRAINT "emails_pkey" PRIMARY KEY ("id")
);
-- CreateTable
CREATE TABLE "events" (
"id" TEXT NOT NULL,
"name" TEXT NOT NULL,
"data" JSONB,
"projectId" TEXT NOT NULL,
"contactId" TEXT,
"emailId" TEXT,
"createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
CONSTRAINT "events_pkey" PRIMARY KEY ("id")
);
-- CreateTable
CREATE TABLE "api_requests" (
"id" TEXT NOT NULL,
"method" TEXT NOT NULL,
"path" TEXT NOT NULL,
"statusCode" INTEGER NOT NULL,
"duration" INTEGER NOT NULL,
"projectId" TEXT,
"userId" TEXT,
"authType" TEXT,
"ip" TEXT,
"userAgent" TEXT,
"errorCode" TEXT,
"errorMessage" TEXT,
"requestSize" INTEGER,
"responseSize" INTEGER,
"createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
CONSTRAINT "api_requests_pkey" PRIMARY KEY ("id")
);
-- CreateIndex
CREATE UNIQUE INDEX "users_email_key" ON "users"("email");
-- CreateIndex
CREATE UNIQUE INDEX "projects_public_key" ON "projects"("public");
-- CreateIndex
CREATE UNIQUE INDEX "projects_secret_key" ON "projects"("secret");
-- CreateIndex
CREATE UNIQUE INDEX "projects_customer_key" ON "projects"("customer");
-- CreateIndex
CREATE UNIQUE INDEX "projects_subscription_key" ON "projects"("subscription");
-- CreateIndex
CREATE INDEX "domains_projectId_idx" ON "domains"("projectId");
-- CreateIndex
CREATE INDEX "domains_projectId_verified_idx" ON "domains"("projectId", "verified");
-- CreateIndex
CREATE UNIQUE INDEX "domains_projectId_domain_key" ON "domains"("projectId", "domain");
-- CreateIndex
CREATE INDEX "contacts_projectId_idx" ON "contacts"("projectId");
-- CreateIndex
CREATE INDEX "contacts_projectId_subscribed_idx" ON "contacts"("projectId", "subscribed");
-- CreateIndex
CREATE UNIQUE INDEX "contacts_projectId_email_key" ON "contacts"("projectId", "email");
-- CreateIndex
CREATE INDEX "templates_projectId_idx" ON "templates"("projectId");
-- CreateIndex
CREATE INDEX "templates_projectId_type_idx" ON "templates"("projectId", "type");
-- CreateIndex
CREATE INDEX "segments_projectId_idx" ON "segments"("projectId");
-- CreateIndex
CREATE INDEX "segment_memberships_segmentId_exitedAt_idx" ON "segment_memberships"("segmentId", "exitedAt");
-- CreateIndex
CREATE INDEX "segment_memberships_contactId_exitedAt_idx" ON "segment_memberships"("contactId", "exitedAt");
-- CreateIndex
CREATE INDEX "segment_memberships_enteredAt_idx" ON "segment_memberships"("enteredAt");
-- CreateIndex
CREATE INDEX "campaigns_projectId_status_idx" ON "campaigns"("projectId", "status");
-- CreateIndex
CREATE INDEX "campaigns_scheduledFor_idx" ON "campaigns"("scheduledFor");
-- CreateIndex
CREATE INDEX "campaigns_segmentId_idx" ON "campaigns"("segmentId");
-- CreateIndex
CREATE INDEX "workflows_projectId_idx" ON "workflows"("projectId");
-- CreateIndex
CREATE INDEX "workflows_projectId_enabled_idx" ON "workflows"("projectId", "enabled");
-- CreateIndex
CREATE INDEX "workflow_steps_workflowId_idx" ON "workflow_steps"("workflowId");
-- CreateIndex
CREATE INDEX "workflow_steps_templateId_idx" ON "workflow_steps"("templateId");
-- CreateIndex
CREATE INDEX "workflow_transitions_fromStepId_idx" ON "workflow_transitions"("fromStepId");
-- CreateIndex
CREATE INDEX "workflow_transitions_toStepId_idx" ON "workflow_transitions"("toStepId");
-- CreateIndex
CREATE INDEX "workflow_executions_workflowId_contactId_idx" ON "workflow_executions"("workflowId", "contactId");
-- CreateIndex
CREATE INDEX "workflow_executions_workflowId_status_idx" ON "workflow_executions"("workflowId", "status");
-- CreateIndex
CREATE INDEX "workflow_executions_contactId_status_idx" ON "workflow_executions"("contactId", "status");
-- CreateIndex
CREATE INDEX "workflow_executions_status_currentStepId_idx" ON "workflow_executions"("status", "currentStepId");
-- CreateIndex
CREATE INDEX "workflow_step_executions_executionId_status_idx" ON "workflow_step_executions"("executionId", "status");
-- CreateIndex
CREATE INDEX "workflow_step_executions_stepId_idx" ON "workflow_step_executions"("stepId");
-- CreateIndex
CREATE INDEX "workflow_step_executions_status_scheduledFor_idx" ON "workflow_step_executions"("status", "scheduledFor");
-- CreateIndex
CREATE INDEX "workflow_step_executions_scheduledFor_idx" ON "workflow_step_executions"("scheduledFor");
-- CreateIndex
CREATE UNIQUE INDEX "emails_messageId_key" ON "emails"("messageId");
-- CreateIndex
CREATE INDEX "emails_projectId_contactId_idx" ON "emails"("projectId", "contactId");
-- CreateIndex
CREATE INDEX "emails_contactId_idx" ON "emails"("contactId");
-- CreateIndex
CREATE INDEX "emails_campaignId_idx" ON "emails"("campaignId");
-- CreateIndex
CREATE INDEX "emails_workflowExecutionId_idx" ON "emails"("workflowExecutionId");
-- CreateIndex
CREATE INDEX "emails_workflowStepExecutionId_idx" ON "emails"("workflowStepExecutionId");
-- CreateIndex
CREATE INDEX "emails_status_idx" ON "emails"("status");
-- CreateIndex
CREATE INDEX "emails_createdAt_idx" ON "emails"("createdAt");
-- CreateIndex
CREATE INDEX "emails_projectId_sourceType_createdAt_idx" ON "emails"("projectId", "sourceType", "createdAt");
-- CreateIndex
CREATE INDEX "events_projectId_name_idx" ON "events"("projectId", "name");
-- CreateIndex
CREATE INDEX "events_contactId_idx" ON "events"("contactId");
-- CreateIndex
CREATE INDEX "events_emailId_idx" ON "events"("emailId");
-- CreateIndex
CREATE INDEX "events_createdAt_idx" ON "events"("createdAt");
-- CreateIndex
CREATE INDEX "api_requests_projectId_createdAt_idx" ON "api_requests"("projectId", "createdAt" DESC);
-- CreateIndex
CREATE INDEX "api_requests_userId_createdAt_idx" ON "api_requests"("userId", "createdAt" DESC);
-- CreateIndex
CREATE INDEX "api_requests_statusCode_createdAt_idx" ON "api_requests"("statusCode", "createdAt" DESC);
-- CreateIndex
CREATE INDEX "api_requests_path_createdAt_idx" ON "api_requests"("path", "createdAt" DESC);
-- CreateIndex
CREATE INDEX "api_requests_createdAt_idx" ON "api_requests"("createdAt" DESC);
-- CreateIndex
CREATE INDEX "api_requests_projectId_statusCode_createdAt_idx" ON "api_requests"("projectId", "statusCode", "createdAt" DESC);
-- AddForeignKey
ALTER TABLE "memberships" ADD CONSTRAINT "memberships_userId_fkey" FOREIGN KEY ("userId") REFERENCES "users"("id") ON DELETE RESTRICT ON UPDATE CASCADE;
-- AddForeignKey
ALTER TABLE "memberships" ADD CONSTRAINT "memberships_projectId_fkey" FOREIGN KEY ("projectId") REFERENCES "projects"("id") ON DELETE RESTRICT ON UPDATE CASCADE;
-- AddForeignKey
ALTER TABLE "domains" ADD CONSTRAINT "domains_projectId_fkey" FOREIGN KEY ("projectId") REFERENCES "projects"("id") ON DELETE CASCADE ON UPDATE CASCADE;
-- AddForeignKey
ALTER TABLE "contacts" ADD CONSTRAINT "contacts_projectId_fkey" FOREIGN KEY ("projectId") REFERENCES "projects"("id") ON DELETE CASCADE ON UPDATE CASCADE;
-- AddForeignKey
ALTER TABLE "templates" ADD CONSTRAINT "templates_projectId_fkey" FOREIGN KEY ("projectId") REFERENCES "projects"("id") ON DELETE CASCADE ON UPDATE CASCADE;
-- AddForeignKey
ALTER TABLE "segments" ADD CONSTRAINT "segments_projectId_fkey" FOREIGN KEY ("projectId") REFERENCES "projects"("id") ON DELETE CASCADE ON UPDATE CASCADE;
-- AddForeignKey
ALTER TABLE "segment_memberships" ADD CONSTRAINT "segment_memberships_contactId_fkey" FOREIGN KEY ("contactId") REFERENCES "contacts"("id") ON DELETE CASCADE ON UPDATE CASCADE;
-- AddForeignKey
ALTER TABLE "segment_memberships" ADD CONSTRAINT "segment_memberships_segmentId_fkey" FOREIGN KEY ("segmentId") REFERENCES "segments"("id") ON DELETE CASCADE ON UPDATE CASCADE;
-- AddForeignKey
ALTER TABLE "campaigns" ADD CONSTRAINT "campaigns_segmentId_fkey" FOREIGN KEY ("segmentId") REFERENCES "segments"("id") ON DELETE SET NULL ON UPDATE CASCADE;
-- AddForeignKey
ALTER TABLE "campaigns" ADD CONSTRAINT "campaigns_projectId_fkey" FOREIGN KEY ("projectId") REFERENCES "projects"("id") ON DELETE CASCADE ON UPDATE CASCADE;
-- AddForeignKey
ALTER TABLE "workflows" ADD CONSTRAINT "workflows_projectId_fkey" FOREIGN KEY ("projectId") REFERENCES "projects"("id") ON DELETE CASCADE ON UPDATE CASCADE;
-- AddForeignKey
ALTER TABLE "workflow_steps" ADD CONSTRAINT "workflow_steps_workflowId_fkey" FOREIGN KEY ("workflowId") REFERENCES "workflows"("id") ON DELETE CASCADE ON UPDATE CASCADE;
-- AddForeignKey
ALTER TABLE "workflow_steps" ADD CONSTRAINT "workflow_steps_templateId_fkey" FOREIGN KEY ("templateId") REFERENCES "templates"("id") ON DELETE SET NULL ON UPDATE CASCADE;
-- AddForeignKey
ALTER TABLE "workflow_transitions" ADD CONSTRAINT "workflow_transitions_fromStepId_fkey" FOREIGN KEY ("fromStepId") REFERENCES "workflow_steps"("id") ON DELETE CASCADE ON UPDATE CASCADE;
-- AddForeignKey
ALTER TABLE "workflow_transitions" ADD CONSTRAINT "workflow_transitions_toStepId_fkey" FOREIGN KEY ("toStepId") REFERENCES "workflow_steps"("id") ON DELETE CASCADE ON UPDATE CASCADE;
-- AddForeignKey
ALTER TABLE "workflow_executions" ADD CONSTRAINT "workflow_executions_workflowId_fkey" FOREIGN KEY ("workflowId") REFERENCES "workflows"("id") ON DELETE CASCADE ON UPDATE CASCADE;
-- AddForeignKey
ALTER TABLE "workflow_executions" ADD CONSTRAINT "workflow_executions_contactId_fkey" FOREIGN KEY ("contactId") REFERENCES "contacts"("id") ON DELETE CASCADE ON UPDATE CASCADE;
-- AddForeignKey
ALTER TABLE "workflow_executions" ADD CONSTRAINT "workflow_executions_currentStepId_fkey" FOREIGN KEY ("currentStepId") REFERENCES "workflow_steps"("id") ON DELETE SET NULL ON UPDATE CASCADE;
-- AddForeignKey
ALTER TABLE "workflow_step_executions" ADD CONSTRAINT "workflow_step_executions_executionId_fkey" FOREIGN KEY ("executionId") REFERENCES "workflow_executions"("id") ON DELETE CASCADE ON UPDATE CASCADE;
-- AddForeignKey
ALTER TABLE "workflow_step_executions" ADD CONSTRAINT "workflow_step_executions_stepId_fkey" FOREIGN KEY ("stepId") REFERENCES "workflow_steps"("id") ON DELETE CASCADE ON UPDATE CASCADE;
-- AddForeignKey
ALTER TABLE "emails" ADD CONSTRAINT "emails_contactId_fkey" FOREIGN KEY ("contactId") REFERENCES "contacts"("id") ON DELETE CASCADE ON UPDATE CASCADE;
-- AddForeignKey
ALTER TABLE "emails" ADD CONSTRAINT "emails_templateId_fkey" FOREIGN KEY ("templateId") REFERENCES "templates"("id") ON DELETE SET NULL ON UPDATE CASCADE;
-- AddForeignKey
ALTER TABLE "emails" ADD CONSTRAINT "emails_campaignId_fkey" FOREIGN KEY ("campaignId") REFERENCES "campaigns"("id") ON DELETE CASCADE ON UPDATE CASCADE;
-- AddForeignKey
ALTER TABLE "emails" ADD CONSTRAINT "emails_workflowExecutionId_fkey" FOREIGN KEY ("workflowExecutionId") REFERENCES "workflow_executions"("id") ON DELETE CASCADE ON UPDATE CASCADE;
-- AddForeignKey
ALTER TABLE "emails" ADD CONSTRAINT "emails_workflowStepExecutionId_fkey" FOREIGN KEY ("workflowStepExecutionId") REFERENCES "workflow_step_executions"("id") ON DELETE CASCADE ON UPDATE CASCADE;
-- AddForeignKey
ALTER TABLE "emails" ADD CONSTRAINT "emails_projectId_fkey" FOREIGN KEY ("projectId") REFERENCES "projects"("id") ON DELETE CASCADE ON UPDATE CASCADE;
-- AddForeignKey
ALTER TABLE "events" ADD CONSTRAINT "events_projectId_fkey" FOREIGN KEY ("projectId") REFERENCES "projects"("id") ON DELETE CASCADE ON UPDATE CASCADE;
-- AddForeignKey
ALTER TABLE "events" ADD CONSTRAINT "events_contactId_fkey" FOREIGN KEY ("contactId") REFERENCES "contacts"("id") ON DELETE CASCADE ON UPDATE CASCADE;
-- AddForeignKey
ALTER TABLE "events" ADD CONSTRAINT "events_emailId_fkey" FOREIGN KEY ("emailId") REFERENCES "emails"("id") ON DELETE CASCADE ON UPDATE CASCADE;
-- AddForeignKey
ALTER TABLE "api_requests" ADD CONSTRAINT "api_requests_projectId_fkey" FOREIGN KEY ("projectId") REFERENCES "projects"("id") ON DELETE SET NULL ON UPDATE CASCADE;
@@ -0,0 +1,3 @@
# Please do not edit this file manually
# It should be added in your version-control system (e.g., Git)
provider = "postgresql"
+727
View File
@@ -0,0 +1,727 @@
generator client {
provider = "prisma-client-js"
binaryTargets = ["native", "linux-musl-openssl-3.0.x", "linux-musl-arm64-openssl-3.0.x"]
}
datasource db {
provider = "postgresql"
url = env("DATABASE_URL")
directUrl = env("DIRECT_DATABASE_URL")
}
// ============================================
// CORE MODELS
// ============================================
model User {
id String @id @default(uuid())
// Credentials
email String @unique
password String?
type AuthMethod
// Relations
memberships Membership[]
// Timestamps
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
@@map("users")
}
model Project {
id String @id @default(uuid())
// Details
name String
// API
public String @unique
secret String @unique
// Admin
disabled Boolean @default(false)
// Billing
customer String? @unique
subscription String? @unique
// Billing Limits (per calendar month, null = unlimited)
billingLimitWorkflows Int? // Max workflow emails per month
billingLimitCampaigns Int? // Max campaign emails per month
billingLimitTransactional Int? // Max transactional emails per month
// Email Tracking
trackingEnabled Boolean @default(true) // Enable/disable open and click tracking
// Relations
members Membership[]
contacts Contact[]
templates Template[]
segments Segment[]
workflows Workflow[]
campaigns Campaign[]
emails Email[]
events Event[]
domains Domain[]
apiRequests ApiRequest[]
// Timestamps
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
@@map("projects")
}
model Membership {
user User @relation(fields: [userId], references: [id])
userId String
project Project @relation(fields: [projectId], references: [id])
projectId String
// Details
role Role @default(MEMBER)
// Timestamps
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
@@id([userId, projectId])
@@map("memberships")
}
model Domain {
id String @id @default(uuid())
// Domain details
domain String
verified Boolean @default(false)
// DKIM tokens for DNS verification
dkimTokens Json? // Array of DKIM token strings
// Relations
project Project @relation(fields: [projectId], references: [id], onDelete: Cascade)
projectId String
// Timestamps
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
@@unique([projectId, domain])
@@index([projectId])
@@index([projectId, verified])
@@map("domains")
}
// ============================================
// CONTACTS & TEMPLATES
// ============================================
model Contact {
id String @id @default(uuid())
// Details
email String
data Json? // Custom fields: { firstName: "John", plan: "pro", ... }
// Subscription
subscribed Boolean @default(true)
// Relations
project Project @relation(fields: [projectId], references: [id], onDelete: Cascade)
projectId String
emails Email[]
workflowExecutions WorkflowExecution[]
events Event[]
segmentMemberships SegmentMembership[]
// Timestamps
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
@@unique([projectId, email])
@@index([projectId])
@@index([projectId, subscribed])
@@map("contacts")
}
model Template {
id String @id @default(uuid())
// Details
name String
description String?
// Content
subject String
body String // HTML content with {{variable}} placeholders
from String
fromName String?
replyTo String?
// Type
type TemplateType @default(MARKETING)
// Relations
project Project @relation(fields: [projectId], references: [id], onDelete: Cascade)
projectId String
emails Email[]
workflowSteps WorkflowStep[]
// Timestamps
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
@@index([projectId])
@@index([projectId, type])
@@map("templates")
}
// ============================================
// SEGMENTS (Dynamic audience groups)
// ============================================
model Segment {
id String @id @default(uuid())
// Details
name String
description String?
// Filter conditions (evaluated dynamically)
filters Json
// Array of conditions with AND/OR logic:
// [
// { field: "data.plan", operator: "equals", value: "FREE" },
// { field: "subscribed", operator: "equals", value: true },
// { field: "emails.openedAt", operator: "within", value: 30, unit: "days" }
// ]
// Operators: equals, notEquals, contains, greaterThan, lessThan, within, exists, etc.
// Track membership changes (enables segment entry/exit events)
trackMembership Boolean @default(false)
// Stats (computed)
memberCount Int @default(0)
// Relations
project Project @relation(fields: [projectId], references: [id], onDelete: Cascade)
projectId String
memberships SegmentMembership[]
campaigns Campaign[]
// Timestamps
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
@@index([projectId])
@@map("segments")
}
model SegmentMembership {
// Relations
contact Contact @relation(fields: [contactId], references: [id], onDelete: Cascade)
contactId String
segment Segment @relation(fields: [segmentId], references: [id], onDelete: Cascade)
segmentId String
// Membership tracking
enteredAt DateTime @default(now())
exitedAt DateTime? // null = currently in segment
// Timestamps
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
@@id([contactId, segmentId])
@@index([segmentId, exitedAt]) // Active members (exitedAt = null)
@@index([contactId, exitedAt])
@@index([enteredAt])
@@map("segment_memberships")
}
// ============================================
// CAMPAIGNS (One-time broadcasts)
// ============================================
model Campaign {
id String @id @default(uuid())
// Details
name String
description String?
status CampaignStatus @default(DRAFT)
// Email content
subject String
body String @db.Text
from String
fromName String?
replyTo String?
// Audience selection
audienceType CampaignAudienceType @default(ALL)
audienceFilter Json? // For FILTERED: manual filter conditions
segment Segment? @relation(fields: [segmentId], references: [id])
segmentId String? // For SEGMENT: reference to saved segment
// Scheduling
scheduledFor DateTime?
// Stats (computed)
totalRecipients Int @default(0)
sentCount Int @default(0)
deliveredCount Int @default(0)
openedCount Int @default(0)
clickedCount Int @default(0)
bouncedCount Int @default(0)
// Relations
project Project @relation(fields: [projectId], references: [id], onDelete: Cascade)
projectId String
emails Email[]
// Timestamps
sentAt DateTime?
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
@@index([projectId, status])
@@index([scheduledFor])
@@index([segmentId])
@@map("campaigns")
}
// ============================================
// WORKFLOWS (Automated sequences)
// ============================================
model Workflow {
id String @id @default(uuid())
// Details
name String
description String?
enabled Boolean @default(false)
// Entry point
triggerType WorkflowTriggerType
triggerConfig Json? // { eventName: "user.signup" } or { schedule: "0 9 * * *" }
// Re-entry behavior
allowReentry Boolean @default(false) // If true, contacts can enter workflow multiple times
// Relations
project Project @relation(fields: [projectId], references: [id], onDelete: Cascade)
projectId String
steps WorkflowStep[]
executions WorkflowExecution[]
// Timestamps
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
@@index([projectId])
@@index([projectId, enabled])
@@map("workflows")
}
model WorkflowStep {
id String @id @default(uuid())
// Step details
type WorkflowStepType
name String
position Json // { x: 100, y: 200 } for visual editor
// Step configuration (flexible JSON)
config Json
// Examples by type:
// TRIGGER: { eventName: "user.signup" }
// SEND_EMAIL: { templateId: "uuid", from: "[email protected]" }
// DELAY: { amount: 24, unit: "hours" }
// WAIT_FOR_EVENT: { eventName: "email.clicked", timeout: 86400 }
// CONDITION: { field: "contact.data.plan", operator: "equals", value: "pro" }
// EXIT: { reason: "unsubscribed" }
// Relations
workflow Workflow @relation(fields: [workflowId], references: [id], onDelete: Cascade)
workflowId String
template Template? @relation(fields: [templateId], references: [id])
templateId String? // For SEND_EMAIL steps
// Graph connections
outgoingTransitions WorkflowTransition[] @relation("FromStep")
incomingTransitions WorkflowTransition[] @relation("ToStep")
// Execution tracking
executions WorkflowStepExecution[]
currentWorkflowExecutions WorkflowExecution[] // Executions currently on this step
// Timestamps
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
@@index([workflowId])
@@index([templateId])
@@map("workflow_steps")
}
model WorkflowTransition {
id String @id @default(uuid())
// From -> To
fromStep WorkflowStep @relation("FromStep", fields: [fromStepId], references: [id], onDelete: Cascade)
fromStepId String
toStep WorkflowStep @relation("ToStep", fields: [toStepId], references: [id], onDelete: Cascade)
toStepId String
// Conditional routing
condition Json? // null = always follow, or { branch: "yes" } for condition steps
priority Int @default(0) // Order to evaluate transitions
// Timestamps
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
@@index([fromStepId])
@@index([toStepId])
@@map("workflow_transitions")
}
model WorkflowExecution {
id String @id @default(uuid())
// Relations
workflow Workflow @relation(fields: [workflowId], references: [id], onDelete: Cascade)
workflowId String
contact Contact @relation(fields: [contactId], references: [id], onDelete: Cascade)
contactId String
// Execution state
status WorkflowExecutionStatus @default(RUNNING)
// Current position
currentStep WorkflowStep? @relation(fields: [currentStepId], references: [id])
currentStepId String?
// Exit information
exitReason String?
// Context data (merged with contact.data when executing)
context Json? // Additional variables for this execution
// Relations
stepExecutions WorkflowStepExecution[]
emails Email[]
// Timestamps
startedAt DateTime @default(now())
completedAt DateTime?
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
@@index([workflowId, contactId]) // Index for querying executions by workflow and contact
@@index([workflowId, status])
@@index([contactId, status])
@@index([status, currentStepId])
@@map("workflow_executions")
}
model WorkflowStepExecution {
id String @id @default(uuid())
// Relations
execution WorkflowExecution @relation(fields: [executionId], references: [id], onDelete: Cascade)
executionId String
step WorkflowStep @relation(fields: [stepId], references: [id], onDelete: Cascade)
stepId String
// Execution state
status StepExecutionStatus @default(PENDING)
// Delay/scheduling
scheduledFor DateTime? // When this step should execute (for DELAY steps)
executeAfter DateTime? // Don't execute before this time (for WAIT_FOR_EVENT timeout)
// Result tracking
output Json? // Step execution result
error String?
// Relations
emails Email[] // Emails sent by this step execution
// Timestamps
startedAt DateTime?
completedAt DateTime?
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
@@index([executionId, status])
@@index([stepId])
@@index([status, scheduledFor]) // For delay queue processor
@@index([scheduledFor])
@@map("workflow_step_executions")
}
// ============================================
// EMAILS (Unified tracking for all sends)
// ============================================
model Email {
id String @id @default(uuid())
// Recipient
contact Contact @relation(fields: [contactId], references: [id], onDelete: Cascade)
contactId String
// Content (denormalized for history)
subject String
body String // Rendered HTML
from String
fromName String?
replyTo String?
headers Json? // Custom email headers
attachments Json? // Array of {filename: string, content: string (base64), contentType: string}
// AWS SES Message ID (for tracking webhooks)
messageId String? @unique
// Source - identifies how this email was triggered
sourceType EmailSourceType
// Relations to source (one will be set based on sourceType)
template Template? @relation(fields: [templateId], references: [id])
templateId String? // For TRANSACTIONAL
campaign Campaign? @relation(fields: [campaignId], references: [id], onDelete: Cascade)
campaignId String? // For CAMPAIGN
workflowExecution WorkflowExecution? @relation(fields: [workflowExecutionId], references: [id], onDelete: Cascade)
workflowExecutionId String? // For WORKFLOW
workflowStepExecution WorkflowStepExecution? @relation(fields: [workflowStepExecutionId], references: [id], onDelete: Cascade)
workflowStepExecutionId String? // For WORKFLOW (specific step)
// Delivery
status EmailStatus @default(PENDING)
// Event timestamps
sentAt DateTime?
deliveredAt DateTime?
openedAt DateTime? // First open
clickedAt DateTime? // First click
bouncedAt DateTime?
complainedAt DateTime?
// Event counts
opens Int @default(0)
clicks Int @default(0)
// Error tracking
error String?
// Relations
project Project @relation(fields: [projectId], references: [id], onDelete: Cascade)
projectId String
events Event[]
// Timestamps
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
@@index([projectId, contactId])
@@index([contactId])
@@index([campaignId])
@@index([workflowExecutionId])
@@index([workflowStepExecutionId])
@@index([status])
@@index([createdAt])
@@index([projectId, sourceType, createdAt]) // For billing limit queries
@@map("emails")
}
// ============================================
// EVENTS (Webhook tracking & workflow triggers)
// ============================================
model Event {
id String @id @default(uuid())
// Event details
name String // e.g., "email.opened", "email.clicked", "user.signup"
data Json? // Event payload
// Relations
project Project @relation(fields: [projectId], references: [id], onDelete: Cascade)
projectId String
contact Contact? @relation(fields: [contactId], references: [id], onDelete: Cascade)
contactId String?
email Email? @relation(fields: [emailId], references: [id], onDelete: Cascade)
emailId String?
// Timestamps
createdAt DateTime @default(now())
@@index([projectId, name])
@@index([contactId])
@@index([emailId])
@@index([createdAt])
@@map("events")
}
// ============================================
// ENUMS
// ============================================
enum AuthMethod {
PASSWORD
GOOGLE_OAUTH
GITHUB_OAUTH
}
enum Role {
OWNER
ADMIN
MEMBER
}
enum TemplateType {
TRANSACTIONAL
MARKETING
}
enum CampaignStatus {
DRAFT
SCHEDULED
SENDING
SENT
CANCELLED
}
enum CampaignAudienceType {
ALL // All contacts
FILTERED // Based on audienceFilter
SEGMENT // Predefined segment
}
enum WorkflowTriggerType {
EVENT // Triggered by an event (e.g., user.signup)
MANUAL // Manually started via API
SCHEDULE // Runs on a schedule (cron)
}
enum WorkflowStepType {
TRIGGER // Entry point
SEND_EMAIL // Send an email
DELAY // Wait for X time
WAIT_FOR_EVENT // Wait for specific event (with timeout)
CONDITION // If/else branching
EXIT // Early exit point
WEBHOOK // Call external webhook
UPDATE_CONTACT // Update contact fields
}
enum WorkflowExecutionStatus {
RUNNING // Currently executing
WAITING // Waiting for event or delay
COMPLETED // Finished normally
EXITED // Exited early (via EXIT step)
FAILED // Failed with error
CANCELLED // Manually cancelled
}
// ============================================
// API REQUEST LOGGING
// ============================================
model ApiRequest {
id String @id // Request ID (UUID from X-Request-ID header)
// Request metadata
method String // HTTP method (GET, POST, etc.)
path String // Request path (/v1/send, /contacts, etc.)
statusCode Int // HTTP response status code
duration Int // Response time in milliseconds
// Authentication context
projectId String? // Project that made the request (if authenticated)
project Project? @relation(fields: [projectId], references: [id], onDelete: SetNull)
userId String? // User that made the request (if JWT auth)
authType String? // "apiKey" or "jwt"
// Request details
ip String? // Client IP address
userAgent String? // User-Agent header
// Error information (if status >= 400)
errorCode String? // Machine-readable error code (VALIDATION_ERROR, etc.)
errorMessage String? // Human-readable error message
// Size tracking (for analytics)
requestSize Int? // Request body size in bytes
responseSize Int? // Response body size in bytes
// Timestamps
createdAt DateTime @default(now())
// Indexes for efficient querying
@@index([projectId, createdAt(sort: Desc)]) // Query requests by project, most recent first
@@index([userId, createdAt(sort: Desc)]) // Query requests by user
@@index([statusCode, createdAt(sort: Desc)]) // Query errors (WHERE statusCode >= 400)
@@index([path, createdAt(sort: Desc)]) // Query by endpoint
@@index([createdAt(sort: Desc)]) // General time-based queries
@@index([projectId, statusCode, createdAt(sort: Desc)]) // Composite for project error queries
@@map("api_requests")
}
// ============================================
// ENUMS
// ============================================
enum StepExecutionStatus {
PENDING // Not yet started
SCHEDULED // Scheduled for future execution (DELAY)
WAITING // Waiting for event (WAIT_FOR_EVENT)
RUNNING // Currently executing
COMPLETED // Successfully completed
SKIPPED // Skipped (e.g., condition not met)
FAILED // Failed with error
}
enum EmailSourceType {
TRANSACTIONAL // Sent via API call
CAMPAIGN // Sent as part of broadcast
WORKFLOW // Sent via workflow automation
}
enum EmailStatus {
PENDING // Queued for sending
SENDING // Currently being sent
SENT // Successfully sent to provider
DELIVERED // Confirmed delivered
OPENED // Recipient opened email
CLICKED // Recipient clicked link
BOUNCED // Bounced (hard or soft)
COMPLAINED // Marked as spam
FAILED // Failed to send
}
+1
View File
@@ -0,0 +1 @@
export * from '@prisma/client';
+8
View File
@@ -0,0 +1,8 @@
{
"extends": "@plunk/typescript-config/base.json",
"compilerOptions": {
"outDir": "dist"
},
"include": ["src"],
"exclude": ["node_modules"]
}