@terreno/api 0.9.2 → 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 +31 -13
- package/dist/errors.js +7 -1
- package/dist/expressServer.d.ts +1 -1
- package/dist/openApi.d.ts +6 -5
- package/dist/openApi.test.js +2 -1
- package/dist/openApiBuilder.d.ts +2 -2
- package/dist/openApiEtag.test.d.ts +1 -0
- package/dist/openApiEtag.test.js +78 -0
- package/dist/populate.d.ts +2 -2
- package/package.json +4 -1
- package/src/api.ts +39 -12
- package/src/errors.ts +8 -5
- package/src/expressServer.ts +1 -1
- package/src/openApi.test.ts +2 -2
- package/src/openApi.ts +14 -7
- package/src/openApiBuilder.ts +3 -3
- package/src/openApiEtag.test.ts +101 -0
- package/src/populate.ts +2 -2
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,
|
|
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:
|
|
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:
|
|
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?:
|
|
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?:
|
|
195
|
-
list?:
|
|
196
|
-
create?:
|
|
197
|
-
update?:
|
|
198
|
-
delete?:
|
|
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?:
|
|
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
|
-
|
|
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;
|
package/dist/expressServer.d.ts
CHANGED
|
@@ -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<
|
|
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>>):
|
|
56
|
-
export declare function listOpenApiMiddleware<T>(model: Model<T>, options: Partial<ModelRouterOptions<T>>):
|
|
57
|
-
export declare function createOpenApiMiddleware<T>(model: Model<T>, options: Partial<ModelRouterOptions<T>>):
|
|
58
|
-
export declare function patchOpenApiMiddleware<T>(model: Model<T>, options: Partial<ModelRouterOptions<T>>):
|
|
59
|
-
export declare function deleteOpenApiMiddleware<T>(model: Model<T>, options: Partial<ModelRouterOptions<T>>):
|
|
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;
|
package/dist/openApi.test.js
CHANGED
|
@@ -292,7 +292,8 @@ function addRoutes(router, options) {
|
|
|
292
292
|
}); });
|
|
293
293
|
});
|
|
294
294
|
function addRoutesPopulate(router, options) {
|
|
295
|
-
|
|
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",
|
package/dist/openApiBuilder.d.ts
CHANGED
|
@@ -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<
|
|
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<
|
|
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
|
+
});
|
package/dist/populate.d.ts
CHANGED
|
@@ -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?:
|
|
10
|
+
extraModelProperties?: Record<string, unknown>;
|
|
11
11
|
}): {
|
|
12
|
-
properties:
|
|
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.
|
|
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,
|
|
114
|
-
) => Record<string,
|
|
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?:
|
|
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:
|
|
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?: (
|
|
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?:
|
|
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?:
|
|
260
|
-
list?:
|
|
261
|
-
create?:
|
|
262
|
-
update?:
|
|
263
|
-
delete?:
|
|
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?:
|
|
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
|
-
|
|
124
|
-
|
|
125
|
-
|
|
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
|
|
package/src/expressServer.ts
CHANGED
|
@@ -42,7 +42,7 @@ export function setupEnvironment(): void {
|
|
|
42
42
|
}
|
|
43
43
|
}
|
|
44
44
|
|
|
45
|
-
export type AddRoutes = (router: Router, options?: Partial<ModelRouterOptions<
|
|
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;
|
package/src/openApi.test.ts
CHANGED
|
@@ -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
|
|
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
|
|
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
|
|
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>(
|
|
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>(
|
|
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
|
}
|
package/src/openApiBuilder.ts
CHANGED
|
@@ -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<
|
|
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<
|
|
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<
|
|
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?:
|
|
142
|
-
): {properties:
|
|
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
|
});
|