Merge branch 'refactor/simplify-render'
Simplify render pipeline, sandbox context, and db helpers; fix route precedence so the most recently saved route wins. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
commit
fb3d904f5b
2 changed files with 175 additions and 207 deletions
59
db.js
59
db.js
|
|
@ -167,51 +167,28 @@ function getRoute(routeId) {
|
||||||
.get(routeId);
|
.get(routeId);
|
||||||
}
|
}
|
||||||
|
|
||||||
function updateRoute(route) {
|
// Update `fields` of a row by id from a plain object. Returns the run info.
|
||||||
const fields = [
|
function update(table, fields, obj) {
|
||||||
"verb",
|
|
||||||
"path",
|
|
||||||
"structure_id",
|
|
||||||
"handler",
|
|
||||||
"updated_at",
|
|
||||||
"error",
|
|
||||||
];
|
|
||||||
const values = fields.map((field) => route[field]);
|
|
||||||
const placeholders = fields.map((field) => `${field} = ?`).join(", ");
|
const placeholders = fields.map((field) => `${field} = ?`).join(", ");
|
||||||
|
const values = fields.map((field) => obj[field]);
|
||||||
|
values.push(obj.id);
|
||||||
|
return db.prepare(`UPDATE ${table} SET ${placeholders} WHERE id = ?`).run(...values);
|
||||||
|
}
|
||||||
|
|
||||||
const sql = `UPDATE routes SET ${placeholders} WHERE id = ?`;
|
function updateRoute(route) {
|
||||||
values.push(route.id); // Add routeId to the end for the WHERE clause
|
update("routes", ["verb", "path", "structure_id", "handler", "updated_at", "error"], route);
|
||||||
db.prepare(sql).run(...values);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
function updateDb(appDb) {
|
function updateDb(appDb) {
|
||||||
const fields = ["name", "library"];
|
update("dbs", ["name", "library"], appDb);
|
||||||
const values = fields.map((field) => appDb[field]);
|
|
||||||
const placeholders = fields.map((field) => `${field} = ?`).join(", ");
|
|
||||||
|
|
||||||
const sql = `UPDATE dbs SET ${placeholders} WHERE id = ?`;
|
|
||||||
values.push(appDb.id); // Add routeId to the end for the WHERE clause
|
|
||||||
db.prepare(sql).run(...values);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
function updateStruct(struct) {
|
function updateStruct(struct) {
|
||||||
const fields = ["name", "route_prefix", "head_injection"];
|
update("structures", ["name", "route_prefix", "head_injection"], struct);
|
||||||
const values = fields.map((field) => struct[field]);
|
|
||||||
const placeholders = fields.map((field) => `${field} = ?`).join(", ");
|
|
||||||
|
|
||||||
const sql = `UPDATE structures SET ${placeholders} WHERE id = ?`;
|
|
||||||
values.push(struct.id); // Add routeId to the end for the WHERE clause
|
|
||||||
db.prepare(sql).run(...values);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
function updateTemplate(template) {
|
function updateTemplate(template) {
|
||||||
const fields = ["content", "name", "test_object"];
|
update("templates", ["content", "name", "test_object"], template);
|
||||||
const values = fields.map((field) => template[field]);
|
|
||||||
const placeholders = fields.map((field) => `${field} = ?`).join(", ");
|
|
||||||
|
|
||||||
const sql = `UPDATE templates SET ${placeholders} WHERE id = ?`;
|
|
||||||
values.push(template.id);
|
|
||||||
db.prepare(sql).run(...values);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
function getTemplates(structureId) {
|
function getTemplates(structureId) {
|
||||||
|
|
@ -490,9 +467,7 @@ function createScaffoldPage(routeId, content=null) {
|
||||||
const route = getRoute(routeId)
|
const route = getRoute(routeId)
|
||||||
const endpoint = buildScaffoldUrl(route.path, route.url_params, route.query_params);
|
const endpoint = buildScaffoldUrl(route.path, route.url_params, route.query_params);
|
||||||
content = getDefaultScaffoldContentByVerb(endpoint, route.verb);
|
content = getDefaultScaffoldContentByVerb(endpoint, route.verb);
|
||||||
console.log("gabga", content, route, endpoint)
|
|
||||||
}
|
}
|
||||||
console.log(content, "content")
|
|
||||||
const stmt = db.prepare(
|
const stmt = db.prepare(
|
||||||
"INSERT INTO scaffold_pages (route_id, content) VALUES (?, ?)"
|
"INSERT INTO scaffold_pages (route_id, content) VALUES (?, ?)"
|
||||||
);
|
);
|
||||||
|
|
@ -508,17 +483,7 @@ function getLatestScaffoldPage(routeId) {
|
||||||
}
|
}
|
||||||
|
|
||||||
function updateScaffoldPage(scaffoldPage) {
|
function updateScaffoldPage(scaffoldPage) {
|
||||||
const fields = ["content"];
|
return update("scaffold_pages", ["content"], scaffoldPage).changes > 0;
|
||||||
const values = fields.map((field) => scaffoldPage[field]);
|
|
||||||
const placeholders = fields.map((field) => `${field} = ?`).join(", ");
|
|
||||||
console.log(scaffoldPage, "gabababab")
|
|
||||||
|
|
||||||
const sql = `UPDATE scaffold_pages SET ${placeholders} WHERE id = ?`;
|
|
||||||
values.push(scaffoldPage.id); // Add scaffoldPageId to the end for the WHERE clause
|
|
||||||
|
|
||||||
const info = db.prepare(sql).run(...values);
|
|
||||||
console.log(info.changes)
|
|
||||||
return info.changes > 0; // Returns true if a row was updated, false otherwise
|
|
||||||
}
|
}
|
||||||
|
|
||||||
function generatePostForm(endpoint) {
|
function generatePostForm(endpoint) {
|
||||||
|
|
|
||||||
319
index.js
319
index.js
|
|
@ -33,6 +33,15 @@ function inspectArgs(args) {
|
||||||
return args.map((v) => util.inspect(v, INSPECT_OPTS)).join(" ");
|
return args.map((v) => util.inspect(v, INSPECT_OPTS)).join(" ");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function formatError(e) {
|
||||||
|
return e.stack ? `${e}\n\n${e.stack}` : `${e}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Record a handler error in the structure's logs table.
|
||||||
|
function logError(structureId, routeId, e) {
|
||||||
|
model.createLog(structureId, routeId, formatError(e), true);
|
||||||
|
}
|
||||||
|
|
||||||
app.use(bodyParser.urlencoded({ extended: true }));
|
app.use(bodyParser.urlencoded({ extended: true }));
|
||||||
app.use(bodyParser.json());
|
app.use(bodyParser.json());
|
||||||
app.use(express.static("public"));
|
app.use(express.static("public"));
|
||||||
|
|
@ -83,53 +92,55 @@ async function saveFile(structureId, req, uploadedFile, asset=false) {
|
||||||
return file
|
return file
|
||||||
}
|
}
|
||||||
|
|
||||||
function bootstrapContext(structureId, routeId, initContext) {
|
// A console whose log() mirrors to stdout and to the structure's logs table.
|
||||||
const allDbInstances = {};
|
function makeConsole(structureId, routeId) {
|
||||||
function getDb(alias) {
|
return {
|
||||||
return allDbInstances[alias];
|
|
||||||
}
|
|
||||||
|
|
||||||
// todo wrap template in data-bliss-edit-template thing since ws can't take us to the editor on a component basis?? maybe...
|
|
||||||
// for now just designing with component approach
|
|
||||||
const eta = model.getTemplater(structureId);
|
|
||||||
const structure = model.getStructure(structureId)
|
|
||||||
const __urlPrefix = structure.route_prefix;
|
|
||||||
|
|
||||||
const libs = { eta, db: getDb, push: webPush, files: { saveFile: (...args) => saveFile(structureId, ...args) } };
|
|
||||||
|
|
||||||
let dbs = model.getDbsForStructure(structureId);
|
|
||||||
let context = vm.createContext({
|
|
||||||
...initContext,
|
|
||||||
require: function (str) {
|
|
||||||
return libs[str];
|
|
||||||
},
|
|
||||||
module: { exports: null },
|
|
||||||
console: {
|
|
||||||
log: function (...content) {
|
log: function (...content) {
|
||||||
content.forEach((c) => console.log(c));
|
content.forEach((c) => console.log(c));
|
||||||
model.createLog(structureId, routeId, inspectArgs(content));
|
model.createLog(structureId, routeId, inspectArgs(content));
|
||||||
},
|
},
|
||||||
},
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
// Evaluate a db's "library" script with `sql` and `console` bound, returning
|
||||||
|
// its exports. Each library runs in its own context so nothing leaks between dbs.
|
||||||
|
function runLibrary(sql, librarySource, console) {
|
||||||
|
const libContext = vm.createContext({ sql, console, module: { exports: null } });
|
||||||
|
vm.runInContext(librarySource, libContext);
|
||||||
|
return libContext.module.exports;
|
||||||
|
}
|
||||||
|
|
||||||
|
// The `require(name)` targets available to a user handler.
|
||||||
|
function makeLibs(structureId, console) {
|
||||||
|
const dbs = {};
|
||||||
|
for (let appDb of model.getDbsForStructure(structureId)) {
|
||||||
|
const sql = model.getDbInstance(appDb.id);
|
||||||
|
dbs[appDb.alias] = { library: runLibrary(sql, appDb.library, console), sql };
|
||||||
|
}
|
||||||
|
return {
|
||||||
|
eta: model.getTemplater(structureId),
|
||||||
|
db: (alias) => dbs[alias],
|
||||||
|
push: webPush,
|
||||||
|
files: { saveFile: (...args) => saveFile(structureId, ...args) },
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
function bootstrapContext(structureId, routeId, initContext) {
|
||||||
|
const structure = model.getStructure(structureId);
|
||||||
|
const console = makeConsole(structureId, routeId);
|
||||||
|
const libs = makeLibs(structureId, console);
|
||||||
|
|
||||||
|
return vm.createContext({
|
||||||
|
...initContext,
|
||||||
|
require: (name) => libs[name],
|
||||||
|
module: { exports: null },
|
||||||
|
console,
|
||||||
vapidPublicKey: vapidPublicKey,
|
vapidPublicKey: vapidPublicKey,
|
||||||
fetch: fetch,
|
fetch: fetch,
|
||||||
clearTimeout: clearTimeout,
|
clearTimeout: clearTimeout,
|
||||||
setTimeout: setTimeout,
|
setTimeout: setTimeout,
|
||||||
route: function (url) {
|
route: makeRoute(structure),
|
||||||
return withPrefix(__urlPrefix, url);
|
|
||||||
},
|
|
||||||
});
|
});
|
||||||
for (let appDb of dbs) {
|
|
||||||
let dbInstance = model.getDbInstance(appDb.id);
|
|
||||||
context.sql = dbInstance;
|
|
||||||
vm.runInContext(appDb.library, context);
|
|
||||||
allDbInstances[appDb.alias] = {
|
|
||||||
library: context.module.exports,
|
|
||||||
sql: dbInstance,
|
|
||||||
};
|
|
||||||
context.module.exports = null;
|
|
||||||
}
|
|
||||||
|
|
||||||
return context;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
function withPrefix(prefix, url) {
|
function withPrefix(prefix, url) {
|
||||||
|
|
@ -160,28 +171,19 @@ function bootstrapWebsocketHandler(route) {
|
||||||
cb(...args)
|
cb(...args)
|
||||||
}
|
}
|
||||||
catch (e) {
|
catch (e) {
|
||||||
const error = e.stack ? `${e}\n\n${e.stack}` : `${e}`;
|
logError(route.structure_id, route.id, e);
|
||||||
model.createLog(route.structure_id, route.id, error, true);
|
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
ws.render = (template, context) => {
|
ws.render = (template, context = {}) => {
|
||||||
context = context || {};
|
|
||||||
const eta = model.getTemplater(route.structure_id);
|
|
||||||
const structure = model.getStructure(route.structure_id);
|
const structure = model.getStructure(route.structure_id);
|
||||||
context.route = function (url) {
|
context.route = makeRoute(structure);
|
||||||
return withPrefix(structure.route_prefix, url);
|
|
||||||
};
|
|
||||||
|
|
||||||
ws.send(
|
ws.send(
|
||||||
bootstrapTemplateWithHTMXetc(
|
decorate(renderTemplate(route.structure_id, template, context), {
|
||||||
eta.render(template, context),
|
headInjection: structure.head_injection,
|
||||||
`/workshop/${route.structure_id}/route/${route.id}`,
|
source: sourceFor(route, req),
|
||||||
`/workshop/${route.structure_id}/clone_modal/`,
|
fragment: true,
|
||||||
route.verb == "GET" ? embedHTML(req.originalUrl) : null,
|
}),
|
||||||
structure.head_injection,
|
|
||||||
htmxRequest = true,
|
|
||||||
),
|
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
@ -195,14 +197,12 @@ function bootstrapWebsocketHandler(route) {
|
||||||
try {
|
try {
|
||||||
return handler(ws, req)
|
return handler(ws, req)
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
const error = e.stack ? `${e}\n\n${e.stack}` : `${e}`;
|
logError(route.structure_id, route.id, e);
|
||||||
model.createLog(route.structure_id, route.id, error, true);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
model.updateRoute({ ...route, error: null });
|
model.updateRoute({ ...route, error: null });
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
const error = e.stack ? `${e}\n\n${e.stack}` : `${e}`;
|
logError(route.structure_id, route.id, e);
|
||||||
model.createLog(route.structure_id, route.id, error, true);
|
|
||||||
model.updateRoute({ ...route, error: e.stack });
|
model.updateRoute({ ...route, error: e.stack });
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -297,30 +297,10 @@ app.all("/logout", async (req, res) => {
|
||||||
// | _ || || | | || _ | _____| || _ || || |
|
// | _ || || | | || _ | _____| || _ || || |
|
||||||
// |__| |__||_______||___| |_||___| |_||_______||__| |__||_______||___|
|
// |__| |__||_______||___| |_||___| |_||_______||__| |__||_______||___|
|
||||||
//
|
//
|
||||||
function bootstrapTemplateWithHTMXetc(
|
// The chrome injected into the <head> of every rendered page: the client-side
|
||||||
htmlString,
|
// stack (htmx/hyperscript/tailwind) plus the in-page editor overlay.
|
||||||
blissRoute,
|
function headChrome(headInjection) {
|
||||||
blissClone,
|
return `
|
||||||
blissCopy,
|
|
||||||
headInjection,
|
|
||||||
htmxRequest = false,
|
|
||||||
) {
|
|
||||||
if (
|
|
||||||
htmlString.toLowerCase().startsWith("<html>") ||
|
|
||||||
htmlString.toLowerCase().startsWith("<!doctype") ||
|
|
||||||
!htmxRequest
|
|
||||||
) {
|
|
||||||
const $ = cheerio.load(htmlString);
|
|
||||||
let head = $("head");
|
|
||||||
|
|
||||||
if (head.length === 0) {
|
|
||||||
$("html").prepend("<head></head>");
|
|
||||||
head = $("head");
|
|
||||||
}
|
|
||||||
|
|
||||||
head.attr("id", "head");
|
|
||||||
|
|
||||||
head.append(`
|
|
||||||
<script src="/js/hyperscript.js"></script>
|
<script src="/js/hyperscript.js"></script>
|
||||||
<script src="/js/tailwind.js"></script>
|
<script src="/js/tailwind.js"></script>
|
||||||
<script src="/js/htmx.js"></script>
|
<script src="/js/htmx.js"></script>
|
||||||
|
|
@ -335,48 +315,96 @@ function bootstrapTemplateWithHTMXetc(
|
||||||
</script>
|
</script>
|
||||||
<style> body { margin: 0; }</style>
|
<style> body { margin: 0; }</style>
|
||||||
${headInjection || ""}
|
${headInjection || ""}
|
||||||
`);
|
`;
|
||||||
|
|
||||||
if (blissRoute) {
|
|
||||||
$("body").attr("data-bliss-route", blissRoute);
|
|
||||||
$("body").attr("data-bliss-clone", blissClone);
|
|
||||||
if (blissCopy) $("body").attr("data-bliss-copy", blissCopy);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
htmlString = $.html();
|
// A `source` is the provenance of a rendered fragment: which structure/route
|
||||||
} else if (htmxRequest && blissRoute) {
|
// produced it, and (for GET routes) the snippet that re-embeds it. It becomes
|
||||||
const $ = cheerio.load(htmlString, null, false);
|
// the data-bliss-* attributes the inspector reads.
|
||||||
|
function blissAttrs(source) {
|
||||||
|
if (!source) return null;
|
||||||
|
const attrs = {
|
||||||
|
"data-bliss-route": `/workshop/${source.structureId}/route/${source.routeId}`,
|
||||||
|
"data-bliss-clone": `/workshop/${source.structureId}/clone_modal/`,
|
||||||
|
};
|
||||||
|
if (source.copyUrl) attrs["data-bliss-copy"] = source.copyUrl;
|
||||||
|
return attrs;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Full HTML document: inject the head chrome and stamp provenance on <body>.
|
||||||
|
function decoratePage(html, { headInjection, source } = {}) {
|
||||||
|
const $ = cheerio.load(html);
|
||||||
|
let head = $("head");
|
||||||
|
if (head.length === 0) {
|
||||||
|
$("html").prepend("<head></head>");
|
||||||
|
head = $("head");
|
||||||
|
}
|
||||||
|
head.attr("id", "head");
|
||||||
|
head.append(headChrome(headInjection));
|
||||||
|
|
||||||
|
const attrs = blissAttrs(source);
|
||||||
|
if (attrs) {
|
||||||
|
for (const [k, v] of Object.entries(attrs)) $("body").attr(k, v);
|
||||||
|
}
|
||||||
|
return $.html();
|
||||||
|
}
|
||||||
|
|
||||||
|
// HTMX partial: stamp provenance on the swapped-in elements (an oob swap loses
|
||||||
|
// its own attrs, so we descend into its children) and append an out-of-band
|
||||||
|
// <head> so head injection still reaches the page.
|
||||||
|
function decorateFragment(html, { headInjection, source } = {}) {
|
||||||
|
const $ = cheerio.load(html, null, false);
|
||||||
|
const attrs = blissAttrs(source);
|
||||||
|
if (attrs) {
|
||||||
const targets = [];
|
const targets = [];
|
||||||
|
for (const child of $.root().children()) {
|
||||||
// when we return an oob thing from htmx, we'll lose all the attrs when it swaps
|
|
||||||
// so this adds those attrs to the children of the swap if possible
|
|
||||||
for (let child of $.root().children()) {
|
|
||||||
if (child.attribs && child.attribs["hx-swap-oob"]) {
|
if (child.attribs && child.attribs["hx-swap-oob"]) {
|
||||||
for (let childchild of child.children) {
|
for (const grandchild of child.children) {
|
||||||
if (childchild.attribs) {
|
if (grandchild.attribs) targets.push(grandchild);
|
||||||
targets.push(childchild);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
targets.push(child);
|
targets.push(child);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
for (const target of targets) Object.assign(target.attribs, attrs);
|
||||||
// Directly update attributes without wrapping in Cheerio
|
}
|
||||||
for (let target of targets) {
|
return (
|
||||||
target.attribs["data-bliss-route"] = blissRoute;
|
$.html() +
|
||||||
target.attribs["data-bliss-clone"] = blissClone;
|
`<head id="head" hx-oob-swap="beforeend">${headInjection || ""}</head>`
|
||||||
if (blissCopy) target.attribs["data-bliss-copy"] = blissCopy;
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
htmlString = $.html();
|
// Single entry point for turning rendered HTML into a response body. A full
|
||||||
|
// document (or any non-htmx request) becomes a decorated page; an htmx partial
|
||||||
if (htmxRequest) {
|
// carrying provenance becomes a decorated fragment; anything else passes through.
|
||||||
htmlString += `<head id="head" hx-oob-swap="beforeend">${headInjection}</head>`;
|
function decorate(html, { headInjection, source, fragment } = {}) {
|
||||||
|
const lower = html.toLowerCase();
|
||||||
|
const isFullDoc =
|
||||||
|
lower.startsWith("<html>") || lower.startsWith("<!doctype");
|
||||||
|
if (isFullDoc || !fragment) {
|
||||||
|
return decoratePage(html, { headInjection, source });
|
||||||
}
|
}
|
||||||
|
if (source) return decorateFragment(html, { headInjection, source });
|
||||||
|
return html;
|
||||||
}
|
}
|
||||||
|
|
||||||
return htmlString;
|
// One prefix-bound route() helper per structure, shared by every render site.
|
||||||
|
function makeRoute(structure) {
|
||||||
|
return (url) => withPrefix(structure.route_prefix, url);
|
||||||
|
}
|
||||||
|
|
||||||
|
function renderTemplate(structureId, templateName, context) {
|
||||||
|
return model.getTemplater(structureId).render(templateName, context);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Provenance for a rendered response: GET routes are re-embeddable, so they
|
||||||
|
// carry the copy-embed snippet; other verbs don't.
|
||||||
|
function sourceFor(route, req) {
|
||||||
|
return {
|
||||||
|
structureId: route.structure_id,
|
||||||
|
routeId: route.id,
|
||||||
|
copyUrl: route.verb === "GET" ? embedHTML(req.originalUrl) : null,
|
||||||
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
app.post("/workshop", (req, res) => {
|
app.post("/workshop", (req, res) => {
|
||||||
|
|
@ -391,7 +419,7 @@ app.get("/workshop", (req, res) => {
|
||||||
});
|
});
|
||||||
|
|
||||||
function renderWorkshop(res, template, context) {
|
function renderWorkshop(res, template, context) {
|
||||||
res.send(bootstrapTemplateWithHTMXetc(eta.render(template, context)));
|
res.send(decorate(eta.render(template, context)));
|
||||||
}
|
}
|
||||||
|
|
||||||
function smartRedirect(req, res, redirectUrl) {
|
function smartRedirect(req, res, redirectUrl) {
|
||||||
|
|
@ -599,24 +627,16 @@ app.get("/workshop/:structure_id/template/:template_id", (req, res) => {
|
||||||
app.get("/workshop/:structure_id/template/:template_id/preview", (req, res) => {
|
app.get("/workshop/:structure_id/template/:template_id/preview", (req, res) => {
|
||||||
const template = model.getTemplate(req.params.template_id);
|
const template = model.getTemplate(req.params.template_id);
|
||||||
const struct = model.getStructure(req.params.structure_id);
|
const struct = model.getStructure(req.params.structure_id);
|
||||||
const eta = model.getTemplater(req.params.structure_id);
|
|
||||||
|
|
||||||
const context = vm.createContext({ it: null });
|
const context = vm.createContext({ it: null });
|
||||||
vm.runInContext(template.test_object, context);
|
vm.runInContext(template.test_object, context);
|
||||||
|
|
||||||
context.it.route = function (url) {
|
context.it.route = makeRoute(struct);
|
||||||
return withPrefix(struct.route_prefix, url);
|
|
||||||
};
|
|
||||||
|
|
||||||
return res.send(
|
return res.send(
|
||||||
bootstrapTemplateWithHTMXetc(
|
decorate(renderTemplate(req.params.structure_id, template.name, context.it), {
|
||||||
eta.render(template.name, context.it),
|
headInjection: struct.head_injection,
|
||||||
null,
|
}),
|
||||||
null,
|
|
||||||
null,
|
|
||||||
struct.head_injection,
|
|
||||||
false,
|
|
||||||
),
|
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
@ -647,21 +667,12 @@ app.get("/workshop/:structure_id/route/:route_id/preview", (req, res) => {
|
||||||
const route = model.getRoute(req.params.route_id);
|
const route = model.getRoute(req.params.route_id);
|
||||||
const struct = model.getStructure(req.params.structure_id);
|
const struct = model.getStructure(req.params.structure_id);
|
||||||
|
|
||||||
const it = {
|
const it = { route: makeRoute(struct) };
|
||||||
route: function (url) {
|
|
||||||
return withPrefix(struct.route_prefix, url);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return res.send(
|
return res.send(
|
||||||
bootstrapTemplateWithHTMXetc(
|
decorate(eta.renderString(route.scaffold_page_content, it), {
|
||||||
eta.renderString(route.scaffold_page_content, it),
|
headInjection: struct.head_injection,
|
||||||
null,
|
}),
|
||||||
null,
|
|
||||||
null,
|
|
||||||
struct.head_injection,
|
|
||||||
false,
|
|
||||||
),
|
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
@ -792,21 +803,21 @@ app.all("*", async (req, res) => {
|
||||||
try {
|
try {
|
||||||
let routeMatch = null;
|
let routeMatch = null;
|
||||||
|
|
||||||
|
// routes[verb] is ordered most-recently-updated first (getAllRoutes sorts
|
||||||
|
// by updated_at DESC), so the first match is the most recently saved route.
|
||||||
for (let { matcher, id } of routes[verb]) {
|
for (let { matcher, id } of routes[verb]) {
|
||||||
let matchFromRoute = matcher(req_path);
|
let matchFromRoute = matcher(req_path);
|
||||||
if (matchFromRoute) {
|
if (matchFromRoute) {
|
||||||
routeMatch = { params: matchFromRoute.params, id: id };
|
routeMatch = { params: matchFromRoute.params, id: id };
|
||||||
|
break;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (routeMatch) {
|
if (routeMatch) {
|
||||||
req.params = routeMatch.params;
|
req.params = routeMatch.params;
|
||||||
const route = db
|
const route = model.getRoute(routeMatch.id);
|
||||||
.prepare("SELECT * FROM routes WHERE id = ?")
|
|
||||||
.get(routeMatch.id);
|
|
||||||
|
|
||||||
const structure = model.getStructure(route.structure_id);
|
const structure = model.getStructure(route.structure_id);
|
||||||
const __urlPrefix = structure.route_prefix;
|
|
||||||
|
|
||||||
try {
|
try {
|
||||||
// todo: only add req res to contexst when running the handler()
|
// todo: only add req res to contexst when running the handler()
|
||||||
|
|
@ -817,21 +828,14 @@ app.all("*", async (req, res) => {
|
||||||
eta,
|
eta,
|
||||||
});
|
});
|
||||||
|
|
||||||
res.render = (template, context) => {
|
res.render = (template, context = {}) => {
|
||||||
context = context || {};
|
context.route = makeRoute(structure);
|
||||||
context.route = function (url) {
|
|
||||||
return withPrefix(__urlPrefix, url);
|
|
||||||
};
|
|
||||||
const eta = model.getTemplater(route.structure_id);
|
|
||||||
res.send(
|
res.send(
|
||||||
bootstrapTemplateWithHTMXetc(
|
decorate(renderTemplate(route.structure_id, template, context), {
|
||||||
eta.render(template, context),
|
headInjection: structure.head_injection,
|
||||||
`/workshop/${route.structure_id}/route/${route.id}`,
|
source: sourceFor(route, req),
|
||||||
`/workshop/${route.structure_id}/clone_modal/`,
|
fragment: req.headers["hx-request"],
|
||||||
verb == "GET" ? embedHTML(req.originalUrl) : null,
|
}),
|
||||||
structure.head_injection,
|
|
||||||
req.headers["hx-request"],
|
|
||||||
),
|
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
const handlerScript = new vm.Script(route.handler, { filename: `handler_${route.id}.js` });
|
const handlerScript = new vm.Script(route.handler, { filename: `handler_${route.id}.js` });
|
||||||
|
|
@ -840,9 +844,8 @@ app.all("*", async (req, res) => {
|
||||||
await handlerScript.runInContext(context);
|
await handlerScript.runInContext(context);
|
||||||
const result = await executionScript.runInContext(context);
|
const result = await executionScript.runInContext(context);
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
const error = e.stack ? `${e}\n\n${e.stack}` : `${e}`;
|
logError(structure.id, route.id, e);
|
||||||
model.createLog(structure.id, route.id, error, true);
|
return res.status(500).send(formatError(e));
|
||||||
return res.status(500).send(error);
|
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
res.status(404).json({ success: false, message: "Path not found" });
|
res.status(404).json({ success: false, message: "Path not found" });
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue