805 lines
24 KiB
JavaScript
805 lines
24 KiB
JavaScript
const fs = require("fs");
|
|
const path = require("path");
|
|
const betterSqlite3 = require("better-sqlite3");
|
|
const { LRUCache } = require("lru-cache");
|
|
const { randomUUID } = require("node:crypto");
|
|
const { Eta } = require("eta");
|
|
const cheerio = require("cheerio");
|
|
|
|
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(username, hashedPassword) {
|
|
return db
|
|
.prepare("INSERT INTO users (username, password) VALUES (?, ?)")
|
|
.run(username, hashedPassword).lastInsertRowid;
|
|
}
|
|
|
|
function getUser(username) {
|
|
return db.prepare("SELECT * from users where username = ?").get(username);
|
|
}
|
|
|
|
function getUserById(id) {
|
|
return db.prepare("SELECT id, username FROM users WHERE id = ?").get(id);
|
|
}
|
|
|
|
// _ _ _______ ______ ___ _ _______ __ __ _______ _______
|
|
// | | _ | || || _ | | | | || || | | || || |
|
|
// | || || || _ || | || | |_| || _____|| |_| || _ || _ |
|
|
// | || | | || |_||_ | _|| |_____ | || | | || |_| |
|
|
// | || |_| || __ || |_ |_____ || || |_| || ___|
|
|
// | _ || || | | || _ | _____| || _ || || |
|
|
// |__| |__||_______||___| |_||___| |_||_______||__| |__||_______||___|
|
|
|
|
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() {
|
|
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 });
|
|
const templateContextCache = new LRUCache({ max: 2000, ttl: 30 * 60 * 1000 });
|
|
|
|
// The inspector needs the values that produced each rendered template, not a
|
|
// fresh request's values. Contexts frequently contain helpers (such as
|
|
// `route`) or cyclic request objects, neither of which belongs in HTML. Keep
|
|
// the JSON-shaped portion and omit the rest rather than making a page fail to
|
|
// render just because it cannot be inspected.
|
|
function inspectableContext(data) {
|
|
const seen = new WeakSet();
|
|
try {
|
|
return JSON.parse(
|
|
JSON.stringify(data ?? {}, (_key, value) => {
|
|
if (typeof value === "function" || typeof value === "undefined") {
|
|
return undefined;
|
|
}
|
|
if (typeof value === "object" && value !== null) {
|
|
if (seen.has(value)) return undefined;
|
|
seen.add(value);
|
|
}
|
|
return value;
|
|
}),
|
|
);
|
|
} catch (_) {
|
|
return {};
|
|
}
|
|
}
|
|
|
|
function annotateTemplateHtml(html, template, data) {
|
|
const context = inspectableContext(data);
|
|
const contextId = randomUUID();
|
|
templateContextCache.set(contextId, { templateId: String(template.id), context });
|
|
function annotate($, element) {
|
|
const current = $(element).attr("data-bliss-templates");
|
|
let templates = [];
|
|
if (current) {
|
|
try {
|
|
templates = JSON.parse(current);
|
|
} catch (_) {}
|
|
}
|
|
if (!templates.some((item) => String(item.id) === String(template.id))) {
|
|
templates.push({ id: String(template.id), name: template.name });
|
|
}
|
|
$(element).attr("data-bliss-templates", JSON.stringify(templates));
|
|
const rawContextIds = $(element).attr("data-bliss-template-context-ids");
|
|
let contextIds = {};
|
|
if (rawContextIds) {
|
|
try {
|
|
contextIds = JSON.parse(rawContextIds);
|
|
} catch (_) {}
|
|
}
|
|
contextIds[String(template.id)] = contextId;
|
|
$(element).attr("data-bliss-template-context-ids", JSON.stringify(contextIds));
|
|
if (!$(element).attr("data-bliss-template-id")) {
|
|
$(element).attr({
|
|
"data-bliss-template-id": String(template.id),
|
|
"data-bliss-template-name": template.name,
|
|
"data-bliss-template-context-id": contextId,
|
|
});
|
|
}
|
|
}
|
|
const lower = html.trimStart().toLowerCase();
|
|
if (lower.startsWith("<html") || lower.startsWith("<!doctype")) {
|
|
const $ = cheerio.load(html);
|
|
annotate($, $("body")[0]);
|
|
return $.html();
|
|
}
|
|
const $ = cheerio.load(html, null, false);
|
|
for (const child of $.root().children()) annotate($, child);
|
|
return $.html();
|
|
}
|
|
|
|
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(structId, templateAlias).content;
|
|
};
|
|
const etaRender = etaInstance.render;
|
|
etaInstance.render = function (templateName, data, meta) {
|
|
const html = etaRender.call(this, templateName, data, meta);
|
|
if (typeof templateName !== "string") return html;
|
|
const template = getTemplateByName(structId, templateName);
|
|
return template ? annotateTemplateHtml(html, template, data) : html;
|
|
};
|
|
templateCache.set(structId, etaInstance);
|
|
}
|
|
|
|
return etaInstance;
|
|
}
|
|
|
|
function getStructures() {
|
|
return db.prepare("SELECT * from structures").all();
|
|
}
|
|
|
|
function getStructure(id) {
|
|
return db.prepare("SELECT * from structures where ID = ?").get(id);
|
|
}
|
|
|
|
function createStructure(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(verb, path, structureId, handler) {
|
|
path = encodeURI(path);
|
|
|
|
const stmt = db.prepare(
|
|
"INSERT INTO routes (verb, path, structure_id, handler) VALUES (?, ?, ?, ?)"
|
|
);
|
|
const info = stmt.run(verb, path, structureId, handler);
|
|
const routeId = info.lastInsertRowid; // Get the newly created route ID
|
|
|
|
if (verb !== 'GET') {
|
|
createScaffoldPage(routeId);
|
|
}
|
|
|
|
recordVersion("route", routeId, structureId, { verb, path, handler });
|
|
|
|
return routeId; // Returns the route_id of the newly created route
|
|
}
|
|
|
|
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
|
|
`;
|
|
|
|
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(routeId) {
|
|
return db
|
|
.prepare(`${ROUTE_SELECT} WHERE r.id = ? ORDER BY sp.created_at DESC, sp2.created_at DESC LIMIT 1`)
|
|
.get(routeId);
|
|
}
|
|
|
|
// Update `fields` of a row by id from a plain object. Returns the run info.
|
|
function update(table, fields, obj) {
|
|
const placeholders = fields.map((field) => `${field} = ?`).join(", ");
|
|
const values = fields.map((field) => obj[field]);
|
|
values.push(obj.id);
|
|
return db.prepare(`UPDATE ${table} SET ${placeholders} WHERE id = ?`).run(...values);
|
|
}
|
|
|
|
// ---- version history (append-only) --------------------------------------
|
|
//
|
|
// Snapshot the code-bearing fields of a route/template/db every time they
|
|
// change, so nothing a user (or the plumber) edits is ever truly lost. Deduped
|
|
// against the latest snapshot for that entity, so no-op saves, error-flag-only
|
|
// route writes, and server-restart WS re-bootstraps don't pile up noise.
|
|
// snapshotFor() defines exactly which fields are versioned per type; keep the
|
|
// key order stable so the dedup string compare is reliable.
|
|
function snapshotFor(entityType, obj) {
|
|
switch (entityType) {
|
|
case "route":
|
|
return { verb: obj.verb, path: obj.path, handler: obj.handler };
|
|
case "template":
|
|
return { name: obj.name, content: obj.content, test_object: obj.test_object };
|
|
case "db":
|
|
return { name: obj.name, library: obj.library };
|
|
default:
|
|
throw new Error(`unknown version entity type: ${entityType}`);
|
|
}
|
|
}
|
|
|
|
function recordVersion(entityType, entityId, structureId, obj) {
|
|
const json = JSON.stringify(snapshotFor(entityType, obj));
|
|
const last = db
|
|
.prepare(
|
|
"SELECT snapshot FROM versions WHERE entity_type = ? AND entity_id = ? ORDER BY id DESC LIMIT 1",
|
|
)
|
|
.get(entityType, entityId);
|
|
if (last && last.snapshot === json) return; // content unchanged — skip
|
|
db.prepare(
|
|
"INSERT INTO versions (entity_type, entity_id, structure_id, snapshot) VALUES (?, ?, ?, ?)",
|
|
).run(entityType, entityId, structureId, json);
|
|
}
|
|
|
|
// Version list for one entity (newest first). Omits the snapshot body to stay
|
|
// scannable; fetch a single version to get its full content.
|
|
function getVersions(entityType, entityId) {
|
|
return db
|
|
.prepare(
|
|
`SELECT id, entity_type, entity_id, structure_id, created_at, length(snapshot) AS bytes
|
|
FROM versions WHERE entity_type = ? AND entity_id = ? ORDER BY id DESC`,
|
|
)
|
|
.all(entityType, entityId);
|
|
}
|
|
|
|
// One version with its full snapshot parsed back into fields.
|
|
function getVersion(versionId) {
|
|
const row = db.prepare("SELECT * FROM versions WHERE id = ?").get(versionId);
|
|
if (!row) return null;
|
|
return { ...row, snapshot: JSON.parse(row.snapshot) };
|
|
}
|
|
|
|
function updateRoute(route) {
|
|
update("routes", ["verb", "path", "structure_id", "handler", "updated_at", "error"], route);
|
|
recordVersion("route", route.id, route.structure_id, route);
|
|
}
|
|
|
|
function updateDb(appDb) {
|
|
update("dbs", ["name", "library"], appDb);
|
|
recordVersion("db", appDb.id, appDb.structure_id, appDb);
|
|
}
|
|
|
|
function updateStruct(struct) {
|
|
update("structures", ["name", "route_prefix", "head_injection"], struct);
|
|
}
|
|
|
|
function updateTemplate(template) {
|
|
update("templates", ["content", "name", "test_object"], template);
|
|
templateCache.delete(template.structure_id);
|
|
recordVersion("template", template.id, template.structure_id, template);
|
|
}
|
|
|
|
function getTemplates(structureId) {
|
|
return db
|
|
.prepare("SELECT * from templates where structure_id = ?")
|
|
.all(structureId);
|
|
}
|
|
|
|
function getTemplate(templateId) {
|
|
return db.prepare("SELECT * from templates where id = ?").get(templateId);
|
|
}
|
|
|
|
function getInspectableTemplateContext(contextId, templateId) {
|
|
const entry = templateContextCache.get(contextId);
|
|
if (!entry || entry.templateId !== String(templateId)) return null;
|
|
return entry.context;
|
|
}
|
|
|
|
function getTemplateContentByName(structId, name) {
|
|
return db
|
|
.prepare(
|
|
"SELECT content from templates where structure_id = ? AND name = ?",
|
|
)
|
|
.get(structId, name);
|
|
}
|
|
|
|
function getTemplateByName(structId, name) {
|
|
return db
|
|
.prepare("SELECT * from templates where structure_id = ? AND name = ?")
|
|
.get(structId, name);
|
|
}
|
|
|
|
function createTemplate(structureId, name, content, testObjectString) {
|
|
const id = db
|
|
.prepare(
|
|
"INSERT INTO templates (structure_id, name, content, test_object) VALUES (?, ?, ?, ?)",
|
|
)
|
|
.run(structureId, name, content, testObjectString).lastInsertRowid;
|
|
recordVersion("template", id, structureId, {
|
|
name,
|
|
content,
|
|
test_object: testObjectString,
|
|
});
|
|
return id;
|
|
}
|
|
|
|
function getDbsForStructure(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(dbId) {
|
|
return db
|
|
.prepare(
|
|
`SELECT *
|
|
FROM dbs
|
|
WHERE id = ?;
|
|
`,
|
|
)
|
|
.get(dbId);
|
|
}
|
|
|
|
function getDbForStructure(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(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;
|
|
});
|
|
|
|
const dbId = transaction();
|
|
recordVersion("db", dbId, structId, { name, library: "" });
|
|
return dbId;
|
|
}
|
|
|
|
function attachDb(structId, dbId, alias) {
|
|
const insertStructureDbStmt = db.prepare(`
|
|
INSERT INTO structure_dbs (db_id, structure_id, alias)
|
|
VALUES (?, ?, ?)
|
|
`);
|
|
insertStructureDbStmt.run(dbId, structId, alias);
|
|
}
|
|
|
|
function getFilesForStruct(structureId) {
|
|
let test = db
|
|
.prepare("SELECT * FROM files WHERE structure_id = ? ORDER BY id DESC")
|
|
.all(structureId);
|
|
|
|
return test;
|
|
}
|
|
|
|
function getFile(fileId) {
|
|
return db.prepare("SELECT * FROM files WHERE id = ?").get(fileId);
|
|
}
|
|
|
|
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 (?, ?, ?, ?, ?)",
|
|
)
|
|
.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);
|
|
}
|
|
|
|
// Clone templates
|
|
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);
|
|
|
|
// Clone routes
|
|
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);
|
|
|
|
// Clone scaffold pages
|
|
const cloneScaffoldPages = db.prepare(`
|
|
INSERT INTO scaffold_pages (route_id, content, created_at)
|
|
SELECT newRoutes.id, sp.content, sp.created_at
|
|
FROM scaffold_pages sp
|
|
JOIN routes oldRoutes ON sp.route_id = oldRoutes.id
|
|
JOIN routes newRoutes ON oldRoutes.path = newRoutes.path AND oldRoutes.verb = newRoutes.verb
|
|
WHERE oldRoutes.structure_id = ? AND newRoutes.structure_id = ?;
|
|
`);
|
|
cloneScaffoldPages.run(structId, newStructId);
|
|
|
|
// Clone scaffold params
|
|
const cloneScaffoldParams = db.prepare(`
|
|
INSERT INTO scaffold_params (route_id, query_params, url_params, created_at)
|
|
SELECT newRoutes.id, sp.query_params, sp.url_params, sp.created_at
|
|
FROM scaffold_params sp
|
|
JOIN routes oldRoutes ON sp.route_id = oldRoutes.id
|
|
JOIN routes newRoutes ON oldRoutes.path = newRoutes.path AND oldRoutes.verb = newRoutes.verb
|
|
WHERE oldRoutes.structure_id = ? AND newRoutes.structure_id = ?;
|
|
`);
|
|
cloneScaffoldParams.run(structId, newStructId);
|
|
|
|
return newStructId;
|
|
});
|
|
|
|
return transaction();
|
|
}
|
|
|
|
function createLog(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(routeId) {
|
|
return db
|
|
.prepare(
|
|
"SELECT * FROM logs WHERE route_id = ? ORDER BY created_at DESC LIMIT 50",
|
|
)
|
|
.all(routeId)
|
|
.reverse();
|
|
}
|
|
|
|
function getNewLogsByRoute(routeId, since) {
|
|
return db
|
|
.prepare(
|
|
"SELECT * FROM logs WHERE route_id = ? AND id > ? ORDER BY created_at DESC",
|
|
)
|
|
.all(routeId, since);
|
|
}
|
|
|
|
function getMostRecentLogIdByRoute(routeId) {
|
|
const result = db
|
|
.prepare("SELECT MAX(id) AS id FROM logs WHERE route_id = ?")
|
|
.get(routeId);
|
|
return result ? result.id : 0;
|
|
}
|
|
|
|
function buildScaffoldUrl(endpoint, urlParams, queryString) {
|
|
let populatedUrl = endpoint.replace(/:([^/]+)/g, () => urlParams.shift() || '');
|
|
return queryString ? `${populatedUrl}?${queryString}` : populatedUrl;
|
|
}
|
|
|
|
function createScaffoldPage(routeId, content=null) {
|
|
if (!content) {
|
|
const route = getRoute(routeId)
|
|
const endpoint = buildScaffoldUrl(route.path, route.url_params, route.query_params);
|
|
content = getDefaultScaffoldContentByVerb(endpoint, route.verb);
|
|
}
|
|
const stmt = db.prepare(
|
|
"INSERT INTO scaffold_pages (route_id, content) VALUES (?, ?)"
|
|
);
|
|
const info = stmt.run(routeId, content);
|
|
return info.lastInsertRowid; // Returns the scaffold_page id of the newly created scaffold page
|
|
}
|
|
|
|
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(scaffoldPage) {
|
|
return update("scaffold_pages", ["content"], scaffoldPage).changes > 0;
|
|
}
|
|
|
|
function generatePostForm(endpoint) {
|
|
return `
|
|
<!DOCTYPE html>
|
|
<html lang="en">
|
|
<head>
|
|
<meta charset="UTF-8">
|
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
|
<title>Test POST Request</title>
|
|
<script src="https://unpkg.com/htmx.org@1.9.2"></script>
|
|
</head>
|
|
<body>
|
|
<h1>Test POST Request</h1>
|
|
|
|
<form action="<%= it.route("${endpoint}") %>" method="POST">
|
|
<!-- Add your form fields here to test post requests. -->
|
|
<label for="testField">Test Field:</label>
|
|
<input type="text" id="testField" name="testField" required>
|
|
|
|
<button type="submit">Submit</button>
|
|
</form>
|
|
|
|
<!-- Add any divs for hx-swaps anywhere on the page. -->
|
|
<div id="result" hx-swap="innerHTML"></div>
|
|
</body>
|
|
</html>
|
|
`;
|
|
}
|
|
|
|
function generateModifyForm(endpoint, verb) {
|
|
return `
|
|
<!DOCTYPE html>
|
|
<html lang="en">
|
|
<head>
|
|
<meta charset="UTF-8">
|
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
|
<title>Test ${verb} Request</title>
|
|
<script src="https://unpkg.com/htmx.org@1.9.2"></script>
|
|
</head>
|
|
<body>
|
|
<h1>Test ${verb} Request</h1>
|
|
|
|
<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>
|
|
|
|
<button type="submit">${verb}</button>
|
|
</form>
|
|
|
|
<!-- Add any divs for hx-swaps anywhere on the page. -->
|
|
<div id="result" hx-swap="innerHTML"></div>
|
|
</body>
|
|
</html>
|
|
`;
|
|
}
|
|
|
|
function generateWSPage(endpoint) {
|
|
return `
|
|
<!DOCTYPE html>
|
|
<html lang="en">
|
|
<head>
|
|
<meta charset="UTF-8">
|
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
|
<title>Test WebSocket</title>
|
|
</head>
|
|
<body>
|
|
<h1>Test WebSocket Connection</h1>
|
|
|
|
<input type="text" id="wsMessage" placeholder="Type a message">
|
|
<button onclick="sendMessage()">Send</button>
|
|
|
|
<div id="wsOutput"></div>
|
|
|
|
<script>
|
|
const ws = new WebSocket('<%= it.route("${endpoint}") %>');
|
|
|
|
ws.onopen = () => {
|
|
document.getElementById('wsOutput').innerHTML += '<p>Connected to WebSocket</p>';
|
|
};
|
|
|
|
ws.onmessage = (event) => {
|
|
document.getElementById('wsOutput').innerHTML += '<p>Received: ' + event.data + '</p>';
|
|
};
|
|
|
|
ws.onclose = () => {
|
|
document.getElementById('wsOutput').innerHTML += '<p>WebSocket connection closed</p>';
|
|
};
|
|
|
|
function sendMessage() {
|
|
const message = document.getElementById('wsMessage').value;
|
|
ws.send(message);
|
|
document.getElementById('wsOutput').innerHTML += '<p>Sent: ' + message + '</p>';
|
|
}
|
|
</script>
|
|
</body>
|
|
</html>
|
|
`;
|
|
}
|
|
|
|
function getDefaultScaffoldContentByVerb(url, verb) {
|
|
if (verb == "POST") {
|
|
return generatePostForm(url);
|
|
}
|
|
else if (verb == "PUT" || verb == "DELETE") {
|
|
return generateModifyForm(url, verb);
|
|
}
|
|
else if (verb == "WS") {
|
|
return generateWSPage(url);
|
|
}
|
|
}
|
|
|
|
module.exports = {
|
|
db,
|
|
applyMigrations,
|
|
getDbInstance,
|
|
getAllRoutes,
|
|
getUser,
|
|
getUserById,
|
|
createUser,
|
|
getStructures,
|
|
getStructure,
|
|
createStructure,
|
|
createRoute,
|
|
getRoutes,
|
|
getRoute,
|
|
buildScaffoldUrl,
|
|
updateRoute,
|
|
updateDb,
|
|
updateStruct,
|
|
updateTemplate,
|
|
getVersions,
|
|
getVersion,
|
|
getTemplater,
|
|
getTemplates,
|
|
getTemplate,
|
|
getInspectableTemplateContext,
|
|
getTemplateContentByName,
|
|
getTemplateByName,
|
|
createTemplate,
|
|
getDbsForStructure,
|
|
getDb,
|
|
getDbForStructure,
|
|
createDb,
|
|
attachDb,
|
|
getFilesForStruct,
|
|
getFile,
|
|
createFile,
|
|
cloneStructure,
|
|
createLog,
|
|
getLogsByRoute,
|
|
getNewLogsByRoute,
|
|
getMostRecentLogIdByRoute,
|
|
createScaffoldPage,
|
|
getLatestScaffoldPage,
|
|
updateScaffoldPage
|
|
};
|