@terreno/api 0.0.18 → 0.2.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/README.md +73 -3
- package/dist/api.d.ts +96 -3
- package/dist/api.js +159 -11
- package/dist/api.test.js +906 -2
- package/dist/auth.js +3 -1
- package/dist/betterAuth.d.ts +91 -0
- package/dist/betterAuth.js +8 -0
- package/dist/betterAuth.test.d.ts +1 -0
- package/dist/betterAuth.test.js +181 -0
- package/dist/betterAuthApp.d.ts +22 -0
- package/dist/betterAuthApp.js +38 -0
- package/dist/betterAuthApp.test.d.ts +1 -0
- package/dist/betterAuthApp.test.js +242 -0
- package/dist/betterAuthSetup.d.ts +60 -0
- package/dist/betterAuthSetup.js +278 -0
- package/dist/betterAuthSetup.test.d.ts +1 -0
- package/dist/betterAuthSetup.test.js +684 -0
- package/dist/errors.js +14 -11
- package/dist/example.js +7 -7
- package/dist/expressServer.js +2 -2
- package/dist/githubAuth.test.js +3 -3
- package/dist/index.d.ts +6 -0
- package/dist/index.js +6 -0
- package/dist/openApi.test.js +8 -5
- package/dist/openApiBuilder.d.ts +69 -1
- package/dist/openApiBuilder.js +109 -5
- package/dist/openApiValidator.d.ts +296 -0
- package/dist/openApiValidator.js +698 -0
- package/dist/openApiValidator.test.d.ts +1 -0
- package/dist/openApiValidator.test.js +346 -0
- package/dist/plugins.test.js +3 -3
- package/dist/terrenoApp.d.ts +189 -0
- package/dist/terrenoApp.js +352 -0
- package/dist/terrenoApp.test.d.ts +1 -0
- package/dist/terrenoApp.test.js +264 -0
- package/dist/terrenoPlugin.d.ts +38 -0
- package/dist/terrenoPlugin.js +2 -0
- package/dist/tests.js +34 -24
- package/package.json +8 -2
- package/src/__snapshots__/openApi.test.ts.snap +399 -0
- package/src/__snapshots__/openApiBuilder.test.ts.snap +108 -0
- package/src/api.test.ts +743 -2
- package/src/api.ts +270 -6
- package/src/auth.ts +3 -1
- package/src/betterAuth.test.ts +160 -0
- package/src/betterAuth.ts +104 -0
- package/src/betterAuthApp.test.ts +114 -0
- package/src/betterAuthApp.ts +60 -0
- package/src/betterAuthSetup.test.ts +485 -0
- package/src/betterAuthSetup.ts +251 -0
- package/src/errors.ts +14 -11
- package/src/example.ts +7 -7
- package/src/expressServer.ts +4 -5
- package/src/githubAuth.test.ts +3 -3
- package/src/index.ts +6 -0
- package/src/openApi.test.ts +8 -5
- package/src/openApiBuilder.ts +188 -15
- package/src/openApiValidator.test.ts +241 -0
- package/src/openApiValidator.ts +860 -0
- package/src/plugins.test.ts +3 -3
- package/src/terrenoApp.test.ts +201 -0
- package/src/terrenoApp.ts +347 -0
- package/src/terrenoPlugin.ts +39 -0
- package/src/tests.ts +34 -24
- package/.cursorrules +0 -107
- package/.windsurfrules +0 -107
- package/AGENTS.md +0 -313
- package/dist/response.d.ts +0 -0
- package/dist/response.js +0 -1
- package/index.ts +0 -1
- package/src/response.ts +0 -0
|
@@ -0,0 +1,346 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __assign = (this && this.__assign) || function () {
|
|
3
|
+
__assign = Object.assign || function(t) {
|
|
4
|
+
for (var s, i = 1, n = arguments.length; i < n; i++) {
|
|
5
|
+
s = arguments[i];
|
|
6
|
+
for (var p in s) if (Object.prototype.hasOwnProperty.call(s, p))
|
|
7
|
+
t[p] = s[p];
|
|
8
|
+
}
|
|
9
|
+
return t;
|
|
10
|
+
};
|
|
11
|
+
return __assign.apply(this, arguments);
|
|
12
|
+
};
|
|
13
|
+
var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) {
|
|
14
|
+
function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); }
|
|
15
|
+
return new (P || (P = Promise))(function (resolve, reject) {
|
|
16
|
+
function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } }
|
|
17
|
+
function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } }
|
|
18
|
+
function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); }
|
|
19
|
+
step((generator = generator.apply(thisArg, _arguments || [])).next());
|
|
20
|
+
});
|
|
21
|
+
};
|
|
22
|
+
var __generator = (this && this.__generator) || function (thisArg, body) {
|
|
23
|
+
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);
|
|
24
|
+
return g.next = verb(0), g["throw"] = verb(1), g["return"] = verb(2), typeof Symbol === "function" && (g[Symbol.iterator] = function() { return this; }), g;
|
|
25
|
+
function verb(n) { return function (v) { return step([n, v]); }; }
|
|
26
|
+
function step(op) {
|
|
27
|
+
if (f) throw new TypeError("Generator is already executing.");
|
|
28
|
+
while (g && (g = 0, op[0] && (_ = 0)), _) try {
|
|
29
|
+
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;
|
|
30
|
+
if (y = 0, t) op = [op[0] & 2, t.value];
|
|
31
|
+
switch (op[0]) {
|
|
32
|
+
case 0: case 1: t = op; break;
|
|
33
|
+
case 4: _.label++; return { value: op[1], done: false };
|
|
34
|
+
case 5: _.label++; y = op[1]; op = [0]; continue;
|
|
35
|
+
case 7: op = _.ops.pop(); _.trys.pop(); continue;
|
|
36
|
+
default:
|
|
37
|
+
if (!(t = _.trys, t = t.length > 0 && t[t.length - 1]) && (op[0] === 6 || op[0] === 2)) { _ = 0; continue; }
|
|
38
|
+
if (op[0] === 3 && (!t || (op[1] > t[0] && op[1] < t[3]))) { _.label = op[1]; break; }
|
|
39
|
+
if (op[0] === 6 && _.label < t[1]) { _.label = t[1]; t = op; break; }
|
|
40
|
+
if (t && _.label < t[2]) { _.label = t[2]; _.ops.push(op); break; }
|
|
41
|
+
if (t[2]) _.ops.pop();
|
|
42
|
+
_.trys.pop(); continue;
|
|
43
|
+
}
|
|
44
|
+
op = body.call(thisArg, _);
|
|
45
|
+
} catch (e) { op = [6, e]; y = 0; } finally { f = t = 0; }
|
|
46
|
+
if (op[0] & 5) throw op[1]; return { value: op[0] ? op[1] : void 0, done: true };
|
|
47
|
+
}
|
|
48
|
+
};
|
|
49
|
+
var __read = (this && this.__read) || function (o, n) {
|
|
50
|
+
var m = typeof Symbol === "function" && o[Symbol.iterator];
|
|
51
|
+
if (!m) return o;
|
|
52
|
+
var i = m.call(o), r, ar = [], e;
|
|
53
|
+
try {
|
|
54
|
+
while ((n === void 0 || n-- > 0) && !(r = i.next()).done) ar.push(r.value);
|
|
55
|
+
}
|
|
56
|
+
catch (error) { e = { error: error }; }
|
|
57
|
+
finally {
|
|
58
|
+
try {
|
|
59
|
+
if (r && !r.done && (m = i["return"])) m.call(i);
|
|
60
|
+
}
|
|
61
|
+
finally { if (e) throw e.error; }
|
|
62
|
+
}
|
|
63
|
+
return ar;
|
|
64
|
+
};
|
|
65
|
+
var __spreadArray = (this && this.__spreadArray) || function (to, from, pack) {
|
|
66
|
+
if (pack || arguments.length === 2) for (var i = 0, l = from.length, ar; i < l; i++) {
|
|
67
|
+
if (ar || !(i in from)) {
|
|
68
|
+
if (!ar) ar = Array.prototype.slice.call(from, 0, i);
|
|
69
|
+
ar[i] = from[i];
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
return to.concat(ar || Array.prototype.slice.call(from));
|
|
73
|
+
};
|
|
74
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
75
|
+
var bun_test_1 = require("bun:test");
|
|
76
|
+
var api_1 = require("./api");
|
|
77
|
+
var auth_1 = require("./auth");
|
|
78
|
+
var openApiValidator_1 = require("./openApiValidator");
|
|
79
|
+
var permissions_1 = require("./permissions");
|
|
80
|
+
var tests_1 = require("./tests");
|
|
81
|
+
// RequiredModel has a clean schema that AJV can compile (no non-standard types).
|
|
82
|
+
// It has: name (String, required), about (String, optional)
|
|
83
|
+
var requiredRouterOptions = {
|
|
84
|
+
permissions: {
|
|
85
|
+
create: [permissions_1.Permissions.IsAuthenticated],
|
|
86
|
+
delete: [permissions_1.Permissions.IsAdmin],
|
|
87
|
+
list: [permissions_1.Permissions.IsAuthenticated],
|
|
88
|
+
read: [permissions_1.Permissions.IsAuthenticated],
|
|
89
|
+
update: [permissions_1.Permissions.IsAuthenticated],
|
|
90
|
+
},
|
|
91
|
+
queryFields: ["name"],
|
|
92
|
+
sort: "-name",
|
|
93
|
+
};
|
|
94
|
+
var setupFreshApp = function () { return __awaiter(void 0, void 0, void 0, function () {
|
|
95
|
+
var freshApp;
|
|
96
|
+
return __generator(this, function (_a) {
|
|
97
|
+
freshApp = (0, tests_1.getBaseServer)();
|
|
98
|
+
(0, auth_1.setupAuth)(freshApp, tests_1.UserModel);
|
|
99
|
+
(0, auth_1.addAuthRoutes)(freshApp, tests_1.UserModel);
|
|
100
|
+
return [2 /*return*/, freshApp];
|
|
101
|
+
});
|
|
102
|
+
}); };
|
|
103
|
+
(0, bun_test_1.describe)("openApiValidator", function () {
|
|
104
|
+
(0, bun_test_1.beforeEach)(function () { return __awaiter(void 0, void 0, void 0, function () {
|
|
105
|
+
return __generator(this, function (_a) {
|
|
106
|
+
switch (_a.label) {
|
|
107
|
+
case 0:
|
|
108
|
+
(0, openApiValidator_1.resetOpenApiValidatorConfig)();
|
|
109
|
+
return [4 /*yield*/, (0, tests_1.setupDb)()];
|
|
110
|
+
case 1:
|
|
111
|
+
_a.sent();
|
|
112
|
+
return [4 /*yield*/, tests_1.RequiredModel.deleteMany({})];
|
|
113
|
+
case 2:
|
|
114
|
+
_a.sent();
|
|
115
|
+
return [2 /*return*/];
|
|
116
|
+
}
|
|
117
|
+
});
|
|
118
|
+
}); });
|
|
119
|
+
(0, bun_test_1.afterEach)(function () {
|
|
120
|
+
(0, openApiValidator_1.resetOpenApiValidatorConfig)();
|
|
121
|
+
});
|
|
122
|
+
(0, bun_test_1.describe)("isConfigured flag", function () {
|
|
123
|
+
(0, bun_test_1.it)("is false by default", function () {
|
|
124
|
+
(0, bun_test_1.expect)((0, openApiValidator_1.isOpenApiValidatorConfigured)()).toBe(false);
|
|
125
|
+
});
|
|
126
|
+
(0, bun_test_1.it)("becomes true after configureOpenApiValidator()", function () {
|
|
127
|
+
(0, openApiValidator_1.configureOpenApiValidator)();
|
|
128
|
+
(0, bun_test_1.expect)((0, openApiValidator_1.isOpenApiValidatorConfigured)()).toBe(true);
|
|
129
|
+
});
|
|
130
|
+
(0, bun_test_1.it)("resets to false after resetOpenApiValidatorConfig()", function () {
|
|
131
|
+
(0, openApiValidator_1.configureOpenApiValidator)();
|
|
132
|
+
(0, bun_test_1.expect)((0, openApiValidator_1.isOpenApiValidatorConfigured)()).toBe(true);
|
|
133
|
+
(0, openApiValidator_1.resetOpenApiValidatorConfig)();
|
|
134
|
+
(0, bun_test_1.expect)((0, openApiValidator_1.isOpenApiValidatorConfigured)()).toBe(false);
|
|
135
|
+
});
|
|
136
|
+
});
|
|
137
|
+
(0, bun_test_1.describe)("no-op when not configured", function () {
|
|
138
|
+
(0, bun_test_1.it)("does not strip or validate when not configured", function () { return __awaiter(void 0, void 0, void 0, function () {
|
|
139
|
+
var freshApp, admin, res;
|
|
140
|
+
return __generator(this, function (_a) {
|
|
141
|
+
switch (_a.label) {
|
|
142
|
+
case 0: return [4 /*yield*/, setupFreshApp()];
|
|
143
|
+
case 1:
|
|
144
|
+
freshApp = _a.sent();
|
|
145
|
+
freshApp.use("/required", (0, api_1.modelRouter)(tests_1.RequiredModel, requiredRouterOptions));
|
|
146
|
+
return [4 /*yield*/, (0, tests_1.authAsUser)(freshApp, "admin")];
|
|
147
|
+
case 2:
|
|
148
|
+
admin = _a.sent();
|
|
149
|
+
return [4 /*yield*/, admin.post("/required").send({ name: "Apple" }).expect(201)];
|
|
150
|
+
case 3:
|
|
151
|
+
res = _a.sent();
|
|
152
|
+
(0, bun_test_1.expect)(res.body.data.name).toBe("Apple");
|
|
153
|
+
return [2 /*return*/];
|
|
154
|
+
}
|
|
155
|
+
});
|
|
156
|
+
}); });
|
|
157
|
+
});
|
|
158
|
+
(0, bun_test_1.describe)("active after configuration", function () {
|
|
159
|
+
(0, bun_test_1.it)("strips extra properties when removeAdditional is true", function () { return __awaiter(void 0, void 0, void 0, function () {
|
|
160
|
+
var freshApp, admin, res;
|
|
161
|
+
return __generator(this, function (_a) {
|
|
162
|
+
switch (_a.label) {
|
|
163
|
+
case 0:
|
|
164
|
+
(0, openApiValidator_1.configureOpenApiValidator)({ removeAdditional: true });
|
|
165
|
+
return [4 /*yield*/, setupFreshApp()];
|
|
166
|
+
case 1:
|
|
167
|
+
freshApp = _a.sent();
|
|
168
|
+
freshApp.use("/required", (0, api_1.modelRouter)(tests_1.RequiredModel, requiredRouterOptions));
|
|
169
|
+
return [4 /*yield*/, (0, tests_1.authAsUser)(freshApp, "admin")];
|
|
170
|
+
case 2:
|
|
171
|
+
admin = _a.sent();
|
|
172
|
+
return [4 /*yield*/, admin
|
|
173
|
+
.post("/required")
|
|
174
|
+
.send({ fakeField: "this should be stripped", name: "Apple" })
|
|
175
|
+
.expect(201)];
|
|
176
|
+
case 3:
|
|
177
|
+
res = _a.sent();
|
|
178
|
+
(0, bun_test_1.expect)(res.body.data.name).toBe("Apple");
|
|
179
|
+
(0, bun_test_1.expect)(res.body.data.fakeField).toBeUndefined();
|
|
180
|
+
return [2 /*return*/];
|
|
181
|
+
}
|
|
182
|
+
});
|
|
183
|
+
}); });
|
|
184
|
+
(0, bun_test_1.it)("rejects missing required fields", function () { return __awaiter(void 0, void 0, void 0, function () {
|
|
185
|
+
var freshApp, admin, res;
|
|
186
|
+
return __generator(this, function (_a) {
|
|
187
|
+
switch (_a.label) {
|
|
188
|
+
case 0:
|
|
189
|
+
(0, openApiValidator_1.configureOpenApiValidator)();
|
|
190
|
+
return [4 /*yield*/, setupFreshApp()];
|
|
191
|
+
case 1:
|
|
192
|
+
freshApp = _a.sent();
|
|
193
|
+
freshApp.use("/required", (0, api_1.modelRouter)(tests_1.RequiredModel, requiredRouterOptions));
|
|
194
|
+
return [4 /*yield*/, (0, tests_1.authAsUser)(freshApp, "admin")];
|
|
195
|
+
case 2:
|
|
196
|
+
admin = _a.sent();
|
|
197
|
+
return [4 /*yield*/, admin.post("/required").send({ about: "no name" }).expect(400)];
|
|
198
|
+
case 3:
|
|
199
|
+
res = _a.sent();
|
|
200
|
+
(0, bun_test_1.expect)(res.body.title).toBe("Request validation failed");
|
|
201
|
+
return [2 /*return*/];
|
|
202
|
+
}
|
|
203
|
+
});
|
|
204
|
+
}); });
|
|
205
|
+
});
|
|
206
|
+
(0, bun_test_1.describe)("onAdditionalPropertiesRemoved hook", function () {
|
|
207
|
+
(0, bun_test_1.it)("fires callback with removed property names", function () { return __awaiter(void 0, void 0, void 0, function () {
|
|
208
|
+
var removedProps, freshApp, admin;
|
|
209
|
+
return __generator(this, function (_a) {
|
|
210
|
+
switch (_a.label) {
|
|
211
|
+
case 0:
|
|
212
|
+
removedProps = [];
|
|
213
|
+
(0, openApiValidator_1.configureOpenApiValidator)({
|
|
214
|
+
onAdditionalPropertiesRemoved: function (props) {
|
|
215
|
+
removedProps.push.apply(removedProps, __spreadArray([], __read(props), false));
|
|
216
|
+
},
|
|
217
|
+
removeAdditional: true,
|
|
218
|
+
});
|
|
219
|
+
return [4 /*yield*/, setupFreshApp()];
|
|
220
|
+
case 1:
|
|
221
|
+
freshApp = _a.sent();
|
|
222
|
+
freshApp.use("/required", (0, api_1.modelRouter)(tests_1.RequiredModel, requiredRouterOptions));
|
|
223
|
+
return [4 /*yield*/, (0, tests_1.authAsUser)(freshApp, "admin")];
|
|
224
|
+
case 2:
|
|
225
|
+
admin = _a.sent();
|
|
226
|
+
return [4 /*yield*/, admin
|
|
227
|
+
.post("/required")
|
|
228
|
+
.send({ extraA: "stripped", extraB: "also stripped", name: "Apple" })
|
|
229
|
+
.expect(201)];
|
|
230
|
+
case 3:
|
|
231
|
+
_a.sent();
|
|
232
|
+
(0, bun_test_1.expect)(removedProps).toContain("extraA");
|
|
233
|
+
(0, bun_test_1.expect)(removedProps).toContain("extraB");
|
|
234
|
+
return [2 /*return*/];
|
|
235
|
+
}
|
|
236
|
+
});
|
|
237
|
+
}); });
|
|
238
|
+
});
|
|
239
|
+
(0, bun_test_1.describe)("per-route validation: false override", function () {
|
|
240
|
+
(0, bun_test_1.it)("skips validation when validation is false", function () { return __awaiter(void 0, void 0, void 0, function () {
|
|
241
|
+
var freshApp, admin, res;
|
|
242
|
+
return __generator(this, function (_a) {
|
|
243
|
+
switch (_a.label) {
|
|
244
|
+
case 0:
|
|
245
|
+
(0, openApiValidator_1.configureOpenApiValidator)({ removeAdditional: true });
|
|
246
|
+
return [4 /*yield*/, setupFreshApp()];
|
|
247
|
+
case 1:
|
|
248
|
+
freshApp = _a.sent();
|
|
249
|
+
freshApp.use("/required", (0, api_1.modelRouter)(tests_1.RequiredModel, __assign(__assign({}, requiredRouterOptions), { validation: false })));
|
|
250
|
+
return [4 /*yield*/, (0, tests_1.authAsUser)(freshApp, "admin")];
|
|
251
|
+
case 2:
|
|
252
|
+
admin = _a.sent();
|
|
253
|
+
return [4 /*yield*/, admin
|
|
254
|
+
.post("/required")
|
|
255
|
+
.send({ fakeField: "not stripped", name: "Apple" })
|
|
256
|
+
.expect(201)];
|
|
257
|
+
case 3:
|
|
258
|
+
res = _a.sent();
|
|
259
|
+
(0, bun_test_1.expect)(res.body.data.name).toBe("Apple");
|
|
260
|
+
return [2 /*return*/];
|
|
261
|
+
}
|
|
262
|
+
});
|
|
263
|
+
}); });
|
|
264
|
+
});
|
|
265
|
+
(0, bun_test_1.describe)("sanitization of non-standard mongoose-to-swagger types", function () {
|
|
266
|
+
(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
|
+
var freshApp, admin, res;
|
|
268
|
+
return __generator(this, function (_a) {
|
|
269
|
+
switch (_a.label) {
|
|
270
|
+
case 0:
|
|
271
|
+
(0, openApiValidator_1.configureOpenApiValidator)({ removeAdditional: true });
|
|
272
|
+
return [4 /*yield*/, setupFreshApp()];
|
|
273
|
+
case 1:
|
|
274
|
+
freshApp = _a.sent();
|
|
275
|
+
freshApp.use("/food", (0, api_1.modelRouter)(tests_1.FoodModel, {
|
|
276
|
+
permissions: {
|
|
277
|
+
create: [permissions_1.Permissions.IsAuthenticated],
|
|
278
|
+
delete: [permissions_1.Permissions.IsAdmin],
|
|
279
|
+
list: [permissions_1.Permissions.IsAuthenticated],
|
|
280
|
+
read: [permissions_1.Permissions.IsAuthenticated],
|
|
281
|
+
update: [permissions_1.Permissions.IsAuthenticated],
|
|
282
|
+
},
|
|
283
|
+
queryFields: ["name", "calories", "hidden"],
|
|
284
|
+
sort: "-created",
|
|
285
|
+
}));
|
|
286
|
+
return [4 /*yield*/, (0, tests_1.authAsUser)(freshApp, "admin")];
|
|
287
|
+
case 2:
|
|
288
|
+
admin = _a.sent();
|
|
289
|
+
return [4 /*yield*/, admin
|
|
290
|
+
.post("/food")
|
|
291
|
+
.send({ calories: 100, likesIds: [], name: "Apple", source: { name: "Test" } })
|
|
292
|
+
.expect(201)];
|
|
293
|
+
case 3:
|
|
294
|
+
res = _a.sent();
|
|
295
|
+
(0, bun_test_1.expect)(res.body.data.name).toBe("Apple");
|
|
296
|
+
return [2 /*return*/];
|
|
297
|
+
}
|
|
298
|
+
});
|
|
299
|
+
}); });
|
|
300
|
+
});
|
|
301
|
+
(0, bun_test_1.describe)("buildQuerySchemaFromFields", function () {
|
|
302
|
+
(0, bun_test_1.it)("always includes limit, page, and sort", function () {
|
|
303
|
+
var schema = (0, openApiValidator_1.buildQuerySchemaFromFields)(tests_1.FoodModel, []);
|
|
304
|
+
(0, bun_test_1.expect)(schema.limit).toBeDefined();
|
|
305
|
+
(0, bun_test_1.expect)(schema.page).toBeDefined();
|
|
306
|
+
(0, bun_test_1.expect)(schema.sort).toBeDefined();
|
|
307
|
+
});
|
|
308
|
+
(0, bun_test_1.it)("includes queryFields from model schema", function () {
|
|
309
|
+
var schema = (0, openApiValidator_1.buildQuerySchemaFromFields)(tests_1.FoodModel, ["name", "calories"]);
|
|
310
|
+
(0, bun_test_1.expect)(schema.name).toBeDefined();
|
|
311
|
+
(0, bun_test_1.expect)(schema.calories).toBeDefined();
|
|
312
|
+
(0, bun_test_1.expect)(schema.hidden).toBeUndefined();
|
|
313
|
+
});
|
|
314
|
+
(0, bun_test_1.it)("marks query fields as not required", function () {
|
|
315
|
+
var schema = (0, openApiValidator_1.buildQuerySchemaFromFields)(tests_1.FoodModel, ["name"]);
|
|
316
|
+
(0, bun_test_1.expect)(schema.name.required).toBe(false);
|
|
317
|
+
});
|
|
318
|
+
});
|
|
319
|
+
(0, bun_test_1.describe)("validateRequestBody middleware", function () {
|
|
320
|
+
(0, bun_test_1.it)("is a no-op when not configured", function () {
|
|
321
|
+
(0, openApiValidator_1.resetOpenApiValidatorConfig)();
|
|
322
|
+
var middleware = (0, openApiValidator_1.validateRequestBody)({
|
|
323
|
+
name: { required: true, type: "string" },
|
|
324
|
+
});
|
|
325
|
+
var nextCalled = false;
|
|
326
|
+
var req = { body: {} };
|
|
327
|
+
var res = {};
|
|
328
|
+
var next = function () {
|
|
329
|
+
nextCalled = true;
|
|
330
|
+
};
|
|
331
|
+
middleware(req, res, next);
|
|
332
|
+
(0, bun_test_1.expect)(nextCalled).toBe(true);
|
|
333
|
+
});
|
|
334
|
+
(0, bun_test_1.it)("validates when configured", function () {
|
|
335
|
+
(0, openApiValidator_1.configureOpenApiValidator)();
|
|
336
|
+
var middleware = (0, openApiValidator_1.validateRequestBody)({
|
|
337
|
+
name: { required: true, type: "string" },
|
|
338
|
+
});
|
|
339
|
+
var req = { body: {}, method: "POST", path: "/test" };
|
|
340
|
+
var res = {};
|
|
341
|
+
(0, bun_test_1.expect)(function () {
|
|
342
|
+
middleware(req, res, function () { });
|
|
343
|
+
}).toThrow();
|
|
344
|
+
});
|
|
345
|
+
});
|
|
346
|
+
});
|
package/dist/plugins.test.js
CHANGED
|
@@ -64,9 +64,9 @@ var permissions_1 = require("./permissions");
|
|
|
64
64
|
var plugins_1 = require("./plugins");
|
|
65
65
|
var tests_1 = require("./tests");
|
|
66
66
|
var stuffSchema = new mongoose_1.Schema({
|
|
67
|
-
date: plugins_1.DateOnly,
|
|
68
|
-
name: String,
|
|
69
|
-
ownerId: String,
|
|
67
|
+
date: { description: "The date associated with this item", type: plugins_1.DateOnly },
|
|
68
|
+
name: { description: "The name of the item", type: String },
|
|
69
|
+
ownerId: { description: "The user who owns this item", type: String },
|
|
70
70
|
});
|
|
71
71
|
stuffSchema.plugin(plugins_1.isDeletedPlugin);
|
|
72
72
|
stuffSchema.plugin(plugins_1.findOneOrNone);
|
|
@@ -0,0 +1,189 @@
|
|
|
1
|
+
import * as Sentry from "@sentry/bun";
|
|
2
|
+
import express from "express";
|
|
3
|
+
import type { ModelRouterRegistration } from "./api";
|
|
4
|
+
import { type UserModel as UserMongooseModel } from "./auth";
|
|
5
|
+
import { type AuthOptions } from "./expressServer";
|
|
6
|
+
import { type GitHubAuthOptions } from "./githubAuth";
|
|
7
|
+
import { type LoggingOptions } from "./logger";
|
|
8
|
+
import type { TerrenoPlugin } from "./terrenoPlugin";
|
|
9
|
+
type CorsOrigin = string | boolean | RegExp | Array<boolean | string | RegExp> | ((requestOrigin: string | undefined, callback: (err: Error | null, origin?: boolean | string | RegExp | Array<boolean | string | RegExp>) => void) => void);
|
|
10
|
+
/**
|
|
11
|
+
* Configuration options for TerrenoApp.
|
|
12
|
+
*/
|
|
13
|
+
export interface TerrenoAppOptions {
|
|
14
|
+
/** Mongoose User model with passport-local-mongoose plugin */
|
|
15
|
+
userModel: UserMongooseModel;
|
|
16
|
+
/** CORS origin configuration (default: "*") */
|
|
17
|
+
corsOrigin?: CorsOrigin;
|
|
18
|
+
/** Logging configuration options */
|
|
19
|
+
loggingOptions?: LoggingOptions;
|
|
20
|
+
/** Authentication configuration options */
|
|
21
|
+
authOptions?: AuthOptions;
|
|
22
|
+
/** GitHub OAuth configuration (enables GitHub authentication if provided) */
|
|
23
|
+
githubAuth?: GitHubAuthOptions;
|
|
24
|
+
/** Skip calling app.listen() in start() method (useful for testing) */
|
|
25
|
+
skipListen?: boolean;
|
|
26
|
+
/** Sentry configuration options */
|
|
27
|
+
sentryOptions?: Sentry.BunOptions;
|
|
28
|
+
/** Maximum number of array items in query parameters (default: 200) */
|
|
29
|
+
arrayLimit?: number;
|
|
30
|
+
/** Whether to log all incoming requests (default: true) */
|
|
31
|
+
logRequests?: boolean;
|
|
32
|
+
}
|
|
33
|
+
/**
|
|
34
|
+
* Fluent API for building Express applications with Terreno framework.
|
|
35
|
+
*
|
|
36
|
+
* TerrenoApp provides an alternative to `setupServer` using a registration
|
|
37
|
+
* pattern instead of callbacks. Build applications by registering model
|
|
38
|
+
* routers and plugins, then calling `start()` to begin listening.
|
|
39
|
+
*
|
|
40
|
+
* The middleware stack is configured in this order:
|
|
41
|
+
* 1. CORS
|
|
42
|
+
* 2. Custom middleware (via addMiddleware)
|
|
43
|
+
* 3. JSON body parser
|
|
44
|
+
* 4. Auth routes (/auth/login, /auth/signup, etc.)
|
|
45
|
+
* 5. JWT authentication setup
|
|
46
|
+
* 6. Request logging
|
|
47
|
+
* 7. Sentry scopes
|
|
48
|
+
* 8. OpenAPI middleware
|
|
49
|
+
* 9. /auth/me routes
|
|
50
|
+
* 10. GitHub OAuth routes (if enabled)
|
|
51
|
+
* 11. Registered model routers and plugins
|
|
52
|
+
* 12. Error handling middleware
|
|
53
|
+
*
|
|
54
|
+
* @example
|
|
55
|
+
* ```typescript
|
|
56
|
+
* // Basic usage with model routers
|
|
57
|
+
* const todoRouter = modelRouter("/todos", Todo, {
|
|
58
|
+
* permissions: { list: [Permissions.IsAuthenticated], ... },
|
|
59
|
+
* });
|
|
60
|
+
*
|
|
61
|
+
* const app = new TerrenoApp({ userModel: User })
|
|
62
|
+
* .register(todoRouter)
|
|
63
|
+
* .register(new HealthApp())
|
|
64
|
+
* .start();
|
|
65
|
+
* ```
|
|
66
|
+
*
|
|
67
|
+
* @example
|
|
68
|
+
* ```typescript
|
|
69
|
+
* // With custom middleware
|
|
70
|
+
* const app = new TerrenoApp({
|
|
71
|
+
* userModel: User,
|
|
72
|
+
* corsOrigin: ["https://app.example.com"],
|
|
73
|
+
* loggingOptions: { logRequests: true },
|
|
74
|
+
* githubAuth: {
|
|
75
|
+
* clientId: process.env.GITHUB_CLIENT_ID!,
|
|
76
|
+
* clientSecret: process.env.GITHUB_CLIENT_SECRET!,
|
|
77
|
+
* callbackURL: process.env.GITHUB_CALLBACK_URL!,
|
|
78
|
+
* },
|
|
79
|
+
* })
|
|
80
|
+
* .addMiddleware((req, res, next) => {
|
|
81
|
+
* res.setHeader("X-Custom-Header", "value");
|
|
82
|
+
* next();
|
|
83
|
+
* })
|
|
84
|
+
* .register(todoRouter)
|
|
85
|
+
* .register(userRouter)
|
|
86
|
+
* .start();
|
|
87
|
+
* ```
|
|
88
|
+
*
|
|
89
|
+
* @see setupServer for the callback-based alternative
|
|
90
|
+
* @see TerrenoPlugin for creating reusable plugins
|
|
91
|
+
* @see modelRouter for creating CRUD route registrations
|
|
92
|
+
*/
|
|
93
|
+
export declare class TerrenoApp {
|
|
94
|
+
private options;
|
|
95
|
+
private registrations;
|
|
96
|
+
private middlewareFns;
|
|
97
|
+
/**
|
|
98
|
+
* Create a new TerrenoApp builder.
|
|
99
|
+
*
|
|
100
|
+
* @param options - Application configuration options including user model and auth settings
|
|
101
|
+
*/
|
|
102
|
+
constructor(options: TerrenoAppOptions);
|
|
103
|
+
/**
|
|
104
|
+
* Register a model router or plugin with the application.
|
|
105
|
+
*
|
|
106
|
+
* Model routers are created with `modelRouter("/path", Model, options)` and
|
|
107
|
+
* provide CRUD endpoints. Plugins implement `TerrenoPlugin` interface and
|
|
108
|
+
* can register custom routes and middleware.
|
|
109
|
+
*
|
|
110
|
+
* Registrations are mounted in the order they are added.
|
|
111
|
+
*
|
|
112
|
+
* @param registration - A ModelRouterRegistration from modelRouter() or a TerrenoPlugin instance
|
|
113
|
+
* @returns This TerrenoApp instance for method chaining
|
|
114
|
+
*
|
|
115
|
+
* @example
|
|
116
|
+
* ```typescript
|
|
117
|
+
* const todoRouter = modelRouter("/todos", Todo, options);
|
|
118
|
+
* const healthPlugin = new HealthApp({ path: "/health" });
|
|
119
|
+
*
|
|
120
|
+
* app.register(todoRouter).register(healthPlugin);
|
|
121
|
+
* ```
|
|
122
|
+
*/
|
|
123
|
+
register(registration: ModelRouterRegistration | TerrenoPlugin): this;
|
|
124
|
+
/**
|
|
125
|
+
* Add custom Express middleware to the application.
|
|
126
|
+
*
|
|
127
|
+
* Middleware is added BEFORE JSON body parsing and authentication setup,
|
|
128
|
+
* allowing you to modify incoming requests early in the middleware stack.
|
|
129
|
+
*
|
|
130
|
+
* @param fn - Express middleware function or a function that configures the app
|
|
131
|
+
* @returns This TerrenoApp instance for method chaining
|
|
132
|
+
*
|
|
133
|
+
* @example
|
|
134
|
+
* ```typescript
|
|
135
|
+
* app.addMiddleware((req, res, next) => {
|
|
136
|
+
* res.setHeader("X-Request-ID", req.id);
|
|
137
|
+
* next();
|
|
138
|
+
* });
|
|
139
|
+
* ```
|
|
140
|
+
*/
|
|
141
|
+
addMiddleware(fn: express.RequestHandler | ((app: express.Application) => void)): this;
|
|
142
|
+
/**
|
|
143
|
+
* Build the Express application without starting the server.
|
|
144
|
+
*
|
|
145
|
+
* Configures the complete middleware stack including:
|
|
146
|
+
* - CORS, JSON parsing, authentication, logging, Sentry, OpenAPI
|
|
147
|
+
* - All registered model routers and plugins
|
|
148
|
+
* - Error handling middleware
|
|
149
|
+
*
|
|
150
|
+
* Use this method when you need the Express app instance for testing
|
|
151
|
+
* or custom server setup. For normal use, call `start()` instead.
|
|
152
|
+
*
|
|
153
|
+
* @returns Configured Express application instance
|
|
154
|
+
*
|
|
155
|
+
* @example
|
|
156
|
+
* ```typescript
|
|
157
|
+
* const app = new TerrenoApp({ userModel: User })
|
|
158
|
+
* .register(todoRouter)
|
|
159
|
+
* .build();
|
|
160
|
+
*
|
|
161
|
+
* // Use app for testing with supertest
|
|
162
|
+
* await request(app).get("/todos").expect(200);
|
|
163
|
+
* ```
|
|
164
|
+
*/
|
|
165
|
+
build(): express.Application;
|
|
166
|
+
/**
|
|
167
|
+
* Build the Express application and start listening on the configured port.
|
|
168
|
+
*
|
|
169
|
+
* Calls `build()` to configure the application, then starts an HTTP server
|
|
170
|
+
* listening on the port specified by the `PORT` environment variable (default: 9000).
|
|
171
|
+
* If `skipListen` option is true, the app is built but the server is not started.
|
|
172
|
+
*
|
|
173
|
+
* @returns Configured Express application instance
|
|
174
|
+
*
|
|
175
|
+
* @throws Process exits with code 1 if the server fails to start
|
|
176
|
+
*
|
|
177
|
+
* @example
|
|
178
|
+
* ```typescript
|
|
179
|
+
* // Start server on port 3000
|
|
180
|
+
* process.env.PORT = "3000";
|
|
181
|
+
* const app = new TerrenoApp({ userModel: User })
|
|
182
|
+
* .register(todoRouter)
|
|
183
|
+
* .start();
|
|
184
|
+
* ```
|
|
185
|
+
*/
|
|
186
|
+
start(): express.Application;
|
|
187
|
+
private isModelRouterRegistration;
|
|
188
|
+
}
|
|
189
|
+
export {};
|