summaryrefslogtreecommitdiff
path: root/src
diff options
context:
space:
mode:
authorEuAndreh <eu@euandre.org>2024-02-23 06:05:19 -0300
committerEuAndreh <eu@euandre.org>2024-02-23 06:05:21 -0300
commitc36bf8e3577da31cf6d575879c7e92d3e9c7e4f1 (patch)
tree4fd5414e76490e297c4c770ff35e09149ef3658f /src
parentRemove C code and cleanup repository (diff)
downloadpapod-c36bf8e3577da31cf6d575879c7e92d3e9c7e4f1.tar.gz
papod-c36bf8e3577da31cf6d575879c7e92d3e9c7e4f1.tar.xz
Big cleanup
- delete all SQLite Node-API code: we'll use the C++ one instead; - implement hero.mjs, with tests! - use ESM all over.
Diffstat (limited to 'src')
-rw-r--r--src/api.mjs (renamed from src/api.js)12
-rwxr-xr-xsrc/bin.mjs4
-rwxr-xr-xsrc/cli4
-rw-r--r--src/db.mjs (renamed from src/db.js)18
-rw-r--r--src/escape.mjs14
-rw-r--r--src/hero.mjs220
-rw-r--r--src/ircd.mjs (renamed from src/ircd.js)10
-rw-r--r--src/package.json3
-rw-r--r--src/utils.mjs (renamed from src/utils.js)37
-rw-r--r--src/web.mjs (renamed from src/web.js)8
10 files changed, 286 insertions, 44 deletions
diff --git a/src/api.js b/src/api.mjs
index 7f0991f..27a14b1 100644
--- a/src/api.js
+++ b/src/api.mjs
@@ -1,8 +1,8 @@
-const { eq } = require("./utils.js");
-const ircd = require("./ircd.js");
-const web = require("./web.js");
+import { eq } from "./utils.mjs";
+import * as ircd from "./ircd.mjs";
+import * as web from "./web.mjs";
-const main = async () => {
+export const main = async () => {
if (process.argv.length === 3 && process.argv[2] === "-V") {
console.log("papo 1970-01-01 0.1.0");
return;
@@ -22,7 +22,3 @@ const main = async () => {
argv: process.argv,
});
};
-
-module.exports = {
- main,
-};
diff --git a/src/bin.mjs b/src/bin.mjs
new file mode 100755
index 0000000..5dbbb25
--- /dev/null
+++ b/src/bin.mjs
@@ -0,0 +1,4 @@
+#!/usr/bin/env node
+import { main } from "./api.mjs";
+
+main();
diff --git a/src/cli b/src/cli
deleted file mode 100755
index 1de3383..0000000
--- a/src/cli
+++ /dev/null
@@ -1,4 +0,0 @@
-#!/usr/bin/env node
-const { main } = require("papo");
-
-main();
diff --git a/src/db.js b/src/db.mjs
index acbc7b4..59d6a0e 100644
--- a/src/db.js
+++ b/src/db.mjs
@@ -1,15 +1,17 @@
-const assert = require("node:assert/strict");
-const fs = require("node:fs");
+import assert from "node:assert/strict";
+import fs from "node:fs";
+import path from "node:path";
+import url from "node:url";
-const sqlite = require("./napi-sqlite.node");
-const { difference, log } = require("./utils");
+import { difference, log } from "./utils.mjs";
-const MIGRATIONS_DIR = __dirname + "/sql/migrations/";
+const DIRNAME = path.dirname(url.fileURLToPath(import.meta.url));
+const MIGRATIONS_DIR = DIRNAME + "/sql/migrations/";
const MIGRATION_FILENAMES = fs.readdirSync(MIGRATIONS_DIR, "UTF-8");
let db = null;
-const init = async (dbName = process.env.PAPO_DB_PATH || ":memory:") => {
+export const init = async (dbName = process.env.PAPO_DB_PATH || ":memory:") => {
assert(dbName);
db = await sqlite.open(dbName);
@@ -45,7 +47,3 @@ const init = async (dbName = process.env.PAPO_DB_PATH || ":memory:") => {
}
await db.exec("COMMIT TRANSACTION;");
};
-
-module.exports = {
- init,
-};
diff --git a/src/escape.mjs b/src/escape.mjs
new file mode 100644
index 0000000..78e08f8
--- /dev/null
+++ b/src/escape.mjs
@@ -0,0 +1,14 @@
+const FROM = /[&<>'"]/g;
+
+const ESCAPES = {
+ '&': '&amp;',
+ '<': '&lt;',
+ '>': '&gt;',
+ "'": '&#39;',
+ '"': '&quot;'
+};
+
+const mappingFn = c => ESCAPES[c];
+
+export const escape = s =>
+ String.prototype.replace.call(s, FROM, mappingFn);
diff --git a/src/hero.mjs b/src/hero.mjs
new file mode 100644
index 0000000..a8532a8
--- /dev/null
+++ b/src/hero.mjs
@@ -0,0 +1,220 @@
+import assert from "node:assert";
+import crypto from "node:crypto";
+import http from "node:http";
+
+import { assocIn, getIn, first, log } from "./utils.mjs";
+
+export const normalizeSegments = segments =>
+ segments.length === 1 && segments[0] === "" ?
+ segments :
+ segments.concat([""]);
+
+export const pathToSegments = path =>
+ normalizeSegments(path
+ .replace(/^\/*/, "")
+ .replace(/\/*$/, "")
+ .replace(/\/+/, "/")
+ .split("/"));
+
+export const hasPathParams = segments =>
+ segments.some(s => s.startsWith(":"));
+
+const HTTP_METHODS = new Set([
+ "GET",
+ "HEAD",
+ "POST",
+ "PUT",
+ "PATCH",
+ "DELETE",
+ "OPTIONS",
+]);
+
+const HTTP_METHODS_ARR = [...HTTP_METHODS.keys()].sort();
+
+export const addRoute = (table, methods, path, handlerFn) => {
+ if (methods === "*") {
+ return addRoute(table, HTTP_METHODS_ARR, path, handlerFn);
+ }
+
+ if (!Array.isArray(methods)) {
+ return addRoute(table, [methods], path, handlerFn);
+ }
+
+ assert.ok(methods.every(m => HTTP_METHODS.has(m)));
+
+ const segments = pathToSegments(path);
+ const kw = hasPathParams(segments) ? "dynamic" : "static";
+ return methods.reduce(
+ (acc, el) => assocIn(acc, [kw, el].concat(segments), handlerFn),
+ table,
+ );
+};
+
+export const buildRoutes = routes =>
+ routes.reduce(
+ (acc, [methods, path, handlerFn]) =>
+ addRoute(acc, methods, path, handlerFn),
+ {}
+ );
+
+export const findStaticHandler = (table, method, segments) => {
+ const handlerFn = getIn(table, ["static", method].concat(segments));
+ return !handlerFn ? null : { handlerFn, params: {} };
+};
+
+/**
+ * "first" as is:
+ * - depth-first, as we look for a full match and use it instead of searching
+ * in parallel;
+ * - the first param in the same level to show up alphabetically, e.g.
+ * ":a-user" matches before ":id" does.
+ */
+export const firstParamMatch = (tree, segments, params) => {
+ assert.notEqual(segments.length, 0);
+
+ const [seg, ...nextSegments] = segments;
+
+ if (segments.length === 1) {
+ assert.equal(seg, "");
+ const handlerFn = tree[""];
+
+ return !handlerFn ? null : { handlerFn, params };
+ }
+
+ const subtree = tree[seg];
+ if (subtree) {
+ const submatch = firstParamMatch(subtree, nextSegments, params);
+ // propagation of the end of recursion
+ if (submatch) {
+ return submatch;
+ }
+ }
+
+ // literal matching failed, we now look for patterns that might match
+ const paramOptions = Object.keys(tree)
+ .filter(s => s.startsWith(":"))
+ .sort();
+ return first(paramOptions, param => firstParamMatch(tree[param], nextSegments, {
+ ...params,
+ [param.slice(1)]: seg
+ }));
+};
+
+export const findDynamicHandler = (table, method, segments) => {
+ const tree = table?.dynamic?.[method];
+ return !tree ? null : firstParamMatch(tree, segments, {});
+};
+
+export const findHandler = (table, method, path) => {
+ const segments = pathToSegments(path);
+ return (
+ findStaticHandler(table, method, segments) ||
+ findDynamicHandler(table, method, segments)
+ );
+};
+
+export const extractQueryParams = s => {
+ const ret = {};
+ for (const [k, v] of new URLSearchParams(s)) {
+ ret[k] = v;
+ }
+ return ret;
+};
+
+export const handleRequest = async (table, method, url) => {
+ const [ path, queryParams ] = url.split("?");
+ const handler = findHandler(table, method, path);
+ if (!handler) {
+ return {
+ status: 404,
+ body: "Not Found",
+ };
+ }
+
+ const request = {
+ params: {
+ path: handler.params,
+ query: extractQueryParams(queryParams),
+ },
+ method,
+ path,
+ handler: handler.handlerFn,
+ };
+
+ return await handler.handlerFn(request);
+};
+
+export const makeRequestListener = table => async (req, res) => {
+ const response = await handleRequest(table, req.method, req.url);
+ res.writeHead(response.status, response.headers);
+ res.end(response.body);
+};
+
+export const interceptorsFn = ({
+ uuidFn,
+ logger,
+} = {
+ uuidFn: crypto.randomUUID,
+ logger: log,
+}) => ({
+ requestId: (req, next) => next({ ...req, id: uuidFn() }),
+ logged: async (req, next) => {
+ const { id, url, method } = req;
+ logger({
+ id,
+ url,
+ method,
+ type: "in-request",
+ });
+ const response = await next(req);
+ const { status } = response;
+ logger({
+ id,
+ status,
+ type: "in-response",
+ });
+ return response;
+ },
+ contentType: async (req, next) => {
+ const response = await next(req);
+ const [mimeType, body] = typeof response.body === "string" ?
+ ["text/html", response.body] :
+ ["application/json", JSON.stringify(response.body) || ""];
+ return {
+ ...response,
+ body,
+ headers: {
+ "Content-Type": mimeType,
+ "Content-Length": Buffer.byteLength(body),
+ ...(response.headers || {})
+ },
+ };
+ },
+ serverError: async (req, next) => {
+ try {
+ const response = await next(req);
+ assert.ok("status" in response, `Missing "status"`);
+ return response;
+ } catch (error) {
+ logger({
+ id: req.id,
+ type: "server-error-interceptor",
+ message: error.message,
+ });
+ return {
+ status: 500,
+ body: "Internal Server Error",
+ };
+ }
+ },
+});
+
+export const interceptors = interceptorsFn();
+
+export const chainInterceptors = arr =>
+ req => arr.length === 0 ?
+ req :
+ arr[0](req, chainInterceptors(arr.slice(1)));
+
+export const wrapHandler = (fn, arr) =>
+ chainInterceptors(arr.concat([ (req, _next) => fn(req) ]));
diff --git a/src/ircd.js b/src/ircd.mjs
index 6a02bd6..38903f8 100644
--- a/src/ircd.js
+++ b/src/ircd.mjs
@@ -1,18 +1,14 @@
-const net = require("node:net");
-const db = require("./db.js");
+import net from "node:net";
+import {} from "./db.mjs";
const server = net.createServer(socket => {
socket.write("olar\r\n");
socket.pipe(socket);
});
-const app = async udsPath => {
+export const app = async udsPath => {
await db.init();
server.listen(udsPath, () => {
console.log("I'm ircd.");
});
};
-
-module.exports = {
- app,
-};
diff --git a/src/package.json b/src/package.json
new file mode 100644
index 0000000..fb3cb61
--- /dev/null
+++ b/src/package.json
@@ -0,0 +1,3 @@
+{
+ "main": "api.mjs"
+}
diff --git a/src/utils.js b/src/utils.mjs
index e1725ef..e0e20a7 100644
--- a/src/utils.js
+++ b/src/utils.mjs
@@ -1,4 +1,4 @@
-const eq = (a, b) => {
+export const eq = (a, b) => {
if (a === b) {
return true;
}
@@ -31,14 +31,14 @@ const eq = (a, b) => {
return true;
};
-const keys = (ks, obj) =>
+export const keys = (ks, obj) =>
ks.reduce(
(ret, k) =>
obj.hasOwnProperty(k) ? {...ret, [k]: obj[k]} : ret,
{},
);
-const difference = (a, b) => {
+export const difference = (a, b) => {
const diff = new Set(a);
for (const el of b) {
diff.delete(el);
@@ -46,12 +46,31 @@ const difference = (a, b) => {
return diff;
};
-const log = o => console.error(JSON.stringify(o));
+export const assocIn = (obj, path, value) =>
+ path.length === 0 ? obj :
+ path.length === 1 ? { ...obj, [path[0]]: value } :
+ {
+ ...obj,
+ [path[0]]: assocIn(
+ (obj[path[0]] || {}),
+ path.slice(1),
+ value
+ )
+ };
+export const getIn = (obj, path) =>
+ path.length === 0 ? obj :
+ getIn(obj?.[path[0]], path.slice(1));
-module.exports = {
- eq,
- keys,
- difference,
- log,
+export const first = (arr, fn) => {
+ for (const x of arr) {
+ const ret = fn(x);
+ if (ret) {
+ return ret;
+ }
+ }
+
+ return null;
};
+
+export const log = o => console.error(JSON.stringify(o));
diff --git a/src/web.js b/src/web.mjs
index bfa1807..8eed5b4 100644
--- a/src/web.js
+++ b/src/web.mjs
@@ -1,4 +1,4 @@
-const http = require("node:http");
+import http from "node:http";
const listProducts = () => {};
const getProduct = () => {};
@@ -23,12 +23,8 @@ const server = http.createServer((req, res) => {
res.end("Hello, web!\n");
});
-const app = udsPath => {
+export const app = udsPath => {
server.listen(udsPath, () => {
console.log("I'm web.");
});
};
-
-module.exports = {
- app,
-};