diff --git a/CLAUDE.md b/CLAUDE.md new file mode 100644 index 0000000..11619e3 --- /dev/null +++ b/CLAUDE.md @@ -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 ``, and attaches `data-bliss-route` / `data-bliss-clone` / `data-bliss-copy` attributes to `` (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`): `
`. 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/`. diff --git a/db.js b/db.js index d6ad103..343d2d3 100644 --- a/db.js +++ b/db.js @@ -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,60 +134,40 @@ 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 = ` - 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.structure_id = ? - ORDER BY sp.created_at DESC, sp2.created_at DESC - `; +const ROUTE_SELECT = ` + 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 +`; - 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) {

Test ${verb} Request

-
+ " method="${verb}"> diff --git a/index.js b/index.js index 7adadfa..efa5c31 100644 --- a/index.js +++ b/index.js @@ -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,20 +380,20 @@ 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"]) { res.set("HX-Redirect", redirectUrl); @@ -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", { - db: structdb, - ...sidebarStuff(db, req.params.structure_id), - }), - ), - ); + return renderWorkshop(res, "workshop/db_garden", { + db: structdb, + ...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, "
henlo <%= it.name %>
", @@ -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", { - template: template, - ...sidebarStuff(db, req.params.structure_id), - }), - ), - ); + return renderWorkshop(res, "workshop/template", { + template: template, + ...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, { - route: route, - previewUrl: previewUrl, - ...sidebarStuff(db, req.params.structure_id), - }), - ), - ); + return renderWorkshop(res, template, { + route: route, + previewUrl: previewUrl, + ...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", { - logs: logs, - structId: req.params.structure_id, - routeId: req.params.route_id, - since: lastId, - }), - ), - ); + 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", { - files, - ...sidebarStuff(db, structure_id), - }), - ), - ); + return renderWorkshop(res, "workshop/files", { + files, + ...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", { - structure, - dbs, - defaultRoutePrefix: path.join(structure.route_prefix || "", "clone"), - }), - ), - ); + 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 {