@terreno/api 0.9.1 → 0.9.3

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/api.d.ts CHANGED
@@ -19,6 +19,26 @@ export declare function addPopulateToQuery(builtQuery: mongoose.Query<any[], any
19
19
  * @returns The sum of `a` and `b`
20
20
  */
21
21
  export type RESTMethod = "list" | "create" | "read" | "update" | "delete";
22
+ /**
23
+ * Interface for the vendored @wesleytodd/openapi Express middleware.
24
+ * Provides methods for building OpenAPI documentation from Express routes.
25
+ */
26
+ export interface OpenApiMiddleware {
27
+ /** The middleware itself is callable as Express middleware. */
28
+ (req: express.Request, res: express.Response, next: express.NextFunction): void;
29
+ /** Register a path-level OpenAPI schema, returning an Express middleware that attaches the schema to the route. */
30
+ path: (schema?: Record<string, unknown>) => express.RequestHandler;
31
+ /** Register or retrieve an OpenAPI component definition (schemas, responses, parameters, etc). */
32
+ component: (type: string, name?: string, description?: Record<string, unknown>) => OpenApiMiddleware | {
33
+ $ref: string;
34
+ } | Record<string, unknown> | undefined;
35
+ /** Shorthand for component("schemas", ...) */
36
+ schema: (name?: string, description?: Record<string, unknown>) => OpenApiMiddleware | {
37
+ $ref: string;
38
+ } | Record<string, unknown> | undefined;
39
+ /** The generated OpenAPI document */
40
+ document: Record<string, unknown>;
41
+ }
22
42
  /**
23
43
  * This is the main configuration.
24
44
  * @param T - the base document type. This should not include Mongoose models, just the types of the object.
@@ -55,7 +75,7 @@ export interface ModelRouterOptions<T> {
55
75
  * You can transform the given query params by returning different values.
56
76
  * If the query is acceptable as-is, return `query` as-is.
57
77
  */
58
- queryFilter?: (user?: User, query?: Record<string, any>) => Record<string, any> | null | Promise<Record<string, any> | null>;
78
+ queryFilter?: (user?: User, query?: Record<string, unknown>) => Record<string, unknown> | null | Promise<Record<string, unknown> | null>;
59
79
  /**
60
80
  * Transformers allow data to be transformed before actions are executed,
61
81
  * and serialized before being returned to the user.
@@ -83,9 +103,7 @@ export interface ModelRouterOptions<T> {
83
103
  * list queries. Accepts any Mongoose-style queries, and runs for all user types.
84
104
  * defaultQueryParams: \{hidden: false\} // By default, don't show objects with hidden=true
85
105
  * These can be overridden by the user if not disallowed by queryFilter. */
86
- defaultQueryParams?: {
87
- [key: string]: any;
88
- };
106
+ defaultQueryParams?: Record<string, unknown>;
89
107
  /**
90
108
  * Manages Mongoose populations before returning from all methods (list, read, create, etc).
91
109
  * For each population:
@@ -109,14 +127,14 @@ export interface ModelRouterOptions<T> {
109
127
  * or 500. */
110
128
  maxLimit?: number;
111
129
  /** Custom route setup function. Receives the router and optionally the full options (including openApi). */
112
- endpoints?: (router: any, options?: Partial<ModelRouterOptions<T>>) => void;
130
+ endpoints?: (router: express.Router, options?: Partial<ModelRouterOptions<T>>) => void;
113
131
  /**
114
132
  * Hook that runs after `transformer.transform` but before the object is created.
115
133
  * Can update the body fields based on the request or the user.
116
134
  * Return null to return a generic 403 error. Throw an APIError to return a 400 with specific
117
135
  * error information.
118
136
  */
119
- preCreate?: (value: any, request: express.Request) => T | Promise<T> | null;
137
+ preCreate?: (value: Partial<T> | (Partial<T> | undefined)[] | null | undefined, request: express.Request) => T | Promise<T> | null;
120
138
  /**
121
139
  * Hook that runs after `transformer.transform` but before changes are made for update operations.
122
140
  * Can update the body fields based on the request or the user.
@@ -185,17 +203,17 @@ export interface ModelRouterOptions<T> {
185
203
  /**
186
204
  * The OpenAPI generator for this server. This is used to generate the OpenAPI documentation.
187
205
  */
188
- openApi?: any;
206
+ openApi?: OpenApiMiddleware;
189
207
  /**
190
208
  * Overwrite parts of the configuration for the OpenAPI generator.
191
209
  * This will be merged with the generated configuration.
192
210
  */
193
211
  openApiOverwrite?: {
194
- get?: any;
195
- list?: any;
196
- create?: any;
197
- update?: any;
198
- delete?: any;
212
+ get?: Record<string, unknown>;
213
+ list?: Record<string, unknown>;
214
+ create?: Record<string, unknown>;
215
+ update?: Record<string, unknown>;
216
+ delete?: Record<string, unknown>;
199
217
  };
200
218
  /**
201
219
  * Overwrite parts of the model properties for the OpenAPI generator.
@@ -203,7 +221,7 @@ export interface ModelRouterOptions<T> {
203
221
  * This is useful if you add custom properties to the model during serialize, for example,
204
222
  * that you want to be documented and typed in the SDK.
205
223
  */
206
- openApiExtraModelProperties?: any;
224
+ openApiExtraModelProperties?: Record<string, unknown>;
207
225
  /**
208
226
  * Enable runtime validation of request bodies against the OpenAPI schema.
209
227
  * When enabled, requests that don't match the documented schema will return 400 errors.
package/dist/errors.js CHANGED
@@ -112,7 +112,13 @@ var APIError = /** @class */ (function (_super) {
112
112
  _this.meta.fields = fields;
113
113
  }
114
114
  _this.error = error;
115
- logger_1.logger.error("APIError(".concat(status, "): ").concat(title, " ").concat(detail ? detail : "").concat(((_a = data.error) === null || _a === void 0 ? void 0 : _a.stack) ? "\n".concat((_b = data.error) === null || _b === void 0 ? void 0 : _b.stack) : ""));
115
+ var logMessage = "APIError(".concat(status, "): ").concat(title, " ").concat(detail ? detail : "").concat(((_a = data.error) === null || _a === void 0 ? void 0 : _a.stack) ? "\n".concat((_b = data.error) === null || _b === void 0 ? void 0 : _b.stack) : "");
116
+ if (data.disableExternalErrorTracking) {
117
+ logger_1.logger.warn(logMessage);
118
+ }
119
+ else {
120
+ logger_1.logger.error(logMessage);
121
+ }
116
122
  return _this;
117
123
  }
118
124
  return APIError;
@@ -6,7 +6,7 @@ import { type UserModel as UserMongooseModel } from "./auth";
6
6
  import { type GitHubAuthOptions } from "./githubAuth";
7
7
  import { type LoggingOptions } from "./logger";
8
8
  export declare function setupEnvironment(): void;
9
- export type AddRoutes = (router: Router, options?: Partial<ModelRouterOptions<any>>) => void;
9
+ export type AddRoutes = (router: Router, options?: Partial<ModelRouterOptions<unknown>>) => void;
10
10
  export declare function logRequests(req: any, res: any, next: any): void;
11
11
  export declare function createRouter(rootPath: string, addRoutes: AddRoutes, middleware?: any[]): any[];
12
12
  export declare function createRouterWithAuth(rootPath: string, addRoutes: (router: Router) => void, middleware?: any[]): any[];
package/dist/openApi.d.ts CHANGED
@@ -1,3 +1,4 @@
1
+ import type express from "express";
1
2
  import type { Model } from "mongoose";
2
3
  import type { ModelRouterOptions } from "./api";
3
4
  export declare const apiErrorContent: {
@@ -52,9 +53,9 @@ export declare const defaultOpenApiErrorResponses: {
52
53
  description: string;
53
54
  };
54
55
  };
55
- export declare function getOpenApiMiddleware<T>(model: Model<T>, options: Partial<ModelRouterOptions<T>>): any;
56
- export declare function listOpenApiMiddleware<T>(model: Model<T>, options: Partial<ModelRouterOptions<T>>): any;
57
- export declare function createOpenApiMiddleware<T>(model: Model<T>, options: Partial<ModelRouterOptions<T>>): any;
58
- export declare function patchOpenApiMiddleware<T>(model: Model<T>, options: Partial<ModelRouterOptions<T>>): any;
59
- export declare function deleteOpenApiMiddleware<T>(model: Model<T>, options: Partial<ModelRouterOptions<T>>): any;
56
+ export declare function getOpenApiMiddleware<T>(model: Model<T>, options: Partial<ModelRouterOptions<T>>): express.RequestHandler;
57
+ export declare function listOpenApiMiddleware<T>(model: Model<T>, options: Partial<ModelRouterOptions<T>>): express.RequestHandler;
58
+ export declare function createOpenApiMiddleware<T>(model: Model<T>, options: Partial<ModelRouterOptions<T>>): express.RequestHandler;
59
+ export declare function patchOpenApiMiddleware<T>(model: Model<T>, options: Partial<ModelRouterOptions<T>>): express.RequestHandler;
60
+ export declare function deleteOpenApiMiddleware<T>(model: Model<T>, options: Partial<ModelRouterOptions<T>>): express.RequestHandler;
60
61
  export declare function readOpenApiMiddleware<T>(options: Partial<ModelRouterOptions<T>>, properties: any, required: string[], queryParameters: any): any;
@@ -292,7 +292,8 @@ function addRoutes(router, options) {
292
292
  }); });
293
293
  });
294
294
  function addRoutesPopulate(router, options) {
295
- options === null || options === void 0 ? void 0 : options.openApi.component("schemas", "LimitedUser", {
295
+ var _a;
296
+ (_a = options === null || options === void 0 ? void 0 : options.openApi) === null || _a === void 0 ? void 0 : _a.component("schemas", "LimitedUser", {
296
297
  properties: {
297
298
  email: {
298
299
  description: "LimitedUser's email",
@@ -209,7 +209,7 @@ export declare class OpenApiMiddlewareBuilder {
209
209
  *
210
210
  * @param options - Router options containing the OpenAPI path configuration
211
211
  */
212
- constructor(options: Partial<ModelRouterOptions<any>>);
212
+ constructor(options: Partial<ModelRouterOptions<unknown>>);
213
213
  /**
214
214
  * Sets the tags for the OpenAPI operation.
215
215
  *
@@ -484,4 +484,4 @@ export declare class OpenApiMiddlewareBuilder {
484
484
  * router.get("/analytics/stats", statsMiddleware, getStatsHandler);
485
485
  * ```
486
486
  */
487
- export declare function createOpenApiBuilder(options: Partial<ModelRouterOptions<any>>): OpenApiMiddlewareBuilder;
487
+ export declare function createOpenApiBuilder(options: Partial<ModelRouterOptions<unknown>>): OpenApiMiddlewareBuilder;
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,78 @@
1
+ "use strict";
2
+ var __importDefault = (this && this.__importDefault) || function (mod) {
3
+ return (mod && mod.__esModule) ? mod : { "default": mod };
4
+ };
5
+ Object.defineProperty(exports, "__esModule", { value: true });
6
+ var bun_test_1 = require("bun:test");
7
+ var node_crypto_1 = __importDefault(require("node:crypto"));
8
+ var openApiEtag_1 = require("./openApiEtag");
9
+ var buildRequest = function (options) {
10
+ if (options === void 0) { options = {}; }
11
+ var ifNoneMatch = options.ifNoneMatch, _a = options.method, method = _a === void 0 ? "GET" : _a, _b = options.path, path = _b === void 0 ? "/openapi.json" : _b;
12
+ return {
13
+ get: function (header) {
14
+ return header === "If-None-Match" ? ifNoneMatch : undefined;
15
+ },
16
+ method: method,
17
+ path: path,
18
+ };
19
+ };
20
+ var buildResponse = function () {
21
+ var originalJson = (0, bun_test_1.mock)(function (body) { return ({ body: body }); });
22
+ var resObject = {
23
+ json: originalJson,
24
+ };
25
+ var set = (0, bun_test_1.mock)(function () { return resObject; });
26
+ var status = (0, bun_test_1.mock)(function () { return resObject; });
27
+ var end = (0, bun_test_1.mock)(function () { return resObject; });
28
+ resObject.set = set;
29
+ resObject.status = status;
30
+ resObject.end = end;
31
+ return {
32
+ end: end,
33
+ originalJson: originalJson,
34
+ res: resObject,
35
+ set: set,
36
+ status: status,
37
+ };
38
+ };
39
+ (0, bun_test_1.describe)("openApiEtagMiddleware", function () {
40
+ (0, bun_test_1.it)("skips non-openapi requests", function () {
41
+ var req = buildRequest({ method: "POST", path: "/health" });
42
+ var _a = buildResponse(), res = _a.res, originalJson = _a.originalJson;
43
+ var next = (0, bun_test_1.mock)(function () { });
44
+ (0, openApiEtag_1.openApiEtagMiddleware)(req, res, next);
45
+ (0, bun_test_1.expect)(next).toHaveBeenCalledTimes(1);
46
+ (0, bun_test_1.expect)(res.json).toBe(originalJson);
47
+ });
48
+ (0, bun_test_1.it)("sets ETag and returns json body when no matching If-None-Match header is provided", function () {
49
+ var req = buildRequest();
50
+ var _a = buildResponse(), res = _a.res, originalJson = _a.originalJson, set = _a.set, status = _a.status, end = _a.end;
51
+ var next = (0, bun_test_1.mock)(function () { });
52
+ var body = { openapi: "3.0.0", paths: { "/todos": { get: {} } } };
53
+ (0, openApiEtag_1.openApiEtagMiddleware)(req, res, next);
54
+ var result = res.json(body);
55
+ var expectedEtag = "\"".concat(node_crypto_1.default.createHash("sha256").update(JSON.stringify(body)).digest("hex").substring(0, 16), "\"");
56
+ (0, bun_test_1.expect)(next).toHaveBeenCalledTimes(1);
57
+ (0, bun_test_1.expect)(set).toHaveBeenCalledWith("ETag", expectedEtag);
58
+ (0, bun_test_1.expect)(originalJson).toHaveBeenCalledWith(body);
59
+ (0, bun_test_1.expect)(status).toHaveBeenCalledTimes(0);
60
+ (0, bun_test_1.expect)(end).toHaveBeenCalledTimes(0);
61
+ (0, bun_test_1.expect)(result).toEqual({ body: body });
62
+ });
63
+ (0, bun_test_1.it)("returns 304 when If-None-Match matches generated ETag", function () {
64
+ var body = { openapi: "3.0.0", paths: { "/users": { post: {} } } };
65
+ var etag = "\"".concat(node_crypto_1.default.createHash("sha256").update(JSON.stringify(body)).digest("hex").substring(0, 16), "\"");
66
+ var req = buildRequest({ ifNoneMatch: etag });
67
+ var _a = buildResponse(), res = _a.res, originalJson = _a.originalJson, set = _a.set, status = _a.status, end = _a.end;
68
+ var next = (0, bun_test_1.mock)(function () { });
69
+ (0, openApiEtag_1.openApiEtagMiddleware)(req, res, next);
70
+ var result = res.json(body);
71
+ (0, bun_test_1.expect)(next).toHaveBeenCalledTimes(1);
72
+ (0, bun_test_1.expect)(set).toHaveBeenCalledWith("ETag", etag);
73
+ (0, bun_test_1.expect)(status).toHaveBeenCalledWith(304);
74
+ (0, bun_test_1.expect)(end).toHaveBeenCalledTimes(1);
75
+ (0, bun_test_1.expect)(originalJson).toHaveBeenCalledTimes(0);
76
+ (0, bun_test_1.expect)(result).toBe(res);
77
+ });
78
+ });
@@ -7,9 +7,9 @@ export type PopulatePath = {
7
7
  export declare const fixMixedFields: (schema: any, properties: Record<string, any>) => void;
8
8
  export declare function getOpenApiSpecForModel(model: any, { populatePaths, extraModelProperties, }?: {
9
9
  populatePaths?: PopulatePath[];
10
- extraModelProperties?: any;
10
+ extraModelProperties?: Record<string, unknown>;
11
11
  }): {
12
- properties: any;
12
+ properties: Record<string, unknown>;
13
13
  required: string[];
14
14
  };
15
15
  export declare function unpopulate<T>(doc: Document<T>, path: string): Document<T>;
package/package.json CHANGED
@@ -77,6 +77,9 @@
77
77
  "license": "Apache-2.0",
78
78
  "main": "dist/index.js",
79
79
  "name": "@terreno/api",
80
+ "publishConfig": {
81
+ "access": "public"
82
+ },
80
83
  "peerDependencies": {
81
84
  "mongoose": "^8.0.0"
82
85
  },
@@ -100,5 +103,5 @@
100
103
  "updateSnapshot": "bun test --update-snapshots"
101
104
  },
102
105
  "types": "dist/index.d.ts",
103
- "version": "0.9.1"
106
+ "version": "0.9.3"
104
107
  }
package/src/api.ts CHANGED
@@ -72,6 +72,30 @@ const COMPLEX_QUERY_PARAMS = ["$and", "$or"];
72
72
  */
73
73
  export type RESTMethod = "list" | "create" | "read" | "update" | "delete";
74
74
 
75
+ /**
76
+ * Interface for the vendored @wesleytodd/openapi Express middleware.
77
+ * Provides methods for building OpenAPI documentation from Express routes.
78
+ */
79
+ export interface OpenApiMiddleware {
80
+ /** The middleware itself is callable as Express middleware. */
81
+ (req: express.Request, res: express.Response, next: express.NextFunction): void;
82
+ /** Register a path-level OpenAPI schema, returning an Express middleware that attaches the schema to the route. */
83
+ path: (schema?: Record<string, unknown>) => express.RequestHandler;
84
+ /** Register or retrieve an OpenAPI component definition (schemas, responses, parameters, etc). */
85
+ component: (
86
+ type: string,
87
+ name?: string,
88
+ description?: Record<string, unknown>
89
+ ) => OpenApiMiddleware | {$ref: string} | Record<string, unknown> | undefined;
90
+ /** Shorthand for component("schemas", ...) */
91
+ schema: (
92
+ name?: string,
93
+ description?: Record<string, unknown>
94
+ ) => OpenApiMiddleware | {$ref: string} | Record<string, unknown> | undefined;
95
+ /** The generated OpenAPI document */
96
+ document: Record<string, unknown>;
97
+ }
98
+
75
99
  /**
76
100
  * This is the main configuration.
77
101
  * @param T - the base document type. This should not include Mongoose models, just the types of the object.
@@ -110,8 +134,8 @@ export interface ModelRouterOptions<T> {
110
134
  */
111
135
  queryFilter?: (
112
136
  user?: User,
113
- query?: Record<string, any>
114
- ) => Record<string, any> | null | Promise<Record<string, any> | null>;
137
+ query?: Record<string, unknown>
138
+ ) => Record<string, unknown> | null | Promise<Record<string, unknown> | null>;
115
139
  /**
116
140
  * Transformers allow data to be transformed before actions are executed,
117
141
  * and serialized before being returned to the user.
@@ -137,7 +161,7 @@ export interface ModelRouterOptions<T> {
137
161
  * list queries. Accepts any Mongoose-style queries, and runs for all user types.
138
162
  * defaultQueryParams: \{hidden: false\} // By default, don't show objects with hidden=true
139
163
  * These can be overridden by the user if not disallowed by queryFilter. */
140
- defaultQueryParams?: {[key: string]: any};
164
+ defaultQueryParams?: Record<string, unknown>;
141
165
  /**
142
166
  * Manages Mongoose populations before returning from all methods (list, read, create, etc).
143
167
  * For each population:
@@ -161,14 +185,17 @@ export interface ModelRouterOptions<T> {
161
185
  * or 500. */
162
186
  maxLimit?: number; // defaults to 500
163
187
  /** Custom route setup function. Receives the router and optionally the full options (including openApi). */
164
- endpoints?: (router: any, options?: Partial<ModelRouterOptions<T>>) => void;
188
+ endpoints?: (router: express.Router, options?: Partial<ModelRouterOptions<T>>) => void;
165
189
  /**
166
190
  * Hook that runs after `transformer.transform` but before the object is created.
167
191
  * Can update the body fields based on the request or the user.
168
192
  * Return null to return a generic 403 error. Throw an APIError to return a 400 with specific
169
193
  * error information.
170
194
  */
171
- preCreate?: (value: any, request: express.Request) => T | Promise<T> | null;
195
+ preCreate?: (
196
+ value: Partial<T> | (Partial<T> | undefined)[] | null | undefined,
197
+ request: express.Request
198
+ ) => T | Promise<T> | null;
172
199
  /**
173
200
  * Hook that runs after `transformer.transform` but before changes are made for update operations.
174
201
  * Can update the body fields based on the request or the user.
@@ -250,17 +277,17 @@ export interface ModelRouterOptions<T> {
250
277
  /**
251
278
  * The OpenAPI generator for this server. This is used to generate the OpenAPI documentation.
252
279
  */
253
- openApi?: any;
280
+ openApi?: OpenApiMiddleware;
254
281
  /**
255
282
  * Overwrite parts of the configuration for the OpenAPI generator.
256
283
  * This will be merged with the generated configuration.
257
284
  */
258
285
  openApiOverwrite?: {
259
- get?: any;
260
- list?: any;
261
- create?: any;
262
- update?: any;
263
- delete?: any;
286
+ get?: Record<string, unknown>;
287
+ list?: Record<string, unknown>;
288
+ create?: Record<string, unknown>;
289
+ update?: Record<string, unknown>;
290
+ delete?: Record<string, unknown>;
264
291
  };
265
292
  /**
266
293
  * Overwrite parts of the model properties for the OpenAPI generator.
@@ -268,7 +295,7 @@ export interface ModelRouterOptions<T> {
268
295
  * This is useful if you add custom properties to the model during serialize, for example,
269
296
  * that you want to be documented and typed in the SDK.
270
297
  */
271
- openApiExtraModelProperties?: any;
298
+ openApiExtraModelProperties?: Record<string, unknown>;
272
299
  /**
273
300
  * Enable runtime validation of request bodies against the OpenAPI schema.
274
301
  * When enabled, requests that don't match the documented schema will return 400 errors.
package/src/errors.ts CHANGED
@@ -120,11 +120,14 @@ export class APIError extends Error {
120
120
  this.meta.fields = fields;
121
121
  }
122
122
  this.error = error;
123
- logger.error(
124
- `APIError(${status}): ${title} ${detail ? detail : ""}${
125
- data.error?.stack ? `\n${data.error?.stack}` : ""
126
- }`
127
- );
123
+ const logMessage = `APIError(${status}): ${title} ${detail ? detail : ""}${
124
+ data.error?.stack ? `\n${data.error?.stack}` : ""
125
+ }`;
126
+ if (data.disableExternalErrorTracking) {
127
+ logger.warn(logMessage);
128
+ } else {
129
+ logger.error(logMessage);
130
+ }
128
131
  }
129
132
  }
130
133
 
@@ -42,7 +42,7 @@ export function setupEnvironment(): void {
42
42
  }
43
43
  }
44
44
 
45
- export type AddRoutes = (router: Router, options?: Partial<ModelRouterOptions<any>>) => void;
45
+ export type AddRoutes = (router: Router, options?: Partial<ModelRouterOptions<unknown>>) => void;
46
46
 
47
47
  const logRequestsFinished = (req: any, res: any, startTime: bigint) => {
48
48
  const options = (res.locals.loggingOptions ?? {}) as LoggingOptions;
@@ -11,7 +11,7 @@ import {Permissions} from "./permissions";
11
11
  import {FoodModel, setupDb, UserModel} from "./tests";
12
12
 
13
13
  function getMessageSummaryOpenApiMiddleware(options: Partial<ModelRouterOptions<any>>): any {
14
- return options.openApi.path({
14
+ return options.openApi!.path({
15
15
  parameters: [
16
16
  {
17
17
  in: "query",
@@ -177,7 +177,7 @@ describe("openApi", () => {
177
177
  });
178
178
 
179
179
  function addRoutesPopulate(router: Router, options?: Partial<ModelRouterOptions<any>>): void {
180
- options?.openApi.component("schemas", "LimitedUser", {
180
+ options?.openApi?.component("schemas", "LimitedUser", {
181
181
  properties: {
182
182
  email: {
183
183
  description: "LimitedUser's email",
package/src/openApi.ts CHANGED
@@ -1,9 +1,10 @@
1
+ import type express from "express";
1
2
  import flatten from "lodash/flatten";
2
3
  import merge from "lodash/merge";
3
4
  import type {Model} from "mongoose";
4
5
  import m2s from "mongoose-to-swagger";
5
6
 
6
- import type {ModelRouterOptions} from "./api";
7
+ import type {ModelRouterOptions, OpenApiMiddleware} from "./api";
7
8
  import {logger} from "./logger";
8
9
  import {getOpenApiSpecForModel} from "./populate";
9
10
 
@@ -43,7 +44,7 @@ export const defaultOpenApiErrorResponses = {
43
44
  };
44
45
 
45
46
  // We repeat this constantly, so we make it a component so we only have to define it once.
46
- function createAPIErrorComponent(openApi: any) {
47
+ function createAPIErrorComponent(openApi?: OpenApiMiddleware) {
47
48
  // Create a schema component called APIError
48
49
  openApi?.component("schemas", "APIError", {
49
50
  properties: {
@@ -112,7 +113,10 @@ function createAPIErrorComponent(openApi: any) {
112
113
  });
113
114
  }
114
115
 
115
- export function getOpenApiMiddleware<T>(model: Model<T>, options: Partial<ModelRouterOptions<T>>) {
116
+ export function getOpenApiMiddleware<T>(
117
+ model: Model<T>,
118
+ options: Partial<ModelRouterOptions<T>>
119
+ ): express.RequestHandler {
116
120
  createAPIErrorComponent(options.openApi);
117
121
  if (!options.openApi?.path) {
118
122
  // Just log this once rather than for each middleware.
@@ -154,7 +158,10 @@ export function getOpenApiMiddleware<T>(model: Model<T>, options: Partial<ModelR
154
158
  );
155
159
  }
156
160
 
157
- export function listOpenApiMiddleware<T>(model: Model<T>, options: Partial<ModelRouterOptions<T>>) {
161
+ export function listOpenApiMiddleware<T>(
162
+ model: Model<T>,
163
+ options: Partial<ModelRouterOptions<T>>
164
+ ): express.RequestHandler {
158
165
  if (!options.openApi?.path) {
159
166
  return noop;
160
167
  }
@@ -320,7 +327,7 @@ export function listOpenApiMiddleware<T>(model: Model<T>, options: Partial<Model
320
327
  export function createOpenApiMiddleware<T>(
321
328
  model: Model<T>,
322
329
  options: Partial<ModelRouterOptions<T>>
323
- ) {
330
+ ): express.RequestHandler {
324
331
  if (!options.openApi?.path) {
325
332
  return noop;
326
333
  }
@@ -372,7 +379,7 @@ export function createOpenApiMiddleware<T>(
372
379
  export function patchOpenApiMiddleware<T>(
373
380
  model: Model<T>,
374
381
  options: Partial<ModelRouterOptions<T>>
375
- ) {
382
+ ): express.RequestHandler {
376
383
  if (!options.openApi?.path) {
377
384
  return noop;
378
385
  }
@@ -424,7 +431,7 @@ export function patchOpenApiMiddleware<T>(
424
431
  export function deleteOpenApiMiddleware<T>(
425
432
  model: Model<T>,
426
433
  options: Partial<ModelRouterOptions<T>>
427
- ) {
434
+ ): express.RequestHandler {
428
435
  if (!options.openApi?.path) {
429
436
  return noop;
430
437
  }
@@ -283,7 +283,7 @@ export interface OpenApiBuildResult {
283
283
  */
284
284
  export class OpenApiMiddlewareBuilder {
285
285
  /** Router options containing OpenAPI configuration */
286
- private options: Partial<ModelRouterOptions<any>>;
286
+ private options: Partial<ModelRouterOptions<unknown>>;
287
287
 
288
288
  /** Accumulated OpenAPI configuration from builder methods */
289
289
  private config: OpenApiConfig;
@@ -302,7 +302,7 @@ export class OpenApiMiddlewareBuilder {
302
302
  *
303
303
  * @param options - Router options containing the OpenAPI path configuration
304
304
  */
305
- constructor(options: Partial<ModelRouterOptions<any>>) {
305
+ constructor(options: Partial<ModelRouterOptions<unknown>>) {
306
306
  this.options = options;
307
307
  this.config = {
308
308
  responses: {},
@@ -803,7 +803,7 @@ export class OpenApiMiddlewareBuilder {
803
803
  * ```
804
804
  */
805
805
  export function createOpenApiBuilder(
806
- options: Partial<ModelRouterOptions<any>>
806
+ options: Partial<ModelRouterOptions<unknown>>
807
807
  ): OpenApiMiddlewareBuilder {
808
808
  return new OpenApiMiddlewareBuilder(options);
809
809
  }
@@ -0,0 +1,101 @@
1
+ import {describe, expect, it, mock} from "bun:test";
2
+ import crypto from "node:crypto";
3
+ import type {NextFunction, Request, Response} from "express";
4
+
5
+ import {openApiEtagMiddleware} from "./openApiEtag";
6
+
7
+ interface BuildRequestOptions {
8
+ ifNoneMatch?: string;
9
+ method?: string;
10
+ path?: string;
11
+ }
12
+
13
+ const buildRequest = (options: BuildRequestOptions = {}): Request => {
14
+ const {ifNoneMatch, method = "GET", path = "/openapi.json"} = options;
15
+ return {
16
+ get: (header: string) => {
17
+ return header === "If-None-Match" ? ifNoneMatch : undefined;
18
+ },
19
+ method,
20
+ path,
21
+ } as Request;
22
+ };
23
+
24
+ const buildResponse = (): {
25
+ originalJson: ReturnType<typeof mock>;
26
+ res: Response;
27
+ set: ReturnType<typeof mock>;
28
+ status: ReturnType<typeof mock>;
29
+ end: ReturnType<typeof mock>;
30
+ } => {
31
+ const originalJson = mock((body: unknown) => ({body}));
32
+ const resObject = {
33
+ json: originalJson,
34
+ } as unknown as Response & Record<string, unknown>;
35
+ const set = mock(() => resObject);
36
+ const status = mock(() => resObject);
37
+ const end = mock(() => resObject);
38
+
39
+ resObject.set = set;
40
+ resObject.status = status;
41
+ resObject.end = end;
42
+
43
+ return {
44
+ end,
45
+ originalJson,
46
+ res: resObject,
47
+ set,
48
+ status,
49
+ };
50
+ };
51
+
52
+ describe("openApiEtagMiddleware", () => {
53
+ it("skips non-openapi requests", () => {
54
+ const req = buildRequest({method: "POST", path: "/health"});
55
+ const {res, originalJson} = buildResponse();
56
+ const next = mock(() => {}) as NextFunction;
57
+
58
+ openApiEtagMiddleware(req, res, next);
59
+
60
+ expect(next).toHaveBeenCalledTimes(1);
61
+ expect(res.json).toBe(originalJson);
62
+ });
63
+
64
+ it("sets ETag and returns json body when no matching If-None-Match header is provided", () => {
65
+ const req = buildRequest();
66
+ const {res, originalJson, set, status, end} = buildResponse();
67
+ const next = mock(() => {}) as NextFunction;
68
+ const body = {openapi: "3.0.0", paths: {"/todos": {get: {}}}};
69
+
70
+ openApiEtagMiddleware(req, res, next);
71
+
72
+ const result = res.json(body) as unknown as {body: typeof body};
73
+ const expectedEtag = `"${crypto.createHash("sha256").update(JSON.stringify(body)).digest("hex").substring(0, 16)}"`;
74
+
75
+ expect(next).toHaveBeenCalledTimes(1);
76
+ expect(set).toHaveBeenCalledWith("ETag", expectedEtag);
77
+ expect(originalJson).toHaveBeenCalledWith(body);
78
+ expect(status).toHaveBeenCalledTimes(0);
79
+ expect(end).toHaveBeenCalledTimes(0);
80
+ expect(result).toEqual({body});
81
+ });
82
+
83
+ it("returns 304 when If-None-Match matches generated ETag", () => {
84
+ const body = {openapi: "3.0.0", paths: {"/users": {post: {}}}};
85
+ const etag = `"${crypto.createHash("sha256").update(JSON.stringify(body)).digest("hex").substring(0, 16)}"`;
86
+ const req = buildRequest({ifNoneMatch: etag});
87
+ const {res, originalJson, set, status, end} = buildResponse();
88
+ const next = mock(() => {}) as NextFunction;
89
+
90
+ openApiEtagMiddleware(req, res, next);
91
+
92
+ const result = res.json(body);
93
+
94
+ expect(next).toHaveBeenCalledTimes(1);
95
+ expect(set).toHaveBeenCalledWith("ETag", etag);
96
+ expect(status).toHaveBeenCalledWith(304);
97
+ expect(end).toHaveBeenCalledTimes(1);
98
+ expect(originalJson).toHaveBeenCalledTimes(0);
99
+ expect(result).toBe(res);
100
+ });
101
+ });
package/src/populate.ts CHANGED
@@ -138,8 +138,8 @@ export function getOpenApiSpecForModel(
138
138
  {
139
139
  populatePaths,
140
140
  extraModelProperties,
141
- }: {populatePaths?: PopulatePath[]; extraModelProperties?: any} = {}
142
- ): {properties: any; required: string[]} {
141
+ }: {populatePaths?: PopulatePath[]; extraModelProperties?: Record<string, unknown>} = {}
142
+ ): {properties: Record<string, unknown>; required: string[]} {
143
143
  const modelSwagger = m2s(model, {
144
144
  props: ["required", "enum"],
145
145
  });