add cloning logic

This commit is contained in:
HRG @ SCExC 2024-06-04 03:01:19 -04:00
parent 365b297397
commit 835c5fdd66
8 changed files with 367 additions and 49 deletions

213
index.js
View file

@ -28,7 +28,16 @@ const db = betterSqlite3("./dbs/0.sqlite");
db.pragma("journal_mode = WAL"); db.pragma("journal_mode = WAL");
function getAllRoutes(db) { function getAllRoutes(db) {
return db.prepare("SELECT * from routes ORDER BY id ASC;").all(); return db
.prepare(
`
SELECT routes.*, structures.route_prefix
FROM routes
JOIN structures ON routes.structure_id = structures.id
ORDER BY routes.id ASC;
`
)
.all();
} }
app.use( app.use(
@ -71,8 +80,11 @@ function applyMigrations() {
function buildRoutes() { function buildRoutes() {
for (let route of getAllRoutes(db)) { for (let route of getAllRoutes(db)) {
const prefix = route.route_prefix;
console.log(route);
const p = prefix ? path.join(route.route_prefix, route.path) : route.path;
routes[route.verb].push({ routes[route.verb].push({
matcher: match(route.path, { decode: decodeURIComponent }), matcher: match(p, { decode: decodeURIComponent }),
id: route.id, id: route.id,
path: route.path, path: route.path,
}); });
@ -211,6 +223,17 @@ function updateDb(db, appDb) {
db.prepare(sql).run(...values); db.prepare(sql).run(...values);
} }
function updateStruct(db, struct) {
const fields = ["name", "route_prefix"];
console.log(struct, fields);
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) { function updateTemplate(db, template) {
const fields = ["content", "name", "test_object"]; const fields = ["content", "name", "test_object"];
const values = fields.map((field) => template[field]); const values = fields.map((field) => template[field]);
@ -371,7 +394,92 @@ function getDbInstance(dbId) {
return dbInstance; return dbInstance;
} }
function bootstrapTemplateWithHTMXetc(htmlString, blissRoute, wrapHTML) { function cloneStructure(
structId,
newStructureName,
userId,
routePrefix = "",
cloneDb = false
) {
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;
if (cloneDb) {
const dbIds = db
.prepare(`select db_id from structure_dbs where structure_id = ?;`)
.all(structId);
for (let { db_id } of dbIds) {
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) => {
console.log(db_id, new_db.id);
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");
});
}
} else {
db.prepare(
`
INSERT INTO structure_dbs (db_id, structure_id, alias)
SELECT db_id, ?, alias FROM structure_dbs WHERE structure_id = ?;
`
).run(newStructId, structId);
}
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,
blissClone,
blissCopy,
wrapHTML
) {
if (htmlString.startsWith("<html>") || wrapHTML) { if (htmlString.startsWith("<html>") || wrapHTML) {
const $ = cheerio.load(htmlString); const $ = cheerio.load(htmlString);
let head = $("head"); let head = $("head");
@ -390,13 +498,18 @@ function bootstrapTemplateWithHTMXetc(htmlString, blissRoute, wrapHTML) {
`); `);
if (blissRoute) { if (blissRoute) {
$.root().children().attr("data-bliss-route", blissRoute); $("body").attr("data-bliss-route", blissRoute);
$("body").attr("data-bliss-clone", blissClone);
if (blissCopy) $("body").attr("data-bliss-copy", blissCopy);
} }
htmlString = $.html(); htmlString = $.html();
} else if (blissRoute) { } else if (blissRoute) {
const $ = cheerio.load(htmlString, null, false); const $ = cheerio.load(htmlString, null, false);
$.root().children().attr("data-bliss-route", blissRoute); $.root().children().attr("data-bliss-route", blissRoute);
$.root().children().attr("data-bliss-clone", blissClone);
if (blissCopy) $.root().children().attr("data-bliss-copy", blissCopy);
console.log(blissCopy, "fleem")
htmlString = $.html(); htmlString = $.html();
} }
@ -444,6 +557,27 @@ app.get("/workshop/:structure_id", (req, res) => {
); );
}); });
app.post("/workshop/:structure_id/clone", (req, res) => {
let routePrefix = req.body.route_prefix;
routePrefix = routePrefix[0] == "/" ? routePrefix : "/" + routePrefix;
const cloneDb = req.body.clone_db == "true";
let newStructureId;
try {
newStructureId = cloneStructure(
req.params.structure_id,
req.body.name,
1,
// req.session.userId,
routePrefix,
cloneDb
);
buildRoutes();
} catch (e) {
return res.send(e.stack);
}
return smartRedirect(req, res, `/workshop/${newStructureId}`);
});
app.post("/workshop/:structure_id/db", (req, res) => { app.post("/workshop/:structure_id/db", (req, res) => {
const dbId = createDb(db, req.params.structure_id, req.body.name); const dbId = createDb(db, req.params.structure_id, req.body.name);
const redirectUrl = `/workshop/${req.params.structure_id}/db/${dbId}`; const redirectUrl = `/workshop/${req.params.structure_id}/db/${dbId}`;
@ -485,6 +619,7 @@ app.post("/workshop/:structure_id/route", (req, res) => {
req.params.structure_id, req.params.structure_id,
"// put your handler code here\n\nfunction handler(req, res) {\n res.send('henlo world') \n}" "// put your handler code here\n\nfunction handler(req, res) {\n res.send('henlo world') \n}"
); );
// todo optimize by only adding new, don't just rebuild all
buildRoutes(); buildRoutes();
return smartRedirect( return smartRedirect(
req, req,
@ -591,15 +726,22 @@ app.get("/workshop/:structure_id/template/:template_id", (req, res) => {
app.get("/workshop/:structure_id/template/:template_id/preview", (req, res) => { app.get("/workshop/:structure_id/template/:template_id/preview", (req, res) => {
const template = getTemplate(db, req.params.template_id); const template = getTemplate(db, req.params.template_id);
const struct = getStructure(db, req.params.structure_id);
const eta = getTemplater(req.params.structure_id); const eta = getTemplater(req.params.structure_id);
const context = vm.createContext({ it: null }); const context = vm.createContext({ it: null });
vm.runInContext(template.test_object, context); vm.runInContext(template.test_object, context);
context.it.route = function (url) {
return struct.route_prefix ? path.join(struct.route_prefix, url) : url;
};
return res.send( return res.send(
bootstrapTemplateWithHTMXetc( bootstrapTemplateWithHTMXetc(
eta.render(template.name, context.it), eta.render(template.name, context.it),
null, null,
null,
null,
true true
) )
); );
@ -607,11 +749,15 @@ app.get("/workshop/:structure_id/template/:template_id/preview", (req, res) => {
app.get("/workshop/:structure_id/route/:id", (req, res) => { app.get("/workshop/:structure_id/route/:id", (req, res) => {
const route = getRoute(db, req.params.id); const route = getRoute(db, req.params.id);
const routePrefix = getStructure(db, req.params.structure_id).route_prefix;
return res.send( return res.send(
bootstrapTemplateWithHTMXetc( bootstrapTemplateWithHTMXetc(
eta.render("workshop/route", { eta.render("workshop/route", {
route: route, route: route,
previewUrl: route["verb"] == "GET" ? route.path : "/workshop/tip", previewUrl:
route["verb"] == "GET"
? path.join(routePrefix, route.path)
: "/workshop/tip",
...sidebarStuff(db, req.params.structure_id), ...sidebarStuff(db, req.params.structure_id),
}) })
) )
@ -682,6 +828,25 @@ app.get("/workshop/:structure_id/files", (req, res) => {
); );
}); });
app.get("/workshop/:structure_id/settings", (req, res) => {
return res.send(
bootstrapTemplateWithHTMXetc(
eta.render("workshop/settings", {
...sidebarStuff(db, req.params.structure_id),
})
)
);
});
app.put("/workshop/:structure_id/settings", (req, res) => {
let structId = req.params.structure_id;
let struct = getStructure(db, structId);
updateStruct(db, { ...struct, route_prefix: req.body.route_prefix });
struct = getStructure(db, structId);
res.send("success!");
});
app.get("/workshop/:structure_id/new_template_modal", (req, res) => { app.get("/workshop/:structure_id/new_template_modal", (req, res) => {
return res.send( return res.send(
bootstrapTemplateWithHTMXetc( bootstrapTemplateWithHTMXetc(
@ -712,15 +877,31 @@ app.get("/workshop/:structure_id/new_db_modal", (req, res) => {
); );
}); });
app.get("/workshop/:structure_id/clone_modal", (req, res) => {
const structure = getStructure(db, req.params.structure_id);
return res.send(
bootstrapTemplateWithHTMXetc(
eta.render("workshop/clone_structure_modal", {
structure,
defaultRoutePrefix: path.join(structure.route_prefix || "", "clone"),
})
)
);
});
function embedHTML(url) {
return `<div hx-get="${url}" hx-trigger="load"></div>`;
}
app.all("*", (req, res) => { app.all("*", (req, res) => {
const path = req.path; const req_path = req.path;
const verb = req.method; const verb = req.method;
try { try {
let routeMatch = null; let routeMatch = null;
for (let { matcher, id } of routes[verb]) { for (let { matcher, id } of routes[verb]) {
let matchFromRoute = matcher(path); let matchFromRoute = matcher(req_path);
if (matchFromRoute) { if (matchFromRoute) {
routeMatch = { params: matchFromRoute.params, id: id }; routeMatch = { params: matchFromRoute.params, id: id };
} }
@ -733,6 +914,7 @@ app.all("*", (req, res) => {
.get(routeMatch.id); .get(routeMatch.id);
let dbs = getDbsForStructure(db, route.structure_id); let dbs = getDbsForStructure(db, route.structure_id);
let __urlPrefix = getStructure(db, route.structure_id).route_prefix;
const allDbInstances = {}; const allDbInstances = {};
@ -762,13 +944,26 @@ app.all("*", (req, res) => {
res.rawSend( res.rawSend(
bootstrapTemplateWithHTMXetc( bootstrapTemplateWithHTMXetc(
args[0], args[0],
`/workshop/${route.structure_id}/route/${route.id}` `/workshop/${route.structure_id}/route/${route.id}`,
`/workshop/${route.structure_id}/clone/`,
verb == "GET" ? embedHTML(req.originalUrl) : null
) )
); );
}; };
res.render = (template, context) => { res.render = (template, context) => {
context.route = function (url) {
return __urlPrefix ? path.join(__urlPrefix, url) : url;
};
const eta = getTemplater(route.structure_id); const eta = getTemplater(route.structure_id);
res.send(bootstrapTemplateWithHTMXetc(eta.render(template, context))); console.log(route.method, "\n\n\n");
res.rawSend(
bootstrapTemplateWithHTMXetc(
eta.render(template, context),
`/workshop/${route.structure_id}/route/${route.id}`,
`/workshop/${route.structure_id}/clone_modal/`,
verb == "GET" ? embedHTML(req.originalUrl) : null
)
);
}; };
return vm.runInContext( return vm.runInContext(
(route.handler += `\n\nhandler(req, res)`), (route.handler += `\n\nhandler(req, res)`),

View file

@ -11,9 +11,12 @@ CREATE TABLE structures (
id INTEGER PRIMARY KEY AUTOINCREMENT, id INTEGER PRIMARY KEY AUTOINCREMENT,
name TEXT NOT NULL, name TEXT NOT NULL,
user_id INTEGER, user_id INTEGER,
route_prefix TEXT,
cloned_from INTEGER,
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
FOREIGN KEY(user_id) REFERENCES users(id), FOREIGN KEY(user_id) REFERENCES users(id),
FOREIGN KEY(cloned_from) REFERENCES structures(id),
UNIQUE(name, user_id) UNIQUE(name, user_id)
); );

View file

@ -1,65 +1,121 @@
document.addEventListener("DOMContentLoaded", function () { document.addEventListener("DOMContentLoaded", function () {
console.log("supp", document.location)
// Create the magnifying glass emoji button // Create the magnifying glass emoji button
const magnifyingGlass = document.createElement('div'); const magnifyingGlass = document.createElement("div");
magnifyingGlass.innerHTML = '🔍'; magnifyingGlass.innerHTML = "🔍";
magnifyingGlass.className = 'fixed top-2 right-2 cursor-pointer text-2xl z-50'; magnifyingGlass.className =
"fixed bottom-2 right-2 cursor-pointer text-2xl z-50";
document.body.appendChild(magnifyingGlass); document.body.appendChild(magnifyingGlass);
let highlighted = false; let highlighted = false;
// Function to highlight elements // Function to highlight elements
function highlightElements() { function highlightElements() {
const elements = document.querySelectorAll('[data-bliss-route]'); const modal = document.createElement("div");
elements.forEach(el => { modal.id = "inspector-modal";
if (!el.classList.contains('highlighted')) { document.body.appendChild(modal);
el.classList.add('border-2', 'border-yellow-500', 'relative', 'highlighted');
const elements = document.querySelectorAll("[data-bliss-route]");
elements.forEach((el) => {
if (!el.classList.contains("highlighted")) {
el.classList.add(
"border-2",
"border-yellow-500",
"relative",
"highlighted",
"p-2"
);
let controls = document.createElement("div");
controls.className =
"flex absolute gap-1 -bottom-3 right-0 inspector-controls";
// Create the edit icon // Create the edit icon
const editIcon = document.createElement('a'); const editIcon = document.createElement("a");
editIcon.href = el.getAttribute('data-bliss-route'); editIcon.href = el.getAttribute("data-bliss-route");
editIcon.innerHTML = '✏️'; editIcon.innerHTML = "✏️";
editIcon.className = 'absolute text-lg bg-white rounded-full p-1 shadow -bottom-3 -right-3'; editIcon.className =
el.appendChild(editIcon); "text-lg bg-white rounded-full p-1 shadow edit-icon";
controls.appendChild(editIcon);
// Create the copy icon
if (el.getAttribute("data-bliss-copy")) {
const copyIcon = document.createElement("button");
copyIcon.innerHTML = "📋";
copyIcon.className =
"text-lg bg-white rounded-full p-1 shadow copy-icon";
controls.appendChild(copyIcon);
copyIcon.addEventListener("click", () => {
navigator.clipboard
.writeText(el.getAttribute("data-bliss-copy"))
.then(() => {
copyIcon.innerHTML = "✅";
})
.catch((err) => {
copyIcon.innerHTML = "❌";
});
});
}
// Create the clone icon
const cloneIcon = document.createElement("div");
cloneIcon.innerHTML = "👯‍♀️";
cloneIcon.className =
"text-lg bg-white rounded-full cursor-pointer p-1 shadow clone-icon";
cloneIcon.setAttribute("hx-get", el.getAttribute("data-bliss-clone"));
cloneIcon.setAttribute("hx-target", "#inspector-modal");
controls.setAttribute(
"_",
`on mouseover set my.style.zIndex to 10000
on mouseout set my.style.zIndex to "initial"
`
);
controls.appendChild(cloneIcon);
el.appendChild(controls);
htmx.process(controls);
_hyperscript.processNode(controls);
} }
}); });
} }
// Function to remove highlights // Function to remove highlights
function removeHighlights() { function removeHighlights() {
const elements = document.querySelectorAll('[data-bliss-route].highlighted'); const elements = document.querySelectorAll(
elements.forEach(el => { "[data-bliss-route].highlighted"
el.classList.remove('border-2', 'border-yellow-500', 'relative', 'highlighted'); );
const editIcon = el.querySelector('a'); elements.forEach((el) => {
if (editIcon) { el.classList.remove(
el.removeChild(editIcon); "border-2",
} "border-yellow-500",
"relative",
"highlighted",
"p-2"
);
el.querySelectorAll(".inspector-controls").forEach((el) => el.remove());
}); });
} }
magnifyingGlass.addEventListener('click', function() { magnifyingGlass.addEventListener("click", function () {
if (!highlighted) { if (!highlighted) {
highlightElements(); highlightElements();
magnifyingGlass.innerHTML = '❌'; magnifyingGlass.innerHTML = "❌";
} else { } else {
removeHighlights(); removeHighlights();
magnifyingGlass.innerHTML = '🔍'; magnifyingGlass.innerHTML = "🔍";
} }
highlighted = !highlighted; highlighted = !highlighted;
}); });
// Observe the document for changes // Observe the document for changes
const observer = new MutationObserver(function(mutations) { // const observer = new MutationObserver(function (mutations) {
mutations.forEach(function(mutation) { // if (highlighted) {
if (highlighted) { // highlightElements();
highlightElements(); // }
} // });
});
});
// Configure the observer // // Configure the observer
observer.observe(document.body, { // observer.observe(document.body, {
childList: true, // childList: true,
subtree: true // subtree: true,
}); // });
}); });

View file

@ -0,0 +1,24 @@
<div class="fixed inset-0 flex items-center justify-center z-10 " _="on closeModal remove me">
<div class="bg-white rounded-md border border-black p-2">
<div class="text-lg">Clone Structure</div>
<form hx-post="/workshop/<%= it.structure.id %>/clone" hx-target="#response" class="flex flex-col gap-2">
<div class="flex flex-col">
<label class="text-xs">Name</label>
<input name="name" id="name" type="text" value="<%= it.structure.name %> clone" />
</div>
<div class="flex flex-col">
<label class="text-xs">Route Prefix</label>
<input name="route_prefix" id="route_prefix" type="text" value="<%= it.defaultRoutePrefix %>" />
</div>
<div class="flex gap-2">
<input id="default-checkbox" type="checkbox" name="cloneDBs" value="true">
<label class="text-xs">Clone DBs</label>
</div>
<section class="flex flex-end gap-2" style="justify-content: flex-end">
<button class="bg-green-500 rounded p-2 hover:bg-green-700 text-white" type="submit">Clone</button>
<button class="bg-gray-400 rounded p-2 hover:bg-gray-700 text-white" type="button" _="on click trigger closeModal">Cancel</button>
</section>
</form>
<div id="response"></div>
</div>
</div>

View file

@ -39,3 +39,4 @@
<div id="response"></div> <div id="response"></div>
</div> </div>
</div> </div>
</div>

View file

@ -20,3 +20,4 @@
<div id="response"></div> <div id="response"></div>
</div> </div>
</div> </div>
</div>

View file

@ -0,0 +1,34 @@
<html>
<head>
<link rel="stylesheet" href="/css/xp.css">
<script src="/js/ace/ace.js" type="text/javascript" charset="utf-8"></script>
</head>
<body>
<div class="flex flex-col w-full h-full window">
<div class="title-bar">
<div class="title-bar-text">Workshop - <%~ include('/workshop/structure_name', {structure: it.structure}) %>
</div>
</div>
<div class="flex flex-row flex-grow window-body">
<div class="w-1/5 h-full">
<%~ include('/workshop/sidebar', it) %>
</div>
<div id="main-editor" class="flex flex-grow">
<div id="left-pane" class="flex-grow h-full ">
<form hx-put="/workshop/<%= it.structure.id %>/settings" hx-target="#result" class="p-2 flex flex-col gap-2 max-w-48">
<div class="field-row-stacked">
<label for="name">Route Prefix</label>
<input name="route_prefix" value="<%= it.structure.route_prefix %>">
</div>
<button id="save-btn" type="submit">Save</button>
<div id="result"></div>
</form>
</div>
</div>
</div>
</div>
</body>
</html>

View file

@ -6,7 +6,8 @@
<% it.routes.map(route => { %> <% it.routes.map(route => { %>
<li> <li>
<a href="/workshop/<%= it.structure.id %>/route/<%= route.id %>"> <a href="/workshop/<%= it.structure.id %>/route/<%= route.id %>">
<%= route.verb %> - <%= route.path %> <%= route.verb %> -
<%= it.structure.route_prefix %><%= route.path %>
</a> </a>
</li> </li>
<% }) %> <% }) %>
@ -59,5 +60,8 @@
<li> <li>
<a href="/workshop/<%= it.structure.id %>/files">Files</a> <a href="/workshop/<%= it.structure.id %>/files">Files</a>
</li> </li>
<li>
<a href="/workshop/<%= it.structure.id %>/settings">Settings</a>
</li>
</ul> </ul>
<div id="modal-zone" style="z-index: 1000000;"></div> <div id="modal-zone" style="z-index: 1000000;"></div>