chore: cleanup of code

This commit is contained in:
Your Name 2026-08-02 22:06:42 -04:00
parent 28a441d98a
commit ae51fc763c
3 changed files with 264 additions and 275 deletions

86
CLAUDE.md Normal file
View file

@ -0,0 +1,86 @@
# CLAUDE.md
This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository.
## Commands
```bash
node index.js # Start the server (port 3000)
npx prettier --write . # Format code
```
No build step. No test suite. Restart the server to pick up changes to `index.js` or `db.js`; changes to Eta templates in `views/` are picked up live (cache is disabled).
Requires a `.env` file with `VAPID_PUBLIC_KEY` and `VAPID_PRIVATE_KEY` for web push.
## What This Is
Bliss is a browser-based low-code platform where users build mini web apps ("Structures") entirely through a `/workshop` UI. Each Structure has routes, templates, and SQLite databases — all stored in the main app database and executed at runtime via Node's `vm` module.
## Architecture
### Two layers of routes
The Express app has two kinds of routes:
1. **Workshop routes** (hardcoded in `index.js`) — The `/workshop/*` editor UI for managing structures, routes, templates, databases, and files.
2. **User routes** (dynamic, loaded from DB) — Caught by the `app.all("*")` handler at the bottom of `index.js`. At request time it matches the path against the `routes` in-memory array (rebuilt by `buildRoutes()`), finds the matching route record, and executes the user's `handler` JS string inside a sandboxed VM context.
`buildRoutes()` must be called after any route is created/updated to refresh the in-memory matcher array.
### Sandboxed execution (`bootstrapContext`)
User route handlers run in `vm.createContext()` with a restricted API:
- `require('eta')` — Eta template instance scoped to the structure
- `require('db')` — getter for named database instances. `require('db')(alias)` returns `{ library, sql }`: `sql` is the raw `better-sqlite3` instance for that db, and `library` is `module.exports` from that db's own "library" script (arbitrary JS, edited via `/workshop/:structure_id/db/:db_id/library`, run once per request with `sql` bound in its own vm context — see `bootstrapContext` in `index.js`). Handlers typically call helper functions off `library` rather than writing raw SQL inline.
- `require('push')` — web-push library
- `require('files')``{ saveFile }` helper
- `console.log` — writes to the app's `logs` table (not stdout)
- `fetch`, `setTimeout`, `clearTimeout`
- `route(url)` — prefixes a URL with the structure's `route_prefix`
Handlers call `res.render(templateName, context)` or `ws.render(templateName, context)` to render Eta templates. These are injected onto `req`/`res` after the context is created (not available inside `bootstrapContext` itself).
WebSocket routes are bootstrapped once by `bootstrapWebsocketHandler` and stored in `wsRoutes`; subsequent saves only update the handler function without re-registering the ws path.
### Template rendering (`bootstrapTemplateWithHTMXetc`)
Every HTML response passes through this function, which uses Cheerio to inject HTMX, Tailwind, Hyperscript, and `bliss_inspector.js` into the `<head>`, and attaches `data-bliss-route` / `data-bliss-clone` / `data-bliss-copy` attributes to `<body>` (or to fragment children for HTMX partial swaps). These attributes drive the in-page editor overlay.
- `data-bliss-route` — link to the workshop editor page for whatever produced this element.
- `data-bliss-clone``hx-get` target for the clone-structure modal.
- `data-bliss-copy` — only set for GET routes, via `embedHTML(url)` (`index.js`): `<div hx-get="{url}" hx-trigger="load"></div>`. This is the actual page-nesting primitive — the inspector's 📋 button copies this snippet to the clipboard, and pasting it into any other template lazy-loads that route as a live child page via HTMX. This is how Structures compose: pages nesting pages nesting pages, each one an independently addressable, independently editable route.
### The inspector (`bliss_inspector.js`) and where it's headed
Currently the 🔍 overlay (injected into every rendered page, see above) only highlights `[data-bliss-route]` elements and offers links out to the editor, a copy-embed button, and a clone-structure modal — it does not let you edit anything in place yet.
The long-term goal is for the inspector to become a real-time, in-page structure editor: instead of jumping out to `/workshop/...`, you'd edit a nested page's route/template directly where it's embedded and see it update live. Two things that design needs and don't exist yet:
- Each embedded view needs to carry its **source** (which structure/route/template produced it) — `data-bliss-route` currently only gives a link out, not enough info to edit inline.
- Each embedded view needs to carry its **args** — the context/params it was rendered with (e.g. `res.render(template, context)`'s `context`, URL/query params) — so the in-place editor can show and modify what was actually passed in, not just the output.
Cross-fragment interaction (nested pages talking to each other) isn't a bespoke Bliss feature — it rides on standard HTMX/Hyperscript behavior since every nested page lives in the same DOM: `hx-swap-oob` for out-of-band updates (already used for head injection), and bubbling custom events (Hyperscript `send`/`trigger`, `hx-trigger="eventName from:body"`) for one nested page to notify siblings/ancestors.
### Database layer (`db.js`)
- Main app DB: `./dbs/0.sqlite` — stores structures, routes, templates, dbs metadata, files, logs.
- User databases: `./dbs/{id}.sqlite` — one SQLite file per user-created DB, opened on demand and cached via LRU (max 25 connections).
- Eta template instances are also LRU-cached per structure (max 100). The cache must be invalidated if template content changes — currently the cache is not explicitly invalidated on update, relying on the LRU eviction policy.
- Migrations in `migrations/` are applied sequentially on startup (tracked in a `migrations` table).
### Structure cloning
`cloneStructure` (in `db.js`) deep-copies a structure's routes, templates, scaffold pages, and optionally its databases (full copy vs. aliased reference). Aliased DBs share the same SQLite file across structures.
### Views vs. user templates
- `views/` — Eta templates for the workshop UI itself (not user content).
- User-created templates are stored as rows in the `templates` table and resolved at render time via `getTemplater()` / `readFile`.
### Scaffold pages
Non-GET routes (POST, PUT, DELETE, WS) get auto-generated scaffold pages — test forms or WebSocket test UIs — stored in `scaffold_pages`. These are served at `/workshop/:structure_id/route/:route_id/preview`.
### Frontend stack (workshop UI)
HTMX + Hyperscript + Tailwind (CDN, preflight disabled) + `bliss_inspector.js` (the in-page editor overlay). All served from `public/js/`.

106
db.js
View file

@ -44,13 +44,13 @@ function applyMigrations() {
// | | _____| || |___ | | | |
// |_______||_______||_______||___| |_|
function createUser(db, username, hashedPassword) {
function createUser(username, hashedPassword) {
return db
.prepare("INSERT INTO users (username, password) VALUES (?, ?)")
.run(username, hashedPassword).lastInsertRowid;
}
function getUser(db, username) {
function getUser(username) {
return db.prepare("SELECT * from users where username = ?").get(username);
}
@ -76,7 +76,7 @@ function getDbInstance(dbId) {
return dbInstance;
}
function getAllRoutes(db) {
function getAllRoutes() {
return db
.prepare(
`
@ -100,7 +100,7 @@ function getTemplater(structId) {
return path;
};
etaInstance.readFile = function (templateAlias) {
return getTemplateContentByName(db, structId, templateAlias).content;
return getTemplateContentByName(structId, templateAlias).content;
};
templateCache.set(structId, etaInstance);
}
@ -108,15 +108,15 @@ function getTemplater(structId) {
return etaInstance;
}
function getStructures(db) {
function getStructures() {
return db.prepare("SELECT * from structures").all();
}
function getStructure(db, id) {
function getStructure(id) {
return db.prepare("SELECT * from structures where ID = ?").get(id);
}
function createStructure(db, name, userId) {
function createStructure(name, userId) {
const stmt = db.prepare(
"INSERT INTO structures (name, user_id) VALUES (?, ?)",
);
@ -124,7 +124,7 @@ function createStructure(db, name, userId) {
return info.lastInsertRowid; // Returns the structure_id of the newly created structure
}
function createRoute(db, verb, path, structureId, handler) {
function createRoute(verb, path, structureId, handler) {
path = encodeURI(path);
const stmt = db.prepare(
@ -134,14 +134,13 @@ function createRoute(db, verb, path, structureId, handler) {
const routeId = info.lastInsertRowid; // Get the newly created route ID
if (verb !== 'GET') {
createScaffoldPage(db, routeId);
createScaffoldPage(routeId);
}
return routeId; // Returns the route_id of the newly created route
}
function getRoutes(db, structureId) {
const query = `
const ROUTE_SELECT = `
SELECT
r.*,
s.route_prefix,
@ -154,40 +153,21 @@ function getRoutes(db, structureId) {
LEFT JOIN scaffold_pages sp ON r.id = sp.route_id
LEFT JOIN scaffold_params sp2 ON r.id = sp2.route_id
LEFT JOIN structures s ON r.structure_id = s.id
WHERE r.structure_id = ?
ORDER BY sp.created_at DESC, sp2.created_at DESC
`;
return db.prepare(query).all(structureId);
function getRoutes(structureId) {
return db
.prepare(`${ROUTE_SELECT} WHERE r.structure_id = ? ORDER BY sp.created_at DESC, sp2.created_at DESC`)
.all(structureId);
}
function getRoute(db, routeId) {
const query = `
SELECT
r.*,
s.route_prefix,
sp.id AS scaffold_page_id,
sp.content AS scaffold_page_content,
sp2.id AS scaffold_params_id,
sp2.query_params,
sp2.url_params
FROM routes r
LEFT JOIN scaffold_pages sp ON r.id = sp.route_id
LEFT JOIN scaffold_params sp2 ON r.id = sp2.route_id
LEFT JOIN structures s ON r.structure_id = s.id
WHERE r.id = ?
ORDER BY sp.created_at DESC, sp2.created_at DESC
LIMIT 1
`;
return db.prepare(query).get(routeId);
function getRoute(routeId) {
return db
.prepare(`${ROUTE_SELECT} WHERE r.id = ? ORDER BY sp.created_at DESC, sp2.created_at DESC LIMIT 1`)
.get(routeId);
}
function defaultEndpointForRoute(db, routeId) {
}
function updateRoute(db, route) {
function updateRoute(route) {
const fields = [
"verb",
"path",
@ -204,7 +184,7 @@ function updateRoute(db, route) {
db.prepare(sql).run(...values);
}
function updateDb(db, appDb) {
function updateDb(appDb) {
const fields = ["name", "library"];
const values = fields.map((field) => appDb[field]);
const placeholders = fields.map((field) => `${field} = ?`).join(", ");
@ -214,7 +194,7 @@ function updateDb(db, appDb) {
db.prepare(sql).run(...values);
}
function updateStruct(db, struct) {
function updateStruct(struct) {
const fields = ["name", "route_prefix", "head_injection"];
const values = fields.map((field) => struct[field]);
const placeholders = fields.map((field) => `${field} = ?`).join(", ");
@ -224,7 +204,7 @@ function updateStruct(db, struct) {
db.prepare(sql).run(...values);
}
function updateTemplate(db, template) {
function updateTemplate(template) {
const fields = ["content", "name", "test_object"];
const values = fields.map((field) => template[field]);
const placeholders = fields.map((field) => `${field} = ?`).join(", ");
@ -234,17 +214,17 @@ function updateTemplate(db, template) {
db.prepare(sql).run(...values);
}
function getTemplates(db, structureId) {
function getTemplates(structureId) {
return db
.prepare("SELECT * from templates where structure_id = ?")
.all(structureId);
}
function getTemplate(db, templateId) {
function getTemplate(templateId) {
return db.prepare("SELECT * from templates where id = ?").get(templateId);
}
function getTemplateContentByName(db, structId, name) {
function getTemplateContentByName(structId, name) {
return db
.prepare(
"SELECT content from templates where structure_id = ? AND name = ?",
@ -252,7 +232,7 @@ function getTemplateContentByName(db, structId, name) {
.get(structId, name);
}
function createTemplate(db, structureId, name, content, testObjectString) {
function createTemplate(structureId, name, content, testObjectString) {
return db
.prepare(
"INSERT INTO templates (structure_id, name, content, test_object) VALUES (?, ?, ?, ?)",
@ -260,7 +240,7 @@ function createTemplate(db, structureId, name, content, testObjectString) {
.run(structureId, name, content, testObjectString).lastInsertRowid;
}
function getDbsForStructure(db, structureId) {
function getDbsForStructure(structureId) {
return db
.prepare(
`SELECT
@ -280,7 +260,7 @@ function getDbsForStructure(db, structureId) {
.all(structureId);
}
function getDb(db, dbId) {
function getDb(dbId) {
return db
.prepare(
`SELECT *
@ -291,7 +271,7 @@ function getDb(db, dbId) {
.get(dbId);
}
function getDbForStructure(db, structureId, dbId) {
function getDbForStructure(structureId, dbId) {
return db
.prepare(
`SELECT *
@ -304,7 +284,7 @@ function getDbForStructure(db, structureId, dbId) {
.get(structureId, dbId);
}
function createDb(db, structId, name) {
function createDb(structId, name) {
const transaction = db.transaction(() => {
const insertDbStmt = db.prepare(`
INSERT INTO dbs (name, structure_id)
@ -328,7 +308,7 @@ function createDb(db, structId, name) {
return transaction();
}
function attachDb(db, structId, dbId, alias) {
function attachDb(structId, dbId, alias) {
const insertStructureDbStmt = db.prepare(`
INSERT INTO structure_dbs (db_id, structure_id, alias)
VALUES (?, ?, ?)
@ -336,7 +316,7 @@ function attachDb(db, structId, dbId, alias) {
insertStructureDbStmt.run(dbId, structId, alias);
}
function getFilesForStruct(db, structureId) {
function getFilesForStruct(structureId) {
let test = db
.prepare("SELECT * FROM files WHERE structure_id = ? ORDER BY id DESC")
.all(structureId);
@ -344,11 +324,11 @@ function getFilesForStruct(db, structureId) {
return test;
}
function getFile(db, fileId) {
function getFile(fileId) {
return db.prepare("SELECT * FROM files WHERE id = ?").get(fileId);
}
function createFile(db, structure_id, name, filePath, mime_type, mime_subtype) {
function createFile(structure_id, name, filePath, mime_type, mime_subtype) {
return db
.prepare(
"INSERT INTO files (structure_id, name, path, mime_type, mime_subtype) VALUES (?, ?, ?, ?, ?)",
@ -468,7 +448,7 @@ function cloneStructure(
return transaction();
}
function createLog(db, structureId, routeId, content, error = false) {
function createLog(structureId, routeId, content, error = false) {
const stmt = db.prepare(
"INSERT INTO logs (error, structure_id, route_id, content) VALUES (?, ?, ?, ?)",
);
@ -476,7 +456,7 @@ function createLog(db, structureId, routeId, content, error = false) {
return info.lastInsertRowid; // Returns the log_id of the newly created log
}
function getLogsByRoute(db, routeId) {
function getLogsByRoute(routeId) {
return db
.prepare(
"SELECT * FROM logs WHERE route_id = ? ORDER BY created_at DESC LIMIT 50",
@ -485,7 +465,7 @@ function getLogsByRoute(db, routeId) {
.reverse();
}
function getNewLogsByRoute(db, routeId, since) {
function getNewLogsByRoute(routeId, since) {
return db
.prepare(
"SELECT * FROM logs WHERE route_id = ? AND id > ? ORDER BY created_at DESC",
@ -493,7 +473,7 @@ function getNewLogsByRoute(db, routeId, since) {
.all(routeId, since);
}
function getMostRecentLogIdByRoute(db, routeId) {
function getMostRecentLogIdByRoute(routeId) {
const result = db
.prepare("SELECT MAX(id) AS id FROM logs WHERE route_id = ?")
.get(routeId);
@ -505,9 +485,9 @@ function buildScaffoldUrl(endpoint, urlParams, queryString) {
return queryString ? `${populatedUrl}?${queryString}` : populatedUrl;
}
function createScaffoldPage(db, routeId, content=null) {
function createScaffoldPage(routeId, content=null) {
if (!content) {
const route = getRoute(db, routeId)
const route = getRoute(routeId)
const endpoint = buildScaffoldUrl(route.path, route.url_params, route.query_params);
content = getDefaultScaffoldContentByVerb(endpoint, route.verb);
console.log("gabga", content, route, endpoint)
@ -520,14 +500,14 @@ function createScaffoldPage(db, routeId, content=null) {
return info.lastInsertRowid; // Returns the scaffold_page id of the newly created scaffold page
}
function getLatestScaffoldPage(db, routeId) {
function getLatestScaffoldPage(routeId) {
const stmt = db.prepare(
"SELECT * FROM scaffold_pages WHERE route_id = ? ORDER BY created_at DESC LIMIT 1"
);
return stmt.get(routeId);
}
function updateScaffoldPage(db, scaffoldPage) {
function updateScaffoldPage(scaffoldPage) {
const fields = ["content"];
const values = fields.map((field) => scaffoldPage[field]);
const placeholders = fields.map((field) => `${field} = ?`).join(", ");
@ -582,7 +562,7 @@ function generateModifyForm(endpoint, verb) {
<body>
<h1>Test ${verb} Request</h1>
<form action="<%= it.route(${endpoint}) %>" method="${verb}">
<form action="<%= it.route("${endpoint}") %>" method="${verb}">
<!-- Add your form fields here to test ${verb.toLowerCase()} requests. -->
<label for="testField">Test Field:</label>
<input type="text" id="testField" name="testField" required>

297
index.js
View file

@ -28,6 +28,11 @@ let routes = { GET: [], POST: [], PUT: [], DELETE: [] };
const wsRoutes = {}
const wsConnections = {}
const INSPECT_OPTS = { showHidden: false, depth: null, colors: false, compact: false };
function inspectArgs(args) {
return args.map((v) => util.inspect(v, INSPECT_OPTS)).join(" ");
}
app.use(bodyParser.urlencoded({ extended: true }));
app.use(bodyParser.json());
app.use(express.static("public"));
@ -64,7 +69,6 @@ async function saveFile(structureId, req, uploadedFile, asset=false) {
await uploadedFile.mv(path.join(uploadPath, name));
let id = model.createFile(
db,
structureId,
name,
storedPath,
@ -73,13 +77,13 @@ async function saveFile(structureId, req, uploadedFile, asset=false) {
asset,
);
let file = model.getFile(db, id);
let file = model.getFile(id);
file.url = prefixUrlWithHost(req, file.path);
return file
}
function bootstrapContext(db, structureId, routeId, initContext) {
function bootstrapContext(structureId, routeId, initContext) {
const allDbInstances = {};
function getDb(alias) {
return allDbInstances[alias];
@ -88,12 +92,12 @@ function bootstrapContext(db, structureId, routeId, initContext) {
// 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(db, 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(db, structureId);
let dbs = model.getDbsForStructure(structureId);
let context = vm.createContext({
...initContext,
require: function (str) {
@ -102,21 +106,8 @@ function bootstrapContext(db, structureId, routeId, initContext) {
module: { exports: null },
console: {
log: function (...content) {
const s = content.reduce((acc, curr) => {
console.log(curr);
return (
acc +
(acc ? " " : "") +
`${util.inspect(curr, {
showHidden: false,
depth: null, // `null` lets you see the full depth of the object
colors: false, // Setting this to true uses ANSI color codes
compact: false,
})}`
);
}, "");
model.createLog(db, structureId, routeId, s);
content.forEach((c) => console.log(c));
model.createLog(structureId, routeId, inspectArgs(content));
},
},
vapidPublicKey: vapidPublicKey,
@ -124,7 +115,7 @@ function bootstrapContext(db, structureId, routeId, initContext) {
clearTimeout: clearTimeout,
setTimeout: setTimeout,
route: function (url) {
return __urlPrefix ? path.join(__urlPrefix, url) : url;
return withPrefix(__urlPrefix, url);
},
});
for (let appDb of dbs) {
@ -141,9 +132,12 @@ function bootstrapContext(db, structureId, routeId, initContext) {
return context;
}
function withPrefix(prefix, url) {
return prefix ? path.join(prefix, url) : url;
}
function routeWithPrefix(route) {
const prefix = route.route_prefix;
return prefix ? path.join(prefix, route.path) : route.path;
return withPrefix(route.route_prefix, route.path);
}
@ -151,7 +145,7 @@ function bootstrapWebsocketHandler(route) {
// todo only expose app when running the handler, should not be available to the handler itself
// need to move to runscript or whatever...or remove app because we can just call it using the handler?
try {
const context = bootstrapContext(db, route.structure_id, route.id, { app });
const context = bootstrapContext(route.structure_id, route.id, { app });
let handler = vm.runInContext(`${route.handler}\n\nhandler;`, context);
if (!wsRoutes[routeWithPrefix(route)]) {
@ -167,17 +161,17 @@ function bootstrapWebsocketHandler(route) {
}
catch (e) {
const error = e.stack ? `${e}\n\n${e.stack}` : `${e}`;
model.createLog(db, route.structure_id, route.id, error, true);
model.createLog(route.structure_id, route.id, error, true);
}
})
}
ws.render = (template, context) => {
context = context || {};
context.route = function (url) {
return __urlPrefix ? path.join(__urlPrefix, url) : url;
};
const eta = model.getTemplater(route.structure_id);
const structure = model.getStructure(db, route.structure_id);
const structure = model.getStructure(route.structure_id);
context.route = function (url) {
return withPrefix(structure.route_prefix, url);
};
ws.send(
bootstrapTemplateWithHTMXetc(
@ -202,14 +196,14 @@ function bootstrapWebsocketHandler(route) {
return handler(ws, req)
} catch (e) {
const error = e.stack ? `${e}\n\n${e.stack}` : `${e}`;
model.createLog(db, route.structure_id, route.id, error, true);
model.createLog(route.structure_id, route.id, error, true);
}
}
model.updateRoute(db, { ...route, error: null });
model.updateRoute({ ...route, error: null });
} catch (e) {
const error = e.stack ? `${e}\n\n${e.stack}` : `${e}`;
model.createLog(db, route.structure_id, route.id, error, true);
model.updateRoute(db, { ...route, error: e.stack });
model.createLog(route.structure_id, route.id, error, true);
model.updateRoute({ ...route, error: e.stack });
}
}
@ -221,7 +215,7 @@ function buildRoutes() {
DELETE: [],
};
for (let route of model.getAllRoutes(db)) {
for (let route of model.getAllRoutes()) {
const p = routeWithPrefix(route);
if (route.verb == "WS") {
bootstrapWebsocketHandler(route);
@ -251,7 +245,7 @@ app.post("/register", async (req, res) => {
try {
const { username, password } = req.body;
const hashedPassword = await bcrypt.hash(password, 10); // 10 is the saltRounds
userId = model.createUser(db, username, hashedPassword);
const userId = model.createUser(username, hashedPassword);
req.session.userId = userId;
res.redirect("/workshop");
} catch (e) {
@ -261,7 +255,7 @@ app.post("/register", async (req, res) => {
app.post("/login", async (req, res) => {
const { username, password } = req.body;
const user = model.getUser(db, username);
const user = model.getUser(username);
if (user && (await bcrypt.compare(password, user.password))) {
req.session.userId = user.id;
@ -386,19 +380,19 @@ function bootstrapTemplateWithHTMXetc(
}
app.post("/workshop", (req, res) => {
let structId = model.createStructure(db, req.body.name);
let structId = model.createStructure(req.body.name);
return res.redirect("/workshop/" + structId);
});
app.get("/workshop", (req, res) => {
return res.send(
bootstrapTemplateWithHTMXetc(
eta.render("workshop/index", {
structures: model.getStructures(db),
}),
),
);
return renderWorkshop(res, "workshop/index", {
structures: model.getStructures(),
});
});
function renderWorkshop(res, template, context) {
res.send(bootstrapTemplateWithHTMXetc(eta.render(template, context)));
}
function smartRedirect(req, res, redirectUrl) {
if (req.headers["hx-request"]) {
@ -409,21 +403,17 @@ function smartRedirect(req, res, redirectUrl) {
}
}
function sidebarStuff(db, structId) {
function sidebarStuff(structId) {
return {
structure: model.getStructure(db, structId),
routes: model.getRoutes(db, structId),
templates: model.getTemplates(db, structId),
dbs: model.getDbsForStructure(db, structId),
structure: model.getStructure(structId),
routes: model.getRoutes(structId),
templates: model.getTemplates(structId),
dbs: model.getDbsForStructure(structId),
};
}
app.get("/workshop/:structure_id", (req, res) => {
return res.send(
bootstrapTemplateWithHTMXetc(
eta.render("workshop/editor", sidebarStuff(db, req.params.structure_id)),
),
);
return renderWorkshop(res, "workshop/editor", sidebarStuff(req.params.structure_id));
});
app.post("/workshop/:structure_id/clone", (req, res) => {
@ -447,34 +437,29 @@ app.post("/workshop/:structure_id/clone", (req, res) => {
});
app.post("/workshop/:structure_id/db", (req, res) => {
const dbId = model.createDb(db, req.params.structure_id, req.body.name);
const dbId = model.createDb(req.params.structure_id, req.body.name);
const redirectUrl = `/workshop/${req.params.structure_id}/db/${dbId}`;
return smartRedirect(req, res, redirectUrl);
});
app.post("/workshop/:structure_id/db/attach", (req, res) => {
model.attachDb(db, req.params.structure_id, req.body.db_id, req.body.alias);
model.attachDb(req.params.structure_id, req.body.db_id, req.body.alias);
const redirectUrl = `/workshop/${req.params.structure_id}/db/${req.body.db_id}`;
return smartRedirect(req, res, redirectUrl);
});
app.get("/workshop/:structure_id/db/:db_id", (req, res) => {
const structdb = model.getDbForStructure(
db,
req.params.structure_id,
req.params.db_id,
);
if (!structdb) {
return res.send("uh oh");
}
return res.send(
bootstrapTemplateWithHTMXetc(
eta.render("workshop/db_garden", {
return renderWorkshop(res, "workshop/db_garden", {
db: structdb,
...sidebarStuff(db, req.params.structure_id),
}),
),
);
...sidebarStuff(req.params.structure_id),
});
});
app.post("/workshop/:structure_id/route", (req, res) => {
@ -494,7 +479,6 @@ app.post("/workshop/:structure_id/route", (req, res) => {
p = p.substring(0, p.length - 1);
}
const route = model.createRoute(
db,
req.body.verb,
p[0] == "/" ? p.substring(1) : "/" + p,
req.params.structure_id,
@ -510,12 +494,12 @@ app.post("/workshop/:structure_id/route", (req, res) => {
});
app.put("/workshop/:structure_id/route/:route_id", (req, res) => {
const route = model.getRoute(db, req.params.route_id);
model.updateRoute(db, { ...route, ...req.body });
const route = model.getRoute(req.params.route_id);
model.updateRoute({ ...route, ...req.body });
if (req.body.scaffold_page) {
// TODO: make this send over scaffold page id too someday
const latestPage = model.getLatestScaffoldPage(db, req.params.route_id)
model.updateScaffoldPage(db, { ...latestPage, content: req.body.scaffold_page })
const latestPage = model.getLatestScaffoldPage(req.params.route_id)
model.updateScaffoldPage({ ...latestPage, content: req.body.scaffold_page })
}
if (route.verb == "WS") {
bootstrapWebsocketHandler({ ...route, ...req.body })
@ -526,9 +510,9 @@ app.put("/workshop/:structure_id/route/:route_id", (req, res) => {
app.put("/workshop/:structure_id/db/:db_id/library", (req, res) => {
try {
let dbId = req.params.db_id;
let appDb = model.getDb(db, dbId);
model.updateDb(db, { ...appDb, library: req.body.library });
appDb = model.getDb(db, dbId);
let appDb = model.getDb(dbId);
model.updateDb({ ...appDb, library: req.body.library });
appDb = model.getDb(dbId);
let dbInstance = model.getDbInstance(dbId);
let capturedOutput = [];
@ -536,20 +520,11 @@ app.put("/workshop/:structure_id/db/:db_id/library", (req, res) => {
module: { exports: null },
sql: dbInstance,
console: {
log: (...args) => capturedOutput.push(args.join(" ")),
log: (...args) => capturedOutput.push(inspectArgs(args)),
},
});
let evaledCode = vm.runInContext(appDb.library, context);
let stdout = capturedOutput
.map((v) => {
return util.inspect(v, {
showHidden: false,
depth: null, // `null` lets you see the full depth of the object
colors: false, // Setting this to true uses ANSI color codes
compact: false,
});
})
.join("\n");
let stdout = capturedOutput.join("\n");
return res.send(`${stdout}\n\n> ${evaledCode}`.trim());
} catch (e) {
return res.send(`${e}\n\n${e.stack}`);
@ -559,26 +534,14 @@ app.put("/workshop/:structure_id/db/:db_id/library", (req, res) => {
app.post("/workshop/:structure_id/db/:db_id/repl", (req, res) => {
try {
let dbId = req.params.db_id;
let appDb = model.getDb(db, dbId);
let appDb = model.getDb(dbId);
let dbInstance = model.getDbInstance(dbId);
let capturedOutput = [];
let context = vm.createContext({
module: { exports: null },
sql: dbInstance,
console: {
log: (...args) =>
capturedOutput.push(
args
.map((v) => {
return util.inspect(v, {
showHidden: false,
depth: null, // `null` lets you see the full depth of the object
colors: false, // Setting this to true uses ANSI color codes
compact: false,
});
})
.join(" "),
),
log: (...args) => capturedOutput.push(inspectArgs(args)),
},
});
const libraryScript = new vm.Script(appDb.library);
@ -597,7 +560,6 @@ app.post("/workshop/:structure_id/template", (req, res) => {
let name = req.body.name;
const template = model.createTemplate(
db,
req.params.structure_id,
name,
"<div>henlo <%= it.name %></div>",
@ -616,8 +578,8 @@ app.put("/workshop/:structure_id/template/:template_id", (req, res) => {
let content = req.body.content;
let test_object = req.body.test_object;
model.updateTemplate(db, {
...model.getTemplate(db, id),
model.updateTemplate({
...model.getTemplate(id),
content,
test_object,
});
@ -626,28 +588,24 @@ app.put("/workshop/:structure_id/template/:template_id", (req, res) => {
});
app.get("/workshop/:structure_id/template/:template_id", (req, res) => {
const template = model.getTemplate(db, req.params.template_id);
const template = model.getTemplate(req.params.template_id);
return res.send(
bootstrapTemplateWithHTMXetc(
eta.render("workshop/template", {
return renderWorkshop(res, "workshop/template", {
template: template,
...sidebarStuff(db, req.params.structure_id),
}),
),
);
...sidebarStuff(req.params.structure_id),
});
});
app.get("/workshop/:structure_id/template/:template_id/preview", (req, res) => {
const template = model.getTemplate(db, req.params.template_id);
const struct = model.getStructure(db, req.params.structure_id);
const template = model.getTemplate(req.params.template_id);
const struct = model.getStructure(req.params.structure_id);
const eta = model.getTemplater(req.params.structure_id);
const context = vm.createContext({ it: null });
vm.runInContext(template.test_object, context);
context.it.route = function (url) {
return struct.route_prefix ? path.join(struct.route_prefix, url) : url;
return withPrefix(struct.route_prefix, url);
};
return res.send(
@ -663,11 +621,8 @@ app.get("/workshop/:structure_id/template/:template_id/preview", (req, res) => {
});
app.get("/workshop/:structure_id/route/:id", (req, res) => {
const route = model.getRoute(db, req.params.id);
let routePrefix = model.getStructure(
db,
req.params.structure_id,
).route_prefix;
const route = model.getRoute(req.params.id);
let routePrefix = model.getStructure(req.params.structure_id).route_prefix;
let previewUrl = null;
if (route["verb"] == "GET" ) {
@ -679,26 +634,22 @@ app.get("/workshop/:structure_id/route/:id", (req, res) => {
previewUrl = `/workshop/${req.params.structure_id}/route/${req.params.id}/preview`;
}
template = route["verb"] == "GET" ? "workshop/route" : "workshop/cud_route";
const template = route["verb"] == "GET" ? "workshop/route" : "workshop/cud_route";
return res.send(
bootstrapTemplateWithHTMXetc(
eta.render(template, {
return renderWorkshop(res, template, {
route: route,
previewUrl: previewUrl,
...sidebarStuff(db, req.params.structure_id),
}),
),
);
...sidebarStuff(req.params.structure_id),
});
});
app.get("/workshop/:structure_id/route/:route_id/preview", (req, res) => {
const route = model.getRoute(db, req.params.route_id);
const struct = model.getStructure(db, req.params.structure_id);
const route = model.getRoute(req.params.route_id);
const struct = model.getStructure(req.params.structure_id);
const it = {
route: function (url) {
return struct.route_prefix ? path.join(struct.route_prefix, url) : url;
return withPrefix(struct.route_prefix, url);
}
}
@ -718,23 +669,19 @@ app.get("/workshop/:structure_id/route/:route_id/logs", (req, res) => {
const since = req.query.since;
let logs = [];
if (since != undefined) {
logs = model.getNewLogsByRoute(db, req.params.route_id, since);
logs = model.getNewLogsByRoute(req.params.route_id, since);
} else {
logs = model.getLogsByRoute(db, req.params.route_id);
logs = model.getLogsByRoute(req.params.route_id);
}
const lastId = model.getMostRecentLogIdByRoute(db, req.params.route_id);
const lastId = model.getMostRecentLogIdByRoute(req.params.route_id);
return res.send(
bootstrapTemplateWithHTMXetc(
eta.render("workshop/logs", {
return renderWorkshop(res, "workshop/logs", {
logs: logs,
structId: req.params.structure_id,
routeId: req.params.route_id,
since: lastId,
}),
),
);
});
});
// POST route to handle file upload
@ -766,34 +713,26 @@ function prefixUrlWithHost(req, path) {
app.get("/workshop/:structure_id/files", (req, res) => {
const { structure_id } = req.params;
const files = model.getFilesForStruct(db, structure_id);
const files = model.getFilesForStruct(structure_id);
files.map((it) => {
it.url = prefixUrlWithHost(req, it.path);
});
return res.send(
bootstrapTemplateWithHTMXetc(
eta.render("workshop/files", {
return renderWorkshop(res, "workshop/files", {
files,
...sidebarStuff(db, structure_id),
}),
),
);
...sidebarStuff(structure_id),
});
});
app.get("/workshop/:structure_id/settings", (req, res) => {
return res.send(
bootstrapTemplateWithHTMXetc(
eta.render("workshop/settings", {
...sidebarStuff(db, req.params.structure_id),
}),
),
);
return renderWorkshop(res, "workshop/settings", {
...sidebarStuff(req.params.structure_id),
});
});
app.put("/workshop/:structure_id/settings", (req, res) => {
let structId = req.params.structure_id;
let struct = model.getStructure(db, structId);
let struct = model.getStructure(structId);
let routePrefix = req.body.route_prefix;
if (routePrefix[0] != "/") {
@ -803,59 +742,43 @@ app.put("/workshop/:structure_id/settings", (req, res) => {
routePrefix = routePrefix.substring(0, routePrefix.length - 1);
}
model.updateStruct(db, {
model.updateStruct({
...struct,
route_prefix: routePrefix,
head_injection: req.body.head_injection,
});
struct = model.getStructure(db, structId);
struct = model.getStructure(structId);
res.send("success!");
});
app.get("/workshop/:structure_id/new_template_modal", (req, res) => {
return res.send(
bootstrapTemplateWithHTMXetc(
eta.render("workshop/new_template_modal", {
structure: model.getStructure(db, req.params.structure_id),
}),
),
);
return renderWorkshop(res, "workshop/new_template_modal", {
structure: model.getStructure(req.params.structure_id),
});
});
app.get("/workshop/:structure_id/new_route_modal", (req, res) => {
return res.send(
bootstrapTemplateWithHTMXetc(
eta.render("workshop/new_route_modal", {
structure: model.getStructure(db, req.params.structure_id),
}),
),
);
return renderWorkshop(res, "workshop/new_route_modal", {
structure: model.getStructure(req.params.structure_id),
});
});
app.get("/workshop/:structure_id/new_db_modal", (req, res) => {
return res.send(
bootstrapTemplateWithHTMXetc(
eta.render("workshop/new_db_modal", {
structure: model.getStructure(db, req.params.structure_id),
}),
),
);
return renderWorkshop(res, "workshop/new_db_modal", {
structure: model.getStructure(req.params.structure_id),
});
});
app.get("/workshop/:structure_id/clone_modal", (req, res) => {
const structure = model.getStructure(db, req.params.structure_id);
const dbs = model.getDbsForStructure(db, req.params.structure_id);
const structure = model.getStructure(req.params.structure_id);
const dbs = model.getDbsForStructure(req.params.structure_id);
return res.send(
bootstrapTemplateWithHTMXetc(
eta.render("workshop/clone_structure_modal", {
return renderWorkshop(res, "workshop/clone_structure_modal", {
structure,
dbs,
defaultRoutePrefix: path.join(structure.route_prefix || "", "clone"),
}),
),
);
});
});
function embedHTML(url) {
@ -882,13 +805,13 @@ app.all("*", async (req, res) => {
.prepare("SELECT * FROM routes WHERE id = ?")
.get(routeMatch.id);
const structure = model.getStructure(db, route.structure_id);
const structure = model.getStructure(route.structure_id);
const __urlPrefix = structure.route_prefix;
try {
// todo: only add req res to contexst when running the handler()
// which means moving to runscript instead of runincontext for that
let context = bootstrapContext(db, route.structure_id, route.id, {
let context = bootstrapContext(route.structure_id, route.id, {
req,
res,
eta,
@ -897,7 +820,7 @@ app.all("*", async (req, res) => {
res.render = (template, context) => {
context = context || {};
context.route = function (url) {
return __urlPrefix ? path.join(__urlPrefix, url) : url;
return withPrefix(__urlPrefix, url);
};
const eta = model.getTemplater(route.structure_id);
res.send(
@ -918,7 +841,7 @@ app.all("*", async (req, res) => {
const result = await executionScript.runInContext(context);
} catch (e) {
const error = e.stack ? `${e}\n\n${e.stack}` : `${e}`;
model.createLog(db, structure.id, route.id, error, true);
model.createLog(structure.id, route.id, error, true);
return res.status(500).send(error);
}
} else {