466 lines
13 KiB
JavaScript
466 lines
13 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)
|
||
|
|
// 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
|
||
|
|
}
|