From 452caee9dc25a09d0de21e534cf024ee4e803601 Mon Sep 17 00:00:00 2001 From: Josh Gachnang Date: Sat, 25 Dec 2021 09:51:45 -0600 Subject: [PATCH] setupServer fix, add script wrapper (#23) * Fix setupServer to return the express app, listen immediately * Add script wrapper * Add postCreate to user model to allow more sign up data * Fixes for scripts, add every minute to cron --- src/expressServer.ts | 60 ++++++++++++++++++++++++++++++- src/mongooseRestFramework.test.ts | 22 ++++++++++-- src/mongooseRestFramework.ts | 13 +++++-- 3 files changed, 89 insertions(+), 6 deletions(-) diff --git a/src/expressServer.ts b/src/expressServer.ts index d3f59de..b9747d8 100644 --- a/src/expressServer.ts +++ b/src/expressServer.ts @@ -173,9 +173,15 @@ export function setupServer(UserModel: UserModel, addRoutes: AddRoutes) { } // Convenince method to execute cronjobs with an always-running server. -export function cronjob(name: string, schedule: "hourly" | string, callback: () => void) { +export function cronjob( + name: string, + schedule: "hourly" | "minutely" | string, + callback: () => void +) { if (schedule === "hourly") { schedule = "0 * * * *"; + } else if (schedule === "minutely") { + schedule = "* * * * *"; } console.info(`Adding cronjob ${name}, running at: ${schedule}`); try { @@ -205,3 +211,55 @@ export async function sendToSlack(text: string, channel = "bots") { console.error("Error posting to slack", (e as any).text); } } + +export interface WrapScriptOptions { + onFinish?: (result?: any) => void | Promise; + terminateTimeout?: number; // in seconds, defaults to 300. Set to 0 to have no termination timeout. + slackChannel?: string; +} +// Wrap up a script with some helpers, such as catching errors, reporting them to sentry, notifying +// Slack of runs, etc. Also supports timeouts. +export async function wrapScript(func: () => Promise, options: WrapScriptOptions = {}) { + const name = require.main?.filename + .split("/") + .slice(-1)[0] + .replace(".ts", ""); + console.log(`Running script ${name}`); + sendToSlack(`Running script ${name}`, options.slackChannel); + + if (options.terminateTimeout !== 0) { + const warnTime = ((options.terminateTimeout ?? 300) / 2) * 1000; + const closeTime = (options.terminateTimeout ?? 300) * 1000; + setTimeout(() => { + const msg = `Script ${name} is taking a while, currently ${warnTime / 1000} seconds`; + sendToSlack(msg); + console.warn(msg); + }, warnTime); + + setTimeout(async () => { + const msg = `Script ${name} took too long, exiting`; + await sendToSlack(msg); + console.error(msg); + Sentry.captureException(new Error(`Script ${name} took too long, exiting`)); + await Sentry.flush(); + process.exit(2); + }, closeTime); + } + + let result: any; + try { + result = await func(); + if (options.onFinish) { + await options.onFinish(result); + } + } catch (e) { + Sentry.captureException(e); + console.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); + } + await sendToSlack(`Success running script ${name}: ${result}`); + // Unclear why we have to exit here to prevent the script for continuing to run. + process.exit(0); +} diff --git a/src/mongooseRestFramework.test.ts b/src/mongooseRestFramework.test.ts index 248c169..4604a4e 100644 --- a/src/mongooseRestFramework.test.ts +++ b/src/mongooseRestFramework.test.ts @@ -20,6 +20,7 @@ interface User { admin: boolean; username: string; email: string; + age?: number; } interface Food { @@ -33,11 +34,16 @@ interface Food { const userSchema = new Schema({ username: String, admin: {type: Boolean, default: false}, + age: Number, }); userSchema.plugin(passportLocalMongoose, {usernameField: "email"}); userSchema.plugin(tokenPlugin); userSchema.plugin(createdDeletedPlugin); +userSchema.methods.postCreate = async function(body: any) { + this.age = body.age; + return this.save(); +}; const UserModel = model("User", userSchema); @@ -522,7 +528,6 @@ describe("mongoose rest framework", () => { .post("/auth/login") .send({email: "notAdmin@example.com", password: "password"}) .expect(200); - console.log("RES", res.body); const foodRes = await agent.get("/food"); const carrots = foodRes.body.data.find((food: Food) => food.name === "Carrots"); const carrotRes = await agent @@ -791,7 +796,6 @@ describe("test token auth", function() { .get("/auth/me") .set("authorization", `Bearer ${token}`) .expect(200); - console.log("ME RES", meRes.body.data); assert.isDefined(meRes.body.data._id); assert.isDefined(meRes.body.data.id); assert.isUndefined(meRes.body.data.hash); @@ -832,6 +836,19 @@ describe("test token auth", function() { assert.equal(updateRes.body.data.name, "PeasAndCarrots"); }); + it("signup with extra data", async function() { + const res = await server + .post("/auth/signup") + .send({email: "new@example.com", password: "123", age: 25}) + .expect(200); + const {userId, token} = res.body.data; + assert.isDefined(userId); + assert.isDefined(token); + + const user = await UserModel.findOne({email: "new@example.com"}); + assert.equal(user?.age, 25); + }); + it("completes token login e2e", async function() { const res = await server .post("/auth/login") @@ -845,7 +862,6 @@ describe("test token auth", function() { .get("/auth/me") .set("authorization", `Bearer ${token}`) .expect(200); - console.log("ME RES", meRes.body.data); assert.isDefined(meRes.body.data._id); assert.isDefined(meRes.body.data.id); assert.isUndefined(meRes.body.data.hash); diff --git a/src/mongooseRestFramework.ts b/src/mongooseRestFramework.ts index 9dd027b..2f33ef9 100644 --- a/src/mongooseRestFramework.ts +++ b/src/mongooseRestFramework.ts @@ -55,6 +55,9 @@ export interface UserModel extends Model { deserializeUser(): any; createAnonymousUser?: (id?: string) => Promise; isValidPassword: (password: string) => boolean; + // Allows additional setup during signup. This will be passed the rest of req.body from the signup + // request. + postCreate?: (body: any) => Promise; } type PermissionMethod = (method: RESTMethod, user?: User, obj?: T) => boolean; @@ -261,11 +264,17 @@ export function setupAuth(app: express.Application, userModel: UserModel) { { usernameField: "email", passwordField: "password", + passReqToCallback: true, }, - async (email, password, done) => { + async (req, email, password, done) => { try { const user = await (userModel as any).register({email}, password); - await (user as any).setPassword(password); + if (user.postCreate) { + const body = req.body; + delete body.email; + delete body.password; + await user.postCreate(body); + } await user.save(); if (!user.token) { throw new Error("Token not created");