feat(scripts): add mustache render-template via Responses pages

Add HTML template kind to Responses with a seeded system email-default
template, render-template.js using Mustache, and html body support in
send-email. Templates are editable but system pages cannot be deleted.

Co-authored-by: Nasyarobby Putra <nasyarobby@gmail.com>
This commit is contained in:
Cursor Agent
2026-08-15 01:44:51 +00:00
co-authored by nsrb
parent 23ce72fec2
commit 46e2cbb83b
12 changed files with 426 additions and 50 deletions
+80 -2
View File
@@ -4,6 +4,9 @@ import { db } from "./db.js";
const MAX_NAME_LENGTH = 128;
const NAME_RE = /^[A-Za-z0-9._-]+$/;
const ALLOWED_MIME = new Set(["html", "json"]);
const ALLOWED_KIND = new Set(["response", "template"]);
export const DEFAULT_EMAIL_TEMPLATE_NAME = "email-default";
function nowIso() {
return new Date().toISOString();
@@ -41,6 +44,20 @@ export function assertMime(mime) {
return /** @type {"html" | "json"} */ (m);
}
/**
* @param {unknown} kind
* @returns {"response" | "template"}
*/
export function assertPageKind(kind, fallback = "response") {
const k = String(kind ?? fallback);
if (!ALLOWED_KIND.has(k)) {
const err = new Error('kind must be "response" or "template"');
err.statusCode = 400;
throw err;
}
return /** @type {"response" | "template"} */ (k);
}
/**
* @param {unknown} status
* @returns {number}
@@ -63,6 +80,8 @@ function publicPage(row) {
content: row.content,
mime: row.mime,
status: row.status,
kind: row.kind ?? "response",
system: Boolean(row.system),
created_at: row.created_at,
updated_at: row.updated_at,
};
@@ -92,27 +111,56 @@ export async function getHttpPageById(id) {
}
/**
* @param {{ name: string, content: string, mime: string, status?: number }} opts
* @param {string} name
*/
export async function upsertHttpPage({ name, content, mime, status }) {
export async function getHttpTemplateByName(name) {
const page = await getHttpPageByName(name);
if (!page) return null;
if (page.kind !== "template") return null;
return page;
}
/**
* @param {{
* name: string,
* content: string,
* mime: string,
* status?: number,
* kind?: string,
* }} opts
*/
export async function upsertHttpPage({ name, content, mime, status, kind }) {
const pageName = assertPageName(name);
const pageMime = assertMime(mime);
const pageKind = assertPageKind(kind, "response");
const pageStatus = assertHttpStatus(status, 200);
if (typeof content !== "string") {
const err = new Error("content must be a string");
err.statusCode = 400;
throw err;
}
if (pageKind === "template" && pageMime !== "html") {
const err = new Error('template pages must use mime "html"');
err.statusCode = 400;
throw err;
}
const now = nowIso();
const existing = await db("http_pages").where({ name: pageName }).first();
if (existing) {
if (Boolean(existing.system) && pageKind !== "template" && existing.kind === "template") {
const err = new Error("system template pages cannot be changed to HTTP responses");
err.statusCode = 400;
throw err;
}
await db("http_pages")
.where({ id: existing.id })
.update({
content,
mime: pageMime,
status: pageStatus,
kind: Boolean(existing.system) ? existing.kind : pageKind,
updated_at: now,
});
return getHttpPageById(existing.id);
@@ -125,6 +173,8 @@ export async function upsertHttpPage({ name, content, mime, status }) {
content,
mime: pageMime,
status: pageStatus,
kind: pageKind,
system: 0,
created_at: now,
updated_at: now,
});
@@ -136,6 +186,13 @@ export async function upsertHttpPage({ name, content, mime, status }) {
* @returns {Promise<boolean>}
*/
export async function deleteHttpPage(id) {
const existing = await db("http_pages").where({ id }).first();
if (!existing) return false;
if (Boolean(existing.system)) {
const err = new Error("system pages cannot be deleted");
err.statusCode = 409;
throw err;
}
const n = await db("http_pages").where({ id }).del();
return n > 0;
}
@@ -148,3 +205,24 @@ export function contentTypeForMime(mime) {
if (mime === "html") return "text/html; charset=utf-8";
return "application/json; charset=utf-8";
}
/**
* Ensure a page referenced by HTTP triggers is a response page, not a template.
* @param {{ kind?: string } | null} page
* @param {string} pageName
* @param {string} label
*/
export function assertHttpResponsePage(page, pageName, label) {
if (!page) {
const err = new Error(`unknown response page "${pageName}"`);
err.statusCode = 400;
throw err;
}
if (page.kind === "template") {
const err = new Error(
`"${pageName}" is an HTML template; ${label} must reference a response page`,
);
err.statusCode = 400;
throw err;
}
}