diff --git a/.editorconfig b/.editorconfig new file mode 100644 index 0000000..9782571 --- /dev/null +++ b/.editorconfig @@ -0,0 +1,7 @@ + +root = true + +[*] +indent_style = space +indent_size = 4 + diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..b512c09 --- /dev/null +++ b/.gitignore @@ -0,0 +1 @@ +node_modules \ No newline at end of file diff --git a/02/hello.js b/02/hello.js new file mode 100644 index 0000000..0a7616d --- /dev/null +++ b/02/hello.js @@ -0,0 +1,4 @@ +function hello(name) { + console.log(`Hello ${name}`); +} +hello("Node.js"); diff --git a/03-fortune/01-fortune-single.js b/03-fortune/01-fortune-single.js new file mode 100644 index 0000000..082bd7f --- /dev/null +++ b/03-fortune/01-fortune-single.js @@ -0,0 +1,4 @@ +const quote = + "Any app that can be written in JavaScript, will eventually be written in JavaScript. Jeff Atwood"; + +console.log(quote); diff --git a/03-fortune/02-fortune-single-multiline.js b/03-fortune/02-fortune-single-multiline.js new file mode 100644 index 0000000..c8e210e --- /dev/null +++ b/03-fortune/02-fortune-single-multiline.js @@ -0,0 +1,7 @@ +const quote = ` + Any app that can be written in JavaScript, + will eventually be written in JavaScript. + -- Jeff Atwood. +`; + +console.log(quote); diff --git a/03-fortune/03-fortune-multi.js b/03-fortune/03-fortune-multi.js new file mode 100644 index 0000000..69ad4b6 --- /dev/null +++ b/03-fortune/03-fortune-multi.js @@ -0,0 +1,18 @@ +const quotes = [ + `Any app that can be written in JavaScript, + will eventually be written in JavaScript. + -- Jeff Atwood. + `, + `JavaScript is the only language that I'm aware of + that people feel they don't need to learn before + they start using it. + -- Douglas Crockford + `, + `Code never lies, comments sometimes do. + -- Anonymous + `, +]; + +const randomIdx = Math.floor(Math.random() * quotes.length); + +console.log(quotes[randomIdx]); diff --git a/03-fortune/04-fortune-multi-json.js b/03-fortune/04-fortune-multi-json.js new file mode 100644 index 0000000..cc506eb --- /dev/null +++ b/03-fortune/04-fortune-multi-json.js @@ -0,0 +1,5 @@ +const quotes = require("./quotes.json"); + +const randomIdx = Math.floor(Math.random() * quotes.length); + +console.log(quotes[randomIdx]); diff --git a/03-fortune/05-read-file.js b/03-fortune/05-read-file.js new file mode 100644 index 0000000..9f21374 --- /dev/null +++ b/03-fortune/05-read-file.js @@ -0,0 +1,9 @@ +const fs = require("fs"); + +fs.readFile("./data/003.txt", "utf-8", (err, data) => { + if (err) { + console.log("Error while reading quote file"); + return; + } + console.log(data); +}); diff --git a/03-fortune/06-read-dir.js b/03-fortune/06-read-dir.js new file mode 100644 index 0000000..068da46 --- /dev/null +++ b/03-fortune/06-read-dir.js @@ -0,0 +1,9 @@ +const fs = require("fs"); + +fs.readdir("./data", (err, files) => { + if (err) { + console.log("Error while reading data directory"); + return; + } + console.log(files); +}); diff --git a/03-fortune/07-fortune-cb.js b/03-fortune/07-fortune-cb.js new file mode 100644 index 0000000..e375954 --- /dev/null +++ b/03-fortune/07-fortune-cb.js @@ -0,0 +1,21 @@ +const fs = require("fs"); + +const QUOTES_DIR = "./data"; + +fs.readdir(QUOTES_DIR, (err, files) => { + if (err) { + console.log("Error while reading data directory"); + return; + } + + const randomIdx = Math.floor(Math.random() * files.length); + const quoteFile = `${QUOTES_DIR}/${files[randomIdx]}`; + + fs.readFile(quoteFile, "utf-8", (err, data) => { + if (err) { + console.log("Error while reading quote file"); + return; + } + console.log(data.toString()); + }); +}); diff --git a/03-fortune/08-fortune-exit-code.js b/03-fortune/08-fortune-exit-code.js new file mode 100644 index 0000000..ce234fb --- /dev/null +++ b/03-fortune/08-fortune-exit-code.js @@ -0,0 +1,23 @@ +const fs = require("fs"); + +const QUOTES_DIR = "./data"; + +fs.readdir(QUOTES_DIR, (err, files) => { + if (err) { + console.log("Error while reading data directory"); + process.exitCode = 1; + return; + } + + const randomIdx = Math.floor(Math.random() * files.length); + const quoteFile = `${QUOTES_DIR}/${files[randomIdx]}`; + + fs.readFile(quoteFile, "utf-8", (err, data) => { + if (err) { + console.log("Error while reading quote file"); + process.exitCode = 1; + return; + } + console.log(data); + }); +}); diff --git a/03-fortune/09-fortune-console-error.js b/03-fortune/09-fortune-console-error.js new file mode 100644 index 0000000..f68efa1 --- /dev/null +++ b/03-fortune/09-fortune-console-error.js @@ -0,0 +1,23 @@ +const fs = require("fs"); + +const QUOTES_DIR = "./data"; + +fs.readdir(QUOTES_DIR, (err, files) => { + if (err) { + console.error(`Error while reading ${QUOTES_DIR} directory`); + process.exitCode = 1; + return; + } + + const randomIdx = Math.floor(Math.random() * files.length); + const quoteFile = `${QUOTES_DIR}/${files[randomIdx]}`; + + fs.readFile(quoteFile, "utf-8", (err, data) => { + if (err) { + console.error(`Error while reading ${quoteFile} file`); + process.exitCode = 1; + return; + } + console.log(data.toString()); + }); +}); diff --git a/03-fortune/10-fortune-txt-files-only.js b/03-fortune/10-fortune-txt-files-only.js new file mode 100644 index 0000000..6cb1846 --- /dev/null +++ b/03-fortune/10-fortune-txt-files-only.js @@ -0,0 +1,27 @@ +const fs = require("fs"); + +const QUOTES_DIR = "./data"; + +fs.readdir(QUOTES_DIR, { withFileTypes: true }, (err, files) => { + if (err) { + console.error(`Error while reading ${QUOTES_DIR} directory`); + process.exitCode = 1; + return; + } + + const txtFiles = files + .filter((f) => f.isFile() && f.name.endsWith(".txt")) + .map((f) => f.name); + + const randomIdx = Math.floor(Math.random() * txtFiles.length); + const quoteFile = `${QUOTES_DIR}/${txtFiles[randomIdx]}`; + + fs.readFile(quoteFile, "utf-8", (err, data) => { + if (err) { + console.error(`Error while reading ${quoteFile} file`); + process.exitCode = 1; + return; + } + console.log(data.toString()); + }); +}); diff --git a/03-fortune/11-fortune-argv.js b/03-fortune/11-fortune-argv.js new file mode 100644 index 0000000..36ec786 --- /dev/null +++ b/03-fortune/11-fortune-argv.js @@ -0,0 +1,27 @@ +const fs = require("fs"); + +const QUOTES_DIR = process.argv[2]; + +fs.readdir(QUOTES_DIR, { withFileTypes: true }, (err, files) => { + if (err) { + console.error(`Error while reading ${QUOTES_DIR} directory`); + process.exitCode = 1; + return; + } + + const txtFiles = files + .filter((f) => f.isFile() && f.name.endsWith(".txt")) + .map((f) => f.name); + + const randomIdx = Math.floor(Math.random() * txtFiles.length); + const quoteFile = `${QUOTES_DIR}/${txtFiles[randomIdx]}`; + + fs.readFile(quoteFile, "utf-8", (err, data) => { + if (err) { + console.error(`Error while reading ${quoteFile} file`); + process.exitCode = 1; + return; + } + console.log(data.toString()); + }); +}); diff --git a/03-fortune/12-fortune-promises.js b/03-fortune/12-fortune-promises.js new file mode 100644 index 0000000..2a4f940 --- /dev/null +++ b/03-fortune/12-fortune-promises.js @@ -0,0 +1,23 @@ +const fs = require("fs/promises"); + +const QUOTES_DIR = process.argv[2]; + +fs.readdir(QUOTES_DIR, { withFileTypes: true }) + .then((files) => { + const txtFiles = files + .filter((f) => f.isFile() && f.name.endsWith(".txt")) + .map((f) => f.name); + + const randomIdx = Math.floor(Math.random() * txtFiles.length); + const quoteFile = `${QUOTES_DIR}/${txtFiles[randomIdx]}`; + + return fs.readFile(quoteFile, "utf-8"); + }) + .then((data) => { + console.log(data.toString()); + }) + .catch((err) => { + console.error(`Error: ${err.message}`); + process.exitCode = 1; + return; + }); diff --git a/03-fortune/13-fortune-async-await.mjs b/03-fortune/13-fortune-async-await.mjs new file mode 100644 index 0000000..3e33b46 --- /dev/null +++ b/03-fortune/13-fortune-async-await.mjs @@ -0,0 +1,19 @@ +import fs from "fs/promises"; + +const QUOTES_DIR = process.argv[2]; + +try { + const files = await fs.readdir(QUOTES_DIR, { withFileTypes: true }); + const txtFiles = files + .filter((f) => f.isFile() && f.name.endsWith(".txt")) + .map((f) => f.name); + + const randomIdx = Math.floor(Math.random() * txtFiles.length); + const quoteFile = `${QUOTES_DIR}/${txtFiles[randomIdx]}`; + + const data = await fs.readFile(quoteFile, "utf-8"); + console.log(data.toString()); +} catch (err) { + console.error(`Error: ${err.message}`); + process.exitCode = 1; +} diff --git a/03-fortune/data/001.txt b/03-fortune/data/001.txt new file mode 100644 index 0000000..3b25a8a --- /dev/null +++ b/03-fortune/data/001.txt @@ -0,0 +1,3 @@ +Non si abita un Paese, si abita una lingua. +Una patria è questo, e niente altro. + -- Emil Cioran \ No newline at end of file diff --git a/03-fortune/data/002.txt b/03-fortune/data/002.txt new file mode 100644 index 0000000..322cd06 --- /dev/null +++ b/03-fortune/data/002.txt @@ -0,0 +1,3 @@ +Pubblicare un libro comporta lo stesso genere di noie +di un matrimonio o di un funerale. + -- Emil Cioran \ No newline at end of file diff --git a/03-fortune/data/003.txt b/03-fortune/data/003.txt new file mode 100644 index 0000000..0933476 --- /dev/null +++ b/03-fortune/data/003.txt @@ -0,0 +1,3 @@ +Un libro che lascia il lettore uguale +a com'era prima di leggerlo è un libro fallito. + -- Emil Cioran \ No newline at end of file diff --git a/03-fortune/data/004.txt b/03-fortune/data/004.txt new file mode 100644 index 0000000..c451813 --- /dev/null +++ b/03-fortune/data/004.txt @@ -0,0 +1,2 @@ +Si tira un aforisma come si tira uno schiaffo. + -- Emil Cioran \ No newline at end of file diff --git a/03-fortune/data/005.txt b/03-fortune/data/005.txt new file mode 100644 index 0000000..9a8ed66 --- /dev/null +++ b/03-fortune/data/005.txt @@ -0,0 +1,3 @@ +Non si scrive perché si ha qualcosa da dire +ma perché si ha voglia di dire qualcosa. + -- Emil Cioran diff --git a/03-fortune/data/006.txt b/03-fortune/data/006.txt new file mode 100644 index 0000000..92d8d71 --- /dev/null +++ b/03-fortune/data/006.txt @@ -0,0 +1,3 @@ +Il fatto che la vita non abbia alcun senso è una ragione di vivere +– la sola, del resto. + -- Emil Cioran \ No newline at end of file diff --git a/03-fortune/quotes.json b/03-fortune/quotes.json new file mode 100644 index 0000000..64c6ffd --- /dev/null +++ b/03-fortune/quotes.json @@ -0,0 +1,5 @@ +[ + "Any app that can be written in JavaScript,\nwill eventually be written in JavaScript.\n\t\t\t-- Jeff Atwood", + "JavaScript is the only language that I'm aware of\nthat people feel they don't need to learn before\nthey start using it.\n\t\t\t-- Douglas Crockford", + "Code never lies, comments sometimes do.\n\t\t\t-- Anonymous" +] diff --git a/04-library/01-min.js b/04-library/01-min.js new file mode 100644 index 0000000..9bde5f8 --- /dev/null +++ b/04-library/01-min.js @@ -0,0 +1,9 @@ +const http = require("http"); + +const server = http.createServer((req, res) => { + res.end("Benvenuto nella biblioteca HTTP"); +}); + +server.listen(3000, () => { + console.log("Server running"); +}); diff --git a/04-library/02-min-clear.js b/04-library/02-min-clear.js new file mode 100644 index 0000000..8ea6fa1 --- /dev/null +++ b/04-library/02-min-clear.js @@ -0,0 +1,13 @@ +const http = require("http"); + +const host = "127.0.0.1"; +const port = 3000; + +const server = http.createServer((req, res) => { + res.statusCode = 200; + res.end("Benvenuto nella biblioteca HTTP"); +}); + +server.listen(port, host, () => { + console.log(`Server running at http://${host}:${port}/`); +}); diff --git a/04-library/03-json.js b/04-library/03-json.js new file mode 100644 index 0000000..346eed0 --- /dev/null +++ b/04-library/03-json.js @@ -0,0 +1,13 @@ +const http = require("http"); + +const host = "127.0.0.1"; +const port = 3000; + +const server = http.createServer((req, res) => { + res.statusCode = 200; + res.end('{"message": "Benvenuto nella biblioteca HTTP"}'); +}); + +server.listen(port, host, () => { + console.log(`Server running at http://${host}:${port}/`); +}); diff --git a/04-library/04-json-clear.js b/04-library/04-json-clear.js new file mode 100644 index 0000000..b21486b --- /dev/null +++ b/04-library/04-json-clear.js @@ -0,0 +1,14 @@ +const http = require("http"); + +const host = "127.0.0.1"; +const port = 3000; + +const server = http.createServer((req, res) => { + res.statusCode = 200; + res.setHeader("Content-Type", "application/json"); + res.end(JSON.stringify({ message: "Benvenuto nella biblioteca HTTP" })); +}); + +server.listen(port, host, () => { + console.log(`Server running at http://${host}:${port}/`); +}); diff --git a/04-library/05-accept.js b/04-library/05-accept.js new file mode 100644 index 0000000..63c6b95 --- /dev/null +++ b/04-library/05-accept.js @@ -0,0 +1,26 @@ +const http = require("http"); + +const host = "127.0.0.1"; +const port = 3000; + +const server = http.createServer((req, res) => { + const acceptJson = req.headers.accept === "application/json"; + const acceptText = req.headers.accept === "text/plain"; + + if (acceptJson) { + res.statusCode = 200; + res.setHeader("Content-Type", "application/json"); + res.end(JSON.stringify({ message: "Benvenuto nella biblioteca HTTP" })); + } else if (acceptText) { + res.statusCode = 200; + res.setHeader("Content-Type", "text/plain"); + res.end("Benvenuto nella biblioteca HTTP"); + } else { + res.statusCode = 406; + res.end(); + } +}); + +server.listen(port, host, () => { + console.log(`Server running at http://${host}:${port}/`); +}); diff --git a/04-library/06-accept-types.js b/04-library/06-accept-types.js new file mode 100644 index 0000000..7257002 --- /dev/null +++ b/04-library/06-accept-types.js @@ -0,0 +1,36 @@ +const http = require("http"); + +const host = "127.0.0.1"; +const port = 3000; + +function resJson(res) { + res.statusCode = 200; + res.setHeader("Content-Type", "application/json"); + res.end(JSON.stringify({ message: "Benvenuto nella biblioteca HTTP" })); +} + +function resText(res) { + res.statusCode = 200; + res.setHeader("Content-Type", "text/plain"); + res.end("Benvenuto nella biblioteca HTTP"); +} + +const server = http.createServer((req, res) => { + const acceptJson = req.headers.accept === "application/json"; + const acceptText = req.headers.accept === "text/plain"; + const acceptAnyText = req.headers.accept === "text/*"; + const acceptAnyType = req.headers.accept === "*/*"; + + if (acceptJson) { + resJson(res); + } else if (acceptText || acceptAnyText || acceptAnyType) { + resText(res); + } else { + res.statusCode = 406; + res.end(); + } +}); + +server.listen(port, host, () => { + console.log(`Server running at http://${host}:${port}/`); +}); diff --git a/04-library/07-accept-complete.js b/04-library/07-accept-complete.js new file mode 100644 index 0000000..8dd47aa --- /dev/null +++ b/04-library/07-accept-complete.js @@ -0,0 +1,39 @@ +const http = require("http"); + +const host = "127.0.0.1"; +const port = 3000; + +function resJson(res) { + res.statusCode = 200; + res.setHeader("Content-Type", "application/json"); + res.end(JSON.stringify({ message: "Benvenuto nella biblioteca HTTP" })); +} +function resText(res) { + res.statusCode = 200; + res.setHeader("Content-Type", "text/plain"); + res.end("Benvenuto nella biblioteca HTTP"); +} +const server = http.createServer((req, res) => { + // ["text/html", "application/xml;q=0.9", ...] + const acceptList = req.headers.accept.split(","); + // ["text/html", "application/xml", ...] + const acceptedTypes = acceptList.map((a) => a.split(";")[0]); + + const acceptJson = acceptedTypes.includes("application/json"); + const acceptText = acceptedTypes.includes("text/plain"); + const acceptAnyText = acceptedTypes.includes("text/*"); + const acceptAnyType = acceptedTypes.includes("*/*"); + + if (acceptJson) { + resJson(res); + } else if (acceptText || acceptAnyText || acceptAnyType) { + resText(res); + } else { + res.statusCode = 406; + res.end(); + } +}); + +server.listen(port, host, () => { + console.log(`Server running at http://${host}:${port}/`); +}); diff --git a/04-library/08-accept-refactor.js b/04-library/08-accept-refactor.js new file mode 100644 index 0000000..df02540 --- /dev/null +++ b/04-library/08-accept-refactor.js @@ -0,0 +1,25 @@ +const http = require("http"); +const { getAcceptedTypes, resText, resJson } = require("./utils"); + +const host = "127.0.0.1"; +const port = 3000; + +const data = { + json: JSON.stringify({ message: "Benvenuto nella biblioteca HTTP" }), + text: "Benvenuto nella biblioteca HTTP", +}; +const server = http.createServer((req, res) => { + const accepts = getAcceptedTypes(req); + if (accepts.json) { + resJson(res, data.json); + } else if (accepts.textPlain || accepts.text || accepts.any) { + resText(res, data.text); + } else { + res.statusCode = 406; + res.end(); + } +}); + +server.listen(port, host, () => { + console.log(`Server running at http://${host}:${port}/`); +}); diff --git a/04-library/09-routing.js b/04-library/09-routing.js new file mode 100644 index 0000000..38d874d --- /dev/null +++ b/04-library/09-routing.js @@ -0,0 +1,59 @@ +const http = require("http"); +const { getAcceptedTypes, resJson, resText } = require("./utils"); + +const host = "127.0.0.1"; +const port = 3000; + +const library = { + message: "Benvenuto nella biblioteca HTTP", + books: [ + { author: "Naomi Klein", title: "Shock economy" }, + { author: "Serge Latouche", title: "L'invenzione dell'economia" }, + { + author: "Yanis Varoufakis", + title: "È l'economia che cambia il mondo", + }, + ], +}; + +const routes = { + "/": { + getText: () => library.message, + getJson: function () { + return JSON.stringify({ message: library.message }); + }, + }, + "/books": { + getText: () => + library.books.reduce((acc, cur) => { + acc += `${cur.author} - ${cur.title}\n`; + return acc; + }, ""), + getJson: function () { + return JSON.stringify(library.books); + }, + }, +}; + +const server = http.createServer((req, res) => { + const route = routes[req.url]; + if (!route) { + res.statusCode = 404; + res.end(); + return; + } + + const accepts = getAcceptedTypes(req); + if (accepts.json) { + resJson(res, route.getJson()); + } else if (accepts.textPlain || accepts.text || accepts.any) { + resText(res, route.getText()); + } else { + res.statusCode = 406; + res.end(); + } +}); + +server.listen(port, host, () => { + console.log(`Server running at http://${host}:${port}/`); +}); diff --git a/04-library/10-head.js b/04-library/10-head.js new file mode 100644 index 0000000..80336a1 --- /dev/null +++ b/04-library/10-head.js @@ -0,0 +1,67 @@ +const http = require("http"); +const { getAcceptedTypes, resJson, resText } = require("./utils"); + +const host = "127.0.0.1"; +const port = 3000; + +const library = { + message: "Benvenuto nella biblioteca HTTP", + books: [ + { author: "Naomi Klein", title: "Shock economy" }, + { author: "Serge Latouche", title: "L'invenzione dell'economia" }, + { + author: "Yanis Varoufakis", + title: "È l'economia che cambia il mondo", + }, + ], +}; + +const routes = { + "/": { + getText: () => library.message, + getJson: () => { + return JSON.stringify({ message: library.message }); + }, + }, + "/books": { + getText: () => + library.books.reduce((acc, cur) => { + acc += `${cur.author} - ${cur.title}\n`; + return acc; + }, ""), + getJson: () => { + return JSON.stringify(library.books); + }, + }, +}; + +const server = http.createServer((req, res) => { + const route = routes[req.url]; + if (!route) { + res.statusCode = 404; + res.end(); + return; + } + + if (req.method === "HEAD") { + res.statusCode = 204; + res.end(); + } else if (req.method === "GET") { + const accepts = getAcceptedTypes(req); + if (accepts.json) { + resJson(res, route.getJson()); + } else if (accepts.textPlain || accepts.text || accepts.any) { + resText(res, route.getText()); + } else { + res.statusCode = 406; + res.end(); + } + } else { + res.statusCode = 405; + res.end(); + } +}); + +server.listen(port, host, () => { + console.log(`Server running at http://${host}:${port}/`); +}); diff --git a/04-library/11-query-params.js b/04-library/11-query-params.js new file mode 100644 index 0000000..cff57df --- /dev/null +++ b/04-library/11-query-params.js @@ -0,0 +1,87 @@ +const http = require("http"); +const { getAcceptedTypes, resJson, resText } = require("./utils"); + +const host = "127.0.0.1"; +const port = 3000; + +const library = { + message: "Benvenuto nella biblioteca HTTP", + books: [ + { author: "Felix Martin", title: "Denaro" }, + { + author: "Yanis Varoufakis", + title: "È l'economia che cambia il mondo", + }, + { author: "Yanis Varoufakis", title: "Adulti nella stanza" }, + { author: "Naomi Klein", title: "Shock economy" }, + { author: "Serge Latouche", title: "L'invenzione dell'economia" }, + { author: "Thomas Piketty", title: "Il capitale nel XXI secolo" }, + ], +}; + +function findBooks(q) { + if (q) { + return library.books.filter( + (b) => b.author.includes(q) || b.title.includes(q) + ); + } else { + return library.books; + } +} + +const routes = { + "/": { + getText: () => library.message, + getJson: () => { + return JSON.stringify({ message: library.message }); + }, + }, + "/books": { + getText: (q) => { + return findBooks(q).reduce((acc, cur) => { + acc += `${cur.author} - ${cur.title}\n`; + return acc; + }, ""); + }, + getJson: (q) => { + return JSON.stringify(findBooks(q)); + }, + }, +}; + +const server = http.createServer((req, res) => { + const { pathname, searchParams } = new URL( + req.url, + `http://${req.headers.host}` + ); + + const route = routes[pathname]; + if (!route) { + res.statusCode = 404; + res.end(); + return; + } + + if (req.method === "HEAD") { + res.statusCode = 204; + res.end(); + } else if (req.method === "GET") { + const accepts = getAcceptedTypes(req); + const q = searchParams.get("q"); + if (accepts.json) { + resJson(res, route.getJson(q)); + } else if (accepts.textPlain || accepts.text || accepts.any) { + resText(res, route.getText(q)); + } else { + res.statusCode = 406; + res.end(); + } + } else { + res.statusCode = 405; + res.end(); + } +}); + +server.listen(port, host, () => { + console.log(`Server running at http://${host}:${port}/`); +}); diff --git a/04-library/12-html.js b/04-library/12-html.js new file mode 100644 index 0000000..7552730 --- /dev/null +++ b/04-library/12-html.js @@ -0,0 +1,120 @@ +const http = require("http"); +const { getAcceptedTypes, resJson, resText, resHtml } = require("./utils"); + +const host = "127.0.0.1"; +const port = 3000; + +const library = { + message: "Benvenuto nella biblioteca HTTP", + books: [ + { author: "Felix Martin", title: "Denaro" }, + { + author: "Yanis Varoufakis", + title: "È l'economia che cambia il mondo", + }, + { author: "Yanis Varoufakis", title: "Adulti nella stanza" }, + { author: "Naomi Klein", title: "Shock economy" }, + { author: "Serge Latouche", title: "L'invenzione dell'economia" }, + { author: "Thomas Piketty", title: "Il capitale nel XXI secolo" }, + ], +}; + +function findBooks(q) { + if (q) { + return library.books.filter( + (b) => b.author.includes(q) || b.title.includes(q) + ); + } else { + return library.books; + } +} + +function htmlLayout(title, body) { + return ` + + + + + ${title} + + + ${body} + +`; +} + +const routes = { + "/": { + getText: () => library.message, + getJson: () => { + return JSON.stringify({ message: library.message }); + }, + getHtml: function () { + return htmlLayout( + library.message, + `

${library.message}

Libri` + ); + }, + }, + "/books": { + getText: (q) => { + return findBooks(q).reduce((acc, cur) => { + acc += `${cur.author} - ${cur.title}\n`; + return acc; + }, ""); + }, + getJson: (q) => { + return JSON.stringify(findBooks(q)); + }, + getHtml: function (q) { + const booksLi = findBooks(q) + .map((b) => `
  • ${b.author} - ${b.title}
  • `) + .join(" "); + return htmlLayout( + "Libri", + ` + Home` + ); + }, + }, +}; + +const server = http.createServer((req, res) => { + const { pathname, searchParams } = new URL( + req.url, + `http://${req.headers.host}` + ); + + const route = routes[pathname]; + if (!route) { + res.statusCode = 404; + res.end(); + return; + } + + if (req.method === "HEAD") { + res.statusCode = 204; + res.end(); + } else if (req.method === "GET") { + const accepts = getAcceptedTypes(req); + const q = searchParams.get("q"); + + if (accepts.textHtml || accepts.any) { + resHtml(res, route.getHtml(q)); + } else if (accepts.json) { + resJson(res, route.getJson(q)); + } else if (accepts.textPlain || accepts.text) { + resText(res, route.getText(q)); + } else { + res.statusCode = 406; + res.end(); + } + } else { + res.statusCode = 405; + res.end(); + } +}); + +server.listen(port, host, () => { + console.log(`Server running at http://${host}:${port}/`); +}); diff --git a/04-library/13-keep-alive.js b/04-library/13-keep-alive.js new file mode 100644 index 0000000..313c446 --- /dev/null +++ b/04-library/13-keep-alive.js @@ -0,0 +1,12 @@ +const http = require("http"); + +const host = "127.0.0.1"; +const port = 3000; + +const server = http.createServer({ keepAliveTimeout: 50000 }, (req, res) => { + res.end("Keeping the connection alive!"); +}); + +server.listen(port, host, () => { + console.log(`Server running at http://${host}:${port}/`); +}); diff --git a/04-library/14-events.js b/04-library/14-events.js new file mode 100644 index 0000000..f683b10 --- /dev/null +++ b/04-library/14-events.js @@ -0,0 +1,15 @@ +const http = require("http"); + +const host = "127.0.0.1"; +const port = 3000; + +const server = http.createServer({ keepAliveTimeout: 50000 }); +server.on("connection", () => console.log("connection")); +server.on("request", (req, res) => { + console.log("request"); + res.end("Hello from the server"); +}); + +server.listen(port, host, () => { + console.log(`Server running at http://${host}:${port}/`); +}); diff --git a/04-library/utils.js b/04-library/utils.js new file mode 100644 index 0000000..0587647 --- /dev/null +++ b/04-library/utils.js @@ -0,0 +1,31 @@ +function resJson(res, data) { + res.statusCode = 200; + res.setHeader("Content-Type", "application/json"); + res.end(data); +} +function resText(res, data) { + res.statusCode = 200; + res.setHeader("Content-Type", "text/plain"); + res.end(data); + res.end(); +} +function resHtml(res, data) { + res.statusCode = 200; + res.setHeader("Content-Type", "text/html"); + res.end(data); +} +function getAcceptedTypes(req) { + const acceptHeaderVal = req.headers.accept || "*/*"; + // ["text/html", "application/xml;q=0.9", ...] + const acceptList = acceptHeaderVal.split(","); + // ["text/html", "application/xml", ...] + const acceptedTypes = acceptList.map((a) => a.split(";")[0]); + + const json = acceptedTypes.includes("application/json"); + const textPlain = acceptedTypes.includes("text/plain"); + const textHtml = acceptedTypes.includes("text/html"); + const text = acceptedTypes.includes("text/*"); + const any = acceptedTypes.includes("*/*"); + return { json, textPlain, textHtml, text, any }; +} +module.exports = { resJson, resText, resHtml, getAcceptedTypes }; diff --git a/05-chat/01-tcp-connection.js b/05-chat/01-tcp-connection.js new file mode 100644 index 0000000..8a406ce --- /dev/null +++ b/05-chat/01-tcp-connection.js @@ -0,0 +1,11 @@ +const net = require("net"); +const port = 5050; +const host = "127.0.0.1"; + +const server = net.createServer(); + +server.listen(port, host, () => { + console.log(`TCP server running at ${host} on port ${port}`); +}); + +server.on("connection", () => console.log("connection")); diff --git a/05-chat/02-tcp-echo.js b/05-chat/02-tcp-echo.js new file mode 100644 index 0000000..bd6285b --- /dev/null +++ b/05-chat/02-tcp-echo.js @@ -0,0 +1,15 @@ +const net = require("net"); +const port = 5050; +const host = "127.0.0.1"; + +const server = net.createServer(); + +server.listen(port, host, () => { + console.log(`TCP server running at ${host} on port ${port}`); +}); + +server.on("connection", function (sock) { + sock.on("data", function (data) { + sock.write("ECHO: " + data); + }); +}); diff --git a/05-chat/03-tcp-buffer.js b/05-chat/03-tcp-buffer.js new file mode 100644 index 0000000..6cd7219 --- /dev/null +++ b/05-chat/03-tcp-buffer.js @@ -0,0 +1,17 @@ +const net = require("net"); +const port = 5050; +const host = "127.0.0.1"; +const { inspect } = require("util"); + +const server = net.createServer(); + +server.listen(port, host, () => { + console.log(`TCP server running at ${host} on port ${port}`); +}); + +server.on("connection", function (sock) { + sock.on("data", function (data) { + sock.write(`Received ${inspect(data)}\n`); + sock.write("ECHO: " + data); + }); +}); diff --git a/05-chat/04-tcp-buffer-match.js b/05-chat/04-tcp-buffer-match.js new file mode 100644 index 0000000..8af2d6c --- /dev/null +++ b/05-chat/04-tcp-buffer-match.js @@ -0,0 +1,24 @@ +const net = require("net"); +const { inspect } = require("util"); +const { Buffer } = require("buffer"); + +const port = 5050; +const host = "127.0.0.1"; + +const server = net.createServer(); + +server.listen(port, host, () => { + console.log(`TCP server running at ${host} on port ${port}`); +}); + +server.on("connection", function (sock) { + sock.on("data", function (data) { + sock.write(`Received ${inspect(data)}\n`); + if (data.equals(Buffer.from("ping\x0d\x0a", "utf-8"))) { + sock.write("data matches ping\\x0d\\x0a\n"); + } + if (data.equals(Buffer.from("ping\r\n", "utf-8"))) { + sock.write("data matches ping\\r\\n\n"); + } + }); +}); diff --git a/05-chat/05-tcp-echo-binary.js b/05-chat/05-tcp-echo-binary.js new file mode 100644 index 0000000..9bcfe1b --- /dev/null +++ b/05-chat/05-tcp-echo-binary.js @@ -0,0 +1,16 @@ +const net = require("net"); + +const port = 5050; +const host = "127.0.0.1"; + +const server = net.createServer(); + +server.listen(port, host, () => { + console.log(`TCP server running at ${host} on port ${port}`); +}); + +server.on("connection", function (sock) { + sock.on("data", function (data) { + sock.write(data); + }); +}); diff --git a/05-chat/06-tcp-data-count.js b/05-chat/06-tcp-data-count.js new file mode 100644 index 0000000..75e329d --- /dev/null +++ b/05-chat/06-tcp-data-count.js @@ -0,0 +1,20 @@ +const net = require("net"); + +const port = 5050; +const host = "127.0.0.1"; + +const server = net.createServer(); + +server.listen(port, host, () => { + console.log(`TCP server running at ${host} on port ${port}`); +}); + +let dataCount = 0; +server.on("connection", function (sock) { + sock.on("data", function (data) { + dataCount++; + console.log( + `dataCount is ${dataCount} and data contains ${data.byteLength} bytes` + ); + }); +}); diff --git a/05-chat/07-chat-broadcasting.js b/05-chat/07-chat-broadcasting.js new file mode 100644 index 0000000..963ad58 --- /dev/null +++ b/05-chat/07-chat-broadcasting.js @@ -0,0 +1,16 @@ +const net = require("net"); +const port = 5050; +const host = "127.0.0.1"; + +const server = net.createServer(); +server.listen(port, host, () => { + console.log("TCP Server is running on port " + port + "."); +}); + +let sockets = []; +server.on("connection", function (sock) { + sockets.push(sock); + sock.on("data", function (data) { + sockets.forEach((sock) => sock.write(data)); + }); +}); diff --git a/05-chat/08-chat-address-port.js b/05-chat/08-chat-address-port.js new file mode 100644 index 0000000..1c437b9 --- /dev/null +++ b/05-chat/08-chat-address-port.js @@ -0,0 +1,18 @@ +const net = require("net"); +const port = 5050; +const host = "127.0.0.1"; + +const server = net.createServer(); +server.listen(port, host, () => { + console.log("TCP Server is running on port " + port + "."); +}); + +let sockets = []; +server.on("connection", function (sock) { + console.log(`CONNECTED: ${sock.remoteAddress}:${sock.remotePort}`); + sockets.push(sock); + sock.on("data", function (data) { + const sender = `${sock.remoteAddress}:${sock.remotePort}`; + sockets.forEach((s) => s.write(`<${sender}> ${data}`)); + }); +}); diff --git a/05-chat/09-chat-broadcast-fixed.js b/05-chat/09-chat-broadcast-fixed.js new file mode 100644 index 0000000..7ed0f92 --- /dev/null +++ b/05-chat/09-chat-broadcast-fixed.js @@ -0,0 +1,23 @@ +const net = require("net"); +const { isSameSocket } = require("./chat-utils"); +const port = 5050; +const host = "127.0.0.1"; + +const server = net.createServer(); +server.listen(port, host, () => { + console.log("TCP Server is running on port " + port + "."); +}); + +let sockets = []; +server.on("connection", function (sock) { + console.log(`CONNECTED: ${sock.remoteAddress}:${sock.remotePort}`); + sockets.push(sock); + sock.on("data", function (data) { + const sender = `${sock.remoteAddress}:${sock.remotePort}`; + sockets.forEach((s) => { + if (!isSameSocket(s, sock)) { + s.write(`<${sender}> ${data}`); + } + }); + }); +}); diff --git a/05-chat/10-chat-refactor.js b/05-chat/10-chat-refactor.js new file mode 100644 index 0000000..0b4b9f8 --- /dev/null +++ b/05-chat/10-chat-refactor.js @@ -0,0 +1,32 @@ +const net = require("net"); +const { + broadcastMessage, + getSocketsExcluding, + removeCRLF, +} = require("./chat-utils"); +const port = 5050; +const host = "127.0.0.1"; + +const server = net.createServer(); +server.listen(port, host, () => { + console.log("TCP Server is running on port " + port + "."); +}); + +let sockets = []; + +function processMessage(sock, message) { + const cleanMsg = removeCRLF(message); + const sender = `${sock.remoteAddress}:${sock.remotePort}`; + broadcastMessage( + getSocketsExcluding(sockets, sock), + `<${sender}> ${cleanMsg}\n` + ); +} + +server.on("connection", function (sock) { + console.log(`CONNECTED: ${sock.remoteAddress}:${sock.remotePort}`); + sockets.push(sock); + sock.on("data", function (data) { + processMessage(sock, data.toString()); + }); +}); diff --git a/05-chat/11-chat-nicks.js b/05-chat/11-chat-nicks.js new file mode 100644 index 0000000..20cd035 --- /dev/null +++ b/05-chat/11-chat-nicks.js @@ -0,0 +1,51 @@ +const net = require("net"); +const { + broadcastMessage, + getSocketsExcluding, + removeCRLF, + socketToId, +} = require("./chat-utils"); +const port = 5050; +const host = "127.0.0.1"; + +const server = net.createServer(); +server.listen(port, host, () => { + console.log("TCP Server is running on port " + port + "."); +}); + +let sockets = []; +let namesMap = {}; + +function setName(sock, name) { + namesMap[socketToId(sock)] = name; +} +function getName(sock) { + return namesMap[socketToId(sock)]; +} + +function processMessage(sock, message) { + const cleanMsg = removeCRLF(message); + + if (cleanMsg.startsWith("/nick ") /* space intended*/) { + const oldName = getName(sock); + const [_, name] = cleanMsg.split(" "); + setName(sock, name); + broadcastMessage(sockets, `${oldName} is now ${name}\n`); + } else { + broadcastMessage( + getSocketsExcluding(sockets, sock), + `<${getName(sock)}> ${cleanMsg}\n` + ); + } +} + +server.on("connection", function (sock) { + console.log(`CONNECTED: ${socketToId(sock)}`); + + sockets.push(sock); + setName(sock, socketToId(sock)); + + sock.on("data", function (data) { + processMessage(sock, data.toString()); + }); +}); diff --git a/05-chat/12-chat-colors.js b/05-chat/12-chat-colors.js new file mode 100644 index 0000000..e327523 --- /dev/null +++ b/05-chat/12-chat-colors.js @@ -0,0 +1,55 @@ +const net = require("net"); +const { + broadcastMessage, + getSocketsExcluding, + removeCRLF, + socketToId, + colorGrey, +} = require("./chat-utils"); +const port = 5050; +const host = "127.0.0.1"; + +const server = net.createServer(); +server.listen(port, host, () => { + console.log("TCP Server is running on port " + port + "."); +}); + +let sockets = []; +let namesMap = {}; + +function setName(sock, name) { + namesMap[socketToId(sock)] = name; +} +function getName(sock) { + return namesMap[socketToId(sock)]; +} + +function processMessage(sock, message) { + const cleanMsg = removeCRLF(message); + + if (cleanMsg.startsWith("/nick ") /* space intended*/) { + const oldName = getName(sock); + const [_, name] = cleanMsg.split(" "); + setName(sock, name); + broadcastMessage( + sockets, + `${colorGrey(`${oldName} is now ${name}`)}\n` + ); + } else { + broadcastMessage( + getSocketsExcluding(sockets, sock), + `<${getName(sock)}> ${cleanMsg}\n` + ); + } +} + +server.on("connection", function (sock) { + console.log(`CONNECTED: ${socketToId(sock)}`); + + sockets.push(sock); + setName(sock, socketToId(sock)); + + sock.on("data", function (data) { + processMessage(sock, data.toString()); + }); +}); diff --git a/05-chat/13-chat-pvt.js b/05-chat/13-chat-pvt.js new file mode 100644 index 0000000..e716648 --- /dev/null +++ b/05-chat/13-chat-pvt.js @@ -0,0 +1,66 @@ +const net = require("net"); +const { + broadcastMessage, + getSocketsExcluding, + removeCRLF, + socketToId, + colorGrey, + colorGreen, + parseNickMessage, + parsePvtMessage, +} = require("./chat-utils"); +const port = 5050; +const host = "127.0.0.1"; + +const server = net.createServer(); +server.listen(port, host, () => { + console.log("TCP Server is running on port " + port + "."); +}); + +let sockets = []; +let namesMap = {}; + +function setName(sock, name) { + namesMap[socketToId(sock)] = name; +} +function getName(sock) { + return namesMap[socketToId(sock)]; +} +function getSocketByName(sockets, name) { + return sockets.find((s) => getName(s) === name); +} + +function processMessage(sock, message) { + const cleanMsg = removeCRLF(message); + + if (cleanMsg.startsWith("/nick ") /* space intended*/) { + const oldName = getName(sock); + const name = parseNickMessage(cleanMsg); + setName(sock, name); + broadcastMessage( + sockets, + `${colorGrey(`${oldName} is now ${name}`)}\n` + ); + } else if (cleanMsg.startsWith("/pvt ") /* space intended*/) { + const [receiver, pvtMsg] = parsePvtMessage(cleanMsg); + const receiverSock = getSocketByName(sockets, receiver); + const preMsg = colorGreen(`(pvt msg from ${getName(sock)})`); + receiverSock.write(`${preMsg} ${pvtMsg}\n`); + } else { + broadcastMessage( + getSocketsExcluding(sockets, sock), + `<${getName(sock)}> ${cleanMsg}\n` + ); + } +} + +server.on("connection", function (sock) { + console.log(`CONNECTED: ${socketToId(sock)}`); + + sockets.push(sock); + setName(sock, socketToId(sock)); + + sock.on("data", function (data) { + processMessage(sock, data.toString()); + }); +}); diff --git a/05-chat/14-chat-list.js b/05-chat/14-chat-list.js new file mode 100644 index 0000000..d070d43 --- /dev/null +++ b/05-chat/14-chat-list.js @@ -0,0 +1,70 @@ +const net = require("net"); +const { + broadcastMessage, + getSocketsExcluding, + removeCRLF, + socketToId, + colorGrey, + colorGreen, + parseNickMessage, + parsePvtMessage, +} = require("./chat-utils"); +const port = 5050; +const host = "127.0.0.1"; + +const server = net.createServer(); +server.listen(port, host, () => { + console.log("TCP Server is running on port " + port + "."); +}); + +let sockets = []; +let namesMap = {}; + +function setName(sock, name) { + namesMap[socketToId(sock)] = name; +} +function getName(sock) { + return namesMap[socketToId(sock)]; +} +function getSocketByName(sockets, name) { + return sockets.find((s) => getName(s) === name); +} + +function processMessage(sock, message) { + const cleanMsg = removeCRLF(message); + + if (cleanMsg.startsWith("/nick ") /* space intended*/) { + const oldName = getName(sock); + const name = parseNickMessage(cleanMsg); + setName(sock, name); + broadcastMessage( + sockets, + `${colorGrey(`${oldName} is now ${name}`)}\n` + ); + } else if (cleanMsg.startsWith("/pvt ") /* space intended*/) { + const [receiver, pvtMsg] = parsePvtMessage(cleanMsg); + const receiverSock = getSocketByName(sockets, receiver); + const preMsg = colorGreen(`(pvt msg from ${getName(sock)})`); + receiverSock.write(`${preMsg} ${pvtMsg}\n`); + } else if (cleanMsg === "/list") { + const preMsg = colorGrey(`(only visible to you)`); + const usersString = sockets.map(getName).join(","); + sock.write(`${preMsg} Users are: ${usersString}\n`); + } else { + broadcastMessage( + getSocketsExcluding(sockets, sock), + `<${getName(sock)}> ${cleanMsg}\n` + ); + } +} + +server.on("connection", function (sock) { + console.log(`CONNECTED: ${socketToId(sock)}`); + + sockets.push(sock); + setName(sock, socketToId(sock)); + + sock.on("data", function (data) { + processMessage(sock, data.toString()); + }); +}); diff --git a/05-chat/15-chat-presence.js b/05-chat/15-chat-presence.js new file mode 100644 index 0000000..a602e4c --- /dev/null +++ b/05-chat/15-chat-presence.js @@ -0,0 +1,81 @@ +const net = require("net"); +const { + broadcastMessage, + getSocketsExcluding, + removeCRLF, + socketToId, + colorGrey, + colorGreen, + parseNickMessage, + parsePvtMessage, +} = require("./chat-utils"); +const port = 5050; +const host = "127.0.0.1"; + +const server = net.createServer(); +server.listen(port, host, () => { + console.log("TCP Server is running on port " + port + "."); +}); + +let sockets = []; +let namesMap = {}; + +function setName(sock, name) { + namesMap[socketToId(sock)] = name; +} +function getName(sock) { + return namesMap[socketToId(sock)]; +} +function getSocketByName(sockets, name) { + return sockets.find((s) => getName(s) === name); +} + +function processMessage(sock, message) { + const cleanMsg = removeCRLF(message); + + if (cleanMsg.startsWith("/nick ") /* space intended*/) { + const oldName = getName(sock); + const name = parseNickMessage(cleanMsg); + setName(sock, name); + broadcastMessage( + sockets, + `${colorGrey(`${oldName} is now ${name}`)}\n` + ); + } else if (cleanMsg.startsWith("/pvt ") /* space intended*/) { + const [receiver, pvtMsg] = parsePvtMessage(cleanMsg); + const receiverSock = getSocketByName(sockets, receiver); + const preMsg = colorGreen(`(pvt msg from ${getName(sock)})`); + receiverSock.write(`${preMsg} ${pvtMsg}\n`); + } else if (cleanMsg === "/list") { + const preMsg = colorGrey(`(only visible to you)`); + const usersString = sockets.map(getName).join(","); + sock.write(`${preMsg} Users are: ${usersString}\n`); + } else { + broadcastMessage( + getSocketsExcluding(sockets, sock), + `<${getName(sock)}> ${cleanMsg}\n` + ); + } +} +const joinedMessage = (sock) => + `${colorGrey(`${getName(sock)} joined the chat`)}\n`; +const leftMessage = (sock) => + `${colorGrey(`${getName(sock)} left the chat`)}\n`; + +server.on("connection", function (sock) { + console.log(`CONNECTED: ${socketToId(sock)}`); + + sockets.push(sock); + setName(sock, socketToId(sock)); + broadcastMessage(sockets, joinedMessage(sock)); + + sock.on("data", function (data) { + processMessage(sock, data.toString()); + }); + + sock.on("close", function () { + sockets = getSocketsExcluding(sockets, sock); + broadcastMessage(sockets, leftMessage(sock)); + console.log("CLOSED: " + socketToId(sock)); + }); +}); diff --git a/05-chat/16-events.js b/05-chat/16-events.js new file mode 100644 index 0000000..adb2172 --- /dev/null +++ b/05-chat/16-events.js @@ -0,0 +1,8 @@ +const EventEmitter = require("events"); + +const emitter = new EventEmitter(); +emitter.on("event", () => { + console.log("an event occurred!"); +}); + +emitter.emit("event"); diff --git a/05-chat/17-clock.js b/05-chat/17-clock.js new file mode 100644 index 0000000..01a3a56 --- /dev/null +++ b/05-chat/17-clock.js @@ -0,0 +1,19 @@ +const EventEmitter = require("events"); + +function getTimeString() { + const now = new Date(); + const time = `${now.getHours()}:${now.getMinutes()}:${now.getSeconds()}:${now.getMilliseconds()}`; + return time; +} + +class Clock extends EventEmitter {} + +const clock = new Clock(); + +clock.on("tick", () => { + console.log(`The clock ticked: ${getTimeString()}`); +}); + +setInterval(() => { + clock.emit("tick"); +}, 1000); diff --git a/05-chat/18-clock-sync.js b/05-chat/18-clock-sync.js new file mode 100644 index 0000000..0e17c4d --- /dev/null +++ b/05-chat/18-clock-sync.js @@ -0,0 +1,26 @@ +const EventEmitter = require("events"); + +function getTimeString() { + const now = new Date(); + const time = `${now.getHours()}:${now.getMinutes()}:${now.getSeconds()}:${now.getMilliseconds()}`; + return time; +} + +class Clock extends EventEmitter {} + +const clock = new Clock(); + +clock.on("tick", () => { + console.log(`[1] The clock ticked ${getTimeString()}`); +}); +clock.on("tick", () => { + console.log(`[2] The clock ticked ${getTimeString()}`); +}); +clock.on("tick", () => { + console.log(`[3] The clock ticked ${getTimeString()}`); +}); + +setInterval(() => { + clock.emit("tick"); + console.log(`Post tick at ${getTimeString()}`); +}, 1000); diff --git a/05-chat/19-clock-async.js b/05-chat/19-clock-async.js new file mode 100644 index 0000000..406090d --- /dev/null +++ b/05-chat/19-clock-async.js @@ -0,0 +1,33 @@ +const EventEmitter = require("events"); + +function getTimeString() { + const now = new Date(); + const time = `${now.getHours()}:${now.getMinutes()}:${now.getSeconds()}:${now.getMilliseconds()}`; + return time; +} + +class Clock extends EventEmitter {} + +const clock = new Clock(); + +clock.on("tick", () => { + setImmediate(() => { + console.log(`[1] The clock ticked ${getTimeString()}`); + }); +}); +clock.on("tick", () => { + setImmediate(() => { + console.log(`[2] The clock ticked ${getTimeString()}`); + }); +}); + +clock.on("tick", () => { + setImmediate(() => { + console.log(`[3] The clock ticked ${getTimeString()}`); + }); +}); + +setInterval(() => { + clock.emit("tick"); + console.log(`Post tick at ${getTimeString()}`); +}, 1000); diff --git a/05-chat/20-clock-broken.js b/05-chat/20-clock-broken.js new file mode 100644 index 0000000..137f2df --- /dev/null +++ b/05-chat/20-clock-broken.js @@ -0,0 +1,24 @@ +const EventEmitter = require("events"); + +function getTimeString() { + const now = new Date(); + const time = `${now.getHours()}:${now.getMinutes()}:${now.getSeconds()}:${now.getMilliseconds()}`; + return time; +} + +class Clock extends EventEmitter {} +const clock = new Clock(); + +clock.on("tick", () => { + console.log(`The clock ticked ${getTimeString()}`); +}); +clock.on("error", (err) => console.log(`Got ${err}`)); + +setInterval(() => { + clock.emit("tick"); + console.log(`Post tick at ${getTimeString()}`); +}, 1000); + +setTimeout(() => { + clock.emit("error", new Error("An error")); +}, 3000); diff --git a/05-chat/21-clock-broken-async.js b/05-chat/21-clock-broken-async.js new file mode 100644 index 0000000..821d429 --- /dev/null +++ b/05-chat/21-clock-broken-async.js @@ -0,0 +1,20 @@ +const EventEmitter = require("events"); + +function getTimeString() { + const now = new Date(); + const time = `${now.getHours()}:${now.getMinutes()}:${now.getSeconds()}:${now.getMilliseconds()}`; + return time; +} + +class Clock extends EventEmitter {} +const clock = new Clock({ captureRejections: true }); + +clock.on("tick", async () => { + throw new Error("Broken listener"); +}); +clock.on("error", (err) => console.log(`Got ${err}`)); + +setInterval(() => { + clock.emit("tick"); + console.log(`Post tick at ${getTimeString()}`); +}, 1000); diff --git a/05-chat/22-chat-lurkers.js b/05-chat/22-chat-lurkers.js new file mode 100644 index 0000000..597e5d1 --- /dev/null +++ b/05-chat/22-chat-lurkers.js @@ -0,0 +1,92 @@ +const net = require("net"); +const { + broadcastMessage, + getSocketsExcluding, + removeCRLF, + socketToId, + colorGrey, + colorGreen, + parseNickMessage, + parsePvtMessage, +} = require("./chat-utils"); +const LurkersDetector = require("./lurkers-detector"); +const port = 5050; +const host = "127.0.0.1"; + +const server = net.createServer(); +server.listen(port, host, () => { + console.log("TCP Server is running on port " + port + "."); +}); + +let sockets = []; +let namesMap = {}; +let ld = new LurkersDetector(30); +ld.on("lurker detected", (name) => { + console.log(`${name} is a lurker!`); + const sock = getSocketByName(sockets, name); + sock.resetAndDestroy(); +}); + +function setName(sock, name) { + namesMap[socketToId(sock)] = name; +} +function getName(sock) { + return namesMap[socketToId(sock)]; +} +function getSocketByName(sockets, name) { + return sockets.find((s) => getName(s) === name); +} + +function processMessage(sock, message) { + const cleanMsg = removeCRLF(message); + + if (cleanMsg.startsWith("/nick ") /* space intended*/) { + const oldName = getName(sock); + const name = parseNickMessage(cleanMsg); + setName(sock, name); + broadcastMessage( + sockets, + `${colorGrey(`${oldName} is now ${name}`)}\n` + ); + ld.renameUser(oldName, name); + } else if (cleanMsg.startsWith("/pvt ") /* space intended*/) { + const [receiver, pvtMsg] = parsePvtMessage(cleanMsg); + const receiverSock = getSocketByName(sockets, receiver); + const preMsg = colorGreen(`(pvt msg from ${getName(sock)})`); + receiverSock.write(`${preMsg} ${pvtMsg}\n`); + } else if (cleanMsg === "/list") { + const preMsg = colorGrey(`(only visible to you)`); + const usersString = sockets.map(getName).join(","); + sock.write(`${preMsg} Users are: ${usersString}\n`); + } else { + broadcastMessage( + getSocketsExcluding(sockets, sock), + `<${getName(sock)}> ${cleanMsg}\n` + ); + } +} +const joinedMessage = (sock) => + `${colorGrey(`${getName(sock)} joined the chat`)}\n`; +const leftMessage = (sock) => + `${colorGrey(`${getName(sock)} left the chat`)}\n`; + +server.on("connection", function (sock) { + console.log(`CONNECTED: ${socketToId(sock)}`); + + sockets.push(sock); + setName(sock, socketToId(sock)); + broadcastMessage(sockets, joinedMessage(sock)); + ld.addUser(socketToId(sock)); + + sock.on("data", function (data) { + ld.touchUser(socketToId(sock)); + processMessage(sock, data.toString()); + }); + + sock.on("close", function () { + sockets = getSocketsExcluding(sockets, sock); + broadcastMessage(sockets, leftMessage(sock)); + console.log("CLOSED: " + socketToId(sock)); + ld.removeUser(socketToId(sock)); + }); +}); diff --git a/05-chat/chat-utils.js b/05-chat/chat-utils.js new file mode 100644 index 0000000..5adb454 --- /dev/null +++ b/05-chat/chat-utils.js @@ -0,0 +1,44 @@ +function isSameSocket(s1, s2) { + return ( + s1.remoteAddress === s2.remoteAddress && s1.remotePort === s2.remotePort + ); +} +function broadcastMessage(sockets, message) { + sockets.forEach((sock) => sock.write(message)); +} +function getSocketsExcluding(sockets, sock) { + return sockets.filter((s) => !isSameSocket(s, sock)); +} +function removeCRLF(str) { + return str.replace(/[\r\n]+$/, ""); +} +function socketToId(sock) { + return `${sock.remoteAddress}:${sock.remotePort}`; +} +function colorGrey(str) { + return `\x1b[97;100m${str}\x1b[0m`; +} +function colorGreen(str) { + return `\x1b[97;42m${str}\x1b[0m`; +} +function parsePvtMessage(msg) { + const [_, receiver, ...rest] = msg.split(" "); + const pvtMsg = rest.join(" "); + return [receiver, pvtMsg]; +} +function parseNickMessage(msg) { + const [_, name] = msg.split(" "); + return name; +} + +module.exports = { + isSameSocket, + broadcastMessage, + getSocketsExcluding, + removeCRLF, + socketToId, + colorGrey, + colorGreen, + parseNickMessage, + parsePvtMessage, +}; diff --git a/05-chat/lurkers-detector.js b/05-chat/lurkers-detector.js new file mode 100644 index 0000000..38354a4 --- /dev/null +++ b/05-chat/lurkers-detector.js @@ -0,0 +1,28 @@ +const EventEmitter = require("events"); + +class LurkersDetector extends EventEmitter { + constructor(tSec) { + super(); + this.users = {}; + this.timeoutMS = tSec * 1000; + } + addUser(name) { + this.users[name] = setTimeout(() => { + this.emit("lurker detected", name); + }, this.timeoutMS); + } + touchUser(name) { + clearTimeout(this.users[name]); + this.addUser(name); + } + renameUser(oldName, newName) { + this.removeUser(oldName); + this.addUser(newName); + } + removeUser(name) { + clearTimeout(this.users[name]); + delete this.users[name]; + } +} + +module.exports = LurkersDetector; diff --git a/06-www/01-w3-server.mjs b/06-www/01-w3-server.mjs new file mode 100644 index 0000000..663404b --- /dev/null +++ b/06-www/01-w3-server.mjs @@ -0,0 +1,25 @@ +import http from "http"; +import fs from "fs/promises"; + +const host = "127.0.0.1"; +const port = 3000; +const root = "files"; + +const server = http.createServer(); +server.on("request", async (req, res) => { + const { pathname } = new URL(req.url, `http://${req.headers.host}`); + const file = `${root}${pathname}`; + console.log(`Requested ${file} file`); + try { + const data = await fs.readFile(file); + res.end(data); + } catch (e) { + console.error(e); + res.statusCode = 404; + res.end(); + } +}); + +server.listen(port, host, () => { + console.log(`Web server running at http://${host}:${port}/`); +}); diff --git a/06-www/02-w3-media-types.mjs b/06-www/02-w3-media-types.mjs new file mode 100644 index 0000000..fb1d425 --- /dev/null +++ b/06-www/02-w3-media-types.mjs @@ -0,0 +1,34 @@ +import http from "http"; +import fs from "fs/promises"; +import mimeMap from "./media-types.mjs"; +import { extname } from "path"; + +const host = "127.0.0.1"; +const port = 3000; +const root = "files"; + +const server = http.createServer(); +server.on("request", async (req, res) => { + const { pathname } = new URL(req.url, `http://${req.headers.host}`); + const file = `${root}${pathname}`; + + console.log(`Requested ${file} file`); + + const mimeType = mimeMap.get(extname(pathname)); + if (mimeType) { + res.setHeader("Content-Type", mimeType); + } + + try { + const data = await fs.readFile(file); + res.end(data); + } catch (e) { + console.error(e); + res.statusCode = 404; + res.end(); + } +}); + +server.listen(port, host, () => { + console.log(`Web server running at http://${host}:${port}/`); +}); diff --git a/06-www/03-w3-head.mjs b/06-www/03-w3-head.mjs new file mode 100644 index 0000000..92cd08b --- /dev/null +++ b/06-www/03-w3-head.mjs @@ -0,0 +1,61 @@ +import http from "http"; +import fs from "fs/promises"; +import mimeMap from "./media-types.mjs"; +import { extname } from "path"; + +const host = "127.0.0.1"; +const port = 3000; +const root = "files"; + +const server = http.createServer(); +server.on("request", async (req, res) => { + const [isGET, isHEAD] = [req.method === "GET", req.method === "HEAD"]; + if (!isGET && !isHEAD) { + res.statusCode = 405; + res.end(); + return; + } + + const { pathname } = new URL(req.url, `http://${req.headers.host}`); + const file = `${root}${pathname}`; + console.log(`Requested ${file} file`); + + let fh; + try { + fh = await fs.open(file); + } catch (e) { + console.error(e); + res.statusCode = 404; + res.end(); + return; + } + + const mimeType = mimeMap.get(extname(pathname)); + if (mimeType) { + res.setHeader("Content-Type", mimeType); + } + + if (isHEAD) { + const fileStat = await fh.stat(); + res.setHeader("Content-Length", fileStat.size); + res.statusCode = 200; + res.end(); + await fh.close(); + return; + } + + try { + const data = await fh.readFile(); + res.end(data); + } catch (e) { + console.error(e); + res.statusCode = 500; + res.end(); + } finally { + await fh.close(); + } +}); + +server.listen(port, host, () => { + console.log(`Web server running at http://${host}:${port}/`); +}); diff --git a/06-www/04-w3-stream.mjs b/06-www/04-w3-stream.mjs new file mode 100644 index 0000000..74b8dfd --- /dev/null +++ b/06-www/04-w3-stream.mjs @@ -0,0 +1,59 @@ +import http from "http"; +import fs from "fs/promises"; +import mimeMap from "./media-types.mjs"; +import { extname } from "path"; + +const host = "127.0.0.1"; +const port = 3000; +const root = "files"; + +const server = http.createServer(); +server.on("request", async (req, res) => { + const [isGET, isHEAD] = [req.method === "GET", req.method === "HEAD"]; + if (!isGET && !isHEAD) { + res.statusCode = 405; + res.end(); + return; + } + + const { pathname } = new URL(req.url, `http://${req.headers.host}`); + const file = `${root}${pathname}`; + console.log(`Requested ${file} file`); + + let fh; + try { + fh = await fs.open(file); + } catch (e) { + console.error(e); + res.statusCode = 404; + res.end(); + return; + } + + const mimeType = mimeMap.get(extname(pathname)); + if (mimeType) { + res.setHeader("Content-Type", mimeType); + } + + if (isHEAD) { + const fileStat = await fh.stat(); + res.setHeader("Content-Length", fileStat.size); + res.statusCode = 200; + res.end(); + await fh.close(); + return; + } + + const fileStream = fh.createReadStream(); + fileStream.on("data", (chunk) => res.write(chunk)); + fileStream.on("end", () => res.end()); + fileStream.on("error", (e) => { + console.error(e); + res.statusCode = 500; + res.end(); + }); +}); + +server.listen(port, host, () => { + console.log(`Web server running at http://${host}:${port}/`); +}); diff --git a/06-www/05-stream-tcp-logger.mjs b/06-www/05-stream-tcp-logger.mjs new file mode 100644 index 0000000..c742fba --- /dev/null +++ b/06-www/05-stream-tcp-logger.mjs @@ -0,0 +1,11 @@ +import net from "net"; + +const server = net.createServer(); + +server.listen(5050, "127.0.0.1"); + +server.on("connection", function (sock) { + sock.on("data", function (data) { + console.log("Received data event with: " + data); + }); +}); diff --git a/06-www/06-stream-flowing-paused.mjs b/06-www/06-stream-flowing-paused.mjs new file mode 100644 index 0000000..2fdcc2f --- /dev/null +++ b/06-www/06-stream-flowing-paused.mjs @@ -0,0 +1,13 @@ +import fs from "fs/promises"; + +const fh = await fs.open("06-stream-flowing-paused.mjs"); +const fileStream = fh.createReadStream(); + +console.log(`Is Readable flowing? ${fileStream.readableFlowing}`); +console.log("Waiting 2 seconds before adding the data event listener..."); + +setTimeout(() => { + console.log(`Is Readable flowing? ${fileStream.readableFlowing}`); + fileStream.on("data", (chunk) => console.log(chunk)); + console.log(`Is Readable flowing? ${fileStream.readableFlowing}`); +}, 2000); diff --git a/06-www/07-w3-writable-logger.mjs b/06-www/07-w3-writable-logger.mjs new file mode 100644 index 0000000..b28d4e2 --- /dev/null +++ b/06-www/07-w3-writable-logger.mjs @@ -0,0 +1,61 @@ +import http from "http"; +import fs from "fs/promises"; +import mimeMap from "./media-types.mjs"; +import { extname } from "path"; +import { log } from "./logger.mjs"; + +const host = "127.0.0.1"; +const port = 3000; +const root = "files"; + +const server = http.createServer(); +server.on("request", async (req, res) => { + const [isGET, isHEAD] = [req.method === "GET", req.method === "HEAD"]; + if (!isGET && !isHEAD) { + res.statusCode = 405; + res.end(); + return; + } + + const { pathname } = new URL(req.url, `http://${req.headers.host}`); + const file = `${root}${pathname}`; + + log(`Requested ${file} file`); + + let fh; + try { + fh = await fs.open(file); + } catch (e) { + console.error(e); + res.statusCode = 404; + res.end(); + return; + } + + const mimeType = mimeMap.get(extname(pathname)); + if (mimeType) { + res.setHeader("Content-Type", mimeType); + } + + if (isHEAD) { + const fileStat = await fh.stat(); + res.setHeader("Content-Length", fileStat.size); + res.statusCode = 200; + res.end(); + await fh.close(); + return; + } + + const fileStream = fh.createReadStream(); + fileStream.on("data", (chunk) => res.write(chunk)); + fileStream.on("end", () => res.end()); + fileStream.on("error", (e) => { + console.error(e); + res.statusCode = 500; + res.end(); + }); +}); + +server.listen(port, host, () => { + console.log(`Web server running at http://${host}:${port}/`); +}); diff --git a/06-www/08-w3-transform-gzip.mjs b/06-www/08-w3-transform-gzip.mjs new file mode 100644 index 0000000..4587bb5 --- /dev/null +++ b/06-www/08-w3-transform-gzip.mjs @@ -0,0 +1,63 @@ +import http from "http"; +import fs from "fs/promises"; +import mimeMap from "./media-types.mjs"; +import { extname } from "path"; +import { log } from "./logger.mjs"; +import { createGzip } from "zlib"; + +const host = "127.0.0.1"; +const port = 3000; +const root = "files"; + +const server = http.createServer(); +server.on("request", async (req, res) => { + const [isGET, isHEAD] = [req.method === "GET", req.method === "HEAD"]; + if (!isGET && !isHEAD) { + res.statusCode = 405; + res.end(); + return; + } + + const { pathname } = new URL(req.url, `http://${req.headers.host}`); + const file = `${root}${pathname}`; + + log(`Requested ${file} file`); + + let fh; + try { + fh = await fs.open(file); + } catch (e) { + console.error(e); + res.statusCode = 404; + res.end(); + return; + } + + const mimeType = mimeMap.get(extname(pathname)); + if (mimeType) { + res.setHeader("Content-Type", mimeType); + } + + if (isHEAD) { + const fileStat = await fh.stat(); + res.setHeader("Content-Length", fileStat.size); + res.statusCode = 200; + res.end(); + await fh.close(); + return; + } + + res.setHeader("Content-Encoding", "gzip"); + const fileStream = fh.createReadStream(); + const gzipTransform = createGzip(); + fileStream.pipe(gzipTransform).pipe(res); + fileStream.on("error", (e) => { + console.error(e); + res.statusCode = 500; + res.end(); + }); +}); + +server.listen(port, host, () => { + console.log(`Web server running at http://${host}:${port}/`); +}); diff --git a/06-www/09-w3-pipe-errors.mjs b/06-www/09-w3-pipe-errors.mjs new file mode 100644 index 0000000..a44165a --- /dev/null +++ b/06-www/09-w3-pipe-errors.mjs @@ -0,0 +1,68 @@ +import http from "http"; +import fs from "fs/promises"; +import mimeMap from "./media-types.mjs"; +import { extname } from "path"; +import { log } from "./logger.mjs"; +import { createGzip } from "zlib"; + +const host = "127.0.0.1"; +const port = 3000; +const root = "files"; + +const server = http.createServer(); +server.on("request", async (req, res) => { + const [isGET, isHEAD] = [req.method === "GET", req.method === "HEAD"]; + if (!isGET && !isHEAD) { + res.statusCode = 405; + res.end(); + return; + } + + const { pathname } = new URL(req.url, `http://${req.headers.host}`); + const file = `${root}${pathname}`; + + log(`Requested ${file} file`); + + let fh; + try { + fh = await fs.open(file); + } catch (e) { + console.error(e); + res.statusCode = 404; + res.end(); + return; + } + + const mimeType = mimeMap.get(extname(pathname)); + if (mimeType) { + res.setHeader("Content-Type", mimeType); + } + + if (isHEAD) { + const fileStat = await fh.stat(); + res.setHeader("Content-Length", fileStat.size); + res.statusCode = 200; + res.end(); + await fh.close(); + return; + } + + res.setHeader("Content-Encoding", "gzip"); + const fileStream = fh.createReadStream(); + const gzipTransform = createGzip(); + const handleStreamError = (err) => { + console.error(err); + fileStream.destroy(); + gzipTransform.destroy(); + res.statusCode = 500; + }; + fileStream + .on("error", handleStreamError) + .pipe(gzipTransform) + .on("error", handleStreamError) + .pipe(res); +}); + +server.listen(port, host, () => { + console.log(`Web server running at http://${host}:${port}/`); +}); diff --git a/06-www/10-w3-log-timing.mjs b/06-www/10-w3-log-timing.mjs new file mode 100644 index 0000000..697db4f --- /dev/null +++ b/06-www/10-w3-log-timing.mjs @@ -0,0 +1,78 @@ +import http from "http"; +import fs from "fs/promises"; +import mimeMap from "./media-types.mjs"; +import { extname } from "path"; +import { log } from "./logger.mjs"; +import { createGzip } from "zlib"; +import { hrtime } from "process"; + +const host = "127.0.0.1"; +const port = 3000; +const root = "files"; + +const server = http.createServer(); +server.on("request", async (req, res) => { + const startTime = hrtime.bigint(); + + const [isGET, isHEAD] = [req.method === "GET", req.method === "HEAD"]; + if (!isGET && !isHEAD) { + res.statusCode = 405; + res.end(); + return; + } + + const { pathname } = new URL(req.url, `http://${req.headers.host}`); + const file = `${root}${pathname}`; + + res.on("finish", () => { + const endTime = hrtime.bigint(); + const duration = (endTime - startTime) / BigInt(1e6); + const timestamp = new Date().toISOString(); + const ip = req.socket.remoteAddress; + const httpDetails = `"${req.method} ${pathname}" ${res.statusCode}`; + log(`${ip} - ${timestamp} ${httpDetails} - ${duration}ms`); + }); + + let fh; + try { + fh = await fs.open(file); + } catch (e) { + console.error(e); + res.statusCode = 404; + res.end(); + return; + } + + const mimeType = mimeMap.get(extname(pathname)); + if (mimeType) { + res.setHeader("Content-Type", mimeType); + } + + if (isHEAD) { + const fileStat = await fh.stat(); + res.setHeader("Content-Length", fileStat.size); + res.statusCode = 200; + res.end(); + await fh.close(); + return; + } + + res.setHeader("Content-Encoding", "gzip"); + const fileStream = fh.createReadStream(); + const gzipTransform = createGzip(); + const handleStreamError = (err) => { + console.error(err); + fileStream.destroy(); + gzipTransform.destroy(); + res.statusCode = 500; + }; + fileStream + .on("error", handleStreamError) + .pipe(gzipTransform) + .on("error", handleStreamError) + .pipe(res); +}); + +server.listen(port, host, () => { + console.log(`Web server running at http://${host}:${port}/`); +}); diff --git a/06-www/11-w3-env-vars.mjs b/06-www/11-w3-env-vars.mjs new file mode 100644 index 0000000..17403c4 --- /dev/null +++ b/06-www/11-w3-env-vars.mjs @@ -0,0 +1,81 @@ +import http from "http"; +import fs from "fs/promises"; +import mimeMap from "./media-types.mjs"; +import { extname } from "path"; +import { log } from "./logger.mjs"; +import { createGzip } from "zlib"; +import { hrtime } from "process"; +import { exitIfNotDir } from "./w3-utils.mjs"; + +const host = process.env.WEB_HOST || "127.0.0.1"; +const port = process.env.WEB_PORT || 3000; +const root = process.env.WEB_ROOT || "files"; + +await exitIfNotDir(root); + +const server = http.createServer(); +server.on("request", async (req, res) => { + const startTime = hrtime.bigint(); + + const [isGET, isHEAD] = [req.method === "GET", req.method === "HEAD"]; + if (!isGET && !isHEAD) { + res.statusCode = 405; + res.end(); + return; + } + + const { pathname } = new URL(req.url, `http://${req.headers.host}`); + const file = `${root}${pathname}`; + + res.on("finish", () => { + const endTime = hrtime.bigint(); + const duration = (endTime - startTime) / BigInt(1e6); + const timestamp = new Date().toISOString(); + const ip = req.socket.remoteAddress; + const httpDetails = `"${req.method} ${pathname}" ${res.statusCode}`; + log(`${ip} - ${timestamp} ${httpDetails} - ${duration}ms`); + }); + + let fh; + try { + fh = await fs.open(file); + } catch (e) { + console.error(e); + res.statusCode = 404; + res.end(); + return; + } + + const mimeType = mimeMap.get(extname(pathname)); + if (mimeType) { + res.setHeader("Content-Type", mimeType); + } + + if (isHEAD) { + const fileStat = await fh.stat(); + res.setHeader("Content-Length", fileStat.size); + res.statusCode = 200; + res.end(); + await fh.close(); + return; + } + + res.setHeader("Content-Encoding", "gzip"); + const fileStream = fh.createReadStream(); + const gzipTransform = createGzip(); + const handleStreamError = (err) => { + console.error(err); + fileStream.destroy(); + gzipTransform.destroy(); + res.statusCode = 500; + }; + fileStream + .on("error", handleStreamError) + .pipe(gzipTransform) + .on("error", handleStreamError) + .pipe(res); +}); + +server.listen(port, host, () => { + console.log(`Web server running at http://${host}:${port}/`); +}); diff --git a/06-www/12-w3-index.mjs b/06-www/12-w3-index.mjs new file mode 100644 index 0000000..6bb5958 --- /dev/null +++ b/06-www/12-w3-index.mjs @@ -0,0 +1,77 @@ +import http from "http"; +import mimeMap from "./media-types.mjs"; +import { extname } from "path"; +import { log } from "./logger.mjs"; +import { createGzip } from "zlib"; +import { hrtime } from "process"; +import { + generateLogString, + tryOpenFile, + isStringTrue, + exitIfNotDir, +} from "./w3-utils.mjs"; + +const host = process.env.WEB_HOST || "127.0.0.1"; +const port = process.env.WEB_PORT || 3000; +const root = process.env.WEB_ROOT || "files"; +const index = isStringTrue(process.env.WEB_INDEX) || false; + +await exitIfNotDir(root); + +const server = http.createServer(); +server.on("request", async (req, res) => { + const startTime = hrtime.bigint(); + + const [isGET, isHEAD] = [req.method === "GET", req.method === "HEAD"]; + if (!isGET && !isHEAD) { + res.statusCode = 405; + res.end(); + return; + } + + const { pathname } = new URL(req.url, `http://${req.headers.host}`); + const localPath = `${root}${pathname}`; + + res.on("finish", () => { + log(generateLogString(req, res, pathname, startTime)); + }); + + const { found, fh, fileStat } = await tryOpenFile(localPath, index); + if (!found) { + res.statusCode = 404; + res.end(); + return; + } + + const mimeType = mimeMap.get(extname(pathname)); + if (mimeType) { + res.setHeader("Content-Type", mimeType); + } + + if (isHEAD) { + res.setHeader("Content-Length", fileStat.size); + res.statusCode = 200; + res.end(); + await fh.close(); + return; + } + + res.setHeader("Content-Encoding", "gzip"); + const fileStream = fh.createReadStream(); + const gzipTransform = createGzip(); + const handleStreamError = (err) => { + console.error(err); + fileStream.destroy(); + gzipTransform.destroy(); + res.statusCode = 500; + }; + fileStream + .on("error", handleStreamError) + .pipe(gzipTransform) + .on("error", handleStreamError) + .pipe(res); +}); + +server.listen(port, host, () => { + console.log(`Web server running at http://${host}:${port}/`); +}); diff --git a/06-www/files/favicon.ico b/06-www/files/favicon.ico new file mode 100644 index 0000000..88a329d Binary files /dev/null and b/06-www/files/favicon.ico differ diff --git a/06-www/files/index.html b/06-www/files/index.html new file mode 100644 index 0000000..172f82b --- /dev/null +++ b/06-www/files/index.html @@ -0,0 +1,20 @@ + + + + + Welcome to Node.js web server + + +

    Welcome to Node.js web server!

    +

    If you see this page, the Node.js web server is working.

    + +

    + For a rich HTML page see + the kitchen sink.
    + This server is also able to serve + funny images. +

    + +

    Thank you for using Node.js web server.

    + + diff --git a/06-www/files/kitchen-sink.html b/06-www/files/kitchen-sink.html new file mode 100644 index 0000000..a8ef5e9 --- /dev/null +++ b/06-www/files/kitchen-sink.html @@ -0,0 +1,167 @@ + + + + + + + + + + + + + My title + + + +
    + +

    Page Heading with Italics and Underline

    +

    Page Subheading with highlighting

    +
    +

    + Italic Link ButtonBold Link Button → +

    +
    +
    +
    +
    +
    +

    Section Heading

    +

    Section Subheading

    +
    + + + +
    +
    +
    +
    + "Quote" +
    - Attribution
    +
    +
    +
    +
    + + + + + + + + + + + + + + + + + + + + + +
    Col ACol BCol C
    Row 1Cell A1Cell B1Cell C1
    Row 2Cell A2Cell B2Cell C2
    +
    +
    +
    +

    Left-aligned header

    +

    Left-aligned paragraph

    + + +
    + Stock photo +
    Image caption
    +
    +
    +
    +
    +
    + Expandable title +

    Revealed content

    +
    +
    + Another expandable title +

    More revealed content

    +
    +
    +

    Inline code snippets

    +
    +                
    +// preformatted code block
    +                
    +            
    +
    +
    +
    +
    +
    +

    Form title

    +
    + + + + + + + +
    +
    +
    + + + diff --git a/06-www/files/licenses/agpl-3.0.txt b/06-www/files/licenses/agpl-3.0.txt new file mode 100644 index 0000000..be3f7b2 --- /dev/null +++ b/06-www/files/licenses/agpl-3.0.txt @@ -0,0 +1,661 @@ + GNU AFFERO GENERAL PUBLIC LICENSE + Version 3, 19 November 2007 + + Copyright (C) 2007 Free Software Foundation, Inc. + Everyone is permitted to copy and distribute verbatim copies + of this license document, but changing it is not allowed. + + Preamble + + The GNU Affero General Public License is a free, copyleft license for +software and other kinds of works, specifically designed to ensure +cooperation with the community in the case of network server software. + + The licenses for most software and other practical works are designed +to take away your freedom to share and change the works. By contrast, +our General Public Licenses are intended to guarantee your freedom to +share and change all versions of a program--to make sure it remains free +software for all its users. + + When we speak of free software, we are referring to freedom, not +price. Our General Public Licenses are designed to make sure that you +have the freedom to distribute copies of free software (and charge for +them if you wish), that you receive source code or can get it if you +want it, that you can change the software or use pieces of it in new +free programs, and that you know you can do these things. + + Developers that use our General Public Licenses protect your rights +with two steps: (1) assert copyright on the software, and (2) offer +you this License which gives you legal permission to copy, distribute +and/or modify the software. + + A secondary benefit of defending all users' freedom is that +improvements made in alternate versions of the program, if they +receive widespread use, become available for other developers to +incorporate. Many developers of free software are heartened and +encouraged by the resulting cooperation. However, in the case of +software used on network servers, this result may fail to come about. +The GNU General Public License permits making a modified version and +letting the public access it on a server without ever releasing its +source code to the public. + + The GNU Affero General Public License is designed specifically to +ensure that, in such cases, the modified source code becomes available +to the community. It requires the operator of a network server to +provide the source code of the modified version running there to the +users of that server. Therefore, public use of a modified version, on +a publicly accessible server, gives the public access to the source +code of the modified version. + + An older license, called the Affero General Public License and +published by Affero, was designed to accomplish similar goals. This is +a different license, not a version of the Affero GPL, but Affero has +released a new version of the Affero GPL which permits relicensing under +this license. + + The precise terms and conditions for copying, distribution and +modification follow. + + TERMS AND CONDITIONS + + 0. Definitions. + + "This License" refers to version 3 of the GNU Affero General Public License. + + "Copyright" also means copyright-like laws that apply to other kinds of +works, such as semiconductor masks. + + "The Program" refers to any copyrightable work licensed under this +License. Each licensee is addressed as "you". "Licensees" and +"recipients" may be individuals or organizations. + + To "modify" a work means to copy from or adapt all or part of the work +in a fashion requiring copyright permission, other than the making of an +exact copy. The resulting work is called a "modified version" of the +earlier work or a work "based on" the earlier work. + + A "covered work" means either the unmodified Program or a work based +on the Program. + + To "propagate" a work means to do anything with it that, without +permission, would make you directly or secondarily liable for +infringement under applicable copyright law, except executing it on a +computer or modifying a private copy. Propagation includes copying, +distribution (with or without modification), making available to the +public, and in some countries other activities as well. + + To "convey" a work means any kind of propagation that enables other +parties to make or receive copies. Mere interaction with a user through +a computer network, with no transfer of a copy, is not conveying. + + An interactive user interface displays "Appropriate Legal Notices" +to the extent that it includes a convenient and prominently visible +feature that (1) displays an appropriate copyright notice, and (2) +tells the user that there is no warranty for the work (except to the +extent that warranties are provided), that licensees may convey the +work under this License, and how to view a copy of this License. If +the interface presents a list of user commands or options, such as a +menu, a prominent item in the list meets this criterion. + + 1. Source Code. + + The "source code" for a work means the preferred form of the work +for making modifications to it. "Object code" means any non-source +form of a work. + + A "Standard Interface" means an interface that either is an official +standard defined by a recognized standards body, or, in the case of +interfaces specified for a particular programming language, one that +is widely used among developers working in that language. + + The "System Libraries" of an executable work include anything, other +than the work as a whole, that (a) is included in the normal form of +packaging a Major Component, but which is not part of that Major +Component, and (b) serves only to enable use of the work with that +Major Component, or to implement a Standard Interface for which an +implementation is available to the public in source code form. A +"Major Component", in this context, means a major essential component +(kernel, window system, and so on) of the specific operating system +(if any) on which the executable work runs, or a compiler used to +produce the work, or an object code interpreter used to run it. + + The "Corresponding Source" for a work in object code form means all +the source code needed to generate, install, and (for an executable +work) run the object code and to modify the work, including scripts to +control those activities. However, it does not include the work's +System Libraries, or general-purpose tools or generally available free +programs which are used unmodified in performing those activities but +which are not part of the work. For example, Corresponding Source +includes interface definition files associated with source files for +the work, and the source code for shared libraries and dynamically +linked subprograms that the work is specifically designed to require, +such as by intimate data communication or control flow between those +subprograms and other parts of the work. + + The Corresponding Source need not include anything that users +can regenerate automatically from other parts of the Corresponding +Source. + + The Corresponding Source for a work in source code form is that +same work. + + 2. Basic Permissions. + + All rights granted under this License are granted for the term of +copyright on the Program, and are irrevocable provided the stated +conditions are met. This License explicitly affirms your unlimited +permission to run the unmodified Program. The output from running a +covered work is covered by this License only if the output, given its +content, constitutes a covered work. This License acknowledges your +rights of fair use or other equivalent, as provided by copyright law. + + You may make, run and propagate covered works that you do not +convey, without conditions so long as your license otherwise remains +in force. You may convey covered works to others for the sole purpose +of having them make modifications exclusively for you, or provide you +with facilities for running those works, provided that you comply with +the terms of this License in conveying all material for which you do +not control copyright. Those thus making or running the covered works +for you must do so exclusively on your behalf, under your direction +and control, on terms that prohibit them from making any copies of +your copyrighted material outside their relationship with you. + + Conveying under any other circumstances is permitted solely under +the conditions stated below. Sublicensing is not allowed; section 10 +makes it unnecessary. + + 3. Protecting Users' Legal Rights From Anti-Circumvention Law. + + No covered work shall be deemed part of an effective technological +measure under any applicable law fulfilling obligations under article +11 of the WIPO copyright treaty adopted on 20 December 1996, or +similar laws prohibiting or restricting circumvention of such +measures. + + When you convey a covered work, you waive any legal power to forbid +circumvention of technological measures to the extent such circumvention +is effected by exercising rights under this License with respect to +the covered work, and you disclaim any intention to limit operation or +modification of the work as a means of enforcing, against the work's +users, your or third parties' legal rights to forbid circumvention of +technological measures. + + 4. Conveying Verbatim Copies. + + You may convey verbatim copies of the Program's source code as you +receive it, in any medium, provided that you conspicuously and +appropriately publish on each copy an appropriate copyright notice; +keep intact all notices stating that this License and any +non-permissive terms added in accord with section 7 apply to the code; +keep intact all notices of the absence of any warranty; and give all +recipients a copy of this License along with the Program. + + You may charge any price or no price for each copy that you convey, +and you may offer support or warranty protection for a fee. + + 5. Conveying Modified Source Versions. + + You may convey a work based on the Program, or the modifications to +produce it from the Program, in the form of source code under the +terms of section 4, provided that you also meet all of these conditions: + + a) The work must carry prominent notices stating that you modified + it, and giving a relevant date. + + b) The work must carry prominent notices stating that it is + released under this License and any conditions added under section + 7. This requirement modifies the requirement in section 4 to + "keep intact all notices". + + c) You must license the entire work, as a whole, under this + License to anyone who comes into possession of a copy. This + License will therefore apply, along with any applicable section 7 + additional terms, to the whole of the work, and all its parts, + regardless of how they are packaged. This License gives no + permission to license the work in any other way, but it does not + invalidate such permission if you have separately received it. + + d) If the work has interactive user interfaces, each must display + Appropriate Legal Notices; however, if the Program has interactive + interfaces that do not display Appropriate Legal Notices, your + work need not make them do so. + + A compilation of a covered work with other separate and independent +works, which are not by their nature extensions of the covered work, +and which are not combined with it such as to form a larger program, +in or on a volume of a storage or distribution medium, is called an +"aggregate" if the compilation and its resulting copyright are not +used to limit the access or legal rights of the compilation's users +beyond what the individual works permit. Inclusion of a covered work +in an aggregate does not cause this License to apply to the other +parts of the aggregate. + + 6. Conveying Non-Source Forms. + + You may convey a covered work in object code form under the terms +of sections 4 and 5, provided that you also convey the +machine-readable Corresponding Source under the terms of this License, +in one of these ways: + + a) Convey the object code in, or embodied in, a physical product + (including a physical distribution medium), accompanied by the + Corresponding Source fixed on a durable physical medium + customarily used for software interchange. + + b) Convey the object code in, or embodied in, a physical product + (including a physical distribution medium), accompanied by a + written offer, valid for at least three years and valid for as + long as you offer spare parts or customer support for that product + model, to give anyone who possesses the object code either (1) a + copy of the Corresponding Source for all the software in the + product that is covered by this License, on a durable physical + medium customarily used for software interchange, for a price no + more than your reasonable cost of physically performing this + conveying of source, or (2) access to copy the + Corresponding Source from a network server at no charge. + + c) Convey individual copies of the object code with a copy of the + written offer to provide the Corresponding Source. This + alternative is allowed only occasionally and noncommercially, and + only if you received the object code with such an offer, in accord + with subsection 6b. + + d) Convey the object code by offering access from a designated + place (gratis or for a charge), and offer equivalent access to the + Corresponding Source in the same way through the same place at no + further charge. You need not require recipients to copy the + Corresponding Source along with the object code. If the place to + copy the object code is a network server, the Corresponding Source + may be on a different server (operated by you or a third party) + that supports equivalent copying facilities, provided you maintain + clear directions next to the object code saying where to find the + Corresponding Source. Regardless of what server hosts the + Corresponding Source, you remain obligated to ensure that it is + available for as long as needed to satisfy these requirements. + + e) Convey the object code using peer-to-peer transmission, provided + you inform other peers where the object code and Corresponding + Source of the work are being offered to the general public at no + charge under subsection 6d. + + A separable portion of the object code, whose source code is excluded +from the Corresponding Source as a System Library, need not be +included in conveying the object code work. + + A "User Product" is either (1) a "consumer product", which means any +tangible personal property which is normally used for personal, family, +or household purposes, or (2) anything designed or sold for incorporation +into a dwelling. In determining whether a product is a consumer product, +doubtful cases shall be resolved in favor of coverage. For a particular +product received by a particular user, "normally used" refers to a +typical or common use of that class of product, regardless of the status +of the particular user or of the way in which the particular user +actually uses, or expects or is expected to use, the product. A product +is a consumer product regardless of whether the product has substantial +commercial, industrial or non-consumer uses, unless such uses represent +the only significant mode of use of the product. + + "Installation Information" for a User Product means any methods, +procedures, authorization keys, or other information required to install +and execute modified versions of a covered work in that User Product from +a modified version of its Corresponding Source. The information must +suffice to ensure that the continued functioning of the modified object +code is in no case prevented or interfered with solely because +modification has been made. + + If you convey an object code work under this section in, or with, or +specifically for use in, a User Product, and the conveying occurs as +part of a transaction in which the right of possession and use of the +User Product is transferred to the recipient in perpetuity or for a +fixed term (regardless of how the transaction is characterized), the +Corresponding Source conveyed under this section must be accompanied +by the Installation Information. But this requirement does not apply +if neither you nor any third party retains the ability to install +modified object code on the User Product (for example, the work has +been installed in ROM). + + The requirement to provide Installation Information does not include a +requirement to continue to provide support service, warranty, or updates +for a work that has been modified or installed by the recipient, or for +the User Product in which it has been modified or installed. Access to a +network may be denied when the modification itself materially and +adversely affects the operation of the network or violates the rules and +protocols for communication across the network. + + Corresponding Source conveyed, and Installation Information provided, +in accord with this section must be in a format that is publicly +documented (and with an implementation available to the public in +source code form), and must require no special password or key for +unpacking, reading or copying. + + 7. Additional Terms. + + "Additional permissions" are terms that supplement the terms of this +License by making exceptions from one or more of its conditions. +Additional permissions that are applicable to the entire Program shall +be treated as though they were included in this License, to the extent +that they are valid under applicable law. If additional permissions +apply only to part of the Program, that part may be used separately +under those permissions, but the entire Program remains governed by +this License without regard to the additional permissions. + + When you convey a copy of a covered work, you may at your option +remove any additional permissions from that copy, or from any part of +it. (Additional permissions may be written to require their own +removal in certain cases when you modify the work.) You may place +additional permissions on material, added by you to a covered work, +for which you have or can give appropriate copyright permission. + + Notwithstanding any other provision of this License, for material you +add to a covered work, you may (if authorized by the copyright holders of +that material) supplement the terms of this License with terms: + + a) Disclaiming warranty or limiting liability differently from the + terms of sections 15 and 16 of this License; or + + b) Requiring preservation of specified reasonable legal notices or + author attributions in that material or in the Appropriate Legal + Notices displayed by works containing it; or + + c) Prohibiting misrepresentation of the origin of that material, or + requiring that modified versions of such material be marked in + reasonable ways as different from the original version; or + + d) Limiting the use for publicity purposes of names of licensors or + authors of the material; or + + e) Declining to grant rights under trademark law for use of some + trade names, trademarks, or service marks; or + + f) Requiring indemnification of licensors and authors of that + material by anyone who conveys the material (or modified versions of + it) with contractual assumptions of liability to the recipient, for + any liability that these contractual assumptions directly impose on + those licensors and authors. + + All other non-permissive additional terms are considered "further +restrictions" within the meaning of section 10. If the Program as you +received it, or any part of it, contains a notice stating that it is +governed by this License along with a term that is a further +restriction, you may remove that term. If a license document contains +a further restriction but permits relicensing or conveying under this +License, you may add to a covered work material governed by the terms +of that license document, provided that the further restriction does +not survive such relicensing or conveying. + + If you add terms to a covered work in accord with this section, you +must place, in the relevant source files, a statement of the +additional terms that apply to those files, or a notice indicating +where to find the applicable terms. + + Additional terms, permissive or non-permissive, may be stated in the +form of a separately written license, or stated as exceptions; +the above requirements apply either way. + + 8. Termination. + + You may not propagate or modify a covered work except as expressly +provided under this License. Any attempt otherwise to propagate or +modify it is void, and will automatically terminate your rights under +this License (including any patent licenses granted under the third +paragraph of section 11). + + However, if you cease all violation of this License, then your +license from a particular copyright holder is reinstated (a) +provisionally, unless and until the copyright holder explicitly and +finally terminates your license, and (b) permanently, if the copyright +holder fails to notify you of the violation by some reasonable means +prior to 60 days after the cessation. + + Moreover, your license from a particular copyright holder is +reinstated permanently if the copyright holder notifies you of the +violation by some reasonable means, this is the first time you have +received notice of violation of this License (for any work) from that +copyright holder, and you cure the violation prior to 30 days after +your receipt of the notice. + + Termination of your rights under this section does not terminate the +licenses of parties who have received copies or rights from you under +this License. If your rights have been terminated and not permanently +reinstated, you do not qualify to receive new licenses for the same +material under section 10. + + 9. Acceptance Not Required for Having Copies. + + You are not required to accept this License in order to receive or +run a copy of the Program. Ancillary propagation of a covered work +occurring solely as a consequence of using peer-to-peer transmission +to receive a copy likewise does not require acceptance. However, +nothing other than this License grants you permission to propagate or +modify any covered work. These actions infringe copyright if you do +not accept this License. Therefore, by modifying or propagating a +covered work, you indicate your acceptance of this License to do so. + + 10. Automatic Licensing of Downstream Recipients. + + Each time you convey a covered work, the recipient automatically +receives a license from the original licensors, to run, modify and +propagate that work, subject to this License. You are not responsible +for enforcing compliance by third parties with this License. + + An "entity transaction" is a transaction transferring control of an +organization, or substantially all assets of one, or subdividing an +organization, or merging organizations. If propagation of a covered +work results from an entity transaction, each party to that +transaction who receives a copy of the work also receives whatever +licenses to the work the party's predecessor in interest had or could +give under the previous paragraph, plus a right to possession of the +Corresponding Source of the work from the predecessor in interest, if +the predecessor has it or can get it with reasonable efforts. + + You may not impose any further restrictions on the exercise of the +rights granted or affirmed under this License. For example, you may +not impose a license fee, royalty, or other charge for exercise of +rights granted under this License, and you may not initiate litigation +(including a cross-claim or counterclaim in a lawsuit) alleging that +any patent claim is infringed by making, using, selling, offering for +sale, or importing the Program or any portion of it. + + 11. Patents. + + A "contributor" is a copyright holder who authorizes use under this +License of the Program or a work on which the Program is based. The +work thus licensed is called the contributor's "contributor version". + + A contributor's "essential patent claims" are all patent claims +owned or controlled by the contributor, whether already acquired or +hereafter acquired, that would be infringed by some manner, permitted +by this License, of making, using, or selling its contributor version, +but do not include claims that would be infringed only as a +consequence of further modification of the contributor version. For +purposes of this definition, "control" includes the right to grant +patent sublicenses in a manner consistent with the requirements of +this License. + + Each contributor grants you a non-exclusive, worldwide, royalty-free +patent license under the contributor's essential patent claims, to +make, use, sell, offer for sale, import and otherwise run, modify and +propagate the contents of its contributor version. + + In the following three paragraphs, a "patent license" is any express +agreement or commitment, however denominated, not to enforce a patent +(such as an express permission to practice a patent or covenant not to +sue for patent infringement). To "grant" such a patent license to a +party means to make such an agreement or commitment not to enforce a +patent against the party. + + If you convey a covered work, knowingly relying on a patent license, +and the Corresponding Source of the work is not available for anyone +to copy, free of charge and under the terms of this License, through a +publicly available network server or other readily accessible means, +then you must either (1) cause the Corresponding Source to be so +available, or (2) arrange to deprive yourself of the benefit of the +patent license for this particular work, or (3) arrange, in a manner +consistent with the requirements of this License, to extend the patent +license to downstream recipients. "Knowingly relying" means you have +actual knowledge that, but for the patent license, your conveying the +covered work in a country, or your recipient's use of the covered work +in a country, would infringe one or more identifiable patents in that +country that you have reason to believe are valid. + + If, pursuant to or in connection with a single transaction or +arrangement, you convey, or propagate by procuring conveyance of, a +covered work, and grant a patent license to some of the parties +receiving the covered work authorizing them to use, propagate, modify +or convey a specific copy of the covered work, then the patent license +you grant is automatically extended to all recipients of the covered +work and works based on it. + + A patent license is "discriminatory" if it does not include within +the scope of its coverage, prohibits the exercise of, or is +conditioned on the non-exercise of one or more of the rights that are +specifically granted under this License. You may not convey a covered +work if you are a party to an arrangement with a third party that is +in the business of distributing software, under which you make payment +to the third party based on the extent of your activity of conveying +the work, and under which the third party grants, to any of the +parties who would receive the covered work from you, a discriminatory +patent license (a) in connection with copies of the covered work +conveyed by you (or copies made from those copies), or (b) primarily +for and in connection with specific products or compilations that +contain the covered work, unless you entered into that arrangement, +or that patent license was granted, prior to 28 March 2007. + + Nothing in this License shall be construed as excluding or limiting +any implied license or other defenses to infringement that may +otherwise be available to you under applicable patent law. + + 12. No Surrender of Others' Freedom. + + If conditions are imposed on you (whether by court order, agreement or +otherwise) that contradict the conditions of this License, they do not +excuse you from the conditions of this License. If you cannot convey a +covered work so as to satisfy simultaneously your obligations under this +License and any other pertinent obligations, then as a consequence you may +not convey it at all. For example, if you agree to terms that obligate you +to collect a royalty for further conveying from those to whom you convey +the Program, the only way you could satisfy both those terms and this +License would be to refrain entirely from conveying the Program. + + 13. Remote Network Interaction; Use with the GNU General Public License. + + Notwithstanding any other provision of this License, if you modify the +Program, your modified version must prominently offer all users +interacting with it remotely through a computer network (if your version +supports such interaction) an opportunity to receive the Corresponding +Source of your version by providing access to the Corresponding Source +from a network server at no charge, through some standard or customary +means of facilitating copying of software. This Corresponding Source +shall include the Corresponding Source for any work covered by version 3 +of the GNU General Public License that is incorporated pursuant to the +following paragraph. + + Notwithstanding any other provision of this License, you have +permission to link or combine any covered work with a work licensed +under version 3 of the GNU General Public License into a single +combined work, and to convey the resulting work. The terms of this +License will continue to apply to the part which is the covered work, +but the work with which it is combined will remain governed by version +3 of the GNU General Public License. + + 14. Revised Versions of this License. + + The Free Software Foundation may publish revised and/or new versions of +the GNU Affero General Public License from time to time. Such new versions +will be similar in spirit to the present version, but may differ in detail to +address new problems or concerns. + + Each version is given a distinguishing version number. If the +Program specifies that a certain numbered version of the GNU Affero General +Public License "or any later version" applies to it, you have the +option of following the terms and conditions either of that numbered +version or of any later version published by the Free Software +Foundation. If the Program does not specify a version number of the +GNU Affero General Public License, you may choose any version ever published +by the Free Software Foundation. + + If the Program specifies that a proxy can decide which future +versions of the GNU Affero General Public License can be used, that proxy's +public statement of acceptance of a version permanently authorizes you +to choose that version for the Program. + + Later license versions may give you additional or different +permissions. However, no additional obligations are imposed on any +author or copyright holder as a result of your choosing to follow a +later version. + + 15. Disclaimer of Warranty. + + THERE IS NO WARRANTY FOR THE PROGRAM, TO THE EXTENT PERMITTED BY +APPLICABLE LAW. EXCEPT WHEN OTHERWISE STATED IN WRITING THE COPYRIGHT +HOLDERS AND/OR OTHER PARTIES PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY +OF ANY KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING, BUT NOT LIMITED TO, +THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR +PURPOSE. THE ENTIRE RISK AS TO THE QUALITY AND PERFORMANCE OF THE PROGRAM +IS WITH YOU. SHOULD THE PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF +ALL NECESSARY SERVICING, REPAIR OR CORRECTION. + + 16. Limitation of Liability. + + IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING +WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MODIFIES AND/OR CONVEYS +THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, INCLUDING ANY +GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING OUT OF THE +USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED TO LOSS OF +DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY YOU OR THIRD +PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER PROGRAMS), +EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF +SUCH DAMAGES. + + 17. Interpretation of Sections 15 and 16. + + If the disclaimer of warranty and limitation of liability provided +above cannot be given local legal effect according to their terms, +reviewing courts shall apply local law that most closely approximates +an absolute waiver of all civil liability in connection with the +Program, unless a warranty or assumption of liability accompanies a +copy of the Program in return for a fee. + + END OF TERMS AND CONDITIONS + + How to Apply These Terms to Your New Programs + + If you develop a new program, and you want it to be of the greatest +possible use to the public, the best way to achieve this is to make it +free software which everyone can redistribute and change under these terms. + + To do so, attach the following notices to the program. It is safest +to attach them to the start of each source file to most effectively +state the exclusion of warranty; and each file should have at least +the "copyright" line and a pointer to where the full notice is found. + + + Copyright (C) + + This program is free software: you can redistribute it and/or modify + it under the terms of the GNU Affero General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU Affero General Public License for more details. + + You should have received a copy of the GNU Affero General Public License + along with this program. If not, see . + +Also add information on how to contact you by electronic and paper mail. + + If your software can interact with users remotely through a computer +network, you should also make sure that it provides a way for users to +get its source. For example, if your program is a web application, its +interface could display a "Source" link that leads users to an archive +of the code. There are many ways you could offer source, and different +solutions will be better for different programs; see section 13 for the +specific requirements. + + You should also get your employer (if you work as a programmer) or school, +if any, to sign a "copyright disclaimer" for the program, if necessary. +For more information on this, and how to apply and follow the GNU AGPL, see +. diff --git a/06-www/files/licenses/gpl-3.0.txt b/06-www/files/licenses/gpl-3.0.txt new file mode 100644 index 0000000..f288702 --- /dev/null +++ b/06-www/files/licenses/gpl-3.0.txt @@ -0,0 +1,674 @@ + GNU GENERAL PUBLIC LICENSE + Version 3, 29 June 2007 + + Copyright (C) 2007 Free Software Foundation, Inc. + Everyone is permitted to copy and distribute verbatim copies + of this license document, but changing it is not allowed. + + Preamble + + The GNU General Public License is a free, copyleft license for +software and other kinds of works. + + The licenses for most software and other practical works are designed +to take away your freedom to share and change the works. By contrast, +the GNU General Public License is intended to guarantee your freedom to +share and change all versions of a program--to make sure it remains free +software for all its users. We, the Free Software Foundation, use the +GNU General Public License for most of our software; it applies also to +any other work released this way by its authors. You can apply it to +your programs, too. + + When we speak of free software, we are referring to freedom, not +price. Our General Public Licenses are designed to make sure that you +have the freedom to distribute copies of free software (and charge for +them if you wish), that you receive source code or can get it if you +want it, that you can change the software or use pieces of it in new +free programs, and that you know you can do these things. + + To protect your rights, we need to prevent others from denying you +these rights or asking you to surrender the rights. Therefore, you have +certain responsibilities if you distribute copies of the software, or if +you modify it: responsibilities to respect the freedom of others. + + For example, if you distribute copies of such a program, whether +gratis or for a fee, you must pass on to the recipients the same +freedoms that you received. You must make sure that they, too, receive +or can get the source code. And you must show them these terms so they +know their rights. + + Developers that use the GNU GPL protect your rights with two steps: +(1) assert copyright on the software, and (2) offer you this License +giving you legal permission to copy, distribute and/or modify it. + + For the developers' and authors' protection, the GPL clearly explains +that there is no warranty for this free software. For both users' and +authors' sake, the GPL requires that modified versions be marked as +changed, so that their problems will not be attributed erroneously to +authors of previous versions. + + Some devices are designed to deny users access to install or run +modified versions of the software inside them, although the manufacturer +can do so. This is fundamentally incompatible with the aim of +protecting users' freedom to change the software. The systematic +pattern of such abuse occurs in the area of products for individuals to +use, which is precisely where it is most unacceptable. Therefore, we +have designed this version of the GPL to prohibit the practice for those +products. If such problems arise substantially in other domains, we +stand ready to extend this provision to those domains in future versions +of the GPL, as needed to protect the freedom of users. + + Finally, every program is threatened constantly by software patents. +States should not allow patents to restrict development and use of +software on general-purpose computers, but in those that do, we wish to +avoid the special danger that patents applied to a free program could +make it effectively proprietary. To prevent this, the GPL assures that +patents cannot be used to render the program non-free. + + The precise terms and conditions for copying, distribution and +modification follow. + + TERMS AND CONDITIONS + + 0. Definitions. + + "This License" refers to version 3 of the GNU General Public License. + + "Copyright" also means copyright-like laws that apply to other kinds of +works, such as semiconductor masks. + + "The Program" refers to any copyrightable work licensed under this +License. Each licensee is addressed as "you". "Licensees" and +"recipients" may be individuals or organizations. + + To "modify" a work means to copy from or adapt all or part of the work +in a fashion requiring copyright permission, other than the making of an +exact copy. The resulting work is called a "modified version" of the +earlier work or a work "based on" the earlier work. + + A "covered work" means either the unmodified Program or a work based +on the Program. + + To "propagate" a work means to do anything with it that, without +permission, would make you directly or secondarily liable for +infringement under applicable copyright law, except executing it on a +computer or modifying a private copy. Propagation includes copying, +distribution (with or without modification), making available to the +public, and in some countries other activities as well. + + To "convey" a work means any kind of propagation that enables other +parties to make or receive copies. Mere interaction with a user through +a computer network, with no transfer of a copy, is not conveying. + + An interactive user interface displays "Appropriate Legal Notices" +to the extent that it includes a convenient and prominently visible +feature that (1) displays an appropriate copyright notice, and (2) +tells the user that there is no warranty for the work (except to the +extent that warranties are provided), that licensees may convey the +work under this License, and how to view a copy of this License. If +the interface presents a list of user commands or options, such as a +menu, a prominent item in the list meets this criterion. + + 1. Source Code. + + The "source code" for a work means the preferred form of the work +for making modifications to it. "Object code" means any non-source +form of a work. + + A "Standard Interface" means an interface that either is an official +standard defined by a recognized standards body, or, in the case of +interfaces specified for a particular programming language, one that +is widely used among developers working in that language. + + The "System Libraries" of an executable work include anything, other +than the work as a whole, that (a) is included in the normal form of +packaging a Major Component, but which is not part of that Major +Component, and (b) serves only to enable use of the work with that +Major Component, or to implement a Standard Interface for which an +implementation is available to the public in source code form. A +"Major Component", in this context, means a major essential component +(kernel, window system, and so on) of the specific operating system +(if any) on which the executable work runs, or a compiler used to +produce the work, or an object code interpreter used to run it. + + The "Corresponding Source" for a work in object code form means all +the source code needed to generate, install, and (for an executable +work) run the object code and to modify the work, including scripts to +control those activities. However, it does not include the work's +System Libraries, or general-purpose tools or generally available free +programs which are used unmodified in performing those activities but +which are not part of the work. For example, Corresponding Source +includes interface definition files associated with source files for +the work, and the source code for shared libraries and dynamically +linked subprograms that the work is specifically designed to require, +such as by intimate data communication or control flow between those +subprograms and other parts of the work. + + The Corresponding Source need not include anything that users +can regenerate automatically from other parts of the Corresponding +Source. + + The Corresponding Source for a work in source code form is that +same work. + + 2. Basic Permissions. + + All rights granted under this License are granted for the term of +copyright on the Program, and are irrevocable provided the stated +conditions are met. This License explicitly affirms your unlimited +permission to run the unmodified Program. The output from running a +covered work is covered by this License only if the output, given its +content, constitutes a covered work. This License acknowledges your +rights of fair use or other equivalent, as provided by copyright law. + + You may make, run and propagate covered works that you do not +convey, without conditions so long as your license otherwise remains +in force. You may convey covered works to others for the sole purpose +of having them make modifications exclusively for you, or provide you +with facilities for running those works, provided that you comply with +the terms of this License in conveying all material for which you do +not control copyright. Those thus making or running the covered works +for you must do so exclusively on your behalf, under your direction +and control, on terms that prohibit them from making any copies of +your copyrighted material outside their relationship with you. + + Conveying under any other circumstances is permitted solely under +the conditions stated below. Sublicensing is not allowed; section 10 +makes it unnecessary. + + 3. Protecting Users' Legal Rights From Anti-Circumvention Law. + + No covered work shall be deemed part of an effective technological +measure under any applicable law fulfilling obligations under article +11 of the WIPO copyright treaty adopted on 20 December 1996, or +similar laws prohibiting or restricting circumvention of such +measures. + + When you convey a covered work, you waive any legal power to forbid +circumvention of technological measures to the extent such circumvention +is effected by exercising rights under this License with respect to +the covered work, and you disclaim any intention to limit operation or +modification of the work as a means of enforcing, against the work's +users, your or third parties' legal rights to forbid circumvention of +technological measures. + + 4. Conveying Verbatim Copies. + + You may convey verbatim copies of the Program's source code as you +receive it, in any medium, provided that you conspicuously and +appropriately publish on each copy an appropriate copyright notice; +keep intact all notices stating that this License and any +non-permissive terms added in accord with section 7 apply to the code; +keep intact all notices of the absence of any warranty; and give all +recipients a copy of this License along with the Program. + + You may charge any price or no price for each copy that you convey, +and you may offer support or warranty protection for a fee. + + 5. Conveying Modified Source Versions. + + You may convey a work based on the Program, or the modifications to +produce it from the Program, in the form of source code under the +terms of section 4, provided that you also meet all of these conditions: + + a) The work must carry prominent notices stating that you modified + it, and giving a relevant date. + + b) The work must carry prominent notices stating that it is + released under this License and any conditions added under section + 7. This requirement modifies the requirement in section 4 to + "keep intact all notices". + + c) You must license the entire work, as a whole, under this + License to anyone who comes into possession of a copy. This + License will therefore apply, along with any applicable section 7 + additional terms, to the whole of the work, and all its parts, + regardless of how they are packaged. This License gives no + permission to license the work in any other way, but it does not + invalidate such permission if you have separately received it. + + d) If the work has interactive user interfaces, each must display + Appropriate Legal Notices; however, if the Program has interactive + interfaces that do not display Appropriate Legal Notices, your + work need not make them do so. + + A compilation of a covered work with other separate and independent +works, which are not by their nature extensions of the covered work, +and which are not combined with it such as to form a larger program, +in or on a volume of a storage or distribution medium, is called an +"aggregate" if the compilation and its resulting copyright are not +used to limit the access or legal rights of the compilation's users +beyond what the individual works permit. Inclusion of a covered work +in an aggregate does not cause this License to apply to the other +parts of the aggregate. + + 6. Conveying Non-Source Forms. + + You may convey a covered work in object code form under the terms +of sections 4 and 5, provided that you also convey the +machine-readable Corresponding Source under the terms of this License, +in one of these ways: + + a) Convey the object code in, or embodied in, a physical product + (including a physical distribution medium), accompanied by the + Corresponding Source fixed on a durable physical medium + customarily used for software interchange. + + b) Convey the object code in, or embodied in, a physical product + (including a physical distribution medium), accompanied by a + written offer, valid for at least three years and valid for as + long as you offer spare parts or customer support for that product + model, to give anyone who possesses the object code either (1) a + copy of the Corresponding Source for all the software in the + product that is covered by this License, on a durable physical + medium customarily used for software interchange, for a price no + more than your reasonable cost of physically performing this + conveying of source, or (2) access to copy the + Corresponding Source from a network server at no charge. + + c) Convey individual copies of the object code with a copy of the + written offer to provide the Corresponding Source. This + alternative is allowed only occasionally and noncommercially, and + only if you received the object code with such an offer, in accord + with subsection 6b. + + d) Convey the object code by offering access from a designated + place (gratis or for a charge), and offer equivalent access to the + Corresponding Source in the same way through the same place at no + further charge. You need not require recipients to copy the + Corresponding Source along with the object code. If the place to + copy the object code is a network server, the Corresponding Source + may be on a different server (operated by you or a third party) + that supports equivalent copying facilities, provided you maintain + clear directions next to the object code saying where to find the + Corresponding Source. Regardless of what server hosts the + Corresponding Source, you remain obligated to ensure that it is + available for as long as needed to satisfy these requirements. + + e) Convey the object code using peer-to-peer transmission, provided + you inform other peers where the object code and Corresponding + Source of the work are being offered to the general public at no + charge under subsection 6d. + + A separable portion of the object code, whose source code is excluded +from the Corresponding Source as a System Library, need not be +included in conveying the object code work. + + A "User Product" is either (1) a "consumer product", which means any +tangible personal property which is normally used for personal, family, +or household purposes, or (2) anything designed or sold for incorporation +into a dwelling. In determining whether a product is a consumer product, +doubtful cases shall be resolved in favor of coverage. For a particular +product received by a particular user, "normally used" refers to a +typical or common use of that class of product, regardless of the status +of the particular user or of the way in which the particular user +actually uses, or expects or is expected to use, the product. A product +is a consumer product regardless of whether the product has substantial +commercial, industrial or non-consumer uses, unless such uses represent +the only significant mode of use of the product. + + "Installation Information" for a User Product means any methods, +procedures, authorization keys, or other information required to install +and execute modified versions of a covered work in that User Product from +a modified version of its Corresponding Source. The information must +suffice to ensure that the continued functioning of the modified object +code is in no case prevented or interfered with solely because +modification has been made. + + If you convey an object code work under this section in, or with, or +specifically for use in, a User Product, and the conveying occurs as +part of a transaction in which the right of possession and use of the +User Product is transferred to the recipient in perpetuity or for a +fixed term (regardless of how the transaction is characterized), the +Corresponding Source conveyed under this section must be accompanied +by the Installation Information. But this requirement does not apply +if neither you nor any third party retains the ability to install +modified object code on the User Product (for example, the work has +been installed in ROM). + + The requirement to provide Installation Information does not include a +requirement to continue to provide support service, warranty, or updates +for a work that has been modified or installed by the recipient, or for +the User Product in which it has been modified or installed. Access to a +network may be denied when the modification itself materially and +adversely affects the operation of the network or violates the rules and +protocols for communication across the network. + + Corresponding Source conveyed, and Installation Information provided, +in accord with this section must be in a format that is publicly +documented (and with an implementation available to the public in +source code form), and must require no special password or key for +unpacking, reading or copying. + + 7. Additional Terms. + + "Additional permissions" are terms that supplement the terms of this +License by making exceptions from one or more of its conditions. +Additional permissions that are applicable to the entire Program shall +be treated as though they were included in this License, to the extent +that they are valid under applicable law. If additional permissions +apply only to part of the Program, that part may be used separately +under those permissions, but the entire Program remains governed by +this License without regard to the additional permissions. + + When you convey a copy of a covered work, you may at your option +remove any additional permissions from that copy, or from any part of +it. (Additional permissions may be written to require their own +removal in certain cases when you modify the work.) You may place +additional permissions on material, added by you to a covered work, +for which you have or can give appropriate copyright permission. + + Notwithstanding any other provision of this License, for material you +add to a covered work, you may (if authorized by the copyright holders of +that material) supplement the terms of this License with terms: + + a) Disclaiming warranty or limiting liability differently from the + terms of sections 15 and 16 of this License; or + + b) Requiring preservation of specified reasonable legal notices or + author attributions in that material or in the Appropriate Legal + Notices displayed by works containing it; or + + c) Prohibiting misrepresentation of the origin of that material, or + requiring that modified versions of such material be marked in + reasonable ways as different from the original version; or + + d) Limiting the use for publicity purposes of names of licensors or + authors of the material; or + + e) Declining to grant rights under trademark law for use of some + trade names, trademarks, or service marks; or + + f) Requiring indemnification of licensors and authors of that + material by anyone who conveys the material (or modified versions of + it) with contractual assumptions of liability to the recipient, for + any liability that these contractual assumptions directly impose on + those licensors and authors. + + All other non-permissive additional terms are considered "further +restrictions" within the meaning of section 10. If the Program as you +received it, or any part of it, contains a notice stating that it is +governed by this License along with a term that is a further +restriction, you may remove that term. If a license document contains +a further restriction but permits relicensing or conveying under this +License, you may add to a covered work material governed by the terms +of that license document, provided that the further restriction does +not survive such relicensing or conveying. + + If you add terms to a covered work in accord with this section, you +must place, in the relevant source files, a statement of the +additional terms that apply to those files, or a notice indicating +where to find the applicable terms. + + Additional terms, permissive or non-permissive, may be stated in the +form of a separately written license, or stated as exceptions; +the above requirements apply either way. + + 8. Termination. + + You may not propagate or modify a covered work except as expressly +provided under this License. Any attempt otherwise to propagate or +modify it is void, and will automatically terminate your rights under +this License (including any patent licenses granted under the third +paragraph of section 11). + + However, if you cease all violation of this License, then your +license from a particular copyright holder is reinstated (a) +provisionally, unless and until the copyright holder explicitly and +finally terminates your license, and (b) permanently, if the copyright +holder fails to notify you of the violation by some reasonable means +prior to 60 days after the cessation. + + Moreover, your license from a particular copyright holder is +reinstated permanently if the copyright holder notifies you of the +violation by some reasonable means, this is the first time you have +received notice of violation of this License (for any work) from that +copyright holder, and you cure the violation prior to 30 days after +your receipt of the notice. + + Termination of your rights under this section does not terminate the +licenses of parties who have received copies or rights from you under +this License. If your rights have been terminated and not permanently +reinstated, you do not qualify to receive new licenses for the same +material under section 10. + + 9. Acceptance Not Required for Having Copies. + + You are not required to accept this License in order to receive or +run a copy of the Program. Ancillary propagation of a covered work +occurring solely as a consequence of using peer-to-peer transmission +to receive a copy likewise does not require acceptance. However, +nothing other than this License grants you permission to propagate or +modify any covered work. These actions infringe copyright if you do +not accept this License. Therefore, by modifying or propagating a +covered work, you indicate your acceptance of this License to do so. + + 10. Automatic Licensing of Downstream Recipients. + + Each time you convey a covered work, the recipient automatically +receives a license from the original licensors, to run, modify and +propagate that work, subject to this License. You are not responsible +for enforcing compliance by third parties with this License. + + An "entity transaction" is a transaction transferring control of an +organization, or substantially all assets of one, or subdividing an +organization, or merging organizations. If propagation of a covered +work results from an entity transaction, each party to that +transaction who receives a copy of the work also receives whatever +licenses to the work the party's predecessor in interest had or could +give under the previous paragraph, plus a right to possession of the +Corresponding Source of the work from the predecessor in interest, if +the predecessor has it or can get it with reasonable efforts. + + You may not impose any further restrictions on the exercise of the +rights granted or affirmed under this License. For example, you may +not impose a license fee, royalty, or other charge for exercise of +rights granted under this License, and you may not initiate litigation +(including a cross-claim or counterclaim in a lawsuit) alleging that +any patent claim is infringed by making, using, selling, offering for +sale, or importing the Program or any portion of it. + + 11. Patents. + + A "contributor" is a copyright holder who authorizes use under this +License of the Program or a work on which the Program is based. The +work thus licensed is called the contributor's "contributor version". + + A contributor's "essential patent claims" are all patent claims +owned or controlled by the contributor, whether already acquired or +hereafter acquired, that would be infringed by some manner, permitted +by this License, of making, using, or selling its contributor version, +but do not include claims that would be infringed only as a +consequence of further modification of the contributor version. For +purposes of this definition, "control" includes the right to grant +patent sublicenses in a manner consistent with the requirements of +this License. + + Each contributor grants you a non-exclusive, worldwide, royalty-free +patent license under the contributor's essential patent claims, to +make, use, sell, offer for sale, import and otherwise run, modify and +propagate the contents of its contributor version. + + In the following three paragraphs, a "patent license" is any express +agreement or commitment, however denominated, not to enforce a patent +(such as an express permission to practice a patent or covenant not to +sue for patent infringement). To "grant" such a patent license to a +party means to make such an agreement or commitment not to enforce a +patent against the party. + + If you convey a covered work, knowingly relying on a patent license, +and the Corresponding Source of the work is not available for anyone +to copy, free of charge and under the terms of this License, through a +publicly available network server or other readily accessible means, +then you must either (1) cause the Corresponding Source to be so +available, or (2) arrange to deprive yourself of the benefit of the +patent license for this particular work, or (3) arrange, in a manner +consistent with the requirements of this License, to extend the patent +license to downstream recipients. "Knowingly relying" means you have +actual knowledge that, but for the patent license, your conveying the +covered work in a country, or your recipient's use of the covered work +in a country, would infringe one or more identifiable patents in that +country that you have reason to believe are valid. + + If, pursuant to or in connection with a single transaction or +arrangement, you convey, or propagate by procuring conveyance of, a +covered work, and grant a patent license to some of the parties +receiving the covered work authorizing them to use, propagate, modify +or convey a specific copy of the covered work, then the patent license +you grant is automatically extended to all recipients of the covered +work and works based on it. + + A patent license is "discriminatory" if it does not include within +the scope of its coverage, prohibits the exercise of, or is +conditioned on the non-exercise of one or more of the rights that are +specifically granted under this License. You may not convey a covered +work if you are a party to an arrangement with a third party that is +in the business of distributing software, under which you make payment +to the third party based on the extent of your activity of conveying +the work, and under which the third party grants, to any of the +parties who would receive the covered work from you, a discriminatory +patent license (a) in connection with copies of the covered work +conveyed by you (or copies made from those copies), or (b) primarily +for and in connection with specific products or compilations that +contain the covered work, unless you entered into that arrangement, +or that patent license was granted, prior to 28 March 2007. + + Nothing in this License shall be construed as excluding or limiting +any implied license or other defenses to infringement that may +otherwise be available to you under applicable patent law. + + 12. No Surrender of Others' Freedom. + + If conditions are imposed on you (whether by court order, agreement or +otherwise) that contradict the conditions of this License, they do not +excuse you from the conditions of this License. If you cannot convey a +covered work so as to satisfy simultaneously your obligations under this +License and any other pertinent obligations, then as a consequence you may +not convey it at all. For example, if you agree to terms that obligate you +to collect a royalty for further conveying from those to whom you convey +the Program, the only way you could satisfy both those terms and this +License would be to refrain entirely from conveying the Program. + + 13. Use with the GNU Affero General Public License. + + Notwithstanding any other provision of this License, you have +permission to link or combine any covered work with a work licensed +under version 3 of the GNU Affero General Public License into a single +combined work, and to convey the resulting work. The terms of this +License will continue to apply to the part which is the covered work, +but the special requirements of the GNU Affero General Public License, +section 13, concerning interaction through a network will apply to the +combination as such. + + 14. Revised Versions of this License. + + The Free Software Foundation may publish revised and/or new versions of +the GNU General Public License from time to time. Such new versions will +be similar in spirit to the present version, but may differ in detail to +address new problems or concerns. + + Each version is given a distinguishing version number. If the +Program specifies that a certain numbered version of the GNU General +Public License "or any later version" applies to it, you have the +option of following the terms and conditions either of that numbered +version or of any later version published by the Free Software +Foundation. If the Program does not specify a version number of the +GNU General Public License, you may choose any version ever published +by the Free Software Foundation. + + If the Program specifies that a proxy can decide which future +versions of the GNU General Public License can be used, that proxy's +public statement of acceptance of a version permanently authorizes you +to choose that version for the Program. + + Later license versions may give you additional or different +permissions. However, no additional obligations are imposed on any +author or copyright holder as a result of your choosing to follow a +later version. + + 15. Disclaimer of Warranty. + + THERE IS NO WARRANTY FOR THE PROGRAM, TO THE EXTENT PERMITTED BY +APPLICABLE LAW. EXCEPT WHEN OTHERWISE STATED IN WRITING THE COPYRIGHT +HOLDERS AND/OR OTHER PARTIES PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY +OF ANY KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING, BUT NOT LIMITED TO, +THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR +PURPOSE. THE ENTIRE RISK AS TO THE QUALITY AND PERFORMANCE OF THE PROGRAM +IS WITH YOU. SHOULD THE PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF +ALL NECESSARY SERVICING, REPAIR OR CORRECTION. + + 16. Limitation of Liability. + + IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING +WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MODIFIES AND/OR CONVEYS +THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, INCLUDING ANY +GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING OUT OF THE +USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED TO LOSS OF +DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY YOU OR THIRD +PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER PROGRAMS), +EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF +SUCH DAMAGES. + + 17. Interpretation of Sections 15 and 16. + + If the disclaimer of warranty and limitation of liability provided +above cannot be given local legal effect according to their terms, +reviewing courts shall apply local law that most closely approximates +an absolute waiver of all civil liability in connection with the +Program, unless a warranty or assumption of liability accompanies a +copy of the Program in return for a fee. + + END OF TERMS AND CONDITIONS + + How to Apply These Terms to Your New Programs + + If you develop a new program, and you want it to be of the greatest +possible use to the public, the best way to achieve this is to make it +free software which everyone can redistribute and change under these terms. + + To do so, attach the following notices to the program. It is safest +to attach them to the start of each source file to most effectively +state the exclusion of warranty; and each file should have at least +the "copyright" line and a pointer to where the full notice is found. + + + Copyright (C) + + This program is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with this program. If not, see . + +Also add information on how to contact you by electronic and paper mail. + + If the program does terminal interaction, make it output a short +notice like this when it starts in an interactive mode: + + Copyright (C) + This program comes with ABSOLUTELY NO WARRANTY; for details type `show w'. + This is free software, and you are welcome to redistribute it + under certain conditions; type `show c' for details. + +The hypothetical commands `show w' and `show c' should show the appropriate +parts of the General Public License. Of course, your program's commands +might be different; for a GUI interface, you would use an "about box". + + You should also get your employer (if you work as a programmer) or school, +if any, to sign a "copyright disclaimer" for the program, if necessary. +For more information on this, and how to apply and follow the GNU GPL, see +. + + The GNU General Public License does not permit incorporating your program +into proprietary programs. If your program is a subroutine library, you +may consider it more useful to permit linking proprietary applications with +the library. If this is what you want to do, use the GNU Lesser General +Public License instead of this License. But first, please read +. diff --git a/06-www/files/selfie.jpg b/06-www/files/selfie.jpg new file mode 100644 index 0000000..500071a Binary files /dev/null and b/06-www/files/selfie.jpg differ diff --git a/06-www/files/style.css b/06-www/files/style.css new file mode 100644 index 0000000..7a64dac --- /dev/null +++ b/06-www/files/style.css @@ -0,0 +1,536 @@ +/* MVP.css v1.14 - https://github.com/andybrewer/mvp */ + +:root { + --active-brightness: 0.85; + --border-radius: 5px; + --box-shadow: 2px 2px 10px; + --color-accent: #118bee15; + --color-bg: #fff; + --color-bg-secondary: #e9e9e9; + --color-link: #118bee; + --color-secondary: #920de9; + --color-secondary-accent: #920de90b; + --color-shadow: #f4f4f4; + --color-table: #118bee; + --color-text: #000; + --color-text-secondary: #999; + --font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Oxygen-Sans, Ubuntu, Cantarell, "Helvetica Neue", sans-serif; + --hover-brightness: 1.2; + --justify-important: center; + --justify-normal: left; + --line-height: 1.5; + --width-card: 285px; + --width-card-medium: 460px; + --width-card-wide: 800px; + --width-content: 1080px; +} + +@media (prefers-color-scheme: dark) { + :root[color-mode="user"] { + --color-accent: #0097fc4f; + --color-bg: #333; + --color-bg-secondary: #555; + --color-link: #0097fc; + --color-secondary: #e20de9; + --color-secondary-accent: #e20de94f; + --color-shadow: #bbbbbb20; + --color-table: #0097fc; + --color-text: #f7f7f7; + --color-text-secondary: #aaa; + } +} + +html { + scroll-behavior: smooth; +} + +@media (prefers-reduced-motion: reduce) { + html { + scroll-behavior: auto; + } +} + +/* Layout */ +article aside { + background: var(--color-secondary-accent); + border-left: 4px solid var(--color-secondary); + padding: 0.01rem 0.8rem; +} + +body { + background: var(--color-bg); + color: var(--color-text); + font-family: var(--font-family); + line-height: var(--line-height); + margin: 0; + overflow-x: hidden; + padding: 0; +} + +footer, +header, +main { + margin: 0 auto; + max-width: var(--width-content); + padding: 3rem 1rem; +} + +hr { + background-color: var(--color-bg-secondary); + border: none; + height: 1px; + margin: 4rem 0; + width: 100%; +} + +section { + display: flex; + flex-wrap: wrap; + justify-content: var(--justify-important); +} + +section img, +article img { + max-width: 100%; +} + +section pre { + overflow: auto; +} + +section aside { + border: 1px solid var(--color-bg-secondary); + border-radius: var(--border-radius); + box-shadow: var(--box-shadow) var(--color-shadow); + margin: 1rem; + padding: 1.25rem; + width: var(--width-card); +} + +section aside:hover { + box-shadow: var(--box-shadow) var(--color-bg-secondary); +} + +[hidden] { + display: none; +} + +/* Headers */ +article header, +div header, +main header { + padding-top: 0; +} + +header { + text-align: var(--justify-important); +} + +header a b, +header a em, +header a i, +header a strong { + margin-left: 0.5rem; + margin-right: 0.5rem; +} + +header nav img { + margin: 1rem 0; +} + +section header { + padding-top: 0; + width: 100%; +} + +/* Nav */ +nav { + align-items: center; + display: flex; + font-weight: bold; + justify-content: space-between; + margin-bottom: 7rem; +} + +nav ul { + list-style: none; + padding: 0; +} + +nav ul li { + display: inline-block; + margin: 0 0.5rem; + position: relative; + text-align: left; +} + +/* Nav Dropdown */ +nav ul li:hover ul { + display: block; +} + +nav ul li ul { + background: var(--color-bg); + border: 1px solid var(--color-bg-secondary); + border-radius: var(--border-radius); + box-shadow: var(--box-shadow) var(--color-shadow); + display: none; + height: auto; + left: -2px; + padding: .5rem 1rem; + position: absolute; + top: 1.7rem; + white-space: nowrap; + width: auto; + z-index: 1; +} + +nav ul li ul::before { + /* fill gap above to make mousing over them easier */ + content: ""; + position: absolute; + left: 0; + right: 0; + top: -0.5rem; + height: 0.5rem; +} + +nav ul li ul li, +nav ul li ul li a { + display: block; +} + +/* Typography */ +code, +samp { + background-color: var(--color-accent); + border-radius: var(--border-radius); + color: var(--color-text); + display: inline-block; + margin: 0 0.1rem; + padding: 0 0.5rem; +} + +details { + margin: 1.3rem 0; +} + +details summary { + font-weight: bold; + cursor: pointer; +} + +h1, +h2, +h3, +h4, +h5, +h6 { + line-height: var(--line-height); + text-wrap: balance; +} + +mark { + padding: 0.1rem; +} + +ol li, +ul li { + padding: 0.2rem 0; +} + +p { + margin: 0.75rem 0; + padding: 0; + width: 100%; +} + +pre { + margin: 1rem 0; + max-width: var(--width-card-wide); + padding: 1rem 0; +} + +pre code, +pre samp { + display: block; + max-width: var(--width-card-wide); + padding: 0.5rem 2rem; + white-space: pre-wrap; +} + +small { + color: var(--color-text-secondary); +} + +sup { + background-color: var(--color-secondary); + border-radius: var(--border-radius); + color: var(--color-bg); + font-size: xx-small; + font-weight: bold; + margin: 0.2rem; + padding: 0.2rem 0.3rem; + position: relative; + top: -2px; +} + +/* Links */ +a { + color: var(--color-link); + display: inline-block; + font-weight: bold; + text-decoration: underline; +} + +a:active { + filter: brightness(var(--active-brightness)); +} + +a:hover { + filter: brightness(var(--hover-brightness)); +} + +a b, +a em, +a i, +a strong, +button, +input[type="submit"] { + border-radius: var(--border-radius); + display: inline-block; + font-size: medium; + font-weight: bold; + line-height: var(--line-height); + margin: 0.5rem 0; + padding: 1rem 2rem; +} + +button, +input[type="submit"] { + font-family: var(--font-family); +} + +button:active, +input[type="submit"]:active { + filter: brightness(var(--active-brightness)); +} + +button:hover, +input[type="submit"]:hover { + cursor: pointer; + filter: brightness(var(--hover-brightness)); +} + +a b, +a strong, +button, +input[type="submit"] { + background-color: var(--color-link); + border: 2px solid var(--color-link); + color: var(--color-bg); +} + +a em, +a i { + border: 2px solid var(--color-link); + border-radius: var(--border-radius); + color: var(--color-link); + display: inline-block; + padding: 1rem 2rem; +} + +article aside a { + color: var(--color-secondary); +} + +/* Images */ +figure { + margin: 0; + padding: 0; +} + +figure img { + max-width: 100%; +} + +figure figcaption { + color: var(--color-text-secondary); +} + +/* Forms */ +button:disabled, +input:disabled { + background: var(--color-bg-secondary); + border-color: var(--color-bg-secondary); + color: var(--color-text-secondary); + cursor: not-allowed; +} + +button[disabled]:hover, +input[type="submit"][disabled]:hover { + filter: none; +} + +form { + border: 1px solid var(--color-bg-secondary); + border-radius: var(--border-radius); + box-shadow: var(--box-shadow) var(--color-shadow); + display: block; + max-width: var(--width-card-wide); + min-width: var(--width-card); + padding: 1.5rem; + text-align: var(--justify-normal); +} + +form header { + margin: 1.5rem 0; + padding: 1.5rem 0; +} + +input, +label, +select, +textarea { + display: block; + font-size: inherit; + max-width: var(--width-card-wide); +} + +input[type="checkbox"], +input[type="radio"] { + display: inline-block; +} + +input[type="checkbox"]+label, +input[type="radio"]+label { + display: inline-block; + font-weight: normal; + position: relative; + top: 1px; +} + +input[type="range"] { + padding: 0.4rem 0; +} + +input, +select, +textarea { + border: 1px solid var(--color-bg-secondary); + border-radius: var(--border-radius); + margin-bottom: 1rem; + padding: 0.4rem 0.8rem; +} + +input[type="text"], +textarea { + width: calc(100% - 1.6rem); +} + +input[readonly], +textarea[readonly] { + background-color: var(--color-bg-secondary); +} + +label { + font-weight: bold; + margin-bottom: 0.2rem; +} + +/* Popups */ +dialog { + border: 1px solid var(--color-bg-secondary); + border-radius: var(--border-radius); + box-shadow: var(--box-shadow) var(--color-shadow); + position: fixed; + top: 50%; + left: 50%; + transform: translate(-50%, -50%); + width: 50%; + z-index: 999; +} + +/* Tables */ +table { + border: 1px solid var(--color-bg-secondary); + border-radius: var(--border-radius); + border-spacing: 0; + display: inline-block; + max-width: 100%; + overflow-x: auto; + padding: 0; + white-space: nowrap; +} + +table td, +table th, +table tr { + padding: 0.4rem 0.8rem; + text-align: var(--justify-important); +} + +table thead { + background-color: var(--color-table); + border-collapse: collapse; + border-radius: var(--border-radius); + color: var(--color-bg); + margin: 0; + padding: 0; +} + +table thead th:first-child { + border-top-left-radius: var(--border-radius); +} + +table thead th:last-child { + border-top-right-radius: var(--border-radius); +} + +table thead th:first-child, +table tr td:first-child { + text-align: var(--justify-normal); +} + +table tr:nth-child(even) { + background-color: var(--color-accent); +} + +/* Quotes */ +blockquote { + display: block; + font-size: x-large; + line-height: var(--line-height); + margin: 1rem auto; + max-width: var(--width-card-medium); + padding: 1.5rem 1rem; + text-align: var(--justify-important); +} + +blockquote footer { + color: var(--color-text-secondary); + display: block; + font-size: small; + line-height: var(--line-height); + padding: 1.5rem 0; +} + +/* Scrollbars */ +* { + scrollbar-width: thin; + scrollbar-color: rgb(202, 202, 232) auto; +} + +*::-webkit-scrollbar { + width: 5px; + height: 5px; +} + +*::-webkit-scrollbar-track { + background: transparent; +} + +*::-webkit-scrollbar-thumb { + background-color: rgb(202, 202, 232); + border-radius: 10px; +} diff --git a/06-www/logger-unsafe.mjs b/06-www/logger-unsafe.mjs new file mode 100644 index 0000000..caad244 --- /dev/null +++ b/06-www/logger-unsafe.mjs @@ -0,0 +1,9 @@ +import fs from "fs/promises"; + +const logFh = await fs.open("./access.log", "a"); + +async function log(msg) { + await logFh.write(`${msg}\n`); +} + +export { log }; diff --git a/06-www/logger.mjs b/06-www/logger.mjs new file mode 100644 index 0000000..17c5e71 --- /dev/null +++ b/06-www/logger.mjs @@ -0,0 +1,11 @@ +import fs from "fs/promises"; + +const logFh = await fs.open("./access.log", "a"); +const logStream = logFh.createWriteStream(); + +const log = function (msg) { + console.log(msg); + logStream.write(`${msg}\n`); +}; + +export { log }; diff --git a/06-www/media-types.mjs b/06-www/media-types.mjs new file mode 100644 index 0000000..57764e7 --- /dev/null +++ b/06-www/media-types.mjs @@ -0,0 +1,26 @@ +const types = { + "text/html": ["html", "htm"], + "text/plain": ["txt", "text"], + "text/css": ["css"], + "application/javascript": ["js", "mjs"], + "application/json": ["json"], + "image/jpeg": ["jpeg", "jpg"], + "image/x-icon": ["ico"], + "image/avif": ["avif"], + "audio/mpeg": ["mpga", "mp2", "mp2a", "mp3", "m2a", "m3a"], + "video/mp4": ["mp4", "mp4v", "mpg4"], + "video/mpeg": ["mpeg", "mpg", "mpe", "m1v", "m2v"], + "video/ogg": ["ogv"], + "video/quicktime": ["qt", "mov"], + "video/x-matroska": ["mkv", "mk3d", "mks"], + "video/x-msvideo": ["avi"], +}; +const extMap = new Map(); + +for (let [type, extensions] of Object.entries(types)) { + for (let extension of extensions) { + extMap.set(`.${extension}`, type); + } +} + +export default extMap; diff --git a/06-www/w3-utils.mjs b/06-www/w3-utils.mjs new file mode 100644 index 0000000..70016de --- /dev/null +++ b/06-www/w3-utils.mjs @@ -0,0 +1,64 @@ +import fs from "fs/promises"; +import { hrtime } from "process"; + +async function exitIfNotDir(path) { + try { + const pathStat = await fs.stat(path); + if (!pathStat.isDirectory()) { + console.error(`${path} is not a directory.`); + process.exit(1); + } + } catch { + console.error(`${path} does not exists or cannot be opened.`); + process.exit(1); + } +} + +function isStringTrue(str) { + if (str === "true") { + return true; + } + return false; +} + +function hasTrailingSlash(path) { + return path[path.length - 1] === "/"; +} + +async function tryOpenFile(path, index) { + let file = path; + if (hasTrailingSlash(path)) { + if (index) { + file = `${path}index.html`; + } else { + return { found: false }; + } + } + + let fh; + try { + fh = await fs.open(file); + } catch (e) { + console.error(e); + return { found: false }; + } + + const fileStat = await fh.stat(); + if (fileStat.isDirectory()) { + //spiegare caso eccezionale - così agganciarsi ai tanti controlli di sicurezza da fare nel caso reale + await fh.close(); + return { found: false }; + } + return { found: true, fh, fileStat }; +} + +function generateLogString(req, res, pathname, startTime) { + const endTime = hrtime.bigint(); + const duration = (endTime - startTime) / BigInt(1e6); + const timestamp = new Date().toISOString(); + const ip = req.socket.remoteAddress; + const httpDetails = `"${req.method} ${pathname}" ${res.statusCode}`; + return `${ip} - ${timestamp} ${httpDetails} - ${duration}ms`; +} + +export { exitIfNotDir, isStringTrue, tryOpenFile, generateLogString }; diff --git a/07-eloop/01-linear-prime.mjs b/07-eloop/01-linear-prime.mjs new file mode 100644 index 0000000..c1da203 --- /dev/null +++ b/07-eloop/01-linear-prime.mjs @@ -0,0 +1,5 @@ +import { isPrime } from "./cpu-intensive.mjs"; + +console.log("Script started"); +console.log(`Is it prime? ${isPrime(2971215073)}`); +console.log("Script ended"); diff --git a/07-eloop/02-callback-prime.mjs b/07-eloop/02-callback-prime.mjs new file mode 100644 index 0000000..e98541a --- /dev/null +++ b/07-eloop/02-callback-prime.mjs @@ -0,0 +1,13 @@ +import { PRIME_BIG, isPrime } from "./cpu-intensive.mjs"; + +function calcPrime(num, cb) { + const isNumPrime = isPrime(num); + cb(isNumPrime); +} +console.log("Script started"); + +calcPrime(PRIME_BIG, (res) => { + console.log(`${PRIME_BIG} is prime? ${res}`); +}); + +console.log("Script ended"); diff --git a/07-eloop/03-callback-timeout.mjs b/07-eloop/03-callback-timeout.mjs new file mode 100644 index 0000000..31e99ac --- /dev/null +++ b/07-eloop/03-callback-timeout.mjs @@ -0,0 +1,15 @@ +import { PRIME_BIG, isPrime } from "./cpu-intensive.mjs"; + +function calcPrime(num, cb) { + setTimeout(() => { + const isNumPrime = isPrime(num); + cb(isNumPrime); + }, 1000); +} +console.log("Script started"); + +calcPrime(PRIME_BIG, (res) => { + console.log(`${PRIME_BIG} is prime? ${res}`); +}); + +console.log("Script ended (?)"); diff --git a/07-eloop/04-timers-poll-check.mjs b/07-eloop/04-timers-poll-check.mjs new file mode 100644 index 0000000..06ed493 --- /dev/null +++ b/07-eloop/04-timers-poll-check.mjs @@ -0,0 +1,18 @@ +import fs from "fs"; +import { isPrime } from "./cpu-intensive.mjs"; + +console.log("running the script..."); + +fs.readFile("./number.txt", { encoding: "utf-8" }, (err, data) => { + const numToCheck = Number.parseInt(data); + console.log(`Number to check: ${numToCheck}`); + + const primeRes = isPrime(numToCheck); + + setTimeout(() => { + console.log(`[timeout] ${numToCheck} is prime? ${primeRes}`); + }, 0); + setImmediate(() => { + console.log(`[immediate] ${numToCheck} is prime? ${primeRes}`); + }); +}); diff --git a/07-eloop/05-draining-queues.mjs b/07-eloop/05-draining-queues.mjs new file mode 100644 index 0000000..82e07ea --- /dev/null +++ b/07-eloop/05-draining-queues.mjs @@ -0,0 +1,18 @@ +setTimeout(() => { + console.log(`[timeout] 1`); +}, 0); +setImmediate(() => { + console.log(`[immediate] 1`); +}); +setTimeout(() => { + console.log(`[timeout] 2`); +}, 0); +setImmediate(() => { + console.log(`[immediate] 2`); +}); +setTimeout(() => { + console.log(`[timeout] 3`); +}, 0); +setImmediate(() => { + console.log(`[immediate] 3`); +}); diff --git a/07-eloop/06-scheduling-timers.mjs b/07-eloop/06-scheduling-timers.mjs new file mode 100644 index 0000000..12017e8 --- /dev/null +++ b/07-eloop/06-scheduling-timers.mjs @@ -0,0 +1,16 @@ +setTimeout(() => { + console.log(`[timeout 0]`); +}, 0); +setImmediate(() => { + console.log(`[immediate 0]`); + setTimeout(() => { + console.log(`[timeout] scheduled from immediate 0`); + }, 0); + setImmediate(() => { + console.log(`[immediate] scheduled from immediate 0`); + }); + let i = 0; + while (i < 10000000) { + i++; + } +}); diff --git a/07-eloop/07-microtasks.mjs b/07-eloop/07-microtasks.mjs new file mode 100644 index 0000000..fee182f --- /dev/null +++ b/07-eloop/07-microtasks.mjs @@ -0,0 +1,17 @@ +setImmediate(() => { + console.log("first immediate"); + Promise.resolve() + .then(() => { + console.log("then 1"); + return; + }) + .then(() => { + console.log("then 2"); + }); + queueMicrotask(() => { + console.log("queueMicrotask"); + }); +}); +setImmediate(() => { + console.log("last immediate"); +}); diff --git a/07-eloop/08-next-tick.mjs b/07-eloop/08-next-tick.mjs new file mode 100644 index 0000000..ed9a041 --- /dev/null +++ b/07-eloop/08-next-tick.mjs @@ -0,0 +1,20 @@ +setImmediate(() => { + console.log("first immediate"); + Promise.resolve() + .then(() => { + console.log("then 1"); + return; + }) + .then(() => { + console.log("then 2"); + }); + queueMicrotask(() => { + console.log("queueMicrotask"); + }); + process.nextTick(() => { + console.log("next tick"); + }); +}); +setImmediate(() => { + console.log("last immediate"); +}); diff --git a/07-eloop/09-partitioned.mjs b/07-eloop/09-partitioned.mjs new file mode 100644 index 0000000..2c0a20e --- /dev/null +++ b/07-eloop/09-partitioned.mjs @@ -0,0 +1,14 @@ +import { PRIME_MED, isPrimeAsync } from "./cpu-intensive.mjs"; +import { hrtime } from "process"; + +let lastTime = hrtime.bigint(); +const interval = setInterval(() => { + const now = hrtime.bigint(); + const duration = (now - lastTime) / BigInt(1e6); + console.log(`[interval] ${duration}ms passed since last one`); +}, 500); + +isPrimeAsync(PRIME_MED, function (res) { + console.log(`${PRIME_MED} is prime? ${res}`); + clearInterval(interval); +}); diff --git a/07-eloop/10-http-prime-blocking.mjs b/07-eloop/10-http-prime-blocking.mjs new file mode 100644 index 0000000..a36e056 --- /dev/null +++ b/07-eloop/10-http-prime-blocking.mjs @@ -0,0 +1,26 @@ +import http from "http"; +import { isPrime } from "./cpu-intensive.mjs"; +import { generateLogString } from "./eloop-utils.mjs"; +import { hrtime } from "process"; + +const server = http.createServer(); +server.on("request", async (req, res) => { + const startTime = hrtime.bigint(); + + const { searchParams, pathname } = new URL( + req.url, + `http://${req.headers.host}` + ); + const num = parseInt(searchParams.get("num")); + res.on("finish", () => { + console.log(generateLogString(req, res, pathname, startTime)); + }); + + const primeRes = isPrime(num); + + res.end(`${num} is prime? ${primeRes}`); +}); + +server.listen(3000, "127.0.0.1", () => { + console.log(`Web server running`); +}); diff --git a/07-eloop/11-http-prime-partitioned.mjs b/07-eloop/11-http-prime-partitioned.mjs new file mode 100644 index 0000000..1ef1bff --- /dev/null +++ b/07-eloop/11-http-prime-partitioned.mjs @@ -0,0 +1,26 @@ +import http from "http"; +import { isPrimeAsync } from "./cpu-intensive.mjs"; +import { generateLogString } from "./eloop-utils.mjs"; +import { hrtime } from "process"; + +const server = http.createServer(); +server.on("request", async (req, res) => { + const startTime = hrtime.bigint(); + + const { searchParams, pathname } = new URL( + req.url, + `http://${req.headers.host}` + ); + const num = parseInt(searchParams.get("num")); + res.on("finish", () => { + console.log(generateLogString(req, res, pathname, startTime)); + }); + + isPrimeAsync(num, (primeRes) => { + res.end(`${num} is prime? ${primeRes}`); + }); +}); + +server.listen(3000, "127.0.0.1", () => { + console.log(`Web server running`); +}); diff --git a/07-eloop/12-child-exec-file.mjs b/07-eloop/12-child-exec-file.mjs new file mode 100644 index 0000000..ba36246 --- /dev/null +++ b/07-eloop/12-child-exec-file.mjs @@ -0,0 +1,11 @@ +import { execFile } from "child_process"; + +execFile("node", ["isprime.mjs", 3], (error, stdout, stderr) => { + if (error) { + console.log("The number is NOT prime"); + } else { + console.log("The number is prime"); + } + console.log(`stdout: ${stdout}`); + console.log(`stderr: ${stderr}`); +}); diff --git a/07-eloop/13-child-exec.mjs b/07-eloop/13-child-exec.mjs new file mode 100644 index 0000000..96af298 --- /dev/null +++ b/07-eloop/13-child-exec.mjs @@ -0,0 +1,9 @@ +import { exec } from "child_process"; + +exec("./isprime.mjs 3", (error, stdout, stderr) => { + if (error) { + console.log("The number is NOT prime"); + } else { + console.log("The number is prime"); + } +}); diff --git a/07-eloop/14-http-prime-child-node.mjs b/07-eloop/14-http-prime-child-node.mjs new file mode 100644 index 0000000..ea3ebec --- /dev/null +++ b/07-eloop/14-http-prime-child-node.mjs @@ -0,0 +1,30 @@ +import http from "http"; +import { generateLogString } from "./eloop-utils.mjs"; +import { hrtime } from "process"; +import { exec } from "child_process"; + +const server = http.createServer(); +server.on("request", async (req, res) => { + const startTime = hrtime.bigint(); + + const { searchParams, pathname } = new URL( + req.url, + `http://${req.headers.host}` + ); + const num = parseInt(searchParams.get("num")); + res.on("finish", () => { + console.log(generateLogString(req, res, pathname, startTime)); + }); + + exec(`node isprime.mjs ${num}`, (error, stdout, stderr) => { + if (error) { + res.end(`${num} is prime? false`); + } else { + res.end(`${num} is prime? true`); + } + }); +}); + +server.listen(3000, "127.0.0.1", () => { + console.log(`Web server running`); +}); diff --git a/07-eloop/15-http-prime-child-c.mjs b/07-eloop/15-http-prime-child-c.mjs new file mode 100644 index 0000000..72b12f8 --- /dev/null +++ b/07-eloop/15-http-prime-child-c.mjs @@ -0,0 +1,30 @@ +import http from "http"; +import { generateLogString } from "./eloop-utils.mjs"; +import { hrtime } from "process"; +import { exec } from "child_process"; + +const server = http.createServer(); +server.on("request", async (req, res) => { + const startTime = hrtime.bigint(); + + const { searchParams, pathname } = new URL( + req.url, + `http://${req.headers.host}` + ); + const num = parseInt(searchParams.get("num")); + res.on("finish", () => { + console.log(generateLogString(req, res, pathname, startTime)); + }); + + exec(`./isprime ${num}`, (error, stdout, stderr) => { + if (error) { + res.end(`${num} is prime? false`); + } else { + res.end(`${num} is prime? true`); + } + }); +}); + +server.listen(3000, "127.0.0.1", () => { + console.log(`Web server running`); +}); diff --git a/07-eloop/16-child-spawn.mjs b/07-eloop/16-child-spawn.mjs new file mode 100644 index 0000000..a6df6fc --- /dev/null +++ b/07-eloop/16-child-spawn.mjs @@ -0,0 +1,15 @@ +import { spawn } from "child_process"; + +const ls = spawn("ls"); + +ls.stdout.on("data", (data) => { + console.log(`ls stdout ${data}`); +}); +ls.stderr.on("data", (data) => { + console.error(`ls stderr: ${data}`); +}); +ls.on("close", (code) => { + if (code !== 0) { + console.log(`ls process exited with code ${code}`); + } +}); diff --git a/07-eloop/17-child-spawn-pipe.mjs b/07-eloop/17-child-spawn-pipe.mjs new file mode 100644 index 0000000..557fb16 --- /dev/null +++ b/07-eloop/17-child-spawn-pipe.mjs @@ -0,0 +1,33 @@ +import { spawn } from "child_process"; + +const ls = spawn("ls", ["*.mjs"], { shell: true }); +const grep = spawn("grep", ["http"]); + +ls.stdout.on("data", (data) => { + grep.stdin.write(data); +}); + +ls.stderr.on("data", (data) => { + console.error(`ls stderr: ${data}`); +}); + +ls.on("close", (code) => { + if (code !== 0) { + console.log(`ls process exited with code ${code}`); + } + grep.stdin.end(); +}); + +grep.stdout.on("data", (data) => { + console.log(data.toString()); +}); + +grep.stderr.on("data", (data) => { + console.error(`grep stderr: ${data}`); +}); + +grep.on("close", (code) => { + if (code !== 0) { + console.log(`grep process exited with code ${code}`); + } +}); diff --git a/07-eloop/18-http-prime-child-spawn.mjs b/07-eloop/18-http-prime-child-spawn.mjs new file mode 100644 index 0000000..d1234bf --- /dev/null +++ b/07-eloop/18-http-prime-child-spawn.mjs @@ -0,0 +1,31 @@ +import http from "http"; +import { generateLogString } from "./eloop-utils.mjs"; +import { hrtime } from "process"; +import { spawn } from "child_process"; + +const server = http.createServer(); +server.on("request", async (req, res) => { + const startTime = hrtime.bigint(); + + const { searchParams, pathname } = new URL( + req.url, + `http://${req.headers.host}` + ); + const num = parseInt(searchParams.get("num")); + res.on("finish", () => { + console.log(generateLogString(req, res, pathname, startTime)); + }); + + const proc = spawn("node", ["isprime.mjs", num]); + proc.on("close", (code) => { + if (code !== 0) { + res.end(`${num} is prime? false`); + } else { + res.end(`${num} is prime? true`); + } + }); +}); + +server.listen(3000, "127.0.0.1", () => { + console.log(`Web server running`); +}); diff --git a/07-eloop/19-child-fork.mjs b/07-eloop/19-child-fork.mjs new file mode 100644 index 0000000..0a35a70 --- /dev/null +++ b/07-eloop/19-child-fork.mjs @@ -0,0 +1,15 @@ +import { fork } from "child_process"; +import { PRIME_HUGE } from "./cpu-intensive.mjs"; + +const interval = setInterval(() => { + console.log( + `${new Date().toISOString()} - isprime() is still running in another process..` + ); +}, 1000); + +const child = fork("isprime-child.mjs"); +child.send(PRIME_HUGE); +child.on("message", (result) => { + console.log(`${PRIME_HUGE} is prime? ${result}`); + clearInterval(interval); +}); diff --git a/07-eloop/20-http-prime-child-fork.mjs b/07-eloop/20-http-prime-child-fork.mjs new file mode 100644 index 0000000..7439e01 --- /dev/null +++ b/07-eloop/20-http-prime-child-fork.mjs @@ -0,0 +1,32 @@ +import http from "http"; +import { generateLogString } from "./eloop-utils.mjs"; +import { hrtime } from "process"; +import { fork } from "child_process"; + +const server = http.createServer(); +server.on("request", async (req, res) => { + const startTime = hrtime.bigint(); + + const { searchParams, pathname } = new URL( + req.url, + `http://${req.headers.host}` + ); + const num = parseInt(searchParams.get("num")); + res.on("finish", () => { + console.log(generateLogString(req, res, pathname, startTime)); + }); + + const child = fork("isprime-child.mjs"); + child.send(num); + child.on("message", (result) => { + if (result) { + res.end(`${num} is prime? true`); + } else { + res.end(`${num} is prime? false`); + } + }); +}); + +server.listen(3000, "127.0.0.1", () => { + console.log(`Web server running`); +}); diff --git a/07-eloop/21-self-fork.mjs b/07-eloop/21-self-fork.mjs new file mode 100644 index 0000000..0ad5379 --- /dev/null +++ b/07-eloop/21-self-fork.mjs @@ -0,0 +1,22 @@ +import { PRIME_HUGE, isPrime } from "./cpu-intensive.mjs"; +import { fork } from "child_process"; + +if (process.argv[2] === "child") { + process.on("message", (msg) => { + const result = isPrime(msg); + process.send(result); + process.exit(); + }); +} else { + const interval = setInterval(() => { + console.log( + `${new Date().toISOString()} - isprime() is still running in another process..` + ); + }, 1000); + const child = fork("21-self-fork.mjs", ["child"]); + child.send(PRIME_HUGE); + child.on("message", (result) => { + console.log(`${PRIME_HUGE} is prime? ${result}`); + clearInterval(interval); + }); +} diff --git a/07-eloop/22-http-prime-cluster.mjs b/07-eloop/22-http-prime-cluster.mjs new file mode 100644 index 0000000..f3f6290 --- /dev/null +++ b/07-eloop/22-http-prime-cluster.mjs @@ -0,0 +1,41 @@ +import cluster from "cluster"; +import http from "http"; +import { availableParallelism } from "os"; +import process, { hrtime } from "process"; +import { generateLogString } from "./eloop-utils.mjs"; +import { isPrime } from "./cpu-intensive.mjs"; + +const numCPUs = availableParallelism(); + +if (cluster.isPrimary) { + console.log(`Primary ${process.pid} is running`); + + for (let i = 0; i < numCPUs; i++) { + cluster.fork(); + } + + cluster.on("exit", (worker, code) => { + console.log(`worker ${worker.process.pid} exited with code ${code}`); + }); +} else { + const server = http.createServer(); + server.on("request", (req, res) => { + const startTime = hrtime.bigint(); + + const { searchParams, pathname } = new URL( + req.url, + `http://${req.headers.host}` + ); + const num = parseInt(searchParams.get("num")); + res.on("finish", () => { + const logMsg = generateLogString(req, res, pathname, startTime); + console.log(`${process.pid} - ${logMsg}`); + }); + + const primeRes = isPrime(num); + res.end(`${num} is prime? ${primeRes}`); + }); + server.listen(3000); + + console.log(`Worker ${process.pid} started`); +} diff --git a/07-eloop/23-worker-thread.mjs b/07-eloop/23-worker-thread.mjs new file mode 100644 index 0000000..be0a4c8 --- /dev/null +++ b/07-eloop/23-worker-thread.mjs @@ -0,0 +1,22 @@ +import { isMainThread, workerData, Worker, parentPort } from "worker_threads"; +import { PRIME_HUGE, isPrime } from "./cpu-intensive.mjs"; + +if (isMainThread) { + console.log(`Main thread pid is ${process.pid}`); + const interval = setInterval(() => { + console.log( + `${new Date().toISOString()} - isprime() is still running in a worker thread..` + ); + }, 1000); + const worker = new Worker("./23-worker-thread.mjs", { + workerData: { num: PRIME_HUGE }, + }); + worker.on("message", (result) => { + console.log(`${PRIME_HUGE} is prime? ${result}`); + clearInterval(interval); + }); +} else { + const result = isPrime(workerData.num); + parentPort.postMessage(result); + process.exit(); +} diff --git a/07-eloop/24-http-prime-workers.mjs b/07-eloop/24-http-prime-workers.mjs new file mode 100644 index 0000000..3b3a56d --- /dev/null +++ b/07-eloop/24-http-prime-workers.mjs @@ -0,0 +1,35 @@ +import http from "http"; +import process, { hrtime } from "process"; +import { generateLogString } from "./eloop-utils.mjs"; +import { isPrime } from "./cpu-intensive.mjs"; +import { Worker, isMainThread, parentPort, workerData } from "worker_threads"; + +if (isMainThread) { + const server = http.createServer(); + server.on("request", (req, res) => { + const startTime = hrtime.bigint(); + + const { searchParams, pathname } = new URL( + req.url, + `http://${req.headers.host}` + ); + const num = parseInt(searchParams.get("num")); + res.on("finish", () => { + console.log(generateLogString(req, res, pathname, startTime)); + }); + + const worker = new Worker("./24-http-prime-workers.mjs", { + workerData: { num: num }, + }); + worker.on("message", (result) => { + res.end(`${num} is prime? ${result}`); + }); + }); + server.listen(3000); + + console.log(`Main thread (server) started with pid ${process.pid}`); +} else { + const result = isPrime(workerData.num); + parentPort.postMessage(result); + process.exit(); +} diff --git a/07-eloop/25-http-prime-workers-pool.mjs b/07-eloop/25-http-prime-workers-pool.mjs new file mode 100644 index 0000000..6a786f7 --- /dev/null +++ b/07-eloop/25-http-prime-workers-pool.mjs @@ -0,0 +1,51 @@ +import http from "http"; +import process, { hrtime } from "process"; +import { generateLogString } from "./eloop-utils.mjs"; +import { isPrime } from "./cpu-intensive.mjs"; +import { Worker, isMainThread, parentPort } from "worker_threads"; +import { availableParallelism } from "os"; + +const numCPUs = availableParallelism(); + +if (isMainThread) { + const workerPool = []; + for (let i = 0; i < numCPUs; i++) { + workerPool.push(new Worker("./25-http-prime-workers-pool.mjs")); + } + console.log(`Created ${workerPool.length} worker threads`); + + const server = http.createServer(); + server.on("request", (req, res) => { + const startTime = hrtime.bigint(); + + const { searchParams, pathname } = new URL( + req.url, + `http://${req.headers.host}` + ); + const num = parseInt(searchParams.get("num")); + res.on("finish", () => { + console.log(generateLogString(req, res, pathname, startTime)); + }); + + const wk = workerPool.pop(); + if (wk) { + const listener = (result) => { + wk.off("message", listener); + workerPool.push(wk); + res.end(`${num} is prime? ${result}`); + }; + wk.on("message", listener); + wk.postMessage(num); + } else { + res.statusCode = 503; + res.end(); + } + }); + server.listen(3000); + console.log(`Main thread (server) started with pid ${process.pid}`); +} else { + parentPort.on("message", (msg) => { + const result = isPrime(msg); + parentPort.postMessage(result); + }); +} diff --git a/07-eloop/26-http-prime-workers-pool-2.mjs b/07-eloop/26-http-prime-workers-pool-2.mjs new file mode 100644 index 0000000..add65a9 --- /dev/null +++ b/07-eloop/26-http-prime-workers-pool-2.mjs @@ -0,0 +1,43 @@ +import http from "http"; +import process, { hrtime } from "process"; +import { generateLogString } from "./eloop-utils.mjs"; +import { Worker } from "worker_threads"; +import { availableParallelism } from "os"; + +const numCPUs = availableParallelism(); + +const workerPool = []; +for (let i = 0; i < numCPUs; i++) { + workerPool.push(new Worker("./isprime-worker.mjs")); +} +console.log(`Created ${workerPool.length} worker threads`); + +const server = http.createServer(); +server.on("request", (req, res) => { + const startTime = hrtime.bigint(); + + const { searchParams, pathname } = new URL( + req.url, + `http://${req.headers.host}` + ); + const num = parseInt(searchParams.get("num")); + res.on("finish", () => { + console.log(generateLogString(req, res, pathname, startTime)); + }); + + const wk = workerPool.pop(); + if (wk) { + const listener = (result) => { + wk.off("message", listener); + workerPool.push(wk); + res.end(`${num} is prime? ${result}`); + }; + wk.on("message", listener); + wk.postMessage(num); + } else { + res.statusCode = 503; + res.end(); + } +}); +server.listen(3000); +console.log(`Main thread (server) started with pid ${process.pid}`); diff --git a/07-eloop/cpu-intensive.mjs b/07-eloop/cpu-intensive.mjs new file mode 100644 index 0000000..6b49250 --- /dev/null +++ b/07-eloop/cpu-intensive.mjs @@ -0,0 +1,51 @@ +function isPrime(num) { + if (num < 2) { + return false; + } + for (let i = 2; i < num; i++) { + if (num % i === 0) { + return false; + } + } + return true; +} + +function factorial(n) { + return n != 1 ? n * factorial(n - 1) : 1; +} + +const PRIME_SMALL = 65537; +const PRIME_MED = 999331; +const PRIME_BIG = 39916801; +const PRIME_HUGE = 2971215073; +const PRIME_ENOR = 87178291199; + +function isPrimeAsync(num, cb) { + process.nextTick(() => { + if (num < 2) { + return cb(false); + } + isPrimeRecursive(num, 2, cb); + }); +} + +function isPrimeRecursive(n, i, cb) { + if (i >= n) { + return cb(true); + } + if (n % i === 0) { + return cb(false); + } + setImmediate(isPrimeRecursive.bind(null, n, i + 1, cb)); +} + +export { + isPrime, + factorial, + PRIME_SMALL, + PRIME_MED, + PRIME_BIG, + PRIME_HUGE, + PRIME_ENOR, + isPrimeAsync, +}; diff --git a/07-eloop/eloop-utils.mjs b/07-eloop/eloop-utils.mjs new file mode 100644 index 0000000..6faf9b3 --- /dev/null +++ b/07-eloop/eloop-utils.mjs @@ -0,0 +1,12 @@ +import { hrtime } from "process"; + +function generateLogString(req, res, pathname, startTime) { + const endTime = hrtime.bigint(); + const duration = (endTime - startTime) / BigInt(1e6); + const timestamp = new Date().toISOString(); + const ip = req.socket.remoteAddress; + const httpDetails = `"${req.method} ${pathname}" ${res.statusCode}`; + return `${ip} - ${timestamp} ${httpDetails} - ${duration}ms`; +} + +export { generateLogString }; diff --git a/07-eloop/isprime-child.mjs b/07-eloop/isprime-child.mjs new file mode 100644 index 0000000..cfda533 --- /dev/null +++ b/07-eloop/isprime-child.mjs @@ -0,0 +1,7 @@ +import { isPrime } from "./cpu-intensive.mjs"; + +process.on("message", (msg) => { + const result = isPrime(msg); + process.send(result); + process.exit(); +}); diff --git a/07-eloop/isprime-worker.mjs b/07-eloop/isprime-worker.mjs new file mode 100644 index 0000000..0217b5f --- /dev/null +++ b/07-eloop/isprime-worker.mjs @@ -0,0 +1,7 @@ +import { isPrime } from "./cpu-intensive.mjs"; +import { parentPort } from "worker_threads"; + +parentPort.on("message", (msg) => { + const result = isPrime(msg); + parentPort.postMessage(result); +}); diff --git a/07-eloop/isprime.c b/07-eloop/isprime.c new file mode 100644 index 0000000..2183757 --- /dev/null +++ b/07-eloop/isprime.c @@ -0,0 +1,25 @@ +#include +#include + +int main(int argc, char **argv) +{ + long i, num; + if (argc < 2) + { + return 1; + } + + num = strtol(argv[1], NULL, 10); + if (num < 2) + { + return 1; + } + for (i = 2; i < num; ++i) + { + if (num % i == 0) + { + return 1; + } + } + return 0; +} \ No newline at end of file diff --git a/07-eloop/isprime.mjs b/07-eloop/isprime.mjs new file mode 100755 index 0000000..e6e3691 --- /dev/null +++ b/07-eloop/isprime.mjs @@ -0,0 +1,16 @@ +#!/usr/bin/env node +import { isPrime } from "./cpu-intensive.mjs"; + +const num = parseInt(process.argv[2]); +if (isNaN(num)) { + console.error(`${num} is not a number`); + process.exit(2); +} + +if (isPrime(num)) { + console.log(`${num} is prime`); + process.exit(0); +} else { + console.log(`${num} is not prime`); + process.exit(1); +} diff --git a/07-eloop/number.txt b/07-eloop/number.txt new file mode 100644 index 0000000..01da44e --- /dev/null +++ b/07-eloop/number.txt @@ -0,0 +1 @@ +39916801 \ No newline at end of file diff --git a/08-pkg/01-check-strings.js b/08-pkg/01-check-strings.js new file mode 100644 index 0000000..b13aae9 --- /dev/null +++ b/08-pkg/01-check-strings.js @@ -0,0 +1,13 @@ +const { isString } = require("./modules/is-string"); + +const toCheck = [ + "a string", + 2, + { prop: "I'm an object" }, + function () {}, + true, +]; + +for (const str of toCheck) { + console.log(isString(str)); +} diff --git a/08-pkg/02-manipulate-strings.js b/08-pkg/02-manipulate-strings.js new file mode 100644 index 0000000..8a4b81c --- /dev/null +++ b/08-pkg/02-manipulate-strings.js @@ -0,0 +1,17 @@ +const { urlify } = require("./modules/string-utils"); +const { inspect } = require("util"); + +const songs = [ + "Guerrilla Radio", + "Killing in the name", + "Bullet in the head", + null, +]; + +for (const s of songs) { + try { + console.log(`${s} -> ${urlify(s)}`); + } catch (e) { + console.error(`Skipped non string value: ${inspect(s)}`); + } +} diff --git a/08-pkg/03-debugging-time.js b/08-pkg/03-debugging-time.js new file mode 100644 index 0000000..0c91390 --- /dev/null +++ b/08-pkg/03-debugging-time.js @@ -0,0 +1,8 @@ +const debug = require("./modules/debug")("03"); + +debug(`Script started at ${new Date().toISOString()}`); +setTimeout(() => { + // doing something... + + debug(`Script ended at ${new Date().toISOString()}`); +}, 1000); diff --git a/08-pkg/04-open-rad-map.js b/08-pkg/04-open-rad-map.js new file mode 100644 index 0000000..99e4a49 --- /dev/null +++ b/08-pkg/04-open-rad-map.js @@ -0,0 +1,6 @@ +const debug = require("./modules/debug")("04"); +const open = require("./modules/open"); + +debug("Script started"); +open("https://jciv.iidj.net/map/"); +debug("Function open called"); diff --git a/08-pkg/05-missing-mod.js b/08-pkg/05-missing-mod.js new file mode 100644 index 0000000..cef38a6 --- /dev/null +++ b/08-pkg/05-missing-mod.js @@ -0,0 +1 @@ +require("./modules/my-mod"); diff --git a/08-pkg/06-shared-counter.js b/08-pkg/06-shared-counter.js new file mode 100644 index 0000000..44b436d --- /dev/null +++ b/08-pkg/06-shared-counter.js @@ -0,0 +1,7 @@ +const counter1 = require("./modules/shared-counter"); +const counter2 = require("./modules/shared-counter"); + +counter1.inc(); +counter2.inc(); + +console.log(counter1.val()); diff --git a/08-pkg/07-lines-counter.mjs b/08-pkg/07-lines-counter.mjs new file mode 100644 index 0000000..990fe04 --- /dev/null +++ b/08-pkg/07-lines-counter.mjs @@ -0,0 +1,5 @@ +import { readFileLines, encoding } from "./modules/read-file-lines.mjs"; + +const file = process.argv[2]; +const lines = await readFileLines(file); +console.log(`${file} (${encoding}) is ${lines.length} lines long`); diff --git a/08-pkg/08-numbered-echo.mjs b/08-pkg/08-numbered-echo.mjs new file mode 100644 index 0000000..a5cff93 --- /dev/null +++ b/08-pkg/08-numbered-echo.mjs @@ -0,0 +1,6 @@ +import { readFileLines } from "./modules/read-file-lines.mjs"; +import printLines from "./modules/print-lines-numbers.mjs"; + +const file = process.argv[2]; +const lines = await readFileLines(file); +printLines(lines); diff --git a/08-pkg/09-open-links.mjs b/08-pkg/09-open-links.mjs new file mode 100644 index 0000000..ac0cd68 --- /dev/null +++ b/08-pkg/09-open-links.mjs @@ -0,0 +1,12 @@ +import printLines from "./modules/print-lines-numbers.mjs"; +import { readFileLines as readLinks } from "./modules/read-file-lines.mjs"; +import readNumber from "./modules/read-number.mjs"; +import open from "./modules/open/open.js"; + +const linksFile = "./links.txt"; +const countFrom = 1; + +const links = await readLinks(linksFile); +printLines(links, countFrom); +const num = await readNumber("Digit a number and press Enter to open it: "); +open(links[num - countFrom]); diff --git a/08-pkg/10-feed-reader/.eslintrc b/08-pkg/10-feed-reader/.eslintrc new file mode 100644 index 0000000..c8b7979 --- /dev/null +++ b/08-pkg/10-feed-reader/.eslintrc @@ -0,0 +1,10 @@ +{ + "env": { + "node": true, + "es2022": true + }, + "parserOptions": { + "sourceType": "module" + }, + "extends": "eslint:recommended" +} diff --git a/08-pkg/10-feed-reader/.gitignore b/08-pkg/10-feed-reader/.gitignore new file mode 100644 index 0000000..6dca520 --- /dev/null +++ b/08-pkg/10-feed-reader/.gitignore @@ -0,0 +1,2 @@ +feed-db.txt +node_modules/ \ No newline at end of file diff --git a/08-pkg/10-feed-reader/app.mjs b/08-pkg/10-feed-reader/app.mjs new file mode 100644 index 0000000..84fd3a7 --- /dev/null +++ b/08-pkg/10-feed-reader/app.mjs @@ -0,0 +1,14 @@ +import printLines from "../modules/print-lines-numbers.mjs"; +import readNumber from "../modules/read-number.mjs"; +import open from "../modules/open/open.js"; +import getFeedEntries from "./get-feeds.mjs"; + +const countFrom = 1; +const feedUrl = process.argv[2]; +const entries = await getFeedEntries(feedUrl); +const titles = entries.map((e) => e.title); + +printLines(titles, countFrom); +const num = await readNumber("Digit a number and press Enter to open it: "); +const selectedEntry = entries[num - countFrom]; +open(selectedEntry.link); diff --git a/08-pkg/10-feed-reader/get-feeds.mjs b/08-pkg/10-feed-reader/get-feeds.mjs new file mode 100644 index 0000000..132a346 --- /dev/null +++ b/08-pkg/10-feed-reader/get-feeds.mjs @@ -0,0 +1,11 @@ +import { parseFeed } from "htmlparser2"; + +export default async function getFeedEntries(url, maxEntries = 5) { + const feedFetched = await fetch(url); + if (!feedFetched.ok) { + throw new Error("Feed fetch failed"); + } + const feedContent = await feedFetched.text(); + const { items } = parseFeed(feedContent); + return items.slice(0, maxEntries); +} diff --git a/08-pkg/10-feed-reader/package-lock.json b/08-pkg/10-feed-reader/package-lock.json new file mode 100644 index 0000000..c6a72ed --- /dev/null +++ b/08-pkg/10-feed-reader/package-lock.json @@ -0,0 +1,1216 @@ +{ + "name": "10-feed-reader", + "version": "1.0.0", + "lockfileVersion": 3, + "requires": true, + "packages": { + "": { + "name": "10-feed-reader", + "dependencies": { + "htmlparser2": "^9.1.0" + }, + "devDependencies": { + "eslint": "^8.56.0" + } + }, + "node_modules/@aashutoshrathi/word-wrap": { + "version": "1.2.6", + "resolved": "https://registry.npmjs.org/@aashutoshrathi/word-wrap/-/word-wrap-1.2.6.tgz", + "integrity": "sha512-1Yjs2SvM8TflER/OD3cOjhWWOZb58A2t7wpE2S9XfBYTiIl+XFhQG2bjy4Pu1I+EAlCNUzRDYDdFwFYUKvXcIA==", + "dev": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/@eslint-community/eslint-utils": { + "version": "4.4.0", + "resolved": "https://registry.npmjs.org/@eslint-community/eslint-utils/-/eslint-utils-4.4.0.tgz", + "integrity": "sha512-1/sA4dwrzBAyeUoQ6oxahHKmrZvsnLCg4RfxW3ZFGGmQkSNQPFNLV9CUEFQP1x9EYXHTo5p6xdhZM1Ne9p/AfA==", + "dev": true, + "dependencies": { + "eslint-visitor-keys": "^3.3.0" + }, + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + }, + "peerDependencies": { + "eslint": "^6.0.0 || ^7.0.0 || >=8.0.0" + } + }, + "node_modules/@eslint-community/regexpp": { + "version": "4.10.0", + "resolved": "https://registry.npmjs.org/@eslint-community/regexpp/-/regexpp-4.10.0.tgz", + "integrity": "sha512-Cu96Sd2By9mCNTx2iyKOmq10v22jUVQv0lQnlGNy16oE9589yE+QADPbrMGCkA51cKZSg3Pu/aTJVTGfL/qjUA==", + "dev": true, + "engines": { + "node": "^12.0.0 || ^14.0.0 || >=16.0.0" + } + }, + "node_modules/@eslint/eslintrc": { + "version": "2.1.4", + "resolved": "https://registry.npmjs.org/@eslint/eslintrc/-/eslintrc-2.1.4.tgz", + "integrity": "sha512-269Z39MS6wVJtsoUl10L60WdkhJVdPG24Q4eZTH3nnF6lpvSShEK3wQjDX9JRWAUPvPh7COouPpU9IrqaZFvtQ==", + "dev": true, + "dependencies": { + "ajv": "^6.12.4", + "debug": "^4.3.2", + "espree": "^9.6.0", + "globals": "^13.19.0", + "ignore": "^5.2.0", + "import-fresh": "^3.2.1", + "js-yaml": "^4.1.0", + "minimatch": "^3.1.2", + "strip-json-comments": "^3.1.1" + }, + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/@eslint/js": { + "version": "8.56.0", + "resolved": "https://registry.npmjs.org/@eslint/js/-/js-8.56.0.tgz", + "integrity": "sha512-gMsVel9D7f2HLkBma9VbtzZRehRogVRfbr++f06nL2vnCGCNlzOD+/MUov/F4p8myyAHspEhVobgjpX64q5m6A==", + "dev": true, + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + } + }, + "node_modules/@humanwhocodes/config-array": { + "version": "0.11.14", + "resolved": "https://registry.npmjs.org/@humanwhocodes/config-array/-/config-array-0.11.14.tgz", + "integrity": "sha512-3T8LkOmg45BV5FICb15QQMsyUSWrQ8AygVfC7ZG32zOalnqrilm018ZVCw0eapXux8FtA33q8PSRSstjee3jSg==", + "dev": true, + "dependencies": { + "@humanwhocodes/object-schema": "^2.0.2", + "debug": "^4.3.1", + "minimatch": "^3.0.5" + }, + "engines": { + "node": ">=10.10.0" + } + }, + "node_modules/@humanwhocodes/module-importer": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/@humanwhocodes/module-importer/-/module-importer-1.0.1.tgz", + "integrity": "sha512-bxveV4V8v5Yb4ncFTT3rPSgZBOpCkjfK0y4oVVVJwIuDVBRMDXrPyXRL988i5ap9m9bnyEEjWfm5WkBmtffLfA==", + "dev": true, + "engines": { + "node": ">=12.22" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/nzakas" + } + }, + "node_modules/@humanwhocodes/object-schema": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/@humanwhocodes/object-schema/-/object-schema-2.0.2.tgz", + "integrity": "sha512-6EwiSjwWYP7pTckG6I5eyFANjPhmPjUX9JRLUSfNPC7FX7zK9gyZAfUEaECL6ALTpGX5AjnBq3C9XmVWPitNpw==", + "dev": true + }, + "node_modules/@nodelib/fs.scandir": { + "version": "2.1.5", + "resolved": "https://registry.npmjs.org/@nodelib/fs.scandir/-/fs.scandir-2.1.5.tgz", + "integrity": "sha512-vq24Bq3ym5HEQm2NKCr3yXDwjc7vTsEThRDnkp2DK9p1uqLR+DHurm/NOTo0KG7HYHU7eppKZj3MyqYuMBf62g==", + "dev": true, + "dependencies": { + "@nodelib/fs.stat": "2.0.5", + "run-parallel": "^1.1.9" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/@nodelib/fs.stat": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/@nodelib/fs.stat/-/fs.stat-2.0.5.tgz", + "integrity": "sha512-RkhPPp2zrqDAQA/2jNhnztcPAlv64XdhIp7a7454A5ovI7Bukxgt7MX7udwAu3zg1DcpPU0rz3VV1SeaqvY4+A==", + "dev": true, + "engines": { + "node": ">= 8" + } + }, + "node_modules/@nodelib/fs.walk": { + "version": "1.2.8", + "resolved": "https://registry.npmjs.org/@nodelib/fs.walk/-/fs.walk-1.2.8.tgz", + "integrity": "sha512-oGB+UxlgWcgQkgwo8GcEGwemoTFt3FIO9ababBmaGwXIoBKZ+GTy0pP185beGg7Llih/NSHSV2XAs1lnznocSg==", + "dev": true, + "dependencies": { + "@nodelib/fs.scandir": "2.1.5", + "fastq": "^1.6.0" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/@ungap/structured-clone": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/@ungap/structured-clone/-/structured-clone-1.2.0.tgz", + "integrity": "sha512-zuVdFrMJiuCDQUMCzQaD6KL28MjnqqN8XnAqiEq9PNm/hCPTSGfrXCOfwj1ow4LFb/tNymJPwsNbVePc1xFqrQ==", + "dev": true + }, + "node_modules/acorn": { + "version": "8.11.3", + "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.11.3.tgz", + "integrity": "sha512-Y9rRfJG5jcKOE0CLisYbojUjIrIEE7AGMzA/Sm4BslANhbS+cDMpgBdcPT91oJ7OuJ9hYJBx59RjbhxVnrF8Xg==", + "dev": true, + "bin": { + "acorn": "bin/acorn" + }, + "engines": { + "node": ">=0.4.0" + } + }, + "node_modules/acorn-jsx": { + "version": "5.3.2", + "resolved": "https://registry.npmjs.org/acorn-jsx/-/acorn-jsx-5.3.2.tgz", + "integrity": "sha512-rq9s+JNhf0IChjtDXxllJ7g41oZk5SlXtp0LHwyA5cejwn7vKmKp4pPri6YEePv2PU65sAsegbXtIinmDFDXgQ==", + "dev": true, + "peerDependencies": { + "acorn": "^6.0.0 || ^7.0.0 || ^8.0.0" + } + }, + "node_modules/ajv": { + "version": "6.12.6", + "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.12.6.tgz", + "integrity": "sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g==", + "dev": true, + "dependencies": { + "fast-deep-equal": "^3.1.1", + "fast-json-stable-stringify": "^2.0.0", + "json-schema-traverse": "^0.4.1", + "uri-js": "^4.2.2" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/epoberezkin" + } + }, + "node_modules/ansi-regex": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", + "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", + "dev": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/ansi-styles": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", + "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", + "dev": true, + "dependencies": { + "color-convert": "^2.0.1" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/argparse": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/argparse/-/argparse-2.0.1.tgz", + "integrity": "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==", + "dev": true + }, + "node_modules/balanced-match": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz", + "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==", + "dev": true + }, + "node_modules/brace-expansion": { + "version": "1.1.11", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz", + "integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==", + "dev": true, + "dependencies": { + "balanced-match": "^1.0.0", + "concat-map": "0.0.1" + } + }, + "node_modules/callsites": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/callsites/-/callsites-3.1.0.tgz", + "integrity": "sha512-P8BjAsXvZS+VIDUI11hHCQEv74YT67YUi5JJFNWIqL235sBmjX4+qx9Muvls5ivyNENctx46xQLQ3aTuE7ssaQ==", + "dev": true, + "engines": { + "node": ">=6" + } + }, + "node_modules/chalk": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", + "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", + "dev": true, + "dependencies": { + "ansi-styles": "^4.1.0", + "supports-color": "^7.1.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/chalk?sponsor=1" + } + }, + "node_modules/color-convert": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", + "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", + "dev": true, + "dependencies": { + "color-name": "~1.1.4" + }, + "engines": { + "node": ">=7.0.0" + } + }, + "node_modules/color-name": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", + "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", + "dev": true + }, + "node_modules/concat-map": { + "version": "0.0.1", + "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz", + "integrity": "sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==", + "dev": true + }, + "node_modules/cross-spawn": { + "version": "7.0.3", + "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.3.tgz", + "integrity": "sha512-iRDPJKUPVEND7dHPO8rkbOnPpyDygcDFtWjpeWNCgy8WP2rXcxXL8TskReQl6OrB2G7+UJrags1q15Fudc7G6w==", + "dev": true, + "dependencies": { + "path-key": "^3.1.0", + "shebang-command": "^2.0.0", + "which": "^2.0.1" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/debug": { + "version": "4.3.4", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.4.tgz", + "integrity": "sha512-PRWFHuSU3eDtQJPvnNY7Jcket1j0t5OuOsFzPPzsekD52Zl8qUfFIPEiswXqIvHWGVHOgX+7G/vCNNhehwxfkQ==", + "dev": true, + "dependencies": { + "ms": "2.1.2" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "node_modules/deep-is": { + "version": "0.1.4", + "resolved": "https://registry.npmjs.org/deep-is/-/deep-is-0.1.4.tgz", + "integrity": "sha512-oIPzksmTg4/MriiaYGO+okXDT7ztn/w3Eptv/+gSIdMdKsJo0u4CfYNFJPy+4SKMuCqGw2wxnA+URMg3t8a/bQ==", + "dev": true + }, + "node_modules/doctrine": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/doctrine/-/doctrine-3.0.0.tgz", + "integrity": "sha512-yS+Q5i3hBf7GBkd4KG8a7eBNNWNGLTaEwwYWUijIYM7zrlYDM0BFXHjjPWlWZ1Rg7UaddZeIDmi9jF3HmqiQ2w==", + "dev": true, + "dependencies": { + "esutils": "^2.0.2" + }, + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/dom-serializer": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/dom-serializer/-/dom-serializer-2.0.0.tgz", + "integrity": "sha512-wIkAryiqt/nV5EQKqQpo3SToSOV9J0DnbJqwK7Wv/Trc92zIAYZ4FlMu+JPFW1DfGFt81ZTCGgDEabffXeLyJg==", + "dependencies": { + "domelementtype": "^2.3.0", + "domhandler": "^5.0.2", + "entities": "^4.2.0" + }, + "funding": { + "url": "https://github.com/cheeriojs/dom-serializer?sponsor=1" + } + }, + "node_modules/domelementtype": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/domelementtype/-/domelementtype-2.3.0.tgz", + "integrity": "sha512-OLETBj6w0OsagBwdXnPdN0cnMfF9opN69co+7ZrbfPGrdpPVNBUj02spi6B1N7wChLQiPn4CSH/zJvXw56gmHw==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/fb55" + } + ] + }, + "node_modules/domhandler": { + "version": "5.0.3", + "resolved": "https://registry.npmjs.org/domhandler/-/domhandler-5.0.3.tgz", + "integrity": "sha512-cgwlv/1iFQiFnU96XXgROh8xTeetsnJiDsTc7TYCLFd9+/WNkIqPTxiM/8pSd8VIrhXGTf1Ny1q1hquVqDJB5w==", + "dependencies": { + "domelementtype": "^2.3.0" + }, + "engines": { + "node": ">= 4" + }, + "funding": { + "url": "https://github.com/fb55/domhandler?sponsor=1" + } + }, + "node_modules/domutils": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/domutils/-/domutils-3.1.0.tgz", + "integrity": "sha512-H78uMmQtI2AhgDJjWeQmHwJJ2bLPD3GMmO7Zja/ZZh84wkm+4ut+IUnUdRa8uCGX88DiVx1j6FRe1XfxEgjEZA==", + "dependencies": { + "dom-serializer": "^2.0.0", + "domelementtype": "^2.3.0", + "domhandler": "^5.0.3" + }, + "funding": { + "url": "https://github.com/fb55/domutils?sponsor=1" + } + }, + "node_modules/entities": { + "version": "4.5.0", + "resolved": "https://registry.npmjs.org/entities/-/entities-4.5.0.tgz", + "integrity": "sha512-V0hjH4dGPh9Ao5p0MoRY6BVqtwCjhz6vI5LT8AJ55H+4g9/4vbHx1I54fS0XuclLhDHArPQCiMjDxjaL8fPxhw==", + "engines": { + "node": ">=0.12" + }, + "funding": { + "url": "https://github.com/fb55/entities?sponsor=1" + } + }, + "node_modules/escape-string-regexp": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-4.0.0.tgz", + "integrity": "sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA==", + "dev": true, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/eslint": { + "version": "8.56.0", + "resolved": "https://registry.npmjs.org/eslint/-/eslint-8.56.0.tgz", + "integrity": "sha512-Go19xM6T9puCOWntie1/P997aXxFsOi37JIHRWI514Hc6ZnaHGKY9xFhrU65RT6CcBEzZoGG1e6Nq+DT04ZtZQ==", + "dev": true, + "dependencies": { + "@eslint-community/eslint-utils": "^4.2.0", + "@eslint-community/regexpp": "^4.6.1", + "@eslint/eslintrc": "^2.1.4", + "@eslint/js": "8.56.0", + "@humanwhocodes/config-array": "^0.11.13", + "@humanwhocodes/module-importer": "^1.0.1", + "@nodelib/fs.walk": "^1.2.8", + "@ungap/structured-clone": "^1.2.0", + "ajv": "^6.12.4", + "chalk": "^4.0.0", + "cross-spawn": "^7.0.2", + "debug": "^4.3.2", + "doctrine": "^3.0.0", + "escape-string-regexp": "^4.0.0", + "eslint-scope": "^7.2.2", + "eslint-visitor-keys": "^3.4.3", + "espree": "^9.6.1", + "esquery": "^1.4.2", + "esutils": "^2.0.2", + "fast-deep-equal": "^3.1.3", + "file-entry-cache": "^6.0.1", + "find-up": "^5.0.0", + "glob-parent": "^6.0.2", + "globals": "^13.19.0", + "graphemer": "^1.4.0", + "ignore": "^5.2.0", + "imurmurhash": "^0.1.4", + "is-glob": "^4.0.0", + "is-path-inside": "^3.0.3", + "js-yaml": "^4.1.0", + "json-stable-stringify-without-jsonify": "^1.0.1", + "levn": "^0.4.1", + "lodash.merge": "^4.6.2", + "minimatch": "^3.1.2", + "natural-compare": "^1.4.0", + "optionator": "^0.9.3", + "strip-ansi": "^6.0.1", + "text-table": "^0.2.0" + }, + "bin": { + "eslint": "bin/eslint.js" + }, + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/eslint-scope": { + "version": "7.2.2", + "resolved": "https://registry.npmjs.org/eslint-scope/-/eslint-scope-7.2.2.tgz", + "integrity": "sha512-dOt21O7lTMhDM+X9mB4GX+DZrZtCUJPL/wlcTqxyrx5IvO0IYtILdtrQGQp+8n5S0gwSVmOf9NQrjMOgfQZlIg==", + "dev": true, + "dependencies": { + "esrecurse": "^4.3.0", + "estraverse": "^5.2.0" + }, + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/eslint-visitor-keys": { + "version": "3.4.3", + "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-3.4.3.tgz", + "integrity": "sha512-wpc+LXeiyiisxPlEkUzU6svyS1frIO3Mgxj1fdy7Pm8Ygzguax2N3Fa/D/ag1WqbOprdI+uY6wMUl8/a2G+iag==", + "dev": true, + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/espree": { + "version": "9.6.1", + "resolved": "https://registry.npmjs.org/espree/-/espree-9.6.1.tgz", + "integrity": "sha512-oruZaFkjorTpF32kDSI5/75ViwGeZginGGy2NoOSg3Q9bnwlnmDm4HLnkl0RE3n+njDXR037aY1+x58Z/zFdwQ==", + "dev": true, + "dependencies": { + "acorn": "^8.9.0", + "acorn-jsx": "^5.3.2", + "eslint-visitor-keys": "^3.4.1" + }, + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/esquery": { + "version": "1.5.0", + "resolved": "https://registry.npmjs.org/esquery/-/esquery-1.5.0.tgz", + "integrity": "sha512-YQLXUplAwJgCydQ78IMJywZCceoqk1oH01OERdSAJc/7U2AylwjhSCLDEtqwg811idIS/9fIU5GjG73IgjKMVg==", + "dev": true, + "dependencies": { + "estraverse": "^5.1.0" + }, + "engines": { + "node": ">=0.10" + } + }, + "node_modules/esrecurse": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/esrecurse/-/esrecurse-4.3.0.tgz", + "integrity": "sha512-KmfKL3b6G+RXvP8N1vr3Tq1kL/oCFgn2NYXEtqP8/L3pKapUA4G8cFVaoF3SU323CD4XypR/ffioHmkti6/Tag==", + "dev": true, + "dependencies": { + "estraverse": "^5.2.0" + }, + "engines": { + "node": ">=4.0" + } + }, + "node_modules/estraverse": { + "version": "5.3.0", + "resolved": "https://registry.npmjs.org/estraverse/-/estraverse-5.3.0.tgz", + "integrity": "sha512-MMdARuVEQziNTeJD8DgMqmhwR11BRQ/cBP+pLtYdSTnf3MIO8fFeiINEbX36ZdNlfU/7A9f3gUw49B3oQsvwBA==", + "dev": true, + "engines": { + "node": ">=4.0" + } + }, + "node_modules/esutils": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/esutils/-/esutils-2.0.3.tgz", + "integrity": "sha512-kVscqXk4OCp68SZ0dkgEKVi6/8ij300KBWTJq32P/dYeWTSwK41WyTxalN1eRmA5Z9UU/LX9D7FWSmV9SAYx6g==", + "dev": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/fast-deep-equal": { + "version": "3.1.3", + "resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz", + "integrity": "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==", + "dev": true + }, + "node_modules/fast-json-stable-stringify": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/fast-json-stable-stringify/-/fast-json-stable-stringify-2.1.0.tgz", + "integrity": "sha512-lhd/wF+Lk98HZoTCtlVraHtfh5XYijIjalXck7saUtuanSDyLMxnHhSXEDJqHxD7msR8D0uCmqlkwjCV8xvwHw==", + "dev": true + }, + "node_modules/fast-levenshtein": { + "version": "2.0.6", + "resolved": "https://registry.npmjs.org/fast-levenshtein/-/fast-levenshtein-2.0.6.tgz", + "integrity": "sha512-DCXu6Ifhqcks7TZKY3Hxp3y6qphY5SJZmrWMDrKcERSOXWQdMhU9Ig/PYrzyw/ul9jOIyh0N4M0tbC5hodg8dw==", + "dev": true + }, + "node_modules/fastq": { + "version": "1.17.1", + "resolved": "https://registry.npmjs.org/fastq/-/fastq-1.17.1.tgz", + "integrity": "sha512-sRVD3lWVIXWg6By68ZN7vho9a1pQcN/WBFaAAsDDFzlJjvoGx0P8z7V1t72grFJfJhu3YPZBuu25f7Kaw2jN1w==", + "dev": true, + "dependencies": { + "reusify": "^1.0.4" + } + }, + "node_modules/file-entry-cache": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/file-entry-cache/-/file-entry-cache-6.0.1.tgz", + "integrity": "sha512-7Gps/XWymbLk2QLYK4NzpMOrYjMhdIxXuIvy2QBsLE6ljuodKvdkWs/cpyJJ3CVIVpH0Oi1Hvg1ovbMzLdFBBg==", + "dev": true, + "dependencies": { + "flat-cache": "^3.0.4" + }, + "engines": { + "node": "^10.12.0 || >=12.0.0" + } + }, + "node_modules/find-up": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/find-up/-/find-up-5.0.0.tgz", + "integrity": "sha512-78/PXT1wlLLDgTzDs7sjq9hzz0vXD+zn+7wypEe4fXQxCmdmqfGsEPQxmiCSQI3ajFV91bVSsvNtrJRiW6nGng==", + "dev": true, + "dependencies": { + "locate-path": "^6.0.0", + "path-exists": "^4.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/flat-cache": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/flat-cache/-/flat-cache-3.2.0.tgz", + "integrity": "sha512-CYcENa+FtcUKLmhhqyctpclsq7QF38pKjZHsGNiSQF5r4FtoKDWabFDl3hzaEQMvT1LHEysw5twgLvpYYb4vbw==", + "dev": true, + "dependencies": { + "flatted": "^3.2.9", + "keyv": "^4.5.3", + "rimraf": "^3.0.2" + }, + "engines": { + "node": "^10.12.0 || >=12.0.0" + } + }, + "node_modules/flatted": { + "version": "3.2.9", + "resolved": "https://registry.npmjs.org/flatted/-/flatted-3.2.9.tgz", + "integrity": "sha512-36yxDn5H7OFZQla0/jFJmbIKTdZAQHngCedGxiMmpNfEZM0sdEeT+WczLQrjK6D7o2aiyLYDnkw0R3JK0Qv1RQ==", + "dev": true + }, + "node_modules/fs.realpath": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/fs.realpath/-/fs.realpath-1.0.0.tgz", + "integrity": "sha512-OO0pH2lK6a0hZnAdau5ItzHPI6pUlvI7jMVnxUQRtw4owF2wk8lOSabtGDCTP4Ggrg2MbGnWO9X8K1t4+fGMDw==", + "dev": true + }, + "node_modules/glob": { + "version": "7.2.3", + "resolved": "https://registry.npmjs.org/glob/-/glob-7.2.3.tgz", + "integrity": "sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q==", + "dev": true, + "dependencies": { + "fs.realpath": "^1.0.0", + "inflight": "^1.0.4", + "inherits": "2", + "minimatch": "^3.1.1", + "once": "^1.3.0", + "path-is-absolute": "^1.0.0" + }, + "engines": { + "node": "*" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/glob-parent": { + "version": "6.0.2", + "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-6.0.2.tgz", + "integrity": "sha512-XxwI8EOhVQgWp6iDL+3b0r86f4d6AX6zSU55HfB4ydCEuXLXc5FcYeOu+nnGftS4TEju/11rt4KJPTMgbfmv4A==", + "dev": true, + "dependencies": { + "is-glob": "^4.0.3" + }, + "engines": { + "node": ">=10.13.0" + } + }, + "node_modules/globals": { + "version": "13.24.0", + "resolved": "https://registry.npmjs.org/globals/-/globals-13.24.0.tgz", + "integrity": "sha512-AhO5QUcj8llrbG09iWhPU2B204J1xnPeL8kQmVorSsy+Sjj1sk8gIyh6cUocGmH4L0UuhAJy+hJMRA4mgA4mFQ==", + "dev": true, + "dependencies": { + "type-fest": "^0.20.2" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/graphemer": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/graphemer/-/graphemer-1.4.0.tgz", + "integrity": "sha512-EtKwoO6kxCL9WO5xipiHTZlSzBm7WLT627TqC/uVRd0HKmq8NXyebnNYxDoBi7wt8eTWrUrKXCOVaFq9x1kgag==", + "dev": true + }, + "node_modules/has-flag": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", + "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", + "dev": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/htmlparser2": { + "version": "9.1.0", + "resolved": "https://registry.npmjs.org/htmlparser2/-/htmlparser2-9.1.0.tgz", + "integrity": "sha512-5zfg6mHUoaer/97TxnGpxmbR7zJtPwIYFMZ/H5ucTlPZhKvtum05yiPK3Mgai3a0DyVxv7qYqoweaEd2nrYQzQ==", + "funding": [ + "https://github.com/fb55/htmlparser2?sponsor=1", + { + "type": "github", + "url": "https://github.com/sponsors/fb55" + } + ], + "dependencies": { + "domelementtype": "^2.3.0", + "domhandler": "^5.0.3", + "domutils": "^3.1.0", + "entities": "^4.5.0" + } + }, + "node_modules/ignore": { + "version": "5.3.1", + "resolved": "https://registry.npmjs.org/ignore/-/ignore-5.3.1.tgz", + "integrity": "sha512-5Fytz/IraMjqpwfd34ke28PTVMjZjJG2MPn5t7OE4eUCUNf8BAa7b5WUS9/Qvr6mwOQS7Mk6vdsMno5he+T8Xw==", + "dev": true, + "engines": { + "node": ">= 4" + } + }, + "node_modules/import-fresh": { + "version": "3.3.0", + "resolved": "https://registry.npmjs.org/import-fresh/-/import-fresh-3.3.0.tgz", + "integrity": "sha512-veYYhQa+D1QBKznvhUHxb8faxlrwUnxseDAbAp457E0wLNio2bOSKnjYDhMj+YiAq61xrMGhQk9iXVk5FzgQMw==", + "dev": true, + "dependencies": { + "parent-module": "^1.0.0", + "resolve-from": "^4.0.0" + }, + "engines": { + "node": ">=6" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/imurmurhash": { + "version": "0.1.4", + "resolved": "https://registry.npmjs.org/imurmurhash/-/imurmurhash-0.1.4.tgz", + "integrity": "sha512-JmXMZ6wuvDmLiHEml9ykzqO6lwFbof0GG4IkcGaENdCRDDmMVnny7s5HsIgHCbaq0w2MyPhDqkhTUgS2LU2PHA==", + "dev": true, + "engines": { + "node": ">=0.8.19" + } + }, + "node_modules/inflight": { + "version": "1.0.6", + "resolved": "https://registry.npmjs.org/inflight/-/inflight-1.0.6.tgz", + "integrity": "sha512-k92I/b08q4wvFscXCLvqfsHCrjrF7yiXsQuIVvVE7N82W3+aqpzuUdBbfhWcy/FZR3/4IgflMgKLOsvPDrGCJA==", + "dev": true, + "dependencies": { + "once": "^1.3.0", + "wrappy": "1" + } + }, + "node_modules/inherits": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz", + "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==", + "dev": true + }, + "node_modules/is-extglob": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-2.1.1.tgz", + "integrity": "sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ==", + "dev": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/is-glob": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/is-glob/-/is-glob-4.0.3.tgz", + "integrity": "sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg==", + "dev": true, + "dependencies": { + "is-extglob": "^2.1.1" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/is-path-inside": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/is-path-inside/-/is-path-inside-3.0.3.tgz", + "integrity": "sha512-Fd4gABb+ycGAmKou8eMftCupSir5lRxqf4aD/vd0cD2qc4HL07OjCeuHMr8Ro4CoMaeCKDB0/ECBOVWjTwUvPQ==", + "dev": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/isexe": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz", + "integrity": "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==", + "dev": true + }, + "node_modules/js-yaml": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.1.0.tgz", + "integrity": "sha512-wpxZs9NoxZaJESJGIZTyDEaYpl0FKSA+FB9aJiyemKhMwkxQg63h4T1KJgUGHpTqPDNRcmmYLugrRjJlBtWvRA==", + "dev": true, + "dependencies": { + "argparse": "^2.0.1" + }, + "bin": { + "js-yaml": "bin/js-yaml.js" + } + }, + "node_modules/json-buffer": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/json-buffer/-/json-buffer-3.0.1.tgz", + "integrity": "sha512-4bV5BfR2mqfQTJm+V5tPPdf+ZpuhiIvTuAB5g8kcrXOZpTT/QwwVRWBywX1ozr6lEuPdbHxwaJlm9G6mI2sfSQ==", + "dev": true + }, + "node_modules/json-schema-traverse": { + "version": "0.4.1", + "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-0.4.1.tgz", + "integrity": "sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg==", + "dev": true + }, + "node_modules/json-stable-stringify-without-jsonify": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/json-stable-stringify-without-jsonify/-/json-stable-stringify-without-jsonify-1.0.1.tgz", + "integrity": "sha512-Bdboy+l7tA3OGW6FjyFHWkP5LuByj1Tk33Ljyq0axyzdk9//JSi2u3fP1QSmd1KNwq6VOKYGlAu87CisVir6Pw==", + "dev": true + }, + "node_modules/keyv": { + "version": "4.5.4", + "resolved": "https://registry.npmjs.org/keyv/-/keyv-4.5.4.tgz", + "integrity": "sha512-oxVHkHR/EJf2CNXnWxRLW6mg7JyCCUcG0DtEGmL2ctUo1PNTin1PUil+r/+4r5MpVgC/fn1kjsx7mjSujKqIpw==", + "dev": true, + "dependencies": { + "json-buffer": "3.0.1" + } + }, + "node_modules/levn": { + "version": "0.4.1", + "resolved": "https://registry.npmjs.org/levn/-/levn-0.4.1.tgz", + "integrity": "sha512-+bT2uH4E5LGE7h/n3evcS/sQlJXCpIp6ym8OWJ5eV6+67Dsql/LaaT7qJBAt2rzfoa/5QBGBhxDix1dMt2kQKQ==", + "dev": true, + "dependencies": { + "prelude-ls": "^1.2.1", + "type-check": "~0.4.0" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/locate-path": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-6.0.0.tgz", + "integrity": "sha512-iPZK6eYjbxRu3uB4/WZ3EsEIMJFMqAoopl3R+zuq0UjcAm/MO6KCweDgPfP3elTztoKP3KtnVHxTn2NHBSDVUw==", + "dev": true, + "dependencies": { + "p-locate": "^5.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/lodash.merge": { + "version": "4.6.2", + "resolved": "https://registry.npmjs.org/lodash.merge/-/lodash.merge-4.6.2.tgz", + "integrity": "sha512-0KpjqXRVvrYyCsX1swR/XTK0va6VQkQM6MNo7PqW77ByjAhoARA8EfrP1N4+KlKj8YS0ZUCtRT/YUuhyYDujIQ==", + "dev": true + }, + "node_modules/minimatch": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", + "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==", + "dev": true, + "dependencies": { + "brace-expansion": "^1.1.7" + }, + "engines": { + "node": "*" + } + }, + "node_modules/ms": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz", + "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==", + "dev": true + }, + "node_modules/natural-compare": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/natural-compare/-/natural-compare-1.4.0.tgz", + "integrity": "sha512-OWND8ei3VtNC9h7V60qff3SVobHr996CTwgxubgyQYEpg290h9J0buyECNNJexkFm5sOajh5G116RYA1c8ZMSw==", + "dev": true + }, + "node_modules/once": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz", + "integrity": "sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w==", + "dev": true, + "dependencies": { + "wrappy": "1" + } + }, + "node_modules/optionator": { + "version": "0.9.3", + "resolved": "https://registry.npmjs.org/optionator/-/optionator-0.9.3.tgz", + "integrity": "sha512-JjCoypp+jKn1ttEFExxhetCKeJt9zhAgAve5FXHixTvFDW/5aEktX9bufBKLRRMdU7bNtpLfcGu94B3cdEJgjg==", + "dev": true, + "dependencies": { + "@aashutoshrathi/word-wrap": "^1.2.3", + "deep-is": "^0.1.3", + "fast-levenshtein": "^2.0.6", + "levn": "^0.4.1", + "prelude-ls": "^1.2.1", + "type-check": "^0.4.0" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/p-limit": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-3.1.0.tgz", + "integrity": "sha512-TYOanM3wGwNGsZN2cVTYPArw454xnXj5qmWF1bEoAc4+cU/ol7GVh7odevjp1FNHduHc3KZMcFduxU5Xc6uJRQ==", + "dev": true, + "dependencies": { + "yocto-queue": "^0.1.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/p-locate": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-5.0.0.tgz", + "integrity": "sha512-LaNjtRWUBY++zB5nE/NwcaoMylSPk+S+ZHNB1TzdbMJMny6dynpAGt7X/tl/QYq3TIeE6nxHppbo2LGymrG5Pw==", + "dev": true, + "dependencies": { + "p-limit": "^3.0.2" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/parent-module": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/parent-module/-/parent-module-1.0.1.tgz", + "integrity": "sha512-GQ2EWRpQV8/o+Aw8YqtfZZPfNRWZYkbidE9k5rpl/hC3vtHHBfGm2Ifi6qWV+coDGkrUKZAxE3Lot5kcsRlh+g==", + "dev": true, + "dependencies": { + "callsites": "^3.0.0" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/path-exists": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/path-exists/-/path-exists-4.0.0.tgz", + "integrity": "sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w==", + "dev": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/path-is-absolute": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/path-is-absolute/-/path-is-absolute-1.0.1.tgz", + "integrity": "sha512-AVbw3UJ2e9bq64vSaS9Am0fje1Pa8pbGqTTsmXfaIiMpnr5DlDhfJOuLj9Sf95ZPVDAUerDfEk88MPmPe7UCQg==", + "dev": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/path-key": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/path-key/-/path-key-3.1.1.tgz", + "integrity": "sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==", + "dev": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/prelude-ls": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/prelude-ls/-/prelude-ls-1.2.1.tgz", + "integrity": "sha512-vkcDPrRZo1QZLbn5RLGPpg/WmIQ65qoWWhcGKf/b5eplkkarX0m9z8ppCat4mlOqUsWpyNuYgO3VRyrYHSzX5g==", + "dev": true, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/punycode": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/punycode/-/punycode-2.3.1.tgz", + "integrity": "sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg==", + "dev": true, + "engines": { + "node": ">=6" + } + }, + "node_modules/queue-microtask": { + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/queue-microtask/-/queue-microtask-1.2.3.tgz", + "integrity": "sha512-NuaNSa6flKT5JaSYQzJok04JzTL1CA6aGhv5rfLW3PgqA+M2ChpZQnAC8h8i4ZFkBS8X5RqkDBHA7r4hej3K9A==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ] + }, + "node_modules/resolve-from": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/resolve-from/-/resolve-from-4.0.0.tgz", + "integrity": "sha512-pb/MYmXstAkysRFx8piNI1tGFNQIFA3vkE3Gq4EuA1dF6gHp/+vgZqsCGJapvy8N3Q+4o7FwvquPJcnZ7RYy4g==", + "dev": true, + "engines": { + "node": ">=4" + } + }, + "node_modules/reusify": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/reusify/-/reusify-1.0.4.tgz", + "integrity": "sha512-U9nH88a3fc/ekCF1l0/UP1IosiuIjyTh7hBvXVMHYgVcfGvt897Xguj2UOLDeI5BG2m7/uwyaLVT6fbtCwTyzw==", + "dev": true, + "engines": { + "iojs": ">=1.0.0", + "node": ">=0.10.0" + } + }, + "node_modules/rimraf": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/rimraf/-/rimraf-3.0.2.tgz", + "integrity": "sha512-JZkJMZkAGFFPP2YqXZXPbMlMBgsxzE8ILs4lMIX/2o0L9UBw9O/Y3o6wFw/i9YLapcUJWwqbi3kdxIPdC62TIA==", + "dev": true, + "dependencies": { + "glob": "^7.1.3" + }, + "bin": { + "rimraf": "bin.js" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/run-parallel": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/run-parallel/-/run-parallel-1.2.0.tgz", + "integrity": "sha512-5l4VyZR86LZ/lDxZTR6jqL8AFE2S0IFLMP26AbjsLVADxHdhB/c0GUsH+y39UfCi3dzz8OlQuPmnaJOMoDHQBA==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "dependencies": { + "queue-microtask": "^1.2.2" + } + }, + "node_modules/shebang-command": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-2.0.0.tgz", + "integrity": "sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==", + "dev": true, + "dependencies": { + "shebang-regex": "^3.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/shebang-regex": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/shebang-regex/-/shebang-regex-3.0.0.tgz", + "integrity": "sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==", + "dev": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/strip-ansi": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", + "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", + "dev": true, + "dependencies": { + "ansi-regex": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/strip-json-comments": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/strip-json-comments/-/strip-json-comments-3.1.1.tgz", + "integrity": "sha512-6fPc+R4ihwqP6N/aIv2f1gMH8lOVtWQHoqC4yK6oSDVVocumAsfCqjkXnqiYMhmMwS/mEHLp7Vehlt3ql6lEig==", + "dev": true, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/supports-color": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", + "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", + "dev": true, + "dependencies": { + "has-flag": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/text-table": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/text-table/-/text-table-0.2.0.tgz", + "integrity": "sha512-N+8UisAXDGk8PFXP4HAzVR9nbfmVJ3zYLAWiTIoqC5v5isinhr+r5uaO8+7r3BMfuNIufIsA7RdpVgacC2cSpw==", + "dev": true + }, + "node_modules/type-check": { + "version": "0.4.0", + "resolved": "https://registry.npmjs.org/type-check/-/type-check-0.4.0.tgz", + "integrity": "sha512-XleUoc9uwGXqjWwXaUTZAmzMcFZ5858QA2vvx1Ur5xIcixXIP+8LnFDgRplU30us6teqdlskFfu+ae4K79Ooew==", + "dev": true, + "dependencies": { + "prelude-ls": "^1.2.1" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/type-fest": { + "version": "0.20.2", + "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-0.20.2.tgz", + "integrity": "sha512-Ne+eE4r0/iWnpAxD852z3A+N0Bt5RN//NjJwRd2VFHEmrywxf5vsZlh4R6lixl6B+wz/8d+maTSAkN1FIkI3LQ==", + "dev": true, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/uri-js": { + "version": "4.4.1", + "resolved": "https://registry.npmjs.org/uri-js/-/uri-js-4.4.1.tgz", + "integrity": "sha512-7rKUyy33Q1yc98pQ1DAmLtwX109F7TIfWlW1Ydo8Wl1ii1SeHieeh0HHfPeL2fMXK6z0s8ecKs9frCuLJvndBg==", + "dev": true, + "dependencies": { + "punycode": "^2.1.0" + } + }, + "node_modules/which": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz", + "integrity": "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==", + "dev": true, + "dependencies": { + "isexe": "^2.0.0" + }, + "bin": { + "node-which": "bin/node-which" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/wrappy": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz", + "integrity": "sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==", + "dev": true + }, + "node_modules/yocto-queue": { + "version": "0.1.0", + "resolved": "https://registry.npmjs.org/yocto-queue/-/yocto-queue-0.1.0.tgz", + "integrity": "sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q==", + "dev": true, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + } + } +} diff --git a/08-pkg/10-feed-reader/package.json b/08-pkg/10-feed-reader/package.json new file mode 100644 index 0000000..d103943 --- /dev/null +++ b/08-pkg/10-feed-reader/package.json @@ -0,0 +1,14 @@ +{ + "name": "10-feed-reader", + "scripts": { + "start": "node app.mjs", + "node-blog": "node app.mjs https://nodejs.org/en/feed/blog.xml", + "start-debug": "NODE_DEBUG=* node app.mjs" + }, + "dependencies": { + "htmlparser2": "^9.1.0" + }, + "devDependencies": { + "eslint": "^8.56.0" + } +} diff --git a/08-pkg/10-feed-reader/wrong.mjs b/08-pkg/10-feed-reader/wrong.mjs new file mode 100644 index 0000000..693ec3e --- /dev/null +++ b/08-pkg/10-feed-reader/wrong.mjs @@ -0,0 +1 @@ +import wrong from "i-am-not-here"; diff --git a/08-pkg/links.txt b/08-pkg/links.txt new file mode 100644 index 0000000..6a0bfb2 --- /dev/null +++ b/08-pkg/links.txt @@ -0,0 +1,4 @@ +https://jciv.iidj.net/map/ +https://remap.jrc.ec.europa.eu/ +https://vimeo.com/135580602 +https://nuclearsecrecy.com/nukemap/ \ No newline at end of file diff --git a/08-pkg/modules/bookmark-utils.mjs b/08-pkg/modules/bookmark-utils.mjs new file mode 100644 index 0000000..8a5bf24 --- /dev/null +++ b/08-pkg/modules/bookmark-utils.mjs @@ -0,0 +1 @@ +//TODO - unire tutto e fare tre export diff --git a/08-pkg/modules/debug/index.js b/08-pkg/modules/debug/index.js new file mode 100644 index 0000000..f41e67f --- /dev/null +++ b/08-pkg/modules/debug/index.js @@ -0,0 +1,12 @@ +function debugFactory(tag) { + const enabled = process.env.DEBUG; + + function debug(msg) { + if (!enabled) { + return; + } + console.log(`[${tag}] ${msg}`); + } + return debug; +} +module.exports = debugFactory; diff --git a/08-pkg/modules/is-string.js b/08-pkg/modules/is-string.js new file mode 100644 index 0000000..0d35585 --- /dev/null +++ b/08-pkg/modules/is-string.js @@ -0,0 +1,5 @@ +function isString(str) { + return typeof str === "string"; +} + +exports.isString = isString; diff --git a/08-pkg/modules/open/open.js b/08-pkg/modules/open/open.js new file mode 100644 index 0000000..f04445d --- /dev/null +++ b/08-pkg/modules/open/open.js @@ -0,0 +1,43 @@ +const process = require("process"); +const childProcess = require("child_process"); +const { isString } = require("../is-string"); +const debug = require("../debug")("open"); + +const { platform } = process; + +const platformCmds = { + darwin: "open", + linux: "xdg-open", + win32: "powershell.exe /c start", +}; + +debug("platform is " + platform); + +function open(target) { + if (!isString(target)) { + throw new TypeError("A target is required"); + } + debug("target resource is " + target); + + const command = platformCmds[platform]; + if (!command) { + throw new Error(`${platform} is not supported.`); + } + + const childProcessOptions = { + stdio: "ignore", + detached: true, + }; + + const spawnedProc = childProcess.spawn( + command, + [target], + childProcessOptions + ); + + spawnedProc.unref(); + debug(`Spawned process PID is ${spawnedProc.pid}`); + return spawnedProc; +} + +module.exports = open; diff --git a/08-pkg/modules/open/package.json b/08-pkg/modules/open/package.json new file mode 100644 index 0000000..563b016 --- /dev/null +++ b/08-pkg/modules/open/package.json @@ -0,0 +1,3 @@ +{ + "main": "./open.js" +} diff --git a/08-pkg/modules/print-lines-numbers.mjs b/08-pkg/modules/print-lines-numbers.mjs new file mode 100644 index 0000000..b410b74 --- /dev/null +++ b/08-pkg/modules/print-lines-numbers.mjs @@ -0,0 +1,7 @@ +function print(lines = [], countFrom = 0) { + for (let idx = 0; idx < lines.length; idx++) { + const line = lines[idx]; + console.log(`[${countFrom + idx}] ${line}`); + } +} +export default print; diff --git a/08-pkg/modules/read-file-lines.mjs b/08-pkg/modules/read-file-lines.mjs new file mode 100644 index 0000000..57fecda --- /dev/null +++ b/08-pkg/modules/read-file-lines.mjs @@ -0,0 +1,12 @@ +import { readFile } from "node:fs/promises"; +const encoding = "utf-8"; +const separator = "\n"; + +async function readFileLines(filePath, sep = separator, enc = encoding) { + const fileContent = await readFile(filePath, { encoding: enc }); + const lines = fileContent.split(sep); + return lines; +} + +export { readFileLines, separator, encoding }; +export default readFileLines; diff --git a/08-pkg/modules/read-number.mjs b/08-pkg/modules/read-number.mjs new file mode 100644 index 0000000..361dd49 --- /dev/null +++ b/08-pkg/modules/read-number.mjs @@ -0,0 +1,9 @@ +import * as readline from "node:readline/promises"; +import { stdin as input, stdout as output } from "node:process"; + +export default async function (prompt = "Insert a number") { + const rl = readline.createInterface({ input, output }); + const answer = await rl.question(prompt); + rl.close(); + return parseInt(answer); +} diff --git a/08-pkg/modules/shared-counter/counter.js b/08-pkg/modules/shared-counter/counter.js new file mode 100644 index 0000000..ea9fdf8 --- /dev/null +++ b/08-pkg/modules/shared-counter/counter.js @@ -0,0 +1,5 @@ +let counter = 0; + +exports.inc = () => ++counter; +exports.dec = () => --counter; +exports.val = () => counter; diff --git a/08-pkg/modules/shared-counter/package.json b/08-pkg/modules/shared-counter/package.json new file mode 100644 index 0000000..3e94af7 --- /dev/null +++ b/08-pkg/modules/shared-counter/package.json @@ -0,0 +1,3 @@ +{ + "main": "./counter.js" +} diff --git a/08-pkg/modules/string-utils/index.js b/08-pkg/modules/string-utils/index.js new file mode 100644 index 0000000..8f8f7fe --- /dev/null +++ b/08-pkg/modules/string-utils/index.js @@ -0,0 +1,13 @@ +function isString(str) { + return typeof str === "string"; +} + +function toUrl(str) { + if (!isString(str)) { + throw new TypeError("A string is required"); + } + return str.toLowerCase().replace(/[^a-z0-9]+/g, "-"); +} + +exports.isString = isString; +exports.urlify = toUrl; diff --git a/09-express/01-apod/apod-api.mjs b/09-express/01-apod/apod-api.mjs new file mode 100644 index 0000000..120a5e2 --- /dev/null +++ b/09-express/01-apod/apod-api.mjs @@ -0,0 +1,12 @@ +const API_URL = "https://api.nasa.gov/planetary/apod"; +const API_KEY = process.env.NASA_API_KEY || "DEMO_KEY"; + +export async function getApodByDate(dateStr) { + const res = await fetch(`${API_URL}?date=${dateStr}&api_key=${API_KEY}`); + if (res.ok) { + const json = await res.json(); + return json; + } else { + throw new Error(`API return a ${res.status} code`); + } +} diff --git a/09-express/01-apod/app_v1.mjs b/09-express/01-apod/app_v1.mjs new file mode 100644 index 0000000..18f8244 --- /dev/null +++ b/09-express/01-apod/app_v1.mjs @@ -0,0 +1,27 @@ +import express from "express"; +const app = express(); +const port = 3000; + +app.get("/", (req, res) => { + res.send("Hello APOD!"); +}); +app.get("/about", (req, res) => { + res.send(` + + + About APOD + +

    Astronomy Picture of the Day

    +

    +Astronomy Picture of the Day (APOD) is originated, written, coordinated, +and edited since 1995 by Robert Nemiroff and Jerry Bonnell. +The APOD archive contains the largest collection of annotated +astronomical images on the internet. +

    + + `); +}); + +app.listen(port, () => { + console.log(`APOD app listening on port ${port}`); +}); diff --git a/09-express/01-apod/app_v2.mjs b/09-express/01-apod/app_v2.mjs new file mode 100644 index 0000000..26f6f34 --- /dev/null +++ b/09-express/01-apod/app_v2.mjs @@ -0,0 +1,43 @@ +import express from "express"; +const app = express(); +const port = 3000; + +app.use(express.static("public")); + +app.get("/", (req, res) => { + res.send(` + + + + APOD + + + +
    +

    Astronomy Picture of the Day

    + +
    + + + `); +}); +app.get("/about", (req, res) => { + res.send(` + + + About APOD + +

    Astronomy Picture of the Day

    +

    +Astronomy Picture of the Day (APOD) is originated, written, coordinated, +and edited since 1995 by Robert Nemiroff and Jerry Bonnell. +The APOD archive contains the largest collection of annotated +astronomical images on the internet. +

    + + `); +}); + +app.listen(port, () => { + console.log(`APOD app listening on port ${port}`); +}); diff --git a/09-express/01-apod/app_v3.mjs b/09-express/01-apod/app_v3.mjs new file mode 100644 index 0000000..d855bfd --- /dev/null +++ b/09-express/01-apod/app_v3.mjs @@ -0,0 +1,21 @@ +import express from "express"; +const app = express(); +const port = 3000; + +app.set("view engine", "hbs"); + +app.use(express.static("public")); + +app.get("/", (req, res) => { + res.render("home", { + title: "Astronomy Picture of the Day", + pic_url: "/saturn-voyager.jpg", + }); +}); +app.get("/about", (req, res) => { + res.render("about"); +}); + +app.listen(port, () => { + console.log(`APOD app listening on port ${port}`); +}); diff --git a/09-express/01-apod/app_v4.mjs b/09-express/01-apod/app_v4.mjs new file mode 100644 index 0000000..06cd32c --- /dev/null +++ b/09-express/01-apod/app_v4.mjs @@ -0,0 +1,24 @@ +import express from "express"; +const app = express(); +const port = 3000; + +app.set("views", "./views2"); +app.set("view engine", "hbs"); + +app.use(express.static("public")); + +app.get("/", (req, res) => { + res.render("home", { + title: "Astronomy Picture of the Day", + pic_url: "/saturn-voyager.jpg", + }); +}); +app.get("/about", (req, res) => { + res.render("about", { + title: "About APOD", + }); +}); + +app.listen(port, () => { + console.log(`APOD app listening on port ${port}`); +}); diff --git a/09-express/01-apod/app_v5.mjs b/09-express/01-apod/app_v5.mjs new file mode 100644 index 0000000..ffc6635 --- /dev/null +++ b/09-express/01-apod/app_v5.mjs @@ -0,0 +1,30 @@ +import express from "express"; +const app = express(); +const port = 3000; + +app.set("views", "./views2"); +app.set("view engine", "hbs"); + +app.use(express.static("public")); + +app.get("/", (req, res) => { + res.render("home", { + title: "Astronomy Picture of the Day", + pic_url: "/saturn-voyager.jpg", + }); +}); +app.get("/apod/:date", (req, res) => { + res.render("apod-pic", { + title: "Astronomy Picture of the Day", + date: req.params.date, + }); +}); +app.get("/about", (req, res) => { + res.render("about", { + title: "About APOD", + }); +}); + +app.listen(port, () => { + console.log(`APOD app listening on port ${port}`); +}); diff --git a/09-express/01-apod/app_v6.mjs b/09-express/01-apod/app_v6.mjs new file mode 100644 index 0000000..a5d281e --- /dev/null +++ b/09-express/01-apod/app_v6.mjs @@ -0,0 +1,44 @@ +import express from "express"; +import { parse, isBefore, isFuture, isValid } from "date-fns"; + +const app = express(); +const port = 3000; + +app.set("views", "./views2"); +app.set("view engine", "hbs"); + +app.use(express.static("public")); + +app.get("/", (req, res) => { + res.render("home", { + title: "Astronomy Picture of the Day", + pic_url: "/saturn-voyager.jpg", + }); +}); + +const firstApodDate = new Date("1995-06-16"); +function validateDate(req, res, next) { + const date = parse(req.params.date, "yyyy-MM-dd", new Date()); + if (!isValid(date) || isBefore(date, firstApodDate) || isFuture(date)) { + res.status(404); + res.render("apod-not-found"); + } else { + next(); + } +} +app.get("/apod/:date", validateDate, (req, res) => { + res.render("apod-pic", { + title: "Astronomy Picture of the Day", + date: req.params.date, + }); +}); + +app.get("/about", (req, res) => { + res.render("about", { + title: "About APOD", + }); +}); + +app.listen(port, () => { + console.log(`APOD app listening on port ${port}`); +}); diff --git a/09-express/01-apod/app_v7.mjs b/09-express/01-apod/app_v7.mjs new file mode 100644 index 0000000..af0916c --- /dev/null +++ b/09-express/01-apod/app_v7.mjs @@ -0,0 +1,54 @@ +import express from "express"; +import { parse, isBefore, isFuture, isValid, format } from "date-fns"; +import { getApodByDate } from "./apod-api.mjs"; + +const app = express(); +const port = 3000; + +app.set("views", "./views2"); +app.set("view engine", "hbs"); + +app.use(express.static("public")); + +app.get("/", (req, res) => { + const today = format(new Date(), "yyyy-MM-dd"); + getApodByDate(today).then((apodInfo) => { + res.render("apod", { + title: `${apodInfo.title} | Astronomy Picture of the Day`, + date: today, + url: apodInfo.url, + description: apodInfo.explanation, + }); + }); +}); + +const firstApodDate = new Date("1995-06-16"); +function validateDate(req, res, next) { + const date = parse(req.params.date, "yyyy-MM-dd", new Date()); + if (!isValid(date) || isBefore(date, firstApodDate) || isFuture(date)) { + res.status(404); + res.render("apod-not-found"); + } else { + next(); + } +} +app.get("/apod/:date", validateDate, (req, res) => { + getApodByDate(req.params.date).then((apodInfo) => { + res.render("apod", { + title: `${apodInfo.title} | Astronomy Picture of the Day`, + date: req.params.date, + url: apodInfo.url, + description: apodInfo.explanation, + }); + }); +}); + +app.get("/about", (req, res) => { + res.render("about", { + title: "About APOD", + }); +}); + +app.listen(port, () => { + console.log(`APOD app listening on port ${port}`); +}); diff --git a/09-express/01-apod/app_v8.mjs b/09-express/01-apod/app_v8.mjs new file mode 100644 index 0000000..994d976 --- /dev/null +++ b/09-express/01-apod/app_v8.mjs @@ -0,0 +1,56 @@ +import express from "express"; +import { parse, isBefore, isFuture, isValid, format } from "date-fns"; +import { getApodByDate } from "./apod-api.mjs"; + +const app = express(); +const port = 3000; + +app.set("views", "./views2"); +app.set("view engine", "hbs"); + +app.use(express.static("public")); + +app.get("/", (req, res) => { + const today = format(new Date(), "yyyy-MM-dd"); + getApodByDate(today).then((apodInfo) => { + res.render("apod-full", { + title: `${apodInfo.title} | Astronomy Picture of the Day`, + date: today, + isImage: apodInfo.media_type === "image", + url: apodInfo.url, + description: apodInfo.explanation, + }); + }); +}); + +const firstApodDate = new Date("1995-06-16"); +function validateDate(req, res, next) { + const date = parse(req.params.date, "yyyy-MM-dd", new Date()); + if (!isValid(date) || isBefore(date, firstApodDate) || isFuture(date)) { + res.status(404); + res.render("apod-not-found"); + } else { + next(); + } +} +app.get("/apod/:date", validateDate, (req, res) => { + getApodByDate(req.params.date).then((apodInfo) => { + res.render("apod-full", { + title: `${apodInfo.title} | Astronomy Picture of the Day`, + date: req.params.date, + isImage: apodInfo.media_type === "image", + url: apodInfo.url, + description: apodInfo.explanation, + }); + }); +}); + +app.get("/about", (req, res) => { + res.render("about", { + title: "About APOD", + }); +}); + +app.listen(port, () => { + console.log(`APOD app listening on port ${port}`); +}); diff --git a/09-express/01-apod/app_v9.mjs b/09-express/01-apod/app_v9.mjs new file mode 100644 index 0000000..bfbe9eb --- /dev/null +++ b/09-express/01-apod/app_v9.mjs @@ -0,0 +1,73 @@ +import express from "express"; +import { parse, isBefore, isFuture, isValid, format } from "date-fns"; +import { getApodByDate } from "./apod-api.mjs"; + +const app = express(); +const port = 3000; + +app.set("views", "./views2"); +app.set("view engine", "hbs"); + +app.use(express.static("public")); + +app.get("/", (req, res, next) => { + const today = format(new Date(), "yyyy-MM-dd"); + getApodByDate(today) + .then((apodInfo) => { + res.render("apod-full", { + title: `${apodInfo.title} | Astronomy Picture of the Day`, + date: today, + isImage: apodInfo.media_type === "image", + url: apodInfo.url, + description: apodInfo.explanation, + }); + }) + .catch(next); +}); + +const firstApodDate = new Date("1995-06-16"); +function validateDate(req, res, next) { + const date = parse(req.params.date, "yyyy-MM-dd", new Date()); + if (!isValid(date) || isBefore(date, firstApodDate) || isFuture(date)) { + res.status(404); + res.render("apod-not-found"); + } else { + next(); + } +} +app.get("/apod/:date", validateDate, (req, res, next) => { + getApodByDate(req.params.date) + .then((apodInfo) => { + res.render("apod-full", { + title: `${apodInfo.title} | Astronomy Picture of the Day`, + date: req.params.date, + isImage: apodInfo.media_type === "image", + url: apodInfo.url, + description: apodInfo.explanation, + }); + }) + .catch(next); +}); + +app.get("/about", (req, res) => { + res.render("about", { + title: "About APOD", + }); +}); + +app.use((req, res, next) => { + res.status(404); + res.render("404"); +}); + +app.use((err, req, res, next) => { + if (res.headersSent) { + return next(err); + } + res.status(500); + res.render("500"); +}); + +app.listen(port, () => { + console.log(`APOD app listening on port ${port}`); +}); diff --git a/09-express/01-apod/package-lock.json b/09-express/01-apod/package-lock.json new file mode 100644 index 0000000..356288e --- /dev/null +++ b/09-express/01-apod/package-lock.json @@ -0,0 +1,789 @@ +{ + "name": "01-apod", + "version": "1.0.0", + "lockfileVersion": 3, + "requires": true, + "packages": { + "": { + "name": "01-apod", + "version": "1.0.0", + "license": "ISC", + "dependencies": { + "date-fns": "^3.3.1", + "express": "^4.18.2", + "hbs": "^4.2.0" + } + }, + "node_modules/accepts": { + "version": "1.3.8", + "resolved": "https://registry.npmjs.org/accepts/-/accepts-1.3.8.tgz", + "integrity": "sha512-PYAthTa2m2VKxuvSD3DPC/Gy+U+sOA1LAuT8mkmRuvw+NACSaeXEQ+NHcVF7rONl6qcaxV3Uuemwawk+7+SJLw==", + "dependencies": { + "mime-types": "~2.1.34", + "negotiator": "0.6.3" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/array-flatten": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/array-flatten/-/array-flatten-1.1.1.tgz", + "integrity": "sha512-PCVAQswWemu6UdxsDFFX/+gVeYqKAod3D3UVm91jHwynguOwAvYPhx8nNlM++NqRcK6CxxpUafjmhIdKiHibqg==" + }, + "node_modules/body-parser": { + "version": "1.20.1", + "resolved": "https://registry.npmjs.org/body-parser/-/body-parser-1.20.1.tgz", + "integrity": "sha512-jWi7abTbYwajOytWCQc37VulmWiRae5RyTpaCyDcS5/lMdtwSz5lOpDE67srw/HYe35f1z3fDQw+3txg7gNtWw==", + "dependencies": { + "bytes": "3.1.2", + "content-type": "~1.0.4", + "debug": "2.6.9", + "depd": "2.0.0", + "destroy": "1.2.0", + "http-errors": "2.0.0", + "iconv-lite": "0.4.24", + "on-finished": "2.4.1", + "qs": "6.11.0", + "raw-body": "2.5.1", + "type-is": "~1.6.18", + "unpipe": "1.0.0" + }, + "engines": { + "node": ">= 0.8", + "npm": "1.2.8000 || >= 1.4.16" + } + }, + "node_modules/bytes": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/bytes/-/bytes-3.1.2.tgz", + "integrity": "sha512-/Nf7TyzTx6S3yRJObOAV7956r8cr2+Oj8AC5dt8wSP3BQAoeX58NoHyCU8P8zGkNXStjTSi6fzO6F0pBdcYbEg==", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/call-bind": { + "version": "1.0.7", + "resolved": "https://registry.npmjs.org/call-bind/-/call-bind-1.0.7.tgz", + "integrity": "sha512-GHTSNSYICQ7scH7sZ+M2rFopRoLh8t2bLSW6BbgrtLsahOIB5iyAVJf9GjWK3cYTDaMj4XdBpM1cA6pIS0Kv2w==", + "dependencies": { + "es-define-property": "^1.0.0", + "es-errors": "^1.3.0", + "function-bind": "^1.1.2", + "get-intrinsic": "^1.2.4", + "set-function-length": "^1.2.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/content-disposition": { + "version": "0.5.4", + "resolved": "https://registry.npmjs.org/content-disposition/-/content-disposition-0.5.4.tgz", + "integrity": "sha512-FveZTNuGw04cxlAiWbzi6zTAL/lhehaWbTtgluJh4/E95DqMwTmha3KZN1aAWA8cFIhHzMZUvLevkw5Rqk+tSQ==", + "dependencies": { + "safe-buffer": "5.2.1" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/content-type": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/content-type/-/content-type-1.0.5.tgz", + "integrity": "sha512-nTjqfcBFEipKdXCv4YDQWCfmcLZKm81ldF0pAopTvyrFGVbcR6P/VAAd5G7N+0tTr8QqiU0tFadD6FK4NtJwOA==", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/cookie": { + "version": "0.5.0", + "resolved": "https://registry.npmjs.org/cookie/-/cookie-0.5.0.tgz", + "integrity": "sha512-YZ3GUyn/o8gfKJlnlX7g7xq4gyO6OSuhGPKaaGssGB2qgDUS0gPgtTvoyZLTt9Ab6dC4hfc9dV5arkvc/OCmrw==", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/cookie-signature": { + "version": "1.0.6", + "resolved": "https://registry.npmjs.org/cookie-signature/-/cookie-signature-1.0.6.tgz", + "integrity": "sha512-QADzlaHc8icV8I7vbaJXJwod9HWYp8uCqf1xa4OfNu1T7JVxQIrUgOWtHdNDtPiywmFbiS12VjotIXLrKM3orQ==" + }, + "node_modules/date-fns": { + "version": "3.3.1", + "resolved": "https://registry.npmjs.org/date-fns/-/date-fns-3.3.1.tgz", + "integrity": "sha512-y8e109LYGgoQDveiEBD3DYXKba1jWf5BA8YU1FL5Tvm0BTdEfy54WLCwnuYWZNnzzvALy/QQ4Hov+Q9RVRv+Zw==", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/kossnocorp" + } + }, + "node_modules/debug": { + "version": "2.6.9", + "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", + "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==", + "dependencies": { + "ms": "2.0.0" + } + }, + "node_modules/define-data-property": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/define-data-property/-/define-data-property-1.1.4.tgz", + "integrity": "sha512-rBMvIzlpA8v6E+SJZoo++HAYqsLrkg7MSfIinMPFhmkorw7X+dOXVJQs+QT69zGkzMyfDnIMN2Wid1+NbL3T+A==", + "dependencies": { + "es-define-property": "^1.0.0", + "es-errors": "^1.3.0", + "gopd": "^1.0.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/depd": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/depd/-/depd-2.0.0.tgz", + "integrity": "sha512-g7nH6P6dyDioJogAAGprGpCtVImJhpPk/roCzdb3fIh61/s/nPsfR6onyMwkCAR/OlC3yBC0lESvUoQEAssIrw==", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/destroy": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/destroy/-/destroy-1.2.0.tgz", + "integrity": "sha512-2sJGJTaXIIaR1w4iJSNoN0hnMY7Gpc/n8D4qSCJw8QqFWXf7cuAgnEHxBpweaVcPevC2l3KpjYCx3NypQQgaJg==", + "engines": { + "node": ">= 0.8", + "npm": "1.2.8000 || >= 1.4.16" + } + }, + "node_modules/ee-first": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/ee-first/-/ee-first-1.1.1.tgz", + "integrity": "sha512-WMwm9LhRUo+WUaRN+vRuETqG89IgZphVSNkdFgeb6sS/E4OrDIN7t48CAewSHXc6C8lefD8KKfr5vY61brQlow==" + }, + "node_modules/encodeurl": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/encodeurl/-/encodeurl-1.0.2.tgz", + "integrity": "sha512-TPJXq8JqFaVYm2CWmPvnP2Iyo4ZSM7/QKcSmuMLDObfpH5fi7RUGmd/rTDf+rut/saiDiQEeVTNgAmJEdAOx0w==", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/es-define-property": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/es-define-property/-/es-define-property-1.0.0.tgz", + "integrity": "sha512-jxayLKShrEqqzJ0eumQbVhTYQM27CfT1T35+gCgDFoL82JLsXqTJ76zv6A0YLOgEnLUMvLzsDsGIrl8NFpT2gQ==", + "dependencies": { + "get-intrinsic": "^1.2.4" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/es-errors": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/es-errors/-/es-errors-1.3.0.tgz", + "integrity": "sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw==", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/escape-html": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/escape-html/-/escape-html-1.0.3.tgz", + "integrity": "sha512-NiSupZ4OeuGwr68lGIeym/ksIZMJodUGOSCZ/FSnTxcrekbvqrgdUxlJOMpijaKZVjAJrWrGs/6Jy8OMuyj9ow==" + }, + "node_modules/etag": { + "version": "1.8.1", + "resolved": "https://registry.npmjs.org/etag/-/etag-1.8.1.tgz", + "integrity": "sha512-aIL5Fx7mawVa300al2BnEE4iNvo1qETxLrPI/o05L7z6go7fCw1J6EQmbK4FmJ2AS7kgVF/KEZWufBfdClMcPg==", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/express": { + "version": "4.18.2", + "resolved": "https://registry.npmjs.org/express/-/express-4.18.2.tgz", + "integrity": "sha512-5/PsL6iGPdfQ/lKM1UuielYgv3BUoJfz1aUwU9vHZ+J7gyvwdQXFEBIEIaxeGf0GIcreATNyBExtalisDbuMqQ==", + "dependencies": { + "accepts": "~1.3.8", + "array-flatten": "1.1.1", + "body-parser": "1.20.1", + "content-disposition": "0.5.4", + "content-type": "~1.0.4", + "cookie": "0.5.0", + "cookie-signature": "1.0.6", + "debug": "2.6.9", + "depd": "2.0.0", + "encodeurl": "~1.0.2", + "escape-html": "~1.0.3", + "etag": "~1.8.1", + "finalhandler": "1.2.0", + "fresh": "0.5.2", + "http-errors": "2.0.0", + "merge-descriptors": "1.0.1", + "methods": "~1.1.2", + "on-finished": "2.4.1", + "parseurl": "~1.3.3", + "path-to-regexp": "0.1.7", + "proxy-addr": "~2.0.7", + "qs": "6.11.0", + "range-parser": "~1.2.1", + "safe-buffer": "5.2.1", + "send": "0.18.0", + "serve-static": "1.15.0", + "setprototypeof": "1.2.0", + "statuses": "2.0.1", + "type-is": "~1.6.18", + "utils-merge": "1.0.1", + "vary": "~1.1.2" + }, + "engines": { + "node": ">= 0.10.0" + } + }, + "node_modules/finalhandler": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/finalhandler/-/finalhandler-1.2.0.tgz", + "integrity": "sha512-5uXcUVftlQMFnWC9qu/svkWv3GTd2PfUhK/3PLkYNAe7FbqJMt3515HaxE6eRL74GdsriiwujiawdaB1BpEISg==", + "dependencies": { + "debug": "2.6.9", + "encodeurl": "~1.0.2", + "escape-html": "~1.0.3", + "on-finished": "2.4.1", + "parseurl": "~1.3.3", + "statuses": "2.0.1", + "unpipe": "~1.0.0" + }, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/foreachasync": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/foreachasync/-/foreachasync-3.0.0.tgz", + "integrity": "sha512-J+ler7Ta54FwwNcx6wQRDhTIbNeyDcARMkOcguEqnEdtm0jKvN3Li3PDAb2Du3ubJYEWfYL83XMROXdsXAXycw==" + }, + "node_modules/forwarded": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/forwarded/-/forwarded-0.2.0.tgz", + "integrity": "sha512-buRG0fpBtRHSTCOASe6hD258tEubFoRLb4ZNA6NxMVHNw2gOcwHo9wyablzMzOA5z9xA9L1KNjk/Nt6MT9aYow==", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/fresh": { + "version": "0.5.2", + "resolved": "https://registry.npmjs.org/fresh/-/fresh-0.5.2.tgz", + "integrity": "sha512-zJ2mQYM18rEFOudeV4GShTGIQ7RbzA7ozbU9I/XBpm7kqgMywgmylMwXHxZJmkVoYkna9d2pVXVXPdYTP9ej8Q==", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/function-bind": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.2.tgz", + "integrity": "sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==", + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/get-intrinsic": { + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.2.4.tgz", + "integrity": "sha512-5uYhsJH8VJBTv7oslg4BznJYhDoRI6waYCxMmCdnTrcCrHA/fCFKoTFz2JKKE0HdDFUF7/oQuhzumXJK7paBRQ==", + "dependencies": { + "es-errors": "^1.3.0", + "function-bind": "^1.1.2", + "has-proto": "^1.0.1", + "has-symbols": "^1.0.3", + "hasown": "^2.0.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/gopd": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/gopd/-/gopd-1.0.1.tgz", + "integrity": "sha512-d65bNlIadxvpb/A2abVdlqKqV563juRnZ1Wtk6s1sIR8uNsXR70xqIzVqxVf1eTqDunwT2MkczEeaezCKTZhwA==", + "dependencies": { + "get-intrinsic": "^1.1.3" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/handlebars": { + "version": "4.7.7", + "resolved": "https://registry.npmjs.org/handlebars/-/handlebars-4.7.7.tgz", + "integrity": "sha512-aAcXm5OAfE/8IXkcZvCepKU3VzW1/39Fb5ZuqMtgI/hT8X2YgoMvBY5dLhq/cpOvw7Lk1nK/UF71aLG/ZnVYRA==", + "dependencies": { + "minimist": "^1.2.5", + "neo-async": "^2.6.0", + "source-map": "^0.6.1", + "wordwrap": "^1.0.0" + }, + "bin": { + "handlebars": "bin/handlebars" + }, + "engines": { + "node": ">=0.4.7" + }, + "optionalDependencies": { + "uglify-js": "^3.1.4" + } + }, + "node_modules/has-property-descriptors": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/has-property-descriptors/-/has-property-descriptors-1.0.2.tgz", + "integrity": "sha512-55JNKuIW+vq4Ke1BjOTjM2YctQIvCT7GFzHwmfZPGo5wnrgkid0YQtnAleFSqumZm4az3n2BS+erby5ipJdgrg==", + "dependencies": { + "es-define-property": "^1.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/has-proto": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/has-proto/-/has-proto-1.0.1.tgz", + "integrity": "sha512-7qE+iP+O+bgF9clE5+UoBFzE65mlBiVj3tKCrlNQ0Ogwm0BjpT/gK4SlLYDMybDh5I3TCTKnPPa0oMG7JDYrhg==", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/has-symbols": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.0.3.tgz", + "integrity": "sha512-l3LCuF6MgDNwTDKkdYGEihYjt5pRPbEg46rtlmnSPlUbgmB8LOIrKJbYYFBSbnPaJexMKtiPO8hmeRjRz2Td+A==", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/hasown": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.1.tgz", + "integrity": "sha512-1/th4MHjnwncwXsIW6QMzlvYL9kG5e/CpVvLRZe4XPa8TOUNbCELqmvhDmnkNsAjwaG4+I8gJJL0JBvTTLO9qA==", + "dependencies": { + "function-bind": "^1.1.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/hbs": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/hbs/-/hbs-4.2.0.tgz", + "integrity": "sha512-dQwHnrfWlTk5PvG9+a45GYpg0VpX47ryKF8dULVd6DtwOE6TEcYQXQ5QM6nyOx/h7v3bvEQbdn19EDAcfUAgZg==", + "dependencies": { + "handlebars": "4.7.7", + "walk": "2.3.15" + }, + "engines": { + "node": ">= 0.8", + "npm": "1.2.8000 || >= 1.4.16" + } + }, + "node_modules/http-errors": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/http-errors/-/http-errors-2.0.0.tgz", + "integrity": "sha512-FtwrG/euBzaEjYeRqOgly7G0qviiXoJWnvEH2Z1plBdXgbyjv34pHTSb9zoeHMyDy33+DWy5Wt9Wo+TURtOYSQ==", + "dependencies": { + "depd": "2.0.0", + "inherits": "2.0.4", + "setprototypeof": "1.2.0", + "statuses": "2.0.1", + "toidentifier": "1.0.1" + }, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/iconv-lite": { + "version": "0.4.24", + "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.4.24.tgz", + "integrity": "sha512-v3MXnZAcvnywkTUEZomIActle7RXXeedOR31wwl7VlyoXO4Qi9arvSenNQWne1TcRwhCL1HwLI21bEqdpj8/rA==", + "dependencies": { + "safer-buffer": ">= 2.1.2 < 3" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/inherits": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz", + "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==" + }, + "node_modules/ipaddr.js": { + "version": "1.9.1", + "resolved": "https://registry.npmjs.org/ipaddr.js/-/ipaddr.js-1.9.1.tgz", + "integrity": "sha512-0KI/607xoxSToH7GjN1FfSbLoU0+btTicjsQSWQlh/hZykN8KpmMf7uYwPW3R+akZ6R/w18ZlXSHBYXiYUPO3g==", + "engines": { + "node": ">= 0.10" + } + }, + "node_modules/media-typer": { + "version": "0.3.0", + "resolved": "https://registry.npmjs.org/media-typer/-/media-typer-0.3.0.tgz", + "integrity": "sha512-dq+qelQ9akHpcOl/gUVRTxVIOkAJ1wR3QAvb4RsVjS8oVoFjDGTc679wJYmUmknUF5HwMLOgb5O+a3KxfWapPQ==", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/merge-descriptors": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/merge-descriptors/-/merge-descriptors-1.0.1.tgz", + "integrity": "sha512-cCi6g3/Zr1iqQi6ySbseM1Xvooa98N0w31jzUYrXPX2xqObmFGHJ0tQ5u74H3mVh7wLouTseZyYIq39g8cNp1w==" + }, + "node_modules/methods": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/methods/-/methods-1.1.2.tgz", + "integrity": "sha512-iclAHeNqNm68zFtnZ0e+1L2yUIdvzNoauKU4WBA3VvH/vPFieF7qfRlwUZU+DA9P9bPXIS90ulxoUoCH23sV2w==", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/mime": { + "version": "1.6.0", + "resolved": "https://registry.npmjs.org/mime/-/mime-1.6.0.tgz", + "integrity": "sha512-x0Vn8spI+wuJ1O6S7gnbaQg8Pxh4NNHb7KSINmEWKiPE4RKOplvijn+NkmYmmRgP68mc70j2EbeTFRsrswaQeg==", + "bin": { + "mime": "cli.js" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/mime-db": { + "version": "1.52.0", + "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.52.0.tgz", + "integrity": "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/mime-types": { + "version": "2.1.35", + "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.35.tgz", + "integrity": "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==", + "dependencies": { + "mime-db": "1.52.0" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/minimist": { + "version": "1.2.8", + "resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.8.tgz", + "integrity": "sha512-2yyAR8qBkN3YuheJanUpWC5U3bb5osDywNB8RzDVlDwDHbocAJveqqj1u8+SVD7jkWT4yvsHCpWqqWqAxb0zCA==", + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/ms": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", + "integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==" + }, + "node_modules/negotiator": { + "version": "0.6.3", + "resolved": "https://registry.npmjs.org/negotiator/-/negotiator-0.6.3.tgz", + "integrity": "sha512-+EUsqGPLsM+j/zdChZjsnX51g4XrHFOIXwfnCVPGlQk/k5giakcKsuxCObBRu6DSm9opw/O6slWbJdghQM4bBg==", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/neo-async": { + "version": "2.6.2", + "resolved": "https://registry.npmjs.org/neo-async/-/neo-async-2.6.2.tgz", + "integrity": "sha512-Yd3UES5mWCSqR+qNT93S3UoYUkqAZ9lLg8a7g9rimsWmYGK8cVToA4/sF3RrshdyV3sAGMXVUmpMYOw+dLpOuw==" + }, + "node_modules/object-inspect": { + "version": "1.13.1", + "resolved": "https://registry.npmjs.org/object-inspect/-/object-inspect-1.13.1.tgz", + "integrity": "sha512-5qoj1RUiKOMsCCNLV1CBiPYE10sziTsnmNxkAI/rZhiD63CF7IqdFGC/XzjWjpSgLf0LxXX3bDFIh0E18f6UhQ==", + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/on-finished": { + "version": "2.4.1", + "resolved": "https://registry.npmjs.org/on-finished/-/on-finished-2.4.1.tgz", + "integrity": "sha512-oVlzkg3ENAhCk2zdv7IJwd/QUD4z2RxRwpkcGY8psCVcCYZNq4wYnVWALHM+brtuJjePWiYF/ClmuDr8Ch5+kg==", + "dependencies": { + "ee-first": "1.1.1" + }, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/parseurl": { + "version": "1.3.3", + "resolved": "https://registry.npmjs.org/parseurl/-/parseurl-1.3.3.tgz", + "integrity": "sha512-CiyeOxFT/JZyN5m0z9PfXw4SCBJ6Sygz1Dpl0wqjlhDEGGBP1GnsUVEL0p63hoG1fcj3fHynXi9NYO4nWOL+qQ==", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/path-to-regexp": { + "version": "0.1.7", + "resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-0.1.7.tgz", + "integrity": "sha512-5DFkuoqlv1uYQKxy8omFBeJPQcdoE07Kv2sferDCrAq1ohOU+MSDswDIbnx3YAM60qIOnYa53wBhXW0EbMonrQ==" + }, + "node_modules/proxy-addr": { + "version": "2.0.7", + "resolved": "https://registry.npmjs.org/proxy-addr/-/proxy-addr-2.0.7.tgz", + "integrity": "sha512-llQsMLSUDUPT44jdrU/O37qlnifitDP+ZwrmmZcoSKyLKvtZxpyV0n2/bD/N4tBAAZ/gJEdZU7KMraoK1+XYAg==", + "dependencies": { + "forwarded": "0.2.0", + "ipaddr.js": "1.9.1" + }, + "engines": { + "node": ">= 0.10" + } + }, + "node_modules/qs": { + "version": "6.11.0", + "resolved": "https://registry.npmjs.org/qs/-/qs-6.11.0.tgz", + "integrity": "sha512-MvjoMCJwEarSbUYk5O+nmoSzSutSsTwF85zcHPQ9OrlFoZOYIjaqBAJIqIXjptyD5vThxGq52Xu/MaJzRkIk4Q==", + "dependencies": { + "side-channel": "^1.0.4" + }, + "engines": { + "node": ">=0.6" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/range-parser": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/range-parser/-/range-parser-1.2.1.tgz", + "integrity": "sha512-Hrgsx+orqoygnmhFbKaHE6c296J+HTAQXoxEF6gNupROmmGJRoyzfG3ccAveqCBrwr/2yxQ5BVd/GTl5agOwSg==", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/raw-body": { + "version": "2.5.1", + "resolved": "https://registry.npmjs.org/raw-body/-/raw-body-2.5.1.tgz", + "integrity": "sha512-qqJBtEyVgS0ZmPGdCFPWJ3FreoqvG4MVQln/kCgF7Olq95IbOp0/BWyMwbdtn4VTvkM8Y7khCQ2Xgk/tcrCXig==", + "dependencies": { + "bytes": "3.1.2", + "http-errors": "2.0.0", + "iconv-lite": "0.4.24", + "unpipe": "1.0.0" + }, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/safe-buffer": { + "version": "5.2.1", + "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.1.tgz", + "integrity": "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ] + }, + "node_modules/safer-buffer": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/safer-buffer/-/safer-buffer-2.1.2.tgz", + "integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==" + }, + "node_modules/send": { + "version": "0.18.0", + "resolved": "https://registry.npmjs.org/send/-/send-0.18.0.tgz", + "integrity": "sha512-qqWzuOjSFOuqPjFe4NOsMLafToQQwBSOEpS+FwEt3A2V3vKubTquT3vmLTQpFgMXp8AlFWFuP1qKaJZOtPpVXg==", + "dependencies": { + "debug": "2.6.9", + "depd": "2.0.0", + "destroy": "1.2.0", + "encodeurl": "~1.0.2", + "escape-html": "~1.0.3", + "etag": "~1.8.1", + "fresh": "0.5.2", + "http-errors": "2.0.0", + "mime": "1.6.0", + "ms": "2.1.3", + "on-finished": "2.4.1", + "range-parser": "~1.2.1", + "statuses": "2.0.1" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/send/node_modules/ms": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", + "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==" + }, + "node_modules/serve-static": { + "version": "1.15.0", + "resolved": "https://registry.npmjs.org/serve-static/-/serve-static-1.15.0.tgz", + "integrity": "sha512-XGuRDNjXUijsUL0vl6nSD7cwURuzEgglbOaFuZM9g3kwDXOWVTck0jLzjPzGD+TazWbboZYu52/9/XPdUgne9g==", + "dependencies": { + "encodeurl": "~1.0.2", + "escape-html": "~1.0.3", + "parseurl": "~1.3.3", + "send": "0.18.0" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/set-function-length": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/set-function-length/-/set-function-length-1.2.1.tgz", + "integrity": "sha512-j4t6ccc+VsKwYHso+kElc5neZpjtq9EnRICFZtWyBsLojhmeF/ZBd/elqm22WJh/BziDe/SBiOeAt0m2mfLD0g==", + "dependencies": { + "define-data-property": "^1.1.2", + "es-errors": "^1.3.0", + "function-bind": "^1.1.2", + "get-intrinsic": "^1.2.3", + "gopd": "^1.0.1", + "has-property-descriptors": "^1.0.1" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/setprototypeof": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/setprototypeof/-/setprototypeof-1.2.0.tgz", + "integrity": "sha512-E5LDX7Wrp85Kil5bhZv46j8jOeboKq5JMmYM3gVGdGH8xFpPWXUMsNrlODCrkoxMEeNi/XZIwuRvY4XNwYMJpw==" + }, + "node_modules/side-channel": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/side-channel/-/side-channel-1.0.5.tgz", + "integrity": "sha512-QcgiIWV4WV7qWExbN5llt6frQB/lBven9pqliLXfGPB+K9ZYXxDozp0wLkHS24kWCm+6YXH/f0HhnObZnZOBnQ==", + "dependencies": { + "call-bind": "^1.0.6", + "es-errors": "^1.3.0", + "get-intrinsic": "^1.2.4", + "object-inspect": "^1.13.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/source-map": { + "version": "0.6.1", + "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz", + "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/statuses": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/statuses/-/statuses-2.0.1.tgz", + "integrity": "sha512-RwNA9Z/7PrK06rYLIzFMlaF+l73iwpzsqRIFgbMLbTcLD6cOao82TaWefPXQvB2fOC4AjuYSEndS7N/mTCbkdQ==", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/toidentifier": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/toidentifier/-/toidentifier-1.0.1.tgz", + "integrity": "sha512-o5sSPKEkg/DIQNmH43V0/uerLrpzVedkUh8tGNvaeXpfpuwjKenlSox/2O/BTlZUtEe+JG7s5YhEz608PlAHRA==", + "engines": { + "node": ">=0.6" + } + }, + "node_modules/type-is": { + "version": "1.6.18", + "resolved": "https://registry.npmjs.org/type-is/-/type-is-1.6.18.tgz", + "integrity": "sha512-TkRKr9sUTxEH8MdfuCSP7VizJyzRNMjj2J2do2Jr3Kym598JVdEksuzPQCnlFPW4ky9Q+iA+ma9BGm06XQBy8g==", + "dependencies": { + "media-typer": "0.3.0", + "mime-types": "~2.1.24" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/uglify-js": { + "version": "3.17.4", + "resolved": "https://registry.npmjs.org/uglify-js/-/uglify-js-3.17.4.tgz", + "integrity": "sha512-T9q82TJI9e/C1TAxYvfb16xO120tMVFZrGA3f9/P4424DNu6ypK103y0GPFVa17yotwSyZW5iYXgjYHkGrJW/g==", + "optional": true, + "bin": { + "uglifyjs": "bin/uglifyjs" + }, + "engines": { + "node": ">=0.8.0" + } + }, + "node_modules/unpipe": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/unpipe/-/unpipe-1.0.0.tgz", + "integrity": "sha512-pjy2bYhSsufwWlKwPc+l3cN7+wuJlK6uz0YdJEOlQDbl6jo/YlPi4mb8agUkVC8BF7V8NuzeyPNqRksA3hztKQ==", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/utils-merge": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/utils-merge/-/utils-merge-1.0.1.tgz", + "integrity": "sha512-pMZTvIkT1d+TFGvDOqodOclx0QWkkgi6Tdoa8gC8ffGAAqz9pzPTZWAybbsHHoED/ztMtkv/VoYTYyShUn81hA==", + "engines": { + "node": ">= 0.4.0" + } + }, + "node_modules/vary": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/vary/-/vary-1.1.2.tgz", + "integrity": "sha512-BNGbWLfd0eUPabhkXUVm0j8uuvREyTh5ovRa/dyow/BqAbZJyC+5fU+IzQOzmAKzYqYRAISoRhdQr3eIZ/PXqg==", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/walk": { + "version": "2.3.15", + "resolved": "https://registry.npmjs.org/walk/-/walk-2.3.15.tgz", + "integrity": "sha512-4eRTBZljBfIISK1Vnt69Gvr2w/wc3U6Vtrw7qiN5iqYJPH7LElcYh/iU4XWhdCy2dZqv1ToMyYlybDylfG/5Vg==", + "dependencies": { + "foreachasync": "^3.0.0" + } + }, + "node_modules/wordwrap": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/wordwrap/-/wordwrap-1.0.0.tgz", + "integrity": "sha512-gvVzJFlPycKc5dZN4yPkP8w7Dc37BtP1yczEneOb4uq34pXZcvrtRTmWV8W+Ume+XCxKgbjM+nevkyFPMybd4Q==" + } + } +} diff --git a/09-express/01-apod/package.json b/09-express/01-apod/package.json new file mode 100644 index 0000000..9eba6fb --- /dev/null +++ b/09-express/01-apod/package.json @@ -0,0 +1,17 @@ +{ + "name": "01-apod", + "version": "1.0.0", + "description": "", + "main": "index.js", + "scripts": { + "test": "echo \"Error: no test specified\" && exit 1" + }, + "keywords": [], + "author": "", + "license": "ISC", + "dependencies": { + "date-fns": "^3.3.1", + "express": "^4.18.2", + "hbs": "^4.2.0" + } +} diff --git a/09-express/01-apod/public/favicon.ico b/09-express/01-apod/public/favicon.ico new file mode 100644 index 0000000..b78bc3c Binary files /dev/null and b/09-express/01-apod/public/favicon.ico differ diff --git a/09-express/01-apod/public/mvp.css b/09-express/01-apod/public/mvp.css new file mode 100644 index 0000000..cec2cf6 --- /dev/null +++ b/09-express/01-apod/public/mvp.css @@ -0,0 +1,536 @@ +/* MVP.css v1.14 - https://github.com/andybrewer/mvp */ + +:root { + --active-brightness: 0.85; + --border-radius: 5px; + --box-shadow: 2px 2px 10px; + --color-accent: #118bee15; + --color-bg: #fff; + --color-bg-secondary: #e9e9e9; + --color-link: #118bee; + --color-secondary: #920de9; + --color-secondary-accent: #920de90b; + --color-shadow: #f4f4f4; + --color-table: #118bee; + --color-text: #000; + --color-text-secondary: #999; + --font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, + Oxygen-Sans, Ubuntu, Cantarell, "Helvetica Neue", sans-serif; + --hover-brightness: 1.2; + --justify-important: center; + --justify-normal: left; + --line-height: 1.5; + --width-card: 285px; + --width-card-medium: 460px; + --width-card-wide: 800px; + --width-content: 1080px; +} + +@media (prefers-color-scheme: dark) { + :root[color-mode="user"] { + --color-accent: #0097fc4f; + --color-bg: #333; + --color-bg-secondary: #555; + --color-link: #0097fc; + --color-secondary: #e20de9; + --color-secondary-accent: #e20de94f; + --color-shadow: #bbbbbb20; + --color-table: #0097fc; + --color-text: #f7f7f7; + --color-text-secondary: #aaa; + } +} + +html { + scroll-behavior: smooth; +} + +@media (prefers-reduced-motion: reduce) { + html { + scroll-behavior: auto; + } +} + +/* Layout */ +article aside { + background: var(--color-secondary-accent); + border-left: 4px solid var(--color-secondary); + padding: 0.01rem 0.8rem; +} + +body { + background: var(--color-bg); + color: var(--color-text); + font-family: var(--font-family); + line-height: var(--line-height); + margin: 0; + overflow-x: hidden; + padding: 0; +} + +footer, +header, +main { + margin: 0 auto; + max-width: var(--width-content); + padding: 0rem 1rem 1rem 1rem; +} + +hr { + background-color: var(--color-bg-secondary); + border: none; + height: 1px; + width: 100%; +} + +section { + display: flex; + flex-wrap: wrap; + justify-content: var(--justify-important); +} + +section img, +article img { + max-width: 100%; +} + +section pre { + overflow: auto; +} + +section aside { + border: 1px solid var(--color-bg-secondary); + border-radius: var(--border-radius); + box-shadow: var(--box-shadow) var(--color-shadow); + margin: 1rem; + padding: 1.25rem; + width: var(--width-card); +} + +section aside:hover { + box-shadow: var(--box-shadow) var(--color-bg-secondary); +} + +[hidden] { + display: none; +} + +/* Headers */ +article header, +div header, +main header { + padding-top: 0; +} + +header { + text-align: var(--justify-important); +} + +header a b, +header a em, +header a i, +header a strong { + margin-left: 0.5rem; + margin-right: 0.5rem; +} + +header nav img { + margin: 1rem 0; +} + +section header { + padding-top: 0; + width: 100%; +} + +/* Nav */ +nav { + align-items: center; + display: flex; + font-weight: bold; + justify-content: space-between; + margin-bottom: 0; +} + +nav ul { + list-style: none; + padding: 0; +} + +nav ul li { + display: inline-block; + margin: 0 0.5rem; + position: relative; + text-align: left; +} + +/* Nav Dropdown */ +nav ul li:hover ul { + display: block; +} + +nav ul li ul { + background: var(--color-bg); + border: 1px solid var(--color-bg-secondary); + border-radius: var(--border-radius); + box-shadow: var(--box-shadow) var(--color-shadow); + display: none; + height: auto; + left: -2px; + padding: 0.5rem 1rem; + position: absolute; + top: 1.7rem; + white-space: nowrap; + width: auto; + z-index: 1; +} + +nav ul li ul::before { + /* fill gap above to make mousing over them easier */ + content: ""; + position: absolute; + left: 0; + right: 0; + top: -0.5rem; + height: 0.5rem; +} + +nav ul li ul li, +nav ul li ul li a { + display: block; +} + +/* Typography */ +code, +samp { + background-color: var(--color-accent); + border-radius: var(--border-radius); + color: var(--color-text); + display: inline-block; + margin: 0 0.1rem; + padding: 0 0.5rem; +} + +details { + margin: 1.3rem 0; +} + +details summary { + font-weight: bold; + cursor: pointer; +} + +h1, +h2, +h3, +h4, +h5, +h6 { + line-height: var(--line-height); + text-wrap: balance; +} + +mark { + padding: 0.1rem; +} + +ol li, +ul li { + padding: 0.2rem 0; +} + +p { + margin: 0.75rem 0; + padding: 0; + width: 100%; +} + +pre { + margin: 1rem 0; + max-width: var(--width-card-wide); + padding: 1rem 0; +} + +pre code, +pre samp { + display: block; + max-width: var(--width-card-wide); + padding: 0.5rem 2rem; + white-space: pre-wrap; +} + +small { + color: var(--color-text-secondary); +} + +sup { + background-color: var(--color-secondary); + border-radius: var(--border-radius); + color: var(--color-bg); + font-size: xx-small; + font-weight: bold; + margin: 0.2rem; + padding: 0.2rem 0.3rem; + position: relative; + top: -2px; +} + +/* Links */ +a { + color: var(--color-link); + display: inline-block; + font-weight: bold; + text-decoration: underline; +} + +a:active { + filter: brightness(var(--active-brightness)); +} + +a:hover { + filter: brightness(var(--hover-brightness)); +} + +a b, +a em, +a i, +a strong, +button, +input[type="submit"] { + border-radius: var(--border-radius); + display: inline-block; + font-size: medium; + font-weight: bold; + line-height: var(--line-height); + margin: 0.5rem 0; + padding: 1rem 2rem; +} + +button, +input[type="submit"] { + font-family: var(--font-family); +} + +button:active, +input[type="submit"]:active { + filter: brightness(var(--active-brightness)); +} + +button:hover, +input[type="submit"]:hover { + cursor: pointer; + filter: brightness(var(--hover-brightness)); +} + +a b, +a strong, +button, +input[type="submit"] { + background-color: var(--color-link); + border: 2px solid var(--color-link); + color: var(--color-bg); +} + +a em, +a i { + border: 2px solid var(--color-link); + border-radius: var(--border-radius); + color: var(--color-link); + display: inline-block; + padding: 1rem 2rem; +} + +article aside a { + color: var(--color-secondary); +} + +/* Images */ +figure { + margin: 0; + padding: 0; +} + +figure img { + max-width: 100%; +} + +figure figcaption { + color: var(--color-text-secondary); +} + +/* Forms */ +button:disabled, +input:disabled { + background: var(--color-bg-secondary); + border-color: var(--color-bg-secondary); + color: var(--color-text-secondary); + cursor: not-allowed; +} + +button[disabled]:hover, +input[type="submit"][disabled]:hover { + filter: none; +} + +form { + border: 1px solid var(--color-bg-secondary); + border-radius: var(--border-radius); + box-shadow: var(--box-shadow) var(--color-shadow); + display: block; + max-width: var(--width-card-wide); + min-width: var(--width-card); + padding: 1.5rem; + text-align: var(--justify-normal); +} + +form header { + margin: 1.5rem 0; + padding: 1.5rem 0; +} + +input, +label, +select, +textarea { + display: block; + font-size: inherit; + max-width: var(--width-card-wide); +} + +input[type="checkbox"], +input[type="radio"] { + display: inline-block; +} + +input[type="checkbox"] + label, +input[type="radio"] + label { + display: inline-block; + font-weight: normal; + position: relative; + top: 1px; +} + +input[type="range"] { + padding: 0.4rem 0; +} + +input, +select, +textarea { + border: 1px solid var(--color-bg-secondary); + border-radius: var(--border-radius); + margin-bottom: 1rem; + padding: 0.4rem 0.8rem; +} + +input[type="text"], +textarea { + width: calc(100% - 1.6rem); +} + +input[readonly], +textarea[readonly] { + background-color: var(--color-bg-secondary); +} + +label { + font-weight: bold; + margin-bottom: 0.2rem; +} + +/* Popups */ +dialog { + border: 1px solid var(--color-bg-secondary); + border-radius: var(--border-radius); + box-shadow: var(--box-shadow) var(--color-shadow); + position: fixed; + top: 50%; + left: 50%; + transform: translate(-50%, -50%); + width: 50%; + z-index: 999; +} + +/* Tables */ +table { + border: 1px solid var(--color-bg-secondary); + border-radius: var(--border-radius); + border-spacing: 0; + display: inline-block; + max-width: 100%; + overflow-x: auto; + padding: 0; + white-space: nowrap; +} + +table td, +table th, +table tr { + padding: 0.4rem 0.8rem; + text-align: var(--justify-important); +} + +table thead { + background-color: var(--color-table); + border-collapse: collapse; + border-radius: var(--border-radius); + color: var(--color-bg); + margin: 0; + padding: 0; +} + +table thead tr:first-child th:first-child { + border-top-left-radius: var(--border-radius); +} + +table thead tr:first-child th:last-child { + border-top-right-radius: var(--border-radius); +} + +table thead th:first-child, +table tr td:first-child { + text-align: var(--justify-normal); +} + +table tr:nth-child(even) { + background-color: var(--color-accent); +} + +/* Quotes */ +blockquote { + display: block; + font-size: x-large; + line-height: var(--line-height); + margin: 1rem auto; + max-width: var(--width-card-medium); + padding: 1.5rem 1rem; + text-align: var(--justify-important); +} + +blockquote footer { + color: var(--color-text-secondary); + display: block; + font-size: small; + line-height: var(--line-height); + padding: 1.5rem 0; +} + +/* Scrollbars */ +* { + scrollbar-width: thin; + scrollbar-color: rgb(202, 202, 232) auto; +} + +*::-webkit-scrollbar { + width: 5px; + height: 5px; +} + +*::-webkit-scrollbar-track { + background: transparent; +} + +*::-webkit-scrollbar-thumb { + background-color: rgb(202, 202, 232); + border-radius: 10px; +} diff --git a/09-express/01-apod/public/saturn-voyager.jpg b/09-express/01-apod/public/saturn-voyager.jpg new file mode 100644 index 0000000..dd712d2 Binary files /dev/null and b/09-express/01-apod/public/saturn-voyager.jpg differ diff --git a/09-express/01-apod/views/about.hbs b/09-express/01-apod/views/about.hbs new file mode 100644 index 0000000..99d3f86 --- /dev/null +++ b/09-express/01-apod/views/about.hbs @@ -0,0 +1,12 @@ + + About APOD + +

    Astronomy Picture of the Day

    +

    + Astronomy Picture of the Day (APOD) is originated, written, + coordinated, and edited since 1995 by Robert Nemiroff and Jerry + Bonnell. The APOD archive contains the largest collection of + annotated astronomical images on the internet. +

    + + \ No newline at end of file diff --git a/09-express/01-apod/views/home.hbs b/09-express/01-apod/views/home.hbs new file mode 100644 index 0000000..2455c71 --- /dev/null +++ b/09-express/01-apod/views/home.hbs @@ -0,0 +1,12 @@ + + + {{title}} + + + +
    +

    {{title}}

    + +
    + + \ No newline at end of file diff --git a/09-express/01-apod/views2/404.hbs b/09-express/01-apod/views2/404.hbs new file mode 100644 index 0000000..82d5852 --- /dev/null +++ b/09-express/01-apod/views2/404.hbs @@ -0,0 +1,2 @@ +

    404

    +

    page not found :\

    \ No newline at end of file diff --git a/09-express/01-apod/views2/500.hbs b/09-express/01-apod/views2/500.hbs new file mode 100644 index 0000000..430ad36 --- /dev/null +++ b/09-express/01-apod/views2/500.hbs @@ -0,0 +1 @@ +

    Internal server error :\

    \ No newline at end of file diff --git a/09-express/01-apod/views2/about.hbs b/09-express/01-apod/views2/about.hbs new file mode 100644 index 0000000..e51e198 --- /dev/null +++ b/09-express/01-apod/views2/about.hbs @@ -0,0 +1,7 @@ +

    {{title}}

    +

    + Astronomy Picture of the Day (APOD) is originated, written, coordinated, and + edited since 1995 by Robert Nemiroff and Jerry Bonnell. The APOD archive + contains the largest collection of annotated astronomical images on the + internet. +

    \ No newline at end of file diff --git a/09-express/01-apod/views2/apod-full.hbs b/09-express/01-apod/views2/apod-full.hbs new file mode 100644 index 0000000..328b2f4 --- /dev/null +++ b/09-express/01-apod/views2/apod-full.hbs @@ -0,0 +1,8 @@ +

    {{title}}

    +

    Picture of {{date}}

    +{{#if isImage}} + +{{else}} + Open video +{{/if}} +

    {{description}}

    \ No newline at end of file diff --git a/09-express/01-apod/views2/apod-not-found.hbs b/09-express/01-apod/views2/apod-not-found.hbs new file mode 100644 index 0000000..2edcd27 --- /dev/null +++ b/09-express/01-apod/views2/apod-not-found.hbs @@ -0,0 +1,2 @@ +

    APOD not found

    +

    Please, try with another date

    \ No newline at end of file diff --git a/09-express/01-apod/views2/apod-pic.hbs b/09-express/01-apod/views2/apod-pic.hbs new file mode 100644 index 0000000..ef13b97 --- /dev/null +++ b/09-express/01-apod/views2/apod-pic.hbs @@ -0,0 +1,2 @@ +

    {{title}}

    +

    Picture of {{date}}

    \ No newline at end of file diff --git a/09-express/01-apod/views2/apod.hbs b/09-express/01-apod/views2/apod.hbs new file mode 100644 index 0000000..70e7176 --- /dev/null +++ b/09-express/01-apod/views2/apod.hbs @@ -0,0 +1,4 @@ +

    {{title}}

    +

    Picture of {{date}}

    + +

    {{description}}

    \ No newline at end of file diff --git a/09-express/01-apod/views2/home.hbs b/09-express/01-apod/views2/home.hbs new file mode 100644 index 0000000..ac77870 --- /dev/null +++ b/09-express/01-apod/views2/home.hbs @@ -0,0 +1,2 @@ +

    {{title}}

    + \ No newline at end of file diff --git a/09-express/01-apod/views2/layout.hbs b/09-express/01-apod/views2/layout.hbs new file mode 100644 index 0000000..1b23447 --- /dev/null +++ b/09-express/01-apod/views2/layout.hbs @@ -0,0 +1,30 @@ + + + + + + {{title}} + + +
    + +
    +
    + {{{body}}} +
    + + + \ No newline at end of file diff --git a/09-express/02-feed-api/app_v1.mjs b/09-express/02-feed-api/app_v1.mjs new file mode 100644 index 0000000..d589e9b --- /dev/null +++ b/09-express/02-feed-api/app_v1.mjs @@ -0,0 +1,34 @@ +import express from "express"; +import { getLatestEntries } from "./get-feeds.mjs"; +const app = express(); +const port = 3000; + +const wrap = + (fn) => + (...args) => + fn(...args).catch(args[2]); + +const sources = []; + +app.use(express.json()); + +app.post("/sources", (req, res) => { + sources.push(req.body.url); + res.json(sources); +}); + +app.get("/sources", (req, res) => { + res.json(sources); +}); + +app.get( + "/feeds", + wrap(async (req, res) => { + const entries = await getLatestEntries(sources); + res.json(entries); + }) +); + +app.listen(port, () => { + console.log(`FEED app listening on port ${port}`); +}); diff --git a/09-express/02-feed-api/app_v2.mjs b/09-express/02-feed-api/app_v2.mjs new file mode 100644 index 0000000..00273e8 --- /dev/null +++ b/09-express/02-feed-api/app_v2.mjs @@ -0,0 +1,50 @@ +import express from "express"; +import { getLatestEntries } from "./get-feeds.mjs"; +import { hrtime } from "process"; +import compression from "compression"; + +const app = express(); +const port = 3000; + +const wrap = + (fn) => + (...args) => + fn(...args).catch(args[2]); + +const sources = []; + +app.use((req, res, next) => { + res.startTime = hrtime.bigint(); + + res.on("finish", () => { + const duration = (hrtime.bigint() - res.startTime) / BigInt(1e6); + console.log( + `${req.method} ${req.path} ${res.statusCode} - ${duration}ms` + ); + }); + next(); +}); + +app.use(compression()); +app.use(express.json()); + +app.post("/sources", (req, res) => { + sources.push(req.body.url); + res.json(sources); +}); + +app.get("/sources", (req, res) => { + res.json(sources); +}); + +app.get( + "/feeds", + wrap(async (req, res) => { + const entries = await getLatestEntries(sources); + res.json(entries); + }) +); + +app.listen(port, () => { + console.log(`FEED app listening on port ${port}`); +}); diff --git a/09-express/02-feed-api/app_v3.mjs b/09-express/02-feed-api/app_v3.mjs new file mode 100644 index 0000000..3dba3a8 --- /dev/null +++ b/09-express/02-feed-api/app_v3.mjs @@ -0,0 +1,41 @@ +import express from "express"; +import { getLatestEntries } from "./get-feeds.mjs"; +import compression from "compression"; +import pino from "pino-http"; + +const app = express(); +const port = 3000; + +const wrap = + (fn) => + (...args) => + fn(...args).catch(args[2]); + +const sources = []; + +app.use(pino()); +app.use(compression()); +app.use(express.json()); + +app.post("/sources", (req, res) => { + sources.push(req.body.url); + req.log.info(`New URL added to sources: ${req.body.url}`); + res.json(sources); +}); + +app.get("/sources", (req, res) => { + res.json(sources); +}); + +app.get( + "/feeds", + wrap(async (req, res) => { + req.log.debug(`Feed sources are ${sources}`); + const entries = await getLatestEntries(sources); + res.json(entries); + }) +); + +app.listen(port, () => { + console.log(`FEED app listening on port ${port}`); +}); diff --git a/09-express/02-feed-api/app_v4.mjs b/09-express/02-feed-api/app_v4.mjs new file mode 100644 index 0000000..92a2fc8 --- /dev/null +++ b/09-express/02-feed-api/app_v4.mjs @@ -0,0 +1,53 @@ +import express from "express"; +import { getLatestEntries } from "./get-feeds.mjs"; +import compression from "compression"; +import pino from "pino-http"; +import mongoose from "mongoose"; +import { Source } from "./models/Source.mjs"; + +await mongoose.connect("mongodb://127.0.0.1:27017/feed-reader"); + +const app = express(); +const port = 3000; + +const wrap = + (fn) => + (...args) => + fn(...args).catch(args[2]); + +app.use(pino()); +app.use(compression()); +app.use(express.json()); + +app.post( + "/sources", + wrap(async (req, res) => { + const source = new Source({ url: req.body.url }); + req.log.info(`Adding new Source: ${source}`); + await source.save(); + res.json(source); + }) +); + +app.get( + "/sources", + wrap(async (req, res) => { + const sources = await Source.find({}).exec(); + res.json(sources); + }) +); + +app.get( + "/feeds", + wrap(async (req, res) => { + const sources = await Source.find({}).exec(); + const urls = sources.map((s) => s.url); + req.log.debug(`Feed sources are ${urls}`); + const entries = await getLatestEntries(urls); + res.json(entries); + }) +); + +app.listen(port, () => { + console.log(`FEED app listening on port ${port}`); +}); diff --git a/09-express/02-feed-api/app_v5.mjs b/09-express/02-feed-api/app_v5.mjs new file mode 100644 index 0000000..60b5a6e --- /dev/null +++ b/09-express/02-feed-api/app_v5.mjs @@ -0,0 +1,72 @@ +import express from "express"; +import { getLatestEntries } from "./get-feeds.mjs"; +import compression from "compression"; +import pino from "pino-http"; +import mongoose from "mongoose"; +import { Source } from "./models/Source_v1.mjs"; + +await mongoose.connect("mongodb://127.0.0.1:27017/feed-reader"); + +const app = express(); +const port = 3000; + +const wrap = + (fn) => + (...args) => + fn(...args).catch(args[2]); + +app.use(pino()); +app.use(compression()); +app.use(express.json()); + +app.post( + "/sources", + wrap(async (req, res) => { + const source = new Source({ url: req.body.url }); + req.log.info(`Adding new Source: ${source}`); + try { + await source.save(); + res.json(source); + } catch (e) { + req.log.error("An error occurred during Source.save()"); + res.status(500).send(); + } + }) +); + +app.get( + "/sources", + wrap(async (req, res) => { + const sources = await Source.find({}).exec(); + res.json(sources); + }) +); + +app.delete( + "/sources/:id", + wrap(async (req, res) => { + try { + await Source.findByIdAndDelete(req.params.id).exec(); + res.status(200).send(); + } catch { + req.log.error( + `Error while deleting Source with id ${req.params.id}` + ); + } + }) +); + +app.get( + "/feeds", + wrap(async (req, res) => { + const sources = await Source.find({}).exec(); + const urls = sources.map((s) => s.url); + req.log.debug(`Feed sources are ${urls}`); + const entries = await getLatestEntries(urls); + res.json(entries); + }) +); + +app.listen(port, () => { + console.log(`FEED app listening on port ${port}`); +}); diff --git a/09-express/02-feed-api/app_v6.mjs b/09-express/02-feed-api/app_v6.mjs new file mode 100644 index 0000000..44d786d --- /dev/null +++ b/09-express/02-feed-api/app_v6.mjs @@ -0,0 +1,77 @@ +import express from "express"; +import compression from "compression"; +import pino from "pino-http"; +import mongoose from "mongoose"; +import { Source } from "./models/Source_v1.mjs"; + +await mongoose.connect("mongodb://127.0.0.1:27017/feed-reader"); + +const app = express(); +const port = 3000; + +const wrap = + (fn) => + (...args) => + fn(...args).catch(args[2]); + +app.use(pino()); +app.use(compression()); +app.use(express.json()); + +app.post( + "/sources", + wrap(async (req, res) => { + const source = new Source({ url: req.body.url }); + req.log.info(`Adding new Source: ${source}`); + try { + await source.save(); + res.json(source); + } catch (e) { + const reason = Source.getErrorReason(e); + if (reason) { + req.log.error(reason); + res.status(400).send(); + } else { + req.log.error("An error occurred during Source.save()"); + res.status(500).send(); + } + } + }) +); + +app.get( + "/sources", + wrap(async (req, res) => { + const sources = await Source.find({}).exec(); + res.json(sources); + }) +); + +app.delete( + "/sources/:id", + wrap(async (req, res) => { + try { + await Source.findByIdAndDelete(req.params.id).exec(); + res.status(200).send(); + } catch { + req.log.error( + `Error while deleting Source with id ${req.params.id}` + ); + } + }) +); + +app.get( + "/feeds", + wrap(async (req, res) => { + const sources = await Source.find({}).exec(); + const urls = sources.map((s) => s.url); + req.log.debug(`Feed sources are ${urls}`); + const entries = await getLatestEntries(urls); + res.json(entries); + }) +); + +app.listen(port, () => { + console.log(`FEED app listening on port ${port}`); +}); diff --git a/09-express/02-feed-api/app_v7.mjs b/09-express/02-feed-api/app_v7.mjs new file mode 100644 index 0000000..b4743ac --- /dev/null +++ b/09-express/02-feed-api/app_v7.mjs @@ -0,0 +1,114 @@ +import express from "express"; +import getFeedEntries from "./get-feeds.mjs"; +import compression from "compression"; +import pino from "pino-http"; +import mongoose from "mongoose"; +import { Source } from "./models/Source_v1.mjs"; +import { FeedEntry } from "./models/FeedEntry.mjs"; + +await mongoose.connect("mongodb://127.0.0.1:27017/feed-reader"); + +const app = express(); +const port = 3000; + +const wrap = + (fn) => + (...args) => + fn(...args).catch(args[2]); + +app.use(pino()); +app.use(compression()); +app.use(express.json()); + +app.post( + "/sources", + wrap(async (req, res) => { + const source = new Source({ url: req.body.url }); + req.log.info(`Adding new Source: ${source}`); + try { + await source.save(); + res.json(source); + } catch (e) { + const reason = Source.getErrorReason(e); + if (reason) { + req.log.error(reason); + res.status(400).send(); + } else { + req.log.error("An error occurred during Source.save()"); + res.status(500).send(); + } + } + }) +); + +app.get( + "/sources", + wrap(async (req, res) => { + const sources = await Source.find({}).exec(); + res.json(sources); + }) +); + +app.delete( + "/sources/:id", + wrap(async (req, res) => { + try { + await Source.findByIdAndDelete(req.params.id).exec(); + res.status(200).send(); + } catch { + req.log.error( + `Error while deleting Source with id ${req.params.id}` + ); + } + }) +); + +app.get( + "/feeds", + wrap(async (req, res) => { + let findQuery = {}; + if (req.query.sourceId) { + findQuery.source = req.query.sourceId; + } + const feedEntries = await FeedEntry.find(findQuery) + .sort({ date: -1 }) + .limit(20) + .exec(); + res.json(feedEntries); + }) +); +function generateFeedEntries(entries, s) { + return entries.map((en) => { + return new FeedEntry({ + source: s._id, + title: en.title, + link: en.link, + date: new Date(en.pubDate), + }).save(); + }); +} +async function waitAndCountFulfilled(proms) { + const promsSettled = await Promise.allSettled(proms); + return promsSettled.filter((r) => r.status === "fulfilled").length; +} +app.post( + "/feeds-update", + wrap(async (req, res) => { + const sources = await Source.find({}).exec(); + let totalNewEntries = 0; + for (const s of sources) { + const entries = await getFeedEntries(s.url); + const savingModels = generateFeedEntries(entries, s); + let newCount = await waitAndCountFulfilled(savingModels); + totalNewEntries += newCount; + req.log.info( + `Feed ${s.url}: found ${entries.length} and saved ${newCount} entries` + ); + } + res.json({ totalNewEntries }); + }) +); + +app.listen(port, () => { + console.log(`FEED app listening on port ${port}`); +}); diff --git a/09-express/02-feed-api/app_v8.mjs b/09-express/02-feed-api/app_v8.mjs new file mode 100644 index 0000000..4033f97 --- /dev/null +++ b/09-express/02-feed-api/app_v8.mjs @@ -0,0 +1,115 @@ +import express from "express"; +import getFeedEntries from "./get-feeds.mjs"; +import compression from "compression"; +import pino from "pino-http"; +import mongoose from "mongoose"; +import { Source } from "./models/Source_v1.mjs"; +import { FeedEntry } from "./models/FeedEntry.mjs"; + +await mongoose.connect("mongodb://127.0.0.1:27017/feed-reader"); + +const app = express(); +const port = 3000; + +const wrap = + (fn) => + (...args) => + fn(...args).catch(args[2]); + +app.use(pino()); +app.use(compression()); +app.use(express.json()); + +app.post( + "/sources", + wrap(async (req, res) => { + const source = new Source({ url: req.body.url }); + req.log.info(`Adding new Source: ${source}`); + try { + await source.save(); + res.json(source); + } catch (e) { + const reason = Source.getErrorReason(e); + if (reason) { + req.log.error(reason); + res.status(400).send(); + } else { + req.log.error("An error occurred during Source.save()"); + res.status(500).send(); + } + } + }) +); + +app.get( + "/sources", + wrap(async (req, res) => { + const sources = await Source.find({}).exec(); + res.json(sources); + }) +); + +app.delete( + "/sources/:id", + wrap(async (req, res) => { + try { + await Source.findByIdAndDelete(req.params.id).exec(); + res.status(200).send(); + } catch { + req.log.error( + `Error while deleting Source with id ${req.params.id}` + ); + } + }) +); + +app.get( + "/feeds", + wrap(async (req, res) => { + let findQuery = {}; + if (req.query.sourceId) { + findQuery.source = req.query.sourceId; + } + const feedEntries = await FeedEntry.find(findQuery) + .sort({ date: -1 }) + .limit(20) + .populate("source") + .exec(); + res.json(feedEntries); + }) +); +function generateFeedEntries(entries, s) { + return entries.map((en) => { + return new FeedEntry({ + source: s._id, + title: en.title, + link: en.link, + date: new Date(en.pubDate), + }).save(); + }); +} +async function waitAndCountFulfilled(proms) { + const promsSettled = await Promise.allSettled(proms); + return promsSettled.filter((r) => r.status === "fulfilled").length; +} +app.post( + "/feeds-update", + wrap(async (req, res) => { + const sources = await Source.find({}).exec(); + let totalNewEntries = 0; + for (const s of sources) { + const entries = await getFeedEntries(s.url); + const savingModels = generateFeedEntries(entries, s); + let newCount = await waitAndCountFulfilled(savingModels); + totalNewEntries += newCount; + req.log.info( + `Feed ${s.url}: found ${entries.length} and saved ${newCount} entries` + ); + } + res.json({ totalNewEntries }); + }) +); + +app.listen(port, () => { + console.log(`FEED app listening on port ${port}`); +}); diff --git a/09-express/02-feed-api/get-feeds.mjs b/09-express/02-feed-api/get-feeds.mjs new file mode 100644 index 0000000..3ddd3b3 --- /dev/null +++ b/09-express/02-feed-api/get-feeds.mjs @@ -0,0 +1,25 @@ +import { parseFeed } from "htmlparser2"; +import { compareDesc } from "date-fns"; + +export default async function getFeedEntries(url, maxEntries = 5) { + const feedFetched = await fetch(url); + if (!feedFetched.ok) { + throw new Error("Feed fetch failed"); + } + const feedContent = await feedFetched.text(); + const { items } = parseFeed(feedContent); + return items.slice(0, maxEntries); +} + +export async function getLatestEntries(urls) { + const feedsFetches = urls.map((s) => getFeedEntries(s)); + const fetchesPromises = await Promise.allSettled(feedsFetches); + const retrievedFeeds = fetchesPromises + .filter((r) => r.status === "fulfilled") + .map((r) => r.value); + const latestItems = retrievedFeeds.flat().map((i) => { + return { title: i.title, link: i.link, date: i.pubDate }; + }); + latestItems.sort((a, b) => compareDesc(a.date, b.date)); + return latestItems; +} diff --git a/09-express/02-feed-api/models/FeedEntry.mjs b/09-express/02-feed-api/models/FeedEntry.mjs new file mode 100644 index 0000000..460312e --- /dev/null +++ b/09-express/02-feed-api/models/FeedEntry.mjs @@ -0,0 +1,12 @@ +import mongoose from "mongoose"; +const { Schema } = mongoose; + +const feedEntrySchema = new Schema({ + source: { type: Schema.Types.ObjectId, ref: "Source" }, + title: { type: String }, + link: { type: String, unique: true }, + date: { type: Date }, +}); + +const FeedEntry = mongoose.model("FeedEntry", feedEntrySchema); +export { FeedEntry }; diff --git a/09-express/02-feed-api/models/Source.mjs b/09-express/02-feed-api/models/Source.mjs new file mode 100644 index 0000000..4124a4f --- /dev/null +++ b/09-express/02-feed-api/models/Source.mjs @@ -0,0 +1,9 @@ +import mongoose from "mongoose"; +const { Schema } = mongoose; + +const sourceSchema = new Schema({ + url: { type: String, required: true }, +}); + +const Source = mongoose.model("Source", sourceSchema); +export { Source }; diff --git a/09-express/02-feed-api/models/Source_v1.mjs b/09-express/02-feed-api/models/Source_v1.mjs new file mode 100644 index 0000000..f358052 --- /dev/null +++ b/09-express/02-feed-api/models/Source_v1.mjs @@ -0,0 +1,36 @@ +import mongoose from "mongoose"; +const { Schema } = mongoose; + +const sourceSchema = new Schema({ + url: { + type: String, + required: true, + unique: true, + validate: { + validator: function (v) { + try { + const url = new URL(v); + if (!url.protocol.startsWith("http")) { + return false; + } + } catch { + return false; + } + return true; + }, + message: (props) => `${props.value} is not a valid URL`, + }, + }, +}); +sourceSchema.static("getErrorReason", (e) => { + if (e.name === "ValidationError") { + return "Source validation failed"; + } else if (e.code === 11000) { + return "Feed already exists"; + } else { + return null; + } +}); + +const Source = mongoose.model("Source", sourceSchema); +export { Source }; diff --git a/09-express/02-feed-api/package-lock.json b/09-express/02-feed-api/package-lock.json new file mode 100644 index 0000000..fb60e79 --- /dev/null +++ b/09-express/02-feed-api/package-lock.json @@ -0,0 +1,1298 @@ +{ + "name": "02-feed-api", + "version": "1.0.0", + "lockfileVersion": 3, + "requires": true, + "packages": { + "": { + "name": "02-feed-api", + "version": "1.0.0", + "license": "ISC", + "dependencies": { + "compression": "^1.7.4", + "date-fns": "^3.3.1", + "express": "^4.18.2", + "htmlparser2": "^9.1.0", + "mongoose": "^8.1.3", + "pino-http": "^9.0.0" + } + }, + "node_modules/@mongodb-js/saslprep": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/@mongodb-js/saslprep/-/saslprep-1.1.4.tgz", + "integrity": "sha512-8zJ8N1x51xo9hwPh6AWnKdLGEC5N3lDa6kms1YHmFBoRhTpJR6HG8wWk0td1MVCu9cD4YBrvjZEtd5Obw0Fbnw==", + "dependencies": { + "sparse-bitfield": "^3.0.3" + } + }, + "node_modules/@types/webidl-conversions": { + "version": "7.0.3", + "resolved": "https://registry.npmjs.org/@types/webidl-conversions/-/webidl-conversions-7.0.3.tgz", + "integrity": "sha512-CiJJvcRtIgzadHCYXw7dqEnMNRjhGZlYK05Mj9OyktqV8uVT8fD2BFOB7S1uwBE3Kj2Z+4UyPmFw/Ixgw/LAlA==" + }, + "node_modules/@types/whatwg-url": { + "version": "11.0.4", + "resolved": "https://registry.npmjs.org/@types/whatwg-url/-/whatwg-url-11.0.4.tgz", + "integrity": "sha512-lXCmTWSHJvf0TRSO58nm978b8HJ/EdsSsEKLd3ODHFjo+3VGAyyTp4v50nWvwtzBxSMQrVOK7tcuN0zGPLICMw==", + "dependencies": { + "@types/webidl-conversions": "*" + } + }, + "node_modules/abort-controller": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/abort-controller/-/abort-controller-3.0.0.tgz", + "integrity": "sha512-h8lQ8tacZYnR3vNQTgibj+tODHI5/+l06Au2Pcriv/Gmet0eaj4TwWH41sO9wnHDiQsEj19q0drzdWdeAHtweg==", + "dependencies": { + "event-target-shim": "^5.0.0" + }, + "engines": { + "node": ">=6.5" + } + }, + "node_modules/accepts": { + "version": "1.3.8", + "resolved": "https://registry.npmjs.org/accepts/-/accepts-1.3.8.tgz", + "integrity": "sha512-PYAthTa2m2VKxuvSD3DPC/Gy+U+sOA1LAuT8mkmRuvw+NACSaeXEQ+NHcVF7rONl6qcaxV3Uuemwawk+7+SJLw==", + "dependencies": { + "mime-types": "~2.1.34", + "negotiator": "0.6.3" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/array-flatten": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/array-flatten/-/array-flatten-1.1.1.tgz", + "integrity": "sha512-PCVAQswWemu6UdxsDFFX/+gVeYqKAod3D3UVm91jHwynguOwAvYPhx8nNlM++NqRcK6CxxpUafjmhIdKiHibqg==" + }, + "node_modules/atomic-sleep": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/atomic-sleep/-/atomic-sleep-1.0.0.tgz", + "integrity": "sha512-kNOjDqAh7px0XWNI+4QbzoiR/nTkHAWNud2uvnJquD1/x5a7EQZMJT0AczqK0Qn67oY/TTQ1LbUKajZpp3I9tQ==", + "engines": { + "node": ">=8.0.0" + } + }, + "node_modules/base64-js": { + "version": "1.5.1", + "resolved": "https://registry.npmjs.org/base64-js/-/base64-js-1.5.1.tgz", + "integrity": "sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ] + }, + "node_modules/body-parser": { + "version": "1.20.1", + "resolved": "https://registry.npmjs.org/body-parser/-/body-parser-1.20.1.tgz", + "integrity": "sha512-jWi7abTbYwajOytWCQc37VulmWiRae5RyTpaCyDcS5/lMdtwSz5lOpDE67srw/HYe35f1z3fDQw+3txg7gNtWw==", + "dependencies": { + "bytes": "3.1.2", + "content-type": "~1.0.4", + "debug": "2.6.9", + "depd": "2.0.0", + "destroy": "1.2.0", + "http-errors": "2.0.0", + "iconv-lite": "0.4.24", + "on-finished": "2.4.1", + "qs": "6.11.0", + "raw-body": "2.5.1", + "type-is": "~1.6.18", + "unpipe": "1.0.0" + }, + "engines": { + "node": ">= 0.8", + "npm": "1.2.8000 || >= 1.4.16" + } + }, + "node_modules/bson": { + "version": "6.3.0", + "resolved": "https://registry.npmjs.org/bson/-/bson-6.3.0.tgz", + "integrity": "sha512-balJfqwwTBddxfnidJZagCBPP/f48zj9Sdp3OJswREOgsJzHiQSaOIAtApSgDQFYgHqAvFkp53AFSqjMDZoTFw==", + "engines": { + "node": ">=16.20.1" + } + }, + "node_modules/buffer": { + "version": "6.0.3", + "resolved": "https://registry.npmjs.org/buffer/-/buffer-6.0.3.tgz", + "integrity": "sha512-FTiCpNxtwiZZHEZbcbTIcZjERVICn9yq/pDFkTl95/AxzD1naBctN7YO68riM/gLSDY7sdrMby8hofADYuuqOA==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "dependencies": { + "base64-js": "^1.3.1", + "ieee754": "^1.2.1" + } + }, + "node_modules/bytes": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/bytes/-/bytes-3.1.2.tgz", + "integrity": "sha512-/Nf7TyzTx6S3yRJObOAV7956r8cr2+Oj8AC5dt8wSP3BQAoeX58NoHyCU8P8zGkNXStjTSi6fzO6F0pBdcYbEg==", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/call-bind": { + "version": "1.0.7", + "resolved": "https://registry.npmjs.org/call-bind/-/call-bind-1.0.7.tgz", + "integrity": "sha512-GHTSNSYICQ7scH7sZ+M2rFopRoLh8t2bLSW6BbgrtLsahOIB5iyAVJf9GjWK3cYTDaMj4XdBpM1cA6pIS0Kv2w==", + "dependencies": { + "es-define-property": "^1.0.0", + "es-errors": "^1.3.0", + "function-bind": "^1.1.2", + "get-intrinsic": "^1.2.4", + "set-function-length": "^1.2.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/compressible": { + "version": "2.0.18", + "resolved": "https://registry.npmjs.org/compressible/-/compressible-2.0.18.tgz", + "integrity": "sha512-AF3r7P5dWxL8MxyITRMlORQNaOA2IkAFaTr4k7BUumjPtRpGDTZpl0Pb1XCO6JeDCBdp126Cgs9sMxqSjgYyRg==", + "dependencies": { + "mime-db": ">= 1.43.0 < 2" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/compression": { + "version": "1.7.4", + "resolved": "https://registry.npmjs.org/compression/-/compression-1.7.4.tgz", + "integrity": "sha512-jaSIDzP9pZVS4ZfQ+TzvtiWhdpFhE2RDHz8QJkpX9SIpLq88VueF5jJw6t+6CUQcAoA6t+x89MLrWAqpfDE8iQ==", + "dependencies": { + "accepts": "~1.3.5", + "bytes": "3.0.0", + "compressible": "~2.0.16", + "debug": "2.6.9", + "on-headers": "~1.0.2", + "safe-buffer": "5.1.2", + "vary": "~1.1.2" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/compression/node_modules/bytes": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/bytes/-/bytes-3.0.0.tgz", + "integrity": "sha512-pMhOfFDPiv9t5jjIXkHosWmkSyQbvsgEVNkz0ERHbuLh2T/7j4Mqqpz523Fe8MVY89KC6Sh/QfS2sM+SjgFDcw==", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/compression/node_modules/safe-buffer": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.2.tgz", + "integrity": "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==" + }, + "node_modules/content-disposition": { + "version": "0.5.4", + "resolved": "https://registry.npmjs.org/content-disposition/-/content-disposition-0.5.4.tgz", + "integrity": "sha512-FveZTNuGw04cxlAiWbzi6zTAL/lhehaWbTtgluJh4/E95DqMwTmha3KZN1aAWA8cFIhHzMZUvLevkw5Rqk+tSQ==", + "dependencies": { + "safe-buffer": "5.2.1" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/content-type": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/content-type/-/content-type-1.0.5.tgz", + "integrity": "sha512-nTjqfcBFEipKdXCv4YDQWCfmcLZKm81ldF0pAopTvyrFGVbcR6P/VAAd5G7N+0tTr8QqiU0tFadD6FK4NtJwOA==", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/cookie": { + "version": "0.5.0", + "resolved": "https://registry.npmjs.org/cookie/-/cookie-0.5.0.tgz", + "integrity": "sha512-YZ3GUyn/o8gfKJlnlX7g7xq4gyO6OSuhGPKaaGssGB2qgDUS0gPgtTvoyZLTt9Ab6dC4hfc9dV5arkvc/OCmrw==", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/cookie-signature": { + "version": "1.0.6", + "resolved": "https://registry.npmjs.org/cookie-signature/-/cookie-signature-1.0.6.tgz", + "integrity": "sha512-QADzlaHc8icV8I7vbaJXJwod9HWYp8uCqf1xa4OfNu1T7JVxQIrUgOWtHdNDtPiywmFbiS12VjotIXLrKM3orQ==" + }, + "node_modules/date-fns": { + "version": "3.3.1", + "resolved": "https://registry.npmjs.org/date-fns/-/date-fns-3.3.1.tgz", + "integrity": "sha512-y8e109LYGgoQDveiEBD3DYXKba1jWf5BA8YU1FL5Tvm0BTdEfy54WLCwnuYWZNnzzvALy/QQ4Hov+Q9RVRv+Zw==", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/kossnocorp" + } + }, + "node_modules/debug": { + "version": "2.6.9", + "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", + "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==", + "dependencies": { + "ms": "2.0.0" + } + }, + "node_modules/define-data-property": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/define-data-property/-/define-data-property-1.1.4.tgz", + "integrity": "sha512-rBMvIzlpA8v6E+SJZoo++HAYqsLrkg7MSfIinMPFhmkorw7X+dOXVJQs+QT69zGkzMyfDnIMN2Wid1+NbL3T+A==", + "dependencies": { + "es-define-property": "^1.0.0", + "es-errors": "^1.3.0", + "gopd": "^1.0.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/depd": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/depd/-/depd-2.0.0.tgz", + "integrity": "sha512-g7nH6P6dyDioJogAAGprGpCtVImJhpPk/roCzdb3fIh61/s/nPsfR6onyMwkCAR/OlC3yBC0lESvUoQEAssIrw==", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/destroy": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/destroy/-/destroy-1.2.0.tgz", + "integrity": "sha512-2sJGJTaXIIaR1w4iJSNoN0hnMY7Gpc/n8D4qSCJw8QqFWXf7cuAgnEHxBpweaVcPevC2l3KpjYCx3NypQQgaJg==", + "engines": { + "node": ">= 0.8", + "npm": "1.2.8000 || >= 1.4.16" + } + }, + "node_modules/dom-serializer": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/dom-serializer/-/dom-serializer-2.0.0.tgz", + "integrity": "sha512-wIkAryiqt/nV5EQKqQpo3SToSOV9J0DnbJqwK7Wv/Trc92zIAYZ4FlMu+JPFW1DfGFt81ZTCGgDEabffXeLyJg==", + "dependencies": { + "domelementtype": "^2.3.0", + "domhandler": "^5.0.2", + "entities": "^4.2.0" + }, + "funding": { + "url": "https://github.com/cheeriojs/dom-serializer?sponsor=1" + } + }, + "node_modules/domelementtype": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/domelementtype/-/domelementtype-2.3.0.tgz", + "integrity": "sha512-OLETBj6w0OsagBwdXnPdN0cnMfF9opN69co+7ZrbfPGrdpPVNBUj02spi6B1N7wChLQiPn4CSH/zJvXw56gmHw==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/fb55" + } + ] + }, + "node_modules/domhandler": { + "version": "5.0.3", + "resolved": "https://registry.npmjs.org/domhandler/-/domhandler-5.0.3.tgz", + "integrity": "sha512-cgwlv/1iFQiFnU96XXgROh8xTeetsnJiDsTc7TYCLFd9+/WNkIqPTxiM/8pSd8VIrhXGTf1Ny1q1hquVqDJB5w==", + "dependencies": { + "domelementtype": "^2.3.0" + }, + "engines": { + "node": ">= 4" + }, + "funding": { + "url": "https://github.com/fb55/domhandler?sponsor=1" + } + }, + "node_modules/domutils": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/domutils/-/domutils-3.1.0.tgz", + "integrity": "sha512-H78uMmQtI2AhgDJjWeQmHwJJ2bLPD3GMmO7Zja/ZZh84wkm+4ut+IUnUdRa8uCGX88DiVx1j6FRe1XfxEgjEZA==", + "dependencies": { + "dom-serializer": "^2.0.0", + "domelementtype": "^2.3.0", + "domhandler": "^5.0.3" + }, + "funding": { + "url": "https://github.com/fb55/domutils?sponsor=1" + } + }, + "node_modules/ee-first": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/ee-first/-/ee-first-1.1.1.tgz", + "integrity": "sha512-WMwm9LhRUo+WUaRN+vRuETqG89IgZphVSNkdFgeb6sS/E4OrDIN7t48CAewSHXc6C8lefD8KKfr5vY61brQlow==" + }, + "node_modules/encodeurl": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/encodeurl/-/encodeurl-1.0.2.tgz", + "integrity": "sha512-TPJXq8JqFaVYm2CWmPvnP2Iyo4ZSM7/QKcSmuMLDObfpH5fi7RUGmd/rTDf+rut/saiDiQEeVTNgAmJEdAOx0w==", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/entities": { + "version": "4.5.0", + "resolved": "https://registry.npmjs.org/entities/-/entities-4.5.0.tgz", + "integrity": "sha512-V0hjH4dGPh9Ao5p0MoRY6BVqtwCjhz6vI5LT8AJ55H+4g9/4vbHx1I54fS0XuclLhDHArPQCiMjDxjaL8fPxhw==", + "engines": { + "node": ">=0.12" + }, + "funding": { + "url": "https://github.com/fb55/entities?sponsor=1" + } + }, + "node_modules/es-define-property": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/es-define-property/-/es-define-property-1.0.0.tgz", + "integrity": "sha512-jxayLKShrEqqzJ0eumQbVhTYQM27CfT1T35+gCgDFoL82JLsXqTJ76zv6A0YLOgEnLUMvLzsDsGIrl8NFpT2gQ==", + "dependencies": { + "get-intrinsic": "^1.2.4" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/es-errors": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/es-errors/-/es-errors-1.3.0.tgz", + "integrity": "sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw==", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/escape-html": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/escape-html/-/escape-html-1.0.3.tgz", + "integrity": "sha512-NiSupZ4OeuGwr68lGIeym/ksIZMJodUGOSCZ/FSnTxcrekbvqrgdUxlJOMpijaKZVjAJrWrGs/6Jy8OMuyj9ow==" + }, + "node_modules/etag": { + "version": "1.8.1", + "resolved": "https://registry.npmjs.org/etag/-/etag-1.8.1.tgz", + "integrity": "sha512-aIL5Fx7mawVa300al2BnEE4iNvo1qETxLrPI/o05L7z6go7fCw1J6EQmbK4FmJ2AS7kgVF/KEZWufBfdClMcPg==", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/event-target-shim": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/event-target-shim/-/event-target-shim-5.0.1.tgz", + "integrity": "sha512-i/2XbnSz/uxRCU6+NdVJgKWDTM427+MqYbkQzD321DuCQJUqOuJKIA0IM2+W2xtYHdKOmZ4dR6fExsd4SXL+WQ==", + "engines": { + "node": ">=6" + } + }, + "node_modules/events": { + "version": "3.3.0", + "resolved": "https://registry.npmjs.org/events/-/events-3.3.0.tgz", + "integrity": "sha512-mQw+2fkQbALzQ7V0MY0IqdnXNOeTtP4r0lN9z7AAawCXgqea7bDii20AYrIBrFd/Hx0M2Ocz6S111CaFkUcb0Q==", + "engines": { + "node": ">=0.8.x" + } + }, + "node_modules/express": { + "version": "4.18.2", + "resolved": "https://registry.npmjs.org/express/-/express-4.18.2.tgz", + "integrity": "sha512-5/PsL6iGPdfQ/lKM1UuielYgv3BUoJfz1aUwU9vHZ+J7gyvwdQXFEBIEIaxeGf0GIcreATNyBExtalisDbuMqQ==", + "dependencies": { + "accepts": "~1.3.8", + "array-flatten": "1.1.1", + "body-parser": "1.20.1", + "content-disposition": "0.5.4", + "content-type": "~1.0.4", + "cookie": "0.5.0", + "cookie-signature": "1.0.6", + "debug": "2.6.9", + "depd": "2.0.0", + "encodeurl": "~1.0.2", + "escape-html": "~1.0.3", + "etag": "~1.8.1", + "finalhandler": "1.2.0", + "fresh": "0.5.2", + "http-errors": "2.0.0", + "merge-descriptors": "1.0.1", + "methods": "~1.1.2", + "on-finished": "2.4.1", + "parseurl": "~1.3.3", + "path-to-regexp": "0.1.7", + "proxy-addr": "~2.0.7", + "qs": "6.11.0", + "range-parser": "~1.2.1", + "safe-buffer": "5.2.1", + "send": "0.18.0", + "serve-static": "1.15.0", + "setprototypeof": "1.2.0", + "statuses": "2.0.1", + "type-is": "~1.6.18", + "utils-merge": "1.0.1", + "vary": "~1.1.2" + }, + "engines": { + "node": ">= 0.10.0" + } + }, + "node_modules/fast-redact": { + "version": "3.3.0", + "resolved": "https://registry.npmjs.org/fast-redact/-/fast-redact-3.3.0.tgz", + "integrity": "sha512-6T5V1QK1u4oF+ATxs1lWUmlEk6P2T9HqJG3e2DnHOdVgZy2rFJBoEnrIedcTXlkAHU/zKC+7KETJ+KGGKwxgMQ==", + "engines": { + "node": ">=6" + } + }, + "node_modules/finalhandler": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/finalhandler/-/finalhandler-1.2.0.tgz", + "integrity": "sha512-5uXcUVftlQMFnWC9qu/svkWv3GTd2PfUhK/3PLkYNAe7FbqJMt3515HaxE6eRL74GdsriiwujiawdaB1BpEISg==", + "dependencies": { + "debug": "2.6.9", + "encodeurl": "~1.0.2", + "escape-html": "~1.0.3", + "on-finished": "2.4.1", + "parseurl": "~1.3.3", + "statuses": "2.0.1", + "unpipe": "~1.0.0" + }, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/forwarded": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/forwarded/-/forwarded-0.2.0.tgz", + "integrity": "sha512-buRG0fpBtRHSTCOASe6hD258tEubFoRLb4ZNA6NxMVHNw2gOcwHo9wyablzMzOA5z9xA9L1KNjk/Nt6MT9aYow==", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/fresh": { + "version": "0.5.2", + "resolved": "https://registry.npmjs.org/fresh/-/fresh-0.5.2.tgz", + "integrity": "sha512-zJ2mQYM18rEFOudeV4GShTGIQ7RbzA7ozbU9I/XBpm7kqgMywgmylMwXHxZJmkVoYkna9d2pVXVXPdYTP9ej8Q==", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/function-bind": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.2.tgz", + "integrity": "sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==", + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/get-caller-file": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/get-caller-file/-/get-caller-file-2.0.5.tgz", + "integrity": "sha512-DyFP3BM/3YHTQOCUL/w0OZHR0lpKeGrxotcHWcqNEdnltqFwXVfhEBQ94eIo34AfQpo0rGki4cyIiftY06h2Fg==", + "engines": { + "node": "6.* || 8.* || >= 10.*" + } + }, + "node_modules/get-intrinsic": { + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.2.4.tgz", + "integrity": "sha512-5uYhsJH8VJBTv7oslg4BznJYhDoRI6waYCxMmCdnTrcCrHA/fCFKoTFz2JKKE0HdDFUF7/oQuhzumXJK7paBRQ==", + "dependencies": { + "es-errors": "^1.3.0", + "function-bind": "^1.1.2", + "has-proto": "^1.0.1", + "has-symbols": "^1.0.3", + "hasown": "^2.0.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/gopd": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/gopd/-/gopd-1.0.1.tgz", + "integrity": "sha512-d65bNlIadxvpb/A2abVdlqKqV563juRnZ1Wtk6s1sIR8uNsXR70xqIzVqxVf1eTqDunwT2MkczEeaezCKTZhwA==", + "dependencies": { + "get-intrinsic": "^1.1.3" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/has-property-descriptors": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/has-property-descriptors/-/has-property-descriptors-1.0.2.tgz", + "integrity": "sha512-55JNKuIW+vq4Ke1BjOTjM2YctQIvCT7GFzHwmfZPGo5wnrgkid0YQtnAleFSqumZm4az3n2BS+erby5ipJdgrg==", + "dependencies": { + "es-define-property": "^1.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/has-proto": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/has-proto/-/has-proto-1.0.3.tgz", + "integrity": "sha512-SJ1amZAJUiZS+PhsVLf5tGydlaVB8EdFpaSO4gmiUKUOxk8qzn5AIy4ZeJUmh22znIdk/uMAUT2pl3FxzVUH+Q==", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/has-symbols": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.0.3.tgz", + "integrity": "sha512-l3LCuF6MgDNwTDKkdYGEihYjt5pRPbEg46rtlmnSPlUbgmB8LOIrKJbYYFBSbnPaJexMKtiPO8hmeRjRz2Td+A==", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/hasown": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.1.tgz", + "integrity": "sha512-1/th4MHjnwncwXsIW6QMzlvYL9kG5e/CpVvLRZe4XPa8TOUNbCELqmvhDmnkNsAjwaG4+I8gJJL0JBvTTLO9qA==", + "dependencies": { + "function-bind": "^1.1.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/htmlparser2": { + "version": "9.1.0", + "resolved": "https://registry.npmjs.org/htmlparser2/-/htmlparser2-9.1.0.tgz", + "integrity": "sha512-5zfg6mHUoaer/97TxnGpxmbR7zJtPwIYFMZ/H5ucTlPZhKvtum05yiPK3Mgai3a0DyVxv7qYqoweaEd2nrYQzQ==", + "funding": [ + "https://github.com/fb55/htmlparser2?sponsor=1", + { + "type": "github", + "url": "https://github.com/sponsors/fb55" + } + ], + "dependencies": { + "domelementtype": "^2.3.0", + "domhandler": "^5.0.3", + "domutils": "^3.1.0", + "entities": "^4.5.0" + } + }, + "node_modules/http-errors": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/http-errors/-/http-errors-2.0.0.tgz", + "integrity": "sha512-FtwrG/euBzaEjYeRqOgly7G0qviiXoJWnvEH2Z1plBdXgbyjv34pHTSb9zoeHMyDy33+DWy5Wt9Wo+TURtOYSQ==", + "dependencies": { + "depd": "2.0.0", + "inherits": "2.0.4", + "setprototypeof": "1.2.0", + "statuses": "2.0.1", + "toidentifier": "1.0.1" + }, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/iconv-lite": { + "version": "0.4.24", + "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.4.24.tgz", + "integrity": "sha512-v3MXnZAcvnywkTUEZomIActle7RXXeedOR31wwl7VlyoXO4Qi9arvSenNQWne1TcRwhCL1HwLI21bEqdpj8/rA==", + "dependencies": { + "safer-buffer": ">= 2.1.2 < 3" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/ieee754": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/ieee754/-/ieee754-1.2.1.tgz", + "integrity": "sha512-dcyqhDvX1C46lXZcVqCpK+FtMRQVdIMN6/Df5js2zouUsqG7I6sFxitIC+7KYK29KdXOLHdu9zL4sFnoVQnqaA==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ] + }, + "node_modules/inherits": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz", + "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==" + }, + "node_modules/ipaddr.js": { + "version": "1.9.1", + "resolved": "https://registry.npmjs.org/ipaddr.js/-/ipaddr.js-1.9.1.tgz", + "integrity": "sha512-0KI/607xoxSToH7GjN1FfSbLoU0+btTicjsQSWQlh/hZykN8KpmMf7uYwPW3R+akZ6R/w18ZlXSHBYXiYUPO3g==", + "engines": { + "node": ">= 0.10" + } + }, + "node_modules/kareem": { + "version": "2.5.1", + "resolved": "https://registry.npmjs.org/kareem/-/kareem-2.5.1.tgz", + "integrity": "sha512-7jFxRVm+jD+rkq3kY0iZDJfsO2/t4BBPeEb2qKn2lR/9KhuksYk5hxzfRYWMPV8P/x2d0kHD306YyWLzjjH+uA==", + "engines": { + "node": ">=12.0.0" + } + }, + "node_modules/media-typer": { + "version": "0.3.0", + "resolved": "https://registry.npmjs.org/media-typer/-/media-typer-0.3.0.tgz", + "integrity": "sha512-dq+qelQ9akHpcOl/gUVRTxVIOkAJ1wR3QAvb4RsVjS8oVoFjDGTc679wJYmUmknUF5HwMLOgb5O+a3KxfWapPQ==", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/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==" + }, + "node_modules/merge-descriptors": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/merge-descriptors/-/merge-descriptors-1.0.1.tgz", + "integrity": "sha512-cCi6g3/Zr1iqQi6ySbseM1Xvooa98N0w31jzUYrXPX2xqObmFGHJ0tQ5u74H3mVh7wLouTseZyYIq39g8cNp1w==" + }, + "node_modules/methods": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/methods/-/methods-1.1.2.tgz", + "integrity": "sha512-iclAHeNqNm68zFtnZ0e+1L2yUIdvzNoauKU4WBA3VvH/vPFieF7qfRlwUZU+DA9P9bPXIS90ulxoUoCH23sV2w==", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/mime": { + "version": "1.6.0", + "resolved": "https://registry.npmjs.org/mime/-/mime-1.6.0.tgz", + "integrity": "sha512-x0Vn8spI+wuJ1O6S7gnbaQg8Pxh4NNHb7KSINmEWKiPE4RKOplvijn+NkmYmmRgP68mc70j2EbeTFRsrswaQeg==", + "bin": { + "mime": "cli.js" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/mime-db": { + "version": "1.52.0", + "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.52.0.tgz", + "integrity": "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/mime-types": { + "version": "2.1.35", + "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.35.tgz", + "integrity": "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==", + "dependencies": { + "mime-db": "1.52.0" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/mongodb": { + "version": "6.3.0", + "resolved": "https://registry.npmjs.org/mongodb/-/mongodb-6.3.0.tgz", + "integrity": "sha512-tt0KuGjGtLUhLoU263+xvQmPHEGTw5LbcNC73EoFRYgSHwZt5tsoJC110hDyO1kjQzpgNrpdcSza9PknWN4LrA==", + "dependencies": { + "@mongodb-js/saslprep": "^1.1.0", + "bson": "^6.2.0", + "mongodb-connection-string-url": "^3.0.0" + }, + "engines": { + "node": ">=16.20.1" + }, + "peerDependencies": { + "@aws-sdk/credential-providers": "^3.188.0", + "@mongodb-js/zstd": "^1.1.0", + "gcp-metadata": "^5.2.0", + "kerberos": "^2.0.1", + "mongodb-client-encryption": ">=6.0.0 <7", + "snappy": "^7.2.2", + "socks": "^2.7.1" + }, + "peerDependenciesMeta": { + "@aws-sdk/credential-providers": { + "optional": true + }, + "@mongodb-js/zstd": { + "optional": true + }, + "gcp-metadata": { + "optional": true + }, + "kerberos": { + "optional": true + }, + "mongodb-client-encryption": { + "optional": true + }, + "snappy": { + "optional": true + }, + "socks": { + "optional": true + } + } + }, + "node_modules/mongodb-connection-string-url": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/mongodb-connection-string-url/-/mongodb-connection-string-url-3.0.0.tgz", + "integrity": "sha512-t1Vf+m1I5hC2M5RJx/7AtxgABy1cZmIPQRMXw+gEIPn/cZNF3Oiy+l0UIypUwVB5trcWHq3crg2g3uAR9aAwsQ==", + "dependencies": { + "@types/whatwg-url": "^11.0.2", + "whatwg-url": "^13.0.0" + } + }, + "node_modules/mongoose": { + "version": "8.1.3", + "resolved": "https://registry.npmjs.org/mongoose/-/mongoose-8.1.3.tgz", + "integrity": "sha512-a5MajZSDJiQgy0iQcR+MIpFe7zehGJI4doJ6Dh1MvnGh8/HNNhr5pn07RPA86KCTjP2vuKdffpFmvXxcHiUOjw==", + "dependencies": { + "bson": "^6.2.0", + "kareem": "2.5.1", + "mongodb": "6.3.0", + "mpath": "0.9.0", + "mquery": "5.0.0", + "ms": "2.1.3", + "sift": "16.0.1" + }, + "engines": { + "node": ">=16.20.1" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/mongoose" + } + }, + "node_modules/mongoose/node_modules/ms": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", + "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==" + }, + "node_modules/mpath": { + "version": "0.9.0", + "resolved": "https://registry.npmjs.org/mpath/-/mpath-0.9.0.tgz", + "integrity": "sha512-ikJRQTk8hw5DEoFVxHG1Gn9T/xcjtdnOKIU1JTmGjZZlg9LST2mBLmcX3/ICIbgJydT2GOc15RnNy5mHmzfSew==", + "engines": { + "node": ">=4.0.0" + } + }, + "node_modules/mquery": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/mquery/-/mquery-5.0.0.tgz", + "integrity": "sha512-iQMncpmEK8R8ncT8HJGsGc9Dsp8xcgYMVSbs5jgnm1lFHTZqMJTUWTDx1LBO8+mK3tPNZWFLBghQEIOULSTHZg==", + "dependencies": { + "debug": "4.x" + }, + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/mquery/node_modules/debug": { + "version": "4.3.4", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.4.tgz", + "integrity": "sha512-PRWFHuSU3eDtQJPvnNY7Jcket1j0t5OuOsFzPPzsekD52Zl8qUfFIPEiswXqIvHWGVHOgX+7G/vCNNhehwxfkQ==", + "dependencies": { + "ms": "2.1.2" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "node_modules/mquery/node_modules/ms": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz", + "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==" + }, + "node_modules/ms": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", + "integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==" + }, + "node_modules/negotiator": { + "version": "0.6.3", + "resolved": "https://registry.npmjs.org/negotiator/-/negotiator-0.6.3.tgz", + "integrity": "sha512-+EUsqGPLsM+j/zdChZjsnX51g4XrHFOIXwfnCVPGlQk/k5giakcKsuxCObBRu6DSm9opw/O6slWbJdghQM4bBg==", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/object-inspect": { + "version": "1.13.1", + "resolved": "https://registry.npmjs.org/object-inspect/-/object-inspect-1.13.1.tgz", + "integrity": "sha512-5qoj1RUiKOMsCCNLV1CBiPYE10sziTsnmNxkAI/rZhiD63CF7IqdFGC/XzjWjpSgLf0LxXX3bDFIh0E18f6UhQ==", + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/on-exit-leak-free": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/on-exit-leak-free/-/on-exit-leak-free-2.1.2.tgz", + "integrity": "sha512-0eJJY6hXLGf1udHwfNftBqH+g73EU4B504nZeKpz1sYRKafAghwxEJunB2O7rDZkL4PGfsMVnTXZ2EjibbqcsA==", + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/on-finished": { + "version": "2.4.1", + "resolved": "https://registry.npmjs.org/on-finished/-/on-finished-2.4.1.tgz", + "integrity": "sha512-oVlzkg3ENAhCk2zdv7IJwd/QUD4z2RxRwpkcGY8psCVcCYZNq4wYnVWALHM+brtuJjePWiYF/ClmuDr8Ch5+kg==", + "dependencies": { + "ee-first": "1.1.1" + }, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/on-headers": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/on-headers/-/on-headers-1.0.2.tgz", + "integrity": "sha512-pZAE+FJLoyITytdqK0U5s+FIpjN0JP3OzFi/u8Rx+EV5/W+JTWGXG8xFzevE7AjBfDqHv/8vL8qQsIhHnqRkrA==", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/parseurl": { + "version": "1.3.3", + "resolved": "https://registry.npmjs.org/parseurl/-/parseurl-1.3.3.tgz", + "integrity": "sha512-CiyeOxFT/JZyN5m0z9PfXw4SCBJ6Sygz1Dpl0wqjlhDEGGBP1GnsUVEL0p63hoG1fcj3fHynXi9NYO4nWOL+qQ==", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/path-to-regexp": { + "version": "0.1.7", + "resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-0.1.7.tgz", + "integrity": "sha512-5DFkuoqlv1uYQKxy8omFBeJPQcdoE07Kv2sferDCrAq1ohOU+MSDswDIbnx3YAM60qIOnYa53wBhXW0EbMonrQ==" + }, + "node_modules/pino": { + "version": "8.19.0", + "resolved": "https://registry.npmjs.org/pino/-/pino-8.19.0.tgz", + "integrity": "sha512-oswmokxkav9bADfJ2ifrvfHUwad6MLp73Uat0IkQWY3iAw5xTRoznXbXksZs8oaOUMpmhVWD+PZogNzllWpJaA==", + "dependencies": { + "atomic-sleep": "^1.0.0", + "fast-redact": "^3.1.1", + "on-exit-leak-free": "^2.1.0", + "pino-abstract-transport": "v1.1.0", + "pino-std-serializers": "^6.0.0", + "process-warning": "^3.0.0", + "quick-format-unescaped": "^4.0.3", + "real-require": "^0.2.0", + "safe-stable-stringify": "^2.3.1", + "sonic-boom": "^3.7.0", + "thread-stream": "^2.0.0" + }, + "bin": { + "pino": "bin.js" + } + }, + "node_modules/pino-abstract-transport": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/pino-abstract-transport/-/pino-abstract-transport-1.1.0.tgz", + "integrity": "sha512-lsleG3/2a/JIWUtf9Q5gUNErBqwIu1tUKTT3dUzaf5DySw9ra1wcqKjJjLX1VTY64Wk1eEOYsVGSaGfCK85ekA==", + "dependencies": { + "readable-stream": "^4.0.0", + "split2": "^4.0.0" + } + }, + "node_modules/pino-http": { + "version": "9.0.0", + "resolved": "https://registry.npmjs.org/pino-http/-/pino-http-9.0.0.tgz", + "integrity": "sha512-Q9QDNEz0vQmbJtMFjOVr2c9yL92vHudjmr3s3m6J1hbw3DBGFZJm3TIj9TWyynZ4GEsEA9SOtni4heRUr6lNOg==", + "dependencies": { + "get-caller-file": "^2.0.5", + "pino": "^8.17.1", + "pino-std-serializers": "^6.2.2", + "process-warning": "^3.0.0" + } + }, + "node_modules/pino-std-serializers": { + "version": "6.2.2", + "resolved": "https://registry.npmjs.org/pino-std-serializers/-/pino-std-serializers-6.2.2.tgz", + "integrity": "sha512-cHjPPsE+vhj/tnhCy/wiMh3M3z3h/j15zHQX+S9GkTBgqJuTuJzYJ4gUyACLhDaJ7kk9ba9iRDmbH2tJU03OiA==" + }, + "node_modules/process": { + "version": "0.11.10", + "resolved": "https://registry.npmjs.org/process/-/process-0.11.10.tgz", + "integrity": "sha512-cdGef/drWFoydD1JsMzuFf8100nZl+GT+yacc2bEced5f9Rjk4z+WtFUTBu9PhOi9j/jfmBPu0mMEY4wIdAF8A==", + "engines": { + "node": ">= 0.6.0" + } + }, + "node_modules/process-warning": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/process-warning/-/process-warning-3.0.0.tgz", + "integrity": "sha512-mqn0kFRl0EoqhnL0GQ0veqFHyIN1yig9RHh/InzORTUiZHFRAur+aMtRkELNwGs9aNwKS6tg/An4NYBPGwvtzQ==" + }, + "node_modules/proxy-addr": { + "version": "2.0.7", + "resolved": "https://registry.npmjs.org/proxy-addr/-/proxy-addr-2.0.7.tgz", + "integrity": "sha512-llQsMLSUDUPT44jdrU/O37qlnifitDP+ZwrmmZcoSKyLKvtZxpyV0n2/bD/N4tBAAZ/gJEdZU7KMraoK1+XYAg==", + "dependencies": { + "forwarded": "0.2.0", + "ipaddr.js": "1.9.1" + }, + "engines": { + "node": ">= 0.10" + } + }, + "node_modules/punycode": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/punycode/-/punycode-2.3.1.tgz", + "integrity": "sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg==", + "engines": { + "node": ">=6" + } + }, + "node_modules/qs": { + "version": "6.11.0", + "resolved": "https://registry.npmjs.org/qs/-/qs-6.11.0.tgz", + "integrity": "sha512-MvjoMCJwEarSbUYk5O+nmoSzSutSsTwF85zcHPQ9OrlFoZOYIjaqBAJIqIXjptyD5vThxGq52Xu/MaJzRkIk4Q==", + "dependencies": { + "side-channel": "^1.0.4" + }, + "engines": { + "node": ">=0.6" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/quick-format-unescaped": { + "version": "4.0.4", + "resolved": "https://registry.npmjs.org/quick-format-unescaped/-/quick-format-unescaped-4.0.4.tgz", + "integrity": "sha512-tYC1Q1hgyRuHgloV/YXs2w15unPVh8qfu/qCTfhTYamaw7fyhumKa2yGpdSo87vY32rIclj+4fWYQXUMs9EHvg==" + }, + "node_modules/range-parser": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/range-parser/-/range-parser-1.2.1.tgz", + "integrity": "sha512-Hrgsx+orqoygnmhFbKaHE6c296J+HTAQXoxEF6gNupROmmGJRoyzfG3ccAveqCBrwr/2yxQ5BVd/GTl5agOwSg==", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/raw-body": { + "version": "2.5.1", + "resolved": "https://registry.npmjs.org/raw-body/-/raw-body-2.5.1.tgz", + "integrity": "sha512-qqJBtEyVgS0ZmPGdCFPWJ3FreoqvG4MVQln/kCgF7Olq95IbOp0/BWyMwbdtn4VTvkM8Y7khCQ2Xgk/tcrCXig==", + "dependencies": { + "bytes": "3.1.2", + "http-errors": "2.0.0", + "iconv-lite": "0.4.24", + "unpipe": "1.0.0" + }, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/readable-stream": { + "version": "4.5.2", + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-4.5.2.tgz", + "integrity": "sha512-yjavECdqeZ3GLXNgRXgeQEdz9fvDDkNKyHnbHRFtOr7/LcfgBcmct7t/ET+HaCTqfh06OzoAxrkN/IfjJBVe+g==", + "dependencies": { + "abort-controller": "^3.0.0", + "buffer": "^6.0.3", + "events": "^3.3.0", + "process": "^0.11.10", + "string_decoder": "^1.3.0" + }, + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + } + }, + "node_modules/real-require": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/real-require/-/real-require-0.2.0.tgz", + "integrity": "sha512-57frrGM/OCTLqLOAh0mhVA9VBMHd+9U7Zb2THMGdBUoZVOtGbJzjxsYGDJ3A9AYYCP4hn6y1TVbaOfzWtm5GFg==", + "engines": { + "node": ">= 12.13.0" + } + }, + "node_modules/safe-buffer": { + "version": "5.2.1", + "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.1.tgz", + "integrity": "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ] + }, + "node_modules/safe-stable-stringify": { + "version": "2.4.3", + "resolved": "https://registry.npmjs.org/safe-stable-stringify/-/safe-stable-stringify-2.4.3.tgz", + "integrity": "sha512-e2bDA2WJT0wxseVd4lsDP4+3ONX6HpMXQa1ZhFQ7SU+GjvORCmShbCMltrtIDfkYhVHrOcPtj+KhmDBdPdZD1g==", + "engines": { + "node": ">=10" + } + }, + "node_modules/safer-buffer": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/safer-buffer/-/safer-buffer-2.1.2.tgz", + "integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==" + }, + "node_modules/send": { + "version": "0.18.0", + "resolved": "https://registry.npmjs.org/send/-/send-0.18.0.tgz", + "integrity": "sha512-qqWzuOjSFOuqPjFe4NOsMLafToQQwBSOEpS+FwEt3A2V3vKubTquT3vmLTQpFgMXp8AlFWFuP1qKaJZOtPpVXg==", + "dependencies": { + "debug": "2.6.9", + "depd": "2.0.0", + "destroy": "1.2.0", + "encodeurl": "~1.0.2", + "escape-html": "~1.0.3", + "etag": "~1.8.1", + "fresh": "0.5.2", + "http-errors": "2.0.0", + "mime": "1.6.0", + "ms": "2.1.3", + "on-finished": "2.4.1", + "range-parser": "~1.2.1", + "statuses": "2.0.1" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/send/node_modules/ms": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", + "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==" + }, + "node_modules/serve-static": { + "version": "1.15.0", + "resolved": "https://registry.npmjs.org/serve-static/-/serve-static-1.15.0.tgz", + "integrity": "sha512-XGuRDNjXUijsUL0vl6nSD7cwURuzEgglbOaFuZM9g3kwDXOWVTck0jLzjPzGD+TazWbboZYu52/9/XPdUgne9g==", + "dependencies": { + "encodeurl": "~1.0.2", + "escape-html": "~1.0.3", + "parseurl": "~1.3.3", + "send": "0.18.0" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/set-function-length": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/set-function-length/-/set-function-length-1.2.1.tgz", + "integrity": "sha512-j4t6ccc+VsKwYHso+kElc5neZpjtq9EnRICFZtWyBsLojhmeF/ZBd/elqm22WJh/BziDe/SBiOeAt0m2mfLD0g==", + "dependencies": { + "define-data-property": "^1.1.2", + "es-errors": "^1.3.0", + "function-bind": "^1.1.2", + "get-intrinsic": "^1.2.3", + "gopd": "^1.0.1", + "has-property-descriptors": "^1.0.1" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/setprototypeof": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/setprototypeof/-/setprototypeof-1.2.0.tgz", + "integrity": "sha512-E5LDX7Wrp85Kil5bhZv46j8jOeboKq5JMmYM3gVGdGH8xFpPWXUMsNrlODCrkoxMEeNi/XZIwuRvY4XNwYMJpw==" + }, + "node_modules/side-channel": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/side-channel/-/side-channel-1.0.5.tgz", + "integrity": "sha512-QcgiIWV4WV7qWExbN5llt6frQB/lBven9pqliLXfGPB+K9ZYXxDozp0wLkHS24kWCm+6YXH/f0HhnObZnZOBnQ==", + "dependencies": { + "call-bind": "^1.0.6", + "es-errors": "^1.3.0", + "get-intrinsic": "^1.2.4", + "object-inspect": "^1.13.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/sift": { + "version": "16.0.1", + "resolved": "https://registry.npmjs.org/sift/-/sift-16.0.1.tgz", + "integrity": "sha512-Wv6BjQ5zbhW7VFefWusVP33T/EM0vYikCaQ2qR8yULbsilAT8/wQaXvuQ3ptGLpoKx+lihJE3y2UTgKDyyNHZQ==" + }, + "node_modules/sonic-boom": { + "version": "3.8.0", + "resolved": "https://registry.npmjs.org/sonic-boom/-/sonic-boom-3.8.0.tgz", + "integrity": "sha512-ybz6OYOUjoQQCQ/i4LU8kaToD8ACtYP+Cj5qd2AO36bwbdewxWJ3ArmJ2cr6AvxlL2o0PqnCcPGUgkILbfkaCA==", + "dependencies": { + "atomic-sleep": "^1.0.0" + } + }, + "node_modules/sparse-bitfield": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/sparse-bitfield/-/sparse-bitfield-3.0.3.tgz", + "integrity": "sha512-kvzhi7vqKTfkh0PZU+2D2PIllw2ymqJKujUcyPMd9Y75Nv4nPbGJZXNhxsgdQab2BmlDct1YnfQCguEvHr7VsQ==", + "dependencies": { + "memory-pager": "^1.0.2" + } + }, + "node_modules/split2": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/split2/-/split2-4.2.0.tgz", + "integrity": "sha512-UcjcJOWknrNkF6PLX83qcHM6KHgVKNkV62Y8a5uYDVv9ydGQVwAHMKqHdJje1VTWpljG0WYpCDhrCdAOYH4TWg==", + "engines": { + "node": ">= 10.x" + } + }, + "node_modules/statuses": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/statuses/-/statuses-2.0.1.tgz", + "integrity": "sha512-RwNA9Z/7PrK06rYLIzFMlaF+l73iwpzsqRIFgbMLbTcLD6cOao82TaWefPXQvB2fOC4AjuYSEndS7N/mTCbkdQ==", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/string_decoder": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.3.0.tgz", + "integrity": "sha512-hkRX8U1WjJFd8LsDJ2yQ/wWWxaopEsABU1XfkM8A+j0+85JAGppt16cr1Whg6KIbb4okU6Mql6BOj+uup/wKeA==", + "dependencies": { + "safe-buffer": "~5.2.0" + } + }, + "node_modules/thread-stream": { + "version": "2.4.1", + "resolved": "https://registry.npmjs.org/thread-stream/-/thread-stream-2.4.1.tgz", + "integrity": "sha512-d/Ex2iWd1whipbT681JmTINKw0ZwOUBZm7+Gjs64DHuX34mmw8vJL2bFAaNacaW72zYiTJxSHi5abUuOi5nsfg==", + "dependencies": { + "real-require": "^0.2.0" + } + }, + "node_modules/toidentifier": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/toidentifier/-/toidentifier-1.0.1.tgz", + "integrity": "sha512-o5sSPKEkg/DIQNmH43V0/uerLrpzVedkUh8tGNvaeXpfpuwjKenlSox/2O/BTlZUtEe+JG7s5YhEz608PlAHRA==", + "engines": { + "node": ">=0.6" + } + }, + "node_modules/tr46": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/tr46/-/tr46-4.1.1.tgz", + "integrity": "sha512-2lv/66T7e5yNyhAAC4NaKe5nVavzuGJQVVtRYLyQ2OI8tsJ61PMLlelehb0wi2Hx6+hT/OJUWZcw8MjlSRnxvw==", + "dependencies": { + "punycode": "^2.3.0" + }, + "engines": { + "node": ">=14" + } + }, + "node_modules/type-is": { + "version": "1.6.18", + "resolved": "https://registry.npmjs.org/type-is/-/type-is-1.6.18.tgz", + "integrity": "sha512-TkRKr9sUTxEH8MdfuCSP7VizJyzRNMjj2J2do2Jr3Kym598JVdEksuzPQCnlFPW4ky9Q+iA+ma9BGm06XQBy8g==", + "dependencies": { + "media-typer": "0.3.0", + "mime-types": "~2.1.24" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/unpipe": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/unpipe/-/unpipe-1.0.0.tgz", + "integrity": "sha512-pjy2bYhSsufwWlKwPc+l3cN7+wuJlK6uz0YdJEOlQDbl6jo/YlPi4mb8agUkVC8BF7V8NuzeyPNqRksA3hztKQ==", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/utils-merge": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/utils-merge/-/utils-merge-1.0.1.tgz", + "integrity": "sha512-pMZTvIkT1d+TFGvDOqodOclx0QWkkgi6Tdoa8gC8ffGAAqz9pzPTZWAybbsHHoED/ztMtkv/VoYTYyShUn81hA==", + "engines": { + "node": ">= 0.4.0" + } + }, + "node_modules/vary": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/vary/-/vary-1.1.2.tgz", + "integrity": "sha512-BNGbWLfd0eUPabhkXUVm0j8uuvREyTh5ovRa/dyow/BqAbZJyC+5fU+IzQOzmAKzYqYRAISoRhdQr3eIZ/PXqg==", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/webidl-conversions": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/webidl-conversions/-/webidl-conversions-7.0.0.tgz", + "integrity": "sha512-VwddBukDzu71offAQR975unBIGqfKZpM+8ZX6ySk8nYhVoo5CYaZyzt3YBvYtRtO+aoGlqxPg/B87NGVZ/fu6g==", + "engines": { + "node": ">=12" + } + }, + "node_modules/whatwg-url": { + "version": "13.0.0", + "resolved": "https://registry.npmjs.org/whatwg-url/-/whatwg-url-13.0.0.tgz", + "integrity": "sha512-9WWbymnqj57+XEuqADHrCJ2eSXzn8WXIW/YSGaZtb2WKAInQ6CHfaUUcTyyver0p8BDg5StLQq8h1vtZuwmOig==", + "dependencies": { + "tr46": "^4.1.1", + "webidl-conversions": "^7.0.0" + }, + "engines": { + "node": ">=16" + } + } + } +} diff --git a/09-express/02-feed-api/package.json b/09-express/02-feed-api/package.json new file mode 100644 index 0000000..2241e26 --- /dev/null +++ b/09-express/02-feed-api/package.json @@ -0,0 +1,20 @@ +{ + "name": "02-feed-api", + "version": "1.0.0", + "description": "", + "main": "index.js", + "scripts": { + "test": "echo \"Error: no test specified\" && exit 1" + }, + "keywords": [], + "author": "", + "license": "ISC", + "dependencies": { + "compression": "^1.7.4", + "date-fns": "^3.3.1", + "express": "^4.18.2", + "htmlparser2": "^9.1.0", + "mongoose": "^8.1.3", + "pino-http": "^9.0.0" + } +} diff --git a/09-express/03-feed-api-router/app.mjs b/09-express/03-feed-api-router/app.mjs new file mode 100644 index 0000000..7648f59 --- /dev/null +++ b/09-express/03-feed-api-router/app.mjs @@ -0,0 +1,22 @@ +import express from "express"; +import compression from "compression"; +import pino from "pino-http"; +import mongoose from "mongoose"; +import sourcesRoutes from "./routes/sources.mjs"; +import feedsRoutes from "./routes/feeds.mjs"; +import feedsUpdateRoutes from "./routes/feeds-update.mjs"; + +await mongoose.connect("mongodb://127.0.0.1:27017/feed-reader"); +const app = express(); +const port = 3000; + +app.use(pino()); +app.use(compression()); + +app.use("/sources", sourcesRoutes); +app.use("/feeds", feedsRoutes); +app.use("/feeds-update", feedsUpdateRoutes); + +app.listen(port, () => { + console.log(`FEED app listening on port ${port}`); +}); diff --git a/09-express/03-feed-api-router/get-feeds.mjs b/09-express/03-feed-api-router/get-feeds.mjs new file mode 100644 index 0000000..3ddd3b3 --- /dev/null +++ b/09-express/03-feed-api-router/get-feeds.mjs @@ -0,0 +1,25 @@ +import { parseFeed } from "htmlparser2"; +import { compareDesc } from "date-fns"; + +export default async function getFeedEntries(url, maxEntries = 5) { + const feedFetched = await fetch(url); + if (!feedFetched.ok) { + throw new Error("Feed fetch failed"); + } + const feedContent = await feedFetched.text(); + const { items } = parseFeed(feedContent); + return items.slice(0, maxEntries); +} + +export async function getLatestEntries(urls) { + const feedsFetches = urls.map((s) => getFeedEntries(s)); + const fetchesPromises = await Promise.allSettled(feedsFetches); + const retrievedFeeds = fetchesPromises + .filter((r) => r.status === "fulfilled") + .map((r) => r.value); + const latestItems = retrievedFeeds.flat().map((i) => { + return { title: i.title, link: i.link, date: i.pubDate }; + }); + latestItems.sort((a, b) => compareDesc(a.date, b.date)); + return latestItems; +} diff --git a/09-express/03-feed-api-router/models/FeedEntry.mjs b/09-express/03-feed-api-router/models/FeedEntry.mjs new file mode 100644 index 0000000..460312e --- /dev/null +++ b/09-express/03-feed-api-router/models/FeedEntry.mjs @@ -0,0 +1,12 @@ +import mongoose from "mongoose"; +const { Schema } = mongoose; + +const feedEntrySchema = new Schema({ + source: { type: Schema.Types.ObjectId, ref: "Source" }, + title: { type: String }, + link: { type: String, unique: true }, + date: { type: Date }, +}); + +const FeedEntry = mongoose.model("FeedEntry", feedEntrySchema); +export { FeedEntry }; diff --git a/09-express/03-feed-api-router/models/Source.mjs b/09-express/03-feed-api-router/models/Source.mjs new file mode 100644 index 0000000..f358052 --- /dev/null +++ b/09-express/03-feed-api-router/models/Source.mjs @@ -0,0 +1,36 @@ +import mongoose from "mongoose"; +const { Schema } = mongoose; + +const sourceSchema = new Schema({ + url: { + type: String, + required: true, + unique: true, + validate: { + validator: function (v) { + try { + const url = new URL(v); + if (!url.protocol.startsWith("http")) { + return false; + } + } catch { + return false; + } + return true; + }, + message: (props) => `${props.value} is not a valid URL`, + }, + }, +}); +sourceSchema.static("getErrorReason", (e) => { + if (e.name === "ValidationError") { + return "Source validation failed"; + } else if (e.code === 11000) { + return "Feed already exists"; + } else { + return null; + } +}); + +const Source = mongoose.model("Source", sourceSchema); +export { Source }; diff --git a/09-express/03-feed-api-router/package-lock.json b/09-express/03-feed-api-router/package-lock.json new file mode 100644 index 0000000..2c24268 --- /dev/null +++ b/09-express/03-feed-api-router/package-lock.json @@ -0,0 +1,1298 @@ +{ + "name": "03-feed-api-router", + "version": "1.0.0", + "lockfileVersion": 3, + "requires": true, + "packages": { + "": { + "name": "03-feed-api-router", + "version": "1.0.0", + "license": "ISC", + "dependencies": { + "compression": "^1.7.4", + "date-fns": "^3.3.1", + "express": "^4.18.2", + "htmlparser2": "^9.1.0", + "mongoose": "^8.1.3", + "pino-http": "^9.0.0" + } + }, + "node_modules/@mongodb-js/saslprep": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/@mongodb-js/saslprep/-/saslprep-1.1.4.tgz", + "integrity": "sha512-8zJ8N1x51xo9hwPh6AWnKdLGEC5N3lDa6kms1YHmFBoRhTpJR6HG8wWk0td1MVCu9cD4YBrvjZEtd5Obw0Fbnw==", + "dependencies": { + "sparse-bitfield": "^3.0.3" + } + }, + "node_modules/@types/webidl-conversions": { + "version": "7.0.3", + "resolved": "https://registry.npmjs.org/@types/webidl-conversions/-/webidl-conversions-7.0.3.tgz", + "integrity": "sha512-CiJJvcRtIgzadHCYXw7dqEnMNRjhGZlYK05Mj9OyktqV8uVT8fD2BFOB7S1uwBE3Kj2Z+4UyPmFw/Ixgw/LAlA==" + }, + "node_modules/@types/whatwg-url": { + "version": "11.0.4", + "resolved": "https://registry.npmjs.org/@types/whatwg-url/-/whatwg-url-11.0.4.tgz", + "integrity": "sha512-lXCmTWSHJvf0TRSO58nm978b8HJ/EdsSsEKLd3ODHFjo+3VGAyyTp4v50nWvwtzBxSMQrVOK7tcuN0zGPLICMw==", + "dependencies": { + "@types/webidl-conversions": "*" + } + }, + "node_modules/abort-controller": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/abort-controller/-/abort-controller-3.0.0.tgz", + "integrity": "sha512-h8lQ8tacZYnR3vNQTgibj+tODHI5/+l06Au2Pcriv/Gmet0eaj4TwWH41sO9wnHDiQsEj19q0drzdWdeAHtweg==", + "dependencies": { + "event-target-shim": "^5.0.0" + }, + "engines": { + "node": ">=6.5" + } + }, + "node_modules/accepts": { + "version": "1.3.8", + "resolved": "https://registry.npmjs.org/accepts/-/accepts-1.3.8.tgz", + "integrity": "sha512-PYAthTa2m2VKxuvSD3DPC/Gy+U+sOA1LAuT8mkmRuvw+NACSaeXEQ+NHcVF7rONl6qcaxV3Uuemwawk+7+SJLw==", + "dependencies": { + "mime-types": "~2.1.34", + "negotiator": "0.6.3" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/array-flatten": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/array-flatten/-/array-flatten-1.1.1.tgz", + "integrity": "sha512-PCVAQswWemu6UdxsDFFX/+gVeYqKAod3D3UVm91jHwynguOwAvYPhx8nNlM++NqRcK6CxxpUafjmhIdKiHibqg==" + }, + "node_modules/atomic-sleep": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/atomic-sleep/-/atomic-sleep-1.0.0.tgz", + "integrity": "sha512-kNOjDqAh7px0XWNI+4QbzoiR/nTkHAWNud2uvnJquD1/x5a7EQZMJT0AczqK0Qn67oY/TTQ1LbUKajZpp3I9tQ==", + "engines": { + "node": ">=8.0.0" + } + }, + "node_modules/base64-js": { + "version": "1.5.1", + "resolved": "https://registry.npmjs.org/base64-js/-/base64-js-1.5.1.tgz", + "integrity": "sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ] + }, + "node_modules/body-parser": { + "version": "1.20.1", + "resolved": "https://registry.npmjs.org/body-parser/-/body-parser-1.20.1.tgz", + "integrity": "sha512-jWi7abTbYwajOytWCQc37VulmWiRae5RyTpaCyDcS5/lMdtwSz5lOpDE67srw/HYe35f1z3fDQw+3txg7gNtWw==", + "dependencies": { + "bytes": "3.1.2", + "content-type": "~1.0.4", + "debug": "2.6.9", + "depd": "2.0.0", + "destroy": "1.2.0", + "http-errors": "2.0.0", + "iconv-lite": "0.4.24", + "on-finished": "2.4.1", + "qs": "6.11.0", + "raw-body": "2.5.1", + "type-is": "~1.6.18", + "unpipe": "1.0.0" + }, + "engines": { + "node": ">= 0.8", + "npm": "1.2.8000 || >= 1.4.16" + } + }, + "node_modules/bson": { + "version": "6.3.0", + "resolved": "https://registry.npmjs.org/bson/-/bson-6.3.0.tgz", + "integrity": "sha512-balJfqwwTBddxfnidJZagCBPP/f48zj9Sdp3OJswREOgsJzHiQSaOIAtApSgDQFYgHqAvFkp53AFSqjMDZoTFw==", + "engines": { + "node": ">=16.20.1" + } + }, + "node_modules/buffer": { + "version": "6.0.3", + "resolved": "https://registry.npmjs.org/buffer/-/buffer-6.0.3.tgz", + "integrity": "sha512-FTiCpNxtwiZZHEZbcbTIcZjERVICn9yq/pDFkTl95/AxzD1naBctN7YO68riM/gLSDY7sdrMby8hofADYuuqOA==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "dependencies": { + "base64-js": "^1.3.1", + "ieee754": "^1.2.1" + } + }, + "node_modules/bytes": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/bytes/-/bytes-3.1.2.tgz", + "integrity": "sha512-/Nf7TyzTx6S3yRJObOAV7956r8cr2+Oj8AC5dt8wSP3BQAoeX58NoHyCU8P8zGkNXStjTSi6fzO6F0pBdcYbEg==", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/call-bind": { + "version": "1.0.7", + "resolved": "https://registry.npmjs.org/call-bind/-/call-bind-1.0.7.tgz", + "integrity": "sha512-GHTSNSYICQ7scH7sZ+M2rFopRoLh8t2bLSW6BbgrtLsahOIB5iyAVJf9GjWK3cYTDaMj4XdBpM1cA6pIS0Kv2w==", + "dependencies": { + "es-define-property": "^1.0.0", + "es-errors": "^1.3.0", + "function-bind": "^1.1.2", + "get-intrinsic": "^1.2.4", + "set-function-length": "^1.2.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/compressible": { + "version": "2.0.18", + "resolved": "https://registry.npmjs.org/compressible/-/compressible-2.0.18.tgz", + "integrity": "sha512-AF3r7P5dWxL8MxyITRMlORQNaOA2IkAFaTr4k7BUumjPtRpGDTZpl0Pb1XCO6JeDCBdp126Cgs9sMxqSjgYyRg==", + "dependencies": { + "mime-db": ">= 1.43.0 < 2" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/compression": { + "version": "1.7.4", + "resolved": "https://registry.npmjs.org/compression/-/compression-1.7.4.tgz", + "integrity": "sha512-jaSIDzP9pZVS4ZfQ+TzvtiWhdpFhE2RDHz8QJkpX9SIpLq88VueF5jJw6t+6CUQcAoA6t+x89MLrWAqpfDE8iQ==", + "dependencies": { + "accepts": "~1.3.5", + "bytes": "3.0.0", + "compressible": "~2.0.16", + "debug": "2.6.9", + "on-headers": "~1.0.2", + "safe-buffer": "5.1.2", + "vary": "~1.1.2" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/compression/node_modules/bytes": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/bytes/-/bytes-3.0.0.tgz", + "integrity": "sha512-pMhOfFDPiv9t5jjIXkHosWmkSyQbvsgEVNkz0ERHbuLh2T/7j4Mqqpz523Fe8MVY89KC6Sh/QfS2sM+SjgFDcw==", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/compression/node_modules/safe-buffer": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.2.tgz", + "integrity": "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==" + }, + "node_modules/content-disposition": { + "version": "0.5.4", + "resolved": "https://registry.npmjs.org/content-disposition/-/content-disposition-0.5.4.tgz", + "integrity": "sha512-FveZTNuGw04cxlAiWbzi6zTAL/lhehaWbTtgluJh4/E95DqMwTmha3KZN1aAWA8cFIhHzMZUvLevkw5Rqk+tSQ==", + "dependencies": { + "safe-buffer": "5.2.1" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/content-type": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/content-type/-/content-type-1.0.5.tgz", + "integrity": "sha512-nTjqfcBFEipKdXCv4YDQWCfmcLZKm81ldF0pAopTvyrFGVbcR6P/VAAd5G7N+0tTr8QqiU0tFadD6FK4NtJwOA==", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/cookie": { + "version": "0.5.0", + "resolved": "https://registry.npmjs.org/cookie/-/cookie-0.5.0.tgz", + "integrity": "sha512-YZ3GUyn/o8gfKJlnlX7g7xq4gyO6OSuhGPKaaGssGB2qgDUS0gPgtTvoyZLTt9Ab6dC4hfc9dV5arkvc/OCmrw==", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/cookie-signature": { + "version": "1.0.6", + "resolved": "https://registry.npmjs.org/cookie-signature/-/cookie-signature-1.0.6.tgz", + "integrity": "sha512-QADzlaHc8icV8I7vbaJXJwod9HWYp8uCqf1xa4OfNu1T7JVxQIrUgOWtHdNDtPiywmFbiS12VjotIXLrKM3orQ==" + }, + "node_modules/date-fns": { + "version": "3.3.1", + "resolved": "https://registry.npmjs.org/date-fns/-/date-fns-3.3.1.tgz", + "integrity": "sha512-y8e109LYGgoQDveiEBD3DYXKba1jWf5BA8YU1FL5Tvm0BTdEfy54WLCwnuYWZNnzzvALy/QQ4Hov+Q9RVRv+Zw==", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/kossnocorp" + } + }, + "node_modules/debug": { + "version": "2.6.9", + "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", + "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==", + "dependencies": { + "ms": "2.0.0" + } + }, + "node_modules/define-data-property": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/define-data-property/-/define-data-property-1.1.4.tgz", + "integrity": "sha512-rBMvIzlpA8v6E+SJZoo++HAYqsLrkg7MSfIinMPFhmkorw7X+dOXVJQs+QT69zGkzMyfDnIMN2Wid1+NbL3T+A==", + "dependencies": { + "es-define-property": "^1.0.0", + "es-errors": "^1.3.0", + "gopd": "^1.0.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/depd": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/depd/-/depd-2.0.0.tgz", + "integrity": "sha512-g7nH6P6dyDioJogAAGprGpCtVImJhpPk/roCzdb3fIh61/s/nPsfR6onyMwkCAR/OlC3yBC0lESvUoQEAssIrw==", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/destroy": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/destroy/-/destroy-1.2.0.tgz", + "integrity": "sha512-2sJGJTaXIIaR1w4iJSNoN0hnMY7Gpc/n8D4qSCJw8QqFWXf7cuAgnEHxBpweaVcPevC2l3KpjYCx3NypQQgaJg==", + "engines": { + "node": ">= 0.8", + "npm": "1.2.8000 || >= 1.4.16" + } + }, + "node_modules/dom-serializer": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/dom-serializer/-/dom-serializer-2.0.0.tgz", + "integrity": "sha512-wIkAryiqt/nV5EQKqQpo3SToSOV9J0DnbJqwK7Wv/Trc92zIAYZ4FlMu+JPFW1DfGFt81ZTCGgDEabffXeLyJg==", + "dependencies": { + "domelementtype": "^2.3.0", + "domhandler": "^5.0.2", + "entities": "^4.2.0" + }, + "funding": { + "url": "https://github.com/cheeriojs/dom-serializer?sponsor=1" + } + }, + "node_modules/domelementtype": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/domelementtype/-/domelementtype-2.3.0.tgz", + "integrity": "sha512-OLETBj6w0OsagBwdXnPdN0cnMfF9opN69co+7ZrbfPGrdpPVNBUj02spi6B1N7wChLQiPn4CSH/zJvXw56gmHw==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/fb55" + } + ] + }, + "node_modules/domhandler": { + "version": "5.0.3", + "resolved": "https://registry.npmjs.org/domhandler/-/domhandler-5.0.3.tgz", + "integrity": "sha512-cgwlv/1iFQiFnU96XXgROh8xTeetsnJiDsTc7TYCLFd9+/WNkIqPTxiM/8pSd8VIrhXGTf1Ny1q1hquVqDJB5w==", + "dependencies": { + "domelementtype": "^2.3.0" + }, + "engines": { + "node": ">= 4" + }, + "funding": { + "url": "https://github.com/fb55/domhandler?sponsor=1" + } + }, + "node_modules/domutils": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/domutils/-/domutils-3.1.0.tgz", + "integrity": "sha512-H78uMmQtI2AhgDJjWeQmHwJJ2bLPD3GMmO7Zja/ZZh84wkm+4ut+IUnUdRa8uCGX88DiVx1j6FRe1XfxEgjEZA==", + "dependencies": { + "dom-serializer": "^2.0.0", + "domelementtype": "^2.3.0", + "domhandler": "^5.0.3" + }, + "funding": { + "url": "https://github.com/fb55/domutils?sponsor=1" + } + }, + "node_modules/ee-first": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/ee-first/-/ee-first-1.1.1.tgz", + "integrity": "sha512-WMwm9LhRUo+WUaRN+vRuETqG89IgZphVSNkdFgeb6sS/E4OrDIN7t48CAewSHXc6C8lefD8KKfr5vY61brQlow==" + }, + "node_modules/encodeurl": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/encodeurl/-/encodeurl-1.0.2.tgz", + "integrity": "sha512-TPJXq8JqFaVYm2CWmPvnP2Iyo4ZSM7/QKcSmuMLDObfpH5fi7RUGmd/rTDf+rut/saiDiQEeVTNgAmJEdAOx0w==", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/entities": { + "version": "4.5.0", + "resolved": "https://registry.npmjs.org/entities/-/entities-4.5.0.tgz", + "integrity": "sha512-V0hjH4dGPh9Ao5p0MoRY6BVqtwCjhz6vI5LT8AJ55H+4g9/4vbHx1I54fS0XuclLhDHArPQCiMjDxjaL8fPxhw==", + "engines": { + "node": ">=0.12" + }, + "funding": { + "url": "https://github.com/fb55/entities?sponsor=1" + } + }, + "node_modules/es-define-property": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/es-define-property/-/es-define-property-1.0.0.tgz", + "integrity": "sha512-jxayLKShrEqqzJ0eumQbVhTYQM27CfT1T35+gCgDFoL82JLsXqTJ76zv6A0YLOgEnLUMvLzsDsGIrl8NFpT2gQ==", + "dependencies": { + "get-intrinsic": "^1.2.4" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/es-errors": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/es-errors/-/es-errors-1.3.0.tgz", + "integrity": "sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw==", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/escape-html": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/escape-html/-/escape-html-1.0.3.tgz", + "integrity": "sha512-NiSupZ4OeuGwr68lGIeym/ksIZMJodUGOSCZ/FSnTxcrekbvqrgdUxlJOMpijaKZVjAJrWrGs/6Jy8OMuyj9ow==" + }, + "node_modules/etag": { + "version": "1.8.1", + "resolved": "https://registry.npmjs.org/etag/-/etag-1.8.1.tgz", + "integrity": "sha512-aIL5Fx7mawVa300al2BnEE4iNvo1qETxLrPI/o05L7z6go7fCw1J6EQmbK4FmJ2AS7kgVF/KEZWufBfdClMcPg==", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/event-target-shim": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/event-target-shim/-/event-target-shim-5.0.1.tgz", + "integrity": "sha512-i/2XbnSz/uxRCU6+NdVJgKWDTM427+MqYbkQzD321DuCQJUqOuJKIA0IM2+W2xtYHdKOmZ4dR6fExsd4SXL+WQ==", + "engines": { + "node": ">=6" + } + }, + "node_modules/events": { + "version": "3.3.0", + "resolved": "https://registry.npmjs.org/events/-/events-3.3.0.tgz", + "integrity": "sha512-mQw+2fkQbALzQ7V0MY0IqdnXNOeTtP4r0lN9z7AAawCXgqea7bDii20AYrIBrFd/Hx0M2Ocz6S111CaFkUcb0Q==", + "engines": { + "node": ">=0.8.x" + } + }, + "node_modules/express": { + "version": "4.18.2", + "resolved": "https://registry.npmjs.org/express/-/express-4.18.2.tgz", + "integrity": "sha512-5/PsL6iGPdfQ/lKM1UuielYgv3BUoJfz1aUwU9vHZ+J7gyvwdQXFEBIEIaxeGf0GIcreATNyBExtalisDbuMqQ==", + "dependencies": { + "accepts": "~1.3.8", + "array-flatten": "1.1.1", + "body-parser": "1.20.1", + "content-disposition": "0.5.4", + "content-type": "~1.0.4", + "cookie": "0.5.0", + "cookie-signature": "1.0.6", + "debug": "2.6.9", + "depd": "2.0.0", + "encodeurl": "~1.0.2", + "escape-html": "~1.0.3", + "etag": "~1.8.1", + "finalhandler": "1.2.0", + "fresh": "0.5.2", + "http-errors": "2.0.0", + "merge-descriptors": "1.0.1", + "methods": "~1.1.2", + "on-finished": "2.4.1", + "parseurl": "~1.3.3", + "path-to-regexp": "0.1.7", + "proxy-addr": "~2.0.7", + "qs": "6.11.0", + "range-parser": "~1.2.1", + "safe-buffer": "5.2.1", + "send": "0.18.0", + "serve-static": "1.15.0", + "setprototypeof": "1.2.0", + "statuses": "2.0.1", + "type-is": "~1.6.18", + "utils-merge": "1.0.1", + "vary": "~1.1.2" + }, + "engines": { + "node": ">= 0.10.0" + } + }, + "node_modules/fast-redact": { + "version": "3.3.0", + "resolved": "https://registry.npmjs.org/fast-redact/-/fast-redact-3.3.0.tgz", + "integrity": "sha512-6T5V1QK1u4oF+ATxs1lWUmlEk6P2T9HqJG3e2DnHOdVgZy2rFJBoEnrIedcTXlkAHU/zKC+7KETJ+KGGKwxgMQ==", + "engines": { + "node": ">=6" + } + }, + "node_modules/finalhandler": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/finalhandler/-/finalhandler-1.2.0.tgz", + "integrity": "sha512-5uXcUVftlQMFnWC9qu/svkWv3GTd2PfUhK/3PLkYNAe7FbqJMt3515HaxE6eRL74GdsriiwujiawdaB1BpEISg==", + "dependencies": { + "debug": "2.6.9", + "encodeurl": "~1.0.2", + "escape-html": "~1.0.3", + "on-finished": "2.4.1", + "parseurl": "~1.3.3", + "statuses": "2.0.1", + "unpipe": "~1.0.0" + }, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/forwarded": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/forwarded/-/forwarded-0.2.0.tgz", + "integrity": "sha512-buRG0fpBtRHSTCOASe6hD258tEubFoRLb4ZNA6NxMVHNw2gOcwHo9wyablzMzOA5z9xA9L1KNjk/Nt6MT9aYow==", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/fresh": { + "version": "0.5.2", + "resolved": "https://registry.npmjs.org/fresh/-/fresh-0.5.2.tgz", + "integrity": "sha512-zJ2mQYM18rEFOudeV4GShTGIQ7RbzA7ozbU9I/XBpm7kqgMywgmylMwXHxZJmkVoYkna9d2pVXVXPdYTP9ej8Q==", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/function-bind": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.2.tgz", + "integrity": "sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==", + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/get-caller-file": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/get-caller-file/-/get-caller-file-2.0.5.tgz", + "integrity": "sha512-DyFP3BM/3YHTQOCUL/w0OZHR0lpKeGrxotcHWcqNEdnltqFwXVfhEBQ94eIo34AfQpo0rGki4cyIiftY06h2Fg==", + "engines": { + "node": "6.* || 8.* || >= 10.*" + } + }, + "node_modules/get-intrinsic": { + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.2.4.tgz", + "integrity": "sha512-5uYhsJH8VJBTv7oslg4BznJYhDoRI6waYCxMmCdnTrcCrHA/fCFKoTFz2JKKE0HdDFUF7/oQuhzumXJK7paBRQ==", + "dependencies": { + "es-errors": "^1.3.0", + "function-bind": "^1.1.2", + "has-proto": "^1.0.1", + "has-symbols": "^1.0.3", + "hasown": "^2.0.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/gopd": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/gopd/-/gopd-1.0.1.tgz", + "integrity": "sha512-d65bNlIadxvpb/A2abVdlqKqV563juRnZ1Wtk6s1sIR8uNsXR70xqIzVqxVf1eTqDunwT2MkczEeaezCKTZhwA==", + "dependencies": { + "get-intrinsic": "^1.1.3" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/has-property-descriptors": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/has-property-descriptors/-/has-property-descriptors-1.0.2.tgz", + "integrity": "sha512-55JNKuIW+vq4Ke1BjOTjM2YctQIvCT7GFzHwmfZPGo5wnrgkid0YQtnAleFSqumZm4az3n2BS+erby5ipJdgrg==", + "dependencies": { + "es-define-property": "^1.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/has-proto": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/has-proto/-/has-proto-1.0.3.tgz", + "integrity": "sha512-SJ1amZAJUiZS+PhsVLf5tGydlaVB8EdFpaSO4gmiUKUOxk8qzn5AIy4ZeJUmh22znIdk/uMAUT2pl3FxzVUH+Q==", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/has-symbols": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.0.3.tgz", + "integrity": "sha512-l3LCuF6MgDNwTDKkdYGEihYjt5pRPbEg46rtlmnSPlUbgmB8LOIrKJbYYFBSbnPaJexMKtiPO8hmeRjRz2Td+A==", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/hasown": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.1.tgz", + "integrity": "sha512-1/th4MHjnwncwXsIW6QMzlvYL9kG5e/CpVvLRZe4XPa8TOUNbCELqmvhDmnkNsAjwaG4+I8gJJL0JBvTTLO9qA==", + "dependencies": { + "function-bind": "^1.1.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/htmlparser2": { + "version": "9.1.0", + "resolved": "https://registry.npmjs.org/htmlparser2/-/htmlparser2-9.1.0.tgz", + "integrity": "sha512-5zfg6mHUoaer/97TxnGpxmbR7zJtPwIYFMZ/H5ucTlPZhKvtum05yiPK3Mgai3a0DyVxv7qYqoweaEd2nrYQzQ==", + "funding": [ + "https://github.com/fb55/htmlparser2?sponsor=1", + { + "type": "github", + "url": "https://github.com/sponsors/fb55" + } + ], + "dependencies": { + "domelementtype": "^2.3.0", + "domhandler": "^5.0.3", + "domutils": "^3.1.0", + "entities": "^4.5.0" + } + }, + "node_modules/http-errors": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/http-errors/-/http-errors-2.0.0.tgz", + "integrity": "sha512-FtwrG/euBzaEjYeRqOgly7G0qviiXoJWnvEH2Z1plBdXgbyjv34pHTSb9zoeHMyDy33+DWy5Wt9Wo+TURtOYSQ==", + "dependencies": { + "depd": "2.0.0", + "inherits": "2.0.4", + "setprototypeof": "1.2.0", + "statuses": "2.0.1", + "toidentifier": "1.0.1" + }, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/iconv-lite": { + "version": "0.4.24", + "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.4.24.tgz", + "integrity": "sha512-v3MXnZAcvnywkTUEZomIActle7RXXeedOR31wwl7VlyoXO4Qi9arvSenNQWne1TcRwhCL1HwLI21bEqdpj8/rA==", + "dependencies": { + "safer-buffer": ">= 2.1.2 < 3" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/ieee754": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/ieee754/-/ieee754-1.2.1.tgz", + "integrity": "sha512-dcyqhDvX1C46lXZcVqCpK+FtMRQVdIMN6/Df5js2zouUsqG7I6sFxitIC+7KYK29KdXOLHdu9zL4sFnoVQnqaA==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ] + }, + "node_modules/inherits": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz", + "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==" + }, + "node_modules/ipaddr.js": { + "version": "1.9.1", + "resolved": "https://registry.npmjs.org/ipaddr.js/-/ipaddr.js-1.9.1.tgz", + "integrity": "sha512-0KI/607xoxSToH7GjN1FfSbLoU0+btTicjsQSWQlh/hZykN8KpmMf7uYwPW3R+akZ6R/w18ZlXSHBYXiYUPO3g==", + "engines": { + "node": ">= 0.10" + } + }, + "node_modules/kareem": { + "version": "2.5.1", + "resolved": "https://registry.npmjs.org/kareem/-/kareem-2.5.1.tgz", + "integrity": "sha512-7jFxRVm+jD+rkq3kY0iZDJfsO2/t4BBPeEb2qKn2lR/9KhuksYk5hxzfRYWMPV8P/x2d0kHD306YyWLzjjH+uA==", + "engines": { + "node": ">=12.0.0" + } + }, + "node_modules/media-typer": { + "version": "0.3.0", + "resolved": "https://registry.npmjs.org/media-typer/-/media-typer-0.3.0.tgz", + "integrity": "sha512-dq+qelQ9akHpcOl/gUVRTxVIOkAJ1wR3QAvb4RsVjS8oVoFjDGTc679wJYmUmknUF5HwMLOgb5O+a3KxfWapPQ==", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/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==" + }, + "node_modules/merge-descriptors": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/merge-descriptors/-/merge-descriptors-1.0.1.tgz", + "integrity": "sha512-cCi6g3/Zr1iqQi6ySbseM1Xvooa98N0w31jzUYrXPX2xqObmFGHJ0tQ5u74H3mVh7wLouTseZyYIq39g8cNp1w==" + }, + "node_modules/methods": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/methods/-/methods-1.1.2.tgz", + "integrity": "sha512-iclAHeNqNm68zFtnZ0e+1L2yUIdvzNoauKU4WBA3VvH/vPFieF7qfRlwUZU+DA9P9bPXIS90ulxoUoCH23sV2w==", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/mime": { + "version": "1.6.0", + "resolved": "https://registry.npmjs.org/mime/-/mime-1.6.0.tgz", + "integrity": "sha512-x0Vn8spI+wuJ1O6S7gnbaQg8Pxh4NNHb7KSINmEWKiPE4RKOplvijn+NkmYmmRgP68mc70j2EbeTFRsrswaQeg==", + "bin": { + "mime": "cli.js" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/mime-db": { + "version": "1.52.0", + "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.52.0.tgz", + "integrity": "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/mime-types": { + "version": "2.1.35", + "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.35.tgz", + "integrity": "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==", + "dependencies": { + "mime-db": "1.52.0" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/mongodb": { + "version": "6.3.0", + "resolved": "https://registry.npmjs.org/mongodb/-/mongodb-6.3.0.tgz", + "integrity": "sha512-tt0KuGjGtLUhLoU263+xvQmPHEGTw5LbcNC73EoFRYgSHwZt5tsoJC110hDyO1kjQzpgNrpdcSza9PknWN4LrA==", + "dependencies": { + "@mongodb-js/saslprep": "^1.1.0", + "bson": "^6.2.0", + "mongodb-connection-string-url": "^3.0.0" + }, + "engines": { + "node": ">=16.20.1" + }, + "peerDependencies": { + "@aws-sdk/credential-providers": "^3.188.0", + "@mongodb-js/zstd": "^1.1.0", + "gcp-metadata": "^5.2.0", + "kerberos": "^2.0.1", + "mongodb-client-encryption": ">=6.0.0 <7", + "snappy": "^7.2.2", + "socks": "^2.7.1" + }, + "peerDependenciesMeta": { + "@aws-sdk/credential-providers": { + "optional": true + }, + "@mongodb-js/zstd": { + "optional": true + }, + "gcp-metadata": { + "optional": true + }, + "kerberos": { + "optional": true + }, + "mongodb-client-encryption": { + "optional": true + }, + "snappy": { + "optional": true + }, + "socks": { + "optional": true + } + } + }, + "node_modules/mongodb-connection-string-url": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/mongodb-connection-string-url/-/mongodb-connection-string-url-3.0.0.tgz", + "integrity": "sha512-t1Vf+m1I5hC2M5RJx/7AtxgABy1cZmIPQRMXw+gEIPn/cZNF3Oiy+l0UIypUwVB5trcWHq3crg2g3uAR9aAwsQ==", + "dependencies": { + "@types/whatwg-url": "^11.0.2", + "whatwg-url": "^13.0.0" + } + }, + "node_modules/mongoose": { + "version": "8.1.3", + "resolved": "https://registry.npmjs.org/mongoose/-/mongoose-8.1.3.tgz", + "integrity": "sha512-a5MajZSDJiQgy0iQcR+MIpFe7zehGJI4doJ6Dh1MvnGh8/HNNhr5pn07RPA86KCTjP2vuKdffpFmvXxcHiUOjw==", + "dependencies": { + "bson": "^6.2.0", + "kareem": "2.5.1", + "mongodb": "6.3.0", + "mpath": "0.9.0", + "mquery": "5.0.0", + "ms": "2.1.3", + "sift": "16.0.1" + }, + "engines": { + "node": ">=16.20.1" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/mongoose" + } + }, + "node_modules/mongoose/node_modules/ms": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", + "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==" + }, + "node_modules/mpath": { + "version": "0.9.0", + "resolved": "https://registry.npmjs.org/mpath/-/mpath-0.9.0.tgz", + "integrity": "sha512-ikJRQTk8hw5DEoFVxHG1Gn9T/xcjtdnOKIU1JTmGjZZlg9LST2mBLmcX3/ICIbgJydT2GOc15RnNy5mHmzfSew==", + "engines": { + "node": ">=4.0.0" + } + }, + "node_modules/mquery": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/mquery/-/mquery-5.0.0.tgz", + "integrity": "sha512-iQMncpmEK8R8ncT8HJGsGc9Dsp8xcgYMVSbs5jgnm1lFHTZqMJTUWTDx1LBO8+mK3tPNZWFLBghQEIOULSTHZg==", + "dependencies": { + "debug": "4.x" + }, + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/mquery/node_modules/debug": { + "version": "4.3.4", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.4.tgz", + "integrity": "sha512-PRWFHuSU3eDtQJPvnNY7Jcket1j0t5OuOsFzPPzsekD52Zl8qUfFIPEiswXqIvHWGVHOgX+7G/vCNNhehwxfkQ==", + "dependencies": { + "ms": "2.1.2" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "node_modules/mquery/node_modules/ms": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz", + "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==" + }, + "node_modules/ms": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", + "integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==" + }, + "node_modules/negotiator": { + "version": "0.6.3", + "resolved": "https://registry.npmjs.org/negotiator/-/negotiator-0.6.3.tgz", + "integrity": "sha512-+EUsqGPLsM+j/zdChZjsnX51g4XrHFOIXwfnCVPGlQk/k5giakcKsuxCObBRu6DSm9opw/O6slWbJdghQM4bBg==", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/object-inspect": { + "version": "1.13.1", + "resolved": "https://registry.npmjs.org/object-inspect/-/object-inspect-1.13.1.tgz", + "integrity": "sha512-5qoj1RUiKOMsCCNLV1CBiPYE10sziTsnmNxkAI/rZhiD63CF7IqdFGC/XzjWjpSgLf0LxXX3bDFIh0E18f6UhQ==", + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/on-exit-leak-free": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/on-exit-leak-free/-/on-exit-leak-free-2.1.2.tgz", + "integrity": "sha512-0eJJY6hXLGf1udHwfNftBqH+g73EU4B504nZeKpz1sYRKafAghwxEJunB2O7rDZkL4PGfsMVnTXZ2EjibbqcsA==", + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/on-finished": { + "version": "2.4.1", + "resolved": "https://registry.npmjs.org/on-finished/-/on-finished-2.4.1.tgz", + "integrity": "sha512-oVlzkg3ENAhCk2zdv7IJwd/QUD4z2RxRwpkcGY8psCVcCYZNq4wYnVWALHM+brtuJjePWiYF/ClmuDr8Ch5+kg==", + "dependencies": { + "ee-first": "1.1.1" + }, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/on-headers": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/on-headers/-/on-headers-1.0.2.tgz", + "integrity": "sha512-pZAE+FJLoyITytdqK0U5s+FIpjN0JP3OzFi/u8Rx+EV5/W+JTWGXG8xFzevE7AjBfDqHv/8vL8qQsIhHnqRkrA==", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/parseurl": { + "version": "1.3.3", + "resolved": "https://registry.npmjs.org/parseurl/-/parseurl-1.3.3.tgz", + "integrity": "sha512-CiyeOxFT/JZyN5m0z9PfXw4SCBJ6Sygz1Dpl0wqjlhDEGGBP1GnsUVEL0p63hoG1fcj3fHynXi9NYO4nWOL+qQ==", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/path-to-regexp": { + "version": "0.1.7", + "resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-0.1.7.tgz", + "integrity": "sha512-5DFkuoqlv1uYQKxy8omFBeJPQcdoE07Kv2sferDCrAq1ohOU+MSDswDIbnx3YAM60qIOnYa53wBhXW0EbMonrQ==" + }, + "node_modules/pino": { + "version": "8.19.0", + "resolved": "https://registry.npmjs.org/pino/-/pino-8.19.0.tgz", + "integrity": "sha512-oswmokxkav9bADfJ2ifrvfHUwad6MLp73Uat0IkQWY3iAw5xTRoznXbXksZs8oaOUMpmhVWD+PZogNzllWpJaA==", + "dependencies": { + "atomic-sleep": "^1.0.0", + "fast-redact": "^3.1.1", + "on-exit-leak-free": "^2.1.0", + "pino-abstract-transport": "v1.1.0", + "pino-std-serializers": "^6.0.0", + "process-warning": "^3.0.0", + "quick-format-unescaped": "^4.0.3", + "real-require": "^0.2.0", + "safe-stable-stringify": "^2.3.1", + "sonic-boom": "^3.7.0", + "thread-stream": "^2.0.0" + }, + "bin": { + "pino": "bin.js" + } + }, + "node_modules/pino-abstract-transport": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/pino-abstract-transport/-/pino-abstract-transport-1.1.0.tgz", + "integrity": "sha512-lsleG3/2a/JIWUtf9Q5gUNErBqwIu1tUKTT3dUzaf5DySw9ra1wcqKjJjLX1VTY64Wk1eEOYsVGSaGfCK85ekA==", + "dependencies": { + "readable-stream": "^4.0.0", + "split2": "^4.0.0" + } + }, + "node_modules/pino-http": { + "version": "9.0.0", + "resolved": "https://registry.npmjs.org/pino-http/-/pino-http-9.0.0.tgz", + "integrity": "sha512-Q9QDNEz0vQmbJtMFjOVr2c9yL92vHudjmr3s3m6J1hbw3DBGFZJm3TIj9TWyynZ4GEsEA9SOtni4heRUr6lNOg==", + "dependencies": { + "get-caller-file": "^2.0.5", + "pino": "^8.17.1", + "pino-std-serializers": "^6.2.2", + "process-warning": "^3.0.0" + } + }, + "node_modules/pino-std-serializers": { + "version": "6.2.2", + "resolved": "https://registry.npmjs.org/pino-std-serializers/-/pino-std-serializers-6.2.2.tgz", + "integrity": "sha512-cHjPPsE+vhj/tnhCy/wiMh3M3z3h/j15zHQX+S9GkTBgqJuTuJzYJ4gUyACLhDaJ7kk9ba9iRDmbH2tJU03OiA==" + }, + "node_modules/process": { + "version": "0.11.10", + "resolved": "https://registry.npmjs.org/process/-/process-0.11.10.tgz", + "integrity": "sha512-cdGef/drWFoydD1JsMzuFf8100nZl+GT+yacc2bEced5f9Rjk4z+WtFUTBu9PhOi9j/jfmBPu0mMEY4wIdAF8A==", + "engines": { + "node": ">= 0.6.0" + } + }, + "node_modules/process-warning": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/process-warning/-/process-warning-3.0.0.tgz", + "integrity": "sha512-mqn0kFRl0EoqhnL0GQ0veqFHyIN1yig9RHh/InzORTUiZHFRAur+aMtRkELNwGs9aNwKS6tg/An4NYBPGwvtzQ==" + }, + "node_modules/proxy-addr": { + "version": "2.0.7", + "resolved": "https://registry.npmjs.org/proxy-addr/-/proxy-addr-2.0.7.tgz", + "integrity": "sha512-llQsMLSUDUPT44jdrU/O37qlnifitDP+ZwrmmZcoSKyLKvtZxpyV0n2/bD/N4tBAAZ/gJEdZU7KMraoK1+XYAg==", + "dependencies": { + "forwarded": "0.2.0", + "ipaddr.js": "1.9.1" + }, + "engines": { + "node": ">= 0.10" + } + }, + "node_modules/punycode": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/punycode/-/punycode-2.3.1.tgz", + "integrity": "sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg==", + "engines": { + "node": ">=6" + } + }, + "node_modules/qs": { + "version": "6.11.0", + "resolved": "https://registry.npmjs.org/qs/-/qs-6.11.0.tgz", + "integrity": "sha512-MvjoMCJwEarSbUYk5O+nmoSzSutSsTwF85zcHPQ9OrlFoZOYIjaqBAJIqIXjptyD5vThxGq52Xu/MaJzRkIk4Q==", + "dependencies": { + "side-channel": "^1.0.4" + }, + "engines": { + "node": ">=0.6" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/quick-format-unescaped": { + "version": "4.0.4", + "resolved": "https://registry.npmjs.org/quick-format-unescaped/-/quick-format-unescaped-4.0.4.tgz", + "integrity": "sha512-tYC1Q1hgyRuHgloV/YXs2w15unPVh8qfu/qCTfhTYamaw7fyhumKa2yGpdSo87vY32rIclj+4fWYQXUMs9EHvg==" + }, + "node_modules/range-parser": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/range-parser/-/range-parser-1.2.1.tgz", + "integrity": "sha512-Hrgsx+orqoygnmhFbKaHE6c296J+HTAQXoxEF6gNupROmmGJRoyzfG3ccAveqCBrwr/2yxQ5BVd/GTl5agOwSg==", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/raw-body": { + "version": "2.5.1", + "resolved": "https://registry.npmjs.org/raw-body/-/raw-body-2.5.1.tgz", + "integrity": "sha512-qqJBtEyVgS0ZmPGdCFPWJ3FreoqvG4MVQln/kCgF7Olq95IbOp0/BWyMwbdtn4VTvkM8Y7khCQ2Xgk/tcrCXig==", + "dependencies": { + "bytes": "3.1.2", + "http-errors": "2.0.0", + "iconv-lite": "0.4.24", + "unpipe": "1.0.0" + }, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/readable-stream": { + "version": "4.5.2", + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-4.5.2.tgz", + "integrity": "sha512-yjavECdqeZ3GLXNgRXgeQEdz9fvDDkNKyHnbHRFtOr7/LcfgBcmct7t/ET+HaCTqfh06OzoAxrkN/IfjJBVe+g==", + "dependencies": { + "abort-controller": "^3.0.0", + "buffer": "^6.0.3", + "events": "^3.3.0", + "process": "^0.11.10", + "string_decoder": "^1.3.0" + }, + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + } + }, + "node_modules/real-require": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/real-require/-/real-require-0.2.0.tgz", + "integrity": "sha512-57frrGM/OCTLqLOAh0mhVA9VBMHd+9U7Zb2THMGdBUoZVOtGbJzjxsYGDJ3A9AYYCP4hn6y1TVbaOfzWtm5GFg==", + "engines": { + "node": ">= 12.13.0" + } + }, + "node_modules/safe-buffer": { + "version": "5.2.1", + "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.1.tgz", + "integrity": "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ] + }, + "node_modules/safe-stable-stringify": { + "version": "2.4.3", + "resolved": "https://registry.npmjs.org/safe-stable-stringify/-/safe-stable-stringify-2.4.3.tgz", + "integrity": "sha512-e2bDA2WJT0wxseVd4lsDP4+3ONX6HpMXQa1ZhFQ7SU+GjvORCmShbCMltrtIDfkYhVHrOcPtj+KhmDBdPdZD1g==", + "engines": { + "node": ">=10" + } + }, + "node_modules/safer-buffer": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/safer-buffer/-/safer-buffer-2.1.2.tgz", + "integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==" + }, + "node_modules/send": { + "version": "0.18.0", + "resolved": "https://registry.npmjs.org/send/-/send-0.18.0.tgz", + "integrity": "sha512-qqWzuOjSFOuqPjFe4NOsMLafToQQwBSOEpS+FwEt3A2V3vKubTquT3vmLTQpFgMXp8AlFWFuP1qKaJZOtPpVXg==", + "dependencies": { + "debug": "2.6.9", + "depd": "2.0.0", + "destroy": "1.2.0", + "encodeurl": "~1.0.2", + "escape-html": "~1.0.3", + "etag": "~1.8.1", + "fresh": "0.5.2", + "http-errors": "2.0.0", + "mime": "1.6.0", + "ms": "2.1.3", + "on-finished": "2.4.1", + "range-parser": "~1.2.1", + "statuses": "2.0.1" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/send/node_modules/ms": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", + "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==" + }, + "node_modules/serve-static": { + "version": "1.15.0", + "resolved": "https://registry.npmjs.org/serve-static/-/serve-static-1.15.0.tgz", + "integrity": "sha512-XGuRDNjXUijsUL0vl6nSD7cwURuzEgglbOaFuZM9g3kwDXOWVTck0jLzjPzGD+TazWbboZYu52/9/XPdUgne9g==", + "dependencies": { + "encodeurl": "~1.0.2", + "escape-html": "~1.0.3", + "parseurl": "~1.3.3", + "send": "0.18.0" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/set-function-length": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/set-function-length/-/set-function-length-1.2.1.tgz", + "integrity": "sha512-j4t6ccc+VsKwYHso+kElc5neZpjtq9EnRICFZtWyBsLojhmeF/ZBd/elqm22WJh/BziDe/SBiOeAt0m2mfLD0g==", + "dependencies": { + "define-data-property": "^1.1.2", + "es-errors": "^1.3.0", + "function-bind": "^1.1.2", + "get-intrinsic": "^1.2.3", + "gopd": "^1.0.1", + "has-property-descriptors": "^1.0.1" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/setprototypeof": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/setprototypeof/-/setprototypeof-1.2.0.tgz", + "integrity": "sha512-E5LDX7Wrp85Kil5bhZv46j8jOeboKq5JMmYM3gVGdGH8xFpPWXUMsNrlODCrkoxMEeNi/XZIwuRvY4XNwYMJpw==" + }, + "node_modules/side-channel": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/side-channel/-/side-channel-1.0.5.tgz", + "integrity": "sha512-QcgiIWV4WV7qWExbN5llt6frQB/lBven9pqliLXfGPB+K9ZYXxDozp0wLkHS24kWCm+6YXH/f0HhnObZnZOBnQ==", + "dependencies": { + "call-bind": "^1.0.6", + "es-errors": "^1.3.0", + "get-intrinsic": "^1.2.4", + "object-inspect": "^1.13.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/sift": { + "version": "16.0.1", + "resolved": "https://registry.npmjs.org/sift/-/sift-16.0.1.tgz", + "integrity": "sha512-Wv6BjQ5zbhW7VFefWusVP33T/EM0vYikCaQ2qR8yULbsilAT8/wQaXvuQ3ptGLpoKx+lihJE3y2UTgKDyyNHZQ==" + }, + "node_modules/sonic-boom": { + "version": "3.8.0", + "resolved": "https://registry.npmjs.org/sonic-boom/-/sonic-boom-3.8.0.tgz", + "integrity": "sha512-ybz6OYOUjoQQCQ/i4LU8kaToD8ACtYP+Cj5qd2AO36bwbdewxWJ3ArmJ2cr6AvxlL2o0PqnCcPGUgkILbfkaCA==", + "dependencies": { + "atomic-sleep": "^1.0.0" + } + }, + "node_modules/sparse-bitfield": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/sparse-bitfield/-/sparse-bitfield-3.0.3.tgz", + "integrity": "sha512-kvzhi7vqKTfkh0PZU+2D2PIllw2ymqJKujUcyPMd9Y75Nv4nPbGJZXNhxsgdQab2BmlDct1YnfQCguEvHr7VsQ==", + "dependencies": { + "memory-pager": "^1.0.2" + } + }, + "node_modules/split2": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/split2/-/split2-4.2.0.tgz", + "integrity": "sha512-UcjcJOWknrNkF6PLX83qcHM6KHgVKNkV62Y8a5uYDVv9ydGQVwAHMKqHdJje1VTWpljG0WYpCDhrCdAOYH4TWg==", + "engines": { + "node": ">= 10.x" + } + }, + "node_modules/statuses": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/statuses/-/statuses-2.0.1.tgz", + "integrity": "sha512-RwNA9Z/7PrK06rYLIzFMlaF+l73iwpzsqRIFgbMLbTcLD6cOao82TaWefPXQvB2fOC4AjuYSEndS7N/mTCbkdQ==", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/string_decoder": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.3.0.tgz", + "integrity": "sha512-hkRX8U1WjJFd8LsDJ2yQ/wWWxaopEsABU1XfkM8A+j0+85JAGppt16cr1Whg6KIbb4okU6Mql6BOj+uup/wKeA==", + "dependencies": { + "safe-buffer": "~5.2.0" + } + }, + "node_modules/thread-stream": { + "version": "2.4.1", + "resolved": "https://registry.npmjs.org/thread-stream/-/thread-stream-2.4.1.tgz", + "integrity": "sha512-d/Ex2iWd1whipbT681JmTINKw0ZwOUBZm7+Gjs64DHuX34mmw8vJL2bFAaNacaW72zYiTJxSHi5abUuOi5nsfg==", + "dependencies": { + "real-require": "^0.2.0" + } + }, + "node_modules/toidentifier": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/toidentifier/-/toidentifier-1.0.1.tgz", + "integrity": "sha512-o5sSPKEkg/DIQNmH43V0/uerLrpzVedkUh8tGNvaeXpfpuwjKenlSox/2O/BTlZUtEe+JG7s5YhEz608PlAHRA==", + "engines": { + "node": ">=0.6" + } + }, + "node_modules/tr46": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/tr46/-/tr46-4.1.1.tgz", + "integrity": "sha512-2lv/66T7e5yNyhAAC4NaKe5nVavzuGJQVVtRYLyQ2OI8tsJ61PMLlelehb0wi2Hx6+hT/OJUWZcw8MjlSRnxvw==", + "dependencies": { + "punycode": "^2.3.0" + }, + "engines": { + "node": ">=14" + } + }, + "node_modules/type-is": { + "version": "1.6.18", + "resolved": "https://registry.npmjs.org/type-is/-/type-is-1.6.18.tgz", + "integrity": "sha512-TkRKr9sUTxEH8MdfuCSP7VizJyzRNMjj2J2do2Jr3Kym598JVdEksuzPQCnlFPW4ky9Q+iA+ma9BGm06XQBy8g==", + "dependencies": { + "media-typer": "0.3.0", + "mime-types": "~2.1.24" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/unpipe": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/unpipe/-/unpipe-1.0.0.tgz", + "integrity": "sha512-pjy2bYhSsufwWlKwPc+l3cN7+wuJlK6uz0YdJEOlQDbl6jo/YlPi4mb8agUkVC8BF7V8NuzeyPNqRksA3hztKQ==", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/utils-merge": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/utils-merge/-/utils-merge-1.0.1.tgz", + "integrity": "sha512-pMZTvIkT1d+TFGvDOqodOclx0QWkkgi6Tdoa8gC8ffGAAqz9pzPTZWAybbsHHoED/ztMtkv/VoYTYyShUn81hA==", + "engines": { + "node": ">= 0.4.0" + } + }, + "node_modules/vary": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/vary/-/vary-1.1.2.tgz", + "integrity": "sha512-BNGbWLfd0eUPabhkXUVm0j8uuvREyTh5ovRa/dyow/BqAbZJyC+5fU+IzQOzmAKzYqYRAISoRhdQr3eIZ/PXqg==", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/webidl-conversions": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/webidl-conversions/-/webidl-conversions-7.0.0.tgz", + "integrity": "sha512-VwddBukDzu71offAQR975unBIGqfKZpM+8ZX6ySk8nYhVoo5CYaZyzt3YBvYtRtO+aoGlqxPg/B87NGVZ/fu6g==", + "engines": { + "node": ">=12" + } + }, + "node_modules/whatwg-url": { + "version": "13.0.0", + "resolved": "https://registry.npmjs.org/whatwg-url/-/whatwg-url-13.0.0.tgz", + "integrity": "sha512-9WWbymnqj57+XEuqADHrCJ2eSXzn8WXIW/YSGaZtb2WKAInQ6CHfaUUcTyyver0p8BDg5StLQq8h1vtZuwmOig==", + "dependencies": { + "tr46": "^4.1.1", + "webidl-conversions": "^7.0.0" + }, + "engines": { + "node": ">=16" + } + } + } +} diff --git a/09-express/03-feed-api-router/package.json b/09-express/03-feed-api-router/package.json new file mode 100644 index 0000000..0132173 --- /dev/null +++ b/09-express/03-feed-api-router/package.json @@ -0,0 +1,20 @@ +{ + "name": "03-feed-api-router", + "version": "1.0.0", + "description": "", + "main": "index.js", + "scripts": { + "test": "echo \"Error: no test specified\" && exit 1" + }, + "keywords": [], + "author": "", + "license": "ISC", + "dependencies": { + "compression": "^1.7.4", + "date-fns": "^3.3.1", + "express": "^4.18.2", + "htmlparser2": "^9.1.0", + "mongoose": "^8.1.3", + "pino-http": "^9.0.0" + } +} diff --git a/09-express/03-feed-api-router/routes/feeds-update.mjs b/09-express/03-feed-api-router/routes/feeds-update.mjs new file mode 100644 index 0000000..2e8a1df --- /dev/null +++ b/09-express/03-feed-api-router/routes/feeds-update.mjs @@ -0,0 +1,42 @@ +import express from "express"; +import { Source } from "../models/Source.mjs"; +import { wrap } from "../utils.mjs"; +import getFeedEntries from "../get-feeds.mjs"; +import { FeedEntry } from "../models/FeedEntry.mjs"; + +const router = express.Router(); + +function generateFeedEntries(entries, s) { + return entries.map((en) => { + return new FeedEntry({ + source: s._id, + title: en.title, + link: en.link, + date: new Date(en.pubDate), + }).save(); + }); +} +async function waitAndCountFulfilled(proms) { + const promsSettled = await Promise.allSettled(proms); + return promsSettled.filter((r) => r.status === "fulfilled").length; +} + +router.post( + "/", + wrap(async (req, res) => { + const sources = await Source.find({}).exec(); + let totalNewEntries = 0; + for (const s of sources) { + const entries = await getFeedEntries(s.url); + const savingModels = generateFeedEntries(entries, s); + let newCount = await waitAndCountFulfilled(savingModels); + totalNewEntries += newCount; + req.log.info( + `Feed ${s.url}: found ${entries.length} and saved ${newCount} entries` + ); + } + res.json({ totalNewEntries }); + }) +); + +export default router; diff --git a/09-express/03-feed-api-router/routes/feeds.mjs b/09-express/03-feed-api-router/routes/feeds.mjs new file mode 100644 index 0000000..5867c82 --- /dev/null +++ b/09-express/03-feed-api-router/routes/feeds.mjs @@ -0,0 +1,23 @@ +import express from "express"; +import { wrap } from "../utils.mjs"; +import { FeedEntry } from "../models/FeedEntry.mjs"; + +const router = express.Router(); + +router.get( + "/", + wrap(async (req, res) => { + let findQuery = {}; + if (req.query.sourceId) { + findQuery.source = req.query.sourceId; + } + const feedEntries = await FeedEntry.find(findQuery) + .sort({ date: -1 }) + .limit(20) + .populate("source") + .exec(); + res.json(feedEntries); + }) +); + +export default router; diff --git a/09-express/03-feed-api-router/routes/sources.mjs b/09-express/03-feed-api-router/routes/sources.mjs new file mode 100644 index 0000000..eea5952 --- /dev/null +++ b/09-express/03-feed-api-router/routes/sources.mjs @@ -0,0 +1,51 @@ +import express from "express"; +import { Source } from "../models/Source.mjs"; +import { wrap } from "../utils.mjs"; + +const router = express.Router(); +router.use(express.json()); + +router.post( + "/", + wrap(async (req, res) => { + const source = new Source({ url: req.body.url }); + req.log.info(`Adding new Source: ${source}`); + try { + await source.save(); + res.json(source); + } catch (e) { + const reason = Source.getErrorReason(e); + if (reason) { + req.log.error(reason); + res.status(400).send(); + } else { + req.log.error("An error occurred during Source.save()"); + res.status(500).send(); + } + } + }) +); + +router.get( + "/", + wrap(async (req, res) => { + const sources = await Source.find({}).exec(); + res.json(sources); + }) +); + +router.delete( + "/:id", + wrap(async (req, res) => { + try { + await Source.findByIdAndDelete(req.params.id).exec(); + res.status(200).send(); + } catch { + req.log.error( + `Error while deleting Source with id ${req.params.id}` + ); + } + }) +); + +export default router; diff --git a/09-express/03-feed-api-router/utils.mjs b/09-express/03-feed-api-router/utils.mjs new file mode 100644 index 0000000..653570e --- /dev/null +++ b/09-express/03-feed-api-router/utils.mjs @@ -0,0 +1,4 @@ +export const wrap = + (fn) => + (...args) => + fn(...args).catch(args[2]); diff --git a/10-fastify/01-essential/01-hello-fastify.mjs b/10-fastify/01-essential/01-hello-fastify.mjs new file mode 100644 index 0000000..43da1cf --- /dev/null +++ b/10-fastify/01-essential/01-hello-fastify.mjs @@ -0,0 +1,16 @@ +import Fastify from "fastify"; +const fastify = Fastify({ + logger: true, +}); + +fastify.get("/", async function handler(request, reply) { + request.log.info("Handling hello world"); + return { hello: "world" }; +}); + +try { + await fastify.listen({ port: 3000 }); +} catch (err) { + fastify.log.error(err); + process.exit(1); +} diff --git a/10-fastify/01-essential/02-fastify-logging.mjs b/10-fastify/01-essential/02-fastify-logging.mjs new file mode 100644 index 0000000..5b05dfb --- /dev/null +++ b/10-fastify/01-essential/02-fastify-logging.mjs @@ -0,0 +1,20 @@ +import Fastify from "fastify"; +const fastify = Fastify({ + logger: { + level: "debug", + }, + requestIdLogLabel: "rid", + requestIdHeader: "X-Request-ID", +}); + +fastify.get("/", async function handler(request, reply) { + request.log.info("Handling hello world"); + return { hello: "world" }; +}); + +try { + await fastify.listen({ port: 3000 }); +} catch (err) { + fastify.log.error(err); + process.exit(1); +} diff --git a/10-fastify/01-essential/03-short-vs-full.mjs b/10-fastify/01-essential/03-short-vs-full.mjs new file mode 100644 index 0000000..92b7ca7 --- /dev/null +++ b/10-fastify/01-essential/03-short-vs-full.mjs @@ -0,0 +1,25 @@ +import Fastify from "fastify"; +const fastify = Fastify({ + logger: true, +}); + +fastify.get("/short", async function handler(request, reply) { + request.log.info("Handling shorthand route"); + return { hello: "short" }; +}); + +fastify.route({ + url: "/full", + method: "GET", + handler: async function handler(request, reply) { + request.log.info("Handling full route"); + return { hello: "full" }; + }, +}); + +try { + await fastify.listen({ port: 3000 }); +} catch (err) { + fastify.log.error(err); + process.exit(1); +} diff --git a/10-fastify/01-essential/04-params.mjs b/10-fastify/01-essential/04-params.mjs new file mode 100644 index 0000000..71bae4d --- /dev/null +++ b/10-fastify/01-essential/04-params.mjs @@ -0,0 +1,18 @@ +import Fastify from "fastify"; +const fastify = Fastify({ + logger: true, +}); + +fastify.get("/hello/:name", async function handler(request, reply) { + return { hello: request.params.name }; +}); +fastify.get("/any/*", async function handler(request, reply) { + return { params: request.params }; +}); + +try { + await fastify.listen({ port: 3000 }); +} catch (err) { + fastify.log.error(err); + process.exit(1); +} diff --git a/10-fastify/01-essential/05-request.mjs b/10-fastify/01-essential/05-request.mjs new file mode 100644 index 0000000..2b125fd --- /dev/null +++ b/10-fastify/01-essential/05-request.mjs @@ -0,0 +1,21 @@ +import Fastify from "fastify"; +const fastify = Fastify({ + logger: true, +}); + +fastify.get("/headers", (request, reply) => { + return { headers: request.headers }; +}); +fastify.get("/query", (request, reply) => { + return { query: request.query }; +}); +fastify.post("/body", async function handler(request, reply) { + return request.body; +}); + +try { + await fastify.listen({ port: 3000 }); +} catch (err) { + fastify.log.error(err); + process.exit(1); +} diff --git a/10-fastify/01-essential/06-reply-send.mjs b/10-fastify/01-essential/06-reply-send.mjs new file mode 100644 index 0000000..0e83fa2 --- /dev/null +++ b/10-fastify/01-essential/06-reply-send.mjs @@ -0,0 +1,28 @@ +import Fastify from "fastify"; +const fastify = Fastify({ + logger: true, +}); + +fastify.get("/return", function handler(request, reply) { + return "return"; +}); +fastify.get("/return-async", function handler(request, reply) { + return "return-async"; +}); +fastify.get("/reply-send", function handler(request, reply) { + reply.send("reply-send"); +}); +fastify.get("/reply-send-async", async function handler(request, reply) { + reply.send("reply-send-async"); + return reply; +}); +fastify.get("/return-promise", function handler(request, reply) { + return Promise.resolve("return-promise"); +}); + +try { + await fastify.listen({ port: 3000 }); +} catch (err) { + fastify.log.error(err); + process.exit(1); +} diff --git a/10-fastify/01-essential/07-reply.mjs b/10-fastify/01-essential/07-reply.mjs new file mode 100644 index 0000000..5817aa5 --- /dev/null +++ b/10-fastify/01-essential/07-reply.mjs @@ -0,0 +1,18 @@ +import Fastify from "fastify"; +const fastify = Fastify({ + logger: true, +}); + +fastify.get("/reply", function handler(request, reply) { + reply + .code(200) + .header("Content-Type", "application/json; charset=utf-8") + .send({ name: "value" }); +}); + +try { + await fastify.listen({ port: 3000 }); +} catch (err) { + fastify.log.error(err); + process.exit(1); +} diff --git a/10-fastify/01-essential/08-errors.mjs b/10-fastify/01-essential/08-errors.mjs new file mode 100644 index 0000000..ad8f09e --- /dev/null +++ b/10-fastify/01-essential/08-errors.mjs @@ -0,0 +1,24 @@ +import Fastify from "fastify"; +const fastify = Fastify({ + logger: true, +}); + +fastify.post("/", function handler(request, reply) { + return "OK"; +}); +fastify.get("/throw", function handler(request, reply) { + throw new Error("Something went wrong"); +}); +fastify.get("/reject", function handler(request, reply) { + return Promise.reject(new Error("Not resolved")); +}); +fastify.get("/await-err", async function handler(request, reply) { + await Promise.reject(new Error("Await error")); +}); + +try { + await fastify.listen({ port: 3000 }); +} catch (err) { + fastify.log.error(err); + process.exit(1); +} diff --git a/10-fastify/01-essential/package-lock.json b/10-fastify/01-essential/package-lock.json new file mode 100644 index 0000000..708e29d --- /dev/null +++ b/10-fastify/01-essential/package-lock.json @@ -0,0 +1,629 @@ +{ + "name": "10-fastify", + "version": "1.0.0", + "lockfileVersion": 3, + "requires": true, + "packages": { + "": { + "name": "10-fastify", + "version": "1.0.0", + "license": "ISC", + "dependencies": { + "fastify": "^4.26.1" + } + }, + "node_modules/@fastify/ajv-compiler": { + "version": "3.5.0", + "resolved": "https://registry.npmjs.org/@fastify/ajv-compiler/-/ajv-compiler-3.5.0.tgz", + "integrity": "sha512-ebbEtlI7dxXF5ziNdr05mOY8NnDiPB1XvAlLHctRt/Rc+C3LCOVW5imUVX+mhvUhnNzmPBHewUkOFgGlCxgdAA==", + "dependencies": { + "ajv": "^8.11.0", + "ajv-formats": "^2.1.1", + "fast-uri": "^2.0.0" + } + }, + "node_modules/@fastify/error": { + "version": "3.4.1", + "resolved": "https://registry.npmjs.org/@fastify/error/-/error-3.4.1.tgz", + "integrity": "sha512-wWSvph+29GR783IhmvdwWnN4bUxTD01Vm5Xad4i7i1VuAOItLvbPAb69sb0IQ2N57yprvhNIwAP5B6xfKTmjmQ==" + }, + "node_modules/@fastify/fast-json-stringify-compiler": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/@fastify/fast-json-stringify-compiler/-/fast-json-stringify-compiler-4.3.0.tgz", + "integrity": "sha512-aZAXGYo6m22Fk1zZzEUKBvut/CIIQe/BapEORnxiD5Qr0kPHqqI69NtEMCme74h+at72sPhbkb4ZrLd1W3KRLA==", + "dependencies": { + "fast-json-stringify": "^5.7.0" + } + }, + "node_modules/@fastify/merge-json-schemas": { + "version": "0.1.1", + "resolved": "https://registry.npmjs.org/@fastify/merge-json-schemas/-/merge-json-schemas-0.1.1.tgz", + "integrity": "sha512-fERDVz7topgNjtXsJTTW1JKLy0rhuLRcquYqNR9rF7OcVpCa2OVW49ZPDIhaRRCaUuvVxI+N416xUoF76HNSXA==", + "dependencies": { + "fast-deep-equal": "^3.1.3" + } + }, + "node_modules/abort-controller": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/abort-controller/-/abort-controller-3.0.0.tgz", + "integrity": "sha512-h8lQ8tacZYnR3vNQTgibj+tODHI5/+l06Au2Pcriv/Gmet0eaj4TwWH41sO9wnHDiQsEj19q0drzdWdeAHtweg==", + "dependencies": { + "event-target-shim": "^5.0.0" + }, + "engines": { + "node": ">=6.5" + } + }, + "node_modules/abstract-logging": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/abstract-logging/-/abstract-logging-2.0.1.tgz", + "integrity": "sha512-2BjRTZxTPvheOvGbBslFSYOUkr+SjPtOnrLP33f+VIWLzezQpZcqVg7ja3L4dBXmzzgwT+a029jRx5PCi3JuiA==" + }, + "node_modules/ajv": { + "version": "8.12.0", + "resolved": "https://registry.npmjs.org/ajv/-/ajv-8.12.0.tgz", + "integrity": "sha512-sRu1kpcO9yLtYxBKvqfTeh9KzZEwO3STyX1HT+4CaDzC6HpTGYhIhPIzj9XuKU7KYDwnaeh5hcOwjy1QuJzBPA==", + "dependencies": { + "fast-deep-equal": "^3.1.1", + "json-schema-traverse": "^1.0.0", + "require-from-string": "^2.0.2", + "uri-js": "^4.2.2" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/epoberezkin" + } + }, + "node_modules/ajv-formats": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/ajv-formats/-/ajv-formats-2.1.1.tgz", + "integrity": "sha512-Wx0Kx52hxE7C18hkMEggYlEifqWZtYaRgouJor+WMdPnQyEK13vgEWyVNup7SoeeoLMsr4kf5h6dOW11I15MUA==", + "dependencies": { + "ajv": "^8.0.0" + }, + "peerDependencies": { + "ajv": "^8.0.0" + }, + "peerDependenciesMeta": { + "ajv": { + "optional": true + } + } + }, + "node_modules/archy": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/archy/-/archy-1.0.0.tgz", + "integrity": "sha512-Xg+9RwCg/0p32teKdGMPTPnVXKD0w3DfHnFTficozsAgsvq2XenPJq/MYpzzQ/v8zrOyJn6Ds39VA4JIDwFfqw==" + }, + "node_modules/atomic-sleep": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/atomic-sleep/-/atomic-sleep-1.0.0.tgz", + "integrity": "sha512-kNOjDqAh7px0XWNI+4QbzoiR/nTkHAWNud2uvnJquD1/x5a7EQZMJT0AczqK0Qn67oY/TTQ1LbUKajZpp3I9tQ==", + "engines": { + "node": ">=8.0.0" + } + }, + "node_modules/avvio": { + "version": "8.3.0", + "resolved": "https://registry.npmjs.org/avvio/-/avvio-8.3.0.tgz", + "integrity": "sha512-VBVH0jubFr9LdFASy/vNtm5giTrnbVquWBhT0fyizuNK2rQ7e7ONU2plZQWUNqtE1EmxFEb+kbSkFRkstiaS9Q==", + "dependencies": { + "@fastify/error": "^3.3.0", + "archy": "^1.0.0", + "debug": "^4.0.0", + "fastq": "^1.17.1" + } + }, + "node_modules/base64-js": { + "version": "1.5.1", + "resolved": "https://registry.npmjs.org/base64-js/-/base64-js-1.5.1.tgz", + "integrity": "sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ] + }, + "node_modules/buffer": { + "version": "6.0.3", + "resolved": "https://registry.npmjs.org/buffer/-/buffer-6.0.3.tgz", + "integrity": "sha512-FTiCpNxtwiZZHEZbcbTIcZjERVICn9yq/pDFkTl95/AxzD1naBctN7YO68riM/gLSDY7sdrMby8hofADYuuqOA==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "dependencies": { + "base64-js": "^1.3.1", + "ieee754": "^1.2.1" + } + }, + "node_modules/cookie": { + "version": "0.6.0", + "resolved": "https://registry.npmjs.org/cookie/-/cookie-0.6.0.tgz", + "integrity": "sha512-U71cyTamuh1CRNCfpGY6to28lxvNwPG4Guz/EVjgf3Jmzv0vlDp1atT9eS5dDjMYHucpHbWns6Lwf3BKz6svdw==", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/debug": { + "version": "4.3.4", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.4.tgz", + "integrity": "sha512-PRWFHuSU3eDtQJPvnNY7Jcket1j0t5OuOsFzPPzsekD52Zl8qUfFIPEiswXqIvHWGVHOgX+7G/vCNNhehwxfkQ==", + "dependencies": { + "ms": "2.1.2" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "node_modules/event-target-shim": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/event-target-shim/-/event-target-shim-5.0.1.tgz", + "integrity": "sha512-i/2XbnSz/uxRCU6+NdVJgKWDTM427+MqYbkQzD321DuCQJUqOuJKIA0IM2+W2xtYHdKOmZ4dR6fExsd4SXL+WQ==", + "engines": { + "node": ">=6" + } + }, + "node_modules/events": { + "version": "3.3.0", + "resolved": "https://registry.npmjs.org/events/-/events-3.3.0.tgz", + "integrity": "sha512-mQw+2fkQbALzQ7V0MY0IqdnXNOeTtP4r0lN9z7AAawCXgqea7bDii20AYrIBrFd/Hx0M2Ocz6S111CaFkUcb0Q==", + "engines": { + "node": ">=0.8.x" + } + }, + "node_modules/fast-content-type-parse": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/fast-content-type-parse/-/fast-content-type-parse-1.1.0.tgz", + "integrity": "sha512-fBHHqSTFLVnR61C+gltJuE5GkVQMV0S2nqUO8TJ+5Z3qAKG8vAx4FKai1s5jq/inV1+sREynIWSuQ6HgoSXpDQ==" + }, + "node_modules/fast-decode-uri-component": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/fast-decode-uri-component/-/fast-decode-uri-component-1.0.1.tgz", + "integrity": "sha512-WKgKWg5eUxvRZGwW8FvfbaH7AXSh2cL+3j5fMGzUMCxWBJ3dV3a7Wz8y2f/uQ0e3B6WmodD3oS54jTQ9HVTIIg==" + }, + "node_modules/fast-deep-equal": { + "version": "3.1.3", + "resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz", + "integrity": "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==" + }, + "node_modules/fast-json-stringify": { + "version": "5.12.0", + "resolved": "https://registry.npmjs.org/fast-json-stringify/-/fast-json-stringify-5.12.0.tgz", + "integrity": "sha512-7Nnm9UPa7SfHRbHVA1kJQrGXCRzB7LMlAAqHXQFkEQqueJm1V8owm0FsE/2Do55/4CcdhwiLQERaKomOnKQkyA==", + "dependencies": { + "@fastify/merge-json-schemas": "^0.1.0", + "ajv": "^8.10.0", + "ajv-formats": "^2.1.1", + "fast-deep-equal": "^3.1.3", + "fast-uri": "^2.1.0", + "json-schema-ref-resolver": "^1.0.1", + "rfdc": "^1.2.0" + } + }, + "node_modules/fast-querystring": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/fast-querystring/-/fast-querystring-1.1.2.tgz", + "integrity": "sha512-g6KuKWmFXc0fID8WWH0jit4g0AGBoJhCkJMb1RmbsSEUNvQ+ZC8D6CUZ+GtF8nMzSPXnhiePyyqqipzNNEnHjg==", + "dependencies": { + "fast-decode-uri-component": "^1.0.1" + } + }, + "node_modules/fast-redact": { + "version": "3.3.0", + "resolved": "https://registry.npmjs.org/fast-redact/-/fast-redact-3.3.0.tgz", + "integrity": "sha512-6T5V1QK1u4oF+ATxs1lWUmlEk6P2T9HqJG3e2DnHOdVgZy2rFJBoEnrIedcTXlkAHU/zKC+7KETJ+KGGKwxgMQ==", + "engines": { + "node": ">=6" + } + }, + "node_modules/fast-uri": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/fast-uri/-/fast-uri-2.3.0.tgz", + "integrity": "sha512-eel5UKGn369gGEWOqBShmFJWfq/xSJvsgDzgLYC845GneayWvXBf0lJCBn5qTABfewy1ZDPoaR5OZCP+kssfuw==" + }, + "node_modules/fastify": { + "version": "4.26.1", + "resolved": "https://registry.npmjs.org/fastify/-/fastify-4.26.1.tgz", + "integrity": "sha512-tznA/G55dsxzM5XChBfcvVSloG2ejeeotfPPJSFaWmHyCDVGMpvf3nRNbsCb/JTBF9RmQFBfuujWt3Nphjesng==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/fastify" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/fastify" + } + ], + "dependencies": { + "@fastify/ajv-compiler": "^3.5.0", + "@fastify/error": "^3.4.0", + "@fastify/fast-json-stringify-compiler": "^4.3.0", + "abstract-logging": "^2.0.1", + "avvio": "^8.3.0", + "fast-content-type-parse": "^1.1.0", + "fast-json-stringify": "^5.8.0", + "find-my-way": "^8.0.0", + "light-my-request": "^5.11.0", + "pino": "^8.17.0", + "process-warning": "^3.0.0", + "proxy-addr": "^2.0.7", + "rfdc": "^1.3.0", + "secure-json-parse": "^2.7.0", + "semver": "^7.5.4", + "toad-cache": "^3.3.0" + } + }, + "node_modules/fastq": { + "version": "1.17.1", + "resolved": "https://registry.npmjs.org/fastq/-/fastq-1.17.1.tgz", + "integrity": "sha512-sRVD3lWVIXWg6By68ZN7vho9a1pQcN/WBFaAAsDDFzlJjvoGx0P8z7V1t72grFJfJhu3YPZBuu25f7Kaw2jN1w==", + "dependencies": { + "reusify": "^1.0.4" + } + }, + "node_modules/find-my-way": { + "version": "8.1.0", + "resolved": "https://registry.npmjs.org/find-my-way/-/find-my-way-8.1.0.tgz", + "integrity": "sha512-41QwjCGcVTODUmLLqTMeoHeiozbMXYMAE1CKFiDyi9zVZ2Vjh0yz3MF0WQZoIb+cmzP/XlbFjlF2NtJmvZHznA==", + "dependencies": { + "fast-deep-equal": "^3.1.3", + "fast-querystring": "^1.0.0", + "safe-regex2": "^2.0.0" + }, + "engines": { + "node": ">=14" + } + }, + "node_modules/forwarded": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/forwarded/-/forwarded-0.2.0.tgz", + "integrity": "sha512-buRG0fpBtRHSTCOASe6hD258tEubFoRLb4ZNA6NxMVHNw2gOcwHo9wyablzMzOA5z9xA9L1KNjk/Nt6MT9aYow==", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/ieee754": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/ieee754/-/ieee754-1.2.1.tgz", + "integrity": "sha512-dcyqhDvX1C46lXZcVqCpK+FtMRQVdIMN6/Df5js2zouUsqG7I6sFxitIC+7KYK29KdXOLHdu9zL4sFnoVQnqaA==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ] + }, + "node_modules/ipaddr.js": { + "version": "1.9.1", + "resolved": "https://registry.npmjs.org/ipaddr.js/-/ipaddr.js-1.9.1.tgz", + "integrity": "sha512-0KI/607xoxSToH7GjN1FfSbLoU0+btTicjsQSWQlh/hZykN8KpmMf7uYwPW3R+akZ6R/w18ZlXSHBYXiYUPO3g==", + "engines": { + "node": ">= 0.10" + } + }, + "node_modules/json-schema-ref-resolver": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/json-schema-ref-resolver/-/json-schema-ref-resolver-1.0.1.tgz", + "integrity": "sha512-EJAj1pgHc1hxF6vo2Z3s69fMjO1INq6eGHXZ8Z6wCQeldCuwxGK9Sxf4/cScGn3FZubCVUehfWtcDM/PLteCQw==", + "dependencies": { + "fast-deep-equal": "^3.1.3" + } + }, + "node_modules/json-schema-traverse": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-1.0.0.tgz", + "integrity": "sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug==" + }, + "node_modules/light-my-request": { + "version": "5.11.1", + "resolved": "https://registry.npmjs.org/light-my-request/-/light-my-request-5.11.1.tgz", + "integrity": "sha512-KXAh2m6VRlkWCk2KfmHE7tLBXKh30JE0tXUJY4dNxje4oLmPKUqlUfImiEQZLphx+Z9KTQcVv4DjGnJxkVOIbA==", + "dependencies": { + "cookie": "^0.6.0", + "process-warning": "^2.0.0", + "set-cookie-parser": "^2.4.1" + } + }, + "node_modules/light-my-request/node_modules/process-warning": { + "version": "2.3.2", + "resolved": "https://registry.npmjs.org/process-warning/-/process-warning-2.3.2.tgz", + "integrity": "sha512-n9wh8tvBe5sFmsqlg+XQhaQLumwpqoAUruLwjCopgTmUBjJ/fjtBsJzKleCaIGBOMXYEhp1YfKl4d7rJ5ZKJGA==" + }, + "node_modules/lru-cache": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-6.0.0.tgz", + "integrity": "sha512-Jo6dJ04CmSjuznwJSS3pUeWmd/H0ffTlkXXgwZi+eq1UCmqQwCh+eLsYOYCwY991i2Fah4h1BEMCx4qThGbsiA==", + "dependencies": { + "yallist": "^4.0.0" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/ms": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz", + "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==" + }, + "node_modules/on-exit-leak-free": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/on-exit-leak-free/-/on-exit-leak-free-2.1.2.tgz", + "integrity": "sha512-0eJJY6hXLGf1udHwfNftBqH+g73EU4B504nZeKpz1sYRKafAghwxEJunB2O7rDZkL4PGfsMVnTXZ2EjibbqcsA==", + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/pino": { + "version": "8.19.0", + "resolved": "https://registry.npmjs.org/pino/-/pino-8.19.0.tgz", + "integrity": "sha512-oswmokxkav9bADfJ2ifrvfHUwad6MLp73Uat0IkQWY3iAw5xTRoznXbXksZs8oaOUMpmhVWD+PZogNzllWpJaA==", + "dependencies": { + "atomic-sleep": "^1.0.0", + "fast-redact": "^3.1.1", + "on-exit-leak-free": "^2.1.0", + "pino-abstract-transport": "v1.1.0", + "pino-std-serializers": "^6.0.0", + "process-warning": "^3.0.0", + "quick-format-unescaped": "^4.0.3", + "real-require": "^0.2.0", + "safe-stable-stringify": "^2.3.1", + "sonic-boom": "^3.7.0", + "thread-stream": "^2.0.0" + }, + "bin": { + "pino": "bin.js" + } + }, + "node_modules/pino-abstract-transport": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/pino-abstract-transport/-/pino-abstract-transport-1.1.0.tgz", + "integrity": "sha512-lsleG3/2a/JIWUtf9Q5gUNErBqwIu1tUKTT3dUzaf5DySw9ra1wcqKjJjLX1VTY64Wk1eEOYsVGSaGfCK85ekA==", + "dependencies": { + "readable-stream": "^4.0.0", + "split2": "^4.0.0" + } + }, + "node_modules/pino-std-serializers": { + "version": "6.2.2", + "resolved": "https://registry.npmjs.org/pino-std-serializers/-/pino-std-serializers-6.2.2.tgz", + "integrity": "sha512-cHjPPsE+vhj/tnhCy/wiMh3M3z3h/j15zHQX+S9GkTBgqJuTuJzYJ4gUyACLhDaJ7kk9ba9iRDmbH2tJU03OiA==" + }, + "node_modules/process": { + "version": "0.11.10", + "resolved": "https://registry.npmjs.org/process/-/process-0.11.10.tgz", + "integrity": "sha512-cdGef/drWFoydD1JsMzuFf8100nZl+GT+yacc2bEced5f9Rjk4z+WtFUTBu9PhOi9j/jfmBPu0mMEY4wIdAF8A==", + "engines": { + "node": ">= 0.6.0" + } + }, + "node_modules/process-warning": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/process-warning/-/process-warning-3.0.0.tgz", + "integrity": "sha512-mqn0kFRl0EoqhnL0GQ0veqFHyIN1yig9RHh/InzORTUiZHFRAur+aMtRkELNwGs9aNwKS6tg/An4NYBPGwvtzQ==" + }, + "node_modules/proxy-addr": { + "version": "2.0.7", + "resolved": "https://registry.npmjs.org/proxy-addr/-/proxy-addr-2.0.7.tgz", + "integrity": "sha512-llQsMLSUDUPT44jdrU/O37qlnifitDP+ZwrmmZcoSKyLKvtZxpyV0n2/bD/N4tBAAZ/gJEdZU7KMraoK1+XYAg==", + "dependencies": { + "forwarded": "0.2.0", + "ipaddr.js": "1.9.1" + }, + "engines": { + "node": ">= 0.10" + } + }, + "node_modules/punycode": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/punycode/-/punycode-2.3.1.tgz", + "integrity": "sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg==", + "engines": { + "node": ">=6" + } + }, + "node_modules/quick-format-unescaped": { + "version": "4.0.4", + "resolved": "https://registry.npmjs.org/quick-format-unescaped/-/quick-format-unescaped-4.0.4.tgz", + "integrity": "sha512-tYC1Q1hgyRuHgloV/YXs2w15unPVh8qfu/qCTfhTYamaw7fyhumKa2yGpdSo87vY32rIclj+4fWYQXUMs9EHvg==" + }, + "node_modules/readable-stream": { + "version": "4.5.2", + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-4.5.2.tgz", + "integrity": "sha512-yjavECdqeZ3GLXNgRXgeQEdz9fvDDkNKyHnbHRFtOr7/LcfgBcmct7t/ET+HaCTqfh06OzoAxrkN/IfjJBVe+g==", + "dependencies": { + "abort-controller": "^3.0.0", + "buffer": "^6.0.3", + "events": "^3.3.0", + "process": "^0.11.10", + "string_decoder": "^1.3.0" + }, + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + } + }, + "node_modules/real-require": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/real-require/-/real-require-0.2.0.tgz", + "integrity": "sha512-57frrGM/OCTLqLOAh0mhVA9VBMHd+9U7Zb2THMGdBUoZVOtGbJzjxsYGDJ3A9AYYCP4hn6y1TVbaOfzWtm5GFg==", + "engines": { + "node": ">= 12.13.0" + } + }, + "node_modules/require-from-string": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/require-from-string/-/require-from-string-2.0.2.tgz", + "integrity": "sha512-Xf0nWe6RseziFMu+Ap9biiUbmplq6S9/p+7w7YXP/JBHhrUDDUhwa+vANyubuqfZWTveU//DYVGsDG7RKL/vEw==", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/ret": { + "version": "0.2.2", + "resolved": "https://registry.npmjs.org/ret/-/ret-0.2.2.tgz", + "integrity": "sha512-M0b3YWQs7R3Z917WRQy1HHA7Ba7D8hvZg6UE5mLykJxQVE2ju0IXbGlaHPPlkY+WN7wFP+wUMXmBFA0aV6vYGQ==", + "engines": { + "node": ">=4" + } + }, + "node_modules/reusify": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/reusify/-/reusify-1.0.4.tgz", + "integrity": "sha512-U9nH88a3fc/ekCF1l0/UP1IosiuIjyTh7hBvXVMHYgVcfGvt897Xguj2UOLDeI5BG2m7/uwyaLVT6fbtCwTyzw==", + "engines": { + "iojs": ">=1.0.0", + "node": ">=0.10.0" + } + }, + "node_modules/rfdc": { + "version": "1.3.1", + "resolved": "https://registry.npmjs.org/rfdc/-/rfdc-1.3.1.tgz", + "integrity": "sha512-r5a3l5HzYlIC68TpmYKlxWjmOP6wiPJ1vWv2HeLhNsRZMrCkxeqxiHlQ21oXmQ4F3SiryXBHhAD7JZqvOJjFmg==" + }, + "node_modules/safe-buffer": { + "version": "5.2.1", + "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.1.tgz", + "integrity": "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ] + }, + "node_modules/safe-regex2": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/safe-regex2/-/safe-regex2-2.0.0.tgz", + "integrity": "sha512-PaUSFsUaNNuKwkBijoAPHAK6/eM6VirvyPWlZ7BAQy4D+hCvh4B6lIG+nPdhbFfIbP+gTGBcrdsOaUs0F+ZBOQ==", + "dependencies": { + "ret": "~0.2.0" + } + }, + "node_modules/safe-stable-stringify": { + "version": "2.4.3", + "resolved": "https://registry.npmjs.org/safe-stable-stringify/-/safe-stable-stringify-2.4.3.tgz", + "integrity": "sha512-e2bDA2WJT0wxseVd4lsDP4+3ONX6HpMXQa1ZhFQ7SU+GjvORCmShbCMltrtIDfkYhVHrOcPtj+KhmDBdPdZD1g==", + "engines": { + "node": ">=10" + } + }, + "node_modules/secure-json-parse": { + "version": "2.7.0", + "resolved": "https://registry.npmjs.org/secure-json-parse/-/secure-json-parse-2.7.0.tgz", + "integrity": "sha512-6aU+Rwsezw7VR8/nyvKTx8QpWH9FrcYiXXlqC4z5d5XQBDRqtbfsRjnwGyqbi3gddNtWHuEk9OANUotL26qKUw==" + }, + "node_modules/semver": { + "version": "7.6.0", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.6.0.tgz", + "integrity": "sha512-EnwXhrlwXMk9gKu5/flx5sv/an57AkRplG3hTK68W7FRDN+k+OWBj65M7719OkA82XLBxrcX0KSHj+X5COhOVg==", + "dependencies": { + "lru-cache": "^6.0.0" + }, + "bin": { + "semver": "bin/semver.js" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/set-cookie-parser": { + "version": "2.6.0", + "resolved": "https://registry.npmjs.org/set-cookie-parser/-/set-cookie-parser-2.6.0.tgz", + "integrity": "sha512-RVnVQxTXuerk653XfuliOxBP81Sf0+qfQE73LIYKcyMYHG94AuH0kgrQpRDuTZnSmjpysHmzxJXKNfa6PjFhyQ==" + }, + "node_modules/sonic-boom": { + "version": "3.8.0", + "resolved": "https://registry.npmjs.org/sonic-boom/-/sonic-boom-3.8.0.tgz", + "integrity": "sha512-ybz6OYOUjoQQCQ/i4LU8kaToD8ACtYP+Cj5qd2AO36bwbdewxWJ3ArmJ2cr6AvxlL2o0PqnCcPGUgkILbfkaCA==", + "dependencies": { + "atomic-sleep": "^1.0.0" + } + }, + "node_modules/split2": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/split2/-/split2-4.2.0.tgz", + "integrity": "sha512-UcjcJOWknrNkF6PLX83qcHM6KHgVKNkV62Y8a5uYDVv9ydGQVwAHMKqHdJje1VTWpljG0WYpCDhrCdAOYH4TWg==", + "engines": { + "node": ">= 10.x" + } + }, + "node_modules/string_decoder": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.3.0.tgz", + "integrity": "sha512-hkRX8U1WjJFd8LsDJ2yQ/wWWxaopEsABU1XfkM8A+j0+85JAGppt16cr1Whg6KIbb4okU6Mql6BOj+uup/wKeA==", + "dependencies": { + "safe-buffer": "~5.2.0" + } + }, + "node_modules/thread-stream": { + "version": "2.4.1", + "resolved": "https://registry.npmjs.org/thread-stream/-/thread-stream-2.4.1.tgz", + "integrity": "sha512-d/Ex2iWd1whipbT681JmTINKw0ZwOUBZm7+Gjs64DHuX34mmw8vJL2bFAaNacaW72zYiTJxSHi5abUuOi5nsfg==", + "dependencies": { + "real-require": "^0.2.0" + } + }, + "node_modules/toad-cache": { + "version": "3.7.0", + "resolved": "https://registry.npmjs.org/toad-cache/-/toad-cache-3.7.0.tgz", + "integrity": "sha512-/m8M+2BJUpoJdgAHoG+baCwBT+tf2VraSfkBgl0Y00qIWt41DJ8R5B8nsEw0I58YwF5IZH6z24/2TobDKnqSWw==", + "engines": { + "node": ">=12" + } + }, + "node_modules/uri-js": { + "version": "4.4.1", + "resolved": "https://registry.npmjs.org/uri-js/-/uri-js-4.4.1.tgz", + "integrity": "sha512-7rKUyy33Q1yc98pQ1DAmLtwX109F7TIfWlW1Ydo8Wl1ii1SeHieeh0HHfPeL2fMXK6z0s8ecKs9frCuLJvndBg==", + "dependencies": { + "punycode": "^2.1.0" + } + }, + "node_modules/yallist": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/yallist/-/yallist-4.0.0.tgz", + "integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==" + } + } +} diff --git a/10-fastify/01-essential/package.json b/10-fastify/01-essential/package.json new file mode 100644 index 0000000..974ddc7 --- /dev/null +++ b/10-fastify/01-essential/package.json @@ -0,0 +1,15 @@ +{ + "name": "10-fastify", + "version": "1.0.0", + "description": "", + "main": "index.js", + "scripts": { + "test": "echo \"Error: no test specified\" && exit 1" + }, + "keywords": [], + "author": "", + "license": "ISC", + "dependencies": { + "fastify": "^4.26.1" + } +} diff --git a/10-fastify/02-feed-api-fast/get-feeds.mjs b/10-fastify/02-feed-api-fast/get-feeds.mjs new file mode 100644 index 0000000..3ddd3b3 --- /dev/null +++ b/10-fastify/02-feed-api-fast/get-feeds.mjs @@ -0,0 +1,25 @@ +import { parseFeed } from "htmlparser2"; +import { compareDesc } from "date-fns"; + +export default async function getFeedEntries(url, maxEntries = 5) { + const feedFetched = await fetch(url); + if (!feedFetched.ok) { + throw new Error("Feed fetch failed"); + } + const feedContent = await feedFetched.text(); + const { items } = parseFeed(feedContent); + return items.slice(0, maxEntries); +} + +export async function getLatestEntries(urls) { + const feedsFetches = urls.map((s) => getFeedEntries(s)); + const fetchesPromises = await Promise.allSettled(feedsFetches); + const retrievedFeeds = fetchesPromises + .filter((r) => r.status === "fulfilled") + .map((r) => r.value); + const latestItems = retrievedFeeds.flat().map((i) => { + return { title: i.title, link: i.link, date: i.pubDate }; + }); + latestItems.sort((a, b) => compareDesc(a.date, b.date)); + return latestItems; +} diff --git a/10-fastify/02-feed-api-fast/models/FeedEntry.mjs b/10-fastify/02-feed-api-fast/models/FeedEntry.mjs new file mode 100644 index 0000000..460312e --- /dev/null +++ b/10-fastify/02-feed-api-fast/models/FeedEntry.mjs @@ -0,0 +1,12 @@ +import mongoose from "mongoose"; +const { Schema } = mongoose; + +const feedEntrySchema = new Schema({ + source: { type: Schema.Types.ObjectId, ref: "Source" }, + title: { type: String }, + link: { type: String, unique: true }, + date: { type: Date }, +}); + +const FeedEntry = mongoose.model("FeedEntry", feedEntrySchema); +export { FeedEntry }; diff --git a/10-fastify/02-feed-api-fast/models/Source.mjs b/10-fastify/02-feed-api-fast/models/Source.mjs new file mode 100644 index 0000000..f358052 --- /dev/null +++ b/10-fastify/02-feed-api-fast/models/Source.mjs @@ -0,0 +1,36 @@ +import mongoose from "mongoose"; +const { Schema } = mongoose; + +const sourceSchema = new Schema({ + url: { + type: String, + required: true, + unique: true, + validate: { + validator: function (v) { + try { + const url = new URL(v); + if (!url.protocol.startsWith("http")) { + return false; + } + } catch { + return false; + } + return true; + }, + message: (props) => `${props.value} is not a valid URL`, + }, + }, +}); +sourceSchema.static("getErrorReason", (e) => { + if (e.name === "ValidationError") { + return "Source validation failed"; + } else if (e.code === 11000) { + return "Feed already exists"; + } else { + return null; + } +}); + +const Source = mongoose.model("Source", sourceSchema); +export { Source }; diff --git a/10-fastify/02-feed-api-fast/package-lock.json b/10-fastify/02-feed-api-fast/package-lock.json new file mode 100644 index 0000000..ff9e9dc --- /dev/null +++ b/10-fastify/02-feed-api-fast/package-lock.json @@ -0,0 +1,1201 @@ +{ + "name": "02-feed-api-fast", + "version": "1.0.0", + "lockfileVersion": 3, + "requires": true, + "packages": { + "": { + "name": "02-feed-api-fast", + "version": "1.0.0", + "license": "ISC", + "dependencies": { + "@fastify/compress": "^7.0.0", + "date-fns": "^3.3.1", + "fastify": "^4.26.1", + "htmlparser2": "^9.1.0", + "mongoose": "^8.2.0" + } + }, + "node_modules/@fastify/accept-negotiator": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@fastify/accept-negotiator/-/accept-negotiator-1.1.0.tgz", + "integrity": "sha512-OIHZrb2ImZ7XG85HXOONLcJWGosv7sIvM2ifAPQVhg9Lv7qdmMBNVaai4QTdyuaqbKM5eO6sLSQOYI7wEQeCJQ==", + "engines": { + "node": ">=14" + } + }, + "node_modules/@fastify/ajv-compiler": { + "version": "3.5.0", + "resolved": "https://registry.npmjs.org/@fastify/ajv-compiler/-/ajv-compiler-3.5.0.tgz", + "integrity": "sha512-ebbEtlI7dxXF5ziNdr05mOY8NnDiPB1XvAlLHctRt/Rc+C3LCOVW5imUVX+mhvUhnNzmPBHewUkOFgGlCxgdAA==", + "dependencies": { + "ajv": "^8.11.0", + "ajv-formats": "^2.1.1", + "fast-uri": "^2.0.0" + } + }, + "node_modules/@fastify/compress": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/@fastify/compress/-/compress-7.0.0.tgz", + "integrity": "sha512-jo/NaBVHP1OXIf8Kmr3bZyYQB0gAIgcy5c8rRKTPjhklHO7lRs/6ZFckUVT0NtbKSvrTuIcmSkxYpjyv3FNHXA==", + "dependencies": { + "@fastify/accept-negotiator": "^1.1.0", + "fastify-plugin": "^4.5.0", + "into-stream": "^6.0.0", + "mime-db": "^1.52.0", + "minipass": "^7.0.2", + "peek-stream": "^1.1.3", + "pump": "^3.0.0", + "pumpify": "^2.0.1" + } + }, + "node_modules/@fastify/error": { + "version": "3.4.1", + "resolved": "https://registry.npmjs.org/@fastify/error/-/error-3.4.1.tgz", + "integrity": "sha512-wWSvph+29GR783IhmvdwWnN4bUxTD01Vm5Xad4i7i1VuAOItLvbPAb69sb0IQ2N57yprvhNIwAP5B6xfKTmjmQ==" + }, + "node_modules/@fastify/fast-json-stringify-compiler": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/@fastify/fast-json-stringify-compiler/-/fast-json-stringify-compiler-4.3.0.tgz", + "integrity": "sha512-aZAXGYo6m22Fk1zZzEUKBvut/CIIQe/BapEORnxiD5Qr0kPHqqI69NtEMCme74h+at72sPhbkb4ZrLd1W3KRLA==", + "dependencies": { + "fast-json-stringify": "^5.7.0" + } + }, + "node_modules/@fastify/merge-json-schemas": { + "version": "0.1.1", + "resolved": "https://registry.npmjs.org/@fastify/merge-json-schemas/-/merge-json-schemas-0.1.1.tgz", + "integrity": "sha512-fERDVz7topgNjtXsJTTW1JKLy0rhuLRcquYqNR9rF7OcVpCa2OVW49ZPDIhaRRCaUuvVxI+N416xUoF76HNSXA==", + "dependencies": { + "fast-deep-equal": "^3.1.3" + } + }, + "node_modules/@mongodb-js/saslprep": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/@mongodb-js/saslprep/-/saslprep-1.1.4.tgz", + "integrity": "sha512-8zJ8N1x51xo9hwPh6AWnKdLGEC5N3lDa6kms1YHmFBoRhTpJR6HG8wWk0td1MVCu9cD4YBrvjZEtd5Obw0Fbnw==", + "dependencies": { + "sparse-bitfield": "^3.0.3" + } + }, + "node_modules/@types/webidl-conversions": { + "version": "7.0.3", + "resolved": "https://registry.npmjs.org/@types/webidl-conversions/-/webidl-conversions-7.0.3.tgz", + "integrity": "sha512-CiJJvcRtIgzadHCYXw7dqEnMNRjhGZlYK05Mj9OyktqV8uVT8fD2BFOB7S1uwBE3Kj2Z+4UyPmFw/Ixgw/LAlA==" + }, + "node_modules/@types/whatwg-url": { + "version": "11.0.4", + "resolved": "https://registry.npmjs.org/@types/whatwg-url/-/whatwg-url-11.0.4.tgz", + "integrity": "sha512-lXCmTWSHJvf0TRSO58nm978b8HJ/EdsSsEKLd3ODHFjo+3VGAyyTp4v50nWvwtzBxSMQrVOK7tcuN0zGPLICMw==", + "dependencies": { + "@types/webidl-conversions": "*" + } + }, + "node_modules/abort-controller": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/abort-controller/-/abort-controller-3.0.0.tgz", + "integrity": "sha512-h8lQ8tacZYnR3vNQTgibj+tODHI5/+l06Au2Pcriv/Gmet0eaj4TwWH41sO9wnHDiQsEj19q0drzdWdeAHtweg==", + "dependencies": { + "event-target-shim": "^5.0.0" + }, + "engines": { + "node": ">=6.5" + } + }, + "node_modules/abstract-logging": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/abstract-logging/-/abstract-logging-2.0.1.tgz", + "integrity": "sha512-2BjRTZxTPvheOvGbBslFSYOUkr+SjPtOnrLP33f+VIWLzezQpZcqVg7ja3L4dBXmzzgwT+a029jRx5PCi3JuiA==" + }, + "node_modules/ajv": { + "version": "8.12.0", + "resolved": "https://registry.npmjs.org/ajv/-/ajv-8.12.0.tgz", + "integrity": "sha512-sRu1kpcO9yLtYxBKvqfTeh9KzZEwO3STyX1HT+4CaDzC6HpTGYhIhPIzj9XuKU7KYDwnaeh5hcOwjy1QuJzBPA==", + "dependencies": { + "fast-deep-equal": "^3.1.1", + "json-schema-traverse": "^1.0.0", + "require-from-string": "^2.0.2", + "uri-js": "^4.2.2" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/epoberezkin" + } + }, + "node_modules/ajv-formats": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/ajv-formats/-/ajv-formats-2.1.1.tgz", + "integrity": "sha512-Wx0Kx52hxE7C18hkMEggYlEifqWZtYaRgouJor+WMdPnQyEK13vgEWyVNup7SoeeoLMsr4kf5h6dOW11I15MUA==", + "dependencies": { + "ajv": "^8.0.0" + }, + "peerDependencies": { + "ajv": "^8.0.0" + }, + "peerDependenciesMeta": { + "ajv": { + "optional": true + } + } + }, + "node_modules/archy": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/archy/-/archy-1.0.0.tgz", + "integrity": "sha512-Xg+9RwCg/0p32teKdGMPTPnVXKD0w3DfHnFTficozsAgsvq2XenPJq/MYpzzQ/v8zrOyJn6Ds39VA4JIDwFfqw==" + }, + "node_modules/atomic-sleep": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/atomic-sleep/-/atomic-sleep-1.0.0.tgz", + "integrity": "sha512-kNOjDqAh7px0XWNI+4QbzoiR/nTkHAWNud2uvnJquD1/x5a7EQZMJT0AczqK0Qn67oY/TTQ1LbUKajZpp3I9tQ==", + "engines": { + "node": ">=8.0.0" + } + }, + "node_modules/avvio": { + "version": "8.3.0", + "resolved": "https://registry.npmjs.org/avvio/-/avvio-8.3.0.tgz", + "integrity": "sha512-VBVH0jubFr9LdFASy/vNtm5giTrnbVquWBhT0fyizuNK2rQ7e7ONU2plZQWUNqtE1EmxFEb+kbSkFRkstiaS9Q==", + "dependencies": { + "@fastify/error": "^3.3.0", + "archy": "^1.0.0", + "debug": "^4.0.0", + "fastq": "^1.17.1" + } + }, + "node_modules/base64-js": { + "version": "1.5.1", + "resolved": "https://registry.npmjs.org/base64-js/-/base64-js-1.5.1.tgz", + "integrity": "sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ] + }, + "node_modules/bson": { + "version": "6.3.0", + "resolved": "https://registry.npmjs.org/bson/-/bson-6.3.0.tgz", + "integrity": "sha512-balJfqwwTBddxfnidJZagCBPP/f48zj9Sdp3OJswREOgsJzHiQSaOIAtApSgDQFYgHqAvFkp53AFSqjMDZoTFw==", + "engines": { + "node": ">=16.20.1" + } + }, + "node_modules/buffer": { + "version": "6.0.3", + "resolved": "https://registry.npmjs.org/buffer/-/buffer-6.0.3.tgz", + "integrity": "sha512-FTiCpNxtwiZZHEZbcbTIcZjERVICn9yq/pDFkTl95/AxzD1naBctN7YO68riM/gLSDY7sdrMby8hofADYuuqOA==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "dependencies": { + "base64-js": "^1.3.1", + "ieee754": "^1.2.1" + } + }, + "node_modules/buffer-from": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/buffer-from/-/buffer-from-1.1.2.tgz", + "integrity": "sha512-E+XQCRwSbaaiChtv6k6Dwgc+bx+Bs6vuKJHHl5kox/BaKbhiXzqQOwK4cO22yElGp2OCmjwVhT3HmxgyPGnJfQ==" + }, + "node_modules/cookie": { + "version": "0.6.0", + "resolved": "https://registry.npmjs.org/cookie/-/cookie-0.6.0.tgz", + "integrity": "sha512-U71cyTamuh1CRNCfpGY6to28lxvNwPG4Guz/EVjgf3Jmzv0vlDp1atT9eS5dDjMYHucpHbWns6Lwf3BKz6svdw==", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/core-util-is": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/core-util-is/-/core-util-is-1.0.3.tgz", + "integrity": "sha512-ZQBvi1DcpJ4GDqanjucZ2Hj3wEO5pZDS89BWbkcrvdxksJorwUDDZamX9ldFkp9aw2lmBDLgkObEA4DWNJ9FYQ==" + }, + "node_modules/date-fns": { + "version": "3.3.1", + "resolved": "https://registry.npmjs.org/date-fns/-/date-fns-3.3.1.tgz", + "integrity": "sha512-y8e109LYGgoQDveiEBD3DYXKba1jWf5BA8YU1FL5Tvm0BTdEfy54WLCwnuYWZNnzzvALy/QQ4Hov+Q9RVRv+Zw==", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/kossnocorp" + } + }, + "node_modules/debug": { + "version": "4.3.4", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.4.tgz", + "integrity": "sha512-PRWFHuSU3eDtQJPvnNY7Jcket1j0t5OuOsFzPPzsekD52Zl8qUfFIPEiswXqIvHWGVHOgX+7G/vCNNhehwxfkQ==", + "dependencies": { + "ms": "2.1.2" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "node_modules/dom-serializer": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/dom-serializer/-/dom-serializer-2.0.0.tgz", + "integrity": "sha512-wIkAryiqt/nV5EQKqQpo3SToSOV9J0DnbJqwK7Wv/Trc92zIAYZ4FlMu+JPFW1DfGFt81ZTCGgDEabffXeLyJg==", + "dependencies": { + "domelementtype": "^2.3.0", + "domhandler": "^5.0.2", + "entities": "^4.2.0" + }, + "funding": { + "url": "https://github.com/cheeriojs/dom-serializer?sponsor=1" + } + }, + "node_modules/domelementtype": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/domelementtype/-/domelementtype-2.3.0.tgz", + "integrity": "sha512-OLETBj6w0OsagBwdXnPdN0cnMfF9opN69co+7ZrbfPGrdpPVNBUj02spi6B1N7wChLQiPn4CSH/zJvXw56gmHw==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/fb55" + } + ] + }, + "node_modules/domhandler": { + "version": "5.0.3", + "resolved": "https://registry.npmjs.org/domhandler/-/domhandler-5.0.3.tgz", + "integrity": "sha512-cgwlv/1iFQiFnU96XXgROh8xTeetsnJiDsTc7TYCLFd9+/WNkIqPTxiM/8pSd8VIrhXGTf1Ny1q1hquVqDJB5w==", + "dependencies": { + "domelementtype": "^2.3.0" + }, + "engines": { + "node": ">= 4" + }, + "funding": { + "url": "https://github.com/fb55/domhandler?sponsor=1" + } + }, + "node_modules/domutils": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/domutils/-/domutils-3.1.0.tgz", + "integrity": "sha512-H78uMmQtI2AhgDJjWeQmHwJJ2bLPD3GMmO7Zja/ZZh84wkm+4ut+IUnUdRa8uCGX88DiVx1j6FRe1XfxEgjEZA==", + "dependencies": { + "dom-serializer": "^2.0.0", + "domelementtype": "^2.3.0", + "domhandler": "^5.0.3" + }, + "funding": { + "url": "https://github.com/fb55/domutils?sponsor=1" + } + }, + "node_modules/duplexify": { + "version": "3.7.1", + "resolved": "https://registry.npmjs.org/duplexify/-/duplexify-3.7.1.tgz", + "integrity": "sha512-07z8uv2wMyS51kKhD1KsdXJg5WQ6t93RneqRxUHnskXVtlYYkLqM0gqStQZ3pj073g687jPCHrqNfCzawLYh5g==", + "dependencies": { + "end-of-stream": "^1.0.0", + "inherits": "^2.0.1", + "readable-stream": "^2.0.0", + "stream-shift": "^1.0.0" + } + }, + "node_modules/duplexify/node_modules/readable-stream": { + "version": "2.3.8", + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-2.3.8.tgz", + "integrity": "sha512-8p0AUk4XODgIewSi0l8Epjs+EVnWiK7NoDIEGU0HhE7+ZyY8D1IMY7odu5lRrFXGg71L15KG8QrPmum45RTtdA==", + "dependencies": { + "core-util-is": "~1.0.0", + "inherits": "~2.0.3", + "isarray": "~1.0.0", + "process-nextick-args": "~2.0.0", + "safe-buffer": "~5.1.1", + "string_decoder": "~1.1.1", + "util-deprecate": "~1.0.1" + } + }, + "node_modules/duplexify/node_modules/safe-buffer": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.2.tgz", + "integrity": "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==" + }, + "node_modules/duplexify/node_modules/string_decoder": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.1.1.tgz", + "integrity": "sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg==", + "dependencies": { + "safe-buffer": "~5.1.0" + } + }, + "node_modules/end-of-stream": { + "version": "1.4.4", + "resolved": "https://registry.npmjs.org/end-of-stream/-/end-of-stream-1.4.4.tgz", + "integrity": "sha512-+uw1inIHVPQoaVuHzRyXd21icM+cnt4CzD5rW+NC1wjOUSTOs+Te7FOv7AhN7vS9x/oIyhLP5PR1H+phQAHu5Q==", + "dependencies": { + "once": "^1.4.0" + } + }, + "node_modules/entities": { + "version": "4.5.0", + "resolved": "https://registry.npmjs.org/entities/-/entities-4.5.0.tgz", + "integrity": "sha512-V0hjH4dGPh9Ao5p0MoRY6BVqtwCjhz6vI5LT8AJ55H+4g9/4vbHx1I54fS0XuclLhDHArPQCiMjDxjaL8fPxhw==", + "engines": { + "node": ">=0.12" + }, + "funding": { + "url": "https://github.com/fb55/entities?sponsor=1" + } + }, + "node_modules/event-target-shim": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/event-target-shim/-/event-target-shim-5.0.1.tgz", + "integrity": "sha512-i/2XbnSz/uxRCU6+NdVJgKWDTM427+MqYbkQzD321DuCQJUqOuJKIA0IM2+W2xtYHdKOmZ4dR6fExsd4SXL+WQ==", + "engines": { + "node": ">=6" + } + }, + "node_modules/events": { + "version": "3.3.0", + "resolved": "https://registry.npmjs.org/events/-/events-3.3.0.tgz", + "integrity": "sha512-mQw+2fkQbALzQ7V0MY0IqdnXNOeTtP4r0lN9z7AAawCXgqea7bDii20AYrIBrFd/Hx0M2Ocz6S111CaFkUcb0Q==", + "engines": { + "node": ">=0.8.x" + } + }, + "node_modules/fast-content-type-parse": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/fast-content-type-parse/-/fast-content-type-parse-1.1.0.tgz", + "integrity": "sha512-fBHHqSTFLVnR61C+gltJuE5GkVQMV0S2nqUO8TJ+5Z3qAKG8vAx4FKai1s5jq/inV1+sREynIWSuQ6HgoSXpDQ==" + }, + "node_modules/fast-decode-uri-component": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/fast-decode-uri-component/-/fast-decode-uri-component-1.0.1.tgz", + "integrity": "sha512-WKgKWg5eUxvRZGwW8FvfbaH7AXSh2cL+3j5fMGzUMCxWBJ3dV3a7Wz8y2f/uQ0e3B6WmodD3oS54jTQ9HVTIIg==" + }, + "node_modules/fast-deep-equal": { + "version": "3.1.3", + "resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz", + "integrity": "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==" + }, + "node_modules/fast-json-stringify": { + "version": "5.12.0", + "resolved": "https://registry.npmjs.org/fast-json-stringify/-/fast-json-stringify-5.12.0.tgz", + "integrity": "sha512-7Nnm9UPa7SfHRbHVA1kJQrGXCRzB7LMlAAqHXQFkEQqueJm1V8owm0FsE/2Do55/4CcdhwiLQERaKomOnKQkyA==", + "dependencies": { + "@fastify/merge-json-schemas": "^0.1.0", + "ajv": "^8.10.0", + "ajv-formats": "^2.1.1", + "fast-deep-equal": "^3.1.3", + "fast-uri": "^2.1.0", + "json-schema-ref-resolver": "^1.0.1", + "rfdc": "^1.2.0" + } + }, + "node_modules/fast-querystring": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/fast-querystring/-/fast-querystring-1.1.2.tgz", + "integrity": "sha512-g6KuKWmFXc0fID8WWH0jit4g0AGBoJhCkJMb1RmbsSEUNvQ+ZC8D6CUZ+GtF8nMzSPXnhiePyyqqipzNNEnHjg==", + "dependencies": { + "fast-decode-uri-component": "^1.0.1" + } + }, + "node_modules/fast-redact": { + "version": "3.3.0", + "resolved": "https://registry.npmjs.org/fast-redact/-/fast-redact-3.3.0.tgz", + "integrity": "sha512-6T5V1QK1u4oF+ATxs1lWUmlEk6P2T9HqJG3e2DnHOdVgZy2rFJBoEnrIedcTXlkAHU/zKC+7KETJ+KGGKwxgMQ==", + "engines": { + "node": ">=6" + } + }, + "node_modules/fast-uri": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/fast-uri/-/fast-uri-2.3.0.tgz", + "integrity": "sha512-eel5UKGn369gGEWOqBShmFJWfq/xSJvsgDzgLYC845GneayWvXBf0lJCBn5qTABfewy1ZDPoaR5OZCP+kssfuw==" + }, + "node_modules/fastify": { + "version": "4.26.1", + "resolved": "https://registry.npmjs.org/fastify/-/fastify-4.26.1.tgz", + "integrity": "sha512-tznA/G55dsxzM5XChBfcvVSloG2ejeeotfPPJSFaWmHyCDVGMpvf3nRNbsCb/JTBF9RmQFBfuujWt3Nphjesng==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/fastify" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/fastify" + } + ], + "dependencies": { + "@fastify/ajv-compiler": "^3.5.0", + "@fastify/error": "^3.4.0", + "@fastify/fast-json-stringify-compiler": "^4.3.0", + "abstract-logging": "^2.0.1", + "avvio": "^8.3.0", + "fast-content-type-parse": "^1.1.0", + "fast-json-stringify": "^5.8.0", + "find-my-way": "^8.0.0", + "light-my-request": "^5.11.0", + "pino": "^8.17.0", + "process-warning": "^3.0.0", + "proxy-addr": "^2.0.7", + "rfdc": "^1.3.0", + "secure-json-parse": "^2.7.0", + "semver": "^7.5.4", + "toad-cache": "^3.3.0" + } + }, + "node_modules/fastify-plugin": { + "version": "4.5.1", + "resolved": "https://registry.npmjs.org/fastify-plugin/-/fastify-plugin-4.5.1.tgz", + "integrity": "sha512-stRHYGeuqpEZTL1Ef0Ovr2ltazUT9g844X5z/zEBFLG8RYlpDiOCIG+ATvYEp+/zmc7sN29mcIMp8gvYplYPIQ==" + }, + "node_modules/fastq": { + "version": "1.17.1", + "resolved": "https://registry.npmjs.org/fastq/-/fastq-1.17.1.tgz", + "integrity": "sha512-sRVD3lWVIXWg6By68ZN7vho9a1pQcN/WBFaAAsDDFzlJjvoGx0P8z7V1t72grFJfJhu3YPZBuu25f7Kaw2jN1w==", + "dependencies": { + "reusify": "^1.0.4" + } + }, + "node_modules/find-my-way": { + "version": "8.1.0", + "resolved": "https://registry.npmjs.org/find-my-way/-/find-my-way-8.1.0.tgz", + "integrity": "sha512-41QwjCGcVTODUmLLqTMeoHeiozbMXYMAE1CKFiDyi9zVZ2Vjh0yz3MF0WQZoIb+cmzP/XlbFjlF2NtJmvZHznA==", + "dependencies": { + "fast-deep-equal": "^3.1.3", + "fast-querystring": "^1.0.0", + "safe-regex2": "^2.0.0" + }, + "engines": { + "node": ">=14" + } + }, + "node_modules/forwarded": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/forwarded/-/forwarded-0.2.0.tgz", + "integrity": "sha512-buRG0fpBtRHSTCOASe6hD258tEubFoRLb4ZNA6NxMVHNw2gOcwHo9wyablzMzOA5z9xA9L1KNjk/Nt6MT9aYow==", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/from2": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/from2/-/from2-2.3.0.tgz", + "integrity": "sha512-OMcX/4IC/uqEPVgGeyfN22LJk6AZrMkRZHxcHBMBvHScDGgwTm2GT2Wkgtocyd3JfZffjj2kYUDXXII0Fk9W0g==", + "dependencies": { + "inherits": "^2.0.1", + "readable-stream": "^2.0.0" + } + }, + "node_modules/from2/node_modules/readable-stream": { + "version": "2.3.8", + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-2.3.8.tgz", + "integrity": "sha512-8p0AUk4XODgIewSi0l8Epjs+EVnWiK7NoDIEGU0HhE7+ZyY8D1IMY7odu5lRrFXGg71L15KG8QrPmum45RTtdA==", + "dependencies": { + "core-util-is": "~1.0.0", + "inherits": "~2.0.3", + "isarray": "~1.0.0", + "process-nextick-args": "~2.0.0", + "safe-buffer": "~5.1.1", + "string_decoder": "~1.1.1", + "util-deprecate": "~1.0.1" + } + }, + "node_modules/from2/node_modules/safe-buffer": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.2.tgz", + "integrity": "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==" + }, + "node_modules/from2/node_modules/string_decoder": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.1.1.tgz", + "integrity": "sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg==", + "dependencies": { + "safe-buffer": "~5.1.0" + } + }, + "node_modules/htmlparser2": { + "version": "9.1.0", + "resolved": "https://registry.npmjs.org/htmlparser2/-/htmlparser2-9.1.0.tgz", + "integrity": "sha512-5zfg6mHUoaer/97TxnGpxmbR7zJtPwIYFMZ/H5ucTlPZhKvtum05yiPK3Mgai3a0DyVxv7qYqoweaEd2nrYQzQ==", + "funding": [ + "https://github.com/fb55/htmlparser2?sponsor=1", + { + "type": "github", + "url": "https://github.com/sponsors/fb55" + } + ], + "dependencies": { + "domelementtype": "^2.3.0", + "domhandler": "^5.0.3", + "domutils": "^3.1.0", + "entities": "^4.5.0" + } + }, + "node_modules/ieee754": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/ieee754/-/ieee754-1.2.1.tgz", + "integrity": "sha512-dcyqhDvX1C46lXZcVqCpK+FtMRQVdIMN6/Df5js2zouUsqG7I6sFxitIC+7KYK29KdXOLHdu9zL4sFnoVQnqaA==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ] + }, + "node_modules/inherits": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz", + "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==" + }, + "node_modules/into-stream": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/into-stream/-/into-stream-6.0.0.tgz", + "integrity": "sha512-XHbaOAvP+uFKUFsOgoNPRjLkwB+I22JFPFe5OjTkQ0nwgj6+pSjb4NmB6VMxaPshLiOf+zcpOCBQuLwC1KHhZA==", + "dependencies": { + "from2": "^2.3.0", + "p-is-promise": "^3.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/ipaddr.js": { + "version": "1.9.1", + "resolved": "https://registry.npmjs.org/ipaddr.js/-/ipaddr.js-1.9.1.tgz", + "integrity": "sha512-0KI/607xoxSToH7GjN1FfSbLoU0+btTicjsQSWQlh/hZykN8KpmMf7uYwPW3R+akZ6R/w18ZlXSHBYXiYUPO3g==", + "engines": { + "node": ">= 0.10" + } + }, + "node_modules/isarray": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/isarray/-/isarray-1.0.0.tgz", + "integrity": "sha512-VLghIWNM6ELQzo7zwmcg0NmTVyWKYjvIeM83yjp0wRDTmUnrM678fQbcKBo6n2CJEF0szoG//ytg+TKla89ALQ==" + }, + "node_modules/json-schema-ref-resolver": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/json-schema-ref-resolver/-/json-schema-ref-resolver-1.0.1.tgz", + "integrity": "sha512-EJAj1pgHc1hxF6vo2Z3s69fMjO1INq6eGHXZ8Z6wCQeldCuwxGK9Sxf4/cScGn3FZubCVUehfWtcDM/PLteCQw==", + "dependencies": { + "fast-deep-equal": "^3.1.3" + } + }, + "node_modules/json-schema-traverse": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-1.0.0.tgz", + "integrity": "sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug==" + }, + "node_modules/kareem": { + "version": "2.5.1", + "resolved": "https://registry.npmjs.org/kareem/-/kareem-2.5.1.tgz", + "integrity": "sha512-7jFxRVm+jD+rkq3kY0iZDJfsO2/t4BBPeEb2qKn2lR/9KhuksYk5hxzfRYWMPV8P/x2d0kHD306YyWLzjjH+uA==", + "engines": { + "node": ">=12.0.0" + } + }, + "node_modules/light-my-request": { + "version": "5.11.1", + "resolved": "https://registry.npmjs.org/light-my-request/-/light-my-request-5.11.1.tgz", + "integrity": "sha512-KXAh2m6VRlkWCk2KfmHE7tLBXKh30JE0tXUJY4dNxje4oLmPKUqlUfImiEQZLphx+Z9KTQcVv4DjGnJxkVOIbA==", + "dependencies": { + "cookie": "^0.6.0", + "process-warning": "^2.0.0", + "set-cookie-parser": "^2.4.1" + } + }, + "node_modules/light-my-request/node_modules/process-warning": { + "version": "2.3.2", + "resolved": "https://registry.npmjs.org/process-warning/-/process-warning-2.3.2.tgz", + "integrity": "sha512-n9wh8tvBe5sFmsqlg+XQhaQLumwpqoAUruLwjCopgTmUBjJ/fjtBsJzKleCaIGBOMXYEhp1YfKl4d7rJ5ZKJGA==" + }, + "node_modules/lru-cache": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-6.0.0.tgz", + "integrity": "sha512-Jo6dJ04CmSjuznwJSS3pUeWmd/H0ffTlkXXgwZi+eq1UCmqQwCh+eLsYOYCwY991i2Fah4h1BEMCx4qThGbsiA==", + "dependencies": { + "yallist": "^4.0.0" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/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==" + }, + "node_modules/mime-db": { + "version": "1.52.0", + "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.52.0.tgz", + "integrity": "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/minipass": { + "version": "7.0.4", + "resolved": "https://registry.npmjs.org/minipass/-/minipass-7.0.4.tgz", + "integrity": "sha512-jYofLM5Dam9279rdkWzqHozUo4ybjdZmCsDHePy5V/PbBcVMiSZR97gmAy45aqi8CK1lG2ECd356FU86avfwUQ==", + "engines": { + "node": ">=16 || 14 >=14.17" + } + }, + "node_modules/mongodb": { + "version": "6.3.0", + "resolved": "https://registry.npmjs.org/mongodb/-/mongodb-6.3.0.tgz", + "integrity": "sha512-tt0KuGjGtLUhLoU263+xvQmPHEGTw5LbcNC73EoFRYgSHwZt5tsoJC110hDyO1kjQzpgNrpdcSza9PknWN4LrA==", + "dependencies": { + "@mongodb-js/saslprep": "^1.1.0", + "bson": "^6.2.0", + "mongodb-connection-string-url": "^3.0.0" + }, + "engines": { + "node": ">=16.20.1" + }, + "peerDependencies": { + "@aws-sdk/credential-providers": "^3.188.0", + "@mongodb-js/zstd": "^1.1.0", + "gcp-metadata": "^5.2.0", + "kerberos": "^2.0.1", + "mongodb-client-encryption": ">=6.0.0 <7", + "snappy": "^7.2.2", + "socks": "^2.7.1" + }, + "peerDependenciesMeta": { + "@aws-sdk/credential-providers": { + "optional": true + }, + "@mongodb-js/zstd": { + "optional": true + }, + "gcp-metadata": { + "optional": true + }, + "kerberos": { + "optional": true + }, + "mongodb-client-encryption": { + "optional": true + }, + "snappy": { + "optional": true + }, + "socks": { + "optional": true + } + } + }, + "node_modules/mongodb-connection-string-url": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/mongodb-connection-string-url/-/mongodb-connection-string-url-3.0.0.tgz", + "integrity": "sha512-t1Vf+m1I5hC2M5RJx/7AtxgABy1cZmIPQRMXw+gEIPn/cZNF3Oiy+l0UIypUwVB5trcWHq3crg2g3uAR9aAwsQ==", + "dependencies": { + "@types/whatwg-url": "^11.0.2", + "whatwg-url": "^13.0.0" + } + }, + "node_modules/mongoose": { + "version": "8.2.0", + "resolved": "https://registry.npmjs.org/mongoose/-/mongoose-8.2.0.tgz", + "integrity": "sha512-la93n6zCYRbPS+c5N9oTDAktvREy5OT9OCljp1Tah0y3+p8UPMTAoabWaLZMdzYruOtF9/9GRf6MasaZjiZP1A==", + "dependencies": { + "bson": "^6.2.0", + "kareem": "2.5.1", + "mongodb": "6.3.0", + "mpath": "0.9.0", + "mquery": "5.0.0", + "ms": "2.1.3", + "sift": "16.0.1" + }, + "engines": { + "node": ">=16.20.1" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/mongoose" + } + }, + "node_modules/mongoose/node_modules/ms": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", + "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==" + }, + "node_modules/mpath": { + "version": "0.9.0", + "resolved": "https://registry.npmjs.org/mpath/-/mpath-0.9.0.tgz", + "integrity": "sha512-ikJRQTk8hw5DEoFVxHG1Gn9T/xcjtdnOKIU1JTmGjZZlg9LST2mBLmcX3/ICIbgJydT2GOc15RnNy5mHmzfSew==", + "engines": { + "node": ">=4.0.0" + } + }, + "node_modules/mquery": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/mquery/-/mquery-5.0.0.tgz", + "integrity": "sha512-iQMncpmEK8R8ncT8HJGsGc9Dsp8xcgYMVSbs5jgnm1lFHTZqMJTUWTDx1LBO8+mK3tPNZWFLBghQEIOULSTHZg==", + "dependencies": { + "debug": "4.x" + }, + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/ms": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz", + "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==" + }, + "node_modules/on-exit-leak-free": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/on-exit-leak-free/-/on-exit-leak-free-2.1.2.tgz", + "integrity": "sha512-0eJJY6hXLGf1udHwfNftBqH+g73EU4B504nZeKpz1sYRKafAghwxEJunB2O7rDZkL4PGfsMVnTXZ2EjibbqcsA==", + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/once": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz", + "integrity": "sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w==", + "dependencies": { + "wrappy": "1" + } + }, + "node_modules/p-is-promise": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/p-is-promise/-/p-is-promise-3.0.0.tgz", + "integrity": "sha512-Wo8VsW4IRQSKVXsJCn7TomUaVtyfjVDn3nUP7kE967BQk0CwFpdbZs0X0uk5sW9mkBa9eNM7hCMaG93WUAwxYQ==", + "engines": { + "node": ">=8" + } + }, + "node_modules/peek-stream": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/peek-stream/-/peek-stream-1.1.3.tgz", + "integrity": "sha512-FhJ+YbOSBb9/rIl2ZeE/QHEsWn7PqNYt8ARAY3kIgNGOk13g9FGyIY6JIl/xB/3TFRVoTv5as0l11weORrTekA==", + "dependencies": { + "buffer-from": "^1.0.0", + "duplexify": "^3.5.0", + "through2": "^2.0.3" + } + }, + "node_modules/pino": { + "version": "8.19.0", + "resolved": "https://registry.npmjs.org/pino/-/pino-8.19.0.tgz", + "integrity": "sha512-oswmokxkav9bADfJ2ifrvfHUwad6MLp73Uat0IkQWY3iAw5xTRoznXbXksZs8oaOUMpmhVWD+PZogNzllWpJaA==", + "dependencies": { + "atomic-sleep": "^1.0.0", + "fast-redact": "^3.1.1", + "on-exit-leak-free": "^2.1.0", + "pino-abstract-transport": "v1.1.0", + "pino-std-serializers": "^6.0.0", + "process-warning": "^3.0.0", + "quick-format-unescaped": "^4.0.3", + "real-require": "^0.2.0", + "safe-stable-stringify": "^2.3.1", + "sonic-boom": "^3.7.0", + "thread-stream": "^2.0.0" + }, + "bin": { + "pino": "bin.js" + } + }, + "node_modules/pino-abstract-transport": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/pino-abstract-transport/-/pino-abstract-transport-1.1.0.tgz", + "integrity": "sha512-lsleG3/2a/JIWUtf9Q5gUNErBqwIu1tUKTT3dUzaf5DySw9ra1wcqKjJjLX1VTY64Wk1eEOYsVGSaGfCK85ekA==", + "dependencies": { + "readable-stream": "^4.0.0", + "split2": "^4.0.0" + } + }, + "node_modules/pino-std-serializers": { + "version": "6.2.2", + "resolved": "https://registry.npmjs.org/pino-std-serializers/-/pino-std-serializers-6.2.2.tgz", + "integrity": "sha512-cHjPPsE+vhj/tnhCy/wiMh3M3z3h/j15zHQX+S9GkTBgqJuTuJzYJ4gUyACLhDaJ7kk9ba9iRDmbH2tJU03OiA==" + }, + "node_modules/process": { + "version": "0.11.10", + "resolved": "https://registry.npmjs.org/process/-/process-0.11.10.tgz", + "integrity": "sha512-cdGef/drWFoydD1JsMzuFf8100nZl+GT+yacc2bEced5f9Rjk4z+WtFUTBu9PhOi9j/jfmBPu0mMEY4wIdAF8A==", + "engines": { + "node": ">= 0.6.0" + } + }, + "node_modules/process-nextick-args": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/process-nextick-args/-/process-nextick-args-2.0.1.tgz", + "integrity": "sha512-3ouUOpQhtgrbOa17J7+uxOTpITYWaGP7/AhoR3+A+/1e9skrzelGi/dXzEYyvbxubEF6Wn2ypscTKiKJFFn1ag==" + }, + "node_modules/process-warning": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/process-warning/-/process-warning-3.0.0.tgz", + "integrity": "sha512-mqn0kFRl0EoqhnL0GQ0veqFHyIN1yig9RHh/InzORTUiZHFRAur+aMtRkELNwGs9aNwKS6tg/An4NYBPGwvtzQ==" + }, + "node_modules/proxy-addr": { + "version": "2.0.7", + "resolved": "https://registry.npmjs.org/proxy-addr/-/proxy-addr-2.0.7.tgz", + "integrity": "sha512-llQsMLSUDUPT44jdrU/O37qlnifitDP+ZwrmmZcoSKyLKvtZxpyV0n2/bD/N4tBAAZ/gJEdZU7KMraoK1+XYAg==", + "dependencies": { + "forwarded": "0.2.0", + "ipaddr.js": "1.9.1" + }, + "engines": { + "node": ">= 0.10" + } + }, + "node_modules/pump": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/pump/-/pump-3.0.0.tgz", + "integrity": "sha512-LwZy+p3SFs1Pytd/jYct4wpv49HiYCqd9Rlc5ZVdk0V+8Yzv6jR5Blk3TRmPL1ft69TxP0IMZGJ+WPFU2BFhww==", + "dependencies": { + "end-of-stream": "^1.1.0", + "once": "^1.3.1" + } + }, + "node_modules/pumpify": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/pumpify/-/pumpify-2.0.1.tgz", + "integrity": "sha512-m7KOje7jZxrmutanlkS1daj1dS6z6BgslzOXmcSEpIlCxM3VJH7lG5QLeck/6hgF6F4crFf01UtQmNsJfweTAw==", + "dependencies": { + "duplexify": "^4.1.1", + "inherits": "^2.0.3", + "pump": "^3.0.0" + } + }, + "node_modules/pumpify/node_modules/duplexify": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/duplexify/-/duplexify-4.1.2.tgz", + "integrity": "sha512-fz3OjcNCHmRP12MJoZMPglx8m4rrFP8rovnk4vT8Fs+aonZoCwGg10dSsQsfP/E62eZcPTMSMP6686fu9Qlqtw==", + "dependencies": { + "end-of-stream": "^1.4.1", + "inherits": "^2.0.3", + "readable-stream": "^3.1.1", + "stream-shift": "^1.0.0" + } + }, + "node_modules/pumpify/node_modules/readable-stream": { + "version": "3.6.2", + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-3.6.2.tgz", + "integrity": "sha512-9u/sniCrY3D5WdsERHzHE4G2YCXqoG5FTHUiCC4SIbr6XcLZBY05ya9EKjYek9O5xOAwjGq+1JdGBAS7Q9ScoA==", + "dependencies": { + "inherits": "^2.0.3", + "string_decoder": "^1.1.1", + "util-deprecate": "^1.0.1" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/punycode": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/punycode/-/punycode-2.3.1.tgz", + "integrity": "sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg==", + "engines": { + "node": ">=6" + } + }, + "node_modules/quick-format-unescaped": { + "version": "4.0.4", + "resolved": "https://registry.npmjs.org/quick-format-unescaped/-/quick-format-unescaped-4.0.4.tgz", + "integrity": "sha512-tYC1Q1hgyRuHgloV/YXs2w15unPVh8qfu/qCTfhTYamaw7fyhumKa2yGpdSo87vY32rIclj+4fWYQXUMs9EHvg==" + }, + "node_modules/readable-stream": { + "version": "4.5.2", + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-4.5.2.tgz", + "integrity": "sha512-yjavECdqeZ3GLXNgRXgeQEdz9fvDDkNKyHnbHRFtOr7/LcfgBcmct7t/ET+HaCTqfh06OzoAxrkN/IfjJBVe+g==", + "dependencies": { + "abort-controller": "^3.0.0", + "buffer": "^6.0.3", + "events": "^3.3.0", + "process": "^0.11.10", + "string_decoder": "^1.3.0" + }, + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + } + }, + "node_modules/real-require": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/real-require/-/real-require-0.2.0.tgz", + "integrity": "sha512-57frrGM/OCTLqLOAh0mhVA9VBMHd+9U7Zb2THMGdBUoZVOtGbJzjxsYGDJ3A9AYYCP4hn6y1TVbaOfzWtm5GFg==", + "engines": { + "node": ">= 12.13.0" + } + }, + "node_modules/require-from-string": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/require-from-string/-/require-from-string-2.0.2.tgz", + "integrity": "sha512-Xf0nWe6RseziFMu+Ap9biiUbmplq6S9/p+7w7YXP/JBHhrUDDUhwa+vANyubuqfZWTveU//DYVGsDG7RKL/vEw==", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/ret": { + "version": "0.2.2", + "resolved": "https://registry.npmjs.org/ret/-/ret-0.2.2.tgz", + "integrity": "sha512-M0b3YWQs7R3Z917WRQy1HHA7Ba7D8hvZg6UE5mLykJxQVE2ju0IXbGlaHPPlkY+WN7wFP+wUMXmBFA0aV6vYGQ==", + "engines": { + "node": ">=4" + } + }, + "node_modules/reusify": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/reusify/-/reusify-1.0.4.tgz", + "integrity": "sha512-U9nH88a3fc/ekCF1l0/UP1IosiuIjyTh7hBvXVMHYgVcfGvt897Xguj2UOLDeI5BG2m7/uwyaLVT6fbtCwTyzw==", + "engines": { + "iojs": ">=1.0.0", + "node": ">=0.10.0" + } + }, + "node_modules/rfdc": { + "version": "1.3.1", + "resolved": "https://registry.npmjs.org/rfdc/-/rfdc-1.3.1.tgz", + "integrity": "sha512-r5a3l5HzYlIC68TpmYKlxWjmOP6wiPJ1vWv2HeLhNsRZMrCkxeqxiHlQ21oXmQ4F3SiryXBHhAD7JZqvOJjFmg==" + }, + "node_modules/safe-buffer": { + "version": "5.2.1", + "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.1.tgz", + "integrity": "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ] + }, + "node_modules/safe-regex2": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/safe-regex2/-/safe-regex2-2.0.0.tgz", + "integrity": "sha512-PaUSFsUaNNuKwkBijoAPHAK6/eM6VirvyPWlZ7BAQy4D+hCvh4B6lIG+nPdhbFfIbP+gTGBcrdsOaUs0F+ZBOQ==", + "dependencies": { + "ret": "~0.2.0" + } + }, + "node_modules/safe-stable-stringify": { + "version": "2.4.3", + "resolved": "https://registry.npmjs.org/safe-stable-stringify/-/safe-stable-stringify-2.4.3.tgz", + "integrity": "sha512-e2bDA2WJT0wxseVd4lsDP4+3ONX6HpMXQa1ZhFQ7SU+GjvORCmShbCMltrtIDfkYhVHrOcPtj+KhmDBdPdZD1g==", + "engines": { + "node": ">=10" + } + }, + "node_modules/secure-json-parse": { + "version": "2.7.0", + "resolved": "https://registry.npmjs.org/secure-json-parse/-/secure-json-parse-2.7.0.tgz", + "integrity": "sha512-6aU+Rwsezw7VR8/nyvKTx8QpWH9FrcYiXXlqC4z5d5XQBDRqtbfsRjnwGyqbi3gddNtWHuEk9OANUotL26qKUw==" + }, + "node_modules/semver": { + "version": "7.6.0", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.6.0.tgz", + "integrity": "sha512-EnwXhrlwXMk9gKu5/flx5sv/an57AkRplG3hTK68W7FRDN+k+OWBj65M7719OkA82XLBxrcX0KSHj+X5COhOVg==", + "dependencies": { + "lru-cache": "^6.0.0" + }, + "bin": { + "semver": "bin/semver.js" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/set-cookie-parser": { + "version": "2.6.0", + "resolved": "https://registry.npmjs.org/set-cookie-parser/-/set-cookie-parser-2.6.0.tgz", + "integrity": "sha512-RVnVQxTXuerk653XfuliOxBP81Sf0+qfQE73LIYKcyMYHG94AuH0kgrQpRDuTZnSmjpysHmzxJXKNfa6PjFhyQ==" + }, + "node_modules/sift": { + "version": "16.0.1", + "resolved": "https://registry.npmjs.org/sift/-/sift-16.0.1.tgz", + "integrity": "sha512-Wv6BjQ5zbhW7VFefWusVP33T/EM0vYikCaQ2qR8yULbsilAT8/wQaXvuQ3ptGLpoKx+lihJE3y2UTgKDyyNHZQ==" + }, + "node_modules/sonic-boom": { + "version": "3.8.0", + "resolved": "https://registry.npmjs.org/sonic-boom/-/sonic-boom-3.8.0.tgz", + "integrity": "sha512-ybz6OYOUjoQQCQ/i4LU8kaToD8ACtYP+Cj5qd2AO36bwbdewxWJ3ArmJ2cr6AvxlL2o0PqnCcPGUgkILbfkaCA==", + "dependencies": { + "atomic-sleep": "^1.0.0" + } + }, + "node_modules/sparse-bitfield": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/sparse-bitfield/-/sparse-bitfield-3.0.3.tgz", + "integrity": "sha512-kvzhi7vqKTfkh0PZU+2D2PIllw2ymqJKujUcyPMd9Y75Nv4nPbGJZXNhxsgdQab2BmlDct1YnfQCguEvHr7VsQ==", + "dependencies": { + "memory-pager": "^1.0.2" + } + }, + "node_modules/split2": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/split2/-/split2-4.2.0.tgz", + "integrity": "sha512-UcjcJOWknrNkF6PLX83qcHM6KHgVKNkV62Y8a5uYDVv9ydGQVwAHMKqHdJje1VTWpljG0WYpCDhrCdAOYH4TWg==", + "engines": { + "node": ">= 10.x" + } + }, + "node_modules/stream-shift": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/stream-shift/-/stream-shift-1.0.3.tgz", + "integrity": "sha512-76ORR0DO1o1hlKwTbi/DM3EXWGf3ZJYO8cXX5RJwnul2DEg2oyoZyjLNoQM8WsvZiFKCRfC1O0J7iCvie3RZmQ==" + }, + "node_modules/string_decoder": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.3.0.tgz", + "integrity": "sha512-hkRX8U1WjJFd8LsDJ2yQ/wWWxaopEsABU1XfkM8A+j0+85JAGppt16cr1Whg6KIbb4okU6Mql6BOj+uup/wKeA==", + "dependencies": { + "safe-buffer": "~5.2.0" + } + }, + "node_modules/thread-stream": { + "version": "2.4.1", + "resolved": "https://registry.npmjs.org/thread-stream/-/thread-stream-2.4.1.tgz", + "integrity": "sha512-d/Ex2iWd1whipbT681JmTINKw0ZwOUBZm7+Gjs64DHuX34mmw8vJL2bFAaNacaW72zYiTJxSHi5abUuOi5nsfg==", + "dependencies": { + "real-require": "^0.2.0" + } + }, + "node_modules/through2": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/through2/-/through2-2.0.5.tgz", + "integrity": "sha512-/mrRod8xqpA+IHSLyGCQ2s8SPHiCDEeQJSep1jqLYeEUClOFG2Qsh+4FU6G9VeqpZnGW/Su8LQGc4YKni5rYSQ==", + "dependencies": { + "readable-stream": "~2.3.6", + "xtend": "~4.0.1" + } + }, + "node_modules/through2/node_modules/readable-stream": { + "version": "2.3.8", + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-2.3.8.tgz", + "integrity": "sha512-8p0AUk4XODgIewSi0l8Epjs+EVnWiK7NoDIEGU0HhE7+ZyY8D1IMY7odu5lRrFXGg71L15KG8QrPmum45RTtdA==", + "dependencies": { + "core-util-is": "~1.0.0", + "inherits": "~2.0.3", + "isarray": "~1.0.0", + "process-nextick-args": "~2.0.0", + "safe-buffer": "~5.1.1", + "string_decoder": "~1.1.1", + "util-deprecate": "~1.0.1" + } + }, + "node_modules/through2/node_modules/safe-buffer": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.2.tgz", + "integrity": "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==" + }, + "node_modules/through2/node_modules/string_decoder": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.1.1.tgz", + "integrity": "sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg==", + "dependencies": { + "safe-buffer": "~5.1.0" + } + }, + "node_modules/toad-cache": { + "version": "3.7.0", + "resolved": "https://registry.npmjs.org/toad-cache/-/toad-cache-3.7.0.tgz", + "integrity": "sha512-/m8M+2BJUpoJdgAHoG+baCwBT+tf2VraSfkBgl0Y00qIWt41DJ8R5B8nsEw0I58YwF5IZH6z24/2TobDKnqSWw==", + "engines": { + "node": ">=12" + } + }, + "node_modules/tr46": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/tr46/-/tr46-4.1.1.tgz", + "integrity": "sha512-2lv/66T7e5yNyhAAC4NaKe5nVavzuGJQVVtRYLyQ2OI8tsJ61PMLlelehb0wi2Hx6+hT/OJUWZcw8MjlSRnxvw==", + "dependencies": { + "punycode": "^2.3.0" + }, + "engines": { + "node": ">=14" + } + }, + "node_modules/uri-js": { + "version": "4.4.1", + "resolved": "https://registry.npmjs.org/uri-js/-/uri-js-4.4.1.tgz", + "integrity": "sha512-7rKUyy33Q1yc98pQ1DAmLtwX109F7TIfWlW1Ydo8Wl1ii1SeHieeh0HHfPeL2fMXK6z0s8ecKs9frCuLJvndBg==", + "dependencies": { + "punycode": "^2.1.0" + } + }, + "node_modules/util-deprecate": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz", + "integrity": "sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==" + }, + "node_modules/webidl-conversions": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/webidl-conversions/-/webidl-conversions-7.0.0.tgz", + "integrity": "sha512-VwddBukDzu71offAQR975unBIGqfKZpM+8ZX6ySk8nYhVoo5CYaZyzt3YBvYtRtO+aoGlqxPg/B87NGVZ/fu6g==", + "engines": { + "node": ">=12" + } + }, + "node_modules/whatwg-url": { + "version": "13.0.0", + "resolved": "https://registry.npmjs.org/whatwg-url/-/whatwg-url-13.0.0.tgz", + "integrity": "sha512-9WWbymnqj57+XEuqADHrCJ2eSXzn8WXIW/YSGaZtb2WKAInQ6CHfaUUcTyyver0p8BDg5StLQq8h1vtZuwmOig==", + "dependencies": { + "tr46": "^4.1.1", + "webidl-conversions": "^7.0.0" + }, + "engines": { + "node": ">=16" + } + }, + "node_modules/wrappy": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz", + "integrity": "sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==" + }, + "node_modules/xtend": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/xtend/-/xtend-4.0.2.tgz", + "integrity": "sha512-LKYU1iAXJXUgAXn9URjiu+MWhyUXHsvfp7mcuYm9dSUKK0/CjtrUwFAxD82/mCWbtLsGjFIad0wIsod4zrTAEQ==", + "engines": { + "node": ">=0.4" + } + }, + "node_modules/yallist": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/yallist/-/yallist-4.0.0.tgz", + "integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==" + } + } +} diff --git a/10-fastify/02-feed-api-fast/package.json b/10-fastify/02-feed-api-fast/package.json new file mode 100644 index 0000000..7bee232 --- /dev/null +++ b/10-fastify/02-feed-api-fast/package.json @@ -0,0 +1,19 @@ +{ + "name": "02-feed-api-fast", + "version": "1.0.0", + "description": "", + "main": "index.js", + "scripts": { + "test": "echo \"Error: no test specified\" && exit 1" + }, + "keywords": [], + "author": "", + "license": "ISC", + "dependencies": { + "@fastify/compress": "^7.0.0", + "date-fns": "^3.3.1", + "fastify": "^4.26.1", + "htmlparser2": "^9.1.0", + "mongoose": "^8.2.0" + } +} diff --git a/10-fastify/02-feed-api-fast/routes/feeds-update.mjs b/10-fastify/02-feed-api-fast/routes/feeds-update.mjs new file mode 100644 index 0000000..6e70afe --- /dev/null +++ b/10-fastify/02-feed-api-fast/routes/feeds-update.mjs @@ -0,0 +1,36 @@ +import { Source } from "../models/Source.mjs"; +import { FeedEntry } from "../models/FeedEntry.mjs"; +import getFeedEntries from "../get-feeds.mjs"; + +function generateFeedEntries(entries, s) { + return entries.map((en) => { + return new FeedEntry({ + source: s._id, + title: en.title, + link: en.link, + date: new Date(en.pubDate), + }).save(); + }); +} +async function waitAndCountFulfilled(proms) { + const promsSettled = await Promise.allSettled(proms); + return promsSettled.filter((r) => r.status === "fulfilled").length; +} + +async function feedsUpdateRoutes(fastify, opts) { + fastify.post("/", async (request, reply) => { + const sources = await Source.find({}).exec(); + let totalNewEntries = 0; + for (const s of sources) { + const entries = await getFeedEntries(s.url); + const savingModels = generateFeedEntries(entries, s); + let newCount = await waitAndCountFulfilled(savingModels); + totalNewEntries += newCount; + request.log.info( + `Feed ${s.url}: found ${entries.length} and saved ${newCount} entries` + ); + } + return { totalNewEntries }; + }); +} +export default feedsUpdateRoutes; diff --git a/10-fastify/02-feed-api-fast/routes/feeds.mjs b/10-fastify/02-feed-api-fast/routes/feeds.mjs new file mode 100644 index 0000000..a38c206 --- /dev/null +++ b/10-fastify/02-feed-api-fast/routes/feeds.mjs @@ -0,0 +1,16 @@ +import { FeedEntry } from "../models/FeedEntry.mjs"; + +async function feedsRoutes(fastify, opts) { + fastify.get("/", async (request, reply) => { + let findQuery = {}; + if (request.query.sourceId) { + findQuery.source = request.query.sourceId; + } + const feedEntries = await FeedEntry.find(findQuery) + .sort({ date: -1 }) + .limit(20) + .exec(); + return feedEntries; + }); +} +export default feedsRoutes; diff --git a/10-fastify/02-feed-api-fast/routes/sources.mjs b/10-fastify/02-feed-api-fast/routes/sources.mjs new file mode 100644 index 0000000..3d9070c --- /dev/null +++ b/10-fastify/02-feed-api-fast/routes/sources.mjs @@ -0,0 +1,39 @@ +import { Source } from "../models/Source.mjs"; + +async function sourcesRoutes(fastify, opts) { + fastify.post("/", async (request, reply) => { + const source = new Source({ url: request.body.url }); + request.log.info(`Adding new Source: ${source}`); + try { + await source.save(); + return source; + } catch (e) { + const reason = Source.getErrorReason(e); + if (reason) { + const err = new Error(reason); + err.statusCode = 400; + throw err; + } else { + throw new Error("An error occurred during Source.save()"); + } + } + }); + + fastify.get("/", async (request, reply) => { + const sources = await Source.find({}).exec(); + return sources; + }); + + fastify.delete("/:id", async (request, reply) => { + try { + await Source.findByIdAndDelete(request.params.id).exec(); + return reply.code(200).send(); + } catch { + throw new Error( + `Error while deleting Source with id ${request.params.id}` + ); + } + }); +} + +export default sourcesRoutes; diff --git a/10-fastify/02-feed-api-fast/server.mjs b/10-fastify/02-feed-api-fast/server.mjs new file mode 100644 index 0000000..dbc3b47 --- /dev/null +++ b/10-fastify/02-feed-api-fast/server.mjs @@ -0,0 +1,24 @@ +import Fastify from "fastify"; +const fastify = Fastify({ + logger: true, +}); +import mongoose from "mongoose"; +import compress from "@fastify/compress"; + +import feedsRoutes from "./routes/feeds.mjs"; +import sourcesRoutes from "./routes/sources.mjs"; +import feedsUpdateRoutes from "./routes/feeds-update.mjs"; + +await mongoose.connect("mongodb://127.0.0.1:27017/feed-reader"); + +fastify.register(compress); +fastify.register(feedsRoutes, { prefix: "/feeds" }); +fastify.register(sourcesRoutes, { prefix: "/sources" }); +fastify.register(feedsUpdateRoutes, { prefix: "/feeds-update" }); + +try { + await fastify.listen({ port: 3000 }); +} catch (err) { + fastify.log.error(err); + process.exit(1); +} diff --git a/10-fastify/03-feed-api-improved/get-feeds.mjs b/10-fastify/03-feed-api-improved/get-feeds.mjs new file mode 100644 index 0000000..3ddd3b3 --- /dev/null +++ b/10-fastify/03-feed-api-improved/get-feeds.mjs @@ -0,0 +1,25 @@ +import { parseFeed } from "htmlparser2"; +import { compareDesc } from "date-fns"; + +export default async function getFeedEntries(url, maxEntries = 5) { + const feedFetched = await fetch(url); + if (!feedFetched.ok) { + throw new Error("Feed fetch failed"); + } + const feedContent = await feedFetched.text(); + const { items } = parseFeed(feedContent); + return items.slice(0, maxEntries); +} + +export async function getLatestEntries(urls) { + const feedsFetches = urls.map((s) => getFeedEntries(s)); + const fetchesPromises = await Promise.allSettled(feedsFetches); + const retrievedFeeds = fetchesPromises + .filter((r) => r.status === "fulfilled") + .map((r) => r.value); + const latestItems = retrievedFeeds.flat().map((i) => { + return { title: i.title, link: i.link, date: i.pubDate }; + }); + latestItems.sort((a, b) => compareDesc(a.date, b.date)); + return latestItems; +} diff --git a/10-fastify/03-feed-api-improved/models/FeedEntry.mjs b/10-fastify/03-feed-api-improved/models/FeedEntry.mjs new file mode 100644 index 0000000..460312e --- /dev/null +++ b/10-fastify/03-feed-api-improved/models/FeedEntry.mjs @@ -0,0 +1,12 @@ +import mongoose from "mongoose"; +const { Schema } = mongoose; + +const feedEntrySchema = new Schema({ + source: { type: Schema.Types.ObjectId, ref: "Source" }, + title: { type: String }, + link: { type: String, unique: true }, + date: { type: Date }, +}); + +const FeedEntry = mongoose.model("FeedEntry", feedEntrySchema); +export { FeedEntry }; diff --git a/10-fastify/03-feed-api-improved/models/Source.mjs b/10-fastify/03-feed-api-improved/models/Source.mjs new file mode 100644 index 0000000..f358052 --- /dev/null +++ b/10-fastify/03-feed-api-improved/models/Source.mjs @@ -0,0 +1,36 @@ +import mongoose from "mongoose"; +const { Schema } = mongoose; + +const sourceSchema = new Schema({ + url: { + type: String, + required: true, + unique: true, + validate: { + validator: function (v) { + try { + const url = new URL(v); + if (!url.protocol.startsWith("http")) { + return false; + } + } catch { + return false; + } + return true; + }, + message: (props) => `${props.value} is not a valid URL`, + }, + }, +}); +sourceSchema.static("getErrorReason", (e) => { + if (e.name === "ValidationError") { + return "Source validation failed"; + } else if (e.code === 11000) { + return "Feed already exists"; + } else { + return null; + } +}); + +const Source = mongoose.model("Source", sourceSchema); +export { Source }; diff --git a/10-fastify/03-feed-api-improved/package-lock.json b/10-fastify/03-feed-api-improved/package-lock.json new file mode 100644 index 0000000..771054f --- /dev/null +++ b/10-fastify/03-feed-api-improved/package-lock.json @@ -0,0 +1,1201 @@ +{ + "name": "03-feed-api-improved", + "version": "1.0.0", + "lockfileVersion": 3, + "requires": true, + "packages": { + "": { + "name": "03-feed-api-improved", + "version": "1.0.0", + "license": "ISC", + "dependencies": { + "@fastify/compress": "^7.0.0", + "date-fns": "^3.3.1", + "fastify": "^4.26.1", + "htmlparser2": "^9.1.0", + "mongoose": "^8.2.0" + } + }, + "node_modules/@fastify/accept-negotiator": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@fastify/accept-negotiator/-/accept-negotiator-1.1.0.tgz", + "integrity": "sha512-OIHZrb2ImZ7XG85HXOONLcJWGosv7sIvM2ifAPQVhg9Lv7qdmMBNVaai4QTdyuaqbKM5eO6sLSQOYI7wEQeCJQ==", + "engines": { + "node": ">=14" + } + }, + "node_modules/@fastify/ajv-compiler": { + "version": "3.5.0", + "resolved": "https://registry.npmjs.org/@fastify/ajv-compiler/-/ajv-compiler-3.5.0.tgz", + "integrity": "sha512-ebbEtlI7dxXF5ziNdr05mOY8NnDiPB1XvAlLHctRt/Rc+C3LCOVW5imUVX+mhvUhnNzmPBHewUkOFgGlCxgdAA==", + "dependencies": { + "ajv": "^8.11.0", + "ajv-formats": "^2.1.1", + "fast-uri": "^2.0.0" + } + }, + "node_modules/@fastify/compress": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/@fastify/compress/-/compress-7.0.0.tgz", + "integrity": "sha512-jo/NaBVHP1OXIf8Kmr3bZyYQB0gAIgcy5c8rRKTPjhklHO7lRs/6ZFckUVT0NtbKSvrTuIcmSkxYpjyv3FNHXA==", + "dependencies": { + "@fastify/accept-negotiator": "^1.1.0", + "fastify-plugin": "^4.5.0", + "into-stream": "^6.0.0", + "mime-db": "^1.52.0", + "minipass": "^7.0.2", + "peek-stream": "^1.1.3", + "pump": "^3.0.0", + "pumpify": "^2.0.1" + } + }, + "node_modules/@fastify/error": { + "version": "3.4.1", + "resolved": "https://registry.npmjs.org/@fastify/error/-/error-3.4.1.tgz", + "integrity": "sha512-wWSvph+29GR783IhmvdwWnN4bUxTD01Vm5Xad4i7i1VuAOItLvbPAb69sb0IQ2N57yprvhNIwAP5B6xfKTmjmQ==" + }, + "node_modules/@fastify/fast-json-stringify-compiler": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/@fastify/fast-json-stringify-compiler/-/fast-json-stringify-compiler-4.3.0.tgz", + "integrity": "sha512-aZAXGYo6m22Fk1zZzEUKBvut/CIIQe/BapEORnxiD5Qr0kPHqqI69NtEMCme74h+at72sPhbkb4ZrLd1W3KRLA==", + "dependencies": { + "fast-json-stringify": "^5.7.0" + } + }, + "node_modules/@fastify/merge-json-schemas": { + "version": "0.1.1", + "resolved": "https://registry.npmjs.org/@fastify/merge-json-schemas/-/merge-json-schemas-0.1.1.tgz", + "integrity": "sha512-fERDVz7topgNjtXsJTTW1JKLy0rhuLRcquYqNR9rF7OcVpCa2OVW49ZPDIhaRRCaUuvVxI+N416xUoF76HNSXA==", + "dependencies": { + "fast-deep-equal": "^3.1.3" + } + }, + "node_modules/@mongodb-js/saslprep": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/@mongodb-js/saslprep/-/saslprep-1.1.4.tgz", + "integrity": "sha512-8zJ8N1x51xo9hwPh6AWnKdLGEC5N3lDa6kms1YHmFBoRhTpJR6HG8wWk0td1MVCu9cD4YBrvjZEtd5Obw0Fbnw==", + "dependencies": { + "sparse-bitfield": "^3.0.3" + } + }, + "node_modules/@types/webidl-conversions": { + "version": "7.0.3", + "resolved": "https://registry.npmjs.org/@types/webidl-conversions/-/webidl-conversions-7.0.3.tgz", + "integrity": "sha512-CiJJvcRtIgzadHCYXw7dqEnMNRjhGZlYK05Mj9OyktqV8uVT8fD2BFOB7S1uwBE3Kj2Z+4UyPmFw/Ixgw/LAlA==" + }, + "node_modules/@types/whatwg-url": { + "version": "11.0.4", + "resolved": "https://registry.npmjs.org/@types/whatwg-url/-/whatwg-url-11.0.4.tgz", + "integrity": "sha512-lXCmTWSHJvf0TRSO58nm978b8HJ/EdsSsEKLd3ODHFjo+3VGAyyTp4v50nWvwtzBxSMQrVOK7tcuN0zGPLICMw==", + "dependencies": { + "@types/webidl-conversions": "*" + } + }, + "node_modules/abort-controller": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/abort-controller/-/abort-controller-3.0.0.tgz", + "integrity": "sha512-h8lQ8tacZYnR3vNQTgibj+tODHI5/+l06Au2Pcriv/Gmet0eaj4TwWH41sO9wnHDiQsEj19q0drzdWdeAHtweg==", + "dependencies": { + "event-target-shim": "^5.0.0" + }, + "engines": { + "node": ">=6.5" + } + }, + "node_modules/abstract-logging": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/abstract-logging/-/abstract-logging-2.0.1.tgz", + "integrity": "sha512-2BjRTZxTPvheOvGbBslFSYOUkr+SjPtOnrLP33f+VIWLzezQpZcqVg7ja3L4dBXmzzgwT+a029jRx5PCi3JuiA==" + }, + "node_modules/ajv": { + "version": "8.12.0", + "resolved": "https://registry.npmjs.org/ajv/-/ajv-8.12.0.tgz", + "integrity": "sha512-sRu1kpcO9yLtYxBKvqfTeh9KzZEwO3STyX1HT+4CaDzC6HpTGYhIhPIzj9XuKU7KYDwnaeh5hcOwjy1QuJzBPA==", + "dependencies": { + "fast-deep-equal": "^3.1.1", + "json-schema-traverse": "^1.0.0", + "require-from-string": "^2.0.2", + "uri-js": "^4.2.2" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/epoberezkin" + } + }, + "node_modules/ajv-formats": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/ajv-formats/-/ajv-formats-2.1.1.tgz", + "integrity": "sha512-Wx0Kx52hxE7C18hkMEggYlEifqWZtYaRgouJor+WMdPnQyEK13vgEWyVNup7SoeeoLMsr4kf5h6dOW11I15MUA==", + "dependencies": { + "ajv": "^8.0.0" + }, + "peerDependencies": { + "ajv": "^8.0.0" + }, + "peerDependenciesMeta": { + "ajv": { + "optional": true + } + } + }, + "node_modules/archy": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/archy/-/archy-1.0.0.tgz", + "integrity": "sha512-Xg+9RwCg/0p32teKdGMPTPnVXKD0w3DfHnFTficozsAgsvq2XenPJq/MYpzzQ/v8zrOyJn6Ds39VA4JIDwFfqw==" + }, + "node_modules/atomic-sleep": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/atomic-sleep/-/atomic-sleep-1.0.0.tgz", + "integrity": "sha512-kNOjDqAh7px0XWNI+4QbzoiR/nTkHAWNud2uvnJquD1/x5a7EQZMJT0AczqK0Qn67oY/TTQ1LbUKajZpp3I9tQ==", + "engines": { + "node": ">=8.0.0" + } + }, + "node_modules/avvio": { + "version": "8.3.0", + "resolved": "https://registry.npmjs.org/avvio/-/avvio-8.3.0.tgz", + "integrity": "sha512-VBVH0jubFr9LdFASy/vNtm5giTrnbVquWBhT0fyizuNK2rQ7e7ONU2plZQWUNqtE1EmxFEb+kbSkFRkstiaS9Q==", + "dependencies": { + "@fastify/error": "^3.3.0", + "archy": "^1.0.0", + "debug": "^4.0.0", + "fastq": "^1.17.1" + } + }, + "node_modules/base64-js": { + "version": "1.5.1", + "resolved": "https://registry.npmjs.org/base64-js/-/base64-js-1.5.1.tgz", + "integrity": "sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ] + }, + "node_modules/bson": { + "version": "6.3.0", + "resolved": "https://registry.npmjs.org/bson/-/bson-6.3.0.tgz", + "integrity": "sha512-balJfqwwTBddxfnidJZagCBPP/f48zj9Sdp3OJswREOgsJzHiQSaOIAtApSgDQFYgHqAvFkp53AFSqjMDZoTFw==", + "engines": { + "node": ">=16.20.1" + } + }, + "node_modules/buffer": { + "version": "6.0.3", + "resolved": "https://registry.npmjs.org/buffer/-/buffer-6.0.3.tgz", + "integrity": "sha512-FTiCpNxtwiZZHEZbcbTIcZjERVICn9yq/pDFkTl95/AxzD1naBctN7YO68riM/gLSDY7sdrMby8hofADYuuqOA==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "dependencies": { + "base64-js": "^1.3.1", + "ieee754": "^1.2.1" + } + }, + "node_modules/buffer-from": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/buffer-from/-/buffer-from-1.1.2.tgz", + "integrity": "sha512-E+XQCRwSbaaiChtv6k6Dwgc+bx+Bs6vuKJHHl5kox/BaKbhiXzqQOwK4cO22yElGp2OCmjwVhT3HmxgyPGnJfQ==" + }, + "node_modules/cookie": { + "version": "0.6.0", + "resolved": "https://registry.npmjs.org/cookie/-/cookie-0.6.0.tgz", + "integrity": "sha512-U71cyTamuh1CRNCfpGY6to28lxvNwPG4Guz/EVjgf3Jmzv0vlDp1atT9eS5dDjMYHucpHbWns6Lwf3BKz6svdw==", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/core-util-is": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/core-util-is/-/core-util-is-1.0.3.tgz", + "integrity": "sha512-ZQBvi1DcpJ4GDqanjucZ2Hj3wEO5pZDS89BWbkcrvdxksJorwUDDZamX9ldFkp9aw2lmBDLgkObEA4DWNJ9FYQ==" + }, + "node_modules/date-fns": { + "version": "3.3.1", + "resolved": "https://registry.npmjs.org/date-fns/-/date-fns-3.3.1.tgz", + "integrity": "sha512-y8e109LYGgoQDveiEBD3DYXKba1jWf5BA8YU1FL5Tvm0BTdEfy54WLCwnuYWZNnzzvALy/QQ4Hov+Q9RVRv+Zw==", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/kossnocorp" + } + }, + "node_modules/debug": { + "version": "4.3.4", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.4.tgz", + "integrity": "sha512-PRWFHuSU3eDtQJPvnNY7Jcket1j0t5OuOsFzPPzsekD52Zl8qUfFIPEiswXqIvHWGVHOgX+7G/vCNNhehwxfkQ==", + "dependencies": { + "ms": "2.1.2" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "node_modules/dom-serializer": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/dom-serializer/-/dom-serializer-2.0.0.tgz", + "integrity": "sha512-wIkAryiqt/nV5EQKqQpo3SToSOV9J0DnbJqwK7Wv/Trc92zIAYZ4FlMu+JPFW1DfGFt81ZTCGgDEabffXeLyJg==", + "dependencies": { + "domelementtype": "^2.3.0", + "domhandler": "^5.0.2", + "entities": "^4.2.0" + }, + "funding": { + "url": "https://github.com/cheeriojs/dom-serializer?sponsor=1" + } + }, + "node_modules/domelementtype": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/domelementtype/-/domelementtype-2.3.0.tgz", + "integrity": "sha512-OLETBj6w0OsagBwdXnPdN0cnMfF9opN69co+7ZrbfPGrdpPVNBUj02spi6B1N7wChLQiPn4CSH/zJvXw56gmHw==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/fb55" + } + ] + }, + "node_modules/domhandler": { + "version": "5.0.3", + "resolved": "https://registry.npmjs.org/domhandler/-/domhandler-5.0.3.tgz", + "integrity": "sha512-cgwlv/1iFQiFnU96XXgROh8xTeetsnJiDsTc7TYCLFd9+/WNkIqPTxiM/8pSd8VIrhXGTf1Ny1q1hquVqDJB5w==", + "dependencies": { + "domelementtype": "^2.3.0" + }, + "engines": { + "node": ">= 4" + }, + "funding": { + "url": "https://github.com/fb55/domhandler?sponsor=1" + } + }, + "node_modules/domutils": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/domutils/-/domutils-3.1.0.tgz", + "integrity": "sha512-H78uMmQtI2AhgDJjWeQmHwJJ2bLPD3GMmO7Zja/ZZh84wkm+4ut+IUnUdRa8uCGX88DiVx1j6FRe1XfxEgjEZA==", + "dependencies": { + "dom-serializer": "^2.0.0", + "domelementtype": "^2.3.0", + "domhandler": "^5.0.3" + }, + "funding": { + "url": "https://github.com/fb55/domutils?sponsor=1" + } + }, + "node_modules/duplexify": { + "version": "3.7.1", + "resolved": "https://registry.npmjs.org/duplexify/-/duplexify-3.7.1.tgz", + "integrity": "sha512-07z8uv2wMyS51kKhD1KsdXJg5WQ6t93RneqRxUHnskXVtlYYkLqM0gqStQZ3pj073g687jPCHrqNfCzawLYh5g==", + "dependencies": { + "end-of-stream": "^1.0.0", + "inherits": "^2.0.1", + "readable-stream": "^2.0.0", + "stream-shift": "^1.0.0" + } + }, + "node_modules/duplexify/node_modules/readable-stream": { + "version": "2.3.8", + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-2.3.8.tgz", + "integrity": "sha512-8p0AUk4XODgIewSi0l8Epjs+EVnWiK7NoDIEGU0HhE7+ZyY8D1IMY7odu5lRrFXGg71L15KG8QrPmum45RTtdA==", + "dependencies": { + "core-util-is": "~1.0.0", + "inherits": "~2.0.3", + "isarray": "~1.0.0", + "process-nextick-args": "~2.0.0", + "safe-buffer": "~5.1.1", + "string_decoder": "~1.1.1", + "util-deprecate": "~1.0.1" + } + }, + "node_modules/duplexify/node_modules/safe-buffer": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.2.tgz", + "integrity": "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==" + }, + "node_modules/duplexify/node_modules/string_decoder": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.1.1.tgz", + "integrity": "sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg==", + "dependencies": { + "safe-buffer": "~5.1.0" + } + }, + "node_modules/end-of-stream": { + "version": "1.4.4", + "resolved": "https://registry.npmjs.org/end-of-stream/-/end-of-stream-1.4.4.tgz", + "integrity": "sha512-+uw1inIHVPQoaVuHzRyXd21icM+cnt4CzD5rW+NC1wjOUSTOs+Te7FOv7AhN7vS9x/oIyhLP5PR1H+phQAHu5Q==", + "dependencies": { + "once": "^1.4.0" + } + }, + "node_modules/entities": { + "version": "4.5.0", + "resolved": "https://registry.npmjs.org/entities/-/entities-4.5.0.tgz", + "integrity": "sha512-V0hjH4dGPh9Ao5p0MoRY6BVqtwCjhz6vI5LT8AJ55H+4g9/4vbHx1I54fS0XuclLhDHArPQCiMjDxjaL8fPxhw==", + "engines": { + "node": ">=0.12" + }, + "funding": { + "url": "https://github.com/fb55/entities?sponsor=1" + } + }, + "node_modules/event-target-shim": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/event-target-shim/-/event-target-shim-5.0.1.tgz", + "integrity": "sha512-i/2XbnSz/uxRCU6+NdVJgKWDTM427+MqYbkQzD321DuCQJUqOuJKIA0IM2+W2xtYHdKOmZ4dR6fExsd4SXL+WQ==", + "engines": { + "node": ">=6" + } + }, + "node_modules/events": { + "version": "3.3.0", + "resolved": "https://registry.npmjs.org/events/-/events-3.3.0.tgz", + "integrity": "sha512-mQw+2fkQbALzQ7V0MY0IqdnXNOeTtP4r0lN9z7AAawCXgqea7bDii20AYrIBrFd/Hx0M2Ocz6S111CaFkUcb0Q==", + "engines": { + "node": ">=0.8.x" + } + }, + "node_modules/fast-content-type-parse": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/fast-content-type-parse/-/fast-content-type-parse-1.1.0.tgz", + "integrity": "sha512-fBHHqSTFLVnR61C+gltJuE5GkVQMV0S2nqUO8TJ+5Z3qAKG8vAx4FKai1s5jq/inV1+sREynIWSuQ6HgoSXpDQ==" + }, + "node_modules/fast-decode-uri-component": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/fast-decode-uri-component/-/fast-decode-uri-component-1.0.1.tgz", + "integrity": "sha512-WKgKWg5eUxvRZGwW8FvfbaH7AXSh2cL+3j5fMGzUMCxWBJ3dV3a7Wz8y2f/uQ0e3B6WmodD3oS54jTQ9HVTIIg==" + }, + "node_modules/fast-deep-equal": { + "version": "3.1.3", + "resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz", + "integrity": "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==" + }, + "node_modules/fast-json-stringify": { + "version": "5.12.0", + "resolved": "https://registry.npmjs.org/fast-json-stringify/-/fast-json-stringify-5.12.0.tgz", + "integrity": "sha512-7Nnm9UPa7SfHRbHVA1kJQrGXCRzB7LMlAAqHXQFkEQqueJm1V8owm0FsE/2Do55/4CcdhwiLQERaKomOnKQkyA==", + "dependencies": { + "@fastify/merge-json-schemas": "^0.1.0", + "ajv": "^8.10.0", + "ajv-formats": "^2.1.1", + "fast-deep-equal": "^3.1.3", + "fast-uri": "^2.1.0", + "json-schema-ref-resolver": "^1.0.1", + "rfdc": "^1.2.0" + } + }, + "node_modules/fast-querystring": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/fast-querystring/-/fast-querystring-1.1.2.tgz", + "integrity": "sha512-g6KuKWmFXc0fID8WWH0jit4g0AGBoJhCkJMb1RmbsSEUNvQ+ZC8D6CUZ+GtF8nMzSPXnhiePyyqqipzNNEnHjg==", + "dependencies": { + "fast-decode-uri-component": "^1.0.1" + } + }, + "node_modules/fast-redact": { + "version": "3.3.0", + "resolved": "https://registry.npmjs.org/fast-redact/-/fast-redact-3.3.0.tgz", + "integrity": "sha512-6T5V1QK1u4oF+ATxs1lWUmlEk6P2T9HqJG3e2DnHOdVgZy2rFJBoEnrIedcTXlkAHU/zKC+7KETJ+KGGKwxgMQ==", + "engines": { + "node": ">=6" + } + }, + "node_modules/fast-uri": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/fast-uri/-/fast-uri-2.3.0.tgz", + "integrity": "sha512-eel5UKGn369gGEWOqBShmFJWfq/xSJvsgDzgLYC845GneayWvXBf0lJCBn5qTABfewy1ZDPoaR5OZCP+kssfuw==" + }, + "node_modules/fastify": { + "version": "4.26.1", + "resolved": "https://registry.npmjs.org/fastify/-/fastify-4.26.1.tgz", + "integrity": "sha512-tznA/G55dsxzM5XChBfcvVSloG2ejeeotfPPJSFaWmHyCDVGMpvf3nRNbsCb/JTBF9RmQFBfuujWt3Nphjesng==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/fastify" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/fastify" + } + ], + "dependencies": { + "@fastify/ajv-compiler": "^3.5.0", + "@fastify/error": "^3.4.0", + "@fastify/fast-json-stringify-compiler": "^4.3.0", + "abstract-logging": "^2.0.1", + "avvio": "^8.3.0", + "fast-content-type-parse": "^1.1.0", + "fast-json-stringify": "^5.8.0", + "find-my-way": "^8.0.0", + "light-my-request": "^5.11.0", + "pino": "^8.17.0", + "process-warning": "^3.0.0", + "proxy-addr": "^2.0.7", + "rfdc": "^1.3.0", + "secure-json-parse": "^2.7.0", + "semver": "^7.5.4", + "toad-cache": "^3.3.0" + } + }, + "node_modules/fastify-plugin": { + "version": "4.5.1", + "resolved": "https://registry.npmjs.org/fastify-plugin/-/fastify-plugin-4.5.1.tgz", + "integrity": "sha512-stRHYGeuqpEZTL1Ef0Ovr2ltazUT9g844X5z/zEBFLG8RYlpDiOCIG+ATvYEp+/zmc7sN29mcIMp8gvYplYPIQ==" + }, + "node_modules/fastq": { + "version": "1.17.1", + "resolved": "https://registry.npmjs.org/fastq/-/fastq-1.17.1.tgz", + "integrity": "sha512-sRVD3lWVIXWg6By68ZN7vho9a1pQcN/WBFaAAsDDFzlJjvoGx0P8z7V1t72grFJfJhu3YPZBuu25f7Kaw2jN1w==", + "dependencies": { + "reusify": "^1.0.4" + } + }, + "node_modules/find-my-way": { + "version": "8.1.0", + "resolved": "https://registry.npmjs.org/find-my-way/-/find-my-way-8.1.0.tgz", + "integrity": "sha512-41QwjCGcVTODUmLLqTMeoHeiozbMXYMAE1CKFiDyi9zVZ2Vjh0yz3MF0WQZoIb+cmzP/XlbFjlF2NtJmvZHznA==", + "dependencies": { + "fast-deep-equal": "^3.1.3", + "fast-querystring": "^1.0.0", + "safe-regex2": "^2.0.0" + }, + "engines": { + "node": ">=14" + } + }, + "node_modules/forwarded": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/forwarded/-/forwarded-0.2.0.tgz", + "integrity": "sha512-buRG0fpBtRHSTCOASe6hD258tEubFoRLb4ZNA6NxMVHNw2gOcwHo9wyablzMzOA5z9xA9L1KNjk/Nt6MT9aYow==", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/from2": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/from2/-/from2-2.3.0.tgz", + "integrity": "sha512-OMcX/4IC/uqEPVgGeyfN22LJk6AZrMkRZHxcHBMBvHScDGgwTm2GT2Wkgtocyd3JfZffjj2kYUDXXII0Fk9W0g==", + "dependencies": { + "inherits": "^2.0.1", + "readable-stream": "^2.0.0" + } + }, + "node_modules/from2/node_modules/readable-stream": { + "version": "2.3.8", + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-2.3.8.tgz", + "integrity": "sha512-8p0AUk4XODgIewSi0l8Epjs+EVnWiK7NoDIEGU0HhE7+ZyY8D1IMY7odu5lRrFXGg71L15KG8QrPmum45RTtdA==", + "dependencies": { + "core-util-is": "~1.0.0", + "inherits": "~2.0.3", + "isarray": "~1.0.0", + "process-nextick-args": "~2.0.0", + "safe-buffer": "~5.1.1", + "string_decoder": "~1.1.1", + "util-deprecate": "~1.0.1" + } + }, + "node_modules/from2/node_modules/safe-buffer": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.2.tgz", + "integrity": "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==" + }, + "node_modules/from2/node_modules/string_decoder": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.1.1.tgz", + "integrity": "sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg==", + "dependencies": { + "safe-buffer": "~5.1.0" + } + }, + "node_modules/htmlparser2": { + "version": "9.1.0", + "resolved": "https://registry.npmjs.org/htmlparser2/-/htmlparser2-9.1.0.tgz", + "integrity": "sha512-5zfg6mHUoaer/97TxnGpxmbR7zJtPwIYFMZ/H5ucTlPZhKvtum05yiPK3Mgai3a0DyVxv7qYqoweaEd2nrYQzQ==", + "funding": [ + "https://github.com/fb55/htmlparser2?sponsor=1", + { + "type": "github", + "url": "https://github.com/sponsors/fb55" + } + ], + "dependencies": { + "domelementtype": "^2.3.0", + "domhandler": "^5.0.3", + "domutils": "^3.1.0", + "entities": "^4.5.0" + } + }, + "node_modules/ieee754": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/ieee754/-/ieee754-1.2.1.tgz", + "integrity": "sha512-dcyqhDvX1C46lXZcVqCpK+FtMRQVdIMN6/Df5js2zouUsqG7I6sFxitIC+7KYK29KdXOLHdu9zL4sFnoVQnqaA==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ] + }, + "node_modules/inherits": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz", + "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==" + }, + "node_modules/into-stream": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/into-stream/-/into-stream-6.0.0.tgz", + "integrity": "sha512-XHbaOAvP+uFKUFsOgoNPRjLkwB+I22JFPFe5OjTkQ0nwgj6+pSjb4NmB6VMxaPshLiOf+zcpOCBQuLwC1KHhZA==", + "dependencies": { + "from2": "^2.3.0", + "p-is-promise": "^3.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/ipaddr.js": { + "version": "1.9.1", + "resolved": "https://registry.npmjs.org/ipaddr.js/-/ipaddr.js-1.9.1.tgz", + "integrity": "sha512-0KI/607xoxSToH7GjN1FfSbLoU0+btTicjsQSWQlh/hZykN8KpmMf7uYwPW3R+akZ6R/w18ZlXSHBYXiYUPO3g==", + "engines": { + "node": ">= 0.10" + } + }, + "node_modules/isarray": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/isarray/-/isarray-1.0.0.tgz", + "integrity": "sha512-VLghIWNM6ELQzo7zwmcg0NmTVyWKYjvIeM83yjp0wRDTmUnrM678fQbcKBo6n2CJEF0szoG//ytg+TKla89ALQ==" + }, + "node_modules/json-schema-ref-resolver": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/json-schema-ref-resolver/-/json-schema-ref-resolver-1.0.1.tgz", + "integrity": "sha512-EJAj1pgHc1hxF6vo2Z3s69fMjO1INq6eGHXZ8Z6wCQeldCuwxGK9Sxf4/cScGn3FZubCVUehfWtcDM/PLteCQw==", + "dependencies": { + "fast-deep-equal": "^3.1.3" + } + }, + "node_modules/json-schema-traverse": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-1.0.0.tgz", + "integrity": "sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug==" + }, + "node_modules/kareem": { + "version": "2.5.1", + "resolved": "https://registry.npmjs.org/kareem/-/kareem-2.5.1.tgz", + "integrity": "sha512-7jFxRVm+jD+rkq3kY0iZDJfsO2/t4BBPeEb2qKn2lR/9KhuksYk5hxzfRYWMPV8P/x2d0kHD306YyWLzjjH+uA==", + "engines": { + "node": ">=12.0.0" + } + }, + "node_modules/light-my-request": { + "version": "5.11.1", + "resolved": "https://registry.npmjs.org/light-my-request/-/light-my-request-5.11.1.tgz", + "integrity": "sha512-KXAh2m6VRlkWCk2KfmHE7tLBXKh30JE0tXUJY4dNxje4oLmPKUqlUfImiEQZLphx+Z9KTQcVv4DjGnJxkVOIbA==", + "dependencies": { + "cookie": "^0.6.0", + "process-warning": "^2.0.0", + "set-cookie-parser": "^2.4.1" + } + }, + "node_modules/light-my-request/node_modules/process-warning": { + "version": "2.3.2", + "resolved": "https://registry.npmjs.org/process-warning/-/process-warning-2.3.2.tgz", + "integrity": "sha512-n9wh8tvBe5sFmsqlg+XQhaQLumwpqoAUruLwjCopgTmUBjJ/fjtBsJzKleCaIGBOMXYEhp1YfKl4d7rJ5ZKJGA==" + }, + "node_modules/lru-cache": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-6.0.0.tgz", + "integrity": "sha512-Jo6dJ04CmSjuznwJSS3pUeWmd/H0ffTlkXXgwZi+eq1UCmqQwCh+eLsYOYCwY991i2Fah4h1BEMCx4qThGbsiA==", + "dependencies": { + "yallist": "^4.0.0" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/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==" + }, + "node_modules/mime-db": { + "version": "1.52.0", + "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.52.0.tgz", + "integrity": "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/minipass": { + "version": "7.0.4", + "resolved": "https://registry.npmjs.org/minipass/-/minipass-7.0.4.tgz", + "integrity": "sha512-jYofLM5Dam9279rdkWzqHozUo4ybjdZmCsDHePy5V/PbBcVMiSZR97gmAy45aqi8CK1lG2ECd356FU86avfwUQ==", + "engines": { + "node": ">=16 || 14 >=14.17" + } + }, + "node_modules/mongodb": { + "version": "6.3.0", + "resolved": "https://registry.npmjs.org/mongodb/-/mongodb-6.3.0.tgz", + "integrity": "sha512-tt0KuGjGtLUhLoU263+xvQmPHEGTw5LbcNC73EoFRYgSHwZt5tsoJC110hDyO1kjQzpgNrpdcSza9PknWN4LrA==", + "dependencies": { + "@mongodb-js/saslprep": "^1.1.0", + "bson": "^6.2.0", + "mongodb-connection-string-url": "^3.0.0" + }, + "engines": { + "node": ">=16.20.1" + }, + "peerDependencies": { + "@aws-sdk/credential-providers": "^3.188.0", + "@mongodb-js/zstd": "^1.1.0", + "gcp-metadata": "^5.2.0", + "kerberos": "^2.0.1", + "mongodb-client-encryption": ">=6.0.0 <7", + "snappy": "^7.2.2", + "socks": "^2.7.1" + }, + "peerDependenciesMeta": { + "@aws-sdk/credential-providers": { + "optional": true + }, + "@mongodb-js/zstd": { + "optional": true + }, + "gcp-metadata": { + "optional": true + }, + "kerberos": { + "optional": true + }, + "mongodb-client-encryption": { + "optional": true + }, + "snappy": { + "optional": true + }, + "socks": { + "optional": true + } + } + }, + "node_modules/mongodb-connection-string-url": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/mongodb-connection-string-url/-/mongodb-connection-string-url-3.0.0.tgz", + "integrity": "sha512-t1Vf+m1I5hC2M5RJx/7AtxgABy1cZmIPQRMXw+gEIPn/cZNF3Oiy+l0UIypUwVB5trcWHq3crg2g3uAR9aAwsQ==", + "dependencies": { + "@types/whatwg-url": "^11.0.2", + "whatwg-url": "^13.0.0" + } + }, + "node_modules/mongoose": { + "version": "8.2.0", + "resolved": "https://registry.npmjs.org/mongoose/-/mongoose-8.2.0.tgz", + "integrity": "sha512-la93n6zCYRbPS+c5N9oTDAktvREy5OT9OCljp1Tah0y3+p8UPMTAoabWaLZMdzYruOtF9/9GRf6MasaZjiZP1A==", + "dependencies": { + "bson": "^6.2.0", + "kareem": "2.5.1", + "mongodb": "6.3.0", + "mpath": "0.9.0", + "mquery": "5.0.0", + "ms": "2.1.3", + "sift": "16.0.1" + }, + "engines": { + "node": ">=16.20.1" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/mongoose" + } + }, + "node_modules/mongoose/node_modules/ms": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", + "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==" + }, + "node_modules/mpath": { + "version": "0.9.0", + "resolved": "https://registry.npmjs.org/mpath/-/mpath-0.9.0.tgz", + "integrity": "sha512-ikJRQTk8hw5DEoFVxHG1Gn9T/xcjtdnOKIU1JTmGjZZlg9LST2mBLmcX3/ICIbgJydT2GOc15RnNy5mHmzfSew==", + "engines": { + "node": ">=4.0.0" + } + }, + "node_modules/mquery": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/mquery/-/mquery-5.0.0.tgz", + "integrity": "sha512-iQMncpmEK8R8ncT8HJGsGc9Dsp8xcgYMVSbs5jgnm1lFHTZqMJTUWTDx1LBO8+mK3tPNZWFLBghQEIOULSTHZg==", + "dependencies": { + "debug": "4.x" + }, + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/ms": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz", + "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==" + }, + "node_modules/on-exit-leak-free": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/on-exit-leak-free/-/on-exit-leak-free-2.1.2.tgz", + "integrity": "sha512-0eJJY6hXLGf1udHwfNftBqH+g73EU4B504nZeKpz1sYRKafAghwxEJunB2O7rDZkL4PGfsMVnTXZ2EjibbqcsA==", + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/once": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz", + "integrity": "sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w==", + "dependencies": { + "wrappy": "1" + } + }, + "node_modules/p-is-promise": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/p-is-promise/-/p-is-promise-3.0.0.tgz", + "integrity": "sha512-Wo8VsW4IRQSKVXsJCn7TomUaVtyfjVDn3nUP7kE967BQk0CwFpdbZs0X0uk5sW9mkBa9eNM7hCMaG93WUAwxYQ==", + "engines": { + "node": ">=8" + } + }, + "node_modules/peek-stream": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/peek-stream/-/peek-stream-1.1.3.tgz", + "integrity": "sha512-FhJ+YbOSBb9/rIl2ZeE/QHEsWn7PqNYt8ARAY3kIgNGOk13g9FGyIY6JIl/xB/3TFRVoTv5as0l11weORrTekA==", + "dependencies": { + "buffer-from": "^1.0.0", + "duplexify": "^3.5.0", + "through2": "^2.0.3" + } + }, + "node_modules/pino": { + "version": "8.19.0", + "resolved": "https://registry.npmjs.org/pino/-/pino-8.19.0.tgz", + "integrity": "sha512-oswmokxkav9bADfJ2ifrvfHUwad6MLp73Uat0IkQWY3iAw5xTRoznXbXksZs8oaOUMpmhVWD+PZogNzllWpJaA==", + "dependencies": { + "atomic-sleep": "^1.0.0", + "fast-redact": "^3.1.1", + "on-exit-leak-free": "^2.1.0", + "pino-abstract-transport": "v1.1.0", + "pino-std-serializers": "^6.0.0", + "process-warning": "^3.0.0", + "quick-format-unescaped": "^4.0.3", + "real-require": "^0.2.0", + "safe-stable-stringify": "^2.3.1", + "sonic-boom": "^3.7.0", + "thread-stream": "^2.0.0" + }, + "bin": { + "pino": "bin.js" + } + }, + "node_modules/pino-abstract-transport": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/pino-abstract-transport/-/pino-abstract-transport-1.1.0.tgz", + "integrity": "sha512-lsleG3/2a/JIWUtf9Q5gUNErBqwIu1tUKTT3dUzaf5DySw9ra1wcqKjJjLX1VTY64Wk1eEOYsVGSaGfCK85ekA==", + "dependencies": { + "readable-stream": "^4.0.0", + "split2": "^4.0.0" + } + }, + "node_modules/pino-std-serializers": { + "version": "6.2.2", + "resolved": "https://registry.npmjs.org/pino-std-serializers/-/pino-std-serializers-6.2.2.tgz", + "integrity": "sha512-cHjPPsE+vhj/tnhCy/wiMh3M3z3h/j15zHQX+S9GkTBgqJuTuJzYJ4gUyACLhDaJ7kk9ba9iRDmbH2tJU03OiA==" + }, + "node_modules/process": { + "version": "0.11.10", + "resolved": "https://registry.npmjs.org/process/-/process-0.11.10.tgz", + "integrity": "sha512-cdGef/drWFoydD1JsMzuFf8100nZl+GT+yacc2bEced5f9Rjk4z+WtFUTBu9PhOi9j/jfmBPu0mMEY4wIdAF8A==", + "engines": { + "node": ">= 0.6.0" + } + }, + "node_modules/process-nextick-args": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/process-nextick-args/-/process-nextick-args-2.0.1.tgz", + "integrity": "sha512-3ouUOpQhtgrbOa17J7+uxOTpITYWaGP7/AhoR3+A+/1e9skrzelGi/dXzEYyvbxubEF6Wn2ypscTKiKJFFn1ag==" + }, + "node_modules/process-warning": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/process-warning/-/process-warning-3.0.0.tgz", + "integrity": "sha512-mqn0kFRl0EoqhnL0GQ0veqFHyIN1yig9RHh/InzORTUiZHFRAur+aMtRkELNwGs9aNwKS6tg/An4NYBPGwvtzQ==" + }, + "node_modules/proxy-addr": { + "version": "2.0.7", + "resolved": "https://registry.npmjs.org/proxy-addr/-/proxy-addr-2.0.7.tgz", + "integrity": "sha512-llQsMLSUDUPT44jdrU/O37qlnifitDP+ZwrmmZcoSKyLKvtZxpyV0n2/bD/N4tBAAZ/gJEdZU7KMraoK1+XYAg==", + "dependencies": { + "forwarded": "0.2.0", + "ipaddr.js": "1.9.1" + }, + "engines": { + "node": ">= 0.10" + } + }, + "node_modules/pump": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/pump/-/pump-3.0.0.tgz", + "integrity": "sha512-LwZy+p3SFs1Pytd/jYct4wpv49HiYCqd9Rlc5ZVdk0V+8Yzv6jR5Blk3TRmPL1ft69TxP0IMZGJ+WPFU2BFhww==", + "dependencies": { + "end-of-stream": "^1.1.0", + "once": "^1.3.1" + } + }, + "node_modules/pumpify": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/pumpify/-/pumpify-2.0.1.tgz", + "integrity": "sha512-m7KOje7jZxrmutanlkS1daj1dS6z6BgslzOXmcSEpIlCxM3VJH7lG5QLeck/6hgF6F4crFf01UtQmNsJfweTAw==", + "dependencies": { + "duplexify": "^4.1.1", + "inherits": "^2.0.3", + "pump": "^3.0.0" + } + }, + "node_modules/pumpify/node_modules/duplexify": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/duplexify/-/duplexify-4.1.2.tgz", + "integrity": "sha512-fz3OjcNCHmRP12MJoZMPglx8m4rrFP8rovnk4vT8Fs+aonZoCwGg10dSsQsfP/E62eZcPTMSMP6686fu9Qlqtw==", + "dependencies": { + "end-of-stream": "^1.4.1", + "inherits": "^2.0.3", + "readable-stream": "^3.1.1", + "stream-shift": "^1.0.0" + } + }, + "node_modules/pumpify/node_modules/readable-stream": { + "version": "3.6.2", + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-3.6.2.tgz", + "integrity": "sha512-9u/sniCrY3D5WdsERHzHE4G2YCXqoG5FTHUiCC4SIbr6XcLZBY05ya9EKjYek9O5xOAwjGq+1JdGBAS7Q9ScoA==", + "dependencies": { + "inherits": "^2.0.3", + "string_decoder": "^1.1.1", + "util-deprecate": "^1.0.1" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/punycode": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/punycode/-/punycode-2.3.1.tgz", + "integrity": "sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg==", + "engines": { + "node": ">=6" + } + }, + "node_modules/quick-format-unescaped": { + "version": "4.0.4", + "resolved": "https://registry.npmjs.org/quick-format-unescaped/-/quick-format-unescaped-4.0.4.tgz", + "integrity": "sha512-tYC1Q1hgyRuHgloV/YXs2w15unPVh8qfu/qCTfhTYamaw7fyhumKa2yGpdSo87vY32rIclj+4fWYQXUMs9EHvg==" + }, + "node_modules/readable-stream": { + "version": "4.5.2", + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-4.5.2.tgz", + "integrity": "sha512-yjavECdqeZ3GLXNgRXgeQEdz9fvDDkNKyHnbHRFtOr7/LcfgBcmct7t/ET+HaCTqfh06OzoAxrkN/IfjJBVe+g==", + "dependencies": { + "abort-controller": "^3.0.0", + "buffer": "^6.0.3", + "events": "^3.3.0", + "process": "^0.11.10", + "string_decoder": "^1.3.0" + }, + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + } + }, + "node_modules/real-require": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/real-require/-/real-require-0.2.0.tgz", + "integrity": "sha512-57frrGM/OCTLqLOAh0mhVA9VBMHd+9U7Zb2THMGdBUoZVOtGbJzjxsYGDJ3A9AYYCP4hn6y1TVbaOfzWtm5GFg==", + "engines": { + "node": ">= 12.13.0" + } + }, + "node_modules/require-from-string": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/require-from-string/-/require-from-string-2.0.2.tgz", + "integrity": "sha512-Xf0nWe6RseziFMu+Ap9biiUbmplq6S9/p+7w7YXP/JBHhrUDDUhwa+vANyubuqfZWTveU//DYVGsDG7RKL/vEw==", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/ret": { + "version": "0.2.2", + "resolved": "https://registry.npmjs.org/ret/-/ret-0.2.2.tgz", + "integrity": "sha512-M0b3YWQs7R3Z917WRQy1HHA7Ba7D8hvZg6UE5mLykJxQVE2ju0IXbGlaHPPlkY+WN7wFP+wUMXmBFA0aV6vYGQ==", + "engines": { + "node": ">=4" + } + }, + "node_modules/reusify": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/reusify/-/reusify-1.0.4.tgz", + "integrity": "sha512-U9nH88a3fc/ekCF1l0/UP1IosiuIjyTh7hBvXVMHYgVcfGvt897Xguj2UOLDeI5BG2m7/uwyaLVT6fbtCwTyzw==", + "engines": { + "iojs": ">=1.0.0", + "node": ">=0.10.0" + } + }, + "node_modules/rfdc": { + "version": "1.3.1", + "resolved": "https://registry.npmjs.org/rfdc/-/rfdc-1.3.1.tgz", + "integrity": "sha512-r5a3l5HzYlIC68TpmYKlxWjmOP6wiPJ1vWv2HeLhNsRZMrCkxeqxiHlQ21oXmQ4F3SiryXBHhAD7JZqvOJjFmg==" + }, + "node_modules/safe-buffer": { + "version": "5.2.1", + "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.1.tgz", + "integrity": "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ] + }, + "node_modules/safe-regex2": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/safe-regex2/-/safe-regex2-2.0.0.tgz", + "integrity": "sha512-PaUSFsUaNNuKwkBijoAPHAK6/eM6VirvyPWlZ7BAQy4D+hCvh4B6lIG+nPdhbFfIbP+gTGBcrdsOaUs0F+ZBOQ==", + "dependencies": { + "ret": "~0.2.0" + } + }, + "node_modules/safe-stable-stringify": { + "version": "2.4.3", + "resolved": "https://registry.npmjs.org/safe-stable-stringify/-/safe-stable-stringify-2.4.3.tgz", + "integrity": "sha512-e2bDA2WJT0wxseVd4lsDP4+3ONX6HpMXQa1ZhFQ7SU+GjvORCmShbCMltrtIDfkYhVHrOcPtj+KhmDBdPdZD1g==", + "engines": { + "node": ">=10" + } + }, + "node_modules/secure-json-parse": { + "version": "2.7.0", + "resolved": "https://registry.npmjs.org/secure-json-parse/-/secure-json-parse-2.7.0.tgz", + "integrity": "sha512-6aU+Rwsezw7VR8/nyvKTx8QpWH9FrcYiXXlqC4z5d5XQBDRqtbfsRjnwGyqbi3gddNtWHuEk9OANUotL26qKUw==" + }, + "node_modules/semver": { + "version": "7.6.0", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.6.0.tgz", + "integrity": "sha512-EnwXhrlwXMk9gKu5/flx5sv/an57AkRplG3hTK68W7FRDN+k+OWBj65M7719OkA82XLBxrcX0KSHj+X5COhOVg==", + "dependencies": { + "lru-cache": "^6.0.0" + }, + "bin": { + "semver": "bin/semver.js" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/set-cookie-parser": { + "version": "2.6.0", + "resolved": "https://registry.npmjs.org/set-cookie-parser/-/set-cookie-parser-2.6.0.tgz", + "integrity": "sha512-RVnVQxTXuerk653XfuliOxBP81Sf0+qfQE73LIYKcyMYHG94AuH0kgrQpRDuTZnSmjpysHmzxJXKNfa6PjFhyQ==" + }, + "node_modules/sift": { + "version": "16.0.1", + "resolved": "https://registry.npmjs.org/sift/-/sift-16.0.1.tgz", + "integrity": "sha512-Wv6BjQ5zbhW7VFefWusVP33T/EM0vYikCaQ2qR8yULbsilAT8/wQaXvuQ3ptGLpoKx+lihJE3y2UTgKDyyNHZQ==" + }, + "node_modules/sonic-boom": { + "version": "3.8.0", + "resolved": "https://registry.npmjs.org/sonic-boom/-/sonic-boom-3.8.0.tgz", + "integrity": "sha512-ybz6OYOUjoQQCQ/i4LU8kaToD8ACtYP+Cj5qd2AO36bwbdewxWJ3ArmJ2cr6AvxlL2o0PqnCcPGUgkILbfkaCA==", + "dependencies": { + "atomic-sleep": "^1.0.0" + } + }, + "node_modules/sparse-bitfield": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/sparse-bitfield/-/sparse-bitfield-3.0.3.tgz", + "integrity": "sha512-kvzhi7vqKTfkh0PZU+2D2PIllw2ymqJKujUcyPMd9Y75Nv4nPbGJZXNhxsgdQab2BmlDct1YnfQCguEvHr7VsQ==", + "dependencies": { + "memory-pager": "^1.0.2" + } + }, + "node_modules/split2": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/split2/-/split2-4.2.0.tgz", + "integrity": "sha512-UcjcJOWknrNkF6PLX83qcHM6KHgVKNkV62Y8a5uYDVv9ydGQVwAHMKqHdJje1VTWpljG0WYpCDhrCdAOYH4TWg==", + "engines": { + "node": ">= 10.x" + } + }, + "node_modules/stream-shift": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/stream-shift/-/stream-shift-1.0.3.tgz", + "integrity": "sha512-76ORR0DO1o1hlKwTbi/DM3EXWGf3ZJYO8cXX5RJwnul2DEg2oyoZyjLNoQM8WsvZiFKCRfC1O0J7iCvie3RZmQ==" + }, + "node_modules/string_decoder": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.3.0.tgz", + "integrity": "sha512-hkRX8U1WjJFd8LsDJ2yQ/wWWxaopEsABU1XfkM8A+j0+85JAGppt16cr1Whg6KIbb4okU6Mql6BOj+uup/wKeA==", + "dependencies": { + "safe-buffer": "~5.2.0" + } + }, + "node_modules/thread-stream": { + "version": "2.4.1", + "resolved": "https://registry.npmjs.org/thread-stream/-/thread-stream-2.4.1.tgz", + "integrity": "sha512-d/Ex2iWd1whipbT681JmTINKw0ZwOUBZm7+Gjs64DHuX34mmw8vJL2bFAaNacaW72zYiTJxSHi5abUuOi5nsfg==", + "dependencies": { + "real-require": "^0.2.0" + } + }, + "node_modules/through2": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/through2/-/through2-2.0.5.tgz", + "integrity": "sha512-/mrRod8xqpA+IHSLyGCQ2s8SPHiCDEeQJSep1jqLYeEUClOFG2Qsh+4FU6G9VeqpZnGW/Su8LQGc4YKni5rYSQ==", + "dependencies": { + "readable-stream": "~2.3.6", + "xtend": "~4.0.1" + } + }, + "node_modules/through2/node_modules/readable-stream": { + "version": "2.3.8", + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-2.3.8.tgz", + "integrity": "sha512-8p0AUk4XODgIewSi0l8Epjs+EVnWiK7NoDIEGU0HhE7+ZyY8D1IMY7odu5lRrFXGg71L15KG8QrPmum45RTtdA==", + "dependencies": { + "core-util-is": "~1.0.0", + "inherits": "~2.0.3", + "isarray": "~1.0.0", + "process-nextick-args": "~2.0.0", + "safe-buffer": "~5.1.1", + "string_decoder": "~1.1.1", + "util-deprecate": "~1.0.1" + } + }, + "node_modules/through2/node_modules/safe-buffer": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.2.tgz", + "integrity": "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==" + }, + "node_modules/through2/node_modules/string_decoder": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.1.1.tgz", + "integrity": "sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg==", + "dependencies": { + "safe-buffer": "~5.1.0" + } + }, + "node_modules/toad-cache": { + "version": "3.7.0", + "resolved": "https://registry.npmjs.org/toad-cache/-/toad-cache-3.7.0.tgz", + "integrity": "sha512-/m8M+2BJUpoJdgAHoG+baCwBT+tf2VraSfkBgl0Y00qIWt41DJ8R5B8nsEw0I58YwF5IZH6z24/2TobDKnqSWw==", + "engines": { + "node": ">=12" + } + }, + "node_modules/tr46": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/tr46/-/tr46-4.1.1.tgz", + "integrity": "sha512-2lv/66T7e5yNyhAAC4NaKe5nVavzuGJQVVtRYLyQ2OI8tsJ61PMLlelehb0wi2Hx6+hT/OJUWZcw8MjlSRnxvw==", + "dependencies": { + "punycode": "^2.3.0" + }, + "engines": { + "node": ">=14" + } + }, + "node_modules/uri-js": { + "version": "4.4.1", + "resolved": "https://registry.npmjs.org/uri-js/-/uri-js-4.4.1.tgz", + "integrity": "sha512-7rKUyy33Q1yc98pQ1DAmLtwX109F7TIfWlW1Ydo8Wl1ii1SeHieeh0HHfPeL2fMXK6z0s8ecKs9frCuLJvndBg==", + "dependencies": { + "punycode": "^2.1.0" + } + }, + "node_modules/util-deprecate": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz", + "integrity": "sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==" + }, + "node_modules/webidl-conversions": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/webidl-conversions/-/webidl-conversions-7.0.0.tgz", + "integrity": "sha512-VwddBukDzu71offAQR975unBIGqfKZpM+8ZX6ySk8nYhVoo5CYaZyzt3YBvYtRtO+aoGlqxPg/B87NGVZ/fu6g==", + "engines": { + "node": ">=12" + } + }, + "node_modules/whatwg-url": { + "version": "13.0.0", + "resolved": "https://registry.npmjs.org/whatwg-url/-/whatwg-url-13.0.0.tgz", + "integrity": "sha512-9WWbymnqj57+XEuqADHrCJ2eSXzn8WXIW/YSGaZtb2WKAInQ6CHfaUUcTyyver0p8BDg5StLQq8h1vtZuwmOig==", + "dependencies": { + "tr46": "^4.1.1", + "webidl-conversions": "^7.0.0" + }, + "engines": { + "node": ">=16" + } + }, + "node_modules/wrappy": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz", + "integrity": "sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==" + }, + "node_modules/xtend": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/xtend/-/xtend-4.0.2.tgz", + "integrity": "sha512-LKYU1iAXJXUgAXn9URjiu+MWhyUXHsvfp7mcuYm9dSUKK0/CjtrUwFAxD82/mCWbtLsGjFIad0wIsod4zrTAEQ==", + "engines": { + "node": ">=0.4" + } + }, + "node_modules/yallist": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/yallist/-/yallist-4.0.0.tgz", + "integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==" + } + } +} diff --git a/10-fastify/03-feed-api-improved/package.json b/10-fastify/03-feed-api-improved/package.json new file mode 100644 index 0000000..60a4f95 --- /dev/null +++ b/10-fastify/03-feed-api-improved/package.json @@ -0,0 +1,19 @@ +{ + "name": "03-feed-api-improved", + "version": "1.0.0", + "description": "", + "main": "index.js", + "scripts": { + "test": "echo \"Error: no test specified\" && exit 1" + }, + "keywords": [], + "author": "", + "license": "ISC", + "dependencies": { + "@fastify/compress": "^7.0.0", + "date-fns": "^3.3.1", + "fastify": "^4.26.1", + "htmlparser2": "^9.1.0", + "mongoose": "^8.2.0" + } +} diff --git a/10-fastify/03-feed-api-improved/plugins/async-utils.mjs b/10-fastify/03-feed-api-improved/plugins/async-utils.mjs new file mode 100644 index 0000000..243667e --- /dev/null +++ b/10-fastify/03-feed-api-improved/plugins/async-utils.mjs @@ -0,0 +1,9 @@ +async function asyncUtils(fastify, opts) { + fastify.decorate("waitAndCountFulfilled", async function (proms) { + const promsSettled = await Promise.allSettled(proms); + return promsSettled.filter((r) => r.status === "fulfilled").length; + }); +} + +asyncUtils[Symbol.for("skip-override")] = true; +export default asyncUtils; diff --git a/10-fastify/03-feed-api-improved/routes/feeds-update.mjs b/10-fastify/03-feed-api-improved/routes/feeds-update.mjs new file mode 100644 index 0000000..c62ce84 --- /dev/null +++ b/10-fastify/03-feed-api-improved/routes/feeds-update.mjs @@ -0,0 +1,32 @@ +import { Source } from "../models/Source.mjs"; +import { FeedEntry } from "../models/FeedEntry.mjs"; +import getFeedEntries from "../get-feeds.mjs"; + +function generateFeedEntries(entries, s) { + return entries.map((en) => { + return new FeedEntry({ + source: s._id, + title: en.title, + link: en.link, + date: new Date(en.pubDate), + }).save(); + }); +} + +async function feedsUpdateRoutes(fastify, opts) { + fastify.post("/", async (request, reply) => { + const sources = await Source.find({}).exec(); + let totalNewEntries = 0; + for (const s of sources) { + const entries = await getFeedEntries(s.url); + const savingModels = generateFeedEntries(entries, s); + let newCount = await fastify.waitAndCountFulfilled(savingModels); + totalNewEntries += newCount; + request.log.info( + `Feed ${s.url}: found ${entries.length} and saved ${newCount} entries` + ); + } + return { totalNewEntries }; + }); +} +export default feedsUpdateRoutes; diff --git a/10-fastify/03-feed-api-improved/routes/feeds.mjs b/10-fastify/03-feed-api-improved/routes/feeds.mjs new file mode 100644 index 0000000..eecfb47 --- /dev/null +++ b/10-fastify/03-feed-api-improved/routes/feeds.mjs @@ -0,0 +1,39 @@ +import { FeedEntry } from "../models/FeedEntry.mjs"; + +async function feedsRoutes(fastify, opts) { + fastify.route({ + method: "GET", + url: "/", + schema: { + querystring: { + sourceId: { type: "string" }, + }, + response: { + 200: { + type: "array", + items: { + type: "object", + properties: { + source: { type: "string" }, + title: { type: "string" }, + link: { type: "string" }, + date: { type: "string" }, + }, + }, + }, + }, + }, + handler: async (request, reply) => { + let findQuery = {}; + if (request.query.sourceId) { + findQuery.source = request.query.sourceId; + } + const feedEntries = await FeedEntry.find(findQuery) + .sort({ date: -1 }) + .limit(20) + .exec(); + return feedEntries; + }, + }); +} +export default feedsRoutes; diff --git a/10-fastify/03-feed-api-improved/routes/sources.mjs b/10-fastify/03-feed-api-improved/routes/sources.mjs new file mode 100644 index 0000000..caeb567 --- /dev/null +++ b/10-fastify/03-feed-api-improved/routes/sources.mjs @@ -0,0 +1,62 @@ +import { Source } from "../models/Source.mjs"; + +async function sourcesRoutes(fastify, opts) { + const postFeedsSchema = { + body: { + type: "object", + required: ["url"], + properties: { + url: { type: "string" }, + }, + }, + }; + fastify.post("/", { schema: postFeedsSchema }, async (request, reply) => { + const source = new Source({ url: request.body.url }); + request.log.info(`Adding new Source: ${source}`); + try { + await source.save(); + return source; + } catch (e) { + const reason = Source.getErrorReason(e); + if (reason) { + const err = new Error(reason); + err.statusCode = 400; + throw err; + } else { + throw new Error("An error occurred during Source.save()"); + } + } + }); + + const getSourcesSchema = { + response: { + 200: { + type: "array", + items: { + type: "object", + properties: { + _id: { type: "string" }, + url: { type: "string" }, + }, + }, + }, + }, + }; + fastify.get("/", { schema: getSourcesSchema }, async (request, reply) => { + const sources = await Source.find({}).exec(); + return sources; + }); + + fastify.delete("/:id", async (request, reply) => { + try { + await Source.findByIdAndDelete(request.params.id).exec(); + return reply.code(200).send(); + } catch { + throw new Error( + `Error while deleting Source with id ${request.params.id}` + ); + } + }); +} + +export default sourcesRoutes; diff --git a/10-fastify/03-feed-api-improved/server.mjs b/10-fastify/03-feed-api-improved/server.mjs new file mode 100644 index 0000000..1eef07c --- /dev/null +++ b/10-fastify/03-feed-api-improved/server.mjs @@ -0,0 +1,29 @@ +import Fastify from "fastify"; +const fastify = Fastify({ + logger: true, +}); +import mongoose from "mongoose"; +import compress from "@fastify/compress"; + +import feedsRoutes from "./routes/feeds.mjs"; +import sourcesRoutes from "./routes/sources.mjs"; +import feedsUpdateRoutes from "./routes/feeds-update.mjs"; +import asyncUtils from "./plugins/async-utils.mjs"; + +await mongoose.connect("mongodb://127.0.0.1:27017/feed-reader"); + +fastify.addHook("onSend", async (request, reply, payload) => { + reply.header("server", "fastify"); +}); +fastify.register(compress); +fastify.register(asyncUtils); +fastify.register(feedsRoutes, { prefix: "/feeds" }); +fastify.register(sourcesRoutes, { prefix: "/sources" }); +fastify.register(feedsUpdateRoutes, { prefix: "/feeds-update" }); + +try { + await fastify.listen({ port: 3000 }); +} catch (err) { + fastify.log.error(err); + process.exit(1); +}