- Fix pg-boss Job type imports (PgBoss.Job -> Job from pg-boss) - Replace deprecated teamConcurrency with localConcurrency - Add null checks for possibly undefined values (clients, import rows) - Fix tone type narrowing in profile.ts - Fix test type assertions (non-null assertions, explicit Record types) - Extract auth middleware into shared module - Fix rate limiter Map generic type
148 lines
5.4 KiB
TypeScript
148 lines
5.4 KiB
TypeScript
import { authMiddleware } from '../middleware/auth';
|
|
import { Elysia, t } from 'elysia';
|
|
import { db } from '../db';
|
|
import { emailTemplates } from '../db/schema';
|
|
import { eq, and, desc, sql } from 'drizzle-orm';
|
|
import type { User } from '../lib/auth';
|
|
|
|
export const templateRoutes = new Elysia({ prefix: '/templates' })
|
|
.use(authMiddleware)
|
|
// List templates
|
|
.get('/', async ({ query, user }: { query: { category?: string }; user: User }) => {
|
|
let conditions = [eq(emailTemplates.userId, user.id)];
|
|
if (query.category) {
|
|
conditions.push(eq(emailTemplates.category, query.category));
|
|
}
|
|
return db.select()
|
|
.from(emailTemplates)
|
|
.where(and(...conditions))
|
|
.orderBy(desc(emailTemplates.usageCount));
|
|
}, {
|
|
query: t.Object({
|
|
category: t.Optional(t.String()),
|
|
}),
|
|
})
|
|
|
|
// Get single template
|
|
.get('/:id', async ({ params, user }: { params: { id: string }; user: User }) => {
|
|
const [template] = await db.select()
|
|
.from(emailTemplates)
|
|
.where(and(eq(emailTemplates.id, params.id), eq(emailTemplates.userId, user.id)))
|
|
.limit(1);
|
|
if (!template) throw new Error('Template not found');
|
|
return template;
|
|
}, {
|
|
params: t.Object({ id: t.String({ format: 'uuid' }) }),
|
|
})
|
|
|
|
// Create template
|
|
.post('/', async ({ body, user }: { body: { name: string; category: string; subject: string; content: string; isDefault?: boolean }; user: User }) => {
|
|
// If marking as default, unmark others in same category
|
|
if (body.isDefault) {
|
|
await db.update(emailTemplates)
|
|
.set({ isDefault: false })
|
|
.where(and(eq(emailTemplates.userId, user.id), eq(emailTemplates.category, body.category)));
|
|
}
|
|
const [template] = await db.insert(emailTemplates)
|
|
.values({
|
|
userId: user.id,
|
|
name: body.name,
|
|
category: body.category,
|
|
subject: body.subject,
|
|
content: body.content,
|
|
isDefault: body.isDefault || false,
|
|
})
|
|
.returning();
|
|
return template;
|
|
}, {
|
|
body: t.Object({
|
|
name: t.String({ minLength: 1 }),
|
|
category: t.String({ minLength: 1 }),
|
|
subject: t.String({ minLength: 1 }),
|
|
content: t.String({ minLength: 1 }),
|
|
isDefault: t.Optional(t.Boolean()),
|
|
}),
|
|
})
|
|
|
|
// Update template
|
|
.put('/:id', async (ctx: any) => {
|
|
const { params, body, user } = ctx as {
|
|
params: { id: string };
|
|
body: { name?: string; category?: string; subject?: string; content?: string; isDefault?: boolean };
|
|
user: User;
|
|
};
|
|
// If marking as default, unmark others
|
|
if (body.isDefault && body.category) {
|
|
await db.update(emailTemplates)
|
|
.set({ isDefault: false })
|
|
.where(and(eq(emailTemplates.userId, user.id), eq(emailTemplates.category, body.category)));
|
|
}
|
|
const updateData: Record<string, unknown> = { updatedAt: new Date() };
|
|
if (body.name !== undefined) updateData.name = body.name;
|
|
if (body.category !== undefined) updateData.category = body.category;
|
|
if (body.subject !== undefined) updateData.subject = body.subject;
|
|
if (body.content !== undefined) updateData.content = body.content;
|
|
if (body.isDefault !== undefined) updateData.isDefault = body.isDefault;
|
|
|
|
const [template] = await db.update(emailTemplates)
|
|
.set(updateData)
|
|
.where(and(eq(emailTemplates.id, params.id), eq(emailTemplates.userId, user.id)))
|
|
.returning();
|
|
if (!template) throw new Error('Template not found');
|
|
return template;
|
|
}, {
|
|
params: t.Object({ id: t.String({ format: 'uuid' }) }),
|
|
body: t.Object({
|
|
name: t.Optional(t.String({ minLength: 1 })),
|
|
category: t.Optional(t.String({ minLength: 1 })),
|
|
subject: t.Optional(t.String({ minLength: 1 })),
|
|
content: t.Optional(t.String({ minLength: 1 })),
|
|
isDefault: t.Optional(t.Boolean()),
|
|
}),
|
|
})
|
|
|
|
// Use template (increment usage count + return with placeholders filled)
|
|
.post('/:id/use', async (ctx: any) => {
|
|
const { params, body, user } = ctx;
|
|
const [template] = await db.select()
|
|
.from(emailTemplates)
|
|
.where(and(eq(emailTemplates.id, params.id), eq(emailTemplates.userId, user.id)))
|
|
.limit(1);
|
|
if (!template) throw new Error('Template not found');
|
|
|
|
// Increment usage count
|
|
await db.update(emailTemplates)
|
|
.set({ usageCount: sql`${emailTemplates.usageCount} + 1` })
|
|
.where(eq(emailTemplates.id, params.id));
|
|
|
|
// Fill placeholders
|
|
let subject = template.subject;
|
|
let content = template.content;
|
|
const vars = body.variables || {};
|
|
for (const [key, value] of Object.entries(vars)) {
|
|
const re = new RegExp(`\\{\\{${key}\\}\\}`, 'g');
|
|
subject = subject.replace(re, value as string);
|
|
content = content.replace(re, value as string);
|
|
}
|
|
|
|
return { subject, content, templateId: template.id, templateName: template.name };
|
|
}, {
|
|
params: t.Object({ id: t.String({ format: 'uuid' }) }),
|
|
body: t.Object({
|
|
clientId: t.Optional(t.String({ format: 'uuid' })),
|
|
variables: t.Optional(t.Record(t.String(), t.String())),
|
|
}),
|
|
})
|
|
|
|
// Delete template
|
|
.delete('/:id', async (ctx: any) => {
|
|
const { params, user } = ctx;
|
|
const [deleted] = await db.delete(emailTemplates)
|
|
.where(and(eq(emailTemplates.id, params.id), eq(emailTemplates.userId, user.id)))
|
|
.returning({ id: emailTemplates.id });
|
|
if (!deleted) throw new Error('Template not found');
|
|
return { success: true, id: deleted.id };
|
|
}, {
|
|
params: t.Object({ id: t.String({ format: 'uuid' }) }),
|
|
});
|