feat: add logs

This commit is contained in:
HRG @ SCExC 2024-10-29 21:29:40 -04:00
parent c40bdcbbed
commit 305bf8b571
11 changed files with 690 additions and 468 deletions

465
db.js Normal file
View file

@ -0,0 +1,465 @@
const fs = require("fs");
const path = require("path");
const betterSqlite3 = require("better-sqlite3");
const { LRUCache } = require("lru-cache");
const { Eta } = require("eta");
const db = betterSqlite3("./dbs/0.sqlite");
db.pragma("journal_mode = WAL");
function applyMigrations() {
db.exec(`
CREATE TABLE IF NOT EXISTS migrations (
id INTEGER PRIMARY KEY AUTOINCREMENT,
filename TEXT NOT NULL,
applied_at DATETIME DEFAULT CURRENT_TIMESTAMP
);
`);
const migrationsDir = path.join(__dirname, "/migrations");
const migrationFiles = fs
.readdirSync(migrationsDir)
.filter((file) => file.endsWith(".sql"));
migrationFiles.forEach((file) => {
const isApplied = db
.prepare("SELECT filename FROM migrations WHERE filename = ?")
.get(file);
if (!isApplied) {
const sql = fs.readFileSync(path.join(migrationsDir, file), "utf-8");
db.exec(sql);
db.prepare("INSERT INTO migrations (filename) VALUES (?)").run(file);
console.log(`Migration applied: ${file}`);
}
});
}
// __ __ _______ _______ ______
// | | | || || || _ |
// | | | || _____|| ___|| | ||
// | |_| || |_____ | |___ | |_||_
// | ||_____ || ___|| __ |
// | | _____| || |___ | | | |
// |_______||_______||_______||___| |_|
function createUser(db, username, hashedPassword) {
return db
.prepare("INSERT INTO users (username, password) VALUES (?, ?)")
.run(username, hashedPassword).lastInsertRowid;
}
function getUser(db, username) {
return db.prepare("SELECT * from users where username = ?").get(username);
}
// _ _ _______ ______ ___ _ _______ __ __ _______ _______
// | | _ | || || _ | | | | || || | | || || |
// | || || || _ || | || | |_| || _____|| |_| || _ || _ |
// | || | | || |_||_ | _|| |_____ | || | | || |_| |
// | || |_| || __ || |_ |_____ || || |_| || ___|
// | _ || || | | || _ | _____| || _ || || |
// |__| |__||_______||___| |_||___| |_||_______||__| |__||_______||___|
const dbCache = new LRUCache({ max: 25 });
function getDbInstance(dbId) {
let dbInstance = dbCache.get(dbId);
if (!dbInstance) {
dbInstance = betterSqlite3(`dbs/${dbId}.sqlite`);
dbInstance.pragma("journal_mode = WAL");
dbCache.set(dbId, dbInstance);
}
return dbInstance;
}
function getAllRoutes(db) {
return db
.prepare(
`
SELECT routes.*, structures.route_prefix
FROM routes
JOIN structures ON routes.structure_id = structures.id
ORDER BY routes.updated_at DESC;
`
)
.all();
}
const templateCache = new LRUCache({ max: 100 });
function getTemplater(structId) {
let etaInstance = templateCache.get(structId);
if (!etaInstance) {
etaInstance = new Eta({});
etaInstance.resolvePath = function (path, _) {
return path;
};
etaInstance.readFile = function (templateAlias) {
return getTemplateContentByName(db, structId, templateAlias).content;
};
templateCache.set(structId, etaInstance);
}
return etaInstance;
}
function getStructures(db) {
return db.prepare("SELECT * from structures").all();
}
function getStructure(db, id) {
return db.prepare("SELECT * from structures where ID = ?").get(id);
}
function createStructure(db, name, userId) {
const stmt = db.prepare(
"INSERT INTO structures (name, user_id) VALUES (?, ?)"
);
const info = stmt.run(name, userId);
return info.lastInsertRowid; // Returns the structure_id of the newly created structure
}
function createRoute(db, verb, path, structureId, handler) {
path = encodeURI(path)
// add default handler here
const stmt = db.prepare(
"INSERT INTO routes (verb, path, structure_id, handler) VALUES (?, ?, ?, ?)"
);
const info = stmt.run(verb, path, structureId, handler);
return info.lastInsertRowid; // Returns the route_id of the newly created route
}
function getRoutes(db, structureId) {
return db
.prepare("SELECT * from routes where structure_id = ?")
.all(structureId);
}
function getRoute(db, routeId) {
return db.prepare("SELECT * from routes where id = ?").get(routeId);
}
function updateRoute(db, route) {
const fields = [
"verb",
"path",
"structure_id",
"handler",
"updated_at",
"error",
];
const values = fields.map((field) => route[field]);
const placeholders = fields.map((field) => `${field} = ?`).join(", ");
const sql = `UPDATE routes SET ${placeholders} WHERE id = ?`;
values.push(route.id); // Add routeId to the end for the WHERE clause
db.prepare(sql).run(...values);
}
function updateDb(db, appDb) {
const fields = ["name", "library"];
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(db, struct) {
const fields = ["name", "route_prefix", "head_injection"];
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(db, template) {
const fields = ["content", "name", "test_object"];
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(db, structureId) {
return db
.prepare("SELECT * from templates where structure_id = ?")
.all(structureId);
}
function getTemplate(db, templateId) {
return db.prepare("SELECT * from templates where id = ?").get(templateId);
}
function getTemplateContentByName(db, structId, name) {
return db
.prepare(
"SELECT content from templates where structure_id = ? AND name = ?"
)
.get(structId, name);
}
function createTemplate(db, structureId, name, content, testObjectString) {
return db
.prepare(
"INSERT INTO templates (structure_id, name, content, test_object) VALUES (?, ?, ?, ?)"
)
.run(structureId, name, content, testObjectString).lastInsertRowid;
}
function getDbsForStructure(db, structureId) {
return db
.prepare(
`SELECT
*,
CASE
WHEN structure_dbs.structure_id != dbs.structure_id THEN 1
ELSE 0
END AS is_aliased,
structure_dbs.structure_id as alias_struct_id,
dbs.structure_id as db_struct_id
FROM structure_dbs
INNER JOIN dbs ON structure_dbs.db_id = dbs.id
WHERE structure_dbs.structure_id = ?
ORDER BY structure_dbs.created_at, is_aliased ASC;
`
)
.all(structureId);
}
function getDb(db, dbId) {
return db
.prepare(
`SELECT *
FROM dbs
WHERE id = ?;
`
)
.get(dbId);
}
function getDbForStructure(db, structureId, dbId) {
return db
.prepare(
`SELECT *
FROM structure_dbs
INNER JOIN dbs ON structure_dbs.db_id = dbs.id
WHERE structure_dbs.structure_id = ?
AND structure_dbs.db_id = ?;
`
)
.get(structureId, dbId);
}
function createDb(db, structId, name) {
const transaction = db.transaction(() => {
const insertDbStmt = db.prepare(`
INSERT INTO dbs (name, structure_id)
VALUES (?, ?)
`);
const result = insertDbStmt.run(name, structId);
const dbId = result.lastInsertRowid;
const insertStructureDbStmt = db.prepare(`
INSERT INTO structure_dbs (db_id, structure_id, alias)
VALUES (?, ?, ?)
`);
insertStructureDbStmt.run(dbId, structId, name);
const newDbPath = path.join("dbs", `${dbId}.sqlite`);
const newDb = betterSqlite3(newDbPath);
newDb.close();
return dbId;
});
return transaction();
}
function attachDb(db, structId, dbId, alias) {
const insertStructureDbStmt = db.prepare(`
INSERT INTO structure_dbs (db_id, structure_id, alias)
VALUES (?, ?, ?)
`);
insertStructureDbStmt.run(dbId, structId, alias);
}
function getFilesForStruct(db, structureId) {
let test = db
.prepare("SELECT * FROM files WHERE structure_id = ? ORDER BY id DESC")
.all(structureId);
return test;
}
function getFile(db, fileId) {
return db.prepare("SELECT * FROM files WHERE id = ?").get(fileId);
}
function createFile(db, structure_id, name, filePath, mime_type, mime_subtype) {
return db
.prepare(
"INSERT INTO files (structure_id, name, path, mime_type, mime_subtype) VALUES (?, ?, ?, ?, ?)"
)
.run(structure_id, name, filePath, mime_type, mime_subtype).lastInsertRowid;
}
function cloneStructure(
structId,
newStructureName,
userId,
routePrefix = "",
cloneDbs = []
) {
const transaction = db.transaction(() => {
const cloneStructure = db.prepare(`
INSERT INTO structures (name, user_id, route_prefix, cloned_from)
VALUES (?, ?, ?, ?);
`);
const newStructId = cloneStructure.run(
newStructureName,
userId,
routePrefix,
structId
).lastInsertRowid;
const dbIds = db
.prepare(`select db_id from structure_dbs where structure_id = ?;`)
.all(structId);
const toClone = new Set(cloneDbs);
const toAlias = new Set();
for (let { db_id } of dbIds) {
if (!toClone.has(db_id.toString())) {
toAlias.add(db_id.toString());
}
}
for (let db_id of toClone) {
const newDb = db
.prepare(
`
INSERT INTO dbs (name, structure_id, library)
SELECT name, ?, library FROM dbs WHERE id = ?;
`
)
.run(newStructId, db_id).lastInsertRowid;
db.prepare(
`
INSERT INTO structure_dbs (db_id, structure_id, alias)
SELECT ?, ?, alias FROM structure_dbs WHERE db_id = ? AND structure_id = ?;
`
).run(newDb, newStructId, db_id, structId);
db.prepare(`select id from dbs where structure_id = ?`)
.all(newStructId)
.map((new_db) => {
const srcPath = path.join(__dirname, "dbs", `${db_id}.sqlite`);
const destPath = path.join(__dirname, "dbs", `${new_db.id}.sqlite`);
fs.copyFileSync(srcPath, destPath);
fs.copyFileSync(srcPath + "-shm", destPath + "-shm");
fs.copyFileSync(srcPath + "-wal", destPath + "-wal");
});
}
for (let db_id of toAlias) {
db.prepare(
`
INSERT INTO structure_dbs (db_id, structure_id, alias)
SELECT db_id, ?, alias FROM structure_dbs WHERE structure_id = ? AND db_id = ?;
`
).run(newStructId, structId, db_id);
}
const cloneTemplates = db.prepare(`
INSERT INTO templates (name, content, structure_id, test_object, engine)
SELECT name, content, ?, test_object, engine FROM templates WHERE structure_id = ?;
`);
cloneTemplates.run(newStructId, structId);
const cloneRoutes = db.prepare(`
INSERT INTO routes (verb, path, structure_id, handler)
SELECT verb, path, ?, handler FROM routes WHERE structure_id = ?;
`);
cloneRoutes.run(newStructId, structId);
return newStructId;
});
return transaction();
}
function createLog(db, structureId, routeId, content, error = false) {
const stmt = db.prepare(
"INSERT INTO logs (error, structure_id, route_id, content) VALUES (?, ?, ?, ?)"
);
const info = stmt.run(error ? 1 : 0, structureId, routeId, content);
return info.lastInsertRowid; // Returns the log_id of the newly created log
}
function getLogsByRoute(db, routeId) {
return db.prepare("SELECT * FROM logs WHERE route_id = ? ORDER BY created_at DESC LIMIT 50").all(routeId).reverse();
}
function getNewLogsByRoute(db, routeId, since) {
return db.prepare("SELECT * FROM logs WHERE route_id = ? AND id > ? ORDER BY created_at DESC").all(routeId, since);
}
function getMostRecentLogIdByRoute(db, routeId) {
const result = db
.prepare("SELECT id FROM logs WHERE route_id = ? ORDER BY created_at DESC LIMIT 1")
.get(routeId);
return result ? result.id : 0;
}
module.exports = {
db,
applyMigrations,
getDbInstance,
getAllRoutes,
getUser,
createUser,
getStructures,
getStructure,
createStructure,
createRoute,
getRoutes,
getRoute,
updateRoute,
updateDb,
updateStruct,
updateTemplate,
getTemplater,
getTemplates,
getTemplate,
getTemplateContentByName,
createTemplate,
getDbsForStructure,
getDb,
getDbForStructure,
createDb,
attachDb,
getFilesForStruct,
getFile,
createFile,
cloneStructure,
createLog,
getLogsByRoute,
getNewLogsByRoute,
getMostRecentLogIdByRoute
}

586
index.js
View file

@ -1,21 +1,24 @@
const fs = require("fs");
const util = require("util");
const vm = require("node:vm");
const path = require("path");
const express = require("express");
const session = require("express-session");
const fileUpload = require("express-fileupload");
const SQLiteStore = require("better-sqlite3-session-store")(session);
const betterSqlite3 = require("better-sqlite3");
const { Eta } = require("eta");
const { match } = require("path-to-regexp");
const { LRUCache } = require("lru-cache");
const bcrypt = require("bcrypt");
const cheerio = require("cheerio");
const webPush = require('web-push');
const app = express();
const _expressWs = require("express-ws")(app);
const bodyParser = require("body-parser");
const model = require("./db")
const PORT = 3000;
const db = model.db
let viewpath = path.join(__dirname, "views");
let eta = new Eta({ views: viewpath, cache: false, autoEscape: true });
@ -25,55 +28,15 @@ app.use(bodyParser.urlencoded({ extended: true }));
app.use(express.static("public"));
app.use(fileUpload());
const db = betterSqlite3("./dbs/0.sqlite");
const publicVapidKey = 'BCW4o4mLShjgONRK21jwTT44ri49JZfrIeJ-W089TVatopdDh8UNujEptonafxM2ttbUPm4wbZF39GvY3dZWAO0';
const privateVapidKey = 'sXpmx2nRwvVWhGt7uBysZnmFStf9QjaafGd9Ereo4Gw';
db.pragma("journal_mode = WAL");
const dbCache = new LRUCache({ max: 25 });
function getDbInstance(dbId) {
let dbInstance = dbCache.get(dbId);
if (!dbInstance) {
dbInstance = betterSqlite3(`dbs/${dbId}.sqlite`);
dbInstance.pragma("journal_mode = WAL");
dbCache.set(dbId, dbInstance);
}
return dbInstance;
}
const templateCache = new LRUCache({ max: 100 });
function getTemplater(structId) {
let etaInstance = templateCache.get(structId);
if (!etaInstance) {
etaInstance = new Eta({});
etaInstance.resolvePath = function (path, _) {
return path;
};
etaInstance.readFile = function (templateAlias) {
return getTemplateContentByName(db, structId, templateAlias).content;
};
templateCache.set(structId, etaInstance);
}
return etaInstance;
}
function getAllRoutes(db) {
return db
.prepare(
`
SELECT routes.*, structures.route_prefix
FROM routes
JOIN structures ON routes.structure_id = structures.id
ORDER BY routes.updated_at DESC;
`
)
.all();
}
// Configure web-push with your VAPID details
webPush.setVapidDetails(
'mailto:signups@sheepmail.net', // a mailto URL or URL
publicVapidKey,
privateVapidKey
);
app.use(
session({
@ -85,35 +48,7 @@ app.use(
})
);
function applyMigrations() {
db.exec(`
CREATE TABLE IF NOT EXISTS migrations (
id INTEGER PRIMARY KEY AUTOINCREMENT,
filename TEXT NOT NULL,
applied_at DATETIME DEFAULT CURRENT_TIMESTAMP
);
`);
const migrationsDir = path.join(__dirname, "/migrations");
const migrationFiles = fs
.readdirSync(migrationsDir)
.filter((file) => file.endsWith(".sql"));
migrationFiles.forEach((file) => {
const isApplied = db
.prepare("SELECT filename FROM migrations WHERE filename = ?")
.get(file);
if (!isApplied) {
const sql = fs.readFileSync(path.join(migrationsDir, file), "utf-8");
db.exec(sql);
db.prepare("INSERT INTO migrations (filename) VALUES (?)").run(file);
console.log(`Migration applied: ${file}`);
}
});
}
function bootstrapContext(db, structureId, initContext) {
function bootstrapContext(db, structureId, routeId, initContext) {
const allDbInstances = {};
function getDb(alias) {
return allDbInstances[alias];
@ -121,20 +56,36 @@ function bootstrapContext(db, structureId, 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 = getTemplater(structureId);
const eta = model.getTemplater(structureId);
const libs = { eta, db: getDb };
let dbs = getDbsForStructure(db, structureId);
let dbs = model.getDbsForStructure(db, structureId);
let context = vm.createContext({
...initContext,
require: function (str) {
return libs[str];
},
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)
}
}
});
for (let appDb of dbs) {
let dbInstance = getDbInstance(appDb.id);
let dbInstance = model.getDbInstance(appDb.id);
context.sql = dbInstance;
vm.runInContext(appDb.library, context);
allDbInstances[appDb.alias] = {
@ -156,15 +107,15 @@ 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
try {
const context = bootstrapContext(db, route.structure_id, { app });
const context = bootstrapContext(db, route.structure_id, route.id, { app });
let result = vm.runInContext(
route.handler + `\n\napp.ws("${routeWithPrefix(route)}", handler)`,
context
);
updateRoute(db, { ...route, error: null });
model.updateRoute(db, { ...route, error: null });
return result;
} catch (e) {
updateRoute(db, { ...route, error: e.stack });
model.updateRoute(db, { ...route, error: e.stack });
}
}
@ -176,7 +127,7 @@ function buildRoutes() {
DELETE: [],
};
for (let route of getAllRoutes(db)) {
for (let route of model.getAllRoutes(db)) {
const p = routeWithPrefix(route);
if (route.verb == "WS") {
bootstrapWebsocketHandler(route);
@ -191,7 +142,7 @@ function buildRoutes() {
routes = newRoutes;
}
applyMigrations();
model.applyMigrations();
buildRoutes();
// __ __ _______ _______ ______
@ -202,21 +153,12 @@ buildRoutes();
// | | _____| || |___ | | | |
// |_______||_______||_______||___| |_|
function createUser(db, username, hashedPassword) {
return db
.prepare("INSERT INTO users (username, password) VALUES (?, ?)")
.run(username, hashedPassword).lastInsertRowid;
}
function getUser(db, username) {
return db.prepare("SELECT * from users where username = ?").get(username);
}
app.post("/register", async (req, res) => {
try {
const { username, password } = req.body;
const hashedPassword = await bcrypt.hash(password, 10); // 10 is the saltRounds
userId = createUser(db, username, hashedPassword);
userId = model.createUser(db, username, hashedPassword);
req.session.userId = userId;
res.redirect("/workshop");
} catch (e) {
@ -226,7 +168,7 @@ app.post("/register", async (req, res) => {
app.post("/login", async (req, res) => {
const { username, password } = req.body;
const user = getUser(db, username);
const user = model.getUser(db, username);
if (user && (await bcrypt.compare(password, user.password))) {
req.session.userId = user.id;
@ -267,300 +209,7 @@ app.all("/logout", async (req, res) => {
// | || |_| || __ || |_ |_____ || || |_| || ___|
// | _ || || | | || _ | _____| || _ || || |
// |__| |__||_______||___| |_||___| |_||_______||__| |__||_______||___|
function getStructures(db) {
return db.prepare("SELECT * from structures").all();
}
function getStructure(db, id) {
return db.prepare("SELECT * from structures where ID = ?").get(id);
}
function createStructure(db, name, userId) {
const stmt = db.prepare(
"INSERT INTO structures (name, user_id) VALUES (?, ?)"
);
const info = stmt.run(name, userId);
return info.lastInsertRowid; // Returns the structure_id of the newly created structure
}
function createRoute(db, verb, path, structureId, handler) {
path = encodeURI(path)
// add default handler here
const stmt = db.prepare(
"INSERT INTO routes (verb, path, structure_id, handler) VALUES (?, ?, ?, ?)"
);
const info = stmt.run(verb, path, structureId, handler);
return info.lastInsertRowid; // Returns the route_id of the newly created route
}
function getRoutes(db, structureId) {
return db
.prepare("SELECT * from routes where structure_id = ?")
.all(structureId);
}
function getRoute(db, routeId) {
return db.prepare("SELECT * from routes where id = ?").get(routeId);
}
function updateRoute(db, route) {
const fields = [
"verb",
"path",
"structure_id",
"handler",
"updated_at",
"error",
];
const values = fields.map((field) => route[field]);
const placeholders = fields.map((field) => `${field} = ?`).join(", ");
const sql = `UPDATE routes SET ${placeholders} WHERE id = ?`;
values.push(route.id); // Add routeId to the end for the WHERE clause
db.prepare(sql).run(...values);
}
function updateDb(db, appDb) {
const fields = ["name", "library"];
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(db, struct) {
const fields = ["name", "route_prefix", "head_injection"];
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(db, template) {
const fields = ["content", "name", "test_object"];
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(db, structureId) {
return db
.prepare("SELECT * from templates where structure_id = ?")
.all(structureId);
}
function getTemplate(db, templateId) {
return db.prepare("SELECT * from templates where id = ?").get(templateId);
}
function getTemplateContentByName(db, structId, name) {
return db
.prepare(
"SELECT content from templates where structure_id = ? AND name = ?"
)
.get(structId, name);
}
function createTemplate(db, structureId, name, content, testObjectString) {
return db
.prepare(
"INSERT INTO templates (structure_id, name, content, test_object) VALUES (?, ?, ?, ?)"
)
.run(structureId, name, content, testObjectString).lastInsertRowid;
}
function getDbsForStructure(db, structureId) {
return db
.prepare(
`SELECT
*,
CASE
WHEN structure_dbs.structure_id != dbs.structure_id THEN 1
ELSE 0
END AS is_aliased,
structure_dbs.structure_id as alias_struct_id,
dbs.structure_id as db_struct_id
FROM structure_dbs
INNER JOIN dbs ON structure_dbs.db_id = dbs.id
WHERE structure_dbs.structure_id = ?
ORDER BY structure_dbs.created_at, is_aliased ASC;
`
)
.all(structureId);
}
function getDb(db, dbId) {
return db
.prepare(
`SELECT *
FROM dbs
WHERE id = ?;
`
)
.get(dbId);
}
function getDbForStructure(db, structureId, dbId) {
return db
.prepare(
`SELECT *
FROM structure_dbs
INNER JOIN dbs ON structure_dbs.db_id = dbs.id
WHERE structure_dbs.structure_id = ?
AND structure_dbs.db_id = ?;
`
)
.get(structureId, dbId);
}
function createDb(db, structId, name) {
const transaction = db.transaction(() => {
const insertDbStmt = db.prepare(`
INSERT INTO dbs (name, structure_id)
VALUES (?, ?)
`);
const result = insertDbStmt.run(name, structId);
const dbId = result.lastInsertRowid;
const insertStructureDbStmt = db.prepare(`
INSERT INTO structure_dbs (db_id, structure_id, alias)
VALUES (?, ?, ?)
`);
insertStructureDbStmt.run(dbId, structId, name);
const newDbPath = path.join("dbs", `${dbId}.sqlite`);
const newDb = betterSqlite3(newDbPath);
newDb.close();
return dbId;
});
return transaction();
}
function attachDb(db, structId, dbId, alias) {
const insertStructureDbStmt = db.prepare(`
INSERT INTO structure_dbs (db_id, structure_id, alias)
VALUES (?, ?, ?)
`);
insertStructureDbStmt.run(dbId, structId, alias);
}
function getFilesForStruct(db, structureId) {
let test = db
.prepare("SELECT * FROM files WHERE structure_id = ? ORDER BY id DESC")
.all(structureId);
return test;
}
function getFile(db, fileId) {
return db.prepare("SELECT * FROM files WHERE id = ?").get(fileId);
}
function createFile(db, structure_id, name, filePath, mime_type, mime_subtype) {
return db
.prepare(
"INSERT INTO files (structure_id, name, path, mime_type, mime_subtype) VALUES (?, ?, ?, ?, ?)"
)
.run(structure_id, name, filePath, mime_type, mime_subtype).lastInsertRowid;
}
function cloneStructure(
structId,
newStructureName,
userId,
routePrefix = "",
cloneDbs = []
) {
const transaction = db.transaction(() => {
const cloneStructure = db.prepare(`
INSERT INTO structures (name, user_id, route_prefix, cloned_from)
VALUES (?, ?, ?, ?);
`);
const newStructId = cloneStructure.run(
newStructureName,
userId,
routePrefix,
structId
).lastInsertRowid;
const dbIds = db
.prepare(`select db_id from structure_dbs where structure_id = ?;`)
.all(structId);
const toClone = new Set(cloneDbs);
const toAlias = new Set();
for (let { db_id } of dbIds) {
if (!toClone.has(db_id.toString())) {
toAlias.add(db_id.toString());
}
}
for (let db_id of toClone) {
const newDb = db
.prepare(
`
INSERT INTO dbs (name, structure_id, library)
SELECT name, ?, library FROM dbs WHERE id = ?;
`
)
.run(newStructId, db_id).lastInsertRowid;
db.prepare(
`
INSERT INTO structure_dbs (db_id, structure_id, alias)
SELECT ?, ?, alias FROM structure_dbs WHERE db_id = ? AND structure_id = ?;
`
).run(newDb, newStructId, db_id, structId);
db.prepare(`select id from dbs where structure_id = ?`)
.all(newStructId)
.map((new_db) => {
const srcPath = path.join(__dirname, "dbs", `${db_id}.sqlite`);
const destPath = path.join(__dirname, "dbs", `${new_db.id}.sqlite`);
fs.copyFileSync(srcPath, destPath);
fs.copyFileSync(srcPath + "-shm", destPath + "-shm");
fs.copyFileSync(srcPath + "-wal", destPath + "-wal");
});
}
for (let db_id of toAlias) {
db.prepare(
`
INSERT INTO structure_dbs (db_id, structure_id, alias)
SELECT db_id, ?, alias FROM structure_dbs WHERE structure_id = ? AND db_id = ?;
`
).run(newStructId, structId, db_id);
}
const cloneTemplates = db.prepare(`
INSERT INTO templates (name, content, structure_id, test_object, engine)
SELECT name, content, ?, test_object, engine FROM templates WHERE structure_id = ?;
`);
cloneTemplates.run(newStructId, structId);
const cloneRoutes = db.prepare(`
INSERT INTO routes (verb, path, structure_id, handler)
SELECT verb, path, ?, handler FROM routes WHERE structure_id = ?;
`);
cloneRoutes.run(newStructId, structId);
return newStructId;
});
return transaction();
}
//
function bootstrapTemplateWithHTMXetc(
htmlString,
blissRoute,
@ -590,6 +239,14 @@ function bootstrapTemplateWithHTMXetc(
<script src="/js/htmx.js"></script>
<script src="https://unpkg.com/htmx.org@1.9.12/dist/ext/ws.js"></script>
<script src="/js/bliss_inspector.js"></script>
<script>
tailwind.config = {
corePlugins: {
preflight: false
}
}
</script>
<style> body { margin: 0; }</style>
${headInjection || ""}
`);
@ -616,7 +273,7 @@ function bootstrapTemplateWithHTMXetc(
}
app.post("/workshop", (req, res) => {
let structId = createStructure(db, req.body.name);
let structId = model.createStructure(db, req.body.name);
return res.redirect("/workshop/" + structId);
});
@ -624,7 +281,7 @@ app.get("/workshop", (req, res) => {
return res.send(
bootstrapTemplateWithHTMXetc(
eta.render("workshop/index", {
structures: getStructures(db),
structures: model.getStructures(db),
})
)
);
@ -641,10 +298,10 @@ function smartRedirect(req, res, redirectUrl) {
function sidebarStuff(db, structId) {
return {
structure: getStructure(db, structId),
routes: getRoutes(db, structId),
templates: getTemplates(db, structId),
dbs: getDbsForStructure(db, structId),
structure: model.getStructure(db, structId),
routes: model.getRoutes(db, structId),
templates: model.getTemplates(db, structId),
dbs: model.getDbsForStructure(db, structId),
};
}
@ -661,7 +318,7 @@ app.post("/workshop/:structure_id/clone", (req, res) => {
routePrefix = routePrefix[0] == "/" ? routePrefix : "/" + routePrefix;
let newStructureId;
try {
newStructureId = cloneStructure(
newStructureId = model.cloneStructure(
req.params.structure_id,
req.body.name,
1,
@ -677,19 +334,19 @@ app.post("/workshop/:structure_id/clone", (req, res) => {
});
app.post("/workshop/:structure_id/db", (req, res) => {
const dbId = createDb(db, req.params.structure_id, req.body.name);
const dbId = model.createDb(db, 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) => {
attachDb(db, req.params.structure_id, req.body.db_id, req.body.alias);
model.attachDb(db, 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 = getDbForStructure(
const structdb = model.getDbForStructure(
db,
req.params.structure_id,
req.params.db_id
@ -723,7 +380,7 @@ app.post("/workshop/:structure_id/route", (req, res) => {
if (p[p.length - 1] == "/") {
p = p.substring(0, p.length - 1);
}
const route = createRoute(
const route = model.createRoute(
db,
req.body.verb,
p[0] == "/" ? p.substring(1) : "/" + p,
@ -740,19 +397,19 @@ app.post("/workshop/:structure_id/route", (req, res) => {
});
app.put("/workshop/:structure_id/route/:route_id", (req, res) => {
const route = getRoute(db, req.params.route_id);
updateRoute(db, { ...route, ...req.body });
const route = model.getRoute(db, req.params.route_id);
model.updateRoute(db, { ...route, ...req.body });
return res.send("good");
});
app.put("/workshop/:structure_id/db/:db_id/library", (req, res) => {
try {
let dbId = req.params.db_id;
let appDb = getDb(db, dbId);
updateDb(db, { ...appDb, library: req.body.library });
appDb = getDb(db, dbId);
let appDb = model.getDb(db, dbId);
model.updateDb(db, { ...appDb, library: req.body.library });
appDb = model.getDb(db, dbId);
let dbInstance = getDbInstance(dbId);
let dbInstance = model.getDbInstance(dbId);
let capturedOutput = [];
let context = vm.createContext({
module: { exports: null },
@ -762,7 +419,14 @@ app.put("/workshop/:structure_id/db/:db_id/library", (req, res) => {
},
});
let evaledCode = vm.runInContext(appDb.library, context);
let stdout = capturedOutput.join("\n");
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");
return res.send(`${stdout}\n\n> ${evaledCode}`.trim());
} catch (e) {
return res.send(`${e}\n\n${e.stack}`);
@ -772,21 +436,30 @@ 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 = getDb(db, dbId);
let dbInstance = getDbInstance(dbId);
let appDb = model.getDb(db, dbId);
let dbInstance = model.getDbInstance(dbId);
let capturedOutput = [];
let context = vm.createContext({
module: { exports: null },
sql: dbInstance,
console: {
log: (...args) => capturedOutput.push(args.join(" ")),
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(" ")),
},
});
const libraryScript = new vm.Script(appDb.library);
libraryScript.runInContext(context);
context.library = context.module.exports;
const replScript = new vm.Script(req.body.code);
let evaledCode = replScript.runInContext(context);
let evaledCode = JSON.stringify(replScript.runInContext(context));
let stdout = capturedOutput.join("\n");
return res.send(`${stdout}\n\n> ${evaledCode}`.trim());
} catch (e) {
@ -797,7 +470,7 @@ app.post("/workshop/:structure_id/db/:db_id/repl", (req, res) => {
app.post("/workshop/:structure_id/template", (req, res) => {
let name = req.body.name;
const template = createTemplate(
const template = model.createTemplate(
db,
req.params.structure_id,
name,
@ -817,13 +490,17 @@ app.put("/workshop/:structure_id/template/:template_id", (req, res) => {
let content = req.body.content;
let test_object = req.body.test_object;
updateTemplate(db, { ...getTemplate(db, id), content, test_object });
model.updateTemplate(db, { ...model.updateTemplate(db, id), content, test_object });
return res.send("good");
});
app.get("/workshop/:structure_id/template/:template_id", (req, res) => {
const template = getTemplate(db, req.params.template_id);
const template = model.getTemplate(db, req.params.template_id);
console.log("wassup", {
template: template,
...sidebarStuff(db, req.params.structure_id),})
return res.send(
bootstrapTemplateWithHTMXetc(
@ -836,9 +513,10 @@ app.get("/workshop/:structure_id/template/:template_id", (req, res) => {
});
app.get("/workshop/:structure_id/template/:template_id/preview", (req, res) => {
const template = getTemplate(db, req.params.template_id);
const struct = getStructure(db, req.params.structure_id);
const eta = getTemplater(req.params.structure_id);
const template = model.getTemplate(db, req.params.template_id);
const struct = model.getStructure(db, req.params.structure_id);
const eta = model.getTemplater(req.params.structure_id);
console.log(template)
const context = vm.createContext({ it: null });
vm.runInContext(template.test_object, context);
@ -860,19 +538,12 @@ app.get("/workshop/:structure_id/template/:template_id/preview", (req, res) => {
});
app.get("/workshop/:structure_id/route/:id", (req, res) => {
const route = getRoute(db, req.params.id);
let routePrefix = getStructure(db, req.params.structure_id).route_prefix;
const route = model.getRoute(db, req.params.id);
let routePrefix = model.getStructure(db, req.params.structure_id).route_prefix;
let previewUrl = routePrefix
? path.join(routePrefix || "", route.path)
: route.path;
console.log(previewUrl, "baba");
// let previewUrl = prefixUrlWithHost(
// req,
// routePrefix ? path.join(routePrefix.substring(1), route.path) : route.path
// );
console.log(previewUrl);
return res.send(
bootstrapTemplateWithHTMXetc(
eta.render("workshop/route", {
@ -885,6 +556,31 @@ app.get("/workshop/:structure_id/route/:id", (req, res) => {
);
});
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)
}
else {
logs = model.getLogsByRoute(db, req.params.route_id)
}
const lastId = model.getMostRecentLogIdByRoute(db, req.params.route_id)
return res.send(
bootstrapTemplateWithHTMXetc(
eta.render("workshop/logs", {
logs: logs.reverse(),
structId: req.params.structure_id,
routeId: req.params.route_id,
since: lastId,
})
)
);
});
// POST route to handle file upload
app.post("/workshop/:structure_id/files", (req, res) => {
if (!req.files || Object.keys(req.files).length === 0) {
@ -906,7 +602,7 @@ app.post("/workshop/:structure_id/files", (req, res) => {
return res.status(500).send(err);
}
let id = createFile(
let id = model.createFile(
db,
structure_id,
name,
@ -915,7 +611,7 @@ app.post("/workshop/:structure_id/files", (req, res) => {
mime_subtype
);
let file = getFile(db, id);
let file = model.getFile(db, id);
file.url = prefixUrlWithHost(req, file.path);
return res.send(eta.render("workshop/file_detail", { file: file }));
@ -934,7 +630,7 @@ function prefixUrlWithHost(req, path) {
app.get("/workshop/:structure_id/files", (req, res) => {
const { structure_id } = req.params;
const files = getFilesForStruct(db, structure_id);
const files = model.getFilesForStruct(db, structure_id);
files.map((it) => {
it.url = prefixUrlWithHost(req, it.path);
});
@ -961,7 +657,7 @@ app.get("/workshop/:structure_id/settings", (req, res) => {
app.put("/workshop/:structure_id/settings", (req, res) => {
let structId = req.params.structure_id;
let struct = getStructure(db, structId);
let struct = model.getStructure(db, structId);
let routePrefix = req.body.route_prefix;
if (routePrefix[0] != "/") {
@ -971,12 +667,12 @@ app.put("/workshop/:structure_id/settings", (req, res) => {
routePrefix = routePrefix.substring(0, routePrefix.length - 1);
}
updateStruct(db, {
model.updateStruct(db, {
...struct,
route_prefix: routePrefix,
head_injection: req.body.head_injection,
});
struct = getStructure(db, structId);
struct = model.getStructure(db, structId);
res.send("success!");
});
@ -985,7 +681,7 @@ app.get("/workshop/:structure_id/new_template_modal", (req, res) => {
return res.send(
bootstrapTemplateWithHTMXetc(
eta.render("workshop/new_template_modal", {
structure: getStructure(db, req.params.structure_id),
structure: model.getStructure(db, req.params.structure_id),
})
)
);
@ -995,7 +691,7 @@ app.get("/workshop/:structure_id/new_route_modal", (req, res) => {
return res.send(
bootstrapTemplateWithHTMXetc(
eta.render("workshop/new_route_modal", {
structure: getStructure(db, req.params.structure_id),
structure: model.getStructure(db, req.params.structure_id),
})
)
);
@ -1005,15 +701,15 @@ app.get("/workshop/:structure_id/new_db_modal", (req, res) => {
return res.send(
bootstrapTemplateWithHTMXetc(
eta.render("workshop/new_db_modal", {
structure: getStructure(db, req.params.structure_id),
structure: model.getStructure(db, req.params.structure_id),
})
)
);
});
app.get("/workshop/:structure_id/clone_modal", (req, res) => {
const structure = getStructure(db, req.params.structure_id);
const dbs = getDbsForStructure(db, req.params.structure_id);
const structure = model.getStructure(db, req.params.structure_id);
const dbs = model.getDbsForStructure(db, req.params.structure_id);
return res.send(
bootstrapTemplateWithHTMXetc(
@ -1050,13 +746,13 @@ app.all("*", (req, res) => {
.prepare("SELECT * FROM routes WHERE id = ?")
.get(routeMatch.id);
const structure = getStructure(db, route.structure_id);
const structure = model.getStructure(db, 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, {
let context = bootstrapContext(db, route.structure_id, route.id, {
req,
res,
eta,
@ -1067,7 +763,7 @@ app.all("*", (req, res) => {
context.route = function (url) {
return __urlPrefix ? path.join(__urlPrefix, url) : url;
};
const eta = getTemplater(route.structure_id);
const eta = model.getTemplater(route.structure_id);
res.send(
bootstrapTemplateWithHTMXetc(
eta.render(template, context),
@ -1084,7 +780,9 @@ app.all("*", (req, res) => {
context
);
} catch (e) {
return res.send(`${e}\n\n${e.stack}`);
const error = e.stack ? `${e}\n\n${e.stack}` : `${e}`;
model.createLog(db, structure.id, route.id, error, true);
return res.send(error);
}
} else {
res.status(404).json({ success: false, message: "Path not found" });

View file

@ -0,0 +1,18 @@
CREATE TABLE logs (
id INTEGER PRIMARY KEY AUTOINCREMENT,
error BOOLEAN NOT NULL,
structure_id INTEGER NOT NULL,
route_id INTEGER,
content TEXT NOT NULL,
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
FOREIGN KEY(structure_id) REFERENCES structures(id),
FOREIGN KEY(route_id) REFERENCES routes(id)
);
-- Trigger to update `created_at` on insert
CREATE TRIGGER update_logs_created_at
AFTER INSERT ON logs
FOR EACH ROW
BEGIN
UPDATE logs SET created_at = CURRENT_TIMESTAMP WHERE id = NEW.id;
END;

24
package-lock.json generated
View file

@ -25,6 +25,9 @@
"lru-cache": "^10.2.0",
"nunjucks": "^3.2.4",
"path-to-regexp": "^6.2.2"
},
"devDependencies": {
"prettier": "3.3.3"
}
},
"node_modules/@mapbox/node-pre-gyp": {
@ -1917,6 +1920,21 @@
"node": ">=10"
}
},
"node_modules/prettier": {
"version": "3.3.3",
"resolved": "https://registry.npmjs.org/prettier/-/prettier-3.3.3.tgz",
"integrity": "sha512-i2tDNA0O5IrMO757lfrdQZCc2jPNDVntV0m/+4whiDfWaTKfMNgR7Qz0NAeGz/nRqF4m5/6CLzbP4/liHt12Ew==",
"dev": true,
"bin": {
"prettier": "bin/prettier.cjs"
},
"engines": {
"node": ">=14"
},
"funding": {
"url": "https://github.com/prettier/prettier?sponsor=1"
}
},
"node_modules/proxy-addr": {
"version": "2.0.7",
"resolved": "https://registry.npmjs.org/proxy-addr/-/proxy-addr-2.0.7.tgz",
@ -3941,6 +3959,12 @@
"tunnel-agent": "^0.6.0"
}
},
"prettier": {
"version": "3.3.3",
"resolved": "https://registry.npmjs.org/prettier/-/prettier-3.3.3.tgz",
"integrity": "sha512-i2tDNA0O5IrMO757lfrdQZCc2jPNDVntV0m/+4whiDfWaTKfMNgR7Qz0NAeGz/nRqF4m5/6CLzbP4/liHt12Ew==",
"dev": true
},
"proxy-addr": {
"version": "2.0.7",
"resolved": "https://registry.npmjs.org/proxy-addr/-/proxy-addr-2.0.7.tgz",

View file

@ -25,5 +25,8 @@
"lru-cache": "^10.2.0",
"nunjucks": "^3.2.4",
"path-to-regexp": "^6.2.2"
},
"devDependencies": {
"prettier": "3.3.3"
}
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.5 MiB

File diff suppressed because one or more lines are too long

View file

@ -17,12 +17,12 @@
</div>
<div id="main-editor" class="flex flex-grow">
<div id="left-pane" class="w-1/2 flex-grow h-full flex flex-col">
<h1>LIBRARY</h1>
<h1 class="text-lg">LIBRARY</h1>
<div id="library-editor" class="w-full flex-grow"><%= it.db.library %></div>
<button id="save-library" type="submit">Save</button>
</div>
<div id="preview-area" class="w-1/2 h-full flex flex-col">
<h1>REPL</h1>
<h1 class="text-lg">REPL</h1>
<div id="repl-editor" class="w-full h-1/2">// evaluate code in here
</div>
<button id="send-repl">RUN</button>
@ -76,4 +76,4 @@
</body>
</html>
</html>

View file

@ -16,7 +16,7 @@
<%~ include('/workshop/sidebar', it) %>
</div>
<div id="main-editor" class="flex flex-grow flex-col">
<h1>Files</h1>
<h1 class="text-lg">Files</h1>
<div>
<form hx-encoding='multipart/form-data' hx-post='/workshop/<%= it.structure.id %>/files' hx-target="#files-list" hx-swap="afterbegin" _='on htmx:xhr:progress(loaded, total) set #progress.value to (loaded/total)*100'>
<input type='file' name='file'>
@ -39,4 +39,4 @@
</body>
</html>
</html>

6
views/workshop/logs.eta Normal file
View file

@ -0,0 +1,6 @@
<div hx-swap="outerHTML" hx-get="/workshop/<%= it.structId %>/route/<%= it.routeId %>/logs?since=<%= it.since || 0 %>" hx-trigger="every 2s" class="hidden"></div>
<% it.logs.forEach((log) => { %>
<div class="<% if (log.error) { %>bg-red-800 text-white <% } %>">
<%= log.content %>
</div>
<% }) %>

View file

@ -18,7 +18,13 @@
<div id="main-editor" class="flex flex-grow">
<div id="left-pane" class="w-1/2 flex-grow h-full flex flex-col">
<div id="editor" class="w-full flex-grow"><%= it.route.handler %></div>
<div class="flex">
<button id="save-btn" type="submit">Save</button>
<button id="save-btn" type="submit" _="on click toggle .hidden on #logs">Logs</button>
</div>
<pre id="logs" class="max-h-[50vh] overflow-scroll hidden">
<ul hx-get="/workshop/<%= it.structure.id %>/route/<%= it.route.id %>/logs" hx-trigger="load" class="flex flex-col-reverse"></ul>
</pre>
</div>
<div id="preview-area" class="w-1/2 h-full flex flex-col">
<form _="on submit halt the event then set #preview's contentWindow.location.href to #url's value">
@ -38,24 +44,23 @@
editor.setTheme("ace/theme/monokai");
editor.session.setMode("ace/mode/javascript");
document.getElementById('save-btn').onclick = function() {
fetch('/workshop/<%= it.structure.id %>/route/<%= it.route.id %>', {
method: 'PUT',
headers: {
'Content-Type': 'application/x-www-form-urlencoded'
},
body: new URLSearchParams({
handler: editor.getValue()
}).toString()
}).then(response => {
if (response.ok) {
document.getElementById('preview').contentWindow.location.reload()
}
});
};
document.getElementById('save-btn').onclick = function() {
fetch('/workshop/<%= it.structure.id %>/route/<%= it.route.id %>', {
method: 'PUT',
headers: {
'Content-Type': 'application/x-www-form-urlencoded'
},
body: new URLSearchParams({
handler: editor.getValue()
}).toString()
}).then(response => {
if (response.ok) {
document.getElementById('preview').contentWindow.location.reload()
}
});
};
</script>
</body>
</html>
</html>