@terreno/api 0.13.1 → 0.13.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.asyncHandler.test.d.ts +1 -0
- package/dist/api.asyncHandler.test.js +236 -0
- package/dist/api.js +9 -0
- package/dist/api.test.js +2 -2
- package/dist/models/versionConfig.d.ts +1 -1
- package/dist/openApiValidator.test.js +64 -0
- package/dist/transformers.d.ts +5 -5
- package/dist/transformers.js +38 -37
- package/package.json +1 -1
- package/src/api.asyncHandler.test.ts +177 -0
- package/src/api.test.ts +2 -2
- package/src/api.ts +9 -0
- package/src/models/versionConfig.ts +1 -1
- package/src/openApiEtag.ts +1 -1
- package/src/openApiValidator.test.ts +58 -0
- package/src/transformers.ts +32 -30
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,236 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) {
|
|
3
|
+
function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); }
|
|
4
|
+
return new (P || (P = Promise))(function (resolve, reject) {
|
|
5
|
+
function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } }
|
|
6
|
+
function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } }
|
|
7
|
+
function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); }
|
|
8
|
+
step((generator = generator.apply(thisArg, _arguments || [])).next());
|
|
9
|
+
});
|
|
10
|
+
};
|
|
11
|
+
var __generator = (this && this.__generator) || function (thisArg, body) {
|
|
12
|
+
var _ = { label: 0, sent: function() { if (t[0] & 1) throw t[1]; return t[1]; }, trys: [], ops: [] }, f, y, t, g = Object.create((typeof Iterator === "function" ? Iterator : Object).prototype);
|
|
13
|
+
return g.next = verb(0), g["throw"] = verb(1), g["return"] = verb(2), typeof Symbol === "function" && (g[Symbol.iterator] = function() { return this; }), g;
|
|
14
|
+
function verb(n) { return function (v) { return step([n, v]); }; }
|
|
15
|
+
function step(op) {
|
|
16
|
+
if (f) throw new TypeError("Generator is already executing.");
|
|
17
|
+
while (g && (g = 0, op[0] && (_ = 0)), _) try {
|
|
18
|
+
if (f = 1, y && (t = op[0] & 2 ? y["return"] : op[0] ? y["throw"] || ((t = y["return"]) && t.call(y), 0) : y.next) && !(t = t.call(y, op[1])).done) return t;
|
|
19
|
+
if (y = 0, t) op = [op[0] & 2, t.value];
|
|
20
|
+
switch (op[0]) {
|
|
21
|
+
case 0: case 1: t = op; break;
|
|
22
|
+
case 4: _.label++; return { value: op[1], done: false };
|
|
23
|
+
case 5: _.label++; y = op[1]; op = [0]; continue;
|
|
24
|
+
case 7: op = _.ops.pop(); _.trys.pop(); continue;
|
|
25
|
+
default:
|
|
26
|
+
if (!(t = _.trys, t = t.length > 0 && t[t.length - 1]) && (op[0] === 6 || op[0] === 2)) { _ = 0; continue; }
|
|
27
|
+
if (op[0] === 3 && (!t || (op[1] > t[0] && op[1] < t[3]))) { _.label = op[1]; break; }
|
|
28
|
+
if (op[0] === 6 && _.label < t[1]) { _.label = t[1]; t = op; break; }
|
|
29
|
+
if (t && _.label < t[2]) { _.label = t[2]; _.ops.push(op); break; }
|
|
30
|
+
if (t[2]) _.ops.pop();
|
|
31
|
+
_.trys.pop(); continue;
|
|
32
|
+
}
|
|
33
|
+
op = body.call(thisArg, _);
|
|
34
|
+
} catch (e) { op = [6, e]; y = 0; } finally { f = t = 0; }
|
|
35
|
+
if (op[0] & 5) throw op[1]; return { value: op[0] ? op[1] : void 0, done: true };
|
|
36
|
+
}
|
|
37
|
+
};
|
|
38
|
+
var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
39
|
+
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
40
|
+
};
|
|
41
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
42
|
+
var bun_test_1 = require("bun:test");
|
|
43
|
+
var express_1 = __importDefault(require("express"));
|
|
44
|
+
var supertest_1 = __importDefault(require("supertest"));
|
|
45
|
+
var api_1 = require("./api");
|
|
46
|
+
var openApiValidator_1 = require("./openApiValidator");
|
|
47
|
+
(0, bun_test_1.afterEach)(function () {
|
|
48
|
+
(0, openApiValidator_1.resetOpenApiValidatorConfig)();
|
|
49
|
+
});
|
|
50
|
+
var createApp = function () {
|
|
51
|
+
var app = (0, express_1.default)();
|
|
52
|
+
app.use(express_1.default.json());
|
|
53
|
+
return app;
|
|
54
|
+
};
|
|
55
|
+
var errorHandler = function (err, _req, res, _next) {
|
|
56
|
+
res.status(err.status || 500).json({ error: err.title || err.message });
|
|
57
|
+
};
|
|
58
|
+
(0, bun_test_1.describe)("asyncHandler with bodySchema validation", function () {
|
|
59
|
+
(0, bun_test_1.it)("validates and accepts a conforming body", function () { return __awaiter(void 0, void 0, void 0, function () {
|
|
60
|
+
var app, res;
|
|
61
|
+
return __generator(this, function (_a) {
|
|
62
|
+
switch (_a.label) {
|
|
63
|
+
case 0:
|
|
64
|
+
(0, openApiValidator_1.configureOpenApiValidator)({});
|
|
65
|
+
app = createApp();
|
|
66
|
+
app.post("/test", (0, api_1.asyncHandler)(function (_req, res) { return __awaiter(void 0, void 0, void 0, function () {
|
|
67
|
+
return __generator(this, function (_a) {
|
|
68
|
+
res.json({ ok: true });
|
|
69
|
+
return [2 /*return*/];
|
|
70
|
+
});
|
|
71
|
+
}); }, {
|
|
72
|
+
bodySchema: { name: { required: true, type: "string" } },
|
|
73
|
+
validate: true,
|
|
74
|
+
}));
|
|
75
|
+
app.use(errorHandler);
|
|
76
|
+
return [4 /*yield*/, (0, supertest_1.default)(app).post("/test").send({ name: "hello" }).expect(200)];
|
|
77
|
+
case 1:
|
|
78
|
+
res = _a.sent();
|
|
79
|
+
(0, bun_test_1.expect)(res.body.ok).toBe(true);
|
|
80
|
+
return [2 /*return*/];
|
|
81
|
+
}
|
|
82
|
+
});
|
|
83
|
+
}); });
|
|
84
|
+
(0, bun_test_1.it)("rejects a body missing a required field", function () { return __awaiter(void 0, void 0, void 0, function () {
|
|
85
|
+
var app;
|
|
86
|
+
return __generator(this, function (_a) {
|
|
87
|
+
switch (_a.label) {
|
|
88
|
+
case 0:
|
|
89
|
+
(0, openApiValidator_1.configureOpenApiValidator)({});
|
|
90
|
+
app = createApp();
|
|
91
|
+
app.post("/test", (0, api_1.asyncHandler)(function (_req, res) { return __awaiter(void 0, void 0, void 0, function () {
|
|
92
|
+
return __generator(this, function (_a) {
|
|
93
|
+
res.json({ ok: true });
|
|
94
|
+
return [2 /*return*/];
|
|
95
|
+
});
|
|
96
|
+
}); }, {
|
|
97
|
+
bodySchema: { name: { required: true, type: "string" } },
|
|
98
|
+
validate: true,
|
|
99
|
+
}));
|
|
100
|
+
app.use(errorHandler);
|
|
101
|
+
return [4 /*yield*/, (0, supertest_1.default)(app).post("/test").send({}).expect(400)];
|
|
102
|
+
case 1:
|
|
103
|
+
_a.sent();
|
|
104
|
+
return [2 /*return*/];
|
|
105
|
+
}
|
|
106
|
+
});
|
|
107
|
+
}); });
|
|
108
|
+
(0, bun_test_1.it)("skips body validation when validate is false", function () { return __awaiter(void 0, void 0, void 0, function () {
|
|
109
|
+
var app, res;
|
|
110
|
+
return __generator(this, function (_a) {
|
|
111
|
+
switch (_a.label) {
|
|
112
|
+
case 0:
|
|
113
|
+
(0, openApiValidator_1.configureOpenApiValidator)({});
|
|
114
|
+
app = createApp();
|
|
115
|
+
app.post("/test", (0, api_1.asyncHandler)(function (_req, res) { return __awaiter(void 0, void 0, void 0, function () {
|
|
116
|
+
return __generator(this, function (_a) {
|
|
117
|
+
res.json({ ok: true });
|
|
118
|
+
return [2 /*return*/];
|
|
119
|
+
});
|
|
120
|
+
}); }, {
|
|
121
|
+
bodySchema: { name: { required: true, type: "string" } },
|
|
122
|
+
validate: false,
|
|
123
|
+
}));
|
|
124
|
+
app.use(errorHandler);
|
|
125
|
+
return [4 /*yield*/, (0, supertest_1.default)(app).post("/test").send({}).expect(200)];
|
|
126
|
+
case 1:
|
|
127
|
+
res = _a.sent();
|
|
128
|
+
(0, bun_test_1.expect)(res.body.ok).toBe(true);
|
|
129
|
+
return [2 /*return*/];
|
|
130
|
+
}
|
|
131
|
+
});
|
|
132
|
+
}); });
|
|
133
|
+
});
|
|
134
|
+
(0, bun_test_1.describe)("asyncHandler with querySchema validation", function () {
|
|
135
|
+
(0, bun_test_1.it)("validates and accepts conforming query params", function () { return __awaiter(void 0, void 0, void 0, function () {
|
|
136
|
+
var app, res;
|
|
137
|
+
return __generator(this, function (_a) {
|
|
138
|
+
switch (_a.label) {
|
|
139
|
+
case 0:
|
|
140
|
+
(0, openApiValidator_1.configureOpenApiValidator)({});
|
|
141
|
+
app = createApp();
|
|
142
|
+
app.get("/test", (0, api_1.asyncHandler)(function (_req, res) { return __awaiter(void 0, void 0, void 0, function () {
|
|
143
|
+
return __generator(this, function (_a) {
|
|
144
|
+
res.json({ ok: true });
|
|
145
|
+
return [2 /*return*/];
|
|
146
|
+
});
|
|
147
|
+
}); }, {
|
|
148
|
+
querySchema: { page: { type: "integer" } },
|
|
149
|
+
validate: true,
|
|
150
|
+
}));
|
|
151
|
+
app.use(errorHandler);
|
|
152
|
+
return [4 /*yield*/, (0, supertest_1.default)(app).get("/test?page=1").expect(200)];
|
|
153
|
+
case 1:
|
|
154
|
+
res = _a.sent();
|
|
155
|
+
(0, bun_test_1.expect)(res.body.ok).toBe(true);
|
|
156
|
+
return [2 /*return*/];
|
|
157
|
+
}
|
|
158
|
+
});
|
|
159
|
+
}); });
|
|
160
|
+
(0, bun_test_1.it)("rejects invalid query params", function () { return __awaiter(void 0, void 0, void 0, function () {
|
|
161
|
+
var app;
|
|
162
|
+
return __generator(this, function (_a) {
|
|
163
|
+
switch (_a.label) {
|
|
164
|
+
case 0:
|
|
165
|
+
(0, openApiValidator_1.configureOpenApiValidator)({});
|
|
166
|
+
app = createApp();
|
|
167
|
+
app.get("/test", (0, api_1.asyncHandler)(function (_req, res) { return __awaiter(void 0, void 0, void 0, function () {
|
|
168
|
+
return __generator(this, function (_a) {
|
|
169
|
+
res.json({ ok: true });
|
|
170
|
+
return [2 /*return*/];
|
|
171
|
+
});
|
|
172
|
+
}); }, {
|
|
173
|
+
querySchema: { page: { required: true, type: "integer" } },
|
|
174
|
+
validate: true,
|
|
175
|
+
}));
|
|
176
|
+
app.use(errorHandler);
|
|
177
|
+
return [4 /*yield*/, (0, supertest_1.default)(app).get("/test").expect(400)];
|
|
178
|
+
case 1:
|
|
179
|
+
_a.sent();
|
|
180
|
+
return [2 /*return*/];
|
|
181
|
+
}
|
|
182
|
+
});
|
|
183
|
+
}); });
|
|
184
|
+
});
|
|
185
|
+
(0, bun_test_1.describe)("asyncHandler with both schemas", function () {
|
|
186
|
+
(0, bun_test_1.it)("runs both body and query validators sequentially", function () { return __awaiter(void 0, void 0, void 0, function () {
|
|
187
|
+
var app, res;
|
|
188
|
+
return __generator(this, function (_a) {
|
|
189
|
+
switch (_a.label) {
|
|
190
|
+
case 0:
|
|
191
|
+
(0, openApiValidator_1.configureOpenApiValidator)({});
|
|
192
|
+
app = createApp();
|
|
193
|
+
app.post("/test", (0, api_1.asyncHandler)(function (_req, res) { return __awaiter(void 0, void 0, void 0, function () {
|
|
194
|
+
return __generator(this, function (_a) {
|
|
195
|
+
res.json({ ok: true });
|
|
196
|
+
return [2 /*return*/];
|
|
197
|
+
});
|
|
198
|
+
}); }, {
|
|
199
|
+
bodySchema: { name: { required: true, type: "string" } },
|
|
200
|
+
querySchema: { page: { type: "integer" } },
|
|
201
|
+
validate: true,
|
|
202
|
+
}));
|
|
203
|
+
app.use(errorHandler);
|
|
204
|
+
return [4 /*yield*/, (0, supertest_1.default)(app).post("/test?page=1").send({ name: "hi" }).expect(200)];
|
|
205
|
+
case 1:
|
|
206
|
+
res = _a.sent();
|
|
207
|
+
(0, bun_test_1.expect)(res.body.ok).toBe(true);
|
|
208
|
+
return [2 /*return*/];
|
|
209
|
+
}
|
|
210
|
+
});
|
|
211
|
+
}); });
|
|
212
|
+
(0, bun_test_1.it)("forwards handler errors through next", function () { return __awaiter(void 0, void 0, void 0, function () {
|
|
213
|
+
var app, res;
|
|
214
|
+
return __generator(this, function (_a) {
|
|
215
|
+
switch (_a.label) {
|
|
216
|
+
case 0:
|
|
217
|
+
(0, openApiValidator_1.configureOpenApiValidator)({});
|
|
218
|
+
app = createApp();
|
|
219
|
+
app.post("/test", (0, api_1.asyncHandler)(function () { return __awaiter(void 0, void 0, void 0, function () {
|
|
220
|
+
return __generator(this, function (_a) {
|
|
221
|
+
throw new Error("handler boom");
|
|
222
|
+
});
|
|
223
|
+
}); }, {
|
|
224
|
+
bodySchema: { name: { type: "string" } },
|
|
225
|
+
validate: true,
|
|
226
|
+
}));
|
|
227
|
+
app.use(errorHandler);
|
|
228
|
+
return [4 /*yield*/, (0, supertest_1.default)(app).post("/test").send({ name: "ok" }).expect(500)];
|
|
229
|
+
case 1:
|
|
230
|
+
res = _a.sent();
|
|
231
|
+
(0, bun_test_1.expect)(res.body.error).toBe("handler boom");
|
|
232
|
+
return [2 /*return*/];
|
|
233
|
+
}
|
|
234
|
+
});
|
|
235
|
+
}); });
|
|
236
|
+
});
|
package/dist/api.js
CHANGED
|
@@ -335,6 +335,9 @@ function _buildModelRouter(model, options) {
|
|
|
335
335
|
body = (0, transformers_1.transform)(options, req.body, "create", req.user);
|
|
336
336
|
}
|
|
337
337
|
catch (error) {
|
|
338
|
+
if ((0, errors_1.isAPIError)(error)) {
|
|
339
|
+
throw error;
|
|
340
|
+
}
|
|
338
341
|
throw new errors_1.APIError({
|
|
339
342
|
disableExternalErrorTracking: (0, errors_1.getDisableExternalErrorTracking)(error),
|
|
340
343
|
error: error,
|
|
@@ -687,6 +690,9 @@ function _buildModelRouter(model, options) {
|
|
|
687
690
|
body = (0, transformers_1.transform)(options, req.body, "update", req.user);
|
|
688
691
|
}
|
|
689
692
|
catch (error) {
|
|
693
|
+
if ((0, errors_1.isAPIError)(error)) {
|
|
694
|
+
throw error;
|
|
695
|
+
}
|
|
690
696
|
throw new errors_1.APIError({
|
|
691
697
|
disableExternalErrorTracking: (0, errors_1.getDisableExternalErrorTracking)(error),
|
|
692
698
|
error: error,
|
|
@@ -968,6 +974,9 @@ function _buildModelRouter(model, options) {
|
|
|
968
974
|
body = (0, transformers_1.transform)(options, body, "update", req.user);
|
|
969
975
|
}
|
|
970
976
|
catch (error) {
|
|
977
|
+
if ((0, errors_1.isAPIError)(error)) {
|
|
978
|
+
throw error;
|
|
979
|
+
}
|
|
971
980
|
throw new errors_1.APIError({
|
|
972
981
|
disableExternalErrorTracking: (0, errors_1.getDisableExternalErrorTracking)(error),
|
|
973
982
|
error: error,
|
package/dist/api.test.js
CHANGED
|
@@ -774,7 +774,7 @@ var transformers_1 = require("./transformers");
|
|
|
774
774
|
}),
|
|
775
775
|
}));
|
|
776
776
|
server = (0, supertest_1.default)(app);
|
|
777
|
-
return [4 /*yield*/, server.post("/food").send({ calories: 15, name: "Broccoli" }).expect(
|
|
777
|
+
return [4 /*yield*/, server.post("/food").send({ calories: 15, name: "Broccoli" }).expect(403)];
|
|
778
778
|
case 1:
|
|
779
779
|
res = _a.sent();
|
|
780
780
|
(0, bun_test_1.expect)(res.body.title).toContain("cannot write fields");
|
|
@@ -1699,7 +1699,7 @@ var transformers_1 = require("./transformers");
|
|
|
1699
1699
|
}),
|
|
1700
1700
|
}));
|
|
1701
1701
|
server = (0, supertest_1.default)(app);
|
|
1702
|
-
return [4 /*yield*/, server.post("/food").send({ calories: 15, name: "Broccoli" }).expect(
|
|
1702
|
+
return [4 /*yield*/, server.post("/food").send({ calories: 15, name: "Broccoli" }).expect(403)];
|
|
1703
1703
|
case 1:
|
|
1704
1704
|
res = _a.sent();
|
|
1705
1705
|
(0, bun_test_1.expect)(res.body.title).toContain("cannot write fields");
|
|
@@ -12,6 +12,6 @@ export interface VersionConfigDocument extends mongoose.Document {
|
|
|
12
12
|
updated?: Date;
|
|
13
13
|
}
|
|
14
14
|
export interface VersionConfigModel extends mongoose.Model<VersionConfigDocument> {
|
|
15
|
-
findOneOrNone(query: Record<string,
|
|
15
|
+
findOneOrNone(query: Record<string, unknown>, errorArgs?: Partial<APIErrorConstructor>): Promise<(Document & VersionConfigDocument) | null>;
|
|
16
16
|
}
|
|
17
17
|
export declare const VersionConfig: VersionConfigModel;
|
|
@@ -262,6 +262,70 @@ var setupFreshApp = function () { return __awaiter(void 0, void 0, void 0, funct
|
|
|
262
262
|
});
|
|
263
263
|
}); });
|
|
264
264
|
});
|
|
265
|
+
(0, bun_test_1.describe)("per-route validation: object-form options", function () {
|
|
266
|
+
(0, bun_test_1.it)("applies object validation options with per-operation control", function () { return __awaiter(void 0, void 0, void 0, function () {
|
|
267
|
+
var removedProps, errorsCaught, freshApp, admin, res;
|
|
268
|
+
return __generator(this, function (_a) {
|
|
269
|
+
switch (_a.label) {
|
|
270
|
+
case 0:
|
|
271
|
+
(0, openApiValidator_1.configureOpenApiValidator)({ removeAdditional: true });
|
|
272
|
+
removedProps = [];
|
|
273
|
+
errorsCaught = [];
|
|
274
|
+
return [4 /*yield*/, setupFreshApp()];
|
|
275
|
+
case 1:
|
|
276
|
+
freshApp = _a.sent();
|
|
277
|
+
freshApp.use("/required", (0, api_1.modelRouter)(tests_1.RequiredModel, __assign(__assign({}, requiredRouterOptions), { validation: {
|
|
278
|
+
excludeFromCreate: ["about"],
|
|
279
|
+
onAdditionalPropertiesRemoved: function (props) {
|
|
280
|
+
removedProps.push.apply(removedProps, __spreadArray([], __read(props), false));
|
|
281
|
+
},
|
|
282
|
+
onError: function (errors) {
|
|
283
|
+
errorsCaught.push(errors);
|
|
284
|
+
},
|
|
285
|
+
validateCreate: true,
|
|
286
|
+
validateQuery: true,
|
|
287
|
+
validateUpdate: true,
|
|
288
|
+
} })));
|
|
289
|
+
return [4 /*yield*/, (0, tests_1.authAsUser)(freshApp, "admin")];
|
|
290
|
+
case 2:
|
|
291
|
+
admin = _a.sent();
|
|
292
|
+
return [4 /*yield*/, admin.post("/required").send({ name: "Validated" }).expect(201)];
|
|
293
|
+
case 3:
|
|
294
|
+
res = _a.sent();
|
|
295
|
+
(0, bun_test_1.expect)(res.body.data.name).toBe("Validated");
|
|
296
|
+
return [2 /*return*/];
|
|
297
|
+
}
|
|
298
|
+
});
|
|
299
|
+
}); });
|
|
300
|
+
(0, bun_test_1.it)("disables create validation when validateCreate is false", function () { return __awaiter(void 0, void 0, void 0, function () {
|
|
301
|
+
var freshApp, admin, res;
|
|
302
|
+
return __generator(this, function (_a) {
|
|
303
|
+
switch (_a.label) {
|
|
304
|
+
case 0:
|
|
305
|
+
(0, openApiValidator_1.configureOpenApiValidator)({ removeAdditional: true });
|
|
306
|
+
return [4 /*yield*/, setupFreshApp()];
|
|
307
|
+
case 1:
|
|
308
|
+
freshApp = _a.sent();
|
|
309
|
+
freshApp.use("/required", (0, api_1.modelRouter)(tests_1.RequiredModel, __assign(__assign({}, requiredRouterOptions), { validation: {
|
|
310
|
+
excludeFromUpdate: ["about"],
|
|
311
|
+
validateCreate: false,
|
|
312
|
+
validateUpdate: true,
|
|
313
|
+
} })));
|
|
314
|
+
return [4 /*yield*/, (0, tests_1.authAsUser)(freshApp, "admin")];
|
|
315
|
+
case 2:
|
|
316
|
+
admin = _a.sent();
|
|
317
|
+
return [4 /*yield*/, admin
|
|
318
|
+
.post("/required")
|
|
319
|
+
.send({ extraField: "ignored", name: "NoCreateValidation" })
|
|
320
|
+
.expect(201)];
|
|
321
|
+
case 3:
|
|
322
|
+
res = _a.sent();
|
|
323
|
+
(0, bun_test_1.expect)(res.body.data.name).toBe("NoCreateValidation");
|
|
324
|
+
return [2 /*return*/];
|
|
325
|
+
}
|
|
326
|
+
});
|
|
327
|
+
}); });
|
|
328
|
+
});
|
|
265
329
|
(0, bun_test_1.describe)("sanitization of non-standard mongoose-to-swagger types", function () {
|
|
266
330
|
(0, bun_test_1.it)("validates models with ObjectId and DateOnly fields after sanitization", function () { return __awaiter(void 0, void 0, void 0, function () {
|
|
267
331
|
var freshApp, admin, res;
|
package/dist/transformers.d.ts
CHANGED
|
@@ -6,7 +6,7 @@ export interface TerrenoTransformer<T> {
|
|
|
6
6
|
transform?: (obj: Partial<T>, method: "create" | "update", user?: User) => Partial<T> | undefined;
|
|
7
7
|
serialize?: (obj: T, user?: User) => Partial<T> | undefined;
|
|
8
8
|
}
|
|
9
|
-
export declare
|
|
9
|
+
export declare const AdminOwnerTransformer: <T>(options: {
|
|
10
10
|
anonReadFields?: string[];
|
|
11
11
|
authReadFields?: string[];
|
|
12
12
|
ownerReadFields?: string[];
|
|
@@ -15,11 +15,11 @@ export declare function AdminOwnerTransformer<T>(options: {
|
|
|
15
15
|
authWriteFields?: string[];
|
|
16
16
|
ownerWriteFields?: string[];
|
|
17
17
|
adminWriteFields?: string[];
|
|
18
|
-
})
|
|
19
|
-
export declare
|
|
20
|
-
export declare
|
|
18
|
+
}) => TerrenoTransformer<T>;
|
|
19
|
+
export declare const transform: <T>(options: ModelRouterOptions<T>, data: Partial<T> | Partial<T>[], method: "create" | "update", user?: User) => Partial<T> | (Partial<T> | undefined)[] | undefined;
|
|
20
|
+
export declare const serialize: <T>(req: express.Request, options: ModelRouterOptions<T>, data: (Document & T) | (Document & T)[]) => Partial<T> | (Partial<T> | undefined)[] | undefined;
|
|
21
21
|
/**
|
|
22
22
|
* Default response handler for modelRouter. Calls toObject on each doc and returns the result,
|
|
23
23
|
* using transformers.serializer if provided.
|
|
24
24
|
*/
|
|
25
|
-
export declare
|
|
25
|
+
export declare const defaultResponseHandler: <T>(doc: (Document & T) | (Document & T)[] | null, method: "list" | "create" | "read" | "update", request: express.Request, options: ModelRouterOptions<T>) => Promise<Partial<T> | (Partial<T> | undefined)[] | null | undefined>;
|
package/dist/transformers.js
CHANGED
|
@@ -72,13 +72,10 @@ var __spreadArray = (this && this.__spreadArray) || function (to, from, pack) {
|
|
|
72
72
|
return to.concat(ar || Array.prototype.slice.call(from));
|
|
73
73
|
};
|
|
74
74
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
75
|
-
exports.AdminOwnerTransformer =
|
|
76
|
-
exports.transform = transform;
|
|
77
|
-
exports.serialize = serialize;
|
|
78
|
-
exports.defaultResponseHandler = defaultResponseHandler;
|
|
75
|
+
exports.defaultResponseHandler = exports.serialize = exports.transform = exports.AdminOwnerTransformer = void 0;
|
|
79
76
|
var errors_1 = require("./errors");
|
|
80
77
|
var logger_1 = require("./logger");
|
|
81
|
-
function
|
|
78
|
+
var getUserType = function (user, obj) {
|
|
82
79
|
if (user === null || user === void 0 ? void 0 : user.admin) {
|
|
83
80
|
return "admin";
|
|
84
81
|
}
|
|
@@ -89,9 +86,9 @@ function getUserType(user, obj) {
|
|
|
89
86
|
return "auth";
|
|
90
87
|
}
|
|
91
88
|
return "anon";
|
|
92
|
-
}
|
|
93
|
-
function
|
|
94
|
-
function
|
|
89
|
+
};
|
|
90
|
+
var AdminOwnerTransformer = function (options) {
|
|
91
|
+
var pickFields = function (obj, fields) {
|
|
95
92
|
var e_1, _a;
|
|
96
93
|
var newData = {};
|
|
97
94
|
try {
|
|
@@ -110,7 +107,7 @@ function AdminOwnerTransformer(options) {
|
|
|
110
107
|
finally { if (e_1) throw e_1.error; }
|
|
111
108
|
}
|
|
112
109
|
return newData;
|
|
113
|
-
}
|
|
110
|
+
};
|
|
114
111
|
return {
|
|
115
112
|
serialize: function (obj, user) {
|
|
116
113
|
var _a, _b, _c, _d;
|
|
@@ -126,7 +123,6 @@ function AdminOwnerTransformer(options) {
|
|
|
126
123
|
}
|
|
127
124
|
return pickFields(obj, __spreadArray(__spreadArray([], __read(((_d = options.anonReadFields) !== null && _d !== void 0 ? _d : [])), false), ["id"], false));
|
|
128
125
|
},
|
|
129
|
-
// TODO: Migrate AdminOwnerTransform to use pre-hooks.
|
|
130
126
|
transform: function (obj, _method, user) {
|
|
131
127
|
var _a, _b, _c, _d;
|
|
132
128
|
var userType = getUserType(user, obj);
|
|
@@ -145,13 +141,17 @@ function AdminOwnerTransformer(options) {
|
|
|
145
141
|
}
|
|
146
142
|
var unallowedFields = Object.keys(obj).filter(function (k) { return !allowedFields.includes(k); });
|
|
147
143
|
if (unallowedFields.length) {
|
|
148
|
-
throw new
|
|
144
|
+
throw new errors_1.APIError({
|
|
145
|
+
status: 403,
|
|
146
|
+
title: "User of type ".concat(userType, " cannot write fields: ").concat(unallowedFields.join(", ")),
|
|
147
|
+
});
|
|
149
148
|
}
|
|
150
149
|
return obj;
|
|
151
150
|
},
|
|
152
151
|
};
|
|
153
|
-
}
|
|
154
|
-
|
|
152
|
+
};
|
|
153
|
+
exports.AdminOwnerTransformer = AdminOwnerTransformer;
|
|
154
|
+
var transform = function (options, data, method, user) {
|
|
155
155
|
var _a, _b;
|
|
156
156
|
if (!((_a = options.transformer) === null || _a === void 0 ? void 0 : _a.transform)) {
|
|
157
157
|
return data;
|
|
@@ -163,8 +163,9 @@ function transform(options, data, method, user) {
|
|
|
163
163
|
return transformFn(data, method, user);
|
|
164
164
|
}
|
|
165
165
|
return data.map(function (d) { return transformFn(d, method, user); });
|
|
166
|
-
}
|
|
167
|
-
|
|
166
|
+
};
|
|
167
|
+
exports.transform = transform;
|
|
168
|
+
var serialize = function (req, options, data) {
|
|
168
169
|
var _a;
|
|
169
170
|
var serializeFn = function (serializeData, serializeUser) {
|
|
170
171
|
var _a, _b;
|
|
@@ -190,30 +191,30 @@ function serialize(req, options, data) {
|
|
|
190
191
|
return serializeFn(data, req.user);
|
|
191
192
|
}
|
|
192
193
|
return data.map(function (d) { return serializeFn(d, req.user); });
|
|
193
|
-
}
|
|
194
|
+
};
|
|
195
|
+
exports.serialize = serialize;
|
|
194
196
|
/**
|
|
195
197
|
* Default response handler for modelRouter. Calls toObject on each doc and returns the result,
|
|
196
198
|
* using transformers.serializer if provided.
|
|
197
199
|
*/
|
|
198
|
-
function
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
return [2 /*return*/];
|
|
217
|
-
});
|
|
200
|
+
var defaultResponseHandler = function (doc, method, request, options) { return __awaiter(void 0, void 0, void 0, function () {
|
|
201
|
+
var errorObj;
|
|
202
|
+
return __generator(this, function (_a) {
|
|
203
|
+
if (!doc) {
|
|
204
|
+
return [2 /*return*/, null];
|
|
205
|
+
}
|
|
206
|
+
try {
|
|
207
|
+
return [2 /*return*/, (0, exports.serialize)(request, options, doc)];
|
|
208
|
+
}
|
|
209
|
+
catch (error) {
|
|
210
|
+
errorObj = error;
|
|
211
|
+
throw new errors_1.APIError({
|
|
212
|
+
error: errorObj,
|
|
213
|
+
status: 400,
|
|
214
|
+
title: "Error serializing ".concat(method, " response: ").concat(errorObj.message),
|
|
215
|
+
});
|
|
216
|
+
}
|
|
217
|
+
return [2 /*return*/];
|
|
218
218
|
});
|
|
219
|
-
}
|
|
219
|
+
}); };
|
|
220
|
+
exports.defaultResponseHandler = defaultResponseHandler;
|
package/package.json
CHANGED
|
@@ -0,0 +1,177 @@
|
|
|
1
|
+
import {afterEach, describe, expect, it} from "bun:test";
|
|
2
|
+
import express, {type NextFunction, type Request, type Response} from "express";
|
|
3
|
+
import supertest from "supertest";
|
|
4
|
+
|
|
5
|
+
import {asyncHandler} from "./api";
|
|
6
|
+
import {configureOpenApiValidator, resetOpenApiValidatorConfig} from "./openApiValidator";
|
|
7
|
+
|
|
8
|
+
afterEach(() => {
|
|
9
|
+
resetOpenApiValidatorConfig();
|
|
10
|
+
});
|
|
11
|
+
|
|
12
|
+
const createApp = (): express.Application => {
|
|
13
|
+
const app = express();
|
|
14
|
+
app.use(express.json());
|
|
15
|
+
return app;
|
|
16
|
+
};
|
|
17
|
+
|
|
18
|
+
const errorHandler = (
|
|
19
|
+
err: {status?: number; title?: string; message?: string},
|
|
20
|
+
_req: Request,
|
|
21
|
+
res: Response,
|
|
22
|
+
_next: NextFunction
|
|
23
|
+
): void => {
|
|
24
|
+
res.status(err.status || 500).json({error: err.title || err.message});
|
|
25
|
+
};
|
|
26
|
+
|
|
27
|
+
describe("asyncHandler with bodySchema validation", () => {
|
|
28
|
+
it("validates and accepts a conforming body", async () => {
|
|
29
|
+
configureOpenApiValidator({});
|
|
30
|
+
const app = createApp();
|
|
31
|
+
app.post(
|
|
32
|
+
"/test",
|
|
33
|
+
asyncHandler(
|
|
34
|
+
async (_req: Request, res: Response) => {
|
|
35
|
+
res.json({ok: true});
|
|
36
|
+
},
|
|
37
|
+
{
|
|
38
|
+
bodySchema: {name: {required: true, type: "string"}},
|
|
39
|
+
validate: true,
|
|
40
|
+
}
|
|
41
|
+
)
|
|
42
|
+
);
|
|
43
|
+
app.use(errorHandler);
|
|
44
|
+
|
|
45
|
+
const res = await supertest(app).post("/test").send({name: "hello"}).expect(200);
|
|
46
|
+
expect(res.body.ok).toBe(true);
|
|
47
|
+
});
|
|
48
|
+
|
|
49
|
+
it("rejects a body missing a required field", async () => {
|
|
50
|
+
configureOpenApiValidator({});
|
|
51
|
+
const app = createApp();
|
|
52
|
+
app.post(
|
|
53
|
+
"/test",
|
|
54
|
+
asyncHandler(
|
|
55
|
+
async (_req: Request, res: Response) => {
|
|
56
|
+
res.json({ok: true});
|
|
57
|
+
},
|
|
58
|
+
{
|
|
59
|
+
bodySchema: {name: {required: true, type: "string"}},
|
|
60
|
+
validate: true,
|
|
61
|
+
}
|
|
62
|
+
)
|
|
63
|
+
);
|
|
64
|
+
app.use(errorHandler);
|
|
65
|
+
|
|
66
|
+
await supertest(app).post("/test").send({}).expect(400);
|
|
67
|
+
});
|
|
68
|
+
|
|
69
|
+
it("skips body validation when validate is false", async () => {
|
|
70
|
+
configureOpenApiValidator({});
|
|
71
|
+
const app = createApp();
|
|
72
|
+
app.post(
|
|
73
|
+
"/test",
|
|
74
|
+
asyncHandler(
|
|
75
|
+
async (_req: Request, res: Response) => {
|
|
76
|
+
res.json({ok: true});
|
|
77
|
+
},
|
|
78
|
+
{
|
|
79
|
+
bodySchema: {name: {required: true, type: "string"}},
|
|
80
|
+
validate: false,
|
|
81
|
+
}
|
|
82
|
+
)
|
|
83
|
+
);
|
|
84
|
+
app.use(errorHandler);
|
|
85
|
+
|
|
86
|
+
const res = await supertest(app).post("/test").send({}).expect(200);
|
|
87
|
+
expect(res.body.ok).toBe(true);
|
|
88
|
+
});
|
|
89
|
+
});
|
|
90
|
+
|
|
91
|
+
describe("asyncHandler with querySchema validation", () => {
|
|
92
|
+
it("validates and accepts conforming query params", async () => {
|
|
93
|
+
configureOpenApiValidator({});
|
|
94
|
+
const app = createApp();
|
|
95
|
+
app.get(
|
|
96
|
+
"/test",
|
|
97
|
+
asyncHandler(
|
|
98
|
+
async (_req: Request, res: Response) => {
|
|
99
|
+
res.json({ok: true});
|
|
100
|
+
},
|
|
101
|
+
{
|
|
102
|
+
querySchema: {page: {type: "integer"}},
|
|
103
|
+
validate: true,
|
|
104
|
+
}
|
|
105
|
+
)
|
|
106
|
+
);
|
|
107
|
+
app.use(errorHandler);
|
|
108
|
+
|
|
109
|
+
const res = await supertest(app).get("/test?page=1").expect(200);
|
|
110
|
+
expect(res.body.ok).toBe(true);
|
|
111
|
+
});
|
|
112
|
+
|
|
113
|
+
it("rejects invalid query params", async () => {
|
|
114
|
+
configureOpenApiValidator({});
|
|
115
|
+
const app = createApp();
|
|
116
|
+
app.get(
|
|
117
|
+
"/test",
|
|
118
|
+
asyncHandler(
|
|
119
|
+
async (_req: Request, res: Response) => {
|
|
120
|
+
res.json({ok: true});
|
|
121
|
+
},
|
|
122
|
+
{
|
|
123
|
+
querySchema: {page: {required: true, type: "integer"}},
|
|
124
|
+
validate: true,
|
|
125
|
+
}
|
|
126
|
+
)
|
|
127
|
+
);
|
|
128
|
+
app.use(errorHandler);
|
|
129
|
+
|
|
130
|
+
await supertest(app).get("/test").expect(400);
|
|
131
|
+
});
|
|
132
|
+
});
|
|
133
|
+
|
|
134
|
+
describe("asyncHandler with both schemas", () => {
|
|
135
|
+
it("runs both body and query validators sequentially", async () => {
|
|
136
|
+
configureOpenApiValidator({});
|
|
137
|
+
const app = createApp();
|
|
138
|
+
app.post(
|
|
139
|
+
"/test",
|
|
140
|
+
asyncHandler(
|
|
141
|
+
async (_req: Request, res: Response) => {
|
|
142
|
+
res.json({ok: true});
|
|
143
|
+
},
|
|
144
|
+
{
|
|
145
|
+
bodySchema: {name: {required: true, type: "string"}},
|
|
146
|
+
querySchema: {page: {type: "integer"}},
|
|
147
|
+
validate: true,
|
|
148
|
+
}
|
|
149
|
+
)
|
|
150
|
+
);
|
|
151
|
+
app.use(errorHandler);
|
|
152
|
+
|
|
153
|
+
const res = await supertest(app).post("/test?page=1").send({name: "hi"}).expect(200);
|
|
154
|
+
expect(res.body.ok).toBe(true);
|
|
155
|
+
});
|
|
156
|
+
|
|
157
|
+
it("forwards handler errors through next", async () => {
|
|
158
|
+
configureOpenApiValidator({});
|
|
159
|
+
const app = createApp();
|
|
160
|
+
app.post(
|
|
161
|
+
"/test",
|
|
162
|
+
asyncHandler(
|
|
163
|
+
async () => {
|
|
164
|
+
throw new Error("handler boom");
|
|
165
|
+
},
|
|
166
|
+
{
|
|
167
|
+
bodySchema: {name: {type: "string"}},
|
|
168
|
+
validate: true,
|
|
169
|
+
}
|
|
170
|
+
)
|
|
171
|
+
);
|
|
172
|
+
app.use(errorHandler);
|
|
173
|
+
|
|
174
|
+
const res = await supertest(app).post("/test").send({name: "ok"}).expect(500);
|
|
175
|
+
expect(res.body.error).toBe("handler boom");
|
|
176
|
+
});
|
|
177
|
+
});
|
package/src/api.test.ts
CHANGED
|
@@ -576,7 +576,7 @@ describe("@terreno/api", () => {
|
|
|
576
576
|
);
|
|
577
577
|
server = supertest(app);
|
|
578
578
|
|
|
579
|
-
const res = await server.post("/food").send({calories: 15, name: "Broccoli"}).expect(
|
|
579
|
+
const res = await server.post("/food").send({calories: 15, name: "Broccoli"}).expect(403);
|
|
580
580
|
expect(res.body.title).toContain("cannot write fields");
|
|
581
581
|
});
|
|
582
582
|
|
|
@@ -1324,7 +1324,7 @@ describe("@terreno/api", () => {
|
|
|
1324
1324
|
);
|
|
1325
1325
|
server = supertest(app);
|
|
1326
1326
|
|
|
1327
|
-
const res = await server.post("/food").send({calories: 15, name: "Broccoli"}).expect(
|
|
1327
|
+
const res = await server.post("/food").send({calories: 15, name: "Broccoli"}).expect(403);
|
|
1328
1328
|
expect(res.body.title).toContain("cannot write fields");
|
|
1329
1329
|
});
|
|
1330
1330
|
|
package/src/api.ts
CHANGED
|
@@ -528,6 +528,9 @@ function _buildModelRouter<T>(model: Model<T>, options: ModelRouterOptions<T>):
|
|
|
528
528
|
try {
|
|
529
529
|
body = transform<T>(options, req.body, "create", req.user);
|
|
530
530
|
} catch (error: any) {
|
|
531
|
+
if (isAPIError(error)) {
|
|
532
|
+
throw error;
|
|
533
|
+
}
|
|
531
534
|
throw new APIError({
|
|
532
535
|
disableExternalErrorTracking: getDisableExternalErrorTracking(error),
|
|
533
536
|
error,
|
|
@@ -827,6 +830,9 @@ function _buildModelRouter<T>(model: Model<T>, options: ModelRouterOptions<T>):
|
|
|
827
830
|
try {
|
|
828
831
|
body = transform<T>(options, req.body, "update", req.user);
|
|
829
832
|
} catch (error: any) {
|
|
833
|
+
if (isAPIError(error)) {
|
|
834
|
+
throw error;
|
|
835
|
+
}
|
|
830
836
|
throw new APIError({
|
|
831
837
|
disableExternalErrorTracking: getDisableExternalErrorTracking(error),
|
|
832
838
|
error,
|
|
@@ -1080,6 +1086,9 @@ function _buildModelRouter<T>(model: Model<T>, options: ModelRouterOptions<T>):
|
|
|
1080
1086
|
try {
|
|
1081
1087
|
body = transform<T>(options, body, "update", req.user) as Partial<T>;
|
|
1082
1088
|
} catch (error: any) {
|
|
1089
|
+
if (isAPIError(error)) {
|
|
1090
|
+
throw error;
|
|
1091
|
+
}
|
|
1083
1092
|
throw new APIError({
|
|
1084
1093
|
disableExternalErrorTracking: getDisableExternalErrorTracking(error),
|
|
1085
1094
|
error,
|
|
@@ -17,7 +17,7 @@ export interface VersionConfigDocument extends mongoose.Document {
|
|
|
17
17
|
|
|
18
18
|
export interface VersionConfigModel extends mongoose.Model<VersionConfigDocument> {
|
|
19
19
|
findOneOrNone(
|
|
20
|
-
query: Record<string,
|
|
20
|
+
query: Record<string, unknown>,
|
|
21
21
|
errorArgs?: Partial<APIErrorConstructor>
|
|
22
22
|
): Promise<(Document & VersionConfigDocument) | null>;
|
|
23
23
|
}
|
package/src/openApiEtag.ts
CHANGED
|
@@ -14,7 +14,7 @@ export const openApiEtagMiddleware = (req: Request, res: Response, next: NextFun
|
|
|
14
14
|
|
|
15
15
|
const originalJson = res.json.bind(res);
|
|
16
16
|
|
|
17
|
-
res.json = (body:
|
|
17
|
+
res.json = (body: unknown) => {
|
|
18
18
|
const jsonString = JSON.stringify(body);
|
|
19
19
|
const etag = `"${crypto.createHash("sha256").update(jsonString).digest("hex").substring(0, 16)}"`;
|
|
20
20
|
|
|
@@ -161,6 +161,64 @@ describe("openApiValidator", () => {
|
|
|
161
161
|
});
|
|
162
162
|
});
|
|
163
163
|
|
|
164
|
+
describe("per-route validation: object-form options", () => {
|
|
165
|
+
it("applies object validation options with per-operation control", async () => {
|
|
166
|
+
configureOpenApiValidator({removeAdditional: true});
|
|
167
|
+
|
|
168
|
+
const removedProps: string[] = [];
|
|
169
|
+
const errorsCaught: ErrorObject[][] = [];
|
|
170
|
+
|
|
171
|
+
const freshApp = await setupFreshApp();
|
|
172
|
+
freshApp.use(
|
|
173
|
+
"/required",
|
|
174
|
+
modelRouter(RequiredModel, {
|
|
175
|
+
...requiredRouterOptions,
|
|
176
|
+
validation: {
|
|
177
|
+
excludeFromCreate: ["about"],
|
|
178
|
+
onAdditionalPropertiesRemoved: (props) => {
|
|
179
|
+
removedProps.push(...props);
|
|
180
|
+
},
|
|
181
|
+
onError: (errors) => {
|
|
182
|
+
errorsCaught.push(errors);
|
|
183
|
+
},
|
|
184
|
+
validateCreate: true,
|
|
185
|
+
validateQuery: true,
|
|
186
|
+
validateUpdate: true,
|
|
187
|
+
},
|
|
188
|
+
})
|
|
189
|
+
);
|
|
190
|
+
const admin = await authAsUser(freshApp, "admin");
|
|
191
|
+
|
|
192
|
+
const res = await admin.post("/required").send({name: "Validated"}).expect(201);
|
|
193
|
+
expect(res.body.data.name).toBe("Validated");
|
|
194
|
+
});
|
|
195
|
+
|
|
196
|
+
it("disables create validation when validateCreate is false", async () => {
|
|
197
|
+
configureOpenApiValidator({removeAdditional: true});
|
|
198
|
+
|
|
199
|
+
const freshApp = await setupFreshApp();
|
|
200
|
+
freshApp.use(
|
|
201
|
+
"/required",
|
|
202
|
+
modelRouter(RequiredModel, {
|
|
203
|
+
...requiredRouterOptions,
|
|
204
|
+
validation: {
|
|
205
|
+
excludeFromUpdate: ["about"],
|
|
206
|
+
validateCreate: false,
|
|
207
|
+
validateUpdate: true,
|
|
208
|
+
},
|
|
209
|
+
})
|
|
210
|
+
);
|
|
211
|
+
const admin = await authAsUser(freshApp, "admin");
|
|
212
|
+
|
|
213
|
+
// Even with extra field, should pass since create validation is disabled
|
|
214
|
+
const res = await admin
|
|
215
|
+
.post("/required")
|
|
216
|
+
.send({extraField: "ignored", name: "NoCreateValidation"})
|
|
217
|
+
.expect(201);
|
|
218
|
+
expect(res.body.data.name).toBe("NoCreateValidation");
|
|
219
|
+
});
|
|
220
|
+
});
|
|
221
|
+
|
|
164
222
|
describe("sanitization of non-standard mongoose-to-swagger types", () => {
|
|
165
223
|
it("validates models with ObjectId and DateOnly fields after sanitization", async () => {
|
|
166
224
|
configureOpenApiValidator({removeAdditional: true});
|
package/src/transformers.ts
CHANGED
|
@@ -15,7 +15,10 @@ export interface TerrenoTransformer<T> {
|
|
|
15
15
|
serialize?: (obj: T, user?: User) => Partial<T> | undefined;
|
|
16
16
|
}
|
|
17
17
|
|
|
18
|
-
|
|
18
|
+
const getUserType = (
|
|
19
|
+
user?: User,
|
|
20
|
+
obj?: Record<string, unknown>
|
|
21
|
+
): "anon" | "auth" | "owner" | "admin" => {
|
|
19
22
|
if (user?.admin) {
|
|
20
23
|
return "admin";
|
|
21
24
|
}
|
|
@@ -26,10 +29,9 @@ function getUserType(user?: User, obj?: any): "anon" | "auth" | "owner" | "admin
|
|
|
26
29
|
return "auth";
|
|
27
30
|
}
|
|
28
31
|
return "anon";
|
|
29
|
-
}
|
|
32
|
+
};
|
|
30
33
|
|
|
31
|
-
export
|
|
32
|
-
// TODO: do something with KeyOf here.
|
|
34
|
+
export const AdminOwnerTransformer = <T>(options: {
|
|
33
35
|
anonReadFields?: string[];
|
|
34
36
|
authReadFields?: string[];
|
|
35
37
|
ownerReadFields?: string[];
|
|
@@ -38,20 +40,20 @@ export function AdminOwnerTransformer<T>(options: {
|
|
|
38
40
|
authWriteFields?: string[];
|
|
39
41
|
ownerWriteFields?: string[];
|
|
40
42
|
adminWriteFields?: string[];
|
|
41
|
-
}): TerrenoTransformer<T> {
|
|
42
|
-
|
|
43
|
+
}): TerrenoTransformer<T> => {
|
|
44
|
+
const pickFields = (obj: Partial<T>, fields: string[]): Partial<T> => {
|
|
43
45
|
const newData: Partial<T> = {};
|
|
44
46
|
for (const field of fields) {
|
|
45
|
-
if (obj[field] !== undefined) {
|
|
46
|
-
newData[field] = obj[field];
|
|
47
|
+
if ((obj as Record<string, unknown>)[field] !== undefined) {
|
|
48
|
+
(newData as Record<string, unknown>)[field] = (obj as Record<string, unknown>)[field];
|
|
47
49
|
}
|
|
48
50
|
}
|
|
49
51
|
return newData;
|
|
50
|
-
}
|
|
52
|
+
};
|
|
51
53
|
|
|
52
54
|
return {
|
|
53
55
|
serialize: (obj: T, user?: User) => {
|
|
54
|
-
const userType = getUserType(user, obj);
|
|
56
|
+
const userType = getUserType(user, obj as Record<string, unknown>);
|
|
55
57
|
if (userType === "admin") {
|
|
56
58
|
return pickFields(obj, [...(options.adminReadFields ?? []), "id"]);
|
|
57
59
|
}
|
|
@@ -63,10 +65,9 @@ export function AdminOwnerTransformer<T>(options: {
|
|
|
63
65
|
}
|
|
64
66
|
return pickFields(obj, [...(options.anonReadFields ?? []), "id"]);
|
|
65
67
|
},
|
|
66
|
-
// TODO: Migrate AdminOwnerTransform to use pre-hooks.
|
|
67
68
|
transform: (obj: Partial<T>, _method: "create" | "update", user?: User) => {
|
|
68
|
-
const userType = getUserType(user, obj);
|
|
69
|
-
let allowedFields:
|
|
69
|
+
const userType = getUserType(user, obj as Record<string, unknown>);
|
|
70
|
+
let allowedFields: string[];
|
|
70
71
|
if (userType === "admin") {
|
|
71
72
|
allowedFields = options.adminWriteFields ?? [];
|
|
72
73
|
} else if (userType === "owner") {
|
|
@@ -78,21 +79,22 @@ export function AdminOwnerTransformer<T>(options: {
|
|
|
78
79
|
}
|
|
79
80
|
const unallowedFields = Object.keys(obj).filter((k) => !allowedFields.includes(k));
|
|
80
81
|
if (unallowedFields.length) {
|
|
81
|
-
throw new
|
|
82
|
-
|
|
83
|
-
|
|
82
|
+
throw new APIError({
|
|
83
|
+
status: 403,
|
|
84
|
+
title: `User of type ${userType} cannot write fields: ${unallowedFields.join(", ")}`,
|
|
85
|
+
});
|
|
84
86
|
}
|
|
85
87
|
return obj;
|
|
86
88
|
},
|
|
87
89
|
};
|
|
88
|
-
}
|
|
90
|
+
};
|
|
89
91
|
|
|
90
|
-
export
|
|
92
|
+
export const transform = <T>(
|
|
91
93
|
options: ModelRouterOptions<T>,
|
|
92
94
|
data: Partial<T> | Partial<T>[],
|
|
93
95
|
method: "create" | "update",
|
|
94
96
|
user?: User
|
|
95
|
-
) {
|
|
97
|
+
) => {
|
|
96
98
|
if (!options.transformer?.transform) {
|
|
97
99
|
return data;
|
|
98
100
|
}
|
|
@@ -108,16 +110,16 @@ export function transform<T>(
|
|
|
108
110
|
return transformFn(data, method, user);
|
|
109
111
|
}
|
|
110
112
|
return data.map((d) => transformFn(d, method, user));
|
|
111
|
-
}
|
|
113
|
+
};
|
|
112
114
|
|
|
113
|
-
export
|
|
115
|
+
export const serialize = <T>(
|
|
114
116
|
req: express.Request,
|
|
115
117
|
options: ModelRouterOptions<T>,
|
|
116
|
-
data: (Document
|
|
117
|
-
) {
|
|
118
|
-
const serializeFn = (serializeData: Document
|
|
118
|
+
data: (Document & T) | (Document & T)[]
|
|
119
|
+
) => {
|
|
120
|
+
const serializeFn = (serializeData: Document & T, serializeUser?: User) => {
|
|
119
121
|
const dataObject = serializeData.toObject() as T;
|
|
120
|
-
(dataObject as
|
|
122
|
+
(dataObject as Record<string, unknown>).id = serializeData._id;
|
|
121
123
|
|
|
122
124
|
// Search for any value that is a Map and transform it to a plain object.
|
|
123
125
|
// Otherwise Express drops the contents.
|
|
@@ -143,18 +145,18 @@ export function serialize<T>(
|
|
|
143
145
|
return serializeFn(data, req.user);
|
|
144
146
|
}
|
|
145
147
|
return data.map((d) => serializeFn(d, req.user));
|
|
146
|
-
}
|
|
148
|
+
};
|
|
147
149
|
|
|
148
150
|
/**
|
|
149
151
|
* Default response handler for modelRouter. Calls toObject on each doc and returns the result,
|
|
150
152
|
* using transformers.serializer if provided.
|
|
151
153
|
*/
|
|
152
|
-
export async
|
|
153
|
-
doc: (Document
|
|
154
|
+
export const defaultResponseHandler = async <T>(
|
|
155
|
+
doc: (Document & T) | (Document & T)[] | null,
|
|
154
156
|
method: "list" | "create" | "read" | "update",
|
|
155
157
|
request: express.Request,
|
|
156
158
|
options: ModelRouterOptions<T>
|
|
157
|
-
) {
|
|
159
|
+
) => {
|
|
158
160
|
if (!doc) {
|
|
159
161
|
return null;
|
|
160
162
|
}
|
|
@@ -168,4 +170,4 @@ export async function defaultResponseHandler<T>(
|
|
|
168
170
|
title: `Error serializing ${method} response: ${errorObj.message}`,
|
|
169
171
|
});
|
|
170
172
|
}
|
|
171
|
-
}
|
|
173
|
+
};
|