bliss/db.js
2025-01-21 02:38:24 -05:00

690 lines
20 KiB
JavaScript

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);
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(db, routeId);
}
return routeId; // Returns the route_id of the newly created route
}
function getRoutes(db, structureId) {
const query = `
SELECT
r.*,
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
WHERE r.structure_id = ?
ORDER BY sp.created_at DESC, sp2.created_at DESC
`;
return db.prepare(query).all(structureId);
}
function getRoute(db, routeId) {
const query = `
SELECT
r.*,
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
WHERE r.id = ?
ORDER BY sp.created_at DESC, sp2.created_at DESC
LIMIT 1
`;
return db.prepare(query).get(routeId);
}
function defaultEndpointForRoute(db, 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);
}
// 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(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 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(db, routeId, content=null) {
if (!content) {
const route = getRoute(db, routeId)
const endpoint = buildScaffoldUrl(route.path, route.url_params, route.query_params);
content = getDefaultScaffoldContentByVerb(endpoint, route.verb);
console.log("gabga", content, route, endpoint)
}
console.log(content, "content")
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(db, 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) {
const fields = ["content"];
const values = fields.map((field) => scaffoldPage[field]);
const placeholders = fields.map((field) => `${field} = ?`).join(", ");
console.log(scaffoldPage, "gabababab")
const sql = `UPDATE scaffold_pages SET ${placeholders} WHERE id = ?`;
values.push(scaffoldPage.id); // Add scaffoldPageId to the end for the WHERE clause
const info = db.prepare(sql).run(...values);
console.log(info.changes)
return info.changes > 0; // Returns true if a row was updated, false otherwise
}
function generatePostForm(endpoint) {
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,
createUser,
getStructures,
getStructure,
createStructure,
createRoute,
getRoutes,
getRoute,
buildScaffoldUrl,
updateRoute,
updateDb,
updateStruct,
updateTemplate,
getTemplater,
getTemplates,
getTemplate,
getTemplateContentByName,
createTemplate,
getDbsForStructure,
getDb,
getDbForStructure,
createDb,
attachDb,
getFilesForStruct,
getFile,
createFile,
cloneStructure,
createLog,
getLogsByRoute,
getNewLogsByRoute,
getMostRecentLogIdByRoute,
createScaffoldPage,
getLatestScaffoldPage,
updateScaffoldPage
};