@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.
Files changed (172) hide show
  1. package/dist/__tests__/versionCheckPlugin.test.js +136 -3
  2. package/dist/api.arrayOperations.test.js +1 -0
  3. package/dist/api.d.ts +15 -4
  4. package/dist/api.errors.test.js +1 -0
  5. package/dist/api.hooks.test.js +1 -0
  6. package/dist/api.js +153 -104
  7. package/dist/api.query.test.js +1 -0
  8. package/dist/api.test.js +174 -0
  9. package/dist/auth.d.ts +10 -5
  10. package/dist/auth.js +163 -90
  11. package/dist/auth.test.js +159 -0
  12. package/dist/betterAuthApp.test.js +1 -0
  13. package/dist/betterAuthSetup.d.ts +5 -6
  14. package/dist/betterAuthSetup.js +30 -17
  15. package/dist/betterAuthSetup.test.js +1 -0
  16. package/dist/config.d.ts +48 -0
  17. package/dist/config.js +257 -0
  18. package/dist/config.test.d.ts +1 -0
  19. package/dist/config.test.js +328 -0
  20. package/dist/configuration.test.js +1 -0
  21. package/dist/configurationApp.d.ts +1 -1
  22. package/dist/configurationApp.js +17 -13
  23. package/dist/configurationPlugin.test.js +1 -0
  24. package/dist/consentApp.test.js +1 -0
  25. package/dist/envConfigurationPlugin.d.ts +2 -0
  26. package/dist/envConfigurationPlugin.js +173 -0
  27. package/dist/envConfigurationPlugin.test.d.ts +1 -0
  28. package/dist/envConfigurationPlugin.test.js +322 -0
  29. package/dist/errors.d.ts +18 -7
  30. package/dist/errors.js +111 -12
  31. package/dist/errors.test.js +16 -1
  32. package/dist/example.js +19 -7
  33. package/dist/expressServer.d.ts +10 -9
  34. package/dist/expressServer.js +62 -53
  35. package/dist/expressServer.test.js +165 -2
  36. package/dist/githubAuth.d.ts +2 -1
  37. package/dist/githubAuth.js +41 -26
  38. package/dist/githubAuth.test.js +1 -0
  39. package/dist/index.d.ts +4 -0
  40. package/dist/index.js +4 -0
  41. package/dist/logger.d.ts +1 -1
  42. package/dist/logger.js +42 -20
  43. package/dist/models/versionConfig.d.ts +2 -0
  44. package/dist/models/versionConfig.js +8 -0
  45. package/dist/notifiers/googleChatNotifier.js +14 -16
  46. package/dist/notifiers/googleChatNotifier.test.js +1 -0
  47. package/dist/notifiers/slackNotifier.js +16 -14
  48. package/dist/notifiers/slackNotifier.test.js +41 -3
  49. package/dist/notifiers/zoomNotifier.js +7 -10
  50. package/dist/notifiers/zoomNotifier.test.js +1 -0
  51. package/dist/openApi.d.ts +1 -1
  52. package/dist/openApi.test.js +1 -0
  53. package/dist/openApiBuilder.d.ts +39 -6
  54. package/dist/openApiBuilder.js +1 -31
  55. package/dist/openApiBuilder.test.js +1 -0
  56. package/dist/openApiValidator.js +1 -0
  57. package/dist/openApiValidator.test.js +1 -0
  58. package/dist/permissions.d.ts +4 -4
  59. package/dist/permissions.js +67 -65
  60. package/dist/permissions.middleware.test.js +1 -0
  61. package/dist/permissions.test.js +1 -0
  62. package/dist/plugins.d.ts +5 -5
  63. package/dist/plugins.js +18 -9
  64. package/dist/plugins.test.js +1 -1
  65. package/dist/populate.d.ts +15 -8
  66. package/dist/populate.js +23 -24
  67. package/dist/populate.test.js +1 -0
  68. package/dist/realtime/changeStreamWatcher.d.ts +73 -0
  69. package/dist/realtime/changeStreamWatcher.js +724 -0
  70. package/dist/realtime/index.d.ts +6 -0
  71. package/dist/realtime/index.js +27 -0
  72. package/dist/realtime/queryMatcher.d.ts +14 -0
  73. package/dist/realtime/queryMatcher.js +250 -0
  74. package/dist/realtime/queryStore.d.ts +37 -0
  75. package/dist/realtime/queryStore.js +195 -0
  76. package/dist/realtime/realtime.test.d.ts +10 -0
  77. package/dist/realtime/realtime.test.js +3066 -0
  78. package/dist/realtime/realtimeApp.d.ts +93 -0
  79. package/dist/realtime/realtimeApp.js +560 -0
  80. package/dist/realtime/registry.d.ts +40 -0
  81. package/dist/realtime/registry.js +38 -0
  82. package/dist/realtime/socketUser.d.ts +10 -0
  83. package/dist/realtime/socketUser.js +17 -0
  84. package/dist/realtime/types.d.ts +100 -0
  85. package/dist/realtime/types.js +2 -0
  86. package/dist/requestContext.d.ts +37 -0
  87. package/dist/requestContext.js +344 -0
  88. package/dist/requestContext.test.d.ts +1 -0
  89. package/dist/requestContext.test.js +384 -0
  90. package/dist/terrenoApp.d.ts +8 -0
  91. package/dist/terrenoApp.js +50 -13
  92. package/dist/terrenoApp.test.js +194 -21
  93. package/dist/terrenoPlugin.d.ts +11 -0
  94. package/dist/tests/bunSetup.js +1 -0
  95. package/dist/tests.js +1 -1
  96. package/dist/transformers.d.ts +2 -2
  97. package/dist/transformers.js +5 -3
  98. package/dist/transformers.test.js +90 -0
  99. package/dist/types/consentResponse.d.ts +6 -3
  100. package/dist/versionCheckPlugin.d.ts +2 -0
  101. package/dist/versionCheckPlugin.js +18 -12
  102. package/package.json +4 -2
  103. package/src/__tests__/versionCheckPlugin.test.ts +94 -3
  104. package/src/api.arrayOperations.test.ts +1 -0
  105. package/src/api.errors.test.ts +1 -0
  106. package/src/api.hooks.test.ts +1 -0
  107. package/src/api.query.test.ts +1 -0
  108. package/src/api.test.ts +132 -0
  109. package/src/api.ts +199 -84
  110. package/src/auth.test.ts +160 -0
  111. package/src/auth.ts +120 -50
  112. package/src/betterAuthApp.test.ts +1 -0
  113. package/src/betterAuthSetup.test.ts +1 -0
  114. package/src/betterAuthSetup.ts +59 -22
  115. package/src/config.test.ts +255 -0
  116. package/src/config.ts +216 -0
  117. package/src/configuration.test.ts +1 -0
  118. package/src/configurationApp.ts +59 -24
  119. package/src/configurationPlugin.test.ts +1 -0
  120. package/src/consentApp.test.ts +1 -0
  121. package/src/envConfigurationPlugin.test.ts +143 -0
  122. package/src/envConfigurationPlugin.ts +100 -0
  123. package/src/errors.test.ts +19 -1
  124. package/src/errors.ts +118 -38
  125. package/src/example.ts +49 -21
  126. package/src/express.d.ts +18 -1
  127. package/src/expressServer.test.ts +147 -2
  128. package/src/expressServer.ts +80 -50
  129. package/src/githubAuth.test.ts +1 -0
  130. package/src/githubAuth.ts +59 -38
  131. package/src/index.ts +4 -0
  132. package/src/logger.ts +47 -17
  133. package/src/models/versionConfig.ts +13 -2
  134. package/src/notifiers/googleChatNotifier.test.ts +1 -0
  135. package/src/notifiers/googleChatNotifier.ts +7 -9
  136. package/src/notifiers/slackNotifier.test.ts +29 -3
  137. package/src/notifiers/slackNotifier.ts +9 -7
  138. package/src/notifiers/zoomNotifier.test.ts +1 -0
  139. package/src/notifiers/zoomNotifier.ts +8 -11
  140. package/src/openApi.test.ts +1 -0
  141. package/src/openApi.ts +4 -4
  142. package/src/openApiBuilder.test.ts +1 -0
  143. package/src/openApiBuilder.ts +14 -11
  144. package/src/openApiValidator.test.ts +1 -0
  145. package/src/openApiValidator.ts +3 -2
  146. package/src/permissions.middleware.test.ts +1 -0
  147. package/src/permissions.test.ts +1 -0
  148. package/src/permissions.ts +30 -25
  149. package/src/plugins.test.ts +1 -1
  150. package/src/plugins.ts +21 -14
  151. package/src/populate.test.ts +1 -0
  152. package/src/populate.ts +44 -36
  153. package/src/realtime/changeStreamWatcher.ts +572 -0
  154. package/src/realtime/index.ts +34 -0
  155. package/src/realtime/queryMatcher.ts +179 -0
  156. package/src/realtime/queryStore.ts +132 -0
  157. package/src/realtime/realtime.test.ts +2465 -0
  158. package/src/realtime/realtimeApp.ts +478 -0
  159. package/src/realtime/registry.ts +64 -0
  160. package/src/realtime/socketUser.ts +25 -0
  161. package/src/realtime/types.ts +112 -0
  162. package/src/requestContext.test.ts +321 -0
  163. package/src/requestContext.ts +368 -0
  164. package/src/terrenoApp.test.ts +137 -11
  165. package/src/terrenoApp.ts +64 -17
  166. package/src/terrenoPlugin.ts +12 -0
  167. package/src/tests/bunSetup.ts +1 -0
  168. package/src/tests.ts +7 -2
  169. package/src/transformers.test.ts +70 -2
  170. package/src/transformers.ts +15 -7
  171. package/src/types/consentResponse.ts +8 -10
  172. 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
- function isPrimitive(val: any) {
7
+ const isPrimitive = (val: unknown) => {
7
8
  return val === null || (typeof val !== "object" && typeof val !== "function");
8
- }
9
+ };
9
10
 
10
- function formatWithInspect(val: any) {
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
- function printf(timestamp = false) {
41
+ const printf = (timestamp = false) => {
21
42
  return (info: winston.Logform.TransformableInfo) => {
22
43
  const msg = formatWithInspect(info.message);
23
- const splatArgs = (info[Symbol.for("splat") as any] || []) as any[];
24
- const rest = splatArgs.map((data: any) => formatWithInspect(data)).join(" ");
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
- function sendToSentry(message: string, level: "debug" | "info" | "warn" | "error") {
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](message);
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 function setupLogging(options?: LoggingOptions) {
150
+ export const setupLogging = (options?: LoggingOptions): void => {
121
151
  winstonLogger.clear();
122
152
  if (!options?.disableConsoleLogging) {
123
- const formats: any[] = [winston.format.simple()];
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,3 +1,4 @@
1
+ // biome-ignore-all lint/suspicious/noExplicitAny: test mock typing
1
2
  import {afterAll, afterEach, beforeEach, describe, expect, it, type Mock, spyOn} from "bun:test";
2
3
  import * as Sentry from "@sentry/bun";
3
4
  import axios from "axios";
@@ -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 Error(msg));
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 Error(msg));
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 errorObj = error as {text?: string; message?: string};
39
- logger.error(`Error posting to Google Chat: ${errorObj.text ?? errorObj.message}`);
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: ${errorObj.text ?? errorObj.message}`,
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} as any);
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
- expect((error as any).name).toBe("APIError");
99
- expect((error as any).title).toMatch(/Error posting to slack/i);
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 Error(`No webhook url set in env for ${channel}. Slack message not sent`)
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 errorObj = error as {text?: string; message?: string};
58
- logger.error(`Error posting to slack: ${errorObj.text ?? errorObj.message}`);
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: ${errorObj.text ?? errorObj.message}`,
65
+ title: `Error posting to slack: ${message}`,
64
66
  });
65
67
  }
66
68
  }
@@ -1,3 +1,4 @@
1
+ // biome-ignore-all lint/suspicious/noExplicitAny: test mock typing
1
2
  import {afterAll, afterEach, beforeEach, describe, expect, it, type Mock, spyOn} from "bun:test";
2
3
  import * as Sentry from "@sentry/bun";
3
4
  import axios from "axios";
@@ -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 Error(msg));
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 Error(msg));
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 Error(msg));
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 errorMessage = error instanceof Error ? error.message : String(error);
100
- logger.error(`Error posting to Zoom: ${errorMessage}`);
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: ${errorMessage}`,
102
+ title: `Error posting to Zoom: ${message}`,
106
103
  });
107
104
  }
108
105
  }
@@ -1,3 +1,4 @@
1
+ // biome-ignore-all lint/suspicious/noExplicitAny: test mock typing
1
2
  import {beforeEach, describe, expect, it} from "bun:test";
2
3
  import type express from "express";
3
4
  import type {Router} from "express";
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: any}[] = [];
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: any,
465
+ properties: Record<string, unknown>,
466
466
  required: string[],
467
- queryParameters: any
468
- ): any {
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(
@@ -1,3 +1,4 @@
1
+ // biome-ignore-all lint/suspicious/noExplicitAny: test mock typing
1
2
  import {beforeEach, describe, expect, it} from "bun:test";
2
3
  import type express from "express";
3
4
  import type {Router} from "express";
@@ -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 type OpenApiSchema = {
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: any;
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, any>>(
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, any>>(
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, any>>(
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: any, _b: any, next: () => void): void => next();
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: any = noop;
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: any, _b: any, next: () => void): void => next();
740
+ const noop: express.RequestHandler = (_a, _b, next) => next();
738
741
 
739
742
  // Build the OpenAPI documentation middleware
740
- let openApiMiddleware: any = noop;
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: any[] = [openApiMiddleware];
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) {
@@ -1,3 +1,4 @@
1
+ // biome-ignore-all lint/suspicious/noExplicitAny: test mock typing
1
2
  import {afterEach, beforeEach, describe, expect, it} from "bun:test";
2
3
  import type {ErrorObject} from "ajv";
3
4
 
@@ -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?: any) => {
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((model as any).schema, modelSwagger.properties);
718
+ fixMixedFields(model.schema, modelSwagger.properties);
718
719
  return modelSwagger.properties as Record<string, OpenApiSchemaProperty>;
719
720
  };
720
721
 
@@ -1,3 +1,4 @@
1
+ // biome-ignore-all lint/suspicious/noExplicitAny: test mock typing
1
2
  import {describe, expect, it, mock, spyOn} from "bun:test";
2
3
  import * as Sentry from "@sentry/bun";
3
4
  import type express from "express";
@@ -1,3 +1,4 @@
1
+ // biome-ignore-all lint/suspicious/noExplicitAny: test mock typing
1
2
  import {beforeEach, describe, expect, it} from "bun:test";
2
3
  import type express from "express";
3
4
  import supertest from "supertest";