Migrate dev server from MongoDB to SQLite

- Replace mongooseAdapter with sqliteAdapter in payload config
- Update database configuration to use file:./dev.db
- Remove MongoDB memory database helper and references
- Simplify start script by removing verbose logging and MongoDB messaging
- Fix email processing with immediate send support and proper queue handling
- Restructure app with route groups for frontend/admin separation
- Add dashboard and test pages for email management
- Update API routes for improved email processing and testing

🤖 Generated with [Claude Code](https://claude.ai/code)

Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
2025-09-14 19:48:45 +02:00
parent 0d295603ef
commit 8993d20526
16 changed files with 496 additions and 593 deletions

BIN
dev.db Normal file

Binary file not shown.

2
dev/.env.local Normal file
View File

@@ -0,0 +1,2 @@
USE_MEMORY_DB=true
PAYLOAD_SECRET=YOUR_SECRET_HERE

View File

@@ -0,0 +1,310 @@
'use client'
import { useState, useEffect } from 'react'
import Link from 'next/link'
interface EmailStats {
total: number
sent: number
pending: number
failed: number
processing: number
}
export default function HomePage() {
const [emailStats, setEmailStats] = useState<EmailStats>({
total: 0,
sent: 0,
pending: 0,
failed: 0,
processing: 0
})
const [loading, setLoading] = useState<boolean>(true)
useEffect(() => {
fetchEmailStats()
}, [])
const fetchEmailStats = async () => {
try {
const response = await fetch('/api/test-email')
const data = await response.json()
if (data.outbox?.emails) {
const emails = data.outbox.emails
const stats: EmailStats = {
total: emails.length,
sent: emails.filter((email: any) => email.status === 'sent').length,
pending: emails.filter((email: any) => email.status === 'pending').length,
failed: emails.filter((email: any) => email.status === 'failed').length,
processing: emails.filter((email: any) => email.status === 'processing').length
}
setEmailStats(stats)
}
} catch (error) {
console.error('Error fetching email statistics:', error)
} finally {
setLoading(false)
}
}
const StatCard = ({ label, value, color, description }: { label: string; value: number; color: string; description: string }) => (
<div style={{
backgroundColor: 'white',
border: '1px solid #e5e7eb',
borderRadius: '12px',
padding: '24px',
textAlign: 'center',
boxShadow: '0 1px 3px rgba(0, 0, 0, 0.1)',
}}>
<div style={{
fontSize: '3rem',
fontWeight: 'bold',
color: color,
marginBottom: '8px'
}}>
{value}
</div>
<div style={{
fontSize: '1.1rem',
fontWeight: '600',
color: '#374151',
marginBottom: '4px'
}}>
{label}
</div>
<div style={{
fontSize: '0.875rem',
color: '#6b7280'
}}>
{description}
</div>
</div>
)
return (
<div style={{
backgroundColor: '#f9fafb',
padding: '40px 20px',
minHeight: 'calc(100vh - 80px)'
}}>
<div style={{
maxWidth: '1200px',
margin: '0 auto'
}}>
{/* Header */}
<div style={{ textAlign: 'center', marginBottom: '48px' }}>
<h1 style={{
fontSize: '3rem',
fontWeight: 'bold',
color: '#1f2937',
marginBottom: '16px'
}}>
📧 PayloadCMS Mailing Plugin
</h1>
<p style={{
fontSize: '1.25rem',
color: '#6b7280',
marginBottom: '24px'
}}>
Development Dashboard
</p>
<div style={{ display: 'flex', gap: '16px', justifyContent: 'center', flexWrap: 'wrap' }}>
<Link
href="/admin"
style={{
backgroundColor: '#3b82f6',
color: 'white',
padding: '12px 24px',
borderRadius: '8px',
textDecoration: 'none',
fontWeight: '500',
transition: 'background-color 0.2s'
}}
>
📊 Admin Panel
</Link>
<Link
href="/mailing-test"
style={{
backgroundColor: '#10b981',
color: 'white',
padding: '12px 24px',
borderRadius: '8px',
textDecoration: 'none',
fontWeight: '500',
transition: 'background-color 0.2s'
}}
>
🧪 Test Interface
</Link>
</div>
</div>
{/* Email Statistics */}
<div style={{ marginBottom: '48px' }}>
<div style={{
display: 'flex',
justifyContent: 'space-between',
alignItems: 'center',
marginBottom: '24px'
}}>
<h2 style={{
fontSize: '2rem',
fontWeight: 'bold',
color: '#1f2937'
}}>
Email Statistics
</h2>
<button
onClick={fetchEmailStats}
disabled={loading}
style={{
backgroundColor: loading ? '#9ca3af' : '#6b7280',
color: 'white',
padding: '8px 16px',
borderRadius: '6px',
border: 'none',
cursor: loading ? 'not-allowed' : 'pointer',
fontWeight: '500'
}}
>
{loading ? 'Loading...' : 'Refresh'}
</button>
</div>
{loading ? (
<div style={{ textAlign: 'center', padding: '48px' }}>
<div style={{ color: '#6b7280', fontSize: '1.1rem' }}>Loading email statistics...</div>
</div>
) : (
<div style={{
display: 'grid',
gridTemplateColumns: 'repeat(auto-fit, minmax(250px, 1fr))',
gap: '24px'
}}>
<StatCard
label="Total Emails"
value={emailStats.total}
color="#1f2937"
description="All emails in the system"
/>
<StatCard
label="Successfully Sent"
value={emailStats.sent}
color="#10b981"
description="Delivered successfully"
/>
<StatCard
label="Pending"
value={emailStats.pending}
color="#f59e0b"
description="Waiting to be sent"
/>
<StatCard
label="Failed"
value={emailStats.failed}
color="#ef4444"
description="Failed to send"
/>
{emailStats.processing > 0 && (
<StatCard
label="Processing"
value={emailStats.processing}
color="#3b82f6"
description="Currently being sent"
/>
)}
</div>
)}
</div>
{/* Quick Actions */}
<div style={{
backgroundColor: 'white',
borderRadius: '12px',
padding: '32px',
border: '1px solid #e5e7eb',
boxShadow: '0 1px 3px rgba(0, 0, 0, 0.1)'
}}>
<h3 style={{
fontSize: '1.5rem',
fontWeight: 'bold',
color: '#1f2937',
marginBottom: '16px'
}}>
Quick Actions
</h3>
<div style={{
display: 'grid',
gridTemplateColumns: 'repeat(auto-fit, minmax(300px, 1fr))',
gap: '16px'
}}>
<div style={{ padding: '16px', backgroundColor: '#f9fafb', borderRadius: '8px' }}>
<h4 style={{ marginBottom: '8px', color: '#1f2937' }}>🎯 Test Email Sending</h4>
<p style={{ color: '#6b7280', marginBottom: '12px', fontSize: '0.9rem' }}>
Send test emails using templates with the interactive testing interface.
</p>
<Link
href="/mailing-test"
style={{
color: '#3b82f6',
textDecoration: 'none',
fontWeight: '500'
}}
>
Open Test Interface
</Link>
</div>
<div style={{ padding: '16px', backgroundColor: '#f9fafb', borderRadius: '8px' }}>
<h4 style={{ marginBottom: '8px', color: '#1f2937' }}>📝 Manage Templates</h4>
<p style={{ color: '#6b7280', marginBottom: '12px', fontSize: '0.9rem' }}>
Create and edit email templates in the Payload admin interface.
</p>
<Link
href="/admin/collections/email-templates"
style={{
color: '#3b82f6',
textDecoration: 'none',
fontWeight: '500'
}}
>
Manage Templates
</Link>
</div>
<div style={{ padding: '16px', backgroundColor: '#f9fafb', borderRadius: '8px' }}>
<h4 style={{ marginBottom: '8px', color: '#1f2937' }}>📬 Email Queue</h4>
<p style={{ color: '#6b7280', marginBottom: '12px', fontSize: '0.9rem' }}>
View and manage the email outbox and delivery status.
</p>
<Link
href="/admin/collections/emails"
style={{
color: '#3b82f6',
textDecoration: 'none',
fontWeight: '500'
}}
>
View Email Queue
</Link>
</div>
</div>
</div>
{/* Footer */}
<div style={{
textAlign: 'center',
marginTop: '48px',
padding: '24px',
color: '#6b7280',
fontSize: '0.875rem'
}}>
PayloadCMS Mailing Plugin Development Environment
</div>
</div>
</div>
)
}

View File

@@ -0,0 +1,24 @@
import type { Metadata } from 'next'
export const metadata: Metadata = {
title: 'PayloadCMS Mailing Plugin - Development',
description: 'Development environment for PayloadCMS Mailing Plugin',
}
export default function FrontendLayout({
children,
}: {
children: React.ReactNode
}) {
return (
<html lang="en">
<body style={{
margin: 0,
padding: 0,
fontFamily: '-apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif'
}}>
{children}
</body>
</html>
)
}

View File

@@ -33,6 +33,8 @@ export default function MailingTestPage() {
const [selectedTemplate, setSelectedTemplate] = useState<string>('') const [selectedTemplate, setSelectedTemplate] = useState<string>('')
const [toEmail, setToEmail] = useState<string>('test@example.com') const [toEmail, setToEmail] = useState<string>('test@example.com')
const [variables, setVariables] = useState<Record<string, any>>({}) const [variables, setVariables] = useState<Record<string, any>>({})
const [jsonVariables, setJsonVariables] = useState<string>('{}')
const [jsonError, setJsonError] = useState<string>('')
const [emailType, setEmailType] = useState<'send' | 'schedule'>('send') const [emailType, setEmailType] = useState<'send' | 'schedule'>('send')
const [scheduleDate, setScheduleDate] = useState<string>('') const [scheduleDate, setScheduleDate] = useState<string>('')
const [loading, setLoading] = useState<boolean>(false) const [loading, setLoading] = useState<boolean>(false)
@@ -58,6 +60,23 @@ export default function MailingTestPage() {
const template = templates.find(t => t.slug === templateSlug) const template = templates.find(t => t.slug === templateSlug)
if (template?.previewData) { if (template?.previewData) {
setVariables(template.previewData) setVariables(template.previewData)
setJsonVariables(JSON.stringify(template.previewData, null, 2))
} else {
setVariables({})
setJsonVariables('{}')
}
setJsonError('')
}
const handleJsonVariablesChange = (jsonString: string) => {
setJsonVariables(jsonString)
setJsonError('')
try {
const parsed = JSON.parse(jsonString)
setVariables(parsed)
} catch (error) {
setJsonError(error instanceof Error ? error.message : 'Invalid JSON')
} }
} }
@@ -67,6 +86,11 @@ export default function MailingTestPage() {
return return
} }
if (jsonError) {
setMessage('Please fix the JSON syntax error before sending')
return
}
setLoading(true) setLoading(true)
setMessage('') setMessage('')
@@ -88,7 +112,8 @@ export default function MailingTestPage() {
const result = await response.json() const result = await response.json()
if (result.success) { if (result.success) {
setMessage(`${result.message} (ID: ${result.emailId})`) const statusIcon = result.status === 'sent' ? '📧' : '📫'
setMessage(`${statusIcon} ${result.message} (ID: ${result.emailId})`)
fetchData() // Refresh email queue fetchData() // Refresh email queue
} else { } else {
setMessage(`❌ Error: ${result.error}`) setMessage(`❌ Error: ${result.error}`)
@@ -204,28 +229,43 @@ export default function MailingTestPage() {
</div> </div>
)} )}
{selectedTemplateData?.variables && ( {selectedTemplate && (
<div style={{ marginBottom: '15px' }}> <div style={{ marginBottom: '15px' }}>
<h3>Template Variables:</h3> <label style={{ display: 'block', marginBottom: '5px' }}>
{selectedTemplateData.variables.map(variable => ( <strong>Template Variables (JSON):</strong>
<div key={variable.name} style={{ marginBottom: '10px' }}> {selectedTemplateData?.variables && (
<label style={{ display: 'block', marginBottom: '5px' }}> <small style={{ color: '#666', marginLeft: '8px' }}>
{variable.name} {variable.required && <span style={{ color: 'red' }}>*</span>} Available variables: {selectedTemplateData.variables.map(v => v.name).join(', ')}
{variable.description && <small style={{ color: '#666' }}> - {variable.description}</small>} </small>
</label> )}
<input </label>
type={variable.type === 'number' ? 'number' : variable.type === 'date' ? 'datetime-local' : 'text'} <textarea
value={variables[variable.name] || ''} value={jsonVariables}
onChange={(e) => setVariables({ onChange={(e) => handleJsonVariablesChange(e.target.value)}
...variables, placeholder='{\n "firstName": "John",\n "siteName": "MyApp",\n "createdAt": "2023-01-01T00:00:00Z"\n}'
[variable.name]: variable.type === 'number' ? Number(e.target.value) : style={{
variable.type === 'boolean' ? e.target.checked : width: '100%',
e.target.value height: '150px',
})} padding: '8px',
style={{ width: '100%', padding: '8px', borderRadius: '4px', border: '1px solid #ddd' }} borderRadius: '4px',
/> border: jsonError ? '2px solid #dc3545' : '1px solid #ddd',
fontFamily: 'monaco, "Courier New", monospace',
fontSize: '13px',
resize: 'vertical'
}}
/>
{jsonError && (
<div style={{
color: '#dc3545',
fontSize: '12px',
marginTop: '5px',
padding: '5px',
backgroundColor: '#f8d7da',
borderRadius: '4px'
}}>
Invalid JSON: {jsonError}
</div> </div>
))} )}
</div> </div>
)} )}

View File

@@ -5,16 +5,17 @@ export async function POST(request: Request) {
try { try {
const payload = await getPayload({ config }) const payload = await getPayload({ config })
// Queue the combined email queue processing job // Run jobs in the default queue (the plugin already schedules email processing on init)
const job = await payload.jobs.queue({ const results = await payload.jobs.run({
task: 'process-email-queue', queue: 'default',
input: {},
}) })
const processedCount = Array.isArray(results) ? results.length : (results ? 1 : 0)
return Response.json({ return Response.json({
success: true, success: true,
message: 'Email queue processing job queued successfully (will process both pending and failed emails)', message: `Email queue processing completed. Processed ${processedCount} jobs.`,
jobId: job.id, processedJobs: processedCount,
}) })
} catch (error) { } catch (error) {
console.error('Process emails error:', error) console.error('Process emails error:', error)

View File

@@ -1,6 +1,6 @@
import { getPayload } from 'payload' import { getPayload } from 'payload'
import config from '@payload-config' import config from '@payload-config'
import { sendEmail } from '@xtr-dev/payload-mailing' import { sendEmail, processEmailById } from '@xtr-dev/payload-mailing'
export async function POST(request: Request) { export async function POST(request: Request) {
try { try {
@@ -41,10 +41,33 @@ export async function POST(request: Request) {
const result = await sendEmail(payload, emailOptions) const result = await sendEmail(payload, emailOptions)
// If it's "send now" (not scheduled), process the email immediately
if (type === 'send' && !scheduledAt) {
try {
await processEmailById(payload, String(result.id))
return Response.json({
success: true,
emailId: result.id,
message: 'Email sent successfully',
status: 'sent'
})
} catch (processError) {
// If immediate processing fails, return that it's queued
console.warn('Failed to process email immediately, left in queue:', processError)
return Response.json({
success: true,
emailId: result.id,
message: 'Email queued successfully (immediate processing failed)',
status: 'queued'
})
}
}
return Response.json({ return Response.json({
success: true, success: true,
emailId: result.id, emailId: result.id,
message: scheduledAt ? 'Email scheduled successfully' : 'Email queued successfully', message: scheduledAt ? 'Email scheduled successfully' : 'Email queued successfully',
status: scheduledAt ? 'scheduled' : 'queued'
}) })
} catch (error) { } catch (error) {
console.error('Test email error:', error) console.error('Test email error:', error)

11
dev/app/page.tsx Normal file
View File

@@ -0,0 +1,11 @@
import { Metadata } from 'next'
import {redirect} from "next/navigation.js"
export const metadata: Metadata = {
title: 'PayloadCMS Mailing Plugin - Development',
description: 'Development environment for PayloadCMS Mailing Plugin',
}
export default function HomePage() {
redirect('/dashboard')
}

BIN
dev/dev.db Normal file

Binary file not shown.

View File

@@ -90,7 +90,7 @@ export interface Config {
'payload-migrations': PayloadMigrationsSelect<false> | PayloadMigrationsSelect<true>; 'payload-migrations': PayloadMigrationsSelect<false> | PayloadMigrationsSelect<true>;
}; };
db: { db: {
defaultIDType: string; defaultIDType: number;
}; };
globals: {}; globals: {};
globalsSelect: {}; globalsSelect: {};
@@ -100,7 +100,7 @@ export interface Config {
}; };
jobs: { jobs: {
tasks: { tasks: {
processEmails: ProcessEmailsJob; 'process-emails': ProcessEmailsTask;
'send-email': TaskSendEmail; 'send-email': TaskSendEmail;
inline: { inline: {
input: unknown; input: unknown;
@@ -133,7 +133,7 @@ export interface UserAuthOperations {
* via the `definition` "users". * via the `definition` "users".
*/ */
export interface User { export interface User {
id: string; id: number;
firstName?: string | null; firstName?: string | null;
lastName?: string | null; lastName?: string | null;
updatedAt: string; updatedAt: string;
@@ -159,7 +159,7 @@ export interface User {
* via the `definition` "posts". * via the `definition` "posts".
*/ */
export interface Post { export interface Post {
id: string; id: number;
updatedAt: string; updatedAt: string;
createdAt: string; createdAt: string;
} }
@@ -168,7 +168,7 @@ export interface Post {
* via the `definition` "media". * via the `definition` "media".
*/ */
export interface Media { export interface Media {
id: string; id: number;
updatedAt: string; updatedAt: string;
createdAt: string; createdAt: string;
url?: string | null; url?: string | null;
@@ -186,7 +186,7 @@ export interface Media {
* via the `definition` "email-templates". * via the `definition` "email-templates".
*/ */
export interface EmailTemplate { export interface EmailTemplate {
id: string; id: number;
/** /**
* A descriptive name for this email template * A descriptive name for this email template
*/ */
@@ -227,11 +227,11 @@ export interface EmailTemplate {
* via the `definition` "emails". * via the `definition` "emails".
*/ */
export interface Email { export interface Email {
id: string; id: number;
/** /**
* Email template used (optional if custom content provided) * Email template used (optional if custom content provided)
*/ */
template?: (string | null) | EmailTemplate; template?: (number | null) | EmailTemplate;
/** /**
* Recipient email addresses * Recipient email addresses
*/ */
@@ -316,7 +316,7 @@ export interface Email {
* via the `definition` "payload-jobs". * via the `definition` "payload-jobs".
*/ */
export interface PayloadJob { export interface PayloadJob {
id: string; id: number;
/** /**
* Input data provided to the job * Input data provided to the job
*/ */
@@ -363,7 +363,7 @@ export interface PayloadJob {
| { | {
executedAt: string; executedAt: string;
completedAt: string; completedAt: string;
taskSlug: 'inline' | 'processEmails' | 'send-email'; taskSlug: 'inline' | 'process-emails' | 'send-email';
taskID: string; taskID: string;
input?: input?:
| { | {
@@ -396,7 +396,7 @@ export interface PayloadJob {
id?: string | null; id?: string | null;
}[] }[]
| null; | null;
taskSlug?: ('inline' | 'processEmails' | 'send-email') | null; taskSlug?: ('inline' | 'process-emails' | 'send-email') | null;
queue?: string | null; queue?: string | null;
waitUntil?: string | null; waitUntil?: string | null;
processing?: boolean | null; processing?: boolean | null;
@@ -408,36 +408,36 @@ export interface PayloadJob {
* via the `definition` "payload-locked-documents". * via the `definition` "payload-locked-documents".
*/ */
export interface PayloadLockedDocument { export interface PayloadLockedDocument {
id: string; id: number;
document?: document?:
| ({ | ({
relationTo: 'users'; relationTo: 'users';
value: string | User; value: number | User;
} | null) } | null)
| ({ | ({
relationTo: 'posts'; relationTo: 'posts';
value: string | Post; value: number | Post;
} | null) } | null)
| ({ | ({
relationTo: 'media'; relationTo: 'media';
value: string | Media; value: number | Media;
} | null) } | null)
| ({ | ({
relationTo: 'email-templates'; relationTo: 'email-templates';
value: string | EmailTemplate; value: number | EmailTemplate;
} | null) } | null)
| ({ | ({
relationTo: 'emails'; relationTo: 'emails';
value: string | Email; value: number | Email;
} | null) } | null)
| ({ | ({
relationTo: 'payload-jobs'; relationTo: 'payload-jobs';
value: string | PayloadJob; value: number | PayloadJob;
} | null); } | null);
globalSlug?: string | null; globalSlug?: string | null;
user: { user: {
relationTo: 'users'; relationTo: 'users';
value: string | User; value: number | User;
}; };
updatedAt: string; updatedAt: string;
createdAt: string; createdAt: string;
@@ -447,10 +447,10 @@ export interface PayloadLockedDocument {
* via the `definition` "payload-preferences". * via the `definition` "payload-preferences".
*/ */
export interface PayloadPreference { export interface PayloadPreference {
id: string; id: number;
user: { user: {
relationTo: 'users'; relationTo: 'users';
value: string | User; value: number | User;
}; };
key?: string | null; key?: string | null;
value?: value?:
@@ -470,7 +470,7 @@ export interface PayloadPreference {
* via the `definition` "payload-migrations". * via the `definition` "payload-migrations".
*/ */
export interface PayloadMigration { export interface PayloadMigration {
id: string; id: number;
name?: string | null; name?: string | null;
batch?: number | null; batch?: number | null;
updatedAt: string; updatedAt: string;
@@ -628,9 +628,9 @@ export interface PayloadMigrationsSelect<T extends boolean = true> {
} }
/** /**
* This interface was referenced by `Config`'s JSON-Schema * This interface was referenced by `Config`'s JSON-Schema
* via the `definition` "ProcessEmailsJob". * via the `definition` "ProcessEmailsTask".
*/ */
export interface ProcessEmailsJob { export interface ProcessEmailsTask {
input?: unknown; input?: unknown;
output?: unknown; output?: unknown;
} }
@@ -640,6 +640,10 @@ export interface ProcessEmailsJob {
*/ */
export interface TaskSendEmail { export interface TaskSendEmail {
input: { input: {
/**
* Process and send the email immediately instead of waiting for the queue processor
*/
processImmediately?: boolean | null;
/** /**
* Use a template (leave empty for direct email) * Use a template (leave empty for direct email)
*/ */

View File

@@ -1,4 +1,4 @@
import { mongooseAdapter } from '@payloadcms/db-mongodb' import { sqliteAdapter } from '@payloadcms/db-sqlite'
import { lexicalEditor } from '@payloadcms/richtext-lexical' import { lexicalEditor } from '@payloadcms/richtext-lexical'
import { import {
FixedToolbarFeature, FixedToolbarFeature,
@@ -6,7 +6,6 @@ import {
HorizontalRuleFeature, HorizontalRuleFeature,
InlineToolbarFeature, InlineToolbarFeature,
} from '@payloadcms/richtext-lexical' } from '@payloadcms/richtext-lexical'
import { MongoMemoryReplSet } from 'mongodb-memory-server'
import path from 'path' import path from 'path'
import { buildConfig } from 'payload' import { buildConfig } from 'payload'
import sharp from 'sharp' import sharp from 'sharp'
@@ -24,36 +23,7 @@ if (!process.env.ROOT_DIR) {
process.env.ROOT_DIR = dirname process.env.ROOT_DIR = dirname
} }
const buildConfigWithMemoryDB = async () => { export default buildConfig({
// Use in-memory MongoDB for development and testing
if (process.env.NODE_ENV === 'test' || process.env.USE_MEMORY_DB === 'true' || !process.env.DATABASE_URI) {
console.log('🚀 Starting MongoDB in-memory database...')
const memoryDB = await MongoMemoryReplSet.create({
replSet: {
count: 1, // Single instance for dev (faster startup)
dbName: process.env.NODE_ENV === 'test' ? 'payloadmemory' : 'payload-mailing-dev',
storageEngine: 'wiredTiger',
},
})
const uri = `${memoryDB.getUri()}&retryWrites=true`
process.env.DATABASE_URI = uri
console.log('✅ MongoDB in-memory database started')
console.log(`📊 Database URI: ${uri.replace(/mongodb:\/\/[^@]*@/, 'mongodb://***@')}`)
// Graceful shutdown
process.on('SIGINT', async () => {
console.log('🛑 Stopping MongoDB in-memory database...')
await memoryDB.stop()
process.exit(0)
})
} else {
console.log(`🔗 Using external MongoDB: ${process.env.DATABASE_URI?.replace(/mongodb:\/\/[^@]*@/, 'mongodb://***@')}`)
}
return buildConfig({
admin: { admin: {
importMap: { importMap: {
baseDir: path.resolve(dirname), baseDir: path.resolve(dirname),
@@ -122,22 +92,36 @@ const buildConfigWithMemoryDB = async () => {
}, },
}, },
], ],
db: mongooseAdapter({ db: sqliteAdapter({
ensureIndexes: true, client: {
url: process.env.DATABASE_URI || '', url: process.env.DATABASE_URI || 'file:./dev.db',
},
}), }),
editor: lexicalEditor(), editor: lexicalEditor(),
email: testEmailAdapter, email: testEmailAdapter,
onInit: async (payload) => { onInit: async (payload) => {
await seed(payload) await seed(payload)
}, },
jobs: {
jobsCollectionOverrides: c => {
if (c.defaultJobsCollection.admin) c.defaultJobsCollection.admin.hidden = false
return c.defaultJobsCollection
},
autoRun: [
{
cron: '*/1 * * * *', // every minute
limit: 10, // limit jobs to process each run
queue: 'default', // name of the queue
},
],
},
plugins: [ plugins: [
mailingPlugin({ mailingPlugin({
defaultFrom: 'noreply@test.com', defaultFrom: 'noreply@test.com',
initOrder: 'after', initOrder: 'after',
retryAttempts: 3, retryAttempts: 3,
retryDelay: 60000, // 1 minute for dev retryDelay: 60000, // 1 minute for dev
queue: 'email-queue', queue: 'default',
// Example: Collection overrides for customization // Example: Collection overrides for customization
// Uncomment and modify as needed for your use case // Uncomment and modify as needed for your use case
@@ -286,6 +270,3 @@ const buildConfigWithMemoryDB = async () => {
outputFile: path.resolve(dirname, 'payload-types.ts'), outputFile: path.resolve(dirname, 'payload-types.ts'),
}, },
}) })
}
export default buildConfigWithMemoryDB()

View File

@@ -3,26 +3,14 @@
// Development startup script for PayloadCMS Mailing Plugin // Development startup script for PayloadCMS Mailing Plugin
// This ensures proper environment setup and provides helpful information // This ensures proper environment setup and provides helpful information
console.log('🚀 PayloadCMS Mailing Plugin - Development Mode')
console.log('=' .repeat(50))
// Set development environment // Set development environment
process.env.NODE_ENV = process.env.NODE_ENV || 'development' process.env.NODE_ENV = process.env.NODE_ENV || 'development'
// Enable in-memory MongoDB by default for development // Set default SQLite database for development
if (!process.env.DATABASE_URI) { if (!process.env.DATABASE_URI) {
process.env.USE_MEMORY_DB = 'true' process.env.DATABASE_URI = 'file:./dev.db'
console.log('📦 Using in-memory MongoDB (no installation required)')
} else {
console.log(`🔗 Using external MongoDB: ${process.env.DATABASE_URI}`)
} }
console.log('')
console.log('🔧 Starting development server...')
console.log('📧 Mailing plugin configured with test transport')
console.log('🎯 Test interface will be available at: /mailing-test')
console.log('')
// Import and start Next.js // Import and start Next.js
import('next/dist/cli/next-dev.js') import('next/dist/cli/next-dev.js')
.then(({ nextDev }) => { .then(({ nextDev }) => {
@@ -35,11 +23,9 @@ import('next/dist/cli/next-dev.js')
// Handle graceful shutdown // Handle graceful shutdown
process.on('SIGTERM', () => { process.on('SIGTERM', () => {
console.log('\n🛑 Shutting down development server...')
process.exit(0) process.exit(0)
}) })
process.on('SIGINT', () => { process.on('SIGINT', () => {
console.log('\n🛑 Shutting down development server...')
process.exit(0) process.exit(0)
}) })

View File

@@ -23,9 +23,6 @@
"dev:generate-importmap": "npm run dev:payload generate:importmap", "dev:generate-importmap": "npm run dev:payload generate:importmap",
"dev:generate-types": "npm run dev:payload generate:types", "dev:generate-types": "npm run dev:payload generate:types",
"dev:payload": "cross-env PAYLOAD_CONFIG_PATH=./dev/payload.config.ts payload", "dev:payload": "cross-env PAYLOAD_CONFIG_PATH=./dev/payload.config.ts payload",
"payload": "cross-env NODE_OPTIONS=--no-deprecation payload",
"generate:importmap": "npm run payload generate:importmap",
"generate:types": "npm run payload generate:types",
"lint": "eslint", "lint": "eslint",
"lint:fix": "eslint ./src --fix", "lint:fix": "eslint ./src --fix",
"prepublishOnly": "npm run clean && npm run build", "prepublishOnly": "npm run clean && npm run build",

View File

@@ -1,31 +0,0 @@
/**
* This config is only used to generate types.
*/
import { BaseDatabaseAdapter, buildConfig, Payload} from 'payload'
import Emails from "./src/collections/Emails.js"
import {createEmailTemplatesCollection} from "./src/collections/EmailTemplates.js"
import path from "path"
import { fileURLToPath } from 'url'
const __filename = fileURLToPath(import.meta.url)
const __dirname = path.dirname(__filename)
export default buildConfig({
collections: [
Emails,
createEmailTemplatesCollection()
],
db: {
allowIDOnCreate: undefined,
defaultIDType: 'number',
init: function (args: { payload: Payload; }): BaseDatabaseAdapter {
throw new Error('Function not implemented.');
},
name: undefined
},
secret: '',
typescript: {
outputFile: path.resolve(__dirname, 'src/payload-types.ts'),
}
});

View File

@@ -19,17 +19,8 @@ export const processEmailsTaskHandler = async (
const { req } = context const { req } = context
const payload = (req as any).payload const payload = (req as any).payload
try { // Use the shared email processing logic
console.log('🔄 Processing email queue (pending + failed emails)...') await processAllEmails(payload)
// Use the shared email processing logic
await processAllEmails(payload)
console.log('✅ Email queue processing completed successfully')
} catch (error) {
console.error('❌ Email queue processing failed:', error)
throw error
}
} }
/** /**

View File

@@ -1,436 +0,0 @@
/* tslint:disable */
/* eslint-disable */
/**
* This file was automatically generated by Payload.
* DO NOT MODIFY IT BY HAND. Instead, modify your source Payload config,
* and re-run `payload generate:types` to regenerate this file.
*/
/**
* Supported timezones in IANA format.
*
* This interface was referenced by `Config`'s JSON-Schema
* via the `definition` "supportedTimezones".
*/
export type SupportedTimezones =
| 'Pacific/Midway'
| 'Pacific/Niue'
| 'Pacific/Honolulu'
| 'Pacific/Rarotonga'
| 'America/Anchorage'
| 'Pacific/Gambier'
| 'America/Los_Angeles'
| 'America/Tijuana'
| 'America/Denver'
| 'America/Phoenix'
| 'America/Chicago'
| 'America/Guatemala'
| 'America/New_York'
| 'America/Bogota'
| 'America/Caracas'
| 'America/Santiago'
| 'America/Buenos_Aires'
| 'America/Sao_Paulo'
| 'Atlantic/South_Georgia'
| 'Atlantic/Azores'
| 'Atlantic/Cape_Verde'
| 'Europe/London'
| 'Europe/Berlin'
| 'Africa/Lagos'
| 'Europe/Athens'
| 'Africa/Cairo'
| 'Europe/Moscow'
| 'Asia/Riyadh'
| 'Asia/Dubai'
| 'Asia/Baku'
| 'Asia/Karachi'
| 'Asia/Tashkent'
| 'Asia/Calcutta'
| 'Asia/Dhaka'
| 'Asia/Almaty'
| 'Asia/Jakarta'
| 'Asia/Bangkok'
| 'Asia/Shanghai'
| 'Asia/Singapore'
| 'Asia/Tokyo'
| 'Asia/Seoul'
| 'Australia/Brisbane'
| 'Australia/Sydney'
| 'Pacific/Guam'
| 'Pacific/Noumea'
| 'Pacific/Auckland'
| 'Pacific/Fiji';
export interface Config {
auth: {
users: UserAuthOperations;
};
blocks: {};
collections: {
emails: Email;
'email-templates': EmailTemplate;
users: User;
'payload-locked-documents': PayloadLockedDocument;
'payload-preferences': PayloadPreference;
'payload-migrations': PayloadMigration;
};
collectionsJoins: {};
collectionsSelect: {
emails: EmailsSelect<false> | EmailsSelect<true>;
'email-templates': EmailTemplatesSelect<false> | EmailTemplatesSelect<true>;
users: UsersSelect<false> | UsersSelect<true>;
'payload-locked-documents': PayloadLockedDocumentsSelect<false> | PayloadLockedDocumentsSelect<true>;
'payload-preferences': PayloadPreferencesSelect<false> | PayloadPreferencesSelect<true>;
'payload-migrations': PayloadMigrationsSelect<false> | PayloadMigrationsSelect<true>;
};
db: {
defaultIDType: number;
};
globals: {};
globalsSelect: {};
locale: null;
user: User & {
collection: 'users';
};
jobs: {
tasks: unknown;
workflows: unknown;
};
}
export interface UserAuthOperations {
forgotPassword: {
email: string;
password: string;
};
login: {
email: string;
password: string;
};
registerFirstUser: {
email: string;
password: string;
};
unlock: {
email: string;
password: string;
};
}
/**
* Email delivery and status tracking
*
* This interface was referenced by `Config`'s JSON-Schema
* via the `definition` "emails".
*/
export interface Email {
id: number;
/**
* Email template used (optional if custom content provided)
*/
template?: (number | null) | EmailTemplate;
/**
* Recipient email addresses
*/
to: string[];
/**
* CC email addresses
*/
cc?: string[] | null;
/**
* BCC email addresses
*/
bcc?: string[] | null;
/**
* Sender email address (optional, uses default if not provided)
*/
from?: string | null;
/**
* Sender display name (optional, e.g., "John Doe" for "John Doe <john@example.com>")
*/
fromName?: string | null;
/**
* Reply-to email address
*/
replyTo?: string | null;
/**
* Email subject line
*/
subject: string;
/**
* Rendered HTML content of the email
*/
html: string;
/**
* Plain text version of the email
*/
text?: string | null;
/**
* Template variables used to render this email
*/
variables?:
| {
[k: string]: unknown;
}
| unknown[]
| string
| number
| boolean
| null;
/**
* When this email should be sent (leave empty for immediate)
*/
scheduledAt?: string | null;
/**
* When this email was actually sent
*/
sentAt?: string | null;
/**
* Current status of this email
*/
status: 'pending' | 'processing' | 'sent' | 'failed';
/**
* Number of send attempts made
*/
attempts?: number | null;
/**
* When the last send attempt was made
*/
lastAttemptAt?: string | null;
/**
* Last error message if send failed
*/
error?: string | null;
/**
* Email priority (1=highest, 10=lowest)
*/
priority?: number | null;
updatedAt: string;
createdAt: string;
}
/**
* This interface was referenced by `Config`'s JSON-Schema
* via the `definition` "email-templates".
*/
export interface EmailTemplate {
id: number;
/**
* A descriptive name for this email template
*/
name: string;
/**
* Unique identifier for this template (e.g., "welcome-email", "password-reset")
*/
slug: string;
/**
* Email subject line. You can use Handlebars variables like {{firstName}} or {{siteName}}.
*/
subject: string;
/**
* Email content with rich text formatting. Supports Handlebars variables like {{firstName}} and helpers like {{formatDate createdAt "long"}}. Content is converted to HTML and plain text automatically.
*/
content: {
root: {
type: string;
children: {
type: string;
version: number;
[k: string]: unknown;
}[];
direction: ('ltr' | 'rtl') | null;
format: 'left' | 'start' | 'center' | 'right' | 'end' | 'justify' | '';
indent: number;
version: number;
};
[k: string]: unknown;
};
updatedAt: string;
createdAt: string;
}
/**
* This interface was referenced by `Config`'s JSON-Schema
* via the `definition` "users".
*/
export interface User {
id: number;
updatedAt: string;
createdAt: string;
email: string;
resetPasswordToken?: string | null;
resetPasswordExpiration?: string | null;
salt?: string | null;
hash?: string | null;
loginAttempts?: number | null;
lockUntil?: string | null;
sessions?:
| {
id: string;
createdAt?: string | null;
expiresAt: string;
}[]
| null;
password?: string | null;
}
/**
* This interface was referenced by `Config`'s JSON-Schema
* via the `definition` "payload-locked-documents".
*/
export interface PayloadLockedDocument {
id: number;
document?:
| ({
relationTo: 'emails';
value: number | Email;
} | null)
| ({
relationTo: 'email-templates';
value: number | EmailTemplate;
} | null)
| ({
relationTo: 'users';
value: number | User;
} | null);
globalSlug?: string | null;
user: {
relationTo: 'users';
value: number | User;
};
updatedAt: string;
createdAt: string;
}
/**
* This interface was referenced by `Config`'s JSON-Schema
* via the `definition` "payload-preferences".
*/
export interface PayloadPreference {
id: number;
user: {
relationTo: 'users';
value: number | User;
};
key?: string | null;
value?:
| {
[k: string]: unknown;
}
| unknown[]
| string
| number
| boolean
| null;
updatedAt: string;
createdAt: string;
}
/**
* This interface was referenced by `Config`'s JSON-Schema
* via the `definition` "payload-migrations".
*/
export interface PayloadMigration {
id: number;
name?: string | null;
batch?: number | null;
updatedAt: string;
createdAt: string;
}
/**
* This interface was referenced by `Config`'s JSON-Schema
* via the `definition` "emails_select".
*/
export interface EmailsSelect<T extends boolean = true> {
template?: T;
to?: T;
cc?: T;
bcc?: T;
from?: T;
fromName?: T;
replyTo?: T;
subject?: T;
html?: T;
text?: T;
variables?: T;
scheduledAt?: T;
sentAt?: T;
status?: T;
attempts?: T;
lastAttemptAt?: T;
error?: T;
priority?: T;
updatedAt?: T;
createdAt?: T;
}
/**
* This interface was referenced by `Config`'s JSON-Schema
* via the `definition` "email-templates_select".
*/
export interface EmailTemplatesSelect<T extends boolean = true> {
name?: T;
slug?: T;
subject?: T;
content?: T;
updatedAt?: T;
createdAt?: T;
}
/**
* This interface was referenced by `Config`'s JSON-Schema
* via the `definition` "users_select".
*/
export interface UsersSelect<T extends boolean = true> {
updatedAt?: T;
createdAt?: T;
email?: T;
resetPasswordToken?: T;
resetPasswordExpiration?: T;
salt?: T;
hash?: T;
loginAttempts?: T;
lockUntil?: T;
sessions?:
| T
| {
id?: T;
createdAt?: T;
expiresAt?: T;
};
}
/**
* This interface was referenced by `Config`'s JSON-Schema
* via the `definition` "payload-locked-documents_select".
*/
export interface PayloadLockedDocumentsSelect<T extends boolean = true> {
document?: T;
globalSlug?: T;
user?: T;
updatedAt?: T;
createdAt?: T;
}
/**
* This interface was referenced by `Config`'s JSON-Schema
* via the `definition` "payload-preferences_select".
*/
export interface PayloadPreferencesSelect<T extends boolean = true> {
user?: T;
key?: T;
value?: T;
updatedAt?: T;
createdAt?: T;
}
/**
* This interface was referenced by `Config`'s JSON-Schema
* via the `definition` "payload-migrations_select".
*/
export interface PayloadMigrationsSelect<T extends boolean = true> {
name?: T;
batch?: T;
updatedAt?: T;
createdAt?: T;
}
/**
* This interface was referenced by `Config`'s JSON-Schema
* via the `definition` "auth".
*/
export interface Auth {
[k: string]: unknown;
}
declare module 'payload' {
export interface GeneratedTypes extends Config {}
}