@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/logger.ts
CHANGED
|
@@ -2,38 +2,62 @@ import fs from "node:fs";
|
|
|
2
2
|
import {inspect} from "node:util";
|
|
3
3
|
import * as Sentry from "@sentry/bun";
|
|
4
4
|
import winston from "winston";
|
|
5
|
+
import {getCurrentLogContext} from "./requestContext";
|
|
5
6
|
|
|
6
|
-
|
|
7
|
+
const isPrimitive = (val: unknown) => {
|
|
7
8
|
return val === null || (typeof val !== "object" && typeof val !== "function");
|
|
8
|
-
}
|
|
9
|
+
};
|
|
9
10
|
|
|
10
|
-
|
|
11
|
+
const formatWithInspect = (val: unknown) => {
|
|
11
12
|
const prefix = isPrimitive(val) ? "" : "\n";
|
|
12
13
|
const shouldFormat = typeof val !== "string";
|
|
13
14
|
|
|
14
15
|
return prefix + (shouldFormat ? inspect(val, {colors: true, depth: null}) : val);
|
|
15
|
-
}
|
|
16
|
+
};
|
|
17
|
+
|
|
18
|
+
const addRequestContextFormat = winston.format((info) => {
|
|
19
|
+
const context = getCurrentLogContext();
|
|
20
|
+
return {...context, ...info};
|
|
21
|
+
});
|
|
22
|
+
|
|
23
|
+
const formatContext = (info: winston.Logform.TransformableInfo): string => {
|
|
24
|
+
const contextParts = [
|
|
25
|
+
info.requestId ? `requestId=${info.requestId}` : undefined,
|
|
26
|
+
info.jobId ? `jobId=${info.jobId}` : undefined,
|
|
27
|
+
info.sessionId ? `sessionId=${info.sessionId}` : undefined,
|
|
28
|
+
info.userId ? `userId=${info.userId}` : undefined,
|
|
29
|
+
info.traceId ? `traceId=${info.traceId}` : undefined,
|
|
30
|
+
].filter(Boolean);
|
|
31
|
+
|
|
32
|
+
if (contextParts.length === 0) {
|
|
33
|
+
return "";
|
|
34
|
+
}
|
|
35
|
+
return ` ${contextParts.join(" ")}`;
|
|
36
|
+
};
|
|
16
37
|
|
|
17
38
|
// Winston doesn't operate like console.log by default, e.g. `logger.error('error',
|
|
18
39
|
// error)` only prints the message and no args. Add handling for all the args,
|
|
19
40
|
// while also supporting splat logging.
|
|
20
|
-
|
|
41
|
+
const printf = (timestamp = false) => {
|
|
21
42
|
return (info: winston.Logform.TransformableInfo) => {
|
|
22
43
|
const msg = formatWithInspect(info.message);
|
|
23
|
-
const
|
|
24
|
-
const
|
|
44
|
+
const splatKey = Symbol.for("splat") as unknown as keyof winston.Logform.TransformableInfo;
|
|
45
|
+
const splatArgs = (info[splatKey] || []) as unknown[];
|
|
46
|
+
const rest = splatArgs.map((data) => formatWithInspect(data)).join(" ");
|
|
47
|
+
const context = formatContext(info);
|
|
25
48
|
if (timestamp) {
|
|
26
|
-
return `${info.timestamp} - ${info.level}: ${msg} ${rest}`;
|
|
49
|
+
return `${info.timestamp} - ${info.level}: ${msg}${context} ${rest}`;
|
|
27
50
|
}
|
|
28
|
-
return `${info.level}: ${msg} ${rest}`;
|
|
51
|
+
return `${info.level}: ${msg}${context} ${rest}`;
|
|
29
52
|
};
|
|
30
|
-
}
|
|
53
|
+
};
|
|
31
54
|
|
|
32
55
|
// Setup a global, default rejection handler.
|
|
33
56
|
winston.add(
|
|
34
57
|
new winston.transports.Console({
|
|
35
58
|
debugStdout: true,
|
|
36
59
|
format: winston.format.combine(
|
|
60
|
+
addRequestContextFormat(),
|
|
37
61
|
winston.format.colorize(),
|
|
38
62
|
winston.format.simple(),
|
|
39
63
|
winston.format.printf(printf(false))
|
|
@@ -46,11 +70,13 @@ winston.add(
|
|
|
46
70
|
|
|
47
71
|
// Setup a default console logger.
|
|
48
72
|
export const winstonLogger = winston.createLogger({
|
|
73
|
+
format: addRequestContextFormat(),
|
|
49
74
|
level: "debug",
|
|
50
75
|
transports: [
|
|
51
76
|
new winston.transports.Console({
|
|
52
77
|
debugStdout: true,
|
|
53
78
|
format: winston.format.combine(
|
|
79
|
+
addRequestContextFormat(),
|
|
54
80
|
winston.format.colorize(),
|
|
55
81
|
winston.format.simple(),
|
|
56
82
|
winston.format.printf(printf(false))
|
|
@@ -63,11 +89,15 @@ export const winstonLogger = winston.createLogger({
|
|
|
63
89
|
});
|
|
64
90
|
|
|
65
91
|
// Helper function to send logs to Sentry if enabled
|
|
66
|
-
|
|
92
|
+
const sendToSentry = (message: string, level: "debug" | "info" | "warn" | "error"): void => {
|
|
67
93
|
if (process.env.USE_SENTRY_LOGGING === "true" && Sentry.logger) {
|
|
68
|
-
Sentry.logger[level](
|
|
94
|
+
const logWithContext = Sentry.logger[level] as (
|
|
95
|
+
message: string,
|
|
96
|
+
attributes?: Record<string, unknown>
|
|
97
|
+
) => void;
|
|
98
|
+
logWithContext(message, getCurrentLogContext());
|
|
69
99
|
}
|
|
70
|
-
}
|
|
100
|
+
};
|
|
71
101
|
|
|
72
102
|
export const logger = {
|
|
73
103
|
// simple way to log a caught exception. e.g. promise().catch(logger.catch)
|
|
@@ -117,10 +147,10 @@ export interface LoggingOptions {
|
|
|
117
147
|
logSlowRequestsWriteMs?: number;
|
|
118
148
|
}
|
|
119
149
|
|
|
120
|
-
export
|
|
150
|
+
export const setupLogging = (options?: LoggingOptions): void => {
|
|
121
151
|
winstonLogger.clear();
|
|
122
152
|
if (!options?.disableConsoleLogging) {
|
|
123
|
-
const formats:
|
|
153
|
+
const formats: winston.Logform.Format[] = [addRequestContextFormat(), winston.format.simple()];
|
|
124
154
|
if (!options?.disableConsoleColors) {
|
|
125
155
|
formats.push(winston.format.colorize());
|
|
126
156
|
}
|
|
@@ -143,7 +173,7 @@ export function setupLogging(options?: LoggingOptions) {
|
|
|
143
173
|
colorize: false,
|
|
144
174
|
compress: true,
|
|
145
175
|
dirname: logDirectory,
|
|
146
|
-
format: winston.format.simple(),
|
|
176
|
+
format: winston.format.combine(addRequestContextFormat(), winston.format.simple()),
|
|
147
177
|
// 30 days of retention
|
|
148
178
|
maxFiles: 30,
|
|
149
179
|
// 50MB max file size
|
|
@@ -187,4 +217,4 @@ export function setupLogging(options?: LoggingOptions) {
|
|
|
187
217
|
winstonLogger.add(transport);
|
|
188
218
|
}
|
|
189
219
|
}
|
|
190
|
-
}
|
|
220
|
+
};
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import mongoose, {type Document} from "mongoose";
|
|
2
2
|
|
|
3
3
|
import type {APIErrorConstructor} from "../errors";
|
|
4
|
-
import {createdUpdatedPlugin, findOneOrNone} from "../plugins";
|
|
4
|
+
import {createdUpdatedPlugin, findExactlyOne, findOneOrNone, isDeletedPlugin} from "../plugins";
|
|
5
5
|
|
|
6
6
|
export interface VersionConfigDocument extends mongoose.Document {
|
|
7
7
|
webWarningVersion: number;
|
|
@@ -11,6 +11,8 @@ export interface VersionConfigDocument extends mongoose.Document {
|
|
|
11
11
|
warningMessage: string;
|
|
12
12
|
requiredMessage: string;
|
|
13
13
|
updateUrl?: string;
|
|
14
|
+
/** How often clients should poll for version updates, in minutes. Defaults to 1440 (24 hours). */
|
|
15
|
+
pollingIntervalMinutes: number;
|
|
14
16
|
created?: Date;
|
|
15
17
|
updated?: Date;
|
|
16
18
|
}
|
|
@@ -22,7 +24,7 @@ export interface VersionConfigModel extends mongoose.Model<VersionConfigDocument
|
|
|
22
24
|
): Promise<(Document & VersionConfigDocument) | null>;
|
|
23
25
|
}
|
|
24
26
|
|
|
25
|
-
const versionConfigSchema = new mongoose.Schema<VersionConfigDocument>(
|
|
27
|
+
const versionConfigSchema = new mongoose.Schema<VersionConfigDocument, VersionConfigModel>(
|
|
26
28
|
{
|
|
27
29
|
mobileRequiredVersion: {
|
|
28
30
|
default: 0,
|
|
@@ -36,6 +38,13 @@ const versionConfigSchema = new mongoose.Schema<VersionConfigDocument>(
|
|
|
36
38
|
min: 0,
|
|
37
39
|
type: Number,
|
|
38
40
|
},
|
|
41
|
+
pollingIntervalMinutes: {
|
|
42
|
+
default: 1440,
|
|
43
|
+
description:
|
|
44
|
+
"How often clients poll for version updates, in minutes (default: 1440 = 24 hours)",
|
|
45
|
+
min: 1,
|
|
46
|
+
type: Number,
|
|
47
|
+
},
|
|
39
48
|
requiredMessage: {
|
|
40
49
|
default: "This version is no longer supported. Please update to continue.",
|
|
41
50
|
description: "Message shown on the blocking screen",
|
|
@@ -84,7 +93,9 @@ const versionConfigSchema = new mongoose.Schema<VersionConfigDocument>(
|
|
|
84
93
|
versionConfigSchema.index({_singleton: 1}, {unique: true});
|
|
85
94
|
|
|
86
95
|
versionConfigSchema.plugin(createdUpdatedPlugin);
|
|
96
|
+
versionConfigSchema.plugin(isDeletedPlugin);
|
|
87
97
|
versionConfigSchema.plugin(findOneOrNone);
|
|
98
|
+
versionConfigSchema.plugin(findExactlyOne);
|
|
88
99
|
|
|
89
100
|
export const VersionConfig = mongoose.model<VersionConfigDocument, VersionConfigModel>(
|
|
90
101
|
"VersionConfig",
|
|
@@ -1,18 +1,17 @@
|
|
|
1
1
|
import * as Sentry from "@sentry/bun";
|
|
2
2
|
import axios from "axios";
|
|
3
3
|
|
|
4
|
-
import {APIError} from "../errors";
|
|
4
|
+
import {APIError, errorMessage} from "../errors";
|
|
5
5
|
import {logger} from "../logger";
|
|
6
6
|
|
|
7
7
|
export const sendToGoogleChat = async (
|
|
8
8
|
messageText: string,
|
|
9
9
|
{channel, shouldThrow = false, env}: {channel?: string; shouldThrow?: boolean; env?: string} = {}
|
|
10
|
-
) => {
|
|
10
|
+
): Promise<void> => {
|
|
11
11
|
const chatWebhooksString = process.env.GOOGLE_CHAT_WEBHOOKS;
|
|
12
12
|
if (!chatWebhooksString) {
|
|
13
13
|
const msg = "GOOGLE_CHAT_WEBHOOKS not set. Google Chat message not sent";
|
|
14
|
-
Sentry.captureException(new
|
|
15
|
-
logger.error(msg);
|
|
14
|
+
Sentry.captureException(new APIError({status: 500, title: msg}));
|
|
16
15
|
return;
|
|
17
16
|
}
|
|
18
17
|
const chatWebhooks = JSON.parse(chatWebhooksString ?? "{}");
|
|
@@ -22,8 +21,7 @@ export const sendToGoogleChat = async (
|
|
|
22
21
|
|
|
23
22
|
if (!chatWebhookUrl) {
|
|
24
23
|
const msg = `No webhook url set in env for ${chatChannel}. Google Chat message not sent`;
|
|
25
|
-
Sentry.captureException(new
|
|
26
|
-
logger.error(msg);
|
|
24
|
+
Sentry.captureException(new APIError({status: 500, title: msg}));
|
|
27
25
|
return;
|
|
28
26
|
}
|
|
29
27
|
|
|
@@ -35,13 +33,13 @@ export const sendToGoogleChat = async (
|
|
|
35
33
|
try {
|
|
36
34
|
await axios.post(chatWebhookUrl, {text: formattedMessageText});
|
|
37
35
|
} catch (error: unknown) {
|
|
38
|
-
const
|
|
39
|
-
logger.error(`Error posting to Google Chat: ${
|
|
36
|
+
const message = errorMessage(error);
|
|
37
|
+
logger.error(`Error posting to Google Chat: ${message}`);
|
|
40
38
|
Sentry.captureException(error);
|
|
41
39
|
if (shouldThrow) {
|
|
42
40
|
throw new APIError({
|
|
43
41
|
status: 500,
|
|
44
|
-
title: `Error posting to Google Chat: ${
|
|
42
|
+
title: `Error posting to Google Chat: ${message}`,
|
|
45
43
|
});
|
|
46
44
|
}
|
|
47
45
|
}
|
|
@@ -2,6 +2,7 @@ import {afterAll, afterEach, beforeEach, describe, expect, it, type Mock, spyOn}
|
|
|
2
2
|
import * as Sentry from "@sentry/bun";
|
|
3
3
|
import axios from "axios";
|
|
4
4
|
|
|
5
|
+
import {APIError} from "../errors";
|
|
5
6
|
import {sendToSlack} from "./slackNotifier";
|
|
6
7
|
|
|
7
8
|
describe("sendToSlack", () => {
|
|
@@ -10,7 +11,7 @@ describe("sendToSlack", () => {
|
|
|
10
11
|
const ORIGINAL_ENV = process.env;
|
|
11
12
|
|
|
12
13
|
beforeEach(() => {
|
|
13
|
-
mockAxiosPost = spyOn(axios, "post").mockResolvedValue({status: 200}
|
|
14
|
+
mockAxiosPost = spyOn(axios, "post").mockResolvedValue({status: 200});
|
|
14
15
|
process.env = {...ORIGINAL_ENV};
|
|
15
16
|
process.env.SLACK_WEBHOOKS = undefined;
|
|
16
17
|
(Sentry.captureException as Mock<typeof Sentry.captureException>).mockClear();
|
|
@@ -85,6 +86,30 @@ describe("sendToSlack", () => {
|
|
|
85
86
|
expect(payload).toEqual({text: "[STG] status ok"});
|
|
86
87
|
});
|
|
87
88
|
|
|
89
|
+
it("reports to Sentry and returns early when channel has no webhook and no default", async () => {
|
|
90
|
+
process.env.SLACK_WEBHOOKS = JSON.stringify({ops: "https://slack.example/ops"});
|
|
91
|
+
|
|
92
|
+
await sendToSlack("orphan message", {slackChannel: "alerts"});
|
|
93
|
+
expect(mockAxiosPost.mock.calls.length).toBe(0);
|
|
94
|
+
expect(
|
|
95
|
+
(Sentry.captureException as Mock<typeof Sentry.captureException>).mock.calls.length
|
|
96
|
+
).toBe(1);
|
|
97
|
+
const captured = (Sentry.captureException as Mock<typeof Sentry.captureException>).mock
|
|
98
|
+
.calls[0][0] as APIError;
|
|
99
|
+
expect(captured).toBeInstanceOf(APIError);
|
|
100
|
+
expect(captured.title).toContain("alerts");
|
|
101
|
+
});
|
|
102
|
+
|
|
103
|
+
it("posts directly using the url parameter without env lookup", async () => {
|
|
104
|
+
mockAxiosPost.mockResolvedValue({status: 200});
|
|
105
|
+
|
|
106
|
+
await sendToSlack("direct msg", {url: "https://direct.example/hook"});
|
|
107
|
+
expect(mockAxiosPost.mock.calls.length).toBe(1);
|
|
108
|
+
const [url, payload] = mockAxiosPost.mock.calls[0];
|
|
109
|
+
expect(url).toBe("https://direct.example/hook");
|
|
110
|
+
expect(payload).toEqual({text: "direct msg"});
|
|
111
|
+
});
|
|
112
|
+
|
|
88
113
|
it("captures error and throws APIError when shouldThrow=true", async () => {
|
|
89
114
|
process.env.SLACK_WEBHOOKS = JSON.stringify({
|
|
90
115
|
default: "https://slack.example/default",
|
|
@@ -95,8 +120,9 @@ describe("sendToSlack", () => {
|
|
|
95
120
|
await sendToSlack("err", {shouldThrow: true});
|
|
96
121
|
throw new Error("Expected sendToSlack to throw APIError");
|
|
97
122
|
} catch (error) {
|
|
98
|
-
|
|
99
|
-
expect(
|
|
123
|
+
const apiError = error as APIError;
|
|
124
|
+
expect(apiError.name).toBe("APIError");
|
|
125
|
+
expect(apiError.title).toMatch(/Error posting to slack/i);
|
|
100
126
|
}
|
|
101
127
|
expect(mockAxiosPost.mock.calls.length).toBe(1);
|
|
102
128
|
});
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import * as Sentry from "@sentry/bun";
|
|
2
2
|
import axios from "axios";
|
|
3
3
|
|
|
4
|
-
import {APIError} from "../errors";
|
|
4
|
+
import {APIError, errorMessage} from "../errors";
|
|
5
5
|
import {logger} from "../logger";
|
|
6
6
|
// Convenience method to send data to a Slack webhook.
|
|
7
7
|
// If `url` is provided, it will be used directly instead of looking up from environment.
|
|
@@ -15,7 +15,7 @@ export const sendToSlack = async (
|
|
|
15
15
|
env,
|
|
16
16
|
url,
|
|
17
17
|
}: {slackChannel?: string; shouldThrow?: boolean; env?: string; url?: string} = {}
|
|
18
|
-
) => {
|
|
18
|
+
): Promise<void> => {
|
|
19
19
|
let slackWebhookUrl = url;
|
|
20
20
|
|
|
21
21
|
if (!slackWebhookUrl) {
|
|
@@ -37,9 +37,11 @@ export const sendToSlack = async (
|
|
|
37
37
|
|
|
38
38
|
if (!slackWebhookUrl) {
|
|
39
39
|
Sentry.captureException(
|
|
40
|
-
new
|
|
40
|
+
new APIError({
|
|
41
|
+
status: 500,
|
|
42
|
+
title: `No webhook url set in env for ${channel}. Slack message not sent`,
|
|
43
|
+
})
|
|
41
44
|
);
|
|
42
|
-
logger.debug(`No webhook url set in env for ${channel}.`);
|
|
43
45
|
return;
|
|
44
46
|
}
|
|
45
47
|
}
|
|
@@ -54,13 +56,13 @@ export const sendToSlack = async (
|
|
|
54
56
|
text: formattedText,
|
|
55
57
|
});
|
|
56
58
|
} catch (error: unknown) {
|
|
57
|
-
const
|
|
58
|
-
logger.error(`Error posting to slack: ${
|
|
59
|
+
const message = errorMessage(error);
|
|
60
|
+
logger.error(`Error posting to slack: ${message}`);
|
|
59
61
|
Sentry.captureException(error);
|
|
60
62
|
if (shouldThrow) {
|
|
61
63
|
throw new APIError({
|
|
62
64
|
status: 500,
|
|
63
|
-
title: `Error posting to slack: ${
|
|
65
|
+
title: `Error posting to slack: ${message}`,
|
|
64
66
|
});
|
|
65
67
|
}
|
|
66
68
|
}
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import * as Sentry from "@sentry/bun";
|
|
2
2
|
import axios from "axios";
|
|
3
3
|
|
|
4
|
-
import {APIError} from "../errors";
|
|
4
|
+
import {APIError, errorMessage} from "../errors";
|
|
5
5
|
import {logger} from "../logger";
|
|
6
6
|
|
|
7
7
|
/**
|
|
@@ -32,12 +32,11 @@ import {logger} from "../logger";
|
|
|
32
32
|
export const sendToZoom = async (
|
|
33
33
|
{header, body, subheader}: {header: string; body: string; subheader?: string},
|
|
34
34
|
{channel, shouldThrow = false, env}: {channel: string; shouldThrow?: boolean; env?: string}
|
|
35
|
-
) => {
|
|
35
|
+
): Promise<void> => {
|
|
36
36
|
const zoomWebhooksString = process.env.ZOOM_CHAT_WEBHOOKS;
|
|
37
37
|
if (!zoomWebhooksString) {
|
|
38
38
|
const msg = "ZOOM_CHAT_WEBHOOKS not set. Zoom message not sent";
|
|
39
|
-
Sentry.captureException(new
|
|
40
|
-
logger.error(msg);
|
|
39
|
+
Sentry.captureException(new APIError({status: 500, title: msg}));
|
|
41
40
|
return;
|
|
42
41
|
}
|
|
43
42
|
const zoomWebhooks: Record<string, {channel: string; verificationToken: string}> = JSON.parse(
|
|
@@ -49,8 +48,7 @@ export const sendToZoom = async (
|
|
|
49
48
|
|
|
50
49
|
if (!zoomWebhookUrl) {
|
|
51
50
|
const msg = `No webhook url set in env for ${zoomChannel}. Zoom message not sent`;
|
|
52
|
-
Sentry.captureException(new
|
|
53
|
-
logger.error(msg);
|
|
51
|
+
Sentry.captureException(new APIError({status: 500, title: msg}));
|
|
54
52
|
return;
|
|
55
53
|
}
|
|
56
54
|
|
|
@@ -58,8 +56,7 @@ export const sendToZoom = async (
|
|
|
58
56
|
zoomWebhooks[zoomChannel]?.verificationToken ?? zoomWebhooks.default?.verificationToken;
|
|
59
57
|
if (!zoomToken) {
|
|
60
58
|
const msg = `No verification token set in env for ${zoomChannel}. Zoom message not sent`;
|
|
61
|
-
Sentry.captureException(new
|
|
62
|
-
logger.error(msg);
|
|
59
|
+
Sentry.captureException(new APIError({status: 500, title: msg}));
|
|
63
60
|
return;
|
|
64
61
|
}
|
|
65
62
|
|
|
@@ -96,13 +93,13 @@ export const sendToZoom = async (
|
|
|
96
93
|
}
|
|
97
94
|
);
|
|
98
95
|
} catch (error: unknown) {
|
|
99
|
-
const
|
|
100
|
-
logger.error(`Error posting to Zoom: ${
|
|
96
|
+
const message = errorMessage(error);
|
|
97
|
+
logger.error(`Error posting to Zoom: ${message}`);
|
|
101
98
|
Sentry.captureException(error);
|
|
102
99
|
if (shouldThrow) {
|
|
103
100
|
throw new APIError({
|
|
104
101
|
status: 500,
|
|
105
|
-
title: `Error posting to Zoom: ${
|
|
102
|
+
title: `Error posting to Zoom: ${message}`,
|
|
106
103
|
});
|
|
107
104
|
}
|
|
108
105
|
}
|
package/src/openApi.test.ts
CHANGED
package/src/openApi.ts
CHANGED
|
@@ -199,7 +199,7 @@ export function listOpenApiMiddleware<T>(
|
|
|
199
199
|
// Remove _id from queryFields, we handle that above.
|
|
200
200
|
?.filter((field) => field !== "_id")
|
|
201
201
|
.map((field) => {
|
|
202
|
-
const params: {name: string; in: "query"; schema:
|
|
202
|
+
const params: {name: string; in: "query"; schema: Record<string, unknown>}[] = [];
|
|
203
203
|
|
|
204
204
|
// Check for datetime/number to support gt/gte/lt/lte
|
|
205
205
|
if (
|
|
@@ -462,10 +462,10 @@ export function deleteOpenApiMiddleware<T>(
|
|
|
462
462
|
// Useful for endpoints that don't directly map to a model.
|
|
463
463
|
export function readOpenApiMiddleware<T>(
|
|
464
464
|
options: Partial<ModelRouterOptions<T>>,
|
|
465
|
-
properties:
|
|
465
|
+
properties: Record<string, unknown>,
|
|
466
466
|
required: string[],
|
|
467
|
-
queryParameters:
|
|
468
|
-
):
|
|
467
|
+
queryParameters: Array<Record<string, unknown>>
|
|
468
|
+
): express.RequestHandler {
|
|
469
469
|
if (!options.openApi?.path) {
|
|
470
470
|
// Just log this once rather than for each middleware.
|
|
471
471
|
logger.debug(
|
package/src/openApiBuilder.ts
CHANGED
|
@@ -29,6 +29,7 @@
|
|
|
29
29
|
* router.get("/stats", middleware, statsHandler);
|
|
30
30
|
* ```
|
|
31
31
|
*/
|
|
32
|
+
import type express from "express";
|
|
32
33
|
import merge from "lodash/merge";
|
|
33
34
|
|
|
34
35
|
import type {ModelRouterOptions} from "./api";
|
|
@@ -111,7 +112,7 @@ export interface OpenApiSchemaProperty {
|
|
|
111
112
|
* };
|
|
112
113
|
* ```
|
|
113
114
|
*/
|
|
114
|
-
export
|
|
115
|
+
export interface OpenApiSchema {
|
|
115
116
|
/** The JSON Schema type (typically "object" or "array") */
|
|
116
117
|
type: string;
|
|
117
118
|
/** Property definitions for object types */
|
|
@@ -122,7 +123,8 @@ export type OpenApiSchema = {
|
|
|
122
123
|
items?: OpenApiSchemaProperty;
|
|
123
124
|
/** Schema for additional properties or boolean to allow/disallow them */
|
|
124
125
|
additionalProperties?: OpenApiSchemaProperty | boolean;
|
|
125
|
-
|
|
126
|
+
[key: string]: unknown;
|
|
127
|
+
}
|
|
126
128
|
|
|
127
129
|
/**
|
|
128
130
|
* Defines a parameter in an OpenAPI operation.
|
|
@@ -246,7 +248,7 @@ interface ValidationConfig {
|
|
|
246
248
|
*/
|
|
247
249
|
export interface OpenApiBuildResult {
|
|
248
250
|
/** The OpenAPI documentation middleware */
|
|
249
|
-
middleware:
|
|
251
|
+
middleware: express.RequestHandler;
|
|
250
252
|
/** Request body schema if defined */
|
|
251
253
|
bodySchema?: Record<string, OpenApiSchemaProperty>;
|
|
252
254
|
/** Query parameter schemas if defined */
|
|
@@ -397,7 +399,7 @@ export class OpenApiMiddlewareBuilder {
|
|
|
397
399
|
* });
|
|
398
400
|
* ```
|
|
399
401
|
*/
|
|
400
|
-
withRequestBody<T extends Record<string,
|
|
402
|
+
withRequestBody<T extends Record<string, unknown>>(
|
|
401
403
|
schema: {
|
|
402
404
|
[K in keyof T]: OpenApiSchemaProperty;
|
|
403
405
|
},
|
|
@@ -454,7 +456,7 @@ export class OpenApiMiddlewareBuilder {
|
|
|
454
456
|
* builder.withResponse(204, "No content");
|
|
455
457
|
* ```
|
|
456
458
|
*/
|
|
457
|
-
withResponse<T extends Record<string,
|
|
459
|
+
withResponse<T extends Record<string, unknown>>(
|
|
458
460
|
statusCode: number,
|
|
459
461
|
schema:
|
|
460
462
|
| {
|
|
@@ -508,7 +510,7 @@ export class OpenApiMiddlewareBuilder {
|
|
|
508
510
|
* }, {description: "List of users"});
|
|
509
511
|
* ```
|
|
510
512
|
*/
|
|
511
|
-
withArrayResponse<T extends Record<string,
|
|
513
|
+
withArrayResponse<T extends Record<string, unknown>>(
|
|
512
514
|
statusCode: number,
|
|
513
515
|
itemSchema: {
|
|
514
516
|
[K in keyof T]: OpenApiSchemaProperty;
|
|
@@ -671,10 +673,10 @@ export class OpenApiMiddlewareBuilder {
|
|
|
671
673
|
* ```
|
|
672
674
|
*/
|
|
673
675
|
buildWithSchemas(): OpenApiBuildResult {
|
|
674
|
-
const noop = (_a
|
|
676
|
+
const noop: express.RequestHandler = (_a, _b, next) => next();
|
|
675
677
|
|
|
676
678
|
// Build the OpenAPI documentation middleware only (no validation middleware)
|
|
677
|
-
let openApiMiddleware:
|
|
679
|
+
let openApiMiddleware: express.RequestHandler = noop;
|
|
678
680
|
if (this.options.openApi?.path) {
|
|
679
681
|
openApiMiddleware = this.options.openApi.path(
|
|
680
682
|
merge(
|
|
@@ -733,11 +735,12 @@ export class OpenApiMiddlewareBuilder {
|
|
|
733
735
|
* router.get("/users/:id", middleware, getUserHandler);
|
|
734
736
|
* ```
|
|
735
737
|
*/
|
|
738
|
+
// biome-ignore lint/suspicious/noExplicitAny: returns either a single RequestHandler or an array depending on validation config — callers spread or invoke
|
|
736
739
|
build(): any {
|
|
737
|
-
const noop = (_a
|
|
740
|
+
const noop: express.RequestHandler = (_a, _b, next) => next();
|
|
738
741
|
|
|
739
742
|
// Build the OpenAPI documentation middleware
|
|
740
|
-
let openApiMiddleware:
|
|
743
|
+
let openApiMiddleware: express.RequestHandler = noop;
|
|
741
744
|
if (this.options.openApi?.path) {
|
|
742
745
|
openApiMiddleware = this.options.openApi.path(
|
|
743
746
|
merge(
|
|
@@ -768,7 +771,7 @@ export class OpenApiMiddlewareBuilder {
|
|
|
768
771
|
}
|
|
769
772
|
|
|
770
773
|
// Build validation middleware
|
|
771
|
-
const validators:
|
|
774
|
+
const validators: express.RequestHandler[] = [openApiMiddleware];
|
|
772
775
|
|
|
773
776
|
// Add body validation if we have a request body schema
|
|
774
777
|
if (this.validationConfig.validateBody && this.requestBodySchema) {
|
package/src/openApiValidator.ts
CHANGED
|
@@ -184,6 +184,7 @@ const getAjvInstance = (): Ajv => {
|
|
|
184
184
|
useDefaults: true,
|
|
185
185
|
validateSchema: false,
|
|
186
186
|
});
|
|
187
|
+
// biome-ignore lint/suspicious/noExplicitAny: ajv-formats has a known type compat issue with AJV instances
|
|
187
188
|
addFormats(instance as any);
|
|
188
189
|
ajvCache.set(key, instance);
|
|
189
190
|
}
|
|
@@ -644,7 +645,7 @@ export const createValidator = (
|
|
|
644
645
|
return (req: Request, res: Response, next: NextFunction): void => {
|
|
645
646
|
// Run body validation first
|
|
646
647
|
if (bodyValidator) {
|
|
647
|
-
bodyValidator(req, res, ((err?:
|
|
648
|
+
bodyValidator(req, res, ((err?: unknown) => {
|
|
648
649
|
if (err) {
|
|
649
650
|
next(err);
|
|
650
651
|
return;
|
|
@@ -714,7 +715,7 @@ const m2sOptions = {
|
|
|
714
715
|
*/
|
|
715
716
|
export const getSchemaFromModel = <T>(model: Model<T>): Record<string, OpenApiSchemaProperty> => {
|
|
716
717
|
const modelSwagger = m2s(model, m2sOptions);
|
|
717
|
-
fixMixedFields(
|
|
718
|
+
fixMixedFields(model.schema, modelSwagger.properties);
|
|
718
719
|
return modelSwagger.properties as Record<string, OpenApiSchemaProperty>;
|
|
719
720
|
};
|
|
720
721
|
|
package/src/permissions.test.ts
CHANGED