diff options
| author | Kevin J Hoerr <kjhoerr@protonmail.com> | 2019-12-07 19:50:16 -0500 |
|---|---|---|
| committer | Kevin J Hoerr <kjhoerr@protonmail.com> | 2019-12-07 19:50:16 -0500 |
| commit | b614360c8a316e0933122ac1e3a631c0d0773a80 (patch) | |
| tree | 71ac2c0064d02cafcf53c3d63f3d02220275c63e | |
| parent | bd041baf0f3c9af7b331becc4c982ce5e835c054 (diff) | |
| download | ao-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.sample | 2 | ||||
| -rw-r--r-- | package-lock.json | 78 | ||||
| -rw-r--r-- | package.json | 2 | ||||
| -rw-r--r-- | src/index.ts | 286 | ||||
| -rw-r--r-- | src/metadata.ts | 54 |
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; |
