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:
@@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user