├── .gitignore ├── DEPLOYMENT.md ├── README.md ├── deploy.sh ├── package.json ├── server ├── .env ├── Caddyfile ├── Dockerfile ├── README.md ├── build │ ├── config │ │ └── default.js │ └── src │ │ ├── app.js │ │ ├── controller │ │ └── shortUrl.controller.js │ │ ├── db.js │ │ ├── middleware │ │ └── validateResourse.js │ │ ├── models │ │ ├── analytics.model.js │ │ └── shortUrl.model.js │ │ ├── routes │ │ └── index.js │ │ └── schemas │ │ └── createShortUrl.schema.js ├── config │ ├── custom-environment-variables.ts │ ├── default.ts │ └── production.ts ├── docker-compose.yml ├── nodemon.json ├── package.json ├── process.json ├── src │ ├── app.ts │ ├── controller │ │ └── shortUrl.controller.ts │ ├── db.ts │ ├── middleware │ │ └── validateResourse.ts │ ├── models │ │ ├── analytics.model.ts │ │ └── shortUrl.model.ts │ ├── routes │ │ └── index.ts │ └── schemas │ │ └── createShortUrl.schema.ts ├── tsconfig.json └── yarn.lock ├── ui ├── README.md ├── UI_README.md ├── package.json ├── public │ ├── favicon.ico │ ├── index.html │ ├── logo192.png │ ├── logo512.png │ ├── manifest.json │ └── robots.txt ├── src │ ├── App.test.tsx │ ├── App.tsx │ ├── components │ │ ├── Background.tsx │ │ └── URLShortenerForm.tsx │ ├── config │ │ └── index.ts │ ├── containers │ │ ├── HandleRedirect.tsx │ │ └── Home.tsx │ ├── index.css │ ├── index.tsx │ ├── react-app-env.d.ts │ ├── reportWebVitals.ts │ └── setupTests.ts ├── tsconfig.json └── yarn.lock ├── yarn-error.log └── yarn.lock /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | build -------------------------------------------------------------------------------- /DEPLOYMENT.md: -------------------------------------------------------------------------------- 1 | # Deployment 2 | ## 1.0 Add env to backend 3 | 1.1 Install dotenv 4 | 1.2 Add custom-environment-variables.ts 5 | 6 | ## 2.0 Configuration 7 | 2.1 Add env_file to docker-compose 8 | 2.2 Configure Caddyfile 9 | 2.3 Create build script 10 | 11 | ## 3.0 Deployment 12 | 3.1 Push code to GitHub 13 | 3.2 Create DigitalOcean droplet 14 | 3.3 Configure domain 15 | 3.4 Install git on droplet `apt-get update && apt-get install git` 16 | 3.5 Pull repository 17 | 3.6 Build and deploy 18 | 19 | ## 4.0 Deploy UI 20 | 4.1 Deploy UI on Vercel -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | ## Let's keep in touch 2 | - [Subscribe on YouTube](https://www.youtube.com/TomDoesTech) 3 | - [Discord](https://discord.gg/4ae2Esm6P7) 4 | - [Twitter](https://twitter.com/tomdoes_tech) 5 | - [TikTok](https://www.tiktok.com/@tomdoestech) 6 | - [Facebook](https://www.facebook.com/tomdoestech) 7 | - [Instagram](https://www.instagram.com/tomdoestech) 8 | 9 | [Buy me a Coffee](https://www.buymeacoffee.com/tomn) 10 | 11 | [Sign up to DigitalOcean 💖](https://m.do.co/c/1b74cb8c56f4) 12 | -------------------------------------------------------------------------------- /deploy.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | git pull 4 | 5 | echo "Building server" 6 | docker-compose -f ./server/docker-compose.yml up -d --build 7 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "dependencies": { 3 | "dotenv": "^8.2.0" 4 | } 5 | } 6 | -------------------------------------------------------------------------------- /server/.env: -------------------------------------------------------------------------------- 1 | CORS_ORIGIN=google.com -------------------------------------------------------------------------------- /server/Caddyfile: -------------------------------------------------------------------------------- 1 | short-api.snipd.io { 2 | reverse_proxy url-shortener-server:4000 { 3 | header_down Strict-Transport-Security max-age=31536000; 4 | } 5 | } -------------------------------------------------------------------------------- /server/Dockerfile: -------------------------------------------------------------------------------- 1 | FROM node:12-alpine 2 | 3 | ADD package.json /tmp/package.json 4 | 5 | RUN rm -rf build 6 | 7 | RUN cd /tmp && npm install -q 8 | 9 | RUN npm dedupe 10 | 11 | # Code base 12 | ADD ./ /src 13 | RUN rm -rf /src/node_modules && cp -a /tmp/node_modules /src/ 14 | 15 | # Define working directory 16 | WORKDIR /src 17 | 18 | RUN npm run-script build 19 | 20 | CMD ["node", "build/src/app.js"] 21 | 22 | -------------------------------------------------------------------------------- /server/README.md: -------------------------------------------------------------------------------- 1 | # URL Shortener backend 2 | ## 1.0 Setup application 3 | 4 | 1.1 Initialize project yarn init 5 | 6 | 1.2 Initialize TypeScript npx typescript --init 7 | 8 | 1.3 Create required files 9 | 10 | 1.4 Setup nodemon 11 | 12 | 1.5 Install required packages 13 | 14 | 1.6 Setup config 15 | 16 | ## 2.0 Add create route 17 | 18 | 2.1 Add model 19 | 20 | 2.2 Add route 21 | 22 | 2.3 Add controller 23 | 24 | ## 3.0 Add analytics 25 | 26 | 3.1 Create analytics model 27 | 28 | 3.2 Add to route 29 | 30 | ## 4.0 Containerize with Docker 31 | 32 | 4.1 Add Dockerfile 33 | 34 | 4.2 Add docker-compose -------------------------------------------------------------------------------- /server/build/config/default.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | Object.defineProperty(exports, "__esModule", { value: true }); 3 | exports.default = { 4 | port: 4000, 5 | dbUri: "mongodb://localhost:27017/url-shortener", 6 | }; 7 | -------------------------------------------------------------------------------- /server/build/src/app.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | var __importDefault = (this && this.__importDefault) || function (mod) { 3 | return (mod && mod.__esModule) ? mod : { "default": mod }; 4 | }; 5 | Object.defineProperty(exports, "__esModule", { value: true }); 6 | var express_1 = __importDefault(require("express")); 7 | var config_1 = __importDefault(require("config")); 8 | var body_parser_1 = __importDefault(require("body-parser")); 9 | var routes_1 = __importDefault(require("./routes")); 10 | var db_1 = __importDefault(require("./db")); 11 | var app = express_1.default(); 12 | var port = config_1.default.get("port"); 13 | // parse application/json 14 | app.use(body_parser_1.default.json()); 15 | app.listen(port, function () { 16 | console.log("Application listening at http://localhost:" + port); 17 | db_1.default(); 18 | routes_1.default(app); 19 | }); 20 | -------------------------------------------------------------------------------- /server/build/src/controller/shortUrl.controller.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) { 3 | function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); } 4 | return new (P || (P = Promise))(function (resolve, reject) { 5 | function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } } 6 | function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } } 7 | function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); } 8 | step((generator = generator.apply(thisArg, _arguments || [])).next()); 9 | }); 10 | }; 11 | var __generator = (this && this.__generator) || function (thisArg, body) { 12 | var _ = { label: 0, sent: function() { if (t[0] & 1) throw t[1]; return t[1]; }, trys: [], ops: [] }, f, y, t, g; 13 | return g = { next: verb(0), "throw": verb(1), "return": verb(2) }, typeof Symbol === "function" && (g[Symbol.iterator] = function() { return this; }), g; 14 | function verb(n) { return function (v) { return step([n, v]); }; } 15 | function step(op) { 16 | if (f) throw new TypeError("Generator is already executing."); 17 | while (_) try { 18 | if (f = 1, y && (t = op[0] & 2 ? y["return"] : op[0] ? y["throw"] || ((t = y["return"]) && t.call(y), 0) : y.next) && !(t = t.call(y, op[1])).done) return t; 19 | if (y = 0, t) op = [op[0] & 2, t.value]; 20 | switch (op[0]) { 21 | case 0: case 1: t = op; break; 22 | case 4: _.label++; return { value: op[1], done: false }; 23 | case 5: _.label++; y = op[1]; op = [0]; continue; 24 | case 7: op = _.ops.pop(); _.trys.pop(); continue; 25 | default: 26 | if (!(t = _.trys, t = t.length > 0 && t[t.length - 1]) && (op[0] === 6 || op[0] === 2)) { _ = 0; continue; } 27 | if (op[0] === 3 && (!t || (op[1] > t[0] && op[1] < t[3]))) { _.label = op[1]; break; } 28 | if (op[0] === 6 && _.label < t[1]) { _.label = t[1]; t = op; break; } 29 | if (t && _.label < t[2]) { _.label = t[2]; _.ops.push(op); break; } 30 | if (t[2]) _.ops.pop(); 31 | _.trys.pop(); continue; 32 | } 33 | op = body.call(thisArg, _); 34 | } catch (e) { op = [6, e]; y = 0; } finally { f = t = 0; } 35 | if (op[0] & 5) throw op[1]; return { value: op[0] ? op[1] : void 0, done: true }; 36 | } 37 | }; 38 | var __importDefault = (this && this.__importDefault) || function (mod) { 39 | return (mod && mod.__esModule) ? mod : { "default": mod }; 40 | }; 41 | Object.defineProperty(exports, "__esModule", { value: true }); 42 | exports.getAnalytics = exports.handleRedirect = exports.createShortUrl = void 0; 43 | var shortUrl_model_1 = __importDefault(require("../models/shortUrl.model")); 44 | var analytics_model_1 = __importDefault(require("../models/analytics.model")); 45 | function createShortUrl(req, res) { 46 | return __awaiter(this, void 0, void 0, function () { 47 | var destination, newUrl; 48 | return __generator(this, function (_a) { 49 | switch (_a.label) { 50 | case 0: 51 | destination = req.body.destination; 52 | return [4 /*yield*/, shortUrl_model_1.default.create({ destination: destination })]; 53 | case 1: 54 | newUrl = _a.sent(); 55 | // Return the shortUrl 56 | return [2 /*return*/, res.send(newUrl)]; 57 | } 58 | }); 59 | }); 60 | } 61 | exports.createShortUrl = createShortUrl; 62 | function handleRedirect(req, res) { 63 | return __awaiter(this, void 0, void 0, function () { 64 | var shortId, short; 65 | return __generator(this, function (_a) { 66 | switch (_a.label) { 67 | case 0: 68 | shortId = req.params.shortId; 69 | return [4 /*yield*/, shortUrl_model_1.default.findOne({ shortId: shortId }).lean()]; 70 | case 1: 71 | short = _a.sent(); 72 | if (!short) { 73 | return [2 /*return*/, res.sendStatus(404)]; 74 | } 75 | analytics_model_1.default.create({ shortUrl: short._id }); 76 | return [2 /*return*/, res.redirect(short.destination)]; 77 | } 78 | }); 79 | }); 80 | } 81 | exports.handleRedirect = handleRedirect; 82 | function getAnalytics(req, res) { 83 | return __awaiter(this, void 0, void 0, function () { 84 | var data; 85 | return __generator(this, function (_a) { 86 | switch (_a.label) { 87 | case 0: return [4 /*yield*/, analytics_model_1.default.find({}).lean()]; 88 | case 1: 89 | data = _a.sent(); 90 | return [2 /*return*/, res.send(data)]; 91 | } 92 | }); 93 | }); 94 | } 95 | exports.getAnalytics = getAnalytics; 96 | -------------------------------------------------------------------------------- /server/build/src/db.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) { 3 | function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); } 4 | return new (P || (P = Promise))(function (resolve, reject) { 5 | function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } } 6 | function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } } 7 | function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); } 8 | step((generator = generator.apply(thisArg, _arguments || [])).next()); 9 | }); 10 | }; 11 | var __generator = (this && this.__generator) || function (thisArg, body) { 12 | var _ = { label: 0, sent: function() { if (t[0] & 1) throw t[1]; return t[1]; }, trys: [], ops: [] }, f, y, t, g; 13 | return g = { next: verb(0), "throw": verb(1), "return": verb(2) }, typeof Symbol === "function" && (g[Symbol.iterator] = function() { return this; }), g; 14 | function verb(n) { return function (v) { return step([n, v]); }; } 15 | function step(op) { 16 | if (f) throw new TypeError("Generator is already executing."); 17 | while (_) try { 18 | if (f = 1, y && (t = op[0] & 2 ? y["return"] : op[0] ? y["throw"] || ((t = y["return"]) && t.call(y), 0) : y.next) && !(t = t.call(y, op[1])).done) return t; 19 | if (y = 0, t) op = [op[0] & 2, t.value]; 20 | switch (op[0]) { 21 | case 0: case 1: t = op; break; 22 | case 4: _.label++; return { value: op[1], done: false }; 23 | case 5: _.label++; y = op[1]; op = [0]; continue; 24 | case 7: op = _.ops.pop(); _.trys.pop(); continue; 25 | default: 26 | if (!(t = _.trys, t = t.length > 0 && t[t.length - 1]) && (op[0] === 6 || op[0] === 2)) { _ = 0; continue; } 27 | if (op[0] === 3 && (!t || (op[1] > t[0] && op[1] < t[3]))) { _.label = op[1]; break; } 28 | if (op[0] === 6 && _.label < t[1]) { _.label = t[1]; t = op; break; } 29 | if (t && _.label < t[2]) { _.label = t[2]; _.ops.push(op); break; } 30 | if (t[2]) _.ops.pop(); 31 | _.trys.pop(); continue; 32 | } 33 | op = body.call(thisArg, _); 34 | } catch (e) { op = [6, e]; y = 0; } finally { f = t = 0; } 35 | if (op[0] & 5) throw op[1]; return { value: op[0] ? op[1] : void 0, done: true }; 36 | } 37 | }; 38 | var __importDefault = (this && this.__importDefault) || function (mod) { 39 | return (mod && mod.__esModule) ? mod : { "default": mod }; 40 | }; 41 | Object.defineProperty(exports, "__esModule", { value: true }); 42 | var mongoose_1 = __importDefault(require("mongoose")); 43 | var config_1 = __importDefault(require("config")); 44 | function db() { 45 | return __awaiter(this, void 0, void 0, function () { 46 | var dbUri, e_1; 47 | return __generator(this, function (_a) { 48 | switch (_a.label) { 49 | case 0: 50 | dbUri = config_1.default.get("dbUri"); 51 | _a.label = 1; 52 | case 1: 53 | _a.trys.push([1, 3, , 4]); 54 | return [4 /*yield*/, mongoose_1.default 55 | .connect(dbUri, { useNewUrlParser: true, useUnifiedTopology: true }) 56 | .then(function () { 57 | console.log("DB connected to " + dbUri); 58 | })]; 59 | case 2: 60 | _a.sent(); 61 | return [3 /*break*/, 4]; 62 | case 3: 63 | e_1 = _a.sent(); 64 | console.error(e_1); 65 | return [3 /*break*/, 4]; 66 | case 4: return [2 /*return*/]; 67 | } 68 | }); 69 | }); 70 | } 71 | exports.default = db; 72 | -------------------------------------------------------------------------------- /server/build/src/middleware/validateResourse.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) { 3 | function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); } 4 | return new (P || (P = Promise))(function (resolve, reject) { 5 | function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } } 6 | function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } } 7 | function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); } 8 | step((generator = generator.apply(thisArg, _arguments || [])).next()); 9 | }); 10 | }; 11 | var __generator = (this && this.__generator) || function (thisArg, body) { 12 | var _ = { label: 0, sent: function() { if (t[0] & 1) throw t[1]; return t[1]; }, trys: [], ops: [] }, f, y, t, g; 13 | return g = { next: verb(0), "throw": verb(1), "return": verb(2) }, typeof Symbol === "function" && (g[Symbol.iterator] = function() { return this; }), g; 14 | function verb(n) { return function (v) { return step([n, v]); }; } 15 | function step(op) { 16 | if (f) throw new TypeError("Generator is already executing."); 17 | while (_) try { 18 | if (f = 1, y && (t = op[0] & 2 ? y["return"] : op[0] ? y["throw"] || ((t = y["return"]) && t.call(y), 0) : y.next) && !(t = t.call(y, op[1])).done) return t; 19 | if (y = 0, t) op = [op[0] & 2, t.value]; 20 | switch (op[0]) { 21 | case 0: case 1: t = op; break; 22 | case 4: _.label++; return { value: op[1], done: false }; 23 | case 5: _.label++; y = op[1]; op = [0]; continue; 24 | case 7: op = _.ops.pop(); _.trys.pop(); continue; 25 | default: 26 | if (!(t = _.trys, t = t.length > 0 && t[t.length - 1]) && (op[0] === 6 || op[0] === 2)) { _ = 0; continue; } 27 | if (op[0] === 3 && (!t || (op[1] > t[0] && op[1] < t[3]))) { _.label = op[1]; break; } 28 | if (op[0] === 6 && _.label < t[1]) { _.label = t[1]; t = op; break; } 29 | if (t && _.label < t[2]) { _.label = t[2]; _.ops.push(op); break; } 30 | if (t[2]) _.ops.pop(); 31 | _.trys.pop(); continue; 32 | } 33 | op = body.call(thisArg, _); 34 | } catch (e) { op = [6, e]; y = 0; } finally { f = t = 0; } 35 | if (op[0] & 5) throw op[1]; return { value: op[0] ? op[1] : void 0, done: true }; 36 | } 37 | }; 38 | Object.defineProperty(exports, "__esModule", { value: true }); 39 | var validateResource = function (resourceSchema) { return function (req, res, next) { return __awaiter(void 0, void 0, void 0, function () { 40 | var e_1; 41 | return __generator(this, function (_a) { 42 | switch (_a.label) { 43 | case 0: 44 | _a.trys.push([0, 2, , 3]); 45 | return [4 /*yield*/, resourceSchema.validate({ 46 | body: req.body, 47 | query: req.query, 48 | params: req.params, 49 | })]; 50 | case 1: 51 | _a.sent(); 52 | next(); 53 | return [3 /*break*/, 3]; 54 | case 2: 55 | e_1 = _a.sent(); 56 | return [2 /*return*/, res.sendStatus(400)]; 57 | case 3: return [2 /*return*/]; 58 | } 59 | }); 60 | }); }; }; 61 | exports.default = validateResource; 62 | -------------------------------------------------------------------------------- /server/build/src/models/analytics.model.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | var __importDefault = (this && this.__importDefault) || function (mod) { 3 | return (mod && mod.__esModule) ? mod : { "default": mod }; 4 | }; 5 | Object.defineProperty(exports, "__esModule", { value: true }); 6 | var mongoose_1 = __importDefault(require("mongoose")); 7 | var schema = new mongoose_1.default.Schema({ 8 | shortUrl: { 9 | type: mongoose_1.default.Schema.Types.ObjectId, 10 | ref: "shortUrl", 11 | required: true, 12 | }, 13 | }, { timestamps: true }); 14 | var analytics = mongoose_1.default.model("analytics", schema); 15 | exports.default = analytics; 16 | -------------------------------------------------------------------------------- /server/build/src/models/shortUrl.model.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | var __importDefault = (this && this.__importDefault) || function (mod) { 3 | return (mod && mod.__esModule) ? mod : { "default": mod }; 4 | }; 5 | Object.defineProperty(exports, "__esModule", { value: true }); 6 | var mongoose_1 = __importDefault(require("mongoose")); 7 | var nanoid_1 = require("nanoid"); 8 | var nanoid = nanoid_1.customAlphabet("abcdefghijklmnopqrstuv0987654321", 6); 9 | var schema = new mongoose_1.default.Schema({ 10 | shortId: { 11 | type: String, 12 | unique: true, 13 | required: true, 14 | default: function () { return nanoid(); }, 15 | }, 16 | destination: { type: String, required: true }, 17 | }); 18 | var shortUrl = mongoose_1.default.model("shortUrl", schema); 19 | exports.default = shortUrl; 20 | -------------------------------------------------------------------------------- /server/build/src/routes/index.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | var __importDefault = (this && this.__importDefault) || function (mod) { 3 | return (mod && mod.__esModule) ? mod : { "default": mod }; 4 | }; 5 | Object.defineProperty(exports, "__esModule", { value: true }); 6 | var shortUrl_controller_1 = require("../controller/shortUrl.controller"); 7 | var validateResourse_1 = __importDefault(require("../middleware/validateResourse")); 8 | var createShortUrl_schema_1 = __importDefault(require("../schemas/createShortUrl.schema")); 9 | function routes(app) { 10 | app.get("/healthcheck", function (req, res) { 11 | return res.send("App is healthy"); 12 | }); 13 | app.post("/api/url", validateResourse_1.default(createShortUrl_schema_1.default), shortUrl_controller_1.createShortUrl); 14 | app.get("/:shortId", shortUrl_controller_1.handleRedirect); 15 | app.get("/api/analytics", shortUrl_controller_1.getAnalytics); 16 | } 17 | exports.default = routes; 18 | -------------------------------------------------------------------------------- /server/build/src/schemas/createShortUrl.schema.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | Object.defineProperty(exports, "__esModule", { value: true }); 3 | var yup_1 = require("yup"); 4 | exports.default = yup_1.object({ 5 | destination: yup_1.string().required("Destination is required"), 6 | }); 7 | -------------------------------------------------------------------------------- /server/config/custom-environment-variables.ts: -------------------------------------------------------------------------------- 1 | export default { 2 | port: "PORT", 3 | dbUri: "DB_URI", 4 | corsOrigin: "CORS_ORIGIN", 5 | }; 6 | -------------------------------------------------------------------------------- /server/config/default.ts: -------------------------------------------------------------------------------- 1 | export default { 2 | port: 4000, 3 | dbUri: "mongodb://localhost:27017/url-shortener", 4 | corsOrigin: "http://localhost:3000", 5 | }; 6 | -------------------------------------------------------------------------------- /server/config/production.ts: -------------------------------------------------------------------------------- 1 | export default { 2 | port: 4000, 3 | dbUri: 4 | "mongodb+srv://...", 5 | }; 6 | -------------------------------------------------------------------------------- /server/docker-compose.yml: -------------------------------------------------------------------------------- 1 | version: '3.7' 2 | services: 3 | url-shortener-server: 4 | restart: always 5 | container_name: url-shortener-server 6 | env_file: 7 | - .env 8 | environment: 9 | - NODE_ENV=production 10 | build: 11 | context: ./ 12 | image: url-shortener-server 13 | 14 | caddy: 15 | image: caddy/caddy:2.2.1-alpine 16 | container_name: caddy-service 17 | restart: unless-stopped 18 | ports: 19 | - "80:80" 20 | - "443:443" 21 | volumes: 22 | - $PWD/server/Caddyfile:/etc/caddy/Caddyfile 23 | - $PWD/server/site:/srv 24 | - caddy_data:/data 25 | - caddy_config:/config 26 | 27 | volumes: 28 | caddy_data: 29 | caddy_config: -------------------------------------------------------------------------------- /server/nodemon.json: -------------------------------------------------------------------------------- 1 | { 2 | "restartable": "rs", 3 | "ignore": [".git", "node_modules/", "dist/", "coverage/"], 4 | "watch": ["src/"], 5 | "execMap": { 6 | "ts": "node -r ts-node/register" 7 | }, 8 | "env": { 9 | "NODE_ENV": "development" 10 | }, 11 | "ext": "js,json,ts" 12 | } -------------------------------------------------------------------------------- /server/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "server", 3 | "version": "1.0.0", 4 | "main": "index.js", 5 | "license": "MIT", 6 | "scripts": { 7 | "dev": "nodemon --config nodemon.json src/app.ts", 8 | "build": "tsc" 9 | }, 10 | "dependencies": { 11 | "body-parser": "^1.19.0", 12 | "config": "^3.3.6", 13 | "cors": "^2.8.5", 14 | "dotenv": "^8.2.0", 15 | "express": "^4.17.1", 16 | "mongoose": "^5.12.2", 17 | "nanoid": "^3.1.22", 18 | "yup": "^0.32.9" 19 | }, 20 | "devDependencies": { 21 | "@types/body-parser": "^1.19.0", 22 | "@types/config": "^0.0.38", 23 | "@types/cors": "^2.8.10", 24 | "@types/express": "^4.17.11", 25 | "@types/nanoid": "^2.1.0", 26 | "@types/node": "^14.14.37", 27 | "@types/yup": "^0.29.11", 28 | "ts-node": "^9.1.1", 29 | "typescript": "^4.2.3" 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /server/process.json: -------------------------------------------------------------------------------- 1 | { 2 | "apps": [ 3 | { 4 | "name": "url-shortener-server", 5 | "script": "build/src/app.js", 6 | "watch": false, 7 | "instances": "max", 8 | "exec_mode": "cluster", 9 | "env": { 10 | "NODE_ENV": "production" 11 | } 12 | } 13 | ] 14 | } 15 | -------------------------------------------------------------------------------- /server/src/app.ts: -------------------------------------------------------------------------------- 1 | import express from "express"; 2 | import config from "config"; 3 | import bodyParser from "body-parser"; 4 | import cors from "cors"; 5 | import routes from "./routes"; 6 | import db from "./db"; 7 | 8 | const app = express(); 9 | 10 | app.use( 11 | cors({ 12 | origin: config.get("corsOrigin"), 13 | }) 14 | ); 15 | 16 | const port = config.get("port") as number; 17 | 18 | // parse application/json 19 | app.use(bodyParser.json()); 20 | 21 | app.listen(port, "0.0.0.0", () => { 22 | console.log(`Application listening at http://localhost:${port}`); 23 | db(); 24 | routes(app); 25 | }); 26 | -------------------------------------------------------------------------------- /server/src/controller/shortUrl.controller.ts: -------------------------------------------------------------------------------- 1 | import { Request, Response } from "express"; 2 | import shortUrl from "../models/shortUrl.model"; 3 | import analytics from "../models/analytics.model"; 4 | 5 | export async function createShortUrl(req: Request, res: Response) { 6 | // Get the destination from the request body 7 | const { destination } = req.body; 8 | 9 | // Create a shortUrl 10 | const newUrl = await shortUrl.create({ destination }); 11 | 12 | // Return the shortUrl 13 | return res.send(newUrl); 14 | } 15 | 16 | export async function handleRedirect(req: Request, res: Response) { 17 | const { shortId } = req.params; 18 | 19 | const short = await shortUrl.findOne({ shortId }).lean(); 20 | 21 | if (!short) { 22 | return res.sendStatus(404); 23 | } 24 | 25 | analytics.create({ shortUrl: short._id }); 26 | 27 | return res.redirect(short.destination); 28 | } 29 | 30 | export async function getAnalytics(req: Request, res: Response) { 31 | const data = await analytics.find({}).lean(); 32 | 33 | return res.send(data); 34 | } 35 | 36 | export async function getShortUrl(req: Request, res: Response) { 37 | const { shortId } = req.params; 38 | const short = await shortUrl.findOne({ shortId }).lean(); 39 | 40 | if (!short) { 41 | return res.sendStatus(404); 42 | } 43 | 44 | return res.json(short); 45 | } 46 | -------------------------------------------------------------------------------- /server/src/db.ts: -------------------------------------------------------------------------------- 1 | import mongoose from "mongoose"; 2 | import config from "config"; 3 | 4 | async function db() { 5 | const dbUri = config.get("dbUri") as string; 6 | try { 7 | await mongoose 8 | .connect(dbUri, { useNewUrlParser: true, useUnifiedTopology: true }) 9 | .then(() => { 10 | console.log(`DB connected to ${dbUri}`); 11 | }); 12 | } catch (e) { 13 | console.error(e); 14 | } 15 | } 16 | 17 | export default db; 18 | -------------------------------------------------------------------------------- /server/src/middleware/validateResourse.ts: -------------------------------------------------------------------------------- 1 | import { AnyObjectSchema } from "yup"; 2 | import { Request, Response, NextFunction } from "express"; 3 | 4 | const validateResource = (resourceSchema: AnyObjectSchema) => async ( 5 | req: Request, 6 | res: Response, 7 | next: NextFunction 8 | ) => { 9 | try { 10 | await resourceSchema.validate({ 11 | body: req.body, 12 | query: req.query, 13 | params: req.params, 14 | }); 15 | next(); 16 | } catch (e) { 17 | return res.status(400).send(e); 18 | } 19 | }; 20 | 21 | export default validateResource; 22 | -------------------------------------------------------------------------------- /server/src/models/analytics.model.ts: -------------------------------------------------------------------------------- 1 | import mongoose, { Document } from "mongoose"; 2 | import { ShortURL } from "./shortUrl.model"; 3 | 4 | interface Analytics extends Document { 5 | shortUrl: ShortURL; 6 | } 7 | 8 | const schema = new mongoose.Schema( 9 | { 10 | shortUrl: { 11 | type: mongoose.Schema.Types.ObjectId, 12 | ref: "shortUrl", 13 | required: true, 14 | }, 15 | }, 16 | { timestamps: true } 17 | ); 18 | 19 | const analytics = mongoose.model("analytics", schema); 20 | 21 | export default analytics; 22 | -------------------------------------------------------------------------------- /server/src/models/shortUrl.model.ts: -------------------------------------------------------------------------------- 1 | import mongoose, { Document } from "mongoose"; 2 | import { customAlphabet } from "nanoid"; 3 | 4 | const nanoid = customAlphabet("abcdefghijklmnopqrstuv0987654321", 6); 5 | 6 | export interface ShortURL extends Document { 7 | shortId: string; 8 | destination: string; 9 | } 10 | 11 | const schema = new mongoose.Schema({ 12 | shortId: { 13 | type: String, 14 | unique: true, 15 | required: true, 16 | default: () => nanoid(), 17 | }, 18 | destination: { type: String, required: true }, 19 | }); 20 | 21 | const shortUrl = mongoose.model("shortUrl", schema); 22 | 23 | export default shortUrl; 24 | -------------------------------------------------------------------------------- /server/src/routes/index.ts: -------------------------------------------------------------------------------- 1 | import { Express, Request, Response } from "express"; 2 | import { 3 | createShortUrl, 4 | handleRedirect, 5 | getAnalytics, 6 | getShortUrl, 7 | } from "../controller/shortUrl.controller"; 8 | import validateResourse from "../middleware/validateResourse"; 9 | import shortUrlSchema from "../schemas/createShortUrl.schema"; 10 | 11 | function routes(app: Express) { 12 | app.get("/healthcheck", (req: Request, res: Response) => { 13 | return res.send("App is healthy"); 14 | }); 15 | 16 | app.post("/api/url", validateResourse(shortUrlSchema), createShortUrl); 17 | 18 | app.get("/:shortId", handleRedirect); 19 | 20 | app.get("/api/url/:shortId", getShortUrl); 21 | 22 | app.get("/api/analytics", getAnalytics); 23 | } 24 | 25 | export default routes; 26 | -------------------------------------------------------------------------------- /server/src/schemas/createShortUrl.schema.ts: -------------------------------------------------------------------------------- 1 | import { object, string } from "yup"; 2 | 3 | export default object({ 4 | body: object({ 5 | destination: string() 6 | .url("Must be a valid URL") 7 | .required("Destination is required"), 8 | }), 9 | }); 10 | -------------------------------------------------------------------------------- /server/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | /* Visit https://aka.ms/tsconfig.json to read more about this file */ 4 | 5 | /* Basic Options */ 6 | // "incremental": true, /* Enable incremental compilation */ 7 | "target": "es5", /* Specify ECMAScript target version: 'ES3' (default), 'ES5', 'ES2015', 'ES2016', 'ES2017', 'ES2018', 'ES2019', 'ES2020', or 'ESNEXT'. */ 8 | "module": "commonjs", /* Specify module code generation: 'none', 'commonjs', 'amd', 'system', 'umd', 'es2015', 'es2020', or 'ESNext'. */ 9 | // "lib": [], /* Specify library files to be included in the compilation. */ 10 | // "allowJs": true, /* Allow javascript files to be compiled. */ 11 | // "checkJs": true, /* Report errors in .js files. */ 12 | // "jsx": "preserve", /* Specify JSX code generation: 'preserve', 'react-native', 'react', 'react-jsx' or 'react-jsxdev'. */ 13 | // "declaration": true, /* Generates corresponding '.d.ts' file. */ 14 | // "declarationMap": true, /* Generates a sourcemap for each corresponding '.d.ts' file. */ 15 | // "sourceMap": true, /* Generates corresponding '.map' file. */ 16 | // "outFile": "./", /* Concatenate and emit output to single file. */ 17 | "outDir": "./build", /* Redirect output structure to the directory. */ 18 | // "rootDir": "./", /* Specify the root directory of input files. Use to control the output directory structure with --outDir. */ 19 | // "composite": true, /* Enable project compilation */ 20 | // "tsBuildInfoFile": "./", /* Specify file to store incremental compilation information */ 21 | // "removeComments": true, /* Do not emit comments to output. */ 22 | // "noEmit": true, /* Do not emit outputs. */ 23 | // "importHelpers": true, /* Import emit helpers from 'tslib'. */ 24 | // "downlevelIteration": true, /* Provide full support for iterables in 'for-of', spread, and destructuring when targeting 'ES5' or 'ES3'. */ 25 | // "isolatedModules": true, /* Transpile each file as a separate module (similar to 'ts.transpileModule'). */ 26 | 27 | /* Strict Type-Checking Options */ 28 | "strict": true, /* Enable all strict type-checking options. */ 29 | // "noImplicitAny": true, /* Raise error on expressions and declarations with an implied 'any' type. */ 30 | // "strictNullChecks": true, /* Enable strict null checks. */ 31 | // "strictFunctionTypes": true, /* Enable strict checking of function types. */ 32 | // "strictBindCallApply": true, /* Enable strict 'bind', 'call', and 'apply' methods on functions. */ 33 | // "strictPropertyInitialization": true, /* Enable strict checking of property initialization in classes. */ 34 | // "noImplicitThis": true, /* Raise error on 'this' expressions with an implied 'any' type. */ 35 | // "alwaysStrict": true, /* Parse in strict mode and emit "use strict" for each source file. */ 36 | 37 | /* Additional Checks */ 38 | // "noUnusedLocals": true, /* Report errors on unused locals. */ 39 | // "noUnusedParameters": true, /* Report errors on unused parameters. */ 40 | // "noImplicitReturns": true, /* Report error when not all code paths in function return a value. */ 41 | // "noFallthroughCasesInSwitch": true, /* Report errors for fallthrough cases in switch statement. */ 42 | // "noUncheckedIndexedAccess": true, /* Include 'undefined' in index signature results */ 43 | // "noPropertyAccessFromIndexSignature": true, /* Require undeclared properties from index signatures to use element accesses. */ 44 | 45 | /* Module Resolution Options */ 46 | // "moduleResolution": "node", /* Specify module resolution strategy: 'node' (Node.js) or 'classic' (TypeScript pre-1.6). */ 47 | // "baseUrl": "./", /* Base directory to resolve non-absolute module names. */ 48 | // "paths": {}, /* A series of entries which re-map imports to lookup locations relative to the 'baseUrl'. */ 49 | // "rootDirs": [], /* List of root folders whose combined content represents the structure of the project at runtime. */ 50 | // "typeRoots": [], /* List of folders to include type definitions from. */ 51 | // "types": [], /* Type declaration files to be included in compilation. */ 52 | // "allowSyntheticDefaultImports": true, /* Allow default imports from modules with no default export. This does not affect code emit, just typechecking. */ 53 | "esModuleInterop": true, /* Enables emit interoperability between CommonJS and ES Modules via creation of namespace objects for all imports. Implies 'allowSyntheticDefaultImports'. */ 54 | // "preserveSymlinks": true, /* Do not resolve the real path of symlinks. */ 55 | // "allowUmdGlobalAccess": true, /* Allow accessing UMD globals from modules. */ 56 | 57 | /* Source Map Options */ 58 | // "sourceRoot": "", /* Specify the location where debugger should locate TypeScript files instead of source locations. */ 59 | // "mapRoot": "", /* Specify the location where debugger should locate map files instead of generated locations. */ 60 | // "inlineSourceMap": true, /* Emit a single file with source maps instead of having a separate file. */ 61 | // "inlineSources": true, /* Emit the source alongside the sourcemaps within a single file; requires '--inlineSourceMap' or '--sourceMap' to be set. */ 62 | 63 | /* Experimental Options */ 64 | // "experimentalDecorators": true, /* Enables experimental support for ES7 decorators. */ 65 | // "emitDecoratorMetadata": true, /* Enables experimental support for emitting type metadata for decorators. */ 66 | 67 | /* Advanced Options */ 68 | "skipLibCheck": true, /* Skip type checking of declaration files. */ 69 | "forceConsistentCasingInFileNames": true /* Disallow inconsistently-cased references to the same file. */ 70 | } 71 | } 72 | -------------------------------------------------------------------------------- /server/yarn.lock: -------------------------------------------------------------------------------- 1 | # THIS IS AN AUTOGENERATED FILE. DO NOT EDIT THIS FILE DIRECTLY. 2 | # yarn lockfile v1 3 | 4 | 5 | "@babel/runtime@^7.10.5": 6 | version "7.13.10" 7 | resolved "https://registry.yarnpkg.com/@babel/runtime/-/runtime-7.13.10.tgz#47d42a57b6095f4468da440388fdbad8bebf0d7d" 8 | integrity sha512-4QPkjJq6Ns3V/RgpEahRk+AGfL0eO6RHHtTWoNNr5mO49G6B5+X6d6THgWEAvTrznU5xYpbAlVKRYcsCgh/Akw== 9 | dependencies: 10 | regenerator-runtime "^0.13.4" 11 | 12 | "@types/body-parser@*", "@types/body-parser@^1.19.0": 13 | version "1.19.0" 14 | resolved "https://registry.yarnpkg.com/@types/body-parser/-/body-parser-1.19.0.tgz#0685b3c47eb3006ffed117cdd55164b61f80538f" 15 | integrity sha512-W98JrE0j2K78swW4ukqMleo8R7h/pFETjM2DQ90MF6XK2i4LO4W3gQ71Lt4w3bfm2EvVSyWHplECvB5sK22yFQ== 16 | dependencies: 17 | "@types/connect" "*" 18 | "@types/node" "*" 19 | 20 | "@types/bson@*": 21 | version "4.0.3" 22 | resolved "https://registry.yarnpkg.com/@types/bson/-/bson-4.0.3.tgz#30889d2ffde6262abbe38659364c631454999fbf" 23 | integrity sha512-mVRvYnTOZJz3ccpxhr3wgxVmSeiYinW+zlzQz3SXWaJmD1DuL05Jeq7nKw3SnbKmbleW5qrLG5vdyWe/A9sXhw== 24 | dependencies: 25 | "@types/node" "*" 26 | 27 | "@types/config@^0.0.38": 28 | version "0.0.38" 29 | resolved "https://registry.yarnpkg.com/@types/config/-/config-0.0.38.tgz#ca30679b21b5b297299467e3a3f1c8e2e64b9170" 30 | integrity sha512-z2WizAfIFgSv8SQfQ8c0LlbDAcK47D/o93XW6bxZ9t3bs4fmmfAPjk1nhAIBTG84PBBCHfSPM+8g7vhLdbFokg== 31 | 32 | "@types/connect@*": 33 | version "3.4.34" 34 | resolved "https://registry.yarnpkg.com/@types/connect/-/connect-3.4.34.tgz#170a40223a6d666006d93ca128af2beb1d9b1901" 35 | integrity sha512-ePPA/JuI+X0vb+gSWlPKOY0NdNAie/rPUqX2GUPpbZwiKTkSPhjXWuee47E4MtE54QVzGCQMQkAL6JhV2E1+cQ== 36 | dependencies: 37 | "@types/node" "*" 38 | 39 | "@types/cors@^2.8.10": 40 | version "2.8.10" 41 | resolved "https://registry.yarnpkg.com/@types/cors/-/cors-2.8.10.tgz#61cc8469849e5bcdd0c7044122265c39cec10cf4" 42 | integrity sha512-C7srjHiVG3Ey1nR6d511dtDkCEjxuN9W1HWAEjGq8kpcwmNM6JJkpC0xvabM7BXTG2wDq8Eu33iH9aQKa7IvLQ== 43 | 44 | "@types/express-serve-static-core@^4.17.18": 45 | version "4.17.19" 46 | resolved "https://registry.yarnpkg.com/@types/express-serve-static-core/-/express-serve-static-core-4.17.19.tgz#00acfc1632e729acac4f1530e9e16f6dd1508a1d" 47 | integrity sha512-DJOSHzX7pCiSElWaGR8kCprwibCB/3yW6vcT8VG3P0SJjnv19gnWG/AZMfM60Xj/YJIp/YCaDHyvzsFVeniARA== 48 | dependencies: 49 | "@types/node" "*" 50 | "@types/qs" "*" 51 | "@types/range-parser" "*" 52 | 53 | "@types/express@^4.17.11": 54 | version "4.17.11" 55 | resolved "https://registry.yarnpkg.com/@types/express/-/express-4.17.11.tgz#debe3caa6f8e5fcda96b47bd54e2f40c4ee59545" 56 | integrity sha512-no+R6rW60JEc59977wIxreQVsIEOAYwgCqldrA/vkpCnbD7MqTefO97lmoBe4WE0F156bC4uLSP1XHDOySnChg== 57 | dependencies: 58 | "@types/body-parser" "*" 59 | "@types/express-serve-static-core" "^4.17.18" 60 | "@types/qs" "*" 61 | "@types/serve-static" "*" 62 | 63 | "@types/lodash@^4.14.165": 64 | version "4.14.168" 65 | resolved "https://registry.yarnpkg.com/@types/lodash/-/lodash-4.14.168.tgz#fe24632e79b7ade3f132891afff86caa5e5ce008" 66 | integrity sha512-oVfRvqHV/V6D1yifJbVRU3TMp8OT6o6BG+U9MkwuJ3U8/CsDHvalRpsxBqivn71ztOFZBTfJMvETbqHiaNSj7Q== 67 | 68 | "@types/mime@^1": 69 | version "1.3.2" 70 | resolved "https://registry.yarnpkg.com/@types/mime/-/mime-1.3.2.tgz#93e25bf9ee75fe0fd80b594bc4feb0e862111b5a" 71 | integrity sha512-YATxVxgRqNH6nHEIsvg6k2Boc1JHI9ZbH5iWFFv/MTkchz3b1ieGDa5T0a9RznNdI0KhVbdbWSN+KWWrQZRxTw== 72 | 73 | "@types/mongodb@^3.5.27": 74 | version "3.6.10" 75 | resolved "https://registry.yarnpkg.com/@types/mongodb/-/mongodb-3.6.10.tgz#80cceaabeec9f460e5b46844e938e8eba74f9266" 76 | integrity sha512-BkwAHFiZSSWdTIqbUVGmgvIsiXXjqAketeK7Izy7oSs6G3N8Bn993tK9eq6QEovQDx6OQ2FGP2KWDDxBzdlJ6Q== 77 | dependencies: 78 | "@types/bson" "*" 79 | "@types/node" "*" 80 | 81 | "@types/nanoid@^2.1.0": 82 | version "2.1.0" 83 | resolved "https://registry.yarnpkg.com/@types/nanoid/-/nanoid-2.1.0.tgz#41edfda78986e9127d0dc14de982de766f994020" 84 | integrity sha512-xdkn/oRTA0GSNPLIKZgHWqDTWZsVrieKomxJBOQUK9YDD+zfSgmwD5t4WJYra5S7XyhTw7tfvwznW+pFexaepQ== 85 | dependencies: 86 | "@types/node" "*" 87 | 88 | "@types/node@*", "@types/node@^14.14.37": 89 | version "14.14.37" 90 | resolved "https://registry.yarnpkg.com/@types/node/-/node-14.14.37.tgz#a3dd8da4eb84a996c36e331df98d82abd76b516e" 91 | integrity sha512-XYmBiy+ohOR4Lh5jE379fV2IU+6Jn4g5qASinhitfyO71b/sCo6MKsMLF5tc7Zf2CE8hViVQyYSobJNke8OvUw== 92 | 93 | "@types/qs@*": 94 | version "6.9.6" 95 | resolved "https://registry.yarnpkg.com/@types/qs/-/qs-6.9.6.tgz#df9c3c8b31a247ec315e6996566be3171df4b3b1" 96 | integrity sha512-0/HnwIfW4ki2D8L8c9GVcG5I72s9jP5GSLVF0VIXDW00kmIpA6O33G7a8n59Tmh7Nz0WUC3rSb7PTY/sdW2JzA== 97 | 98 | "@types/range-parser@*": 99 | version "1.2.3" 100 | resolved "https://registry.yarnpkg.com/@types/range-parser/-/range-parser-1.2.3.tgz#7ee330ba7caafb98090bece86a5ee44115904c2c" 101 | integrity sha512-ewFXqrQHlFsgc09MK5jP5iR7vumV/BYayNC6PgJO2LPe8vrnNFyjQjSppfEngITi0qvfKtzFvgKymGheFM9UOA== 102 | 103 | "@types/serve-static@*": 104 | version "1.13.9" 105 | resolved "https://registry.yarnpkg.com/@types/serve-static/-/serve-static-1.13.9.tgz#aacf28a85a05ee29a11fb7c3ead935ac56f33e4e" 106 | integrity sha512-ZFqF6qa48XsPdjXV5Gsz0Zqmux2PerNd3a/ktL45mHpa19cuMi/cL8tcxdAx497yRh+QtYPuofjT9oWw9P7nkA== 107 | dependencies: 108 | "@types/mime" "^1" 109 | "@types/node" "*" 110 | 111 | "@types/yup@^0.29.11": 112 | version "0.29.11" 113 | resolved "https://registry.yarnpkg.com/@types/yup/-/yup-0.29.11.tgz#d654a112973f5e004bf8438122bd7e56a8e5cd7e" 114 | integrity sha512-9cwk3c87qQKZrT251EDoibiYRILjCmxBvvcb4meofCmx1vdnNcR9gyildy5vOHASpOKMsn42CugxUvcwK5eu1g== 115 | 116 | accepts@~1.3.7: 117 | version "1.3.7" 118 | resolved "https://registry.yarnpkg.com/accepts/-/accepts-1.3.7.tgz#531bc726517a3b2b41f850021c6cc15eaab507cd" 119 | integrity sha512-Il80Qs2WjYlJIBNzNkK6KYqlVMTbZLXgHx2oT0pU/fjRHyEp+PEfEPY0R3WCwAGVOtauxh1hOxNgIf5bv7dQpA== 120 | dependencies: 121 | mime-types "~2.1.24" 122 | negotiator "0.6.2" 123 | 124 | arg@^4.1.0: 125 | version "4.1.3" 126 | resolved "https://registry.yarnpkg.com/arg/-/arg-4.1.3.tgz#269fc7ad5b8e42cb63c896d5666017261c144089" 127 | integrity sha512-58S9QDqG0Xx27YwPSt9fJxivjYl432YCwfDMfZ+71RAqUrZef7LrKQZ3LHLOwCS4FLNBplP533Zx895SeOCHvA== 128 | 129 | array-flatten@1.1.1: 130 | version "1.1.1" 131 | resolved "https://registry.yarnpkg.com/array-flatten/-/array-flatten-1.1.1.tgz#9a5f699051b1e7073328f2a008968b64ea2955d2" 132 | integrity sha1-ml9pkFGx5wczKPKgCJaLZOopVdI= 133 | 134 | bl@^2.2.1: 135 | version "2.2.1" 136 | resolved "https://registry.yarnpkg.com/bl/-/bl-2.2.1.tgz#8c11a7b730655c5d56898cdc871224f40fd901d5" 137 | integrity sha512-6Pesp1w0DEX1N550i/uGV/TqucVL4AM/pgThFSN/Qq9si1/DF9aIHs1BxD8V/QU0HoeHO6cQRTAuYnLPKq1e4g== 138 | dependencies: 139 | readable-stream "^2.3.5" 140 | safe-buffer "^5.1.1" 141 | 142 | bluebird@3.5.1: 143 | version "3.5.1" 144 | resolved "https://registry.yarnpkg.com/bluebird/-/bluebird-3.5.1.tgz#d9551f9de98f1fcda1e683d17ee91a0602ee2eb9" 145 | integrity sha512-MKiLiV+I1AA596t9w1sQJ8jkiSr5+ZKi0WKrYGUn6d1Fx+Ij4tIj+m2WMQSGczs5jZVxV339chE8iwk6F64wjA== 146 | 147 | body-parser@1.19.0, body-parser@^1.19.0: 148 | version "1.19.0" 149 | resolved "https://registry.yarnpkg.com/body-parser/-/body-parser-1.19.0.tgz#96b2709e57c9c4e09a6fd66a8fd979844f69f08a" 150 | integrity sha512-dhEPs72UPbDnAQJ9ZKMNTP6ptJaionhP5cBb541nXPlW60Jepo9RV/a4fX4XWW9CuFNK22krhrj1+rgzifNCsw== 151 | dependencies: 152 | bytes "3.1.0" 153 | content-type "~1.0.4" 154 | debug "2.6.9" 155 | depd "~1.1.2" 156 | http-errors "1.7.2" 157 | iconv-lite "0.4.24" 158 | on-finished "~2.3.0" 159 | qs "6.7.0" 160 | raw-body "2.4.0" 161 | type-is "~1.6.17" 162 | 163 | bson@^1.1.4: 164 | version "1.1.6" 165 | resolved "https://registry.yarnpkg.com/bson/-/bson-1.1.6.tgz#fb819be9a60cd677e0853aee4ca712a785d6618a" 166 | integrity sha512-EvVNVeGo4tHxwi8L6bPj3y3itEvStdwvvlojVxxbyYfoaxJ6keLgrTuKdyfEAszFK+H3olzBuafE0yoh0D1gdg== 167 | 168 | buffer-from@^1.0.0: 169 | version "1.1.1" 170 | resolved "https://registry.yarnpkg.com/buffer-from/-/buffer-from-1.1.1.tgz#32713bc028f75c02fdb710d7c7bcec1f2c6070ef" 171 | integrity sha512-MQcXEUbCKtEo7bhqEs6560Hyd4XaovZlO/k9V3hjVUF/zwW7KBVdSK4gIt/bzwS9MbR5qob+F5jusZsb0YQK2A== 172 | 173 | bytes@3.1.0: 174 | version "3.1.0" 175 | resolved "https://registry.yarnpkg.com/bytes/-/bytes-3.1.0.tgz#f6cf7933a360e0588fa9fde85651cdc7f805d1f6" 176 | integrity sha512-zauLjrfCG+xvoyaqLoV8bLVXXNGC4JqlxFCutSDWA6fJrTo2ZuvLYTqZ7aHBLZSMOopbzwv8f+wZcVzfVTI2Dg== 177 | 178 | config@^3.3.6: 179 | version "3.3.6" 180 | resolved "https://registry.yarnpkg.com/config/-/config-3.3.6.tgz#b87799db7399cc34988f55379b5f43465b1b065c" 181 | integrity sha512-Hj5916C5HFawjYJat1epbyY2PlAgLpBtDUlr0MxGLgo3p5+7kylyvnRY18PqJHgnNWXcdd0eWDemT7eYWuFgwg== 182 | dependencies: 183 | json5 "^2.1.1" 184 | 185 | content-disposition@0.5.3: 186 | version "0.5.3" 187 | resolved "https://registry.yarnpkg.com/content-disposition/-/content-disposition-0.5.3.tgz#e130caf7e7279087c5616c2007d0485698984fbd" 188 | integrity sha512-ExO0774ikEObIAEV9kDo50o+79VCUdEB6n6lzKgGwupcVeRlhrj3qGAfwq8G6uBJjkqLrhT0qEYFcWng8z1z0g== 189 | dependencies: 190 | safe-buffer "5.1.2" 191 | 192 | content-type@~1.0.4: 193 | version "1.0.4" 194 | resolved "https://registry.yarnpkg.com/content-type/-/content-type-1.0.4.tgz#e138cc75e040c727b1966fe5e5f8c9aee256fe3b" 195 | integrity sha512-hIP3EEPs8tB9AT1L+NUqtwOAps4mk2Zob89MWXMHjHWg9milF/j4osnnQLXBCBFBk/tvIG/tUc9mOUJiPBhPXA== 196 | 197 | cookie-signature@1.0.6: 198 | version "1.0.6" 199 | resolved "https://registry.yarnpkg.com/cookie-signature/-/cookie-signature-1.0.6.tgz#e303a882b342cc3ee8ca513a79999734dab3ae2c" 200 | integrity sha1-4wOogrNCzD7oylE6eZmXNNqzriw= 201 | 202 | cookie@0.4.0: 203 | version "0.4.0" 204 | resolved "https://registry.yarnpkg.com/cookie/-/cookie-0.4.0.tgz#beb437e7022b3b6d49019d088665303ebe9c14ba" 205 | integrity sha512-+Hp8fLp57wnUSt0tY0tHEXh4voZRDnoIrZPqlo3DPiI4y9lwg/jqx+1Om94/W6ZaPDOUbnjOt/99w66zk+l1Xg== 206 | 207 | core-util-is@~1.0.0: 208 | version "1.0.2" 209 | resolved "https://registry.yarnpkg.com/core-util-is/-/core-util-is-1.0.2.tgz#b5fd54220aa2bc5ab57aab7140c940754503c1a7" 210 | integrity sha1-tf1UIgqivFq1eqtxQMlAdUUDwac= 211 | 212 | cors@^2.8.5: 213 | version "2.8.5" 214 | resolved "https://registry.yarnpkg.com/cors/-/cors-2.8.5.tgz#eac11da51592dd86b9f06f6e7ac293b3df875d29" 215 | integrity sha512-KIHbLJqu73RGr/hnbrO9uBeixNGuvSQjul/jdFvS/KFSIH1hWVd1ng7zOHx+YrEfInLG7q4n6GHQ9cDtxv/P6g== 216 | dependencies: 217 | object-assign "^4" 218 | vary "^1" 219 | 220 | create-require@^1.1.0: 221 | version "1.1.1" 222 | resolved "https://registry.yarnpkg.com/create-require/-/create-require-1.1.1.tgz#c1d7e8f1e5f6cfc9ff65f9cd352d37348756c333" 223 | integrity sha512-dcKFX3jn0MpIaXjisoRvexIJVEKzaq7z2rZKxf+MSr9TkdmHmsU4m2lcLojrj/FHl8mk5VxMmYA+ftRkP/3oKQ== 224 | 225 | debug@2.6.9: 226 | version "2.6.9" 227 | resolved "https://registry.yarnpkg.com/debug/-/debug-2.6.9.tgz#5d128515df134ff327e90a4c93f4e077a536341f" 228 | integrity sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA== 229 | dependencies: 230 | ms "2.0.0" 231 | 232 | debug@3.1.0: 233 | version "3.1.0" 234 | resolved "https://registry.yarnpkg.com/debug/-/debug-3.1.0.tgz#5bb5a0672628b64149566ba16819e61518c67261" 235 | integrity sha512-OX8XqP7/1a9cqkxYw2yXss15f26NKWBpDXQd0/uK/KPqdQhxbPa994hnzjcE2VqQpDslf55723cKPUOGSmMY3g== 236 | dependencies: 237 | ms "2.0.0" 238 | 239 | denque@^1.4.1: 240 | version "1.5.0" 241 | resolved "https://registry.yarnpkg.com/denque/-/denque-1.5.0.tgz#773de0686ff2d8ec2ff92914316a47b73b1c73de" 242 | integrity sha512-CYiCSgIF1p6EUByQPlGkKnP1M9g0ZV3qMIrqMqZqdwazygIA/YP2vrbcyl1h/WppKJTdl1F85cXIle+394iDAQ== 243 | 244 | depd@~1.1.2: 245 | version "1.1.2" 246 | resolved "https://registry.yarnpkg.com/depd/-/depd-1.1.2.tgz#9bcd52e14c097763e749b274c4346ed2e560b5a9" 247 | integrity sha1-m81S4UwJd2PnSbJ0xDRu0uVgtak= 248 | 249 | destroy@~1.0.4: 250 | version "1.0.4" 251 | resolved "https://registry.yarnpkg.com/destroy/-/destroy-1.0.4.tgz#978857442c44749e4206613e37946205826abd80" 252 | integrity sha1-l4hXRCxEdJ5CBmE+N5RiBYJqvYA= 253 | 254 | diff@^4.0.1: 255 | version "4.0.2" 256 | resolved "https://registry.yarnpkg.com/diff/-/diff-4.0.2.tgz#60f3aecb89d5fae520c11aa19efc2bb982aade7d" 257 | integrity sha512-58lmxKSA4BNyLz+HHMUzlOEpg09FV+ev6ZMe3vJihgdxzgcwZ8VoEEPmALCZG9LmqfVoNMMKpttIYTVG6uDY7A== 258 | 259 | dotenv@^8.2.0: 260 | version "8.2.0" 261 | resolved "https://registry.yarnpkg.com/dotenv/-/dotenv-8.2.0.tgz#97e619259ada750eea3e4ea3e26bceea5424b16a" 262 | integrity sha512-8sJ78ElpbDJBHNeBzUbUVLsqKdccaa/BXF1uPTw3GrvQTBgrQrtObr2mUrE38vzYd8cEv+m/JBfDLioYcfXoaw== 263 | 264 | ee-first@1.1.1: 265 | version "1.1.1" 266 | resolved "https://registry.yarnpkg.com/ee-first/-/ee-first-1.1.1.tgz#590c61156b0ae2f4f0255732a158b266bc56b21d" 267 | integrity sha1-WQxhFWsK4vTwJVcyoViyZrxWsh0= 268 | 269 | encodeurl@~1.0.2: 270 | version "1.0.2" 271 | resolved "https://registry.yarnpkg.com/encodeurl/-/encodeurl-1.0.2.tgz#ad3ff4c86ec2d029322f5a02c3a9a606c95b3f59" 272 | integrity sha1-rT/0yG7C0CkyL1oCw6mmBslbP1k= 273 | 274 | escape-html@~1.0.3: 275 | version "1.0.3" 276 | resolved "https://registry.yarnpkg.com/escape-html/-/escape-html-1.0.3.tgz#0258eae4d3d0c0974de1c169188ef0051d1d1988" 277 | integrity sha1-Aljq5NPQwJdN4cFpGI7wBR0dGYg= 278 | 279 | etag@~1.8.1: 280 | version "1.8.1" 281 | resolved "https://registry.yarnpkg.com/etag/-/etag-1.8.1.tgz#41ae2eeb65efa62268aebfea83ac7d79299b0887" 282 | integrity sha1-Qa4u62XvpiJorr/qg6x9eSmbCIc= 283 | 284 | express@^4.17.1: 285 | version "4.17.1" 286 | resolved "https://registry.yarnpkg.com/express/-/express-4.17.1.tgz#4491fc38605cf51f8629d39c2b5d026f98a4c134" 287 | integrity sha512-mHJ9O79RqluphRrcw2X/GTh3k9tVv8YcoyY4Kkh4WDMUYKRZUq0h1o0w2rrrxBqM7VoeUVqgb27xlEMXTnYt4g== 288 | dependencies: 289 | accepts "~1.3.7" 290 | array-flatten "1.1.1" 291 | body-parser "1.19.0" 292 | content-disposition "0.5.3" 293 | content-type "~1.0.4" 294 | cookie "0.4.0" 295 | cookie-signature "1.0.6" 296 | debug "2.6.9" 297 | depd "~1.1.2" 298 | encodeurl "~1.0.2" 299 | escape-html "~1.0.3" 300 | etag "~1.8.1" 301 | finalhandler "~1.1.2" 302 | fresh "0.5.2" 303 | merge-descriptors "1.0.1" 304 | methods "~1.1.2" 305 | on-finished "~2.3.0" 306 | parseurl "~1.3.3" 307 | path-to-regexp "0.1.7" 308 | proxy-addr "~2.0.5" 309 | qs "6.7.0" 310 | range-parser "~1.2.1" 311 | safe-buffer "5.1.2" 312 | send "0.17.1" 313 | serve-static "1.14.1" 314 | setprototypeof "1.1.1" 315 | statuses "~1.5.0" 316 | type-is "~1.6.18" 317 | utils-merge "1.0.1" 318 | vary "~1.1.2" 319 | 320 | finalhandler@~1.1.2: 321 | version "1.1.2" 322 | resolved "https://registry.yarnpkg.com/finalhandler/-/finalhandler-1.1.2.tgz#b7e7d000ffd11938d0fdb053506f6ebabe9f587d" 323 | integrity sha512-aAWcW57uxVNrQZqFXjITpW3sIUQmHGG3qSb9mUah9MgMC4NeWhNOlNjXEYq3HjRAvL6arUviZGGJsBg6z0zsWA== 324 | dependencies: 325 | debug "2.6.9" 326 | encodeurl "~1.0.2" 327 | escape-html "~1.0.3" 328 | on-finished "~2.3.0" 329 | parseurl "~1.3.3" 330 | statuses "~1.5.0" 331 | unpipe "~1.0.0" 332 | 333 | forwarded@~0.1.2: 334 | version "0.1.2" 335 | resolved "https://registry.yarnpkg.com/forwarded/-/forwarded-0.1.2.tgz#98c23dab1175657b8c0573e8ceccd91b0ff18c84" 336 | integrity sha1-mMI9qxF1ZXuMBXPozszZGw/xjIQ= 337 | 338 | fresh@0.5.2: 339 | version "0.5.2" 340 | resolved "https://registry.yarnpkg.com/fresh/-/fresh-0.5.2.tgz#3d8cadd90d976569fa835ab1f8e4b23a105605a7" 341 | integrity sha1-PYyt2Q2XZWn6g1qx+OSyOhBWBac= 342 | 343 | http-errors@1.7.2: 344 | version "1.7.2" 345 | resolved "https://registry.yarnpkg.com/http-errors/-/http-errors-1.7.2.tgz#4f5029cf13239f31036e5b2e55292bcfbcc85c8f" 346 | integrity sha512-uUQBt3H/cSIVfch6i1EuPNy/YsRSOUBXTVfZ+yR7Zjez3qjBz6i9+i4zjNaoqcoFVI4lQJ5plg63TvGfRSDCRg== 347 | dependencies: 348 | depd "~1.1.2" 349 | inherits "2.0.3" 350 | setprototypeof "1.1.1" 351 | statuses ">= 1.5.0 < 2" 352 | toidentifier "1.0.0" 353 | 354 | http-errors@~1.7.2: 355 | version "1.7.3" 356 | resolved "https://registry.yarnpkg.com/http-errors/-/http-errors-1.7.3.tgz#6c619e4f9c60308c38519498c14fbb10aacebb06" 357 | integrity sha512-ZTTX0MWrsQ2ZAhA1cejAwDLycFsd7I7nVtnkT3Ol0aqodaKW+0CTZDQ1uBv5whptCnc8e8HeRRJxRs0kmm/Qfw== 358 | dependencies: 359 | depd "~1.1.2" 360 | inherits "2.0.4" 361 | setprototypeof "1.1.1" 362 | statuses ">= 1.5.0 < 2" 363 | toidentifier "1.0.0" 364 | 365 | iconv-lite@0.4.24: 366 | version "0.4.24" 367 | resolved "https://registry.yarnpkg.com/iconv-lite/-/iconv-lite-0.4.24.tgz#2022b4b25fbddc21d2f524974a474aafe733908b" 368 | integrity sha512-v3MXnZAcvnywkTUEZomIActle7RXXeedOR31wwl7VlyoXO4Qi9arvSenNQWne1TcRwhCL1HwLI21bEqdpj8/rA== 369 | dependencies: 370 | safer-buffer ">= 2.1.2 < 3" 371 | 372 | inherits@2.0.3: 373 | version "2.0.3" 374 | resolved "https://registry.yarnpkg.com/inherits/-/inherits-2.0.3.tgz#633c2c83e3da42a502f52466022480f4208261de" 375 | integrity sha1-Yzwsg+PaQqUC9SRmAiSA9CCCYd4= 376 | 377 | inherits@2.0.4, inherits@~2.0.3: 378 | version "2.0.4" 379 | resolved "https://registry.yarnpkg.com/inherits/-/inherits-2.0.4.tgz#0fa2c64f932917c3433a0ded55363aae37416b7c" 380 | integrity sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ== 381 | 382 | ipaddr.js@1.9.1: 383 | version "1.9.1" 384 | resolved "https://registry.yarnpkg.com/ipaddr.js/-/ipaddr.js-1.9.1.tgz#bff38543eeb8984825079ff3a2a8e6cbd46781b3" 385 | integrity sha512-0KI/607xoxSToH7GjN1FfSbLoU0+btTicjsQSWQlh/hZykN8KpmMf7uYwPW3R+akZ6R/w18ZlXSHBYXiYUPO3g== 386 | 387 | isarray@~1.0.0: 388 | version "1.0.0" 389 | resolved "https://registry.yarnpkg.com/isarray/-/isarray-1.0.0.tgz#bb935d48582cba168c06834957a54a3e07124f11" 390 | integrity sha1-u5NdSFgsuhaMBoNJV6VKPgcSTxE= 391 | 392 | json5@^2.1.1: 393 | version "2.2.0" 394 | resolved "https://registry.yarnpkg.com/json5/-/json5-2.2.0.tgz#2dfefe720c6ba525d9ebd909950f0515316c89a3" 395 | integrity sha512-f+8cldu7X/y7RAJurMEJmdoKXGB/X550w2Nr3tTbezL6RwEE/iMcm+tZnXeoZtKuOq6ft8+CqzEkrIgx1fPoQA== 396 | dependencies: 397 | minimist "^1.2.5" 398 | 399 | kareem@2.3.2: 400 | version "2.3.2" 401 | resolved "https://registry.yarnpkg.com/kareem/-/kareem-2.3.2.tgz#78c4508894985b8d38a0dc15e1a8e11078f2ca93" 402 | integrity sha512-STHz9P7X2L4Kwn72fA4rGyqyXdmrMSdxqHx9IXon/FXluXieaFA6KJ2upcHAHxQPQ0LeM/OjLrhFxifHewOALQ== 403 | 404 | lodash-es@^4.17.15: 405 | version "4.17.21" 406 | resolved "https://registry.yarnpkg.com/lodash-es/-/lodash-es-4.17.21.tgz#43e626c46e6591b7750beb2b50117390c609e3ee" 407 | integrity sha512-mKnC+QJ9pWVzv+C4/U3rRsHapFfHvQFoFB92e52xeyGMcX6/OlIl78je1u8vePzYZSkkogMPJ2yjxxsb89cxyw== 408 | 409 | lodash@^4.17.20: 410 | version "4.17.21" 411 | resolved "https://registry.yarnpkg.com/lodash/-/lodash-4.17.21.tgz#679591c564c3bffaae8454cf0b3df370c3d6911c" 412 | integrity sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg== 413 | 414 | make-error@^1.1.1: 415 | version "1.3.6" 416 | resolved "https://registry.yarnpkg.com/make-error/-/make-error-1.3.6.tgz#2eb2e37ea9b67c4891f684a1394799af484cf7a2" 417 | integrity sha512-s8UhlNe7vPKomQhC1qFelMokr/Sc3AgNbso3n74mVPA5LTZwkB9NlXf4XPamLxJE8h0gh73rM94xvwRT2CVInw== 418 | 419 | media-typer@0.3.0: 420 | version "0.3.0" 421 | resolved "https://registry.yarnpkg.com/media-typer/-/media-typer-0.3.0.tgz#8710d7af0aa626f8fffa1ce00168545263255748" 422 | integrity sha1-hxDXrwqmJvj/+hzgAWhUUmMlV0g= 423 | 424 | memory-pager@^1.0.2: 425 | version "1.5.0" 426 | resolved "https://registry.yarnpkg.com/memory-pager/-/memory-pager-1.5.0.tgz#d8751655d22d384682741c972f2c3d6dfa3e66b5" 427 | integrity sha512-ZS4Bp4r/Zoeq6+NLJpP+0Zzm0pR8whtGPf1XExKLJBAczGMnSi3It14OiNCStjQjM6NU1okjQGSxgEZN8eBYKg== 428 | 429 | merge-descriptors@1.0.1: 430 | version "1.0.1" 431 | resolved "https://registry.yarnpkg.com/merge-descriptors/-/merge-descriptors-1.0.1.tgz#b00aaa556dd8b44568150ec9d1b953f3f90cbb61" 432 | integrity sha1-sAqqVW3YtEVoFQ7J0blT8/kMu2E= 433 | 434 | methods@~1.1.2: 435 | version "1.1.2" 436 | resolved "https://registry.yarnpkg.com/methods/-/methods-1.1.2.tgz#5529a4d67654134edcc5266656835b0f851afcee" 437 | integrity sha1-VSmk1nZUE07cxSZmVoNbD4Ua/O4= 438 | 439 | mime-db@1.46.0: 440 | version "1.46.0" 441 | resolved "https://registry.yarnpkg.com/mime-db/-/mime-db-1.46.0.tgz#6267748a7f799594de3cbc8cde91def349661cee" 442 | integrity sha512-svXaP8UQRZ5K7or+ZmfNhg2xX3yKDMUzqadsSqi4NCH/KomcH75MAMYAGVlvXn4+b/xOPhS3I2uHKRUzvjY7BQ== 443 | 444 | mime-types@~2.1.24: 445 | version "2.1.29" 446 | resolved "https://registry.yarnpkg.com/mime-types/-/mime-types-2.1.29.tgz#1d4ab77da64b91f5f72489df29236563754bb1b2" 447 | integrity sha512-Y/jMt/S5sR9OaqteJtslsFZKWOIIqMACsJSiHghlCAyhf7jfVYjKBmLiX8OgpWeW+fjJ2b+Az69aPFPkUOY6xQ== 448 | dependencies: 449 | mime-db "1.46.0" 450 | 451 | mime@1.6.0: 452 | version "1.6.0" 453 | resolved "https://registry.yarnpkg.com/mime/-/mime-1.6.0.tgz#32cd9e5c64553bd58d19a568af452acff04981b1" 454 | integrity sha512-x0Vn8spI+wuJ1O6S7gnbaQg8Pxh4NNHb7KSINmEWKiPE4RKOplvijn+NkmYmmRgP68mc70j2EbeTFRsrswaQeg== 455 | 456 | minimist@^1.2.5: 457 | version "1.2.5" 458 | resolved "https://registry.yarnpkg.com/minimist/-/minimist-1.2.5.tgz#67d66014b66a6a8aaa0c083c5fd58df4e4e97602" 459 | integrity sha512-FM9nNUYrRBAELZQT3xeZQ7fmMOBg6nWNmJKTcgsJeaLstP/UODVpGsr5OhXhhXg6f+qtJ8uiZ+PUxkDWcgIXLw== 460 | 461 | mongodb@3.6.5: 462 | version "3.6.5" 463 | resolved "https://registry.yarnpkg.com/mongodb/-/mongodb-3.6.5.tgz#c27d786fd4d3c83dc19302483707d12a9d2aee5f" 464 | integrity sha512-mQlYKw1iGbvJJejcPuyTaytq0xxlYbIoVDm2FODR+OHxyEiMR021vc32bTvamgBjCswsD54XIRwhg3yBaWqJjg== 465 | dependencies: 466 | bl "^2.2.1" 467 | bson "^1.1.4" 468 | denque "^1.4.1" 469 | require_optional "^1.0.1" 470 | safe-buffer "^5.1.2" 471 | optionalDependencies: 472 | saslprep "^1.0.0" 473 | 474 | mongoose-legacy-pluralize@1.0.2: 475 | version "1.0.2" 476 | resolved "https://registry.yarnpkg.com/mongoose-legacy-pluralize/-/mongoose-legacy-pluralize-1.0.2.tgz#3ba9f91fa507b5186d399fb40854bff18fb563e4" 477 | integrity sha512-Yo/7qQU4/EyIS8YDFSeenIvXxZN+ld7YdV9LqFVQJzTLye8unujAWPZ4NWKfFA+RNjh+wvTWKY9Z3E5XM6ZZiQ== 478 | 479 | mongoose@^5.12.2: 480 | version "5.12.2" 481 | resolved "https://registry.yarnpkg.com/mongoose/-/mongoose-5.12.2.tgz#3274630dfb9a8e63dbda0c6e7124fd475623c6ff" 482 | integrity sha512-kT9t6Nvu9WPsfssn7Gzke446Il8UdMilY7Sa5vALtwoOoNOGtZEVjekZBFwsBFzTWtBA/x5gBmJoYFP+1LeDlg== 483 | dependencies: 484 | "@types/mongodb" "^3.5.27" 485 | bson "^1.1.4" 486 | kareem "2.3.2" 487 | mongodb "3.6.5" 488 | mongoose-legacy-pluralize "1.0.2" 489 | mpath "0.8.3" 490 | mquery "3.2.4" 491 | ms "2.1.2" 492 | regexp-clone "1.0.0" 493 | safe-buffer "5.2.1" 494 | sift "7.0.1" 495 | sliced "1.0.1" 496 | 497 | mpath@0.8.3: 498 | version "0.8.3" 499 | resolved "https://registry.yarnpkg.com/mpath/-/mpath-0.8.3.tgz#828ac0d187f7f42674839d74921970979abbdd8f" 500 | integrity sha512-eb9rRvhDltXVNL6Fxd2zM9D4vKBxjVVQNLNijlj7uoXUy19zNDsIif5zR+pWmPCWNKwAtqyo4JveQm4nfD5+eA== 501 | 502 | mquery@3.2.4: 503 | version "3.2.4" 504 | resolved "https://registry.yarnpkg.com/mquery/-/mquery-3.2.4.tgz#9c5c2e285ea6c6f20673f3528973c99ee1aaa1a0" 505 | integrity sha512-uOLpp7iRX0BV1Uu6YpsqJ5b42LwYnmu0WeF/f8qgD/On3g0XDaQM6pfn0m6UxO6SM8DioZ9Bk6xxbWIGHm2zHg== 506 | dependencies: 507 | bluebird "3.5.1" 508 | debug "3.1.0" 509 | regexp-clone "^1.0.0" 510 | safe-buffer "5.1.2" 511 | sliced "1.0.1" 512 | 513 | ms@2.0.0: 514 | version "2.0.0" 515 | resolved "https://registry.yarnpkg.com/ms/-/ms-2.0.0.tgz#5608aeadfc00be6c2901df5f9861788de0d597c8" 516 | integrity sha1-VgiurfwAvmwpAd9fmGF4jeDVl8g= 517 | 518 | ms@2.1.1: 519 | version "2.1.1" 520 | resolved "https://registry.yarnpkg.com/ms/-/ms-2.1.1.tgz#30a5864eb3ebb0a66f2ebe6d727af06a09d86e0a" 521 | integrity sha512-tgp+dl5cGk28utYktBsrFqA7HKgrhgPsg6Z/EfhWI4gl1Hwq8B/GmY/0oXZ6nF8hDVesS/FpnYaD/kOWhYQvyg== 522 | 523 | ms@2.1.2: 524 | version "2.1.2" 525 | resolved "https://registry.yarnpkg.com/ms/-/ms-2.1.2.tgz#d09d1f357b443f493382a8eb3ccd183872ae6009" 526 | integrity sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w== 527 | 528 | nanoclone@^0.2.1: 529 | version "0.2.1" 530 | resolved "https://registry.yarnpkg.com/nanoclone/-/nanoclone-0.2.1.tgz#dd4090f8f1a110d26bb32c49ed2f5b9235209ed4" 531 | integrity sha512-wynEP02LmIbLpcYw8uBKpcfF6dmg2vcpKqxeH5UcoKEYdExslsdUA4ugFauuaeYdTB76ez6gJW8XAZ6CgkXYxA== 532 | 533 | nanoid@^3.1.22: 534 | version "3.1.22" 535 | resolved "https://registry.yarnpkg.com/nanoid/-/nanoid-3.1.22.tgz#b35f8fb7d151990a8aebd5aa5015c03cf726f844" 536 | integrity sha512-/2ZUaJX2ANuLtTvqTlgqBQNJoQO398KyJgZloL0PZkC0dpysjncRUPsFe3DUPzz/y3h+u7C46np8RMuvF3jsSQ== 537 | 538 | negotiator@0.6.2: 539 | version "0.6.2" 540 | resolved "https://registry.yarnpkg.com/negotiator/-/negotiator-0.6.2.tgz#feacf7ccf525a77ae9634436a64883ffeca346fb" 541 | integrity sha512-hZXc7K2e+PgeI1eDBe/10Ard4ekbfrrqG8Ep+8Jmf4JID2bNg7NvCPOZN+kfF574pFQI7mum2AUqDidoKqcTOw== 542 | 543 | object-assign@^4: 544 | version "4.1.1" 545 | resolved "https://registry.yarnpkg.com/object-assign/-/object-assign-4.1.1.tgz#2109adc7965887cfc05cbbd442cac8bfbb360863" 546 | integrity sha1-IQmtx5ZYh8/AXLvUQsrIv7s2CGM= 547 | 548 | on-finished@~2.3.0: 549 | version "2.3.0" 550 | resolved "https://registry.yarnpkg.com/on-finished/-/on-finished-2.3.0.tgz#20f1336481b083cd75337992a16971aa2d906947" 551 | integrity sha1-IPEzZIGwg811M3mSoWlxqi2QaUc= 552 | dependencies: 553 | ee-first "1.1.1" 554 | 555 | parseurl@~1.3.3: 556 | version "1.3.3" 557 | resolved "https://registry.yarnpkg.com/parseurl/-/parseurl-1.3.3.tgz#9da19e7bee8d12dff0513ed5b76957793bc2e8d4" 558 | integrity sha512-CiyeOxFT/JZyN5m0z9PfXw4SCBJ6Sygz1Dpl0wqjlhDEGGBP1GnsUVEL0p63hoG1fcj3fHynXi9NYO4nWOL+qQ== 559 | 560 | path-to-regexp@0.1.7: 561 | version "0.1.7" 562 | resolved "https://registry.yarnpkg.com/path-to-regexp/-/path-to-regexp-0.1.7.tgz#df604178005f522f15eb4490e7247a1bfaa67f8c" 563 | integrity sha1-32BBeABfUi8V60SQ5yR6G/qmf4w= 564 | 565 | process-nextick-args@~2.0.0: 566 | version "2.0.1" 567 | resolved "https://registry.yarnpkg.com/process-nextick-args/-/process-nextick-args-2.0.1.tgz#7820d9b16120cc55ca9ae7792680ae7dba6d7fe2" 568 | integrity sha512-3ouUOpQhtgrbOa17J7+uxOTpITYWaGP7/AhoR3+A+/1e9skrzelGi/dXzEYyvbxubEF6Wn2ypscTKiKJFFn1ag== 569 | 570 | property-expr@^2.0.4: 571 | version "2.0.4" 572 | resolved "https://registry.yarnpkg.com/property-expr/-/property-expr-2.0.4.tgz#37b925478e58965031bb612ec5b3260f8241e910" 573 | integrity sha512-sFPkHQjVKheDNnPvotjQmm3KD3uk1fWKUN7CrpdbwmUx3CrG3QiM8QpTSimvig5vTXmTvjz7+TDvXOI9+4rkcg== 574 | 575 | proxy-addr@~2.0.5: 576 | version "2.0.6" 577 | resolved "https://registry.yarnpkg.com/proxy-addr/-/proxy-addr-2.0.6.tgz#fdc2336505447d3f2f2c638ed272caf614bbb2bf" 578 | integrity sha512-dh/frvCBVmSsDYzw6n926jv974gddhkFPfiN8hPOi30Wax25QZyZEGveluCgliBnqmuM+UJmBErbAUFIoDbjOw== 579 | dependencies: 580 | forwarded "~0.1.2" 581 | ipaddr.js "1.9.1" 582 | 583 | qs@6.7.0: 584 | version "6.7.0" 585 | resolved "https://registry.yarnpkg.com/qs/-/qs-6.7.0.tgz#41dc1a015e3d581f1621776be31afb2876a9b1bc" 586 | integrity sha512-VCdBRNFTX1fyE7Nb6FYoURo/SPe62QCaAyzJvUjwRaIsc+NePBEniHlvxFmmX56+HZphIGtV0XeCirBtpDrTyQ== 587 | 588 | range-parser@~1.2.1: 589 | version "1.2.1" 590 | resolved "https://registry.yarnpkg.com/range-parser/-/range-parser-1.2.1.tgz#3cf37023d199e1c24d1a55b84800c2f3e6468031" 591 | integrity sha512-Hrgsx+orqoygnmhFbKaHE6c296J+HTAQXoxEF6gNupROmmGJRoyzfG3ccAveqCBrwr/2yxQ5BVd/GTl5agOwSg== 592 | 593 | raw-body@2.4.0: 594 | version "2.4.0" 595 | resolved "https://registry.yarnpkg.com/raw-body/-/raw-body-2.4.0.tgz#a1ce6fb9c9bc356ca52e89256ab59059e13d0332" 596 | integrity sha512-4Oz8DUIwdvoa5qMJelxipzi/iJIi40O5cGV1wNYp5hvZP8ZN0T+jiNkL0QepXs+EsQ9XJ8ipEDoiH70ySUJP3Q== 597 | dependencies: 598 | bytes "3.1.0" 599 | http-errors "1.7.2" 600 | iconv-lite "0.4.24" 601 | unpipe "1.0.0" 602 | 603 | readable-stream@^2.3.5: 604 | version "2.3.7" 605 | resolved "https://registry.yarnpkg.com/readable-stream/-/readable-stream-2.3.7.tgz#1eca1cf711aef814c04f62252a36a62f6cb23b57" 606 | integrity sha512-Ebho8K4jIbHAxnuxi7o42OrZgF/ZTNcsZj6nRKyUmkhLFq8CHItp/fy6hQZuZmP/n3yZ9VBUbp4zz/mX8hmYPw== 607 | dependencies: 608 | core-util-is "~1.0.0" 609 | inherits "~2.0.3" 610 | isarray "~1.0.0" 611 | process-nextick-args "~2.0.0" 612 | safe-buffer "~5.1.1" 613 | string_decoder "~1.1.1" 614 | util-deprecate "~1.0.1" 615 | 616 | regenerator-runtime@^0.13.4: 617 | version "0.13.7" 618 | resolved "https://registry.yarnpkg.com/regenerator-runtime/-/regenerator-runtime-0.13.7.tgz#cac2dacc8a1ea675feaabaeb8ae833898ae46f55" 619 | integrity sha512-a54FxoJDIr27pgf7IgeQGxmqUNYrcV338lf/6gH456HZ/PhX+5BcwHXG9ajESmwe6WRO0tAzRUrRmNONWgkrew== 620 | 621 | regexp-clone@1.0.0, regexp-clone@^1.0.0: 622 | version "1.0.0" 623 | resolved "https://registry.yarnpkg.com/regexp-clone/-/regexp-clone-1.0.0.tgz#222db967623277056260b992626354a04ce9bf63" 624 | integrity sha512-TuAasHQNamyyJ2hb97IuBEif4qBHGjPHBS64sZwytpLEqtBQ1gPJTnOaQ6qmpET16cK14kkjbazl6+p0RRv0yw== 625 | 626 | require_optional@^1.0.1: 627 | version "1.0.1" 628 | resolved "https://registry.yarnpkg.com/require_optional/-/require_optional-1.0.1.tgz#4cf35a4247f64ca3df8c2ef208cc494b1ca8fc2e" 629 | integrity sha512-qhM/y57enGWHAe3v/NcwML6a3/vfESLe/sGM2dII+gEO0BpKRUkWZow/tyloNqJyN6kXSl3RyyM8Ll5D/sJP8g== 630 | dependencies: 631 | resolve-from "^2.0.0" 632 | semver "^5.1.0" 633 | 634 | resolve-from@^2.0.0: 635 | version "2.0.0" 636 | resolved "https://registry.yarnpkg.com/resolve-from/-/resolve-from-2.0.0.tgz#9480ab20e94ffa1d9e80a804c7ea147611966b57" 637 | integrity sha1-lICrIOlP+h2egKgEx+oUdhGWa1c= 638 | 639 | safe-buffer@5.1.2, safe-buffer@~5.1.0, safe-buffer@~5.1.1: 640 | version "5.1.2" 641 | resolved "https://registry.yarnpkg.com/safe-buffer/-/safe-buffer-5.1.2.tgz#991ec69d296e0313747d59bdfd2b745c35f8828d" 642 | integrity sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g== 643 | 644 | safe-buffer@5.2.1, safe-buffer@^5.1.1, safe-buffer@^5.1.2: 645 | version "5.2.1" 646 | resolved "https://registry.yarnpkg.com/safe-buffer/-/safe-buffer-5.2.1.tgz#1eaf9fa9bdb1fdd4ec75f58f9cdb4e6b7827eec6" 647 | integrity sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ== 648 | 649 | "safer-buffer@>= 2.1.2 < 3": 650 | version "2.1.2" 651 | resolved "https://registry.yarnpkg.com/safer-buffer/-/safer-buffer-2.1.2.tgz#44fa161b0187b9549dd84bb91802f9bd8385cd6a" 652 | integrity sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg== 653 | 654 | saslprep@^1.0.0: 655 | version "1.0.3" 656 | resolved "https://registry.yarnpkg.com/saslprep/-/saslprep-1.0.3.tgz#4c02f946b56cf54297e347ba1093e7acac4cf226" 657 | integrity sha512-/MY/PEMbk2SuY5sScONwhUDsV2p77Znkb/q3nSVstq/yQzYJOH/Azh29p9oJLsl3LnQwSvZDKagDGBsBwSooag== 658 | dependencies: 659 | sparse-bitfield "^3.0.3" 660 | 661 | semver@^5.1.0: 662 | version "5.7.1" 663 | resolved "https://registry.yarnpkg.com/semver/-/semver-5.7.1.tgz#a954f931aeba508d307bbf069eff0c01c96116f7" 664 | integrity sha512-sauaDf/PZdVgrLTNYHRtpXa1iRiKcaebiKQ1BJdpQlWH2lCvexQdX55snPFyK7QzpudqbCI0qXFfOasHdyNDGQ== 665 | 666 | send@0.17.1: 667 | version "0.17.1" 668 | resolved "https://registry.yarnpkg.com/send/-/send-0.17.1.tgz#c1d8b059f7900f7466dd4938bdc44e11ddb376c8" 669 | integrity sha512-BsVKsiGcQMFwT8UxypobUKyv7irCNRHk1T0G680vk88yf6LBByGcZJOTJCrTP2xVN6yI+XjPJcNuE3V4fT9sAg== 670 | dependencies: 671 | debug "2.6.9" 672 | depd "~1.1.2" 673 | destroy "~1.0.4" 674 | encodeurl "~1.0.2" 675 | escape-html "~1.0.3" 676 | etag "~1.8.1" 677 | fresh "0.5.2" 678 | http-errors "~1.7.2" 679 | mime "1.6.0" 680 | ms "2.1.1" 681 | on-finished "~2.3.0" 682 | range-parser "~1.2.1" 683 | statuses "~1.5.0" 684 | 685 | serve-static@1.14.1: 686 | version "1.14.1" 687 | resolved "https://registry.yarnpkg.com/serve-static/-/serve-static-1.14.1.tgz#666e636dc4f010f7ef29970a88a674320898b2f9" 688 | integrity sha512-JMrvUwE54emCYWlTI+hGrGv5I8dEwmco/00EvkzIIsR7MqrHonbD9pO2MOfFnpFntl7ecpZs+3mW+XbQZu9QCg== 689 | dependencies: 690 | encodeurl "~1.0.2" 691 | escape-html "~1.0.3" 692 | parseurl "~1.3.3" 693 | send "0.17.1" 694 | 695 | setprototypeof@1.1.1: 696 | version "1.1.1" 697 | resolved "https://registry.yarnpkg.com/setprototypeof/-/setprototypeof-1.1.1.tgz#7e95acb24aa92f5885e0abef5ba131330d4ae683" 698 | integrity sha512-JvdAWfbXeIGaZ9cILp38HntZSFSo3mWg6xGcJJsd+d4aRMOqauag1C63dJfDw7OaMYwEbHMOxEZ1lqVRYP2OAw== 699 | 700 | sift@7.0.1: 701 | version "7.0.1" 702 | resolved "https://registry.yarnpkg.com/sift/-/sift-7.0.1.tgz#47d62c50b159d316f1372f8b53f9c10cd21a4b08" 703 | integrity sha512-oqD7PMJ+uO6jV9EQCl0LrRw1OwsiPsiFQR5AR30heR+4Dl7jBBbDLnNvWiak20tzZlSE1H7RB30SX/1j/YYT7g== 704 | 705 | sliced@1.0.1: 706 | version "1.0.1" 707 | resolved "https://registry.yarnpkg.com/sliced/-/sliced-1.0.1.tgz#0b3a662b5d04c3177b1926bea82b03f837a2ef41" 708 | integrity sha1-CzpmK10Ewxd7GSa+qCsD+Dei70E= 709 | 710 | source-map-support@^0.5.17: 711 | version "0.5.19" 712 | resolved "https://registry.yarnpkg.com/source-map-support/-/source-map-support-0.5.19.tgz#a98b62f86dcaf4f67399648c085291ab9e8fed61" 713 | integrity sha512-Wonm7zOCIJzBGQdB+thsPar0kYuCIzYvxZwlBa87yi/Mdjv7Tip2cyVbLj5o0cFPN4EVkuTwb3GDDyUx2DGnGw== 714 | dependencies: 715 | buffer-from "^1.0.0" 716 | source-map "^0.6.0" 717 | 718 | source-map@^0.6.0: 719 | version "0.6.1" 720 | resolved "https://registry.yarnpkg.com/source-map/-/source-map-0.6.1.tgz#74722af32e9614e9c287a8d0bbde48b5e2f1a263" 721 | integrity sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g== 722 | 723 | sparse-bitfield@^3.0.3: 724 | version "3.0.3" 725 | resolved "https://registry.yarnpkg.com/sparse-bitfield/-/sparse-bitfield-3.0.3.tgz#ff4ae6e68656056ba4b3e792ab3334d38273ca11" 726 | integrity sha1-/0rm5oZWBWuks+eSqzM004JzyhE= 727 | dependencies: 728 | memory-pager "^1.0.2" 729 | 730 | "statuses@>= 1.5.0 < 2", statuses@~1.5.0: 731 | version "1.5.0" 732 | resolved "https://registry.yarnpkg.com/statuses/-/statuses-1.5.0.tgz#161c7dac177659fd9811f43771fa99381478628c" 733 | integrity sha1-Fhx9rBd2Wf2YEfQ3cfqZOBR4Yow= 734 | 735 | string_decoder@~1.1.1: 736 | version "1.1.1" 737 | resolved "https://registry.yarnpkg.com/string_decoder/-/string_decoder-1.1.1.tgz#9cf1611ba62685d7030ae9e4ba34149c3af03fc8" 738 | integrity sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg== 739 | dependencies: 740 | safe-buffer "~5.1.0" 741 | 742 | toidentifier@1.0.0: 743 | version "1.0.0" 744 | resolved "https://registry.yarnpkg.com/toidentifier/-/toidentifier-1.0.0.tgz#7e1be3470f1e77948bc43d94a3c8f4d7752ba553" 745 | integrity sha512-yaOH/Pk/VEhBWWTlhI+qXxDFXlejDGcQipMlyxda9nthulaxLZUNcUqFxokp0vcYnvteJln5FNQDRrxj3YcbVw== 746 | 747 | toposort@^2.0.2: 748 | version "2.0.2" 749 | resolved "https://registry.yarnpkg.com/toposort/-/toposort-2.0.2.tgz#ae21768175d1559d48bef35420b2f4962f09c330" 750 | integrity sha1-riF2gXXRVZ1IvvNUILL0li8JwzA= 751 | 752 | ts-node@^9.1.1: 753 | version "9.1.1" 754 | resolved "https://registry.yarnpkg.com/ts-node/-/ts-node-9.1.1.tgz#51a9a450a3e959401bda5f004a72d54b936d376d" 755 | integrity sha512-hPlt7ZACERQGf03M253ytLY3dHbGNGrAq9qIHWUY9XHYl1z7wYngSr3OQ5xmui8o2AaxsONxIzjafLUiWBo1Fg== 756 | dependencies: 757 | arg "^4.1.0" 758 | create-require "^1.1.0" 759 | diff "^4.0.1" 760 | make-error "^1.1.1" 761 | source-map-support "^0.5.17" 762 | yn "3.1.1" 763 | 764 | type-is@~1.6.17, type-is@~1.6.18: 765 | version "1.6.18" 766 | resolved "https://registry.yarnpkg.com/type-is/-/type-is-1.6.18.tgz#4e552cd05df09467dcbc4ef739de89f2cf37c131" 767 | integrity sha512-TkRKr9sUTxEH8MdfuCSP7VizJyzRNMjj2J2do2Jr3Kym598JVdEksuzPQCnlFPW4ky9Q+iA+ma9BGm06XQBy8g== 768 | dependencies: 769 | media-typer "0.3.0" 770 | mime-types "~2.1.24" 771 | 772 | typescript@^4.2.3: 773 | version "4.2.3" 774 | resolved "https://registry.yarnpkg.com/typescript/-/typescript-4.2.3.tgz#39062d8019912d43726298f09493d598048c1ce3" 775 | integrity sha512-qOcYwxaByStAWrBf4x0fibwZvMRG+r4cQoTjbPtUlrWjBHbmCAww1i448U0GJ+3cNNEtebDteo/cHOR3xJ4wEw== 776 | 777 | unpipe@1.0.0, unpipe@~1.0.0: 778 | version "1.0.0" 779 | resolved "https://registry.yarnpkg.com/unpipe/-/unpipe-1.0.0.tgz#b2bf4ee8514aae6165b4817829d21b2ef49904ec" 780 | integrity sha1-sr9O6FFKrmFltIF4KdIbLvSZBOw= 781 | 782 | util-deprecate@~1.0.1: 783 | version "1.0.2" 784 | resolved "https://registry.yarnpkg.com/util-deprecate/-/util-deprecate-1.0.2.tgz#450d4dc9fa70de732762fbd2d4a28981419a0ccf" 785 | integrity sha1-RQ1Nyfpw3nMnYvvS1KKJgUGaDM8= 786 | 787 | utils-merge@1.0.1: 788 | version "1.0.1" 789 | resolved "https://registry.yarnpkg.com/utils-merge/-/utils-merge-1.0.1.tgz#9f95710f50a267947b2ccc124741c1028427e713" 790 | integrity sha1-n5VxD1CiZ5R7LMwSR0HBAoQn5xM= 791 | 792 | vary@^1, vary@~1.1.2: 793 | version "1.1.2" 794 | resolved "https://registry.yarnpkg.com/vary/-/vary-1.1.2.tgz#2299f02c6ded30d4a5961b0b9f74524a18f634fc" 795 | integrity sha1-IpnwLG3tMNSllhsLn3RSShj2NPw= 796 | 797 | yn@3.1.1: 798 | version "3.1.1" 799 | resolved "https://registry.yarnpkg.com/yn/-/yn-3.1.1.tgz#1e87401a09d767c1d5eab26a6e4c185182d2eb50" 800 | integrity sha512-Ux4ygGWsu2c7isFWe8Yu1YluJmqVhxqK2cLXNQA5AcC3QfbGNpM7fu0Y8b/z16pXLnFxZYvWhd3fhBY9DLmC6Q== 801 | 802 | yup@^0.32.9: 803 | version "0.32.9" 804 | resolved "https://registry.yarnpkg.com/yup/-/yup-0.32.9.tgz#9367bec6b1b0e39211ecbca598702e106019d872" 805 | integrity sha512-Ci1qN+i2H0XpY7syDQ0k5zKQ/DoxO0LzPg8PAR/X4Mpj6DqaeCoIYEEjDJwhArh3Fa7GWbQQVDZKeXYlSH4JMg== 806 | dependencies: 807 | "@babel/runtime" "^7.10.5" 808 | "@types/lodash" "^4.14.165" 809 | lodash "^4.17.20" 810 | lodash-es "^4.17.15" 811 | nanoclone "^0.2.1" 812 | property-expr "^2.0.4" 813 | toposort "^2.0.2" 814 | -------------------------------------------------------------------------------- /ui/README.md: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/TomDoesTech/FS-URL-shortener-tutorial/6d4cced479e5acfb6dcaae7905777355216b2faf/ui/README.md -------------------------------------------------------------------------------- /ui/UI_README.md: -------------------------------------------------------------------------------- 1 | # URL Shortener user interface 2 | 3 | ## 1.0 Bootstrap React application 4 | 1.1 Create application - npx create-react-app ui --template typescript 5 | 6 | 1.2 Cleanup files 7 | 8 | 1.3 Install required packages - yarn add @chakra-ui/react axios 9 | 10 | 1.4 Add configuration (server endpoint) 11 | 12 | ## 2.0 Network request 13 | 14 | 2.1 Make request to server 15 | 16 | 2.2 Display new short URL 17 | 18 | ## 3.0 Fix backend bugs 19 | 3.1 Validate destination as a URL 20 | 21 | 3.2 Enable CORS 22 | 23 | ## 4.0 Background image 24 | 25 | 4.1 Get the background image from Unsplash 26 | 27 | 4.2 Resize the image on window size change -------------------------------------------------------------------------------- /ui/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "ui", 3 | "version": "0.1.0", 4 | "private": true, 5 | "dependencies": { 6 | "@chakra-ui/react": "^1.4.2", 7 | "@emotion/react": "^11.1.5", 8 | "@emotion/styled": "^11.1.5", 9 | "@testing-library/jest-dom": "^5.11.4", 10 | "@testing-library/react": "^11.1.0", 11 | "@testing-library/user-event": "^12.1.10", 12 | "@types/jest": "^26.0.15", 13 | "@types/node": "^12.0.0", 14 | "@types/react": "^17.0.0", 15 | "@types/react-dom": "^17.0.0", 16 | "axios": "^0.21.1", 17 | "framer-motion": "^4.0.3", 18 | "react": "^17.0.2", 19 | "react-dom": "^17.0.2", 20 | "react-router-dom": "^5.2.0", 21 | "react-scripts": "4.0.3", 22 | "typescript": "^4.1.2", 23 | "web-vitals": "^1.0.1" 24 | }, 25 | "scripts": { 26 | "start": "react-scripts start", 27 | "build": "react-scripts build", 28 | "test": "react-scripts test", 29 | "eject": "react-scripts eject" 30 | }, 31 | "eslintConfig": { 32 | "extends": [ 33 | "react-app", 34 | "react-app/jest" 35 | ] 36 | }, 37 | "browserslist": { 38 | "production": [ 39 | ">0.2%", 40 | "not dead", 41 | "not op_mini all" 42 | ], 43 | "development": [ 44 | "last 1 chrome version", 45 | "last 1 firefox version", 46 | "last 1 safari version" 47 | ] 48 | }, 49 | "devDependencies": { 50 | "@types/react-router-dom": "^5.1.7" 51 | } 52 | } 53 | -------------------------------------------------------------------------------- /ui/public/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/TomDoesTech/FS-URL-shortener-tutorial/6d4cced479e5acfb6dcaae7905777355216b2faf/ui/public/favicon.ico -------------------------------------------------------------------------------- /ui/public/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 12 | 13 | 17 | 18 | 27 | React App 28 | 29 | 30 | 31 |
32 | 42 | 43 | 44 | -------------------------------------------------------------------------------- /ui/public/logo192.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/TomDoesTech/FS-URL-shortener-tutorial/6d4cced479e5acfb6dcaae7905777355216b2faf/ui/public/logo192.png -------------------------------------------------------------------------------- /ui/public/logo512.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/TomDoesTech/FS-URL-shortener-tutorial/6d4cced479e5acfb6dcaae7905777355216b2faf/ui/public/logo512.png -------------------------------------------------------------------------------- /ui/public/manifest.json: -------------------------------------------------------------------------------- 1 | { 2 | "short_name": "React App", 3 | "name": "Create React App Sample", 4 | "icons": [ 5 | { 6 | "src": "favicon.ico", 7 | "sizes": "64x64 32x32 24x24 16x16", 8 | "type": "image/x-icon" 9 | }, 10 | { 11 | "src": "logo192.png", 12 | "type": "image/png", 13 | "sizes": "192x192" 14 | }, 15 | { 16 | "src": "logo512.png", 17 | "type": "image/png", 18 | "sizes": "512x512" 19 | } 20 | ], 21 | "start_url": ".", 22 | "display": "standalone", 23 | "theme_color": "#000000", 24 | "background_color": "#ffffff" 25 | } 26 | -------------------------------------------------------------------------------- /ui/public/robots.txt: -------------------------------------------------------------------------------- 1 | # https://www.robotstxt.org/robotstxt.html 2 | User-agent: * 3 | Disallow: 4 | -------------------------------------------------------------------------------- /ui/src/App.test.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { render, screen } from '@testing-library/react'; 3 | import App from './App'; 4 | 5 | test('renders learn react link', () => { 6 | render(); 7 | const linkElement = screen.getByText(/learn react/i); 8 | expect(linkElement).toBeInTheDocument(); 9 | }); 10 | -------------------------------------------------------------------------------- /ui/src/App.tsx: -------------------------------------------------------------------------------- 1 | import { BrowserRouter as Router, Switch, Route } from "react-router-dom"; 2 | import HomeContainer from "./containers/Home"; 3 | import HandleRedirectContainer from "./containers/HandleRedirect"; 4 | 5 | function App() { 6 | return ( 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | ); 18 | } 19 | 20 | export default App; 21 | -------------------------------------------------------------------------------- /ui/src/components/Background.tsx: -------------------------------------------------------------------------------- 1 | import { useState, useEffect } from "react"; 2 | import { Image } from "@chakra-ui/react"; 3 | 4 | function getWindowDimensions() { 5 | const { innerWidth: width, innerHeight: height } = window; 6 | return { 7 | width, 8 | height, 9 | }; 10 | } 11 | 12 | function useWindowDimensions() { 13 | const [windowDimensions, setWindowDimensions] = useState( 14 | getWindowDimensions() 15 | ); 16 | 17 | useEffect(() => { 18 | function handleResize() { 19 | setWindowDimensions(getWindowDimensions()); 20 | } 21 | 22 | window.addEventListener("resize", handleResize); 23 | return () => window.removeEventListener("resize", handleResize); 24 | }, []); 25 | 26 | return windowDimensions; 27 | } 28 | 29 | function Background() { 30 | const { width, height } = useWindowDimensions(); 31 | const img = `https://source.unsplash.com/random/${width}x${height}`; 32 | 33 | return ( 34 | bg 44 | ); 45 | } 46 | 47 | export default Background; 48 | -------------------------------------------------------------------------------- /ui/src/components/URLShortenerForm.tsx: -------------------------------------------------------------------------------- 1 | import { Input, Button, Box, InputGroup } from "@chakra-ui/react"; 2 | import axios from "axios"; 3 | import { useState } from "react"; 4 | import { SERVER_ENDPOINTS } from "../config"; 5 | 6 | function URLShortenerForm() { 7 | const [destination, setDestination] = useState(); 8 | const [shortUrl, setShortUrl] = useState<{ 9 | shortId: string; 10 | } | null>(null); 11 | 12 | async function handleSubmit(e: React.FormEvent) { 13 | e.preventDefault(); 14 | setShortUrl(null); 15 | const result = await axios 16 | .post(`${SERVER_ENDPOINTS}/api/url`, { 17 | destination, 18 | }) 19 | .then((resp) => resp.data); 20 | 21 | setShortUrl(result); 22 | } 23 | 24 | return ( 25 | 26 |
27 | 28 | setDestination(e.target.value)} 30 | placeholder="https://example.com" 31 | /> 32 | 33 | 34 |
35 | {shortUrl && ( 36 | 37 | {window.location.origin}/{shortUrl?.shortId} 38 | 39 | )} 40 |
41 | ); 42 | } 43 | 44 | export default URLShortenerForm; 45 | -------------------------------------------------------------------------------- /ui/src/config/index.ts: -------------------------------------------------------------------------------- 1 | export const SERVER_ENDPOINTS = 2 | process.env.REACT_APP_SERVER_ENDPOINT || "http://localhost:4000"; 3 | -------------------------------------------------------------------------------- /ui/src/containers/HandleRedirect.tsx: -------------------------------------------------------------------------------- 1 | import axios from "axios"; 2 | import { useEffect, useState } from "react"; 3 | import { useRouteMatch } from "react-router-dom"; 4 | import { Spinner, Box } from "@chakra-ui/react"; 5 | 6 | const SERVER_ENDPOINT = 7 | process.env.REACT_APP_SERVER_ENDPOINT || "http://localhost:4000"; 8 | 9 | function HandleRedirectContainer() { 10 | const [destination, setDestination] = useState(null); 11 | const [error, setError] = useState(); 12 | 13 | const { 14 | params: { shortId }, 15 | } = useRouteMatch<{ 16 | shortId: string; 17 | }>(); 18 | 19 | useEffect(() => { 20 | async function getData() { 21 | return axios 22 | .get(`${SERVER_ENDPOINT}/api/url/${shortId}`) 23 | .then((res) => setDestination(res.data.destination)) 24 | .catch((error) => { 25 | setError(error.message); 26 | }); 27 | } 28 | getData(); 29 | }, [shortId]); 30 | 31 | useEffect(() => { 32 | if (destination) { 33 | window.location.replace(destination); 34 | } 35 | }, [destination]); 36 | 37 | if (!destination && !error) { 38 | return ( 39 | 45 | 46 | 47 | ); 48 | } 49 | 50 | return

{error && JSON.stringify(error)}

; 51 | } 52 | 53 | export default HandleRedirectContainer; 54 | -------------------------------------------------------------------------------- /ui/src/containers/Home.tsx: -------------------------------------------------------------------------------- 1 | import { Box } from "@chakra-ui/react"; 2 | import URLShortenerForm from "../components/URLShortenerForm"; 3 | import Background from "../components/Background"; 4 | 5 | function Home() { 6 | return ( 7 | 13 | 14 | 15 | 16 | ); 17 | } 18 | 19 | export default Home; 20 | -------------------------------------------------------------------------------- /ui/src/index.css: -------------------------------------------------------------------------------- 1 | html, body, #root { 2 | height: 100% 3 | } -------------------------------------------------------------------------------- /ui/src/index.tsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import ReactDOM from "react-dom"; 3 | import { ChakraProvider } from "@chakra-ui/react"; 4 | import "./index.css"; 5 | import App from "./App"; 6 | import reportWebVitals from "./reportWebVitals"; 7 | 8 | ReactDOM.render( 9 | 10 | 11 | 12 | 13 | , 14 | document.getElementById("root") 15 | ); 16 | 17 | // If you want to start measuring performance in your app, pass a function 18 | // to log results (for example: reportWebVitals(console.log)) 19 | // or send to an analytics endpoint. Learn more: https://bit.ly/CRA-vitals 20 | reportWebVitals(); 21 | -------------------------------------------------------------------------------- /ui/src/react-app-env.d.ts: -------------------------------------------------------------------------------- 1 | /// 2 | -------------------------------------------------------------------------------- /ui/src/reportWebVitals.ts: -------------------------------------------------------------------------------- 1 | import { ReportHandler } from 'web-vitals'; 2 | 3 | const reportWebVitals = (onPerfEntry?: ReportHandler) => { 4 | if (onPerfEntry && onPerfEntry instanceof Function) { 5 | import('web-vitals').then(({ getCLS, getFID, getFCP, getLCP, getTTFB }) => { 6 | getCLS(onPerfEntry); 7 | getFID(onPerfEntry); 8 | getFCP(onPerfEntry); 9 | getLCP(onPerfEntry); 10 | getTTFB(onPerfEntry); 11 | }); 12 | } 13 | }; 14 | 15 | export default reportWebVitals; 16 | -------------------------------------------------------------------------------- /ui/src/setupTests.ts: -------------------------------------------------------------------------------- 1 | // jest-dom adds custom jest matchers for asserting on DOM nodes. 2 | // allows you to do things like: 3 | // expect(element).toHaveTextContent(/react/i) 4 | // learn more: https://github.com/testing-library/jest-dom 5 | import '@testing-library/jest-dom'; 6 | -------------------------------------------------------------------------------- /ui/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "es5", 4 | "lib": [ 5 | "dom", 6 | "dom.iterable", 7 | "esnext" 8 | ], 9 | "allowJs": true, 10 | "skipLibCheck": true, 11 | "esModuleInterop": true, 12 | "allowSyntheticDefaultImports": true, 13 | "strict": true, 14 | "forceConsistentCasingInFileNames": true, 15 | "noFallthroughCasesInSwitch": true, 16 | "module": "esnext", 17 | "moduleResolution": "node", 18 | "resolveJsonModule": true, 19 | "isolatedModules": true, 20 | "noEmit": true, 21 | "jsx": "react-jsx" 22 | }, 23 | "include": [ 24 | "src" 25 | ] 26 | } 27 | -------------------------------------------------------------------------------- /yarn-error.log: -------------------------------------------------------------------------------- 1 | Arguments: 2 | /Users/tom/.nvm/versions/node/v12.20.1/bin/node /Users/tom/.nvm/versions/node/v12.20.1/bin/yarn add .env 3 | 4 | PATH: 5 | /Users/tom/.nvm/versions/node/v12.20.1/bin:/usr/local/bin:/usr/bin:/bin:/usr/sbin:/sbin:/usr/local/go/bin:/Users/tom/.nvm/versions/node/v12.20.1/bin:/Users/tom/.cargo/bin:/Applications/Visual Studio Code.app/Contents/Resources/app/bin:/Applications/Visual Studio Code.app/Contents/Resources/app/bin 6 | 7 | Yarn version: 8 | 1.22.10 9 | 10 | Node version: 11 | 12.20.1 12 | 13 | Platform: 14 | darwin x64 15 | 16 | Trace: 17 | Error: https://registry.npmjs.com/.env: Not found 18 | at Request.params.callback [as _callback] (/Users/tom/.nvm/versions/node/v12.20.1/lib/node_modules/yarn/lib/cli.js:66988:18) 19 | at Request.self.callback (/Users/tom/.nvm/versions/node/v12.20.1/lib/node_modules/yarn/lib/cli.js:140662:22) 20 | at Request.emit (events.js:314:20) 21 | at Request. (/Users/tom/.nvm/versions/node/v12.20.1/lib/node_modules/yarn/lib/cli.js:141634:10) 22 | at Request.emit (events.js:314:20) 23 | at IncomingMessage. (/Users/tom/.nvm/versions/node/v12.20.1/lib/node_modules/yarn/lib/cli.js:141556:12) 24 | at Object.onceWrapper (events.js:420:28) 25 | at IncomingMessage.emit (events.js:326:22) 26 | at endReadableNT (_stream_readable.js:1241:12) 27 | at processTicksAndRejections (internal/process/task_queues.js:84:21) 28 | 29 | npm manifest: 30 | { 31 | "dependencies": { 32 | "dotenv": "^8.2.0" 33 | } 34 | } 35 | 36 | yarn manifest: 37 | No manifest 38 | 39 | Lockfile: 40 | # THIS IS AN AUTOGENERATED FILE. DO NOT EDIT THIS FILE DIRECTLY. 41 | # yarn lockfile v1 42 | 43 | 44 | dotenv@^8.2.0: 45 | version "8.2.0" 46 | resolved "https://registry.yarnpkg.com/dotenv/-/dotenv-8.2.0.tgz#97e619259ada750eea3e4ea3e26bceea5424b16a" 47 | integrity sha512-8sJ78ElpbDJBHNeBzUbUVLsqKdccaa/BXF1uPTw3GrvQTBgrQrtObr2mUrE38vzYd8cEv+m/JBfDLioYcfXoaw== 48 | -------------------------------------------------------------------------------- /yarn.lock: -------------------------------------------------------------------------------- 1 | # THIS IS AN AUTOGENERATED FILE. DO NOT EDIT THIS FILE DIRECTLY. 2 | # yarn lockfile v1 3 | 4 | 5 | dotenv@^8.2.0: 6 | version "8.2.0" 7 | resolved "https://registry.yarnpkg.com/dotenv/-/dotenv-8.2.0.tgz#97e619259ada750eea3e4ea3e26bceea5424b16a" 8 | integrity sha512-8sJ78ElpbDJBHNeBzUbUVLsqKdccaa/BXF1uPTw3GrvQTBgrQrtObr2mUrE38vzYd8cEv+m/JBfDLioYcfXoaw== 9 | --------------------------------------------------------------------------------