@terreno/api 0.9.2 → 0.10.0
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/bunfig.unit.toml +3 -0
- package/dist/api.d.ts +31 -13
- package/dist/errors.js +7 -1
- package/dist/expressServer.d.ts +1 -1
- package/dist/expressServer.js +3 -9
- package/dist/expressServer.test.js +4 -7
- package/dist/openApi.d.ts +6 -5
- package/dist/openApi.test.js +5 -1
- package/dist/openApiBuilder.d.ts +2 -2
- package/dist/openApiEtag.test.d.ts +1 -0
- package/dist/openApiEtag.test.js +86 -0
- package/dist/permissions.middleware.test.d.ts +1 -0
- package/dist/permissions.middleware.test.js +341 -0
- package/dist/populate.d.ts +2 -2
- package/dist/syncConsents.js +2 -2
- package/dist/tests/bunSetup.js +27 -22
- package/package.json +4 -1
- package/src/api.ts +39 -12
- package/src/errors.ts +8 -5
- package/src/expressServer.test.ts +4 -11
- package/src/expressServer.ts +5 -9
- package/src/openApi.test.ts +5 -1
- package/src/openApi.ts +14 -7
- package/src/openApiBuilder.ts +3 -3
- package/src/openApiEtag.test.ts +112 -0
- package/src/permissions.middleware.test.ts +197 -0
- package/src/populate.ts +2 -2
- package/src/syncConsents.ts +1 -1
- package/src/tests/bunSetup.ts +14 -8
package/bunfig.unit.toml
ADDED
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/expressServer.js
CHANGED
|
@@ -341,16 +341,10 @@ function setupServer(options) {
|
|
|
341
341
|
}
|
|
342
342
|
// Convenience method to execute cronjobs with an always-running server.
|
|
343
343
|
function cronjob(name, schedule, callback) {
|
|
344
|
-
var
|
|
345
|
-
|
|
346
|
-
_cronSchedule = "0 * * * *";
|
|
347
|
-
}
|
|
348
|
-
else if (schedule === "minutely") {
|
|
349
|
-
_cronSchedule = "* * * * *";
|
|
350
|
-
}
|
|
351
|
-
logger_1.logger.info("Adding cronjob ".concat(name, ", running at: ").concat(schedule));
|
|
344
|
+
var cronSchedule = schedule === "hourly" ? "0 * * * *" : schedule === "minutely" ? "* * * * *" : schedule;
|
|
345
|
+
logger_1.logger.info("Adding cronjob ".concat(name, ", running at: ").concat(cronSchedule));
|
|
352
346
|
try {
|
|
353
|
-
new cron_1.default.CronJob(
|
|
347
|
+
new cron_1.default.CronJob(cronSchedule, callback, null, true, "America/Chicago");
|
|
354
348
|
}
|
|
355
349
|
catch (error) {
|
|
356
350
|
throw new Error("Failed to create cronjob: ".concat(error));
|
|
@@ -383,16 +383,13 @@ var tests_1 = require("./tests");
|
|
|
383
383
|
var callback = function () { };
|
|
384
384
|
(0, bun_test_1.expect)(function () { return (0, expressServer_1.cronjob)("test-invalid", "invalid-cron", callback); }).toThrow("Failed to create cronjob");
|
|
385
385
|
});
|
|
386
|
-
|
|
387
|
-
// schedule to a cron expression but then use the original schedule string.
|
|
388
|
-
// This test documents that current (buggy) behavior.
|
|
389
|
-
(0, bun_test_1.it)("hourly alias fails due to bug in implementation", function () {
|
|
386
|
+
(0, bun_test_1.it)("accepts hourly alias", function () {
|
|
390
387
|
var callback = function () { };
|
|
391
|
-
(0, bun_test_1.expect)(function () { return (0, expressServer_1.cronjob)("test-hourly-alias", "hourly", callback); }).toThrow(
|
|
388
|
+
(0, bun_test_1.expect)(function () { return (0, expressServer_1.cronjob)("test-hourly-alias", "hourly", callback); }).not.toThrow();
|
|
392
389
|
});
|
|
393
|
-
(0, bun_test_1.it)("minutely alias
|
|
390
|
+
(0, bun_test_1.it)("accepts minutely alias", function () {
|
|
394
391
|
var callback = function () { };
|
|
395
|
-
(0, bun_test_1.expect)(function () { return (0, expressServer_1.cronjob)("test-minutely-alias", "minutely", callback); }).toThrow(
|
|
392
|
+
(0, bun_test_1.expect)(function () { return (0, expressServer_1.cronjob)("test-minutely-alias", "minutely", callback); }).not.toThrow();
|
|
396
393
|
});
|
|
397
394
|
});
|
|
398
395
|
(0, bun_test_1.describe)("createRouter routePathMiddleware", function () {
|
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
|
@@ -74,6 +74,9 @@ var expressServer_1 = require("./expressServer");
|
|
|
74
74
|
var permissions_1 = require("./permissions");
|
|
75
75
|
var tests_1 = require("./tests");
|
|
76
76
|
function getMessageSummaryOpenApiMiddleware(options) {
|
|
77
|
+
if (!options.openApi) {
|
|
78
|
+
throw new Error("Expected openApi to be configured for test routes");
|
|
79
|
+
}
|
|
77
80
|
return options.openApi.path({
|
|
78
81
|
parameters: [
|
|
79
82
|
{
|
|
@@ -292,7 +295,8 @@ function addRoutes(router, options) {
|
|
|
292
295
|
}); });
|
|
293
296
|
});
|
|
294
297
|
function addRoutesPopulate(router, options) {
|
|
295
|
-
|
|
298
|
+
var _a;
|
|
299
|
+
(_a = options === null || options === void 0 ? void 0 : options.openApi) === null || _a === void 0 ? void 0 : _a.component("schemas", "LimitedUser", {
|
|
296
300
|
properties: {
|
|
297
301
|
email: {
|
|
298
302
|
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,86 @@
|
|
|
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)("skips GET requests for non-openapi.json paths", function () {
|
|
49
|
+
var req = buildRequest({ method: "GET", path: "/health" });
|
|
50
|
+
var _a = buildResponse(), res = _a.res, originalJson = _a.originalJson;
|
|
51
|
+
var next = (0, bun_test_1.mock)(function () { });
|
|
52
|
+
(0, openApiEtag_1.openApiEtagMiddleware)(req, res, next);
|
|
53
|
+
(0, bun_test_1.expect)(next).toHaveBeenCalledTimes(1);
|
|
54
|
+
(0, bun_test_1.expect)(res.json).toBe(originalJson);
|
|
55
|
+
});
|
|
56
|
+
(0, bun_test_1.it)("sets ETag and returns json body when no matching If-None-Match header is provided", function () {
|
|
57
|
+
var req = buildRequest();
|
|
58
|
+
var _a = buildResponse(), res = _a.res, originalJson = _a.originalJson, set = _a.set, status = _a.status, end = _a.end;
|
|
59
|
+
var next = (0, bun_test_1.mock)(function () { });
|
|
60
|
+
var body = { openapi: "3.0.0", paths: { "/todos": { get: {} } } };
|
|
61
|
+
(0, openApiEtag_1.openApiEtagMiddleware)(req, res, next);
|
|
62
|
+
var result = res.json(body);
|
|
63
|
+
var expectedEtag = "\"".concat(node_crypto_1.default.createHash("sha256").update(JSON.stringify(body)).digest("hex").substring(0, 16), "\"");
|
|
64
|
+
(0, bun_test_1.expect)(next).toHaveBeenCalledTimes(1);
|
|
65
|
+
(0, bun_test_1.expect)(set).toHaveBeenCalledWith("ETag", expectedEtag);
|
|
66
|
+
(0, bun_test_1.expect)(originalJson).toHaveBeenCalledWith(body);
|
|
67
|
+
(0, bun_test_1.expect)(status).toHaveBeenCalledTimes(0);
|
|
68
|
+
(0, bun_test_1.expect)(end).toHaveBeenCalledTimes(0);
|
|
69
|
+
(0, bun_test_1.expect)(result).toEqual({ body: body });
|
|
70
|
+
});
|
|
71
|
+
(0, bun_test_1.it)("returns 304 when If-None-Match matches generated ETag", function () {
|
|
72
|
+
var body = { openapi: "3.0.0", paths: { "/users": { post: {} } } };
|
|
73
|
+
var etag = "\"".concat(node_crypto_1.default.createHash("sha256").update(JSON.stringify(body)).digest("hex").substring(0, 16), "\"");
|
|
74
|
+
var req = buildRequest({ ifNoneMatch: etag });
|
|
75
|
+
var _a = buildResponse(), res = _a.res, originalJson = _a.originalJson, set = _a.set, status = _a.status, end = _a.end;
|
|
76
|
+
var next = (0, bun_test_1.mock)(function () { });
|
|
77
|
+
(0, openApiEtag_1.openApiEtagMiddleware)(req, res, next);
|
|
78
|
+
var result = res.json(body);
|
|
79
|
+
(0, bun_test_1.expect)(next).toHaveBeenCalledTimes(1);
|
|
80
|
+
(0, bun_test_1.expect)(set).toHaveBeenCalledWith("ETag", etag);
|
|
81
|
+
(0, bun_test_1.expect)(status).toHaveBeenCalledWith(304);
|
|
82
|
+
(0, bun_test_1.expect)(end).toHaveBeenCalledTimes(1);
|
|
83
|
+
(0, bun_test_1.expect)(originalJson).toHaveBeenCalledTimes(0);
|
|
84
|
+
(0, bun_test_1.expect)(result).toBe(res);
|
|
85
|
+
});
|
|
86
|
+
});
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|