@terreno/api 0.13.3 → 0.14.1
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 +136 -3
- package/dist/api.arrayOperations.test.js +1 -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 +30 -17
- package/dist/betterAuthSetup.test.js +1 -0
- package/dist/config.d.ts +48 -0
- package/dist/config.js +257 -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 +111 -12
- package/dist/errors.test.js +16 -1
- package/dist/example.js +19 -7
- package/dist/expressServer.d.ts +10 -9
- package/dist/expressServer.js +62 -53
- package/dist/expressServer.test.js +165 -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 +1 -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 +724 -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 +3066 -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 +384 -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 +94 -3
- package/src/api.arrayOperations.test.ts +1 -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 +59 -22
- package/src/config.test.ts +255 -0
- package/src/config.ts +216 -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 +118 -38
- package/src/example.ts +49 -21
- package/src/express.d.ts +18 -1
- package/src/expressServer.test.ts +147 -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 +1 -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 +572 -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 +2465 -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 +321 -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/errors.ts
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
// https://jsonapi.org/format/#errors
|
|
2
2
|
import * as Sentry from "@sentry/bun";
|
|
3
3
|
import type {NextFunction, Request, Response} from "express";
|
|
4
|
-
import {Schema} from "mongoose";
|
|
4
|
+
import mongoose, {Schema} from "mongoose";
|
|
5
5
|
|
|
6
6
|
import {logger} from "./logger";
|
|
7
7
|
|
|
@@ -42,7 +42,7 @@ export interface APIErrorConstructor {
|
|
|
42
42
|
};
|
|
43
43
|
// A meta object containing non-standard meta-information about the error.
|
|
44
44
|
meta?: {[id: string]: string};
|
|
45
|
-
error?:
|
|
45
|
+
error?: unknown;
|
|
46
46
|
// If true, this error will not be sent to external error reporting tools like Sentry.
|
|
47
47
|
disableExternalErrorTracking?: boolean;
|
|
48
48
|
}
|
|
@@ -82,19 +82,17 @@ export class APIError extends Error {
|
|
|
82
82
|
}
|
|
83
83
|
| undefined;
|
|
84
84
|
|
|
85
|
-
meta: {[id: string]:
|
|
85
|
+
meta: {[id: string]: unknown} | undefined;
|
|
86
86
|
|
|
87
|
-
error?:
|
|
87
|
+
error?: unknown;
|
|
88
88
|
|
|
89
89
|
disableExternalErrorTracking?: boolean;
|
|
90
90
|
|
|
91
91
|
constructor(data: APIErrorConstructor) {
|
|
92
|
+
const errorStack =
|
|
93
|
+
data.error instanceof Error && data.error.stack ? `\n${data.error.stack}` : "";
|
|
92
94
|
// Include details in when the error is printed to the console or sent to Sentry.
|
|
93
|
-
super(
|
|
94
|
-
`${data.title}${data.detail ? `: ${data.detail}` : ""}${
|
|
95
|
-
data.error ? `\n${data.error.stack}` : ""
|
|
96
|
-
}`
|
|
97
|
-
);
|
|
95
|
+
super(`${data.title}${data.detail ? `: ${data.detail}` : ""}${errorStack}`);
|
|
98
96
|
this.name = "APIError";
|
|
99
97
|
|
|
100
98
|
let {title, id, links, status, code, detail, source, meta, fields, error} = data;
|
|
@@ -120,9 +118,9 @@ export class APIError extends Error {
|
|
|
120
118
|
this.meta.fields = fields;
|
|
121
119
|
}
|
|
122
120
|
this.error = error;
|
|
123
|
-
const
|
|
124
|
-
data.error
|
|
125
|
-
}`;
|
|
121
|
+
const dataErrorStack =
|
|
122
|
+
data.error instanceof Error && data.error.stack ? `\n${data.error.stack}` : "";
|
|
123
|
+
const logMessage = `APIError(${status}): ${title} ${detail ? detail : ""}${dataErrorStack}`;
|
|
126
124
|
if (data.disableExternalErrorTracking) {
|
|
127
125
|
logger.warn(logMessage);
|
|
128
126
|
} else {
|
|
@@ -138,32 +136,54 @@ export class APIError extends Error {
|
|
|
138
136
|
// Create an errors field for storing error information in a JSONAPI compatible form directly on a
|
|
139
137
|
// model.
|
|
140
138
|
export const errorsPlugin = (schema: Schema): void => {
|
|
141
|
-
const errorSchema = new Schema(
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
type: String,
|
|
139
|
+
const errorSchema = new Schema(
|
|
140
|
+
{
|
|
141
|
+
code: {description: "Application-specific error code", type: String},
|
|
142
|
+
detail: {description: "Human-readable explanation of the error", type: String},
|
|
143
|
+
id: {description: "Unique identifier for this error occurrence", type: String},
|
|
144
|
+
links: {
|
|
145
|
+
about: {description: "Link to documentation about this error", type: String},
|
|
146
|
+
type: {description: "Link describing the error type", type: String},
|
|
147
|
+
},
|
|
148
|
+
meta: {
|
|
149
|
+
description: "Non-standard meta information about the error",
|
|
150
|
+
type: Schema.Types.Mixed,
|
|
151
|
+
},
|
|
152
|
+
source: {
|
|
153
|
+
header: {description: "HTTP header that caused the error", type: String},
|
|
154
|
+
parameter: {description: "Query parameter that caused the error", type: String},
|
|
155
|
+
pointer: {
|
|
156
|
+
description: "JSON pointer to the request field that caused the error",
|
|
157
|
+
type: String,
|
|
158
|
+
},
|
|
156
159
|
},
|
|
160
|
+
status: {description: "HTTP status code for this error", type: Number},
|
|
161
|
+
title: {description: "Short summary of the error", required: true, type: String},
|
|
157
162
|
},
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
});
|
|
163
|
+
{_id: false, strict: "throw"}
|
|
164
|
+
);
|
|
161
165
|
|
|
162
166
|
schema.add({apiErrors: errorSchema});
|
|
163
167
|
};
|
|
164
168
|
|
|
165
|
-
export const isAPIError = (error:
|
|
166
|
-
return error.name === "APIError";
|
|
169
|
+
export const isAPIError = (error: unknown): error is APIError => {
|
|
170
|
+
return error instanceof Error && error.name === "APIError";
|
|
171
|
+
};
|
|
172
|
+
|
|
173
|
+
/** Extract a human-readable message from an unknown error. */
|
|
174
|
+
export const errorMessage = (error: unknown): string => {
|
|
175
|
+
if (error instanceof Error) {
|
|
176
|
+
return error.message;
|
|
177
|
+
}
|
|
178
|
+
return String(error);
|
|
179
|
+
};
|
|
180
|
+
|
|
181
|
+
/** Extract a stack trace string from an unknown error. */
|
|
182
|
+
export const errorStack = (error: unknown): string => {
|
|
183
|
+
if (error instanceof Error && error.stack) {
|
|
184
|
+
return error.stack;
|
|
185
|
+
}
|
|
186
|
+
return String(error);
|
|
167
187
|
};
|
|
168
188
|
|
|
169
189
|
/**
|
|
@@ -186,8 +206,9 @@ export const getDisableExternalErrorTracking = (error: unknown): boolean | undef
|
|
|
186
206
|
// Creates an APIError body to send to clients as JSON. Errors don't have a toJSON defined,
|
|
187
207
|
// and we want to strip out things like message, name, and stack for the client.
|
|
188
208
|
// There is almost certainly a more elegant solution to this.
|
|
189
|
-
export const getAPIErrorBody = (error: APIError):
|
|
190
|
-
const errorData = {status: error.status, title: error.title};
|
|
209
|
+
export const getAPIErrorBody = (error: APIError): Record<string, unknown> => {
|
|
210
|
+
const errorData: Record<string, unknown> = {status: error.status, title: error.title};
|
|
211
|
+
const indexable = error as unknown as Record<string, unknown>;
|
|
191
212
|
for (const key of [
|
|
192
213
|
"id",
|
|
193
214
|
"links",
|
|
@@ -198,8 +219,8 @@ export const getAPIErrorBody = (error: APIError): {[id: string]: any} => {
|
|
|
198
219
|
"meta",
|
|
199
220
|
"disableExternalErrorTracking",
|
|
200
221
|
]) {
|
|
201
|
-
if (
|
|
202
|
-
errorData[key] =
|
|
222
|
+
if (indexable[key]) {
|
|
223
|
+
errorData[key] = indexable[key];
|
|
203
224
|
}
|
|
204
225
|
}
|
|
205
226
|
return errorData;
|
|
@@ -219,6 +240,40 @@ export const apiUnauthorizedMiddleware = (
|
|
|
219
240
|
}
|
|
220
241
|
};
|
|
221
242
|
|
|
243
|
+
/**
|
|
244
|
+
* Converts Mongoose validation/cast errors into client-friendly APIErrors.
|
|
245
|
+
*/
|
|
246
|
+
export const mongooseErrorToAPIError = (err: Error): APIError | null => {
|
|
247
|
+
if (err instanceof mongoose.Error.ValidationError) {
|
|
248
|
+
const fields: {[id: string]: string} = {};
|
|
249
|
+
for (const [path, subErr] of Object.entries(err.errors)) {
|
|
250
|
+
fields[path] = subErr.message;
|
|
251
|
+
}
|
|
252
|
+
return new APIError({
|
|
253
|
+
detail: err.message,
|
|
254
|
+
disableExternalErrorTracking: true,
|
|
255
|
+
fields,
|
|
256
|
+
status: 400,
|
|
257
|
+
title: "Validation failed",
|
|
258
|
+
});
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
if (err instanceof mongoose.Error.CastError) {
|
|
262
|
+
const path = err.path ?? "field";
|
|
263
|
+
return new APIError({
|
|
264
|
+
detail: `Invalid value for ${path}`,
|
|
265
|
+
disableExternalErrorTracking: true,
|
|
266
|
+
fields: {
|
|
267
|
+
[path]: `Expected ${err.kind ?? "a valid value"}, got ${JSON.stringify(err.value)}`,
|
|
268
|
+
},
|
|
269
|
+
status: 400,
|
|
270
|
+
title: "Validation failed",
|
|
271
|
+
});
|
|
272
|
+
}
|
|
273
|
+
|
|
274
|
+
return null;
|
|
275
|
+
};
|
|
276
|
+
|
|
222
277
|
export const apiErrorMiddleware = (
|
|
223
278
|
err: Error,
|
|
224
279
|
_req: Request,
|
|
@@ -230,7 +285,32 @@ export const apiErrorMiddleware = (
|
|
|
230
285
|
Sentry.captureException(err);
|
|
231
286
|
}
|
|
232
287
|
res.status(err.status).json(getAPIErrorBody(err)).send();
|
|
233
|
-
|
|
234
|
-
|
|
288
|
+
return;
|
|
289
|
+
}
|
|
290
|
+
|
|
291
|
+
const mongooseError = mongooseErrorToAPIError(err);
|
|
292
|
+
if (mongooseError) {
|
|
293
|
+
res.status(mongooseError.status).json(getAPIErrorBody(mongooseError)).send();
|
|
294
|
+
return;
|
|
295
|
+
}
|
|
296
|
+
|
|
297
|
+
next(err);
|
|
298
|
+
};
|
|
299
|
+
|
|
300
|
+
/**
|
|
301
|
+
* Final Express error handler for unexpected errors. Always returns JSON so
|
|
302
|
+
* clients (e.g. RTK Query) can parse the response.
|
|
303
|
+
*/
|
|
304
|
+
export const apiFallthroughErrorMiddleware = (
|
|
305
|
+
err: Error,
|
|
306
|
+
_req: Request,
|
|
307
|
+
res: Response,
|
|
308
|
+
_next: NextFunction
|
|
309
|
+
): void => {
|
|
310
|
+
logger.error(`Fallthrough error: ${err}${err.stack ? `\n${err.stack}` : ""}`);
|
|
311
|
+
Sentry.captureException(err);
|
|
312
|
+
if (res.headersSent) {
|
|
313
|
+
return;
|
|
235
314
|
}
|
|
315
|
+
res.status(500).json({status: 500, title: "Internal server error"}).send();
|
|
236
316
|
};
|
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,46 @@ 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);
|
|
51
|
+
userSchema.plugin(isDeletedPlugin);
|
|
52
|
+
userSchema.plugin(findOneOrNone);
|
|
53
|
+
userSchema.plugin(findExactlyOne);
|
|
41
54
|
userSchema.plugin(baseUserPlugin);
|
|
42
55
|
const UserModel = model<User>("User", userSchema);
|
|
43
56
|
|
|
44
|
-
const schema = new Schema<Food>(
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
57
|
+
const schema = new Schema<Food>(
|
|
58
|
+
{
|
|
59
|
+
calories: {description: "Number of calories in the food", type: Number},
|
|
60
|
+
created: {description: "When this food was created", type: Date},
|
|
61
|
+
hidden: {
|
|
62
|
+
default: false,
|
|
63
|
+
description: "Whether this food is hidden from listings",
|
|
64
|
+
type: Boolean,
|
|
65
|
+
},
|
|
66
|
+
name: {description: "The name of the food", type: String},
|
|
67
|
+
ownerId: {description: "The user who owns this food entry", ref: "User", type: "ObjectId"},
|
|
68
|
+
},
|
|
69
|
+
{strict: "throw", toJSON: {virtuals: true}, toObject: {virtuals: true}}
|
|
70
|
+
);
|
|
71
|
+
|
|
72
|
+
schema.plugin(createdUpdatedPlugin);
|
|
73
|
+
schema.plugin(isDeletedPlugin);
|
|
74
|
+
schema.plugin(findOneOrNone);
|
|
75
|
+
schema.plugin(findExactlyOne);
|
|
51
76
|
|
|
52
77
|
const FoodModel = model<Food>("Food", schema);
|
|
53
78
|
|
|
54
|
-
|
|
79
|
+
const getBaseServer = () => {
|
|
55
80
|
const app = express();
|
|
56
81
|
|
|
57
82
|
app.use((req, res, next) => {
|
|
@@ -65,14 +90,17 @@ function getBaseServer() {
|
|
|
65
90
|
}
|
|
66
91
|
});
|
|
67
92
|
app.use(express.json());
|
|
68
|
-
setupAuth(app, UserModel as
|
|
69
|
-
addAuthRoutes(app, UserModel as
|
|
93
|
+
setupAuth(app, UserModel as unknown as UserMongooseModel);
|
|
94
|
+
addAuthRoutes(app, UserModel as unknown as UserMongooseModel);
|
|
70
95
|
|
|
71
|
-
|
|
96
|
+
const addRoutes = (
|
|
97
|
+
router: express.Router,
|
|
98
|
+
options?: Partial<ModelRouterOptions<unknown>>
|
|
99
|
+
): void => {
|
|
72
100
|
router.use(
|
|
73
101
|
"/food",
|
|
74
102
|
modelRouter(FoodModel, {
|
|
75
|
-
...options,
|
|
103
|
+
...(options as Partial<ModelRouterOptions<Food>>),
|
|
76
104
|
openApiOverwrite: {
|
|
77
105
|
get: {responses: {200: {description: "Get all the food"}}},
|
|
78
106
|
},
|
|
@@ -86,14 +114,14 @@ function getBaseServer() {
|
|
|
86
114
|
queryFields: ["name", "calories", "created", "ownerId", "hidden"],
|
|
87
115
|
})
|
|
88
116
|
);
|
|
89
|
-
}
|
|
117
|
+
};
|
|
90
118
|
|
|
91
119
|
return setupServer({
|
|
92
120
|
addRoutes,
|
|
93
121
|
loggingOptions: {
|
|
94
122
|
level: "debug",
|
|
95
123
|
},
|
|
96
|
-
userModel: UserModel as
|
|
124
|
+
userModel: UserModel as unknown as UserMongooseModel,
|
|
97
125
|
});
|
|
98
|
-
}
|
|
126
|
+
};
|
|
99
127
|
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;
|
|
@@ -723,6 +772,103 @@ describe("expressServer", () => {
|
|
|
723
772
|
});
|
|
724
773
|
});
|
|
725
774
|
|
|
775
|
+
describe("setupServer with listen", () => {
|
|
776
|
+
const originalEnv = process.env;
|
|
777
|
+
const http = require("node:http");
|
|
778
|
+
let activeServer: any = null;
|
|
779
|
+
let originalListen: any = null;
|
|
780
|
+
|
|
781
|
+
beforeEach(() => {
|
|
782
|
+
process.env = {
|
|
783
|
+
...originalEnv,
|
|
784
|
+
PORT: "0",
|
|
785
|
+
REFRESH_TOKEN_SECRET: "test-refresh-secret",
|
|
786
|
+
SESSION_SECRET: "test-session-secret",
|
|
787
|
+
TOKEN_EXPIRES_IN: "1h",
|
|
788
|
+
TOKEN_ISSUER: "test-issuer",
|
|
789
|
+
TOKEN_SECRET: "test-secret",
|
|
790
|
+
};
|
|
791
|
+
|
|
792
|
+
originalListen = http.Server.prototype.listen;
|
|
793
|
+
http.Server.prototype.listen = function (...args: any[]) {
|
|
794
|
+
activeServer = this;
|
|
795
|
+
return originalListen.apply(this, args);
|
|
796
|
+
};
|
|
797
|
+
});
|
|
798
|
+
|
|
799
|
+
afterEach(async () => {
|
|
800
|
+
process.env = originalEnv;
|
|
801
|
+
http.Server.prototype.listen = originalListen;
|
|
802
|
+
if (activeServer) {
|
|
803
|
+
await new Promise<void>((resolve) => activeServer.close(() => resolve()));
|
|
804
|
+
activeServer = null;
|
|
805
|
+
}
|
|
806
|
+
});
|
|
807
|
+
|
|
808
|
+
it("starts listening on a port when skipListen is false", async () => {
|
|
809
|
+
const addRoutes = () => {};
|
|
810
|
+
|
|
811
|
+
const app = setupServer({
|
|
812
|
+
addRoutes,
|
|
813
|
+
skipListen: false,
|
|
814
|
+
userModel: UserModel as any,
|
|
815
|
+
});
|
|
816
|
+
|
|
817
|
+
expect(app).toBeDefined();
|
|
818
|
+
await new Promise((resolve) => setTimeout(resolve, 100));
|
|
819
|
+
});
|
|
820
|
+
});
|
|
821
|
+
|
|
822
|
+
describe("wrapScript timeout callbacks", () => {
|
|
823
|
+
const originalEnv = process.env;
|
|
824
|
+
const originalExit = process.exit;
|
|
825
|
+
const originalSetTimeout = globalThis.setTimeout;
|
|
826
|
+
const timerIds: ReturnType<typeof setTimeout>[] = [];
|
|
827
|
+
const timerCallbacks: Array<{callback: () => void; delay: number}> = [];
|
|
828
|
+
|
|
829
|
+
beforeEach(() => {
|
|
830
|
+
process.env = {
|
|
831
|
+
...process.env,
|
|
832
|
+
REFRESH_TOKEN_SECRET: "test-refresh-secret",
|
|
833
|
+
SESSION_SECRET: "test-session-secret",
|
|
834
|
+
TOKEN_EXPIRES_IN: "1h",
|
|
835
|
+
TOKEN_ISSUER: "test-issuer",
|
|
836
|
+
TOKEN_SECRET: "test-secret",
|
|
837
|
+
};
|
|
838
|
+
process.exit = mock(() => {
|
|
839
|
+
throw new Error("__EXIT__");
|
|
840
|
+
}) as unknown as typeof process.exit;
|
|
841
|
+
|
|
842
|
+
timerCallbacks.length = 0;
|
|
843
|
+
timerIds.length = 0;
|
|
844
|
+
globalThis.setTimeout = ((cb: () => void, delay: number) => {
|
|
845
|
+
timerCallbacks.push({callback: cb, delay});
|
|
846
|
+
const id = originalSetTimeout(cb, delay);
|
|
847
|
+
timerIds.push(id);
|
|
848
|
+
return id;
|
|
849
|
+
}) as typeof setTimeout;
|
|
850
|
+
});
|
|
851
|
+
|
|
852
|
+
afterEach(() => {
|
|
853
|
+
for (const id of timerIds) {
|
|
854
|
+
clearTimeout(id);
|
|
855
|
+
}
|
|
856
|
+
globalThis.setTimeout = originalSetTimeout;
|
|
857
|
+
process.exit = originalExit;
|
|
858
|
+
process.env = originalEnv;
|
|
859
|
+
});
|
|
860
|
+
|
|
861
|
+
it("registers warn and terminate timeouts with correct delays", async () => {
|
|
862
|
+
const func = async () => "ok";
|
|
863
|
+
await expect(wrapScript(func, {terminateTimeout: 100})).rejects.toThrow("__EXIT__");
|
|
864
|
+
|
|
865
|
+
const warnTimer = timerCallbacks.find((t) => t.delay === 50000);
|
|
866
|
+
const closeTimer = timerCallbacks.find((t) => t.delay === 100000);
|
|
867
|
+
expect(warnTimer).toBeDefined();
|
|
868
|
+
expect(closeTimer).toBeDefined();
|
|
869
|
+
});
|
|
870
|
+
});
|
|
871
|
+
|
|
726
872
|
describe("setupServer error handling", () => {
|
|
727
873
|
const originalEnv = process.env;
|
|
728
874
|
|
|
@@ -750,7 +896,6 @@ describe("expressServer", () => {
|
|
|
750
896
|
setupServer({
|
|
751
897
|
addRoutes,
|
|
752
898
|
skipListen: true,
|
|
753
|
-
// biome-ignore lint/suspicious/noExplicitAny: Test mock for UserModel.
|
|
754
899
|
userModel: UserModel as any,
|
|
755
900
|
})
|
|
756
901
|
).toThrow("route initialization failed");
|