feat: Projekt-Scaffold + Core Utilities — Result Pattern, hashEmail, Zod-Schemas, Campaign-Typen
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
19
src/lib/crypto.test.ts
Normal file
19
src/lib/crypto.test.ts
Normal file
@@ -0,0 +1,19 @@
|
||||
import { describe, it, expect } from 'vitest'
|
||||
import { hashEmail } from './crypto'
|
||||
|
||||
describe('hashEmail', () => {
|
||||
it('gibt SHA256-Hash zurück', () => {
|
||||
const hash = hashEmail('test@example.com')
|
||||
expect(hash).toHaveLength(64)
|
||||
expect(hash).toMatch(/^[a-f0-9]+$/)
|
||||
})
|
||||
|
||||
it('normalisiert E-Mail vor Hash (lowercase)', () => {
|
||||
expect(hashEmail('Test@Example.COM')).toBe(hashEmail('test@example.com'))
|
||||
})
|
||||
|
||||
it('gibt nie die Klartext-E-Mail zurück', () => {
|
||||
const hash = hashEmail('test@example.com')
|
||||
expect(hash).not.toContain('@')
|
||||
})
|
||||
})
|
||||
5
src/lib/crypto.ts
Normal file
5
src/lib/crypto.ts
Normal file
@@ -0,0 +1,5 @@
|
||||
import { createHash } from 'crypto'
|
||||
|
||||
export function hashEmail(email: string): string {
|
||||
return createHash('sha256').update(email.toLowerCase().trim()).digest('hex')
|
||||
}
|
||||
16
src/lib/result.test.ts
Normal file
16
src/lib/result.test.ts
Normal file
@@ -0,0 +1,16 @@
|
||||
import { describe, it, expect } from 'vitest'
|
||||
import { ok, err } from './result'
|
||||
|
||||
describe('Result', () => {
|
||||
it('ok wraps data', () => {
|
||||
const r = ok('hello')
|
||||
expect(r.ok).toBe(true)
|
||||
if (r.ok) expect(r.data).toBe('hello')
|
||||
})
|
||||
|
||||
it('err wraps error', () => {
|
||||
const r = err(new Error('fail'))
|
||||
expect(r.ok).toBe(false)
|
||||
if (!r.ok) expect(r.error.message).toBe('fail')
|
||||
})
|
||||
})
|
||||
6
src/lib/result.ts
Normal file
6
src/lib/result.ts
Normal file
@@ -0,0 +1,6 @@
|
||||
export type Result<T, E = Error> =
|
||||
| { ok: true; data: T }
|
||||
| { ok: false; error: E }
|
||||
|
||||
export const ok = <T>(data: T): Result<T> => ({ ok: true, data })
|
||||
export const err = <E = Error>(error: E): Result<never, E> => ({ ok: false, error })
|
||||
21
src/lib/validation.ts
Normal file
21
src/lib/validation.ts
Normal file
@@ -0,0 +1,21 @@
|
||||
import { z } from 'zod'
|
||||
|
||||
export const CreateCampaignSchema = z.object({
|
||||
name: z.string().min(1).max(255),
|
||||
subject: z.string().min(1).max(998),
|
||||
htmlBody: z.string().min(1),
|
||||
plainBody: z.string().min(1),
|
||||
})
|
||||
|
||||
export const UpdateCampaignSchema = CreateCampaignSchema.partial()
|
||||
|
||||
export const ScheduleCampaignSchema = z.union([
|
||||
z.object({ type: z.literal('immediate') }),
|
||||
z.object({ type: z.literal('once'), scheduledAt: z.coerce.date() }),
|
||||
z.object({ type: z.literal('cron'), cronExpression: z.string().min(9) }),
|
||||
])
|
||||
|
||||
export const RecipientSchema = z.union([
|
||||
z.object({ listId: z.string().uuid(), segmentId: z.null().optional() }),
|
||||
z.object({ segmentId: z.string().uuid(), listId: z.null().optional() }),
|
||||
])
|
||||
48
src/types/index.ts
Normal file
48
src/types/index.ts
Normal file
@@ -0,0 +1,48 @@
|
||||
export type CampaignStatus =
|
||||
| 'draft'
|
||||
| 'scheduled'
|
||||
| 'sending'
|
||||
| 'sent'
|
||||
| 'paused'
|
||||
| 'cancelled'
|
||||
|
||||
export type TriggerType = 'cron' | 'event'
|
||||
|
||||
export interface Campaign {
|
||||
id: string
|
||||
name: string
|
||||
subject: string
|
||||
htmlBody: string
|
||||
plainBody: string
|
||||
status: CampaignStatus
|
||||
scheduledAt: Date | null
|
||||
cronExpression: string | null
|
||||
createdAt: Date
|
||||
updatedAt: Date
|
||||
}
|
||||
|
||||
export interface CampaignRecipient {
|
||||
id: string
|
||||
campaignId: string
|
||||
listId: string | null
|
||||
segmentId: string | null
|
||||
}
|
||||
|
||||
export interface CampaignTrigger {
|
||||
id: string
|
||||
campaignId: string
|
||||
triggerType: TriggerType
|
||||
triggerValue: string
|
||||
}
|
||||
|
||||
export interface CampaignAnalytics {
|
||||
campaignId: string
|
||||
sent: number
|
||||
opens: number
|
||||
clicks: number
|
||||
bounces: number
|
||||
unsubscribes: number
|
||||
openRate: number
|
||||
clickRate: number
|
||||
bounceRate: number
|
||||
}
|
||||
Reference in New Issue
Block a user