Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
-- CreateTable
CREATE TABLE "EmailTemplate" (
"id" SERIAL NOT NULL,
"name" TEXT NOT NULL,
"subject" TEXT NOT NULL,
"htmlBody" TEXT NOT NULL,
"textBody" TEXT,
"createdById" TEXT,
"createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
"updatedById" TEXT,
"updatedAt" TIMESTAMP(3) NOT NULL,

CONSTRAINT "EmailTemplate_pkey" PRIMARY KEY ("id")
);

-- CreateIndex
CREATE UNIQUE INDEX "EmailTemplate_name_key" ON "EmailTemplate"("name");

-- AddForeignKey
ALTER TABLE "EmailTemplate" ADD CONSTRAINT "EmailTemplate_createdById_fkey" FOREIGN KEY ("createdById") REFERENCES "Member"("id") ON DELETE SET NULL ON UPDATE CASCADE;

-- AddForeignKey
ALTER TABLE "EmailTemplate" ADD CONSTRAINT "EmailTemplate_updatedById_fkey" FOREIGN KEY ("updatedById") REFERENCES "Member"("id") ON DELETE SET NULL ON UPDATE CASCADE;
17 changes: 17 additions & 0 deletions prisma/schema.prisma
Original file line number Diff line number Diff line change
Expand Up @@ -54,6 +54,8 @@ model Member {
updatedProjects Project[] @relation("ProjectUpdatedBy")
updatedSitePageContent SitePageContent[] @relation("SitePageContentUpdatedBy")
updatedSiteActions SiteAction[] @relation("SiteActionUpdatedBy")
createdEmailTemplates EmailTemplate[] @relation("EmailTemplateCreatedBy")
updatedEmailTemplates EmailTemplate[] @relation("EmailTemplateUpdatedBy")
}

model Account {
Expand Down Expand Up @@ -219,3 +221,18 @@ model SiteAction {
updatedAt DateTime @updatedAt
}


model EmailTemplate {
id Int @id @default(autoincrement())
name String @unique
subject String
htmlBody String
textBody String?

createdBy Member? @relation("EmailTemplateCreatedBy", fields: [createdById], references: [id], onDelete: SetNull)
createdById String?
createdAt DateTime @default(now())
updatedBy Member? @relation("EmailTemplateUpdatedBy", fields: [updatedById], references: [id], onDelete: SetNull)
updatedById String?
updatedAt DateTime @updatedAt
}
66 changes: 66 additions & 0 deletions src/controllers/emailTemplate.controller.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,66 @@
import { Request, Response } from "express";
import * as emailTemplateService from "../services/emailTemplate.service";
import { ApiError } from "../utils/apiError";

// GET /email/templates
export const listTemplates = async (_req: Request, res: Response) => {
const templates = await emailTemplateService.listTemplates();
res.status(200).json({ success: true, templates });
};

// GET /email/templates/:id
export const getTemplate = async (req: Request, res: Response) => {
const id = Number(req.params.id);
if (isNaN(id)) throw new ApiError("Invalid template id", 400);

const template = await emailTemplateService.getTemplateById(id);
if (!template) throw new ApiError("Template not found", 404);

res.status(200).json({ success: true, template });
};

// POST /email/templates
export const createTemplate = async (req: Request, res: Response) => {
const { name, subject, htmlBody, textBody, createdById } = req.body;

if (!name || !subject || !htmlBody) {
throw new ApiError("name, subject and htmlBody are required", 400);
}

const template = await emailTemplateService.createTemplate(
name,
subject,
htmlBody,
textBody,
createdById,
);

res.status(201).json({ success: true, template });
};

// PATCH /email/templates/:id
export const updateTemplate = async (req: Request, res: Response) => {
const id = Number(req.params.id);
if (isNaN(id)) throw new ApiError("Invalid template id", 400);

const { name, subject, htmlBody, textBody, updatedById } = req.body;

const template = await emailTemplateService.updateTemplate(id, {
name,
subject,
htmlBody,
textBody,
updatedById,
});

res.status(200).json({ success: true, template });
};

// DELETE /email/templates/:id
export const deleteTemplate = async (req: Request, res: Response) => {
const id = Number(req.params.id);
if (isNaN(id)) throw new ApiError("Invalid template id", 400);

await emailTemplateService.deleteTemplate(id);
res.status(200).json({ success: true, message: "Template deleted" });
};
94 changes: 94 additions & 0 deletions src/routes/email.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,94 @@
import express from "express";
import * as emailTemplateCtrl from "../controllers/emailTemplate.controller";

export default function emailRouter() {
const router = express.Router();

/**
* @api {get} /email/templates List all email templates
* @apiName ListEmailTemplates
* @apiGroup EmailTemplate
*
* @apiSuccess {Object[]} templates Array of email templates.
*
* @apiExample {curl} Example usage:
* curl -X GET http://localhost:3000/api/v1/email/templates
*/
router.get("/templates", emailTemplateCtrl.listTemplates);

/**
* @api {get} /email/templates/:id Get a single email template
* @apiName GetEmailTemplate
* @apiGroup EmailTemplate
*
* @apiParam (URL Params) {Number} id Template ID.
*
* @apiSuccess {Object} template Email template object.
*
* @apiExample {curl} Example usage:
* curl -X GET http://localhost:3000/api/v1/email/templates/1
*/
router.get("/templates/:id", emailTemplateCtrl.getTemplate);

/**
* @api {post} /email/templates Save a new email template
* @apiName CreateEmailTemplate
* @apiGroup EmailTemplate
*
* @apiBody {String} name Unique template name (e.g. "welcome"). (Required)
* @apiBody {String} subject Email subject line. (Required)
* @apiBody {String} htmlBody HTML email body. Supports {{name}}, {{email}},
* {{whatsapp_link}}, {{discord_link}}, {{year}}
* placeholders. (Required)
* @apiBody {String} [textBody] Plain-text fallback body.
*
* @apiSuccess {Object} template Saved template object.
* @apiError (400) BadRequest Missing required fields.
*
* @apiExample {curl} Example usage:
* curl -X POST http://localhost:3000/api/v1/email/templates \
* -H "Content-Type: application/json" \
* -d '{
* "name": "welcome",
* "subject": "Welcome to Call of Code!",
* "htmlBody": "<h1>Hi {{name}}</h1><p>Join WhatsApp: {{whatsapp_link}}</p>"
* }'
*/
router.post("/templates", emailTemplateCtrl.createTemplate);

/**
* @api {patch} /email/templates/:id Update an email template
* @apiName UpdateEmailTemplate
* @apiGroup EmailTemplate
*
* @apiParam (URL Params) {Number} id Template ID.
* @apiBody {String} [name] New unique name.
* @apiBody {String} [subject] New subject.
* @apiBody {String} [htmlBody] New HTML body.
* @apiBody {String} [textBody] New plain-text body.
*
* @apiSuccess {Object} template Updated template object.
*
* @apiExample {curl} Example usage:
* curl -X PATCH http://localhost:3000/api/v1/email/templates/1 \
* -H "Content-Type: application/json" \
* -d '{"subject": "Welcome aboard!"}'
*/
router.patch("/templates/:id", emailTemplateCtrl.updateTemplate);

/**
* @api {delete} /email/templates/:id Delete an email template
* @apiName DeleteEmailTemplate
* @apiGroup EmailTemplate
*
* @apiParam (URL Params) {Number} id Template ID.
*
* @apiSuccess {String} message Confirmation message.
*
* @apiExample {curl} Example usage:
* curl -X DELETE http://localhost:3000/api/v1/email/templates/1
*/
router.delete("/templates/:id", emailTemplateCtrl.deleteTemplate);

return router;
}
2 changes: 2 additions & 0 deletions src/routes/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ import quetionsRouter from './questions'
import progressRouter from './progress'
import membersRouter from './members'
import siteContentRouter from './site-content'
import emailRouter from './email'

export default function routes(upload: Multer) {
const router = Router();
Expand All @@ -27,6 +28,7 @@ export default function routes(upload: Multer) {
router.use("/progress", progressRouter());

router.use("/site-content", siteContentRouter(upload));
router.use("/email", emailRouter());

return router;
}
Expand Down
47 changes: 47 additions & 0 deletions src/services/emailTemplate.service.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
import prisma from "../db/client";
import { ApiError } from "../utils/apiError";

export const createTemplate = async (
name: string,
subject: string,
htmlBody: string,
textBody?: string,
createdById?: string,
) => {
return await prisma.emailTemplate.create({
data: { name, subject, htmlBody, textBody, createdById },
});
};

export const listTemplates = async () => {
return await prisma.emailTemplate.findMany({
orderBy: { createdAt: "desc" },
});
};

export const getTemplateById = async (id: number) => {
return await prisma.emailTemplate.findUnique({ where: { id } });
};

export const updateTemplate = async (
id: number,
payload: Partial<{
name: string;
subject: string;
htmlBody: string;
textBody: string;
updatedById: string;
}>,
) => {
const exists = await prisma.emailTemplate.findUnique({ where: { id } });
if (!exists) throw new ApiError("Template not found", 404);

return await prisma.emailTemplate.update({ where: { id }, data: payload });
};

export const deleteTemplate = async (id: number) => {
const exists = await prisma.emailTemplate.findUnique({ where: { id } });
if (!exists) throw new ApiError("Template not found", 404);

return await prisma.emailTemplate.delete({ where: { id } });
};
Loading