@terreno/api 0.13.2 → 0.14.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/__tests__/versionCheckPlugin.test.js +53 -3
- package/dist/api.arrayOperations.test.js +1 -0
- package/dist/api.asyncHandler.test.d.ts +1 -0
- package/dist/api.asyncHandler.test.js +236 -0
- package/dist/api.d.ts +15 -4
- package/dist/api.errors.test.js +1 -0
- package/dist/api.hooks.test.js +1 -0
- package/dist/api.js +153 -104
- package/dist/api.query.test.js +1 -0
- package/dist/api.test.js +174 -0
- package/dist/auth.d.ts +10 -5
- package/dist/auth.js +163 -90
- package/dist/auth.test.js +159 -0
- package/dist/betterAuthApp.test.js +1 -0
- package/dist/betterAuthSetup.d.ts +5 -6
- package/dist/betterAuthSetup.js +17 -14
- package/dist/betterAuthSetup.test.js +1 -0
- package/dist/config.d.ts +48 -0
- package/dist/config.js +248 -0
- package/dist/config.test.d.ts +1 -0
- package/dist/config.test.js +328 -0
- package/dist/configuration.test.js +1 -0
- package/dist/configurationApp.d.ts +1 -1
- package/dist/configurationApp.js +17 -13
- package/dist/configurationPlugin.test.js +1 -0
- package/dist/consentApp.test.js +1 -0
- package/dist/envConfigurationPlugin.d.ts +2 -0
- package/dist/envConfigurationPlugin.js +173 -0
- package/dist/envConfigurationPlugin.test.d.ts +1 -0
- package/dist/envConfigurationPlugin.test.js +322 -0
- package/dist/errors.d.ts +18 -7
- package/dist/errors.js +106 -10
- package/dist/errors.test.js +16 -1
- package/dist/example.js +16 -7
- package/dist/expressServer.d.ts +10 -9
- package/dist/expressServer.js +62 -53
- package/dist/expressServer.test.js +53 -2
- package/dist/githubAuth.d.ts +2 -1
- package/dist/githubAuth.js +41 -26
- package/dist/githubAuth.test.js +1 -0
- package/dist/index.d.ts +4 -0
- package/dist/index.js +4 -0
- package/dist/logger.d.ts +1 -1
- package/dist/logger.js +42 -20
- package/dist/models/versionConfig.d.ts +2 -0
- package/dist/models/versionConfig.js +8 -0
- package/dist/notifiers/googleChatNotifier.js +14 -16
- package/dist/notifiers/googleChatNotifier.test.js +1 -0
- package/dist/notifiers/slackNotifier.js +16 -14
- package/dist/notifiers/slackNotifier.test.js +41 -3
- package/dist/notifiers/zoomNotifier.js +7 -10
- package/dist/notifiers/zoomNotifier.test.js +1 -0
- package/dist/openApi.d.ts +1 -1
- package/dist/openApi.test.js +1 -0
- package/dist/openApiBuilder.d.ts +39 -6
- package/dist/openApiBuilder.js +1 -31
- package/dist/openApiBuilder.test.js +1 -0
- package/dist/openApiValidator.js +1 -0
- package/dist/openApiValidator.test.js +65 -0
- package/dist/permissions.d.ts +4 -4
- package/dist/permissions.js +67 -65
- package/dist/permissions.middleware.test.js +1 -0
- package/dist/permissions.test.js +1 -0
- package/dist/plugins.d.ts +5 -5
- package/dist/plugins.js +18 -9
- package/dist/plugins.test.js +1 -1
- package/dist/populate.d.ts +15 -8
- package/dist/populate.js +23 -24
- package/dist/populate.test.js +1 -0
- package/dist/realtime/changeStreamWatcher.d.ts +73 -0
- package/dist/realtime/changeStreamWatcher.js +720 -0
- package/dist/realtime/index.d.ts +6 -0
- package/dist/realtime/index.js +27 -0
- package/dist/realtime/queryMatcher.d.ts +14 -0
- package/dist/realtime/queryMatcher.js +250 -0
- package/dist/realtime/queryStore.d.ts +37 -0
- package/dist/realtime/queryStore.js +195 -0
- package/dist/realtime/realtime.test.d.ts +10 -0
- package/dist/realtime/realtime.test.js +2158 -0
- package/dist/realtime/realtimeApp.d.ts +93 -0
- package/dist/realtime/realtimeApp.js +560 -0
- package/dist/realtime/registry.d.ts +40 -0
- package/dist/realtime/registry.js +38 -0
- package/dist/realtime/socketUser.d.ts +10 -0
- package/dist/realtime/socketUser.js +17 -0
- package/dist/realtime/types.d.ts +100 -0
- package/dist/realtime/types.js +2 -0
- package/dist/requestContext.d.ts +37 -0
- package/dist/requestContext.js +344 -0
- package/dist/requestContext.test.d.ts +1 -0
- package/dist/requestContext.test.js +241 -0
- package/dist/terrenoApp.d.ts +8 -0
- package/dist/terrenoApp.js +50 -13
- package/dist/terrenoApp.test.js +194 -21
- package/dist/terrenoPlugin.d.ts +11 -0
- package/dist/tests/bunSetup.js +1 -0
- package/dist/tests.js +1 -1
- package/dist/transformers.d.ts +2 -2
- package/dist/transformers.js +5 -3
- package/dist/transformers.test.js +90 -0
- package/dist/types/consentResponse.d.ts +6 -3
- package/dist/versionCheckPlugin.d.ts +2 -0
- package/dist/versionCheckPlugin.js +18 -12
- package/package.json +4 -2
- package/src/__tests__/versionCheckPlugin.test.ts +37 -3
- package/src/api.arrayOperations.test.ts +1 -0
- package/src/api.asyncHandler.test.ts +177 -0
- package/src/api.errors.test.ts +1 -0
- package/src/api.hooks.test.ts +1 -0
- package/src/api.query.test.ts +1 -0
- package/src/api.test.ts +132 -0
- package/src/api.ts +199 -84
- package/src/auth.test.ts +160 -0
- package/src/auth.ts +120 -50
- package/src/betterAuthApp.test.ts +1 -0
- package/src/betterAuthSetup.test.ts +1 -0
- package/src/betterAuthSetup.ts +46 -19
- package/src/config.test.ts +255 -0
- package/src/config.ts +206 -0
- package/src/configuration.test.ts +1 -0
- package/src/configurationApp.ts +59 -24
- package/src/configurationPlugin.test.ts +1 -0
- package/src/consentApp.test.ts +1 -0
- package/src/envConfigurationPlugin.test.ts +143 -0
- package/src/envConfigurationPlugin.ts +100 -0
- package/src/errors.test.ts +19 -1
- package/src/errors.ts +94 -20
- package/src/example.ts +46 -21
- package/src/express.d.ts +18 -1
- package/src/expressServer.test.ts +50 -2
- package/src/expressServer.ts +80 -50
- package/src/githubAuth.test.ts +1 -0
- package/src/githubAuth.ts +59 -38
- package/src/index.ts +4 -0
- package/src/logger.ts +47 -17
- package/src/models/versionConfig.ts +13 -2
- package/src/notifiers/googleChatNotifier.test.ts +1 -0
- package/src/notifiers/googleChatNotifier.ts +7 -9
- package/src/notifiers/slackNotifier.test.ts +29 -3
- package/src/notifiers/slackNotifier.ts +9 -7
- package/src/notifiers/zoomNotifier.test.ts +1 -0
- package/src/notifiers/zoomNotifier.ts +8 -11
- package/src/openApi.test.ts +1 -0
- package/src/openApi.ts +4 -4
- package/src/openApiBuilder.test.ts +1 -0
- package/src/openApiBuilder.ts +14 -11
- package/src/openApiValidator.test.ts +59 -0
- package/src/openApiValidator.ts +3 -2
- package/src/permissions.middleware.test.ts +1 -0
- package/src/permissions.test.ts +1 -0
- package/src/permissions.ts +30 -25
- package/src/plugins.test.ts +1 -1
- package/src/plugins.ts +21 -14
- package/src/populate.test.ts +1 -0
- package/src/populate.ts +44 -36
- package/src/realtime/changeStreamWatcher.ts +568 -0
- package/src/realtime/index.ts +34 -0
- package/src/realtime/queryMatcher.ts +179 -0
- package/src/realtime/queryStore.ts +132 -0
- package/src/realtime/realtime.test.ts +1755 -0
- package/src/realtime/realtimeApp.ts +478 -0
- package/src/realtime/registry.ts +64 -0
- package/src/realtime/socketUser.ts +25 -0
- package/src/realtime/types.ts +112 -0
- package/src/requestContext.test.ts +196 -0
- package/src/requestContext.ts +368 -0
- package/src/terrenoApp.test.ts +137 -11
- package/src/terrenoApp.ts +64 -17
- package/src/terrenoPlugin.ts +12 -0
- package/src/tests/bunSetup.ts +1 -0
- package/src/tests.ts +7 -2
- package/src/transformers.test.ts +70 -2
- package/src/transformers.ts +15 -7
- package/src/types/consentResponse.ts +8 -10
- package/src/versionCheckPlugin.ts +15 -7
package/src/example.ts
CHANGED
|
@@ -3,11 +3,17 @@ import mongoose, {model, Schema} from "mongoose";
|
|
|
3
3
|
import passportLocalMongoose from "passport-local-mongoose";
|
|
4
4
|
|
|
5
5
|
import {type ModelRouterOptions, modelRouter} from "./api";
|
|
6
|
-
import {addAuthRoutes, setupAuth} from "./auth";
|
|
6
|
+
import {addAuthRoutes, setupAuth, type UserModel as UserMongooseModel} from "./auth";
|
|
7
7
|
import {setupServer} from "./expressServer";
|
|
8
8
|
import {logger} from "./logger";
|
|
9
9
|
import {Permissions} from "./permissions";
|
|
10
|
-
import {
|
|
10
|
+
import {
|
|
11
|
+
baseUserPlugin,
|
|
12
|
+
createdUpdatedPlugin,
|
|
13
|
+
findExactlyOne,
|
|
14
|
+
findOneOrNone,
|
|
15
|
+
isDeletedPlugin,
|
|
16
|
+
} from "./plugins";
|
|
11
17
|
|
|
12
18
|
mongoose
|
|
13
19
|
.connect("mongodb://localhost:27017/example")
|
|
@@ -31,27 +37,43 @@ interface Food {
|
|
|
31
37
|
hidden?: boolean;
|
|
32
38
|
}
|
|
33
39
|
|
|
34
|
-
const userSchema = new Schema<User>(
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
}
|
|
40
|
+
const userSchema = new Schema<User>(
|
|
41
|
+
{
|
|
42
|
+
admin: {default: false, description: "Whether the user has admin privileges", type: Boolean},
|
|
43
|
+
username: {description: "The user's username", type: String},
|
|
44
|
+
},
|
|
45
|
+
{strict: "throw", toJSON: {virtuals: true}, toObject: {virtuals: true}}
|
|
46
|
+
);
|
|
38
47
|
|
|
48
|
+
// biome-ignore lint/suspicious/noExplicitAny: passport-local-mongoose's plugin type is incompatible with mongoose Schema generics
|
|
39
49
|
userSchema.plugin(passportLocalMongoose as any, {usernameField: "email"});
|
|
40
50
|
userSchema.plugin(createdUpdatedPlugin);
|
|
41
51
|
userSchema.plugin(baseUserPlugin);
|
|
42
52
|
const UserModel = model<User>("User", userSchema);
|
|
43
53
|
|
|
44
|
-
const schema = new Schema<Food>(
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
54
|
+
const schema = new Schema<Food>(
|
|
55
|
+
{
|
|
56
|
+
calories: {description: "Number of calories in the food", type: Number},
|
|
57
|
+
created: {description: "When this food was created", type: Date},
|
|
58
|
+
hidden: {
|
|
59
|
+
default: false,
|
|
60
|
+
description: "Whether this food is hidden from listings",
|
|
61
|
+
type: Boolean,
|
|
62
|
+
},
|
|
63
|
+
name: {description: "The name of the food", type: String},
|
|
64
|
+
ownerId: {description: "The user who owns this food entry", ref: "User", type: "ObjectId"},
|
|
65
|
+
},
|
|
66
|
+
{strict: "throw", toJSON: {virtuals: true}, toObject: {virtuals: true}}
|
|
67
|
+
);
|
|
68
|
+
|
|
69
|
+
schema.plugin(createdUpdatedPlugin);
|
|
70
|
+
schema.plugin(isDeletedPlugin);
|
|
71
|
+
schema.plugin(findOneOrNone);
|
|
72
|
+
schema.plugin(findExactlyOne);
|
|
51
73
|
|
|
52
74
|
const FoodModel = model<Food>("Food", schema);
|
|
53
75
|
|
|
54
|
-
|
|
76
|
+
const getBaseServer = () => {
|
|
55
77
|
const app = express();
|
|
56
78
|
|
|
57
79
|
app.use((req, res, next) => {
|
|
@@ -65,14 +87,17 @@ function getBaseServer() {
|
|
|
65
87
|
}
|
|
66
88
|
});
|
|
67
89
|
app.use(express.json());
|
|
68
|
-
setupAuth(app, UserModel as
|
|
69
|
-
addAuthRoutes(app, UserModel as
|
|
90
|
+
setupAuth(app, UserModel as unknown as UserMongooseModel);
|
|
91
|
+
addAuthRoutes(app, UserModel as unknown as UserMongooseModel);
|
|
70
92
|
|
|
71
|
-
|
|
93
|
+
const addRoutes = (
|
|
94
|
+
router: express.Router,
|
|
95
|
+
options?: Partial<ModelRouterOptions<unknown>>
|
|
96
|
+
): void => {
|
|
72
97
|
router.use(
|
|
73
98
|
"/food",
|
|
74
99
|
modelRouter(FoodModel, {
|
|
75
|
-
...options,
|
|
100
|
+
...(options as Partial<ModelRouterOptions<Food>>),
|
|
76
101
|
openApiOverwrite: {
|
|
77
102
|
get: {responses: {200: {description: "Get all the food"}}},
|
|
78
103
|
},
|
|
@@ -86,14 +111,14 @@ function getBaseServer() {
|
|
|
86
111
|
queryFields: ["name", "calories", "created", "ownerId", "hidden"],
|
|
87
112
|
})
|
|
88
113
|
);
|
|
89
|
-
}
|
|
114
|
+
};
|
|
90
115
|
|
|
91
116
|
return setupServer({
|
|
92
117
|
addRoutes,
|
|
93
118
|
loggingOptions: {
|
|
94
119
|
level: "debug",
|
|
95
120
|
},
|
|
96
|
-
userModel: UserModel as
|
|
121
|
+
userModel: UserModel as unknown as UserMongooseModel,
|
|
97
122
|
});
|
|
98
|
-
}
|
|
123
|
+
};
|
|
99
124
|
getBaseServer();
|
package/src/express.d.ts
CHANGED
|
@@ -1,5 +1,22 @@
|
|
|
1
1
|
declare namespace Express {
|
|
2
2
|
export interface Request {
|
|
3
|
-
|
|
3
|
+
authTokenPayload?: {
|
|
4
|
+
sid?: string;
|
|
5
|
+
sessionId?: string;
|
|
6
|
+
[key: string]: unknown;
|
|
7
|
+
};
|
|
8
|
+
jobId?: string;
|
|
9
|
+
requestId?: string;
|
|
10
|
+
sessionId?: string;
|
|
11
|
+
user?: {
|
|
12
|
+
_id: string | ObjectId;
|
|
13
|
+
id: string;
|
|
14
|
+
admin: boolean;
|
|
15
|
+
disabled?: boolean;
|
|
16
|
+
type?: string;
|
|
17
|
+
testUser?: boolean;
|
|
18
|
+
email?: string;
|
|
19
|
+
[key: string]: unknown;
|
|
20
|
+
};
|
|
4
21
|
}
|
|
5
22
|
}
|
|
@@ -1,6 +1,9 @@
|
|
|
1
|
+
// biome-ignore-all lint/suspicious/noExplicitAny: test mock typing
|
|
1
2
|
import {afterEach, beforeEach, describe, expect, it, mock} from "bun:test";
|
|
3
|
+
import {Writable} from "node:stream";
|
|
2
4
|
import express from "express";
|
|
3
5
|
import supertest from "supertest";
|
|
6
|
+
import winston from "winston";
|
|
4
7
|
|
|
5
8
|
import {
|
|
6
9
|
createRouter,
|
|
@@ -11,6 +14,7 @@ import {
|
|
|
11
14
|
setupServer,
|
|
12
15
|
wrapScript,
|
|
13
16
|
} from "./expressServer";
|
|
17
|
+
import {logger, winstonLogger} from "./logger";
|
|
14
18
|
import {UserModel} from "./tests";
|
|
15
19
|
|
|
16
20
|
describe("expressServer", () => {
|
|
@@ -58,6 +62,52 @@ describe("expressServer", () => {
|
|
|
58
62
|
});
|
|
59
63
|
|
|
60
64
|
describe("logRequests", () => {
|
|
65
|
+
it("attaches request and session context to route logs", async () => {
|
|
66
|
+
const logs: string[] = [];
|
|
67
|
+
const logStream = new Writable({
|
|
68
|
+
write(chunk, _encoding, callback) {
|
|
69
|
+
logs.push(chunk.toString());
|
|
70
|
+
callback();
|
|
71
|
+
},
|
|
72
|
+
});
|
|
73
|
+
const transport = new winston.transports.Stream({
|
|
74
|
+
format: winston.format.json(),
|
|
75
|
+
stream: logStream,
|
|
76
|
+
});
|
|
77
|
+
|
|
78
|
+
const app = setupServer({
|
|
79
|
+
addRoutes: (router) => {
|
|
80
|
+
router.get("/context-test", (req, res) => {
|
|
81
|
+
logger.info("context route log");
|
|
82
|
+
return res.json({requestId: req.requestId, sessionId: req.sessionId});
|
|
83
|
+
});
|
|
84
|
+
},
|
|
85
|
+
logRequests: false,
|
|
86
|
+
skipListen: true,
|
|
87
|
+
userModel: UserModel as any,
|
|
88
|
+
});
|
|
89
|
+
winstonLogger.add(transport);
|
|
90
|
+
|
|
91
|
+
const res = await supertest(app)
|
|
92
|
+
.get("/context-test")
|
|
93
|
+
.set("X-Request-ID", "req-123")
|
|
94
|
+
.set("X-Session-ID", "session-123")
|
|
95
|
+
.expect(200);
|
|
96
|
+
|
|
97
|
+
expect(res.headers["x-request-id"]).toBe("req-123");
|
|
98
|
+
expect(res.headers["x-session-id"]).toBe("session-123");
|
|
99
|
+
expect(res.body).toEqual({requestId: "req-123", sessionId: "session-123"});
|
|
100
|
+
|
|
101
|
+
const parsedLog = logs
|
|
102
|
+
.map((entry) => JSON.parse(entry))
|
|
103
|
+
.find((entry) => entry.message === "context route log");
|
|
104
|
+
expect(parsedLog).toBeDefined();
|
|
105
|
+
expect(parsedLog.requestId).toBe("req-123");
|
|
106
|
+
expect(parsedLog.sessionId).toBe("session-123");
|
|
107
|
+
|
|
108
|
+
winstonLogger.remove(transport);
|
|
109
|
+
});
|
|
110
|
+
|
|
61
111
|
it("logs request with admin user type", () => {
|
|
62
112
|
const req = {
|
|
63
113
|
body: {},
|
|
@@ -653,7 +703,6 @@ describe("expressServer", () => {
|
|
|
653
703
|
const timerIds: ReturnType<typeof setTimeout>[] = [];
|
|
654
704
|
|
|
655
705
|
beforeEach(() => {
|
|
656
|
-
// biome-ignore lint/suspicious/noExplicitAny: Mock requires type override for process.exit.
|
|
657
706
|
process.exit = mock(() => {
|
|
658
707
|
throw new Error("process.exit called");
|
|
659
708
|
}) as unknown as typeof process.exit;
|
|
@@ -750,7 +799,6 @@ describe("expressServer", () => {
|
|
|
750
799
|
setupServer({
|
|
751
800
|
addRoutes,
|
|
752
801
|
skipListen: true,
|
|
753
|
-
// biome-ignore lint/suspicious/noExplicitAny: Test mock for UserModel.
|
|
754
802
|
userModel: UserModel as any,
|
|
755
803
|
})
|
|
756
804
|
).toThrow("route initialization failed");
|
package/src/expressServer.ts
CHANGED
|
@@ -9,19 +9,28 @@ import passport from "passport";
|
|
|
9
9
|
import qs from "qs";
|
|
10
10
|
import type {ModelRouterOptions} from "./api";
|
|
11
11
|
import {addAuthRoutes, addMeRoutes, setupAuth, type UserModel as UserMongooseModel} from "./auth";
|
|
12
|
-
import {
|
|
12
|
+
import {
|
|
13
|
+
apiErrorMiddleware,
|
|
14
|
+
apiFallthroughErrorMiddleware,
|
|
15
|
+
apiUnauthorizedMiddleware,
|
|
16
|
+
} from "./errors";
|
|
13
17
|
import {addGitHubAuthRoutes, type GitHubAuthOptions, setupGitHubAuth} from "./githubAuth";
|
|
14
18
|
import {type LoggingOptions, logger, setupLogging} from "./logger";
|
|
15
19
|
import {sendToSlack} from "./notifiers";
|
|
16
20
|
import {openApiCompatMiddleware, patchAppUse} from "./openApiCompat";
|
|
17
21
|
import {openApiEtagMiddleware} from "./openApiEtag";
|
|
22
|
+
import {
|
|
23
|
+
getCurrentRequestContext,
|
|
24
|
+
requestContextMiddleware,
|
|
25
|
+
updateRequestContextFromRequest,
|
|
26
|
+
} from "./requestContext";
|
|
18
27
|
import openapi from "./vendor/wesleytodd-openapi/index";
|
|
19
28
|
|
|
20
29
|
const SLOW_READ_MAX = 200;
|
|
21
30
|
const SLOW_WRITE_MAX = 500;
|
|
22
31
|
const IS_JEST = process.env.JEST_WORKER_ID !== undefined;
|
|
23
32
|
|
|
24
|
-
export
|
|
33
|
+
export const setupEnvironment = (): void => {
|
|
25
34
|
if (!process.env.TOKEN_ISSUER) {
|
|
26
35
|
throw new Error("TOKEN_ISSUER must be set in env.");
|
|
27
36
|
}
|
|
@@ -40,12 +49,13 @@ export function setupEnvironment(): void {
|
|
|
40
49
|
if (!process.env.REFRESH_TOKEN_EXPIRES_IN && !IS_JEST) {
|
|
41
50
|
logger.warn("REFRESH_TOKEN_EXPIRES_IN not set so using default.");
|
|
42
51
|
}
|
|
43
|
-
}
|
|
52
|
+
};
|
|
44
53
|
|
|
45
54
|
export type AddRoutes = (router: Router, options?: Partial<ModelRouterOptions<unknown>>) => void;
|
|
46
55
|
|
|
56
|
+
// biome-ignore lint/suspicious/noExplicitAny: also called from tests with mock request/response objects
|
|
47
57
|
const logRequestsFinished = (req: any, res: any, startTime: bigint) => {
|
|
48
|
-
const options = (res.locals
|
|
58
|
+
const options = (res.locals?.loggingOptions ?? {}) as LoggingOptions;
|
|
49
59
|
|
|
50
60
|
const slowReadMs = options.logSlowRequestsReadMs ?? SLOW_READ_MAX;
|
|
51
61
|
const slowWriteMs = options.logSlowRequestsWriteMs ?? SLOW_WRITE_MAX;
|
|
@@ -84,7 +94,8 @@ const logRequestsFinished = (req: any, res: any, startTime: bigint) => {
|
|
|
84
94
|
}
|
|
85
95
|
};
|
|
86
96
|
|
|
87
|
-
|
|
97
|
+
// biome-ignore lint/suspicious/noExplicitAny: also called from tests with mock request/response objects
|
|
98
|
+
export const logRequests = (req: any, res: any, next: express.NextFunction): void => {
|
|
88
99
|
const startTime = process.hrtime.bigint();
|
|
89
100
|
|
|
90
101
|
let userString = "";
|
|
@@ -114,37 +125,48 @@ export function logRequests(req: any, res: any, next: any) {
|
|
|
114
125
|
}
|
|
115
126
|
onFinished(res, () => logRequestsFinished(req, res, startTime));
|
|
116
127
|
next();
|
|
117
|
-
}
|
|
128
|
+
};
|
|
118
129
|
|
|
119
|
-
export
|
|
120
|
-
|
|
130
|
+
export const createRouter = (
|
|
131
|
+
rootPath: string,
|
|
132
|
+
addRoutes: AddRoutes,
|
|
133
|
+
middleware: express.RequestHandler[] = []
|
|
134
|
+
): Array<string | express.RequestHandler | Router> => {
|
|
135
|
+
const routePathMiddleware = (
|
|
136
|
+
req: express.Request & {routeMount?: string[]},
|
|
137
|
+
_res: express.Response,
|
|
138
|
+
next: express.NextFunction
|
|
139
|
+
): void => {
|
|
121
140
|
if (!req.routeMount) {
|
|
122
141
|
req.routeMount = [];
|
|
123
142
|
}
|
|
124
143
|
req.routeMount.push(rootPath);
|
|
125
144
|
next();
|
|
126
|
-
}
|
|
145
|
+
};
|
|
127
146
|
|
|
128
147
|
const router = express.Router();
|
|
129
148
|
router.use(routePathMiddleware);
|
|
130
149
|
addRoutes(router);
|
|
131
150
|
return [rootPath, ...middleware, router];
|
|
132
|
-
}
|
|
151
|
+
};
|
|
133
152
|
|
|
134
|
-
export
|
|
153
|
+
export const createRouterWithAuth = (
|
|
135
154
|
rootPath: string,
|
|
136
155
|
addRoutes: (router: Router) => void,
|
|
137
|
-
middleware:
|
|
138
|
-
) {
|
|
156
|
+
middleware: express.RequestHandler[] = []
|
|
157
|
+
): Array<string | express.RequestHandler | Router> => {
|
|
139
158
|
return createRouter(rootPath, addRoutes, [
|
|
140
159
|
passport.authenticate("firebase-jwt", {session: false}),
|
|
141
160
|
...middleware,
|
|
142
161
|
]);
|
|
143
|
-
}
|
|
162
|
+
};
|
|
144
163
|
|
|
145
164
|
export interface AuthOptions {
|
|
146
|
-
|
|
165
|
+
// biome-ignore lint/suspicious/noExplicitAny: user shape is provided by the consumer's User model — any preserves the loose-binding contract
|
|
166
|
+
generateJWTPayload?: (user: any) => Record<string, unknown>;
|
|
167
|
+
// biome-ignore lint/suspicious/noExplicitAny: see above
|
|
147
168
|
generateTokenExpiration?: (user: any) => number | jwt.SignOptions["expiresIn"];
|
|
169
|
+
// biome-ignore lint/suspicious/noExplicitAny: see above
|
|
148
170
|
generateRefreshTokenExpiration?: (user: any) => number | jwt.SignOptions["expiresIn"];
|
|
149
171
|
}
|
|
150
172
|
|
|
@@ -173,19 +195,20 @@ interface InitializeRoutesOptions {
|
|
|
173
195
|
githubAuth?: GitHubAuthOptions;
|
|
174
196
|
}
|
|
175
197
|
|
|
176
|
-
|
|
198
|
+
const initializeRoutes = (
|
|
177
199
|
UserModel: UserMongooseModel,
|
|
178
200
|
addRoutes: AddRoutes,
|
|
179
201
|
options: InitializeRoutesOptions = {}
|
|
180
|
-
): express.Application {
|
|
202
|
+
): express.Application => {
|
|
181
203
|
const app = express();
|
|
182
204
|
|
|
183
205
|
// Record mount paths on layers for Express 5 → OpenAPI compat
|
|
184
206
|
patchAppUse(app);
|
|
185
207
|
|
|
186
|
-
// TODO: Log a warning when we hit the array limit.
|
|
187
208
|
app.set("query parser", (str: string) => qs.parse(str, {arrayLimit: options.arrayLimit ?? 200}));
|
|
188
209
|
|
|
210
|
+
app.use(requestContextMiddleware);
|
|
211
|
+
|
|
189
212
|
app.use(
|
|
190
213
|
cors({
|
|
191
214
|
origin: options.corsOrigin ?? "*",
|
|
@@ -199,8 +222,12 @@ function initializeRoutes(
|
|
|
199
222
|
app.use(express.json({limit: "50mb"}));
|
|
200
223
|
|
|
201
224
|
// Add login/signup/refresh_token before the JWT/auth middlewares
|
|
202
|
-
addAuthRoutes(app, UserModel
|
|
203
|
-
setupAuth(app
|
|
225
|
+
addAuthRoutes(app, UserModel, options?.authOptions);
|
|
226
|
+
setupAuth(app, UserModel);
|
|
227
|
+
app.use((req, res, next) => {
|
|
228
|
+
updateRequestContextFromRequest(req, res);
|
|
229
|
+
next();
|
|
230
|
+
});
|
|
204
231
|
|
|
205
232
|
if (options.logRequests !== false) {
|
|
206
233
|
app.use(logRequests);
|
|
@@ -213,9 +240,13 @@ function initializeRoutes(
|
|
|
213
240
|
});
|
|
214
241
|
|
|
215
242
|
// Add Sentry scopes for session, transaction, and userId if any are set
|
|
216
|
-
app.use((req:
|
|
243
|
+
app.use((req: express.Request, _res: express.Response, next: express.NextFunction) => {
|
|
244
|
+
const context = getCurrentRequestContext();
|
|
217
245
|
const transactionId = req.header("X-Transaction-ID");
|
|
218
|
-
const sessionId = req.header("X-Session-ID");
|
|
246
|
+
const sessionId = context?.sessionId ?? req.header("X-Session-ID");
|
|
247
|
+
if (context?.requestId) {
|
|
248
|
+
Sentry.getCurrentScope().setTag("request_id", context.requestId);
|
|
249
|
+
}
|
|
219
250
|
if (transactionId) {
|
|
220
251
|
Sentry.getCurrentScope().setTag("transaction_id", transactionId);
|
|
221
252
|
}
|
|
@@ -223,7 +254,7 @@ function initializeRoutes(
|
|
|
223
254
|
Sentry.getCurrentScope().setTag("session_id", sessionId);
|
|
224
255
|
}
|
|
225
256
|
if (req.user?._id) {
|
|
226
|
-
Sentry.getCurrentScope().setTag("user", req.user._id);
|
|
257
|
+
Sentry.getCurrentScope().setTag("user", String(req.user._id));
|
|
227
258
|
}
|
|
228
259
|
next();
|
|
229
260
|
});
|
|
@@ -246,12 +277,12 @@ function initializeRoutes(
|
|
|
246
277
|
app.use("/swagger", oapi.swaggerui());
|
|
247
278
|
}
|
|
248
279
|
|
|
249
|
-
addMeRoutes(app, UserModel
|
|
280
|
+
addMeRoutes(app, UserModel, options?.authOptions);
|
|
250
281
|
|
|
251
282
|
// Set up GitHub OAuth if configured (works with JWT auth)
|
|
252
283
|
if (options.githubAuth) {
|
|
253
|
-
setupGitHubAuth(app, UserModel
|
|
254
|
-
addGitHubAuthRoutes(app, UserModel
|
|
284
|
+
setupGitHubAuth(app, UserModel, options.githubAuth);
|
|
285
|
+
addGitHubAuthRoutes(app, UserModel, options.githubAuth, options.authOptions);
|
|
255
286
|
}
|
|
256
287
|
|
|
257
288
|
addRoutes(app, {openApi: oapi});
|
|
@@ -262,20 +293,17 @@ function initializeRoutes(
|
|
|
262
293
|
app.use(apiUnauthorizedMiddleware);
|
|
263
294
|
app.use(apiErrorMiddleware);
|
|
264
295
|
|
|
265
|
-
app.use(
|
|
266
|
-
logger.error(`Fallthrough error: ${err}${err?.stack ? `\n${err.stack}` : ""}}`);
|
|
267
|
-
Sentry.captureException(err);
|
|
268
|
-
res.statusCode = 500;
|
|
269
|
-
res.end(`${res.sentry}\n`);
|
|
270
|
-
});
|
|
296
|
+
app.use(apiFallthroughErrorMiddleware);
|
|
271
297
|
|
|
272
298
|
return app;
|
|
273
|
-
}
|
|
299
|
+
};
|
|
274
300
|
|
|
275
301
|
export interface SetupServerOptions {
|
|
276
302
|
userModel: UserMongooseModel;
|
|
277
303
|
addRoutes: AddRoutes;
|
|
278
304
|
loggingOptions?: LoggingOptions;
|
|
305
|
+
// Whether requests should be logged. Defaults to true.
|
|
306
|
+
logRequests?: boolean;
|
|
279
307
|
authOptions?: AuthOptions;
|
|
280
308
|
/**
|
|
281
309
|
* GitHub OAuth configuration. When provided, enables GitHub authentication.
|
|
@@ -300,8 +328,7 @@ export interface SetupServerOptions {
|
|
|
300
328
|
sentryOptions?: Sentry.BunOptions;
|
|
301
329
|
}
|
|
302
330
|
|
|
303
|
-
|
|
304
|
-
export function setupServer(options: SetupServerOptions): express.Application {
|
|
331
|
+
export const setupServer = (options: SetupServerOptions): express.Application => {
|
|
305
332
|
const UserModel = options.userModel;
|
|
306
333
|
const addRoutes = options.addRoutes;
|
|
307
334
|
|
|
@@ -314,9 +341,12 @@ export function setupServer(options: SetupServerOptions): express.Application {
|
|
|
314
341
|
authOptions: options.authOptions,
|
|
315
342
|
corsOrigin: options.corsOrigin,
|
|
316
343
|
githubAuth: options.githubAuth,
|
|
344
|
+
loggingOptions: options.loggingOptions,
|
|
345
|
+
logRequests: options.logRequests,
|
|
317
346
|
});
|
|
318
|
-
} catch (error:
|
|
319
|
-
|
|
347
|
+
} catch (error: unknown) {
|
|
348
|
+
const stack = error instanceof Error && error.stack ? error.stack : String(error);
|
|
349
|
+
logger.error(`Error initializing routes: ${stack}`);
|
|
320
350
|
throw error;
|
|
321
351
|
}
|
|
322
352
|
|
|
@@ -327,19 +357,19 @@ export function setupServer(options: SetupServerOptions): express.Application {
|
|
|
327
357
|
logger.info(`Listening on port ${port}`);
|
|
328
358
|
});
|
|
329
359
|
} catch (error) {
|
|
330
|
-
|
|
360
|
+
const stack = error instanceof Error ? error.stack : String(error);
|
|
361
|
+
logger.error(`Error trying to start HTTP server: ${error}\n${stack}`);
|
|
331
362
|
process.exit(1);
|
|
332
363
|
}
|
|
333
364
|
}
|
|
334
365
|
return app;
|
|
335
|
-
}
|
|
366
|
+
};
|
|
336
367
|
|
|
337
|
-
|
|
338
|
-
export function cronjob(
|
|
368
|
+
export const cronjob = (
|
|
339
369
|
name: string,
|
|
340
370
|
schedule: "hourly" | "minutely" | string,
|
|
341
371
|
callback: () => void
|
|
342
|
-
) {
|
|
372
|
+
): void => {
|
|
343
373
|
const cronSchedule =
|
|
344
374
|
schedule === "hourly" ? "0 * * * *" : schedule === "minutely" ? "* * * * *" : schedule;
|
|
345
375
|
logger.info(`Adding cronjob ${name}, running at: ${cronSchedule}`);
|
|
@@ -348,16 +378,17 @@ export function cronjob(
|
|
|
348
378
|
} catch (error) {
|
|
349
379
|
throw new Error(`Failed to create cronjob: ${error}`);
|
|
350
380
|
}
|
|
351
|
-
}
|
|
381
|
+
};
|
|
352
382
|
|
|
353
383
|
export interface WrapScriptOptions {
|
|
354
|
-
onFinish?: (result?:
|
|
384
|
+
onFinish?: (result?: unknown) => void | Promise<void>;
|
|
355
385
|
terminateTimeout?: number; // in seconds, defaults to 300. Set to 0 to have no termination timeout.
|
|
356
386
|
slackChannel?: string;
|
|
357
387
|
}
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
388
|
+
export const wrapScript = async (
|
|
389
|
+
func: () => Promise<unknown>,
|
|
390
|
+
options: WrapScriptOptions = {}
|
|
391
|
+
): Promise<void> => {
|
|
361
392
|
const name = require.main?.filename.split("/").slice(-1)[0].replace(".ts", "");
|
|
362
393
|
logger.info(`Running script ${name}`);
|
|
363
394
|
await sendToSlack(`Running script ${name}`, {
|
|
@@ -383,7 +414,7 @@ export async function wrapScript(func: () => Promise<any>, options: WrapScriptOp
|
|
|
383
414
|
}, closeTime);
|
|
384
415
|
}
|
|
385
416
|
|
|
386
|
-
let result:
|
|
417
|
+
let result: unknown;
|
|
387
418
|
try {
|
|
388
419
|
result = await func();
|
|
389
420
|
if (options.onFinish) {
|
|
@@ -397,6 +428,5 @@ export async function wrapScript(func: () => Promise<any>, options: WrapScriptOp
|
|
|
397
428
|
process.exit(1);
|
|
398
429
|
}
|
|
399
430
|
await sendToSlack(`Success running script ${name}: ${result}`);
|
|
400
|
-
// Unclear why we have to exit here to prevent the script for continuing to run.
|
|
401
431
|
process.exit(0);
|
|
402
|
-
}
|
|
432
|
+
};
|
package/src/githubAuth.test.ts
CHANGED