diff --git a/package.json b/package.json index 5ae7ae6..c734cd5 100644 --- a/package.json +++ b/package.json @@ -30,30 +30,31 @@ "@sentry/node": "^6.15.0", "axios": "^0.24.0", "cron": "^1.8.2", - "lodash": "^4.17.21", - "mongoose-rest-framework": "^0.1.1", - "on-finished": "^2.3.0", - "passport-firebase-jwt": "^1.2.1", "express": "^4.17.1", "express-session": "^1.17.2", "jsonwebtoken": "^8.5.1", + "lodash": "^4.17.21", "mongoose": "^6.0.8", + "mongoose-rest-framework": "^0.1.1", + "on-finished": "^2.3.0", "passport": "^0.5.0", "passport-anonymous": "^1.0.1", + "passport-firebase-jwt": "^1.2.1", "passport-jwt": "^4.0.0", - "passport-local-mongoose": "^6.1.0" + "passport-local-mongoose": "^6.1.0", + "winston": "^3.3.3" }, "devDependencies": { - "@types/cron": "^1.7.3", - "@types/lodash": "^4.14.177", - "@types/on-finished": "^2.3.1", "@types/bcrypt": "^5.0.0", "@types/chai": "^4.2.22", + "@types/cron": "^1.7.3", "@types/express": "^4.17.8", "@types/express-session": "^1.17.4", "@types/jest": "^26.0.20", "@types/jsonwebtoken": "^8.5.5", + "@types/lodash": "^4.14.177", "@types/node": "^14.14.27", + "@types/on-finished": "^2.3.1", "@types/passport": "^1.0.7", "@types/passport-anonymous": "^1.0.3", "@types/passport-jwt": "^3.0.6", diff --git a/src/expressServer.ts b/src/expressServer.ts index b9747d8..501f072 100644 --- a/src/expressServer.ts +++ b/src/expressServer.ts @@ -2,10 +2,12 @@ import * as Sentry from "@sentry/node"; import axios from "axios"; import cron from "cron"; import express, {Router} from "express"; +import fs from "fs"; import cloneDeep from "lodash/cloneDeep"; -import {Env, setupAuth, UserModel} from "./mongooseRestFramework"; import onFinished from "on-finished"; import passport from "passport"; +import winston, {level} from "winston"; +import {Env, setupAuth, UserModel} from "./mongooseRestFramework"; const SLOW_READ_MAX = 200; const SLOW_WRITE_MAX = 500; @@ -20,6 +22,95 @@ if (process.env.NODE_ENV === "production") { export type AddRoutes = (router: Router) => void; +// Setup a default console logger. +export const logger = winston.createLogger({ + level: "debug", + transports: [ + new winston.transports.Console({ + debugStdout: true, + level: "debug", + format: winston.format.combine(winston.format.colorize(), winston.format.simple()), + }), + ], +}); + +interface LoggingOptions { + level?: "debug" | "info" | "warn" | "error"; + transports?: winston.transport[]; + disableFileLogging?: boolean; + disableConsoleLogging?: boolean; + logDirectory?: string; +} + +function setupLogging(options?: LoggingOptions) { + logger.clear(); + if (!options?.disableConsoleLogging) { + logger.add( + new winston.transports.Console({ + debugStdout: !options?.level || options?.level === "debug", + level: options?.level ?? "debug", + format: winston.format.combine(winston.format.colorize(), winston.format.simple()), + }) + ); + } + if (!options?.disableFileLogging) { + const logDirectory = options?.logDirectory ?? "./log"; + if (!fs.existsSync(logDirectory)) { + fs.mkdirSync(logDirectory); + } + + const FILE_LOG_DEFAULTS = { + colorize: false, + compress: true, + dirname: logDirectory, + format: winston.format.simple(), + // 30 days of retention + maxFiles: 30, + // 50MB max file size + maxSize: 1024 * 1024 * 50, + // Only readable by server user + options: {mode: 0o600}, + }; + + logger.add( + new winston.transports.Stream({ + ...FILE_LOG_DEFAULTS, + level: "error", + handleExceptions: true, + // Use stream so we can open log in append mode rather than overwriting. + stream: fs.createWriteStream("error.log", {flags: "a"}), + }) + ); + + logger.add( + new winston.transports.Stream({ + ...FILE_LOG_DEFAULTS, + level: "info", + // Use stream so we can open log in append mode rather than overwriting. + stream: fs.createWriteStream("out.log", {flags: "a"}), + }) + ); + if (!options?.level || options?.level === "debug") { + logger.add( + new winston.transports.Stream({ + ...FILE_LOG_DEFAULTS, + level: "debug", + // Use stream so we can open log in append mode rather than overwriting. + stream: fs.createWriteStream("debug.log", {flags: "a"}), + }) + ); + } + } + + if (options?.transports) { + for (const transport of options.transports) { + logger.add(transport); + } + } + + logger.debug("Logger set up complete"); +} + const logRequestsFinished = function(req: any, res: any, startTime: [number, number]) { const diff = process.hrtime(startTime); const diffInMs = Math.round(diff[0] * 1000 + diff[1] * 0.000001); @@ -29,18 +120,18 @@ const logRequestsFinished = function(req: any, res: any, startTime: [number, num } else if (req.route) { pathName = req.route.path; } else if (res.statusCode < 400) { - console.warn(`Request without route: ${req.originalUrl}`); + logger.warn(`Request without route: ${req.originalUrl}`); } - console.debug(`${req.method} -> ${req.originalUrl} ${res.statusCode} ${diffInMs + "ms"}`); + logger.debug(`${req.method} -> ${req.originalUrl} ${res.statusCode} ${diffInMs + "ms"}`); if (diffInMs > SLOW_READ_MAX && req.method === "GET") { - console.warn("Slow GET request", { + logger.warn("Slow GET request", { requestTime: diffInMs, pathName: pathName, url: req.originalUrl, }); } else if (diffInMs > SLOW_WRITE_MAX) { - console.warn("Slow write request", { + logger.warn("Slow write request", { requestTime: diffInMs, pathName: pathName, url: req.originalUrl, @@ -67,7 +158,7 @@ function logRequests(req: any, res: any, next: any) { body = ` Body: ${JSON.stringify(bodyCopy)}`; } - console.debug(`${req.method} <- ${req.url}${userString}${body}`); + logger.debug(`${req.method} <- ${req.url}${userString}${body}`); onFinished(res, () => logRequestsFinished(req, res, startTime)); next(); } @@ -135,38 +226,49 @@ function initializeRoutes(UserModel: UserModel, addRoutes: AddRoutes) { // eslint-disable-next-line @typescript-eslint/no-unused-vars app.use(function onError(_err: any, _req: any, res: any, _next: any) { - console.error("Fallthrough error", _err); + logger.error("Fallthrough error", _err); res.statusCode = 500; res.end(res.sentry + "\n"); }); - console.debug("Listening on routes:"); + logger.debug("Listening on routes:"); app._router.stack.forEach((r: any) => { if (r.route && r.route.path) { - console.debug("[Route] " + r.route.path); + logger.debug("[Route] " + r.route.path); } }); return app; } +export interface SetupServerOptions { + userModel: UserModel; + addRoutes: AddRoutes; + loggingOptions?: LoggingOptions; +} + // Sets up the routes and returns a function to launch the API. -export function setupServer(UserModel: UserModel, addRoutes: AddRoutes) { +export function setupServer(options: SetupServerOptions) { + const UserModel = options.userModel; + const addRoutes = options.addRoutes; + + setupLogging(options.loggingOptions); + let app: express.Application; try { app = initializeRoutes(UserModel, addRoutes); } catch (e) { - console.error("Error initializing routes", e); + logger.error("Error initializing routes", e); throw e; } const port = process.env.PORT || "9000"; try { app.listen(port, () => { - console.info(`Listening at on port ${port}`); + logger.info(`Listening at on port ${port}`); }); } catch (err) { - console.error(`Error trying to start HTTP server: ${err}\n${(err as any).stack}`); + logger.error(`Error trying to start HTTP server: ${err}\n${(err as any).stack}`); process.exit(1); } return app; @@ -183,7 +285,7 @@ export function cronjob( } else if (schedule === "minutely") { schedule = "* * * * *"; } - console.info(`Adding cronjob ${name}, running at: ${schedule}`); + logger.info(`Adding cronjob ${name}, running at: ${schedule}`); try { new cron.CronJob({ cronTime: schedule, @@ -208,7 +310,7 @@ export async function sendToSlack(text: string, channel = "bots") { channel, }); } catch (e) { - console.error("Error posting to slack", (e as any).text); + logger.error("Error posting to slack", (e as any).text); } } @@ -224,7 +326,7 @@ export async function wrapScript(func: () => Promise, options: WrapScriptOp .split("/") .slice(-1)[0] .replace(".ts", ""); - console.log(`Running script ${name}`); + logger.info(`Running script ${name}`); sendToSlack(`Running script ${name}`, options.slackChannel); if (options.terminateTimeout !== 0) { @@ -233,13 +335,13 @@ export async function wrapScript(func: () => Promise, options: WrapScriptOp setTimeout(() => { const msg = `Script ${name} is taking a while, currently ${warnTime / 1000} seconds`; sendToSlack(msg); - console.warn(msg); + logger.warn(msg); }, warnTime); setTimeout(async () => { const msg = `Script ${name} took too long, exiting`; await sendToSlack(msg); - console.error(msg); + logger.error(msg); Sentry.captureException(new Error(`Script ${name} took too long, exiting`)); await Sentry.flush(); process.exit(2); @@ -254,7 +356,7 @@ export async function wrapScript(func: () => Promise, options: WrapScriptOp } } catch (e) { Sentry.captureException(e); - console.error(`Error running script ${name}: ${e}\n${(e as Error).stack}`); + logger.error(`Error running script ${name}: ${e}\n${(e as Error).stack}`); sendToSlack(`Error running script ${name}: ${e}\n${(e as Error).stack}`); await Sentry.flush(); process.exit(1); diff --git a/src/mongooseRestFramework.test.ts b/src/mongooseRestFramework.test.ts index 4604a4e..17b296b 100644 --- a/src/mongooseRestFramework.test.ts +++ b/src/mongooseRestFramework.test.ts @@ -11,6 +11,7 @@ import { setupAuth, tokenPlugin, } from "./mongooseRestFramework"; +import qs from "qs"; const assert = chai.assert; @@ -29,6 +30,9 @@ interface Food { created: Date; ownerId: mongoose.Types.ObjectId | User; hidden?: boolean; + source: { + name: string; + }; } const userSchema = new Schema({ @@ -52,6 +56,9 @@ const schema = new Schema({ calories: Number, created: Date, ownerId: {type: "ObjectId", ref: "User"}, + source: { + name: String, + }, hidden: {type: Boolean, default: false}, }); @@ -576,6 +583,7 @@ describe("mongoose rest framework", () => { let spinach: Food; let apple: Food; let carrots: Food; + let pizza: Food; describe("list options", function() { let notAdmin: any; @@ -593,25 +601,38 @@ describe("mongoose rest framework", () => { await (admin as any).setPassword("securePassword"); await admin.save(); - [spinach, apple, carrots] = await Promise.all([ + [spinach, apple, carrots, pizza] = await Promise.all([ FoodModel.create({ name: "Spinach", calories: 1, - created: new Date(), + created: new Date("2021-12-03T00:00:20.000Z"), ownerId: notAdmin._id, hidden: false, + source: { + name: "Brand", + }, }), FoodModel.create({ name: "Apple", calories: 100, - created: new Date().getTime() - 10, + created: new Date("2021-12-03T00:00:30.000Z"), ownerId: admin._id, hidden: true, }), FoodModel.create({ name: "Carrots", calories: 100, - created: new Date().getTime() - 20, + created: new Date("2021-12-03T00:00:00.000Z"), + ownerId: admin._id, + hidden: false, + source: { + name: "USDA", + }, + }), + FoodModel.create({ + name: "Pizza", + calories: 400, + created: new Date("2021-12-03T00:00:10.000Z"), ownerId: admin._id, hidden: false, }), @@ -629,10 +650,10 @@ describe("mongoose rest framework", () => { delete: [Permissions.IsAdmin], }, defaultLimit: 2, - maxLimit: 2, + maxLimit: 3, sort: {created: "descending"}, defaultQueryParams: {hidden: false}, - queryFields: ["hidden", "calories"], + queryFields: ["hidden", "calories", "created", "source.name"], populatePaths: ["ownerId"], }) ); @@ -647,22 +668,32 @@ describe("mongoose rest framework", () => { }); it("list limit over", async function() { + // This shouldn't be seen, it's the end of the list. + await FoodModel.create({ + name: "Pizza", + calories: 400, + created: new Date("2021-12-02T00:00:10.000Z"), + ownerId: admin._id, + hidden: false, + }); const res = await server.get("/food?limit=4").expect(200); - assert.lengthOf(res.body.data, 2); + console.log(res.body.data); + assert.lengthOf(res.body.data, 3); assert.equal(res.body.data[0].id, (spinach as any).id); - assert.equal(res.body.data[1].id, (carrots as any).id); + assert.equal(res.body.data[1].id, (pizza as any).id); + assert.equal(res.body.data[2].id, (carrots as any).id); }); it("list page", async function() { // Should skip to carrots since apples are hidden const res = await server.get("/food?limit=1&page=2").expect(200); assert.lengthOf(res.body.data, 1); - assert.equal(res.body.data[0].id, (carrots as any).id); + assert.equal(res.body.data[0].id, (pizza as any).id); }); it("list page over", async function() { // Should skip to carrots since apples are hidden - const res = await server.get("/food?limit=1&page=4").expect(200); + const res = await server.get("/food?limit=1&page=5").expect(200); assert.lengthOf(res.body.data, 0); }); @@ -678,6 +709,72 @@ describe("mongoose rest framework", () => { const res = await server.get("/food?name=Apple").expect(400); assert.equal(res.body.message, "name is not allowed as a query param."); }); + + it("list query by nested param", async function() { + // Should skip to carrots since apples are hidden + const res = await server.get("/food?source.name=USDA").expect(200); + assert.lengthOf(res.body.data, 1); + assert.equal(res.body.data[0].id, (carrots as any).id); + }); + + it("query by date", async function() { + const authRes = await server + .post("/auth/login") + .send({email: "admin@example.com", password: "securePassword"}) + .expect(200); + const token = authRes.body.data.token; + + // Inclusive + let res = await server + .get( + `/food?limit=3&${qs.stringify({ + created: { + $lte: "2021-12-03T00:00:20.000Z", + $gte: "2021-12-03T00:00:00.000Z", + }, + })}` + ) + .set("authorization", `Bearer ${token}`) + .expect(200); + assert.sameDeepMembers( + ["2021-12-03T00:00:20.000Z", "2021-12-03T00:00:10.000Z", "2021-12-03T00:00:00.000Z"], + res.body.data.map((d: any) => d.created) + ); + + // Inclusive one side + res = await server + .get( + `/food?limit=3&${qs.stringify({ + created: { + $lt: "2021-12-03T00:00:20.000Z", + $gte: "2021-12-03T00:00:00.000Z", + }, + })}` + ) + .set("authorization", `Bearer ${token}`) + .expect(200); + assert.sameDeepMembers( + ["2021-12-03T00:00:10.000Z", "2021-12-03T00:00:00.000Z"], + res.body.data.map((d: any) => d.created) + ); + + // Inclusive both sides + res = await server + .get( + `/food?limit=3&${qs.stringify({ + created: { + $lt: "2021-12-03T00:00:20.000Z", + $gt: "2021-12-03T00:00:00.000Z", + }, + })}` + ) + .set("authorization", `Bearer ${token}`) + .expect(200); + assert.sameDeepMembers( + ["2021-12-03T00:00:10.000Z"], + res.body.data.map((d: any) => d.created) + ); + }); }); }); diff --git a/src/mongooseRestFramework.ts b/src/mongooseRestFramework.ts index 2f33ef9..24fed87 100644 --- a/src/mongooseRestFramework.ts +++ b/src/mongooseRestFramework.ts @@ -7,6 +7,7 @@ import passport from "passport"; import {Strategy as JwtStrategy} from "passport-jwt"; import {Strategy as AnonymousStrategy} from "passport-anonymous"; import {Strategy as LocalStrategy} from "passport-local"; +import {logger} from "./expressServer"; export interface Env { NODE_ENV?: string; @@ -224,7 +225,7 @@ export function createdDeletedPlugin(schema: Schema) { schema.add({created: {type: Date, index: true}}); schema.pre("save", function(next) { - if (this.disablecreatedDeletedPlugin === true) { + if (this.disableCreatedDeletedPlugin === true) { next(); return; } @@ -299,20 +300,20 @@ export function setupAuth(app: express.Application, userModel: UserModel) { const user = await userModel.findOne({email}); if (!user) { - console.debug("Could not find login user for", email); + logger.debug("Could not find login user for", email); return done(null, false, {message: "User not found"}); } const validate = await (user as any).authenticate(password); if (!validate) { - console.debug("Invalid password for", email); + logger.debug("Invalid password for", email); return done(null, false, {message: "Wrong Password"}); } return done(null, user, {message: "Logged in Successfully"}); } catch (error) { - console.error("Login error", error); + logger.error("Login error", error); return done(error); } } @@ -334,7 +335,7 @@ export function setupAuth(app: express.Application, userModel: UserModel) { passport.deserializeUser(userModel.deserializeUser()); if ((process.env as Env).TOKEN_SECRET) { - console.debug("Setting up JWT Authentication"); + logger.debug("Setting up JWT Authentication"); const customExtractor = function(req: express.Request) { let token = null; @@ -368,18 +369,18 @@ export function setupAuth(app: express.Application, userModel: UserModel) { try { user = await userModel.findById((payload as any).id); } catch (e) { - console.warn("[jwt] Error finding user from id", e); + logger.warn("[jwt] Error finding user from id", e); return done(e, false); } if (user) { return done(null, user); } else { if (userModel.createAnonymousUser) { - console.log("[jwt] Creating anonymous user"); + logger.info("[jwt] Creating anonymous user"); user = await userModel.createAnonymousUser(); return done(null, user); } else { - console.log("[jwt] No user found from token"); + logger.info("[jwt] No user found from token"); return done(null, false); } } @@ -447,6 +448,7 @@ export function setupAuth(app: express.Application, userModel: UserModel) { app.use(passport.initialize() as any); app.use(passport.session()); + app.set("etag", false); app.use("/auth", router); } @@ -568,7 +570,7 @@ export function gooseRestRouter( // TODO Toggle anonymous auth middleware based on settings for route. router.post("/", authenticateMiddleware(true), async (req, res) => { if (!checkPermissions("create", options.permissions.create, req.user)) { - console.warn(`Access to CREATE on ${model.name} denied for ${req.user?.id}`); + logger.warn(`Access to CREATE on ${model.name} denied for ${req.user?.id}`); return res.status(405).send(); } @@ -584,7 +586,7 @@ export function gooseRestRouter( router.get("/", authenticateMiddleware(true), async (req, res) => { if (!checkPermissions("list", options.permissions.list, req.user)) { - console.warn(`Access to LIST on ${model.name} denied for ${req.user?.id}`); + logger.warn(`Access to LIST on ${model.name} denied for ${req.user?.id}`); return res.status(403).send(); } @@ -607,7 +609,7 @@ export function gooseRestRouter( query[queryParam] = req.query[queryParam]; } } else { - console.debug("Unallowed query param", queryParam); + logger.debug("Unallowed query param", queryParam); return res.status(400).json({message: `${queryParam} is not allowed as a query param.`}); } } @@ -633,8 +635,8 @@ export function gooseRestRouter( } let limit = options.defaultLimit ?? 100; - if (req.query.limit && Number(req.query.limit) < (options.maxLimit ?? 500)) { - limit = Number(req.query.limit); + if (Number(req.query.limit)) { + limit = Math.min(Number(req.query.limit), options.maxLimit ?? 500); } let builtQuery = model.find(query).limit(limit); @@ -656,21 +658,21 @@ export function gooseRestRouter( try { data = await builtQuery.exec(); } catch (e) { - console.error("List error", e); + logger.error("List error", e); return res.status(500).send(); } // TODO add pagination try { return res.json({data: serialize(data, req.user)}); } catch (e) { - console.error("Serialization error", e); + logger.error("Serialization error", e); return res.status(500).send(); } }); router.get("/:id", authenticateMiddleware(true), async (req, res) => { if (!checkPermissions("read", options.permissions.read, req.user)) { - console.warn(`Access to READ on ${model.name} denied for ${req.user?.id}`); + logger.warn(`Access to READ on ${model.name} denied for ${req.user?.id}`); return res.status(405).send(); } @@ -681,7 +683,7 @@ export function gooseRestRouter( } if (!checkPermissions("read", options.permissions.read, req.user, data)) { - console.warn(`Access to READ on ${model.name}:${req.params.id} denied for ${req.user?.id}`); + logger.warn(`Access to READ on ${model.name}:${req.params.id} denied for ${req.user?.id}`); return res.status(403).send(); } @@ -695,7 +697,7 @@ export function gooseRestRouter( router.patch("/:id", authenticateMiddleware(true), async (req, res) => { if (!checkPermissions("update", options.permissions.update, req.user)) { - console.warn(`Access to PATCH on ${model.name} denied for ${req.user?.id}`); + logger.warn(`Access to PATCH on ${model.name} denied for ${req.user?.id}`); return res.status(405).send(); } @@ -706,7 +708,7 @@ export function gooseRestRouter( } if (!checkPermissions("update", options.permissions.update, req.user, doc)) { - console.warn(`Patch not allowed for user ${req.user?.id} on doc ${doc._id}`); + logger.warn(`Patch not allowed for user ${req.user?.id} on doc ${doc._id}`); return res.status(403).send(); } @@ -714,7 +716,7 @@ export function gooseRestRouter( try { body = transform(req.body, "update", req.user); } catch (e) { - console.warn(`Patch failed for user ${req.user?.id}: ${(e as any).message}`); + logger.warn(`Patch failed for user ${req.user?.id}: ${(e as any).message}`); return res.status(403).send({message: (e as any).message}); } doc = await model.findOneAndUpdate({_id: req.params.id}, body, {new: true}); @@ -723,7 +725,7 @@ export function gooseRestRouter( router.delete("/:id", authenticateMiddleware(true), async (req, res) => { if (!checkPermissions("delete", options.permissions.delete, req.user)) { - console.warn(`Access to DELETE on ${model.name} denied for ${req.user?.id}`); + logger.warn(`Access to DELETE on ${model.name} denied for ${req.user?.id}`); return res.status(405).send(); } @@ -734,7 +736,7 @@ export function gooseRestRouter( } if (!checkPermissions("delete", options.permissions.delete, req.user, data)) { - console.warn(`Access to DELETE on ${model.name}:${req.params.id} denied for ${req.user?.id}`); + logger.warn(`Access to DELETE on ${model.name}:${req.params.id} denied for ${req.user?.id}`); return res.status(403).send(); } diff --git a/yarn.lock b/yarn.lock index 9d0e024..3bcf348 100644 --- a/yarn.lock +++ b/yarn.lock @@ -318,6 +318,15 @@ exec-sh "^0.3.2" minimist "^1.2.0" +"@dabh/diagnostics@^2.0.2": + version "2.0.2" + resolved "https://registry.yarnpkg.com/@dabh/diagnostics/-/diagnostics-2.0.2.tgz#290d08f7b381b8f94607dc8f471a12c675f9db31" + integrity sha512-+A1YivoVDNNVCdfozHSR8v/jyuuLTMXwjWuxPFlFlUapXoGc+Gj9mDlTDDfrwl7rXCl2tNZ0kE8sIBO6YOn96Q== + dependencies: + colorspace "1.1.x" + enabled "2.0.x" + kuler "^2.0.0" + "@istanbuljs/load-nyc-config@^1.0.0": version "1.1.0" resolved "https://registry.yarnpkg.com/@istanbuljs/load-nyc-config/-/load-nyc-config-1.1.0.tgz#fd3db1d59ecf7cf121e80650bb86712f9b55eced" @@ -1329,6 +1338,11 @@ astral-regex@^1.0.0: resolved "https://registry.yarnpkg.com/astral-regex/-/astral-regex-1.0.0.tgz#6c8c3fb827dd43ee3918f27b82782ab7658a6fd9" integrity sha512-+Ryf6g3BKoRc7jfp7ad8tM4TtMiaWvbF/1/sQcZPkkS7ag3D5nMBCe2UfOTONtAkaG0tO0ij3C5Lwmf1EiyjHg== +async@^3.1.0: + version "3.2.2" + resolved "https://registry.yarnpkg.com/async/-/async-3.2.2.tgz#2eb7671034bb2194d45d30e31e24ec7e7f9670cd" + integrity sha512-H0E+qZaDEfx/FY4t7iLRv1W2fFI6+pyCeTw1uN20AQPiwqwM6ojPxHxdLv4z8hi2DtnW9BOckSspLucW7pIE5g== + asynckit@^0.4.0: version "0.4.0" resolved "https://registry.yarnpkg.com/asynckit/-/asynckit-0.4.0.tgz#c79ed97f7f34cb8f2ba1bc9790bcc366474b4b79" @@ -1748,7 +1762,7 @@ collection-visit@^1.0.0: map-visit "^1.0.0" object-visit "^1.0.0" -color-convert@^1.9.0: +color-convert@^1.9.0, color-convert@^1.9.3: version "1.9.3" resolved "https://registry.yarnpkg.com/color-convert/-/color-convert-1.9.3.tgz#bb71850690e1f136567de629d2d5471deda4c1e8" integrity sha512-QfAUtd+vFdAtFQcC8CCyYt1fYWxSqAiK2cSD6zDB8N3cpsEBAvRxp9zOGg6G/SHHJYAT88/az/IuDGALsNVbGg== @@ -1767,16 +1781,45 @@ color-name@1.1.3: resolved "https://registry.yarnpkg.com/color-name/-/color-name-1.1.3.tgz#a7d0558bd89c42f795dd42328f740831ca53bc25" integrity sha1-p9BVi9icQveV3UIyj3QIMcpTvCU= -color-name@~1.1.4: +color-name@^1.0.0, color-name@~1.1.4: version "1.1.4" resolved "https://registry.yarnpkg.com/color-name/-/color-name-1.1.4.tgz#c2a09a87acbde69543de6f63fa3995c826c536a2" integrity sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA== +color-string@^1.6.0: + version "1.9.0" + resolved "https://registry.yarnpkg.com/color-string/-/color-string-1.9.0.tgz#63b6ebd1bec11999d1df3a79a7569451ac2be8aa" + integrity sha512-9Mrz2AQLefkH1UvASKj6v6hj/7eWgjnT/cVsR8CumieLoT+g900exWeNogqtweI8dxloXN9BDQTYro1oWu/5CQ== + dependencies: + color-name "^1.0.0" + simple-swizzle "^0.2.2" + color-support@^1.1.2: version "1.1.3" resolved "https://registry.yarnpkg.com/color-support/-/color-support-1.1.3.tgz#93834379a1cc9a0c61f82f52f0d04322251bd5a2" integrity sha512-qiBjkpbMLO/HL68y+lh4q0/O1MZFj2RX6X/KmMa3+gJD3z+WwI1ZzDHysvqHGS3mP6mznPckpXmw1nI9cJjyRg== +color@^3.1.3: + version "3.2.1" + resolved "https://registry.yarnpkg.com/color/-/color-3.2.1.tgz#3544dc198caf4490c3ecc9a790b54fe9ff45e164" + integrity sha512-aBl7dZI9ENN6fUGC7mWpMTPNHmWUSNan9tuWN6ahh5ZLNk9baLJOnSMlrQkHcrfFgz2/RigjUVAjdx36VcemKA== + dependencies: + color-convert "^1.9.3" + color-string "^1.6.0" + +colors@^1.2.1: + version "1.4.0" + resolved "https://registry.yarnpkg.com/colors/-/colors-1.4.0.tgz#c50491479d4c1bdaed2c9ced32cf7c7dc2360f78" + integrity sha512-a+UqTh4kgZg/SlGvfbzDHpgRu7AAQOmmqRHJnxhRZICKFUT91brVhNNt58CMWU9PsBbv3PDCZUHbVxuDiH2mtA== + +colorspace@1.1.x: + version "1.1.4" + resolved "https://registry.yarnpkg.com/colorspace/-/colorspace-1.1.4.tgz#8d442d1186152f60453bf8070cd66eb364e59243" + integrity sha512-BgvKJiuVu1igBUF2kEjRCZXol6wiiGbY5ipL/oVPwm0BL9sIpMIzM8IK7vwuxIIzOXMV3Ey5w+vxhm0rR/TN8w== + dependencies: + color "^3.1.3" + text-hex "1.0.x" + combined-stream@^1.0.8: version "1.0.8" resolved "https://registry.yarnpkg.com/combined-stream/-/combined-stream-1.0.8.tgz#c3d45a8b34fd730631a110a8a2520682b31d5a7f" @@ -2135,6 +2178,11 @@ emojis-list@^3.0.0: resolved "https://registry.yarnpkg.com/emojis-list/-/emojis-list-3.0.0.tgz#5570662046ad29e2e916e71aae260abdff4f6a78" integrity sha512-/kyM18EfinwXZbno9FyUGeFh87KC8HRQBQGildHZbEuRyWFOmv1U10o9BBp8XVZDVNNuQKyIGIu5ZYAAXJ0V2Q== +enabled@2.0.x: + version "2.0.0" + resolved "https://registry.yarnpkg.com/enabled/-/enabled-2.0.0.tgz#f9dd92ec2d6f4bbc0d5d1e64e21d61cd4665e7c2" + integrity sha512-AKrN98kuwOzMIdAizXGI86UFBoo26CL21UM763y1h/GMSJ4/OHU9k2YlsmBpyScFo/wbLzWQJBMCW4+IO3/+OQ== + encodeurl@~1.0.2: version "1.0.2" resolved "https://registry.yarnpkg.com/encodeurl/-/encodeurl-1.0.2.tgz#ad3ff4c86ec2d029322f5a02c3a9a606c95b3f59" @@ -2724,6 +2772,11 @@ fb-watchman@^2.0.0: dependencies: bser "2.1.1" +fecha@^4.2.0: + version "4.2.1" + resolved "https://registry.yarnpkg.com/fecha/-/fecha-4.2.1.tgz#0a83ad8f86ef62a091e22bb5a039cd03d23eecce" + integrity sha512-MMMQ0ludy/nBs1/o0zVOiKTpG7qMbonKUzjJgQFEuvq6INZ1OraKPRAWkBq5vlKLOUMpmNYG1JoN3oDPUQ9m3Q== + figures@^2.0.0: version "2.0.0" resolved "https://registry.yarnpkg.com/figures/-/figures-2.0.0.tgz#3ab1a2d2a62c8bfb431a0c94cb797a2fce27c962" @@ -2822,6 +2875,11 @@ flatted@^2.0.0: resolved "https://registry.yarnpkg.com/flatted/-/flatted-2.0.2.tgz#4575b21e2bcee7434aa9be662f4b7b5f9c2b5138" integrity sha512-r5wGx7YeOwNWNlCA0wQ86zKyDLMQr+/RB8xy74M4hTphfmjlijTSSXGuH8rnvKZnfT9i+75zmd8jcKdMR4O6jA== +fn.name@1.x.x: + version "1.1.0" + resolved "https://registry.yarnpkg.com/fn.name/-/fn.name-1.1.0.tgz#26cad8017967aea8731bc42961d04a3d5988accc" + integrity sha512-GRnmB5gPyJpAhTQdSZTSp9uaPSvl09KoYcMQtsB9rQoOmzs9dH6ffeccH+Z+cv6P68Hu5bC6JjRh4Ah/mHSNRw== + follow-redirects@^1.14.4: version "1.14.5" resolved "https://registry.yarnpkg.com/follow-redirects/-/follow-redirects-1.14.5.tgz#f09a5848981d3c772b5392309778523f8d85c381" @@ -3270,6 +3328,11 @@ is-arrayish@^0.2.1: resolved "https://registry.yarnpkg.com/is-arrayish/-/is-arrayish-0.2.1.tgz#77c99840527aa8ecb1a8ba697b80645a7a926a9d" integrity sha1-d8mYQFJ6qOyxqLppe4BkWnqSap0= +is-arrayish@^0.3.1: + version "0.3.2" + resolved "https://registry.yarnpkg.com/is-arrayish/-/is-arrayish-0.3.2.tgz#4574a2ae56f7ab206896fb431eaeed066fdf8f03" + integrity sha512-eVRqCvVlZbuw3GrM63ovNSNAeA1K16kaR/LRY/92w0zxQ5/1YzwblUX652i4Xs9RwAGjW9d9y6X88t8OaAJfWQ== + is-bigint@^1.0.1: version "1.0.4" resolved "https://registry.yarnpkg.com/is-bigint/-/is-bigint-1.0.4.tgz#08147a1875bc2b32005d41ccd8291dffc6691df3" @@ -4102,6 +4165,11 @@ kleur@^3.0.3: resolved "https://registry.yarnpkg.com/kleur/-/kleur-3.0.3.tgz#a79c9ecc86ee1ce3fa6206d1216c501f147fc07e" integrity sha512-eTIzlVOSUR+JxdDFepEYcBMtZ9Qqdef+rnzWdRZuMbOywu5tO2w2N7rqjoANZ5k9vywhL6Br1VRjUIgTQx4E8w== +kuler@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/kuler/-/kuler-2.0.0.tgz#e2c570a3800388fb44407e851531c1d670b061b3" + integrity sha512-Xq9nH7KlWZmXAtodXDDRE7vs6DU1gTU8zYDHDiWLSip45Egwq3plLHzPn27NgvzL2r1LMPC1vdqh98sQxtqj4A== + language-subtag-registry@~0.3.2: version "0.3.21" resolved "https://registry.yarnpkg.com/language-subtag-registry/-/language-subtag-registry-0.3.21.tgz#04ac218bea46f04cb039084602c6da9e788dd45a" @@ -4226,6 +4294,17 @@ lodash@4.x, lodash@^4.17.11, lodash@^4.17.12, lodash@^4.17.14, lodash@^4.17.15, resolved "https://registry.yarnpkg.com/lodash/-/lodash-4.17.21.tgz#679591c564c3bffaae8454cf0b3df370c3d6911c" integrity sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg== +logform@^2.2.0: + version "2.3.0" + resolved "https://registry.yarnpkg.com/logform/-/logform-2.3.0.tgz#a3997a05985de2ebd325ae0d166dffc9c6fe6b57" + integrity sha512-graeoWUH2knKbGthMtuG1EfaSPMZFZBIrhuJHhkS5ZseFBrc7DupCzihOQAzsK/qIKPQaPJ/lFQFctILUY5ARQ== + dependencies: + colors "^1.2.1" + fecha "^4.2.0" + ms "^2.1.1" + safe-stable-stringify "^1.1.0" + triple-beam "^1.3.0" + lolex@^4.2.0: version "4.2.0" resolved "https://registry.yarnpkg.com/lolex/-/lolex-4.2.0.tgz#ddbd7f6213ca1ea5826901ab1222b65d714b3cd7" @@ -4820,6 +4899,13 @@ once@^1.3.0, once@^1.3.1, once@^1.4.0: dependencies: wrappy "1" +one-time@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/one-time/-/one-time-1.0.0.tgz#e06bc174aed214ed58edede573b433bbf827cb45" + integrity sha512-5DXOiRKwuSEcQ/l0kGCF6Q3jcADFv5tSmRaJck/OqkVFcOzutB134KRSfF0xDrL39MNnqxbHBbUUcjZIhTgb2g== + dependencies: + fn.name "1.x.x" + onetime@^2.0.0: version "2.0.1" resolved "https://registry.yarnpkg.com/onetime/-/onetime-2.0.1.tgz#067428230fd67443b2794b22bba528b6867962d4" @@ -5340,7 +5426,7 @@ readable-stream@^2.3.5: string_decoder "~1.1.1" util-deprecate "~1.0.1" -readable-stream@^3.6.0: +readable-stream@^3.4.0, readable-stream@^3.6.0: version "3.6.0" resolved "https://registry.yarnpkg.com/readable-stream/-/readable-stream-3.6.0.tgz#337bbda3adc0706bd3e024426a286d4b4b2c9198" integrity sha512-BViHy7LKeTz4oNnkcLJ+lVSL6vpiFeX6/d3oSH8zCW7UxP2onchk+vTGB143xuFjHS3deTgkKoXXymXqymiIdA== @@ -5526,6 +5612,11 @@ safe-regex@^1.1.0: dependencies: ret "~0.1.10" +safe-stable-stringify@^1.1.0: + version "1.1.1" + resolved "https://registry.yarnpkg.com/safe-stable-stringify/-/safe-stable-stringify-1.1.1.tgz#c8a220ab525cd94e60ebf47ddc404d610dc5d84a" + integrity sha512-ERq4hUjKDbJfE4+XtZLFPCDi8Vb1JqaxAPTxWFLBx8XcAlf9Bda/ZJdVezs/NAfsMQScyIlUMx+Yeu7P7rx5jw== + "safer-buffer@>= 2.1.2 < 3": version "2.1.2" resolved "https://registry.yarnpkg.com/safer-buffer/-/safer-buffer-2.1.2.tgz#44fa161b0187b9549dd84bb91802f9bd8385cd6a" @@ -5688,6 +5779,13 @@ signal-exit@^3.0.0, signal-exit@^3.0.2: resolved "https://registry.yarnpkg.com/signal-exit/-/signal-exit-3.0.5.tgz#9e3e8cc0c75a99472b44321033a7702e7738252f" integrity sha512-KWcOiKeQj6ZyXx7zq4YxSMgHRlod4czeBQZrPb8OKcohcqAXShm7E20kEMle9WBt26hFcAf0qLOcp5zmY7kOqQ== +simple-swizzle@^0.2.2: + version "0.2.2" + resolved "https://registry.yarnpkg.com/simple-swizzle/-/simple-swizzle-0.2.2.tgz#a4da6b635ffcccca33f70d17cb92592de95e557a" + integrity sha1-pNprY1/8zMoz9w0Xy5JZLeleVXo= + dependencies: + is-arrayish "^0.3.1" + sinon@^7.5.0: version "7.5.0" resolved "https://registry.yarnpkg.com/sinon/-/sinon-7.5.0.tgz#e9488ea466070ea908fd44a3d6478fd4923c67ec" @@ -5839,6 +5937,11 @@ sprintf-js@~1.0.2: resolved "https://registry.yarnpkg.com/sprintf-js/-/sprintf-js-1.0.3.tgz#04e6926f662895354f3dd015203633b857297e2c" integrity sha1-BOaSb2YolTVPPdAVIDYzuFcpfiw= +stack-trace@0.0.x: + version "0.0.10" + resolved "https://registry.yarnpkg.com/stack-trace/-/stack-trace-0.0.10.tgz#547c70b347e8d32b4e108ea1a2a159e5fdde19c0" + integrity sha1-VHxws0fo0ytOEI6hoqFZ5f3eGcA= + stack-utils@^2.0.2: version "2.0.5" resolved "https://registry.yarnpkg.com/stack-utils/-/stack-utils-2.0.5.tgz#d25265fca995154659dbbfba3b49254778d2fdd5" @@ -6074,6 +6177,11 @@ test-exclude@^6.0.0: glob "^7.1.4" minimatch "^3.0.4" +text-hex@1.0.x: + version "1.0.0" + resolved "https://registry.yarnpkg.com/text-hex/-/text-hex-1.0.0.tgz#69dc9c1b17446ee79a92bf5b884bb4b9127506f5" + integrity sha512-uuVGNWzgJ4yhRaNSiubPY7OjISw4sw4E5Uv0wbjp+OzcbmVU/rsT8ujgcXJhn9ypzsgr5vlzpPqP+MBBKcGvbg== + text-table@^0.2.0: version "0.2.0" resolved "https://registry.yarnpkg.com/text-table/-/text-table-0.2.0.tgz#7f5ee823ae805207c00af2df4a84ec3fcfa570b4" @@ -6164,6 +6272,11 @@ tr46@~0.0.3: resolved "https://registry.yarnpkg.com/tr46/-/tr46-0.0.3.tgz#8184fd347dac9cdc185992f3a6622e14b9d9ab6a" integrity sha1-gYT9NH2snNwYWZLzpmIuFLnZq2o= +triple-beam@^1.2.0, triple-beam@^1.3.0: + version "1.3.0" + resolved "https://registry.yarnpkg.com/triple-beam/-/triple-beam-1.3.0.tgz#a595214c7298db8339eeeee083e4d10bd8cb8dd9" + integrity sha512-XrHUvV5HpdLmIj4uVMxHggLbFSZYIn7HEWsqePZcI50pco+MPqJ50wMGY794X7AOOhxOBAjbkqfAbEe/QMp2Lw== + ts-jest@^26.5.1: version "26.5.6" resolved "https://registry.yarnpkg.com/ts-jest/-/ts-jest-26.5.6.tgz#c32e0746425274e1dfe333f43cd3c800e014ec35" @@ -6458,6 +6571,30 @@ wide-align@^1.1.2: dependencies: string-width "^1.0.2 || 2 || 3 || 4" +winston-transport@^4.4.0: + version "4.4.1" + resolved "https://registry.yarnpkg.com/winston-transport/-/winston-transport-4.4.1.tgz#42a830e07363719c13c12bd2fb87a226f692dc75" + integrity sha512-ciZRlU4CSjHqHe8RQG1iPxKMRVwv6ZJ0RC7DxStKWd0KjpAhPDy5gVYSCpIUq+5CUsP+IyNOTZy1X0tO2QZqjg== + dependencies: + logform "^2.2.0" + readable-stream "^3.4.0" + triple-beam "^1.2.0" + +winston@^3.3.3: + version "3.3.3" + resolved "https://registry.yarnpkg.com/winston/-/winston-3.3.3.tgz#ae6172042cafb29786afa3d09c8ff833ab7c9170" + integrity sha512-oEXTISQnC8VlSAKf1KYSSd7J6IWuRPQqDdo8eoRNaYKLvwSb5+79Z3Yi1lrl6KDpU6/VWaxpakDAtb1oQ4n9aw== + dependencies: + "@dabh/diagnostics" "^2.0.2" + async "^3.1.0" + is-stream "^2.0.0" + logform "^2.2.0" + one-time "^1.0.0" + readable-stream "^3.4.0" + stack-trace "0.0.x" + triple-beam "^1.3.0" + winston-transport "^4.4.0" + word-wrap@~1.2.3: version "1.2.3" resolved "https://registry.yarnpkg.com/word-wrap/-/word-wrap-1.2.3.tgz#610636f6b1f703891bd34771ccb17fb93b47079c"