aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorKevin J Hoerr <kjhoerr@protonmail.com>2019-12-07 19:50:16 -0500
committerKevin J Hoerr <kjhoerr@protonmail.com>2019-12-07 19:50:16 -0500
commitb614360c8a316e0933122ac1e3a631c0d0773a80 (patch)
tree71ac2c0064d02cafcf53c3d63f3d02220275c63e
parentbd041baf0f3c9af7b331becc4c982ce5e835c054 (diff)
downloadao-coverage-b614360c8a316e0933122ac1e3a631c0d0773a80.tar.gz
ao-coverage-b614360c8a316e0933122ac1e3a631c0d0773a80.tar.bz2
ao-coverage-b614360c8a316e0933122ac1e3a631c0d0773a80.zip
Add Metadata core with MongoDB for persistence
With the new process dependency, process handling has been added to ensure that the ExpressJS server and MongoDB client connections get closed up properly. As noted in the Metadata file above the Branch interface, the schema is definitely not finalized. Eventually metadata will be needed at the repo level anyways, so reorganizing the document schema is high on the priority list.
-rw-r--r--.env.sample2
-rw-r--r--package-lock.json78
-rw-r--r--package.json2
-rw-r--r--src/index.ts286
-rw-r--r--src/metadata.ts54
5 files changed, 318 insertions, 104 deletions
diff --git a/.env.sample b/.env.sample
index 281d0ec..b3fdc29 100644
--- a/.env.sample
+++ b/.env.sample
@@ -2,3 +2,5 @@ PORT=3000
TOKEN=
HOST_DIR=/dist
UPLOAD_LIMIT=4194304
+MONGO_URI=mongodb://localhost:27017
+MONGO_DB=ao-coverage \ No newline at end of file
diff --git a/package-lock.json b/package-lock.json
index 0479af0..84cbad6 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -1,6 +1,6 @@
{
"name": "ao-coverage",
- "version": "1.0.0",
+ "version": "0.1.0",
"lockfileVersion": 1,
"requires": true,
"dependencies": {
@@ -13,6 +13,14 @@
"@types/node": "12.12.7"
}
},
+ "@types/bson": {
+ "version": "4.0.1",
+ "resolved": "https://registry.npmjs.org/@types/bson/-/bson-4.0.1.tgz",
+ "integrity": "sha512-K6VAEdLVJFBxKp8m5cRTbUfeZpuSvOuLKJLrgw9ANIXo00RiyGzgH4BKWWR4F520gV4tWmxG7q9sKQRVDuzrBw==",
+ "requires": {
+ "@types/node": "12.12.7"
+ }
+ },
"@types/connect": {
"version": "3.4.32",
"resolved": "https://registry.npmjs.org/@types/connect/-/connect-3.4.32.tgz",
@@ -62,6 +70,15 @@
"resolved": "https://registry.npmjs.org/@types/mime/-/mime-2.0.1.tgz",
"integrity": "sha512-FwI9gX75FgVBJ7ywgnq/P7tw+/o1GUbtP0KzbtusLigAOgIgNISRK0ZPl4qertvXSIE8YbsVJueQ90cDt9YYyw=="
},
+ "@types/mongodb": {
+ "version": "3.3.12",
+ "resolved": "https://registry.npmjs.org/@types/mongodb/-/mongodb-3.3.12.tgz",
+ "integrity": "sha512-gWIdrA8YKC4OetBk4eT5Zsp4p3oy/BJQKt80tXfgPnfBuLigumcmwNZseVSkLQJ3XkN/1OR0/kIunGWlew3rmQ==",
+ "requires": {
+ "@types/bson": "4.0.1",
+ "@types/node": "12.12.7"
+ }
+ },
"@types/node": {
"version": "12.12.7",
"resolved": "https://registry.npmjs.org/@types/node/-/node-12.12.7.tgz",
@@ -215,6 +232,11 @@
"resolved": "https://registry.npmjs.org/browser-process-hrtime/-/browser-process-hrtime-0.1.3.tgz",
"integrity": "sha512-bRFnI4NnjO6cnyLmOV/7PVoDEMJChlcfN0z4s1YMBY989/SvlfMI1lgCnkFUs53e9gQF+w7qu7XdllSTiSl8Aw=="
},
+ "bson": {
+ "version": "1.1.3",
+ "resolved": "https://registry.npmjs.org/bson/-/bson-1.1.3.tgz",
+ "integrity": "sha512-TdiJxMVnodVS7r0BdL42y/pqC9cL2iKynVwA0Ho3qbsQYr428veL3l7BQyuqiw+Q5SqqoT0m4srSY/BlZ9AxXg=="
+ },
"bytes": {
"version": "3.1.0",
"resolved": "https://registry.npmjs.org/bytes/-/bytes-3.1.0.tgz",
@@ -670,6 +692,12 @@
"resolved": "https://registry.npmjs.org/media-typer/-/media-typer-0.3.0.tgz",
"integrity": "sha1-hxDXrwqmJvj/+hzgAWhUUmMlV0g="
},
+ "memory-pager": {
+ "version": "1.5.0",
+ "resolved": "https://registry.npmjs.org/memory-pager/-/memory-pager-1.5.0.tgz",
+ "integrity": "sha512-ZS4Bp4r/Zoeq6+NLJpP+0Zzm0pR8whtGPf1XExKLJBAczGMnSi3It14OiNCStjQjM6NU1okjQGSxgEZN8eBYKg==",
+ "optional": true
+ },
"merge-descriptors": {
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/merge-descriptors/-/merge-descriptors-1.0.1.tgz",
@@ -698,6 +726,17 @@
"mime-db": "1.42.0"
}
},
+ "mongodb": {
+ "version": "3.3.5",
+ "resolved": "https://registry.npmjs.org/mongodb/-/mongodb-3.3.5.tgz",
+ "integrity": "sha512-6NAv5gTFdwRyVfCz+O+KDszvjpyxmZw+VlmqmqKR2GmpkeKrKFRv/ZslgTtZba2dc9JYixIf99T5Gih7TIWv7Q==",
+ "requires": {
+ "bson": "1.1.3",
+ "require_optional": "1.0.1",
+ "safe-buffer": "5.1.2",
+ "saslprep": "1.0.3"
+ }
+ },
"ms": {
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz",
@@ -892,6 +931,20 @@
}
}
},
+ "require_optional": {
+ "version": "1.0.1",
+ "resolved": "https://registry.npmjs.org/require_optional/-/require_optional-1.0.1.tgz",
+ "integrity": "sha512-qhM/y57enGWHAe3v/NcwML6a3/vfESLe/sGM2dII+gEO0BpKRUkWZow/tyloNqJyN6kXSl3RyyM8Ll5D/sJP8g==",
+ "requires": {
+ "resolve-from": "2.0.0",
+ "semver": "5.7.1"
+ }
+ },
+ "resolve-from": {
+ "version": "2.0.0",
+ "resolved": "https://registry.npmjs.org/resolve-from/-/resolve-from-2.0.0.tgz",
+ "integrity": "sha1-lICrIOlP+h2egKgEx+oUdhGWa1c="
+ },
"safe-buffer": {
"version": "5.1.2",
"resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.2.tgz",
@@ -902,6 +955,15 @@
"resolved": "https://registry.npmjs.org/safer-buffer/-/safer-buffer-2.1.2.tgz",
"integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg=="
},
+ "saslprep": {
+ "version": "1.0.3",
+ "resolved": "https://registry.npmjs.org/saslprep/-/saslprep-1.0.3.tgz",
+ "integrity": "sha512-/MY/PEMbk2SuY5sScONwhUDsV2p77Znkb/q3nSVstq/yQzYJOH/Azh29p9oJLsl3LnQwSvZDKagDGBsBwSooag==",
+ "optional": true,
+ "requires": {
+ "sparse-bitfield": "3.0.3"
+ }
+ },
"saxes": {
"version": "3.1.11",
"resolved": "https://registry.npmjs.org/saxes/-/saxes-3.1.11.tgz",
@@ -910,6 +972,11 @@
"xmlchars": "2.2.0"
}
},
+ "semver": {
+ "version": "5.7.1",
+ "resolved": "https://registry.npmjs.org/semver/-/semver-5.7.1.tgz",
+ "integrity": "sha512-sauaDf/PZdVgrLTNYHRtpXa1iRiKcaebiKQ1BJdpQlWH2lCvexQdX55snPFyK7QzpudqbCI0qXFfOasHdyNDGQ=="
+ },
"send": {
"version": "0.17.1",
"resolved": "https://registry.npmjs.org/send/-/send-0.17.1.tgz",
@@ -959,6 +1026,15 @@
"integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==",
"optional": true
},
+ "sparse-bitfield": {
+ "version": "3.0.3",
+ "resolved": "https://registry.npmjs.org/sparse-bitfield/-/sparse-bitfield-3.0.3.tgz",
+ "integrity": "sha1-/0rm5oZWBWuks+eSqzM004JzyhE=",
+ "optional": true,
+ "requires": {
+ "memory-pager": "1.5.0"
+ }
+ },
"sshpk": {
"version": "1.16.1",
"resolved": "https://registry.npmjs.org/sshpk/-/sshpk-1.16.1.tgz",
diff --git a/package.json b/package.json
index 9699d44..818b361 100644
--- a/package.json
+++ b/package.json
@@ -16,10 +16,12 @@
"dependencies": {
"@types/express": "^4.17.2",
"@types/jsdom": "^12.2.4",
+ "@types/mongodb": "^3.3.12",
"badgen": "3.0.1",
"dotenv": "8.2.0",
"express": "4.17.1",
"jsdom": "^15.2.1",
+ "mongodb": "^3.3.5",
"typescript": "^3.7.2"
},
"devDependencies": {
diff --git a/src/index.ts b/src/index.ts
index b6f41e5..4955ba6 100644
--- a/src/index.ts
+++ b/src/index.ts
@@ -2,16 +2,24 @@ import dotenv from "dotenv";
import express from "express";
import { JSDOM } from "jsdom";
import { badgen } from "badgen";
+import { MongoClient } from "mongodb";
import path from "path";
import fs from "fs";
import formats, { Format } from "./formats";
+import Metadata from "./metadata";
// Start-up configuration
dotenv.config();
const PORT = Number(process.env.PORT || 3000);
const TOKEN = process.env.TOKEN || "";
const UPLOAD_LIMIT = Number(process.env.UPLOAD_LIMIT || 4194304);
+const MONGO_URI =
+ process.env.MONGO_URI ||
+ (() => {
+ throw Error("MONGO_URI must be defined");
+ })();
+const MONGO_DB = process.env.MONGO_DB || "ao-coverage";
const HOST_DIR =
process.env.HOST_DIR ||
(() => {
@@ -23,111 +31,183 @@ if (!path.isAbsolute(HOST_DIR)) {
throw Error("HOST_DIR must be an absolute path");
}
-const app: express.Application = express();
-
-// Upload HTML file
-app.post("/v1/:org/:repo/:branch/:commit.html", (req, res) => {
- const { org, repo, branch, commit } = req.params;
- console.info(
- "POST request to /v1/%s/%s/%s/%s.html",
- org,
- repo,
- branch,
- commit
- );
-
- const { token, format } = req.query;
- //TODO @Metadata token should come from metadata
- if (token != TOKEN) {
- return res.status(401).send("Invalid token");
- }
+new MongoClient(MONGO_URI, { useUnifiedTopology: true }).connect(
+ (err, mongo) => {
+ if (err !== null) {
+ throw err;
+ }
- if (!formats.list_formats().includes(format)) {
- return res.status(406).send("Report format unknown");
- }
+ const app: express.Application = express();
+ const metadata = new Metadata(mongo.db(MONGO_DB));
+
+ // Upload HTML file
+ app.post("/v1/:org/:repo/:branch/:commit.html", (req, res) => {
+ const { org, repo, branch, commit } = req.params;
+ console.info(
+ "POST request to /v1/%s/%s/%s/%s.html",
+ org,
+ repo,
+ branch,
+ commit
+ );
+
+ const { token, format } = req.query;
+ //TODO @Metadata token should come from metadata
+ if (token != TOKEN) {
+ return res.status(401).send("Invalid token");
+ }
+
+ if (!formats.list_formats().includes(format)) {
+ return res.status(406).send("Report format unknown");
+ }
+
+ var contents = "";
+ req.on("data", raw => {
+ if (contents.length + raw.length > UPLOAD_LIMIT) {
+ res.status(413).send("Uploaded file is too large");
+ } else {
+ contents += raw;
+ }
+ });
+ req.on("end", () => {
+ let formatter: Format, coverage: number;
+ try {
+ const doc = new JSDOM(contents).window.document;
+ formatter = formats.get_format(format);
+ coverage = formatter.parse_coverage(doc);
+ } catch {
+ return res.status(400).send("Invalid report document");
+ }
+
+ const badge = badgen({
+ label: "coverage",
+ status: Math.floor(coverage).toString() + "%",
+ //TODO @Metadata stage values should come from metadata
+ color: formatter.match_color(coverage, 95, 80)
+ });
+
+ const report_path = path.join(HOST_DIR, org, repo, branch, commit);
+
+ fs.promises
+ .mkdir(report_path, { recursive: true })
+ .then(() =>
+ fs.promises.writeFile(path.join(report_path, "badge.svg"), badge)
+ )
+ .then(() =>
+ fs.promises.writeFile(
+ path.join(report_path, "index.html"),
+ contents
+ )
+ )
+ .then(() =>
+ metadata.updateBranch({ org, repo, name: branch, head: commit })
+ )
+ .then(result =>
+ result
+ ? res.status(200).send()
+ : res.status(500).send("Unknown error occurred")
+ );
+ });
+ });
- var contents = "";
- req.on("data", raw => {
- if (contents.length + raw.length > UPLOAD_LIMIT) {
- res.status(413).send("Uploaded file is too large");
- } else {
- contents += raw;
- }
- });
- req.on("end", () => {
- let formatter: Format, coverage: number;
- try {
- const doc = new JSDOM(contents).window.document;
- formatter = formats.get_format(format);
- coverage = formatter.parse_coverage(doc);
- } catch {
- return res.status(400).send("Invalid report document");
- }
+ app.get("/v1/:org/:repo/:branch.svg", (req, res) => {
+ const { org, repo, branch } = req.params;
+ console.info("GET request to /v1/%s/%s/%s.svg", org, repo, branch);
+
+ metadata.getHeadCommit(org, repo, branch).then(
+ commit => {
+ console.debug(
+ "Found commit %s for ORB %s/%s/%s",
+ commit,
+ org,
+ repo,
+ branch
+ );
+
+ res.sendFile(
+ path.join(HOST_DIR, org, repo, branch, commit, "badge.svg")
+ );
+ },
+ () => {
+ res.status(500).send("Unknown error occurred");
+ }
+ );
+ });
+
+ app.get("/v1/:org/:repo/:branch.html", (req, res) => {
+ const { org, repo, branch } = req.params;
+ console.info("GET request to /v1/%s/%s/%s.html", org, repo, branch);
+
+ metadata.getHeadCommit(org, repo, branch).then(
+ commit => {
+ console.debug(
+ "Found commit %s for ORB %s/%s/%s",
+ commit,
+ org,
+ repo,
+ branch
+ );
+
+ res.sendFile(
+ path.join(HOST_DIR, org, repo, branch, commit, "index.html")
+ );
+ },
+ () => {
+ res.status(500).send("Unknown error occurred");
+ }
+ );
+ });
+
+ // provide hard link for commit
+ app.get("/v1/:org/:repo/:branch/:commit.svg", (req, res) => {
+ const { org, repo, branch, commit } = req.params;
+ console.info(
+ "GET request to /v1/%s/%s/%s/%s.svg",
+ org,
+ repo,
+ branch,
+ commit
+ );
+
+ res.sendFile(path.join(HOST_DIR, org, repo, branch, commit, "badge.svg"));
+ });
+
+ // provide hard link for commit
+ app.get("/v1/:org/:repo/:branch/:commit.html", (req, res) => {
+ const { org, repo, branch, commit } = req.params;
+ console.info(
+ "GET request to /v1/%s/%s/%s/%s.html",
+ org,
+ repo,
+ branch,
+ commit
+ );
+
+ res.sendFile(
+ path.join(HOST_DIR, org, repo, branch, commit, "index.html")
+ );
+ });
- const badge = badgen({
- label: "coverage",
- status: Math.floor(coverage).toString() + "%",
- //TODO @Metadata stage values should come from metadata
- color: formatter.match_color(coverage, 95, 80)
+ const server = app.listen(PORT, () => {
+ console.log("Express started on port " + PORT);
});
- const report_path = path.join(HOST_DIR, org, repo, branch, commit);
-
- fs.promises
- .mkdir(report_path, { recursive: true })
- .then(() =>
- fs.promises.writeFile(path.join(report_path, "badge.svg"), badge)
- )
- .then(() =>
- fs.promises.writeFile(path.join(report_path, "index.html"), contents)
- )
- //TODO @Metadata set branch alias for badge / report file
- .then(() => res.status(200).send());
- });
-});
-
-app.get("/v1/:org/:repo/:branch.svg", (req, res) => {
- const { org, repo, branch } = req.params;
- console.info("GET request to /v1/%s/%s/%s.svg", org, repo, branch);
-
- //TODO @Metadata get the commit @@ via metadata
- const commit = "";
-
- return res.status(501).send();
-});
-
-app.get("/v1/:org/:repo/:branch.html", (req, res) => {
- const { org, repo, branch } = req.params;
- console.info("GET request to /v1/%s/%s/%s.html", org, repo, branch);
-
- //TODO @Metadata get the commit @@ via metadata
- const commit = "";
-
- return res.status(501).send();
-});
-
-// provide hard link for commit
-app.get("/v1/:org/:repo/:branch/:commit.svg", (req, res) => {
- const { org, repo, branch, commit } = req.params;
- console.info("GET request to /v1/%s/%s/%s/%s.svg", org, repo, branch, commit);
-
- res.sendFile(path.join(HOST_DIR, org, repo, branch, commit, "badge.svg"));
-});
-
-// provide hard link for commit
-app.get("/v1/:org/:repo/:branch/:commit.html", (req, res) => {
- const { org, repo, branch, commit } = req.params;
- console.info(
- "GET request to /v1/%s/%s/%s/%s.html",
- org,
- repo,
- branch,
- commit
- );
-
- res.sendFile(path.join(HOST_DIR, org, repo, branch, commit, "index.html"));
-});
-
-app.listen(PORT, () => {
- console.log("Express started on port " + PORT);
-});
+ // application exit handling
+ const handle_closure = (signal: NodeJS.Signals) => {
+ console.log("%s signal received. Closing shop.", signal);
+
+ mongo.close().then(() => {
+ console.log("Mongo client connection closed.");
+ server.close(() => {
+ console.log("Express down.");
+ process.exit();
+ });
+ });
+ };
+
+ const handle_codes: NodeJS.Signals[] = ["SIGTERM", "SIGHUP", "SIGINT"];
+ const process_event = (code: NodeJS.Signals) =>
+ process.on(code, handle_closure);
+ handle_codes.map(process_event);
+ }
+);
diff --git a/src/metadata.ts b/src/metadata.ts
new file mode 100644
index 0000000..1eb7f1a
--- /dev/null
+++ b/src/metadata.ts
@@ -0,0 +1,54 @@
+import { Db } from "mongodb";
+
+/** //FIXME fix document schema
+ * Rather than using branches as the core, this should be adopted into the following document model:
+ * repo:
+ * - org
+ * - name
+ * - token
+ * - branches: {
+ * [branchname]: {
+ * head
+ * }
+ * }
+ */
+export interface Branch {
+ org: string;
+ repo: string;
+ name: string;
+ head: string;
+}
+
+class Metadata {
+ database: Db;
+
+ constructor(client: Db) {
+ this.database = client;
+ }
+
+ async getHeadCommit(
+ org: string,
+ repo: string,
+ branch: string
+ ): Promise<string> {
+ const result = await this.database
+ .collection<Branch>("branch")
+ .findOne({ org, repo, name: branch });
+
+ if (result !== null) {
+ return result.head;
+ } else {
+ throw Error("Branch not found");
+ }
+ }
+
+ async updateBranch(branch: Branch): Promise<boolean> {
+ const { head, ...matcher } = branch;
+ const { result } = await this.database
+ .collection<Branch>("branch")
+ .replaceOne(matcher, branch, { upsert: true });
+ return result.ok === 1;
+ }
+}
+
+export default Metadata;