@terreno/api 0.0.1
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/LICENSE +202 -0
- package/README.md +170 -0
- package/biome.jsonc +22 -0
- package/bunfig.toml +4 -0
- package/dist/api.d.ts +227 -0
- package/dist/api.js +1024 -0
- package/dist/api.test.d.ts +1 -0
- package/dist/api.test.js +2143 -0
- package/dist/auth.d.ts +50 -0
- package/dist/auth.js +512 -0
- package/dist/auth.test.d.ts +1 -0
- package/dist/auth.test.js +778 -0
- package/dist/errors.d.ts +75 -0
- package/dist/errors.js +216 -0
- package/dist/example.d.ts +1 -0
- package/dist/example.js +118 -0
- package/dist/expressServer.d.ts +35 -0
- package/dist/expressServer.js +436 -0
- package/dist/index.d.ts +14 -0
- package/dist/index.js +30 -0
- package/dist/logger.d.ts +23 -0
- package/dist/logger.js +249 -0
- package/dist/middleware.d.ts +10 -0
- package/dist/middleware.js +52 -0
- package/dist/notifiers/googleChatNotifier.d.ts +5 -0
- package/dist/notifiers/googleChatNotifier.js +130 -0
- package/dist/notifiers/googleChatNotifier.test.d.ts +1 -0
- package/dist/notifiers/googleChatNotifier.test.js +260 -0
- package/dist/notifiers/index.d.ts +3 -0
- package/dist/notifiers/index.js +19 -0
- package/dist/notifiers/slackNotifier.d.ts +5 -0
- package/dist/notifiers/slackNotifier.js +130 -0
- package/dist/notifiers/slackNotifier.test.d.ts +1 -0
- package/dist/notifiers/slackNotifier.test.js +259 -0
- package/dist/notifiers/zoomNotifier.d.ts +34 -0
- package/dist/notifiers/zoomNotifier.js +181 -0
- package/dist/notifiers/zoomNotifier.test.d.ts +1 -0
- package/dist/notifiers/zoomNotifier.test.js +370 -0
- package/dist/openApi.d.ts +60 -0
- package/dist/openApi.js +441 -0
- package/dist/openApi.test.d.ts +1 -0
- package/dist/openApi.test.js +445 -0
- package/dist/openApiBuilder.d.ts +419 -0
- package/dist/openApiBuilder.js +424 -0
- package/dist/openApiBuilder.test.d.ts +1 -0
- package/dist/openApiBuilder.test.js +509 -0
- package/dist/openApiEtag.d.ts +7 -0
- package/dist/openApiEtag.js +38 -0
- package/dist/permissions.d.ts +26 -0
- package/dist/permissions.js +331 -0
- package/dist/permissions.test.d.ts +1 -0
- package/dist/permissions.test.js +413 -0
- package/dist/plugins.d.ts +67 -0
- package/dist/plugins.js +315 -0
- package/dist/plugins.test.d.ts +1 -0
- package/dist/plugins.test.js +639 -0
- package/dist/populate.d.ts +14 -0
- package/dist/populate.js +315 -0
- package/dist/populate.test.d.ts +1 -0
- package/dist/populate.test.js +133 -0
- package/dist/response.d.ts +0 -0
- package/dist/response.js +1 -0
- package/dist/tests/bunSetup.d.ts +1 -0
- package/dist/tests/bunSetup.js +297 -0
- package/dist/tests/index.d.ts +1 -0
- package/dist/tests/index.js +17 -0
- package/dist/tests.d.ts +99 -0
- package/dist/tests.js +273 -0
- package/dist/transformers.d.ts +25 -0
- package/dist/transformers.js +217 -0
- package/dist/transformers.test.d.ts +1 -0
- package/dist/transformers.test.js +370 -0
- package/dist/utils.d.ts +11 -0
- package/dist/utils.js +143 -0
- package/dist/utils.test.d.ts +1 -0
- package/dist/utils.test.js +14 -0
- package/index.ts +1 -0
- package/package.json +88 -0
- package/src/__snapshots__/openApi.test.ts.snap +4814 -0
- package/src/__snapshots__/openApiBuilder.test.ts.snap +1485 -0
- package/src/api.test.ts +1661 -0
- package/src/api.ts +1036 -0
- package/src/auth.test.ts +550 -0
- package/src/auth.ts +408 -0
- package/src/errors.ts +225 -0
- package/src/example.ts +99 -0
- package/src/express.d.ts +5 -0
- package/src/expressServer.ts +387 -0
- package/src/index.ts +14 -0
- package/src/logger.ts +190 -0
- package/src/middleware.ts +18 -0
- package/src/notifiers/googleChatNotifier.test.ts +114 -0
- package/src/notifiers/googleChatNotifier.ts +47 -0
- package/src/notifiers/index.ts +3 -0
- package/src/notifiers/slackNotifier.test.ts +113 -0
- package/src/notifiers/slackNotifier.ts +55 -0
- package/src/notifiers/zoomNotifier.test.ts +207 -0
- package/src/notifiers/zoomNotifier.ts +111 -0
- package/src/openApi.test.ts +331 -0
- package/src/openApi.ts +494 -0
- package/src/openApiBuilder.test.ts +442 -0
- package/src/openApiBuilder.ts +636 -0
- package/src/openApiEtag.ts +40 -0
- package/src/permissions.test.ts +219 -0
- package/src/permissions.ts +228 -0
- package/src/plugins.test.ts +390 -0
- package/src/plugins.ts +289 -0
- package/src/populate.test.ts +65 -0
- package/src/populate.ts +258 -0
- package/src/response.ts +0 -0
- package/src/tests/bunSetup.ts +234 -0
- package/src/tests/index.ts +1 -0
- package/src/tests.ts +218 -0
- package/src/transformers.test.ts +202 -0
- package/src/transformers.ts +170 -0
- package/src/utils.test.ts +14 -0
- package/src/utils.ts +47 -0
- package/tsconfig.json +60 -0
- package/types.d.ts +17 -0
|
@@ -0,0 +1,509 @@
|
|
|
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 __importDefault = (this && this.__importDefault) || function (mod) {
|
|
50
|
+
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
51
|
+
};
|
|
52
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
53
|
+
var bun_test_1 = require("bun:test");
|
|
54
|
+
var supertest_1 = __importDefault(require("supertest"));
|
|
55
|
+
var api_1 = require("./api");
|
|
56
|
+
var auth_1 = require("./auth");
|
|
57
|
+
var expressServer_1 = require("./expressServer");
|
|
58
|
+
var openApiBuilder_1 = require("./openApiBuilder");
|
|
59
|
+
var permissions_1 = require("./permissions");
|
|
60
|
+
var tests_1 = require("./tests");
|
|
61
|
+
function addRoutesWithBuilder(router, options) {
|
|
62
|
+
var _this = this;
|
|
63
|
+
// Add a custom endpoint using the OpenApiMiddlewareBuilder
|
|
64
|
+
var statsMiddleware = (0, openApiBuilder_1.createOpenApiBuilder)(options !== null && options !== void 0 ? options : {})
|
|
65
|
+
.withTags(["Stats"])
|
|
66
|
+
.withSummary("Get food statistics")
|
|
67
|
+
.withDescription("Returns aggregated statistics about food items")
|
|
68
|
+
.withQueryParameter("category", { type: "string" }, {
|
|
69
|
+
description: "Filter by food category",
|
|
70
|
+
required: false,
|
|
71
|
+
})
|
|
72
|
+
.withResponse(200, {
|
|
73
|
+
avgCalories: { description: "Average calories", type: "number" },
|
|
74
|
+
count: { description: "Total number of food items", type: "number" },
|
|
75
|
+
})
|
|
76
|
+
.build();
|
|
77
|
+
router.get("/food/stats", statsMiddleware, function (_req, res) { return __awaiter(_this, void 0, void 0, function () {
|
|
78
|
+
return __generator(this, function (_a) {
|
|
79
|
+
res.json({ avgCalories: 250, count: 10 });
|
|
80
|
+
return [2 /*return*/];
|
|
81
|
+
});
|
|
82
|
+
}); });
|
|
83
|
+
// Add endpoint with request body
|
|
84
|
+
var createReportMiddleware = (0, openApiBuilder_1.createOpenApiBuilder)(options !== null && options !== void 0 ? options : {})
|
|
85
|
+
.withTags(["Reports"])
|
|
86
|
+
.withSummary("Create a food report")
|
|
87
|
+
.withDescription("Generates a report based on provided criteria")
|
|
88
|
+
.withRequestBody({
|
|
89
|
+
endDate: {
|
|
90
|
+
description: "Report end date",
|
|
91
|
+
format: "date",
|
|
92
|
+
required: true,
|
|
93
|
+
type: "string",
|
|
94
|
+
},
|
|
95
|
+
includeDeleted: {
|
|
96
|
+
description: "Whether to include deleted items",
|
|
97
|
+
type: "boolean",
|
|
98
|
+
},
|
|
99
|
+
startDate: {
|
|
100
|
+
description: "Report start date",
|
|
101
|
+
format: "date",
|
|
102
|
+
required: true,
|
|
103
|
+
type: "string",
|
|
104
|
+
},
|
|
105
|
+
})
|
|
106
|
+
.withResponse(201, {
|
|
107
|
+
reportId: { description: "Generated report ID", type: "string" },
|
|
108
|
+
}, { description: "Report created successfully" })
|
|
109
|
+
.build();
|
|
110
|
+
router.post("/food/reports", createReportMiddleware, function (_req, res) { return __awaiter(_this, void 0, void 0, function () {
|
|
111
|
+
return __generator(this, function (_a) {
|
|
112
|
+
res.status(201).json({ reportId: "report-123" });
|
|
113
|
+
return [2 /*return*/];
|
|
114
|
+
});
|
|
115
|
+
}); });
|
|
116
|
+
// Add endpoint with array response
|
|
117
|
+
var listCategoriesMiddleware = (0, openApiBuilder_1.createOpenApiBuilder)(options !== null && options !== void 0 ? options : {})
|
|
118
|
+
.withTags(["Categories"])
|
|
119
|
+
.withSummary("List food categories")
|
|
120
|
+
.withArrayResponse(200, {
|
|
121
|
+
count: { description: "Number of items in category", type: "number" },
|
|
122
|
+
id: { description: "Category ID", type: "string" },
|
|
123
|
+
name: { description: "Category name", type: "string" },
|
|
124
|
+
}, { description: "List of categories" })
|
|
125
|
+
.build();
|
|
126
|
+
router.get("/food/categories", listCategoriesMiddleware, function (_req, res) { return __awaiter(_this, void 0, void 0, function () {
|
|
127
|
+
return __generator(this, function (_a) {
|
|
128
|
+
res.json([
|
|
129
|
+
{ count: 5, id: "1", name: "Fruits" },
|
|
130
|
+
{ count: 3, id: "2", name: "Vegetables" },
|
|
131
|
+
]);
|
|
132
|
+
return [2 /*return*/];
|
|
133
|
+
});
|
|
134
|
+
}); });
|
|
135
|
+
// Add endpoint with path parameter
|
|
136
|
+
var getCategoryMiddleware = (0, openApiBuilder_1.createOpenApiBuilder)(options !== null && options !== void 0 ? options : {})
|
|
137
|
+
.withTags(["Categories"])
|
|
138
|
+
.withSummary("Get category by ID")
|
|
139
|
+
.withPathParameter("categoryId", { type: "string" }, {
|
|
140
|
+
description: "The category identifier",
|
|
141
|
+
})
|
|
142
|
+
.withResponse(200, {
|
|
143
|
+
id: { type: "string" },
|
|
144
|
+
name: { type: "string" },
|
|
145
|
+
})
|
|
146
|
+
.withResponse(404, "Category not found")
|
|
147
|
+
.build();
|
|
148
|
+
router.get("/food/categories/:categoryId", getCategoryMiddleware, function (req, res) { return __awaiter(_this, void 0, void 0, function () {
|
|
149
|
+
return __generator(this, function (_a) {
|
|
150
|
+
res.json({ id: req.params.categoryId, name: "Fruits" });
|
|
151
|
+
return [2 /*return*/];
|
|
152
|
+
});
|
|
153
|
+
}); });
|
|
154
|
+
// Standard modelRouter for food
|
|
155
|
+
router.use("/food", (0, api_1.modelRouter)(tests_1.FoodModel, __assign(__assign({}, options), { allowAnonymous: true, permissions: {
|
|
156
|
+
create: [permissions_1.Permissions.IsAny],
|
|
157
|
+
delete: [permissions_1.Permissions.IsAny],
|
|
158
|
+
list: [permissions_1.Permissions.IsAny],
|
|
159
|
+
read: [permissions_1.Permissions.IsAny],
|
|
160
|
+
update: [permissions_1.Permissions.IsAny],
|
|
161
|
+
} })));
|
|
162
|
+
}
|
|
163
|
+
(0, bun_test_1.describe)("OpenApiMiddlewareBuilder", function () {
|
|
164
|
+
var server;
|
|
165
|
+
var app;
|
|
166
|
+
(0, bun_test_1.beforeEach)(function () { return __awaiter(void 0, void 0, void 0, function () {
|
|
167
|
+
return __generator(this, function (_a) {
|
|
168
|
+
process.env.REFRESH_TOKEN_SECRET = "testsecret1234";
|
|
169
|
+
process.env.ENABLE_SWAGGER = "true";
|
|
170
|
+
app = (0, expressServer_1.setupServer)({
|
|
171
|
+
addRoutes: addRoutesWithBuilder,
|
|
172
|
+
skipListen: true,
|
|
173
|
+
userModel: tests_1.UserModel,
|
|
174
|
+
});
|
|
175
|
+
(0, auth_1.setupAuth)(app, tests_1.UserModel);
|
|
176
|
+
(0, auth_1.addAuthRoutes)(app, tests_1.UserModel);
|
|
177
|
+
return [2 /*return*/];
|
|
178
|
+
});
|
|
179
|
+
}); });
|
|
180
|
+
(0, bun_test_1.describe)("builder pattern", function () {
|
|
181
|
+
(0, bun_test_1.it)("returns a builder instance from createOpenApiBuilder", function () {
|
|
182
|
+
var builder = (0, openApiBuilder_1.createOpenApiBuilder)({});
|
|
183
|
+
(0, bun_test_1.expect)(builder).toBeInstanceOf(openApiBuilder_1.OpenApiMiddlewareBuilder);
|
|
184
|
+
});
|
|
185
|
+
(0, bun_test_1.it)("supports method chaining", function () {
|
|
186
|
+
var builder = (0, openApiBuilder_1.createOpenApiBuilder)({});
|
|
187
|
+
var result = builder
|
|
188
|
+
.withTags(["test"])
|
|
189
|
+
.withSummary("Test summary")
|
|
190
|
+
.withDescription("Test description");
|
|
191
|
+
(0, bun_test_1.expect)(result).toBe(builder);
|
|
192
|
+
});
|
|
193
|
+
(0, bun_test_1.it)("returns noop middleware when openApi is not configured", function () {
|
|
194
|
+
var middleware = (0, openApiBuilder_1.createOpenApiBuilder)({}).build();
|
|
195
|
+
(0, bun_test_1.expect)(typeof middleware).toBe("function");
|
|
196
|
+
(0, bun_test_1.expect)(middleware.length).toBe(3); // Express middleware signature
|
|
197
|
+
});
|
|
198
|
+
});
|
|
199
|
+
(0, bun_test_1.describe)("OpenAPI spec generation", function () {
|
|
200
|
+
(0, bun_test_1.it)("includes custom endpoint with query parameter in OpenAPI spec", function () { return __awaiter(void 0, void 0, void 0, function () {
|
|
201
|
+
var res, statsPath, categoryParam;
|
|
202
|
+
return __generator(this, function (_a) {
|
|
203
|
+
switch (_a.label) {
|
|
204
|
+
case 0:
|
|
205
|
+
server = (0, supertest_1.default)(app);
|
|
206
|
+
return [4 /*yield*/, server.get("/openapi.json").expect(200)];
|
|
207
|
+
case 1:
|
|
208
|
+
res = _a.sent();
|
|
209
|
+
statsPath = res.body.paths["/food/stats"];
|
|
210
|
+
(0, bun_test_1.expect)(statsPath).toBeDefined();
|
|
211
|
+
(0, bun_test_1.expect)(statsPath.get).toBeDefined();
|
|
212
|
+
(0, bun_test_1.expect)(statsPath.get.tags).toContain("Stats");
|
|
213
|
+
(0, bun_test_1.expect)(statsPath.get.summary).toBe("Get food statistics");
|
|
214
|
+
(0, bun_test_1.expect)(statsPath.get.description).toBe("Returns aggregated statistics about food items");
|
|
215
|
+
categoryParam = statsPath.get.parameters.find(function (p) { return p.name === "category"; });
|
|
216
|
+
(0, bun_test_1.expect)(categoryParam).toBeDefined();
|
|
217
|
+
(0, bun_test_1.expect)(categoryParam.in).toBe("query");
|
|
218
|
+
(0, bun_test_1.expect)(categoryParam.schema.type).toBe("string");
|
|
219
|
+
(0, bun_test_1.expect)(categoryParam.description).toBe("Filter by food category");
|
|
220
|
+
(0, bun_test_1.expect)(categoryParam.required).toBe(false);
|
|
221
|
+
return [2 /*return*/];
|
|
222
|
+
}
|
|
223
|
+
});
|
|
224
|
+
}); });
|
|
225
|
+
(0, bun_test_1.it)("includes request body schema in OpenAPI spec", function () { return __awaiter(void 0, void 0, void 0, function () {
|
|
226
|
+
var res, reportsPath, requestBody, schema;
|
|
227
|
+
return __generator(this, function (_a) {
|
|
228
|
+
switch (_a.label) {
|
|
229
|
+
case 0:
|
|
230
|
+
server = (0, supertest_1.default)(app);
|
|
231
|
+
return [4 /*yield*/, server.get("/openapi.json").expect(200)];
|
|
232
|
+
case 1:
|
|
233
|
+
res = _a.sent();
|
|
234
|
+
reportsPath = res.body.paths["/food/reports"];
|
|
235
|
+
(0, bun_test_1.expect)(reportsPath).toBeDefined();
|
|
236
|
+
(0, bun_test_1.expect)(reportsPath.post).toBeDefined();
|
|
237
|
+
(0, bun_test_1.expect)(reportsPath.post.tags).toContain("Reports");
|
|
238
|
+
requestBody = reportsPath.post.requestBody;
|
|
239
|
+
(0, bun_test_1.expect)(requestBody).toBeDefined();
|
|
240
|
+
(0, bun_test_1.expect)(requestBody.required).toBe(true);
|
|
241
|
+
schema = requestBody.content["application/json"].schema;
|
|
242
|
+
(0, bun_test_1.expect)(schema.type).toBe("object");
|
|
243
|
+
(0, bun_test_1.expect)(schema.properties.startDate.type).toBe("string");
|
|
244
|
+
(0, bun_test_1.expect)(schema.properties.startDate.format).toBe("date");
|
|
245
|
+
(0, bun_test_1.expect)(schema.properties.endDate.type).toBe("string");
|
|
246
|
+
(0, bun_test_1.expect)(schema.properties.includeDeleted.type).toBe("boolean");
|
|
247
|
+
(0, bun_test_1.expect)(schema.required).toContain("startDate");
|
|
248
|
+
(0, bun_test_1.expect)(schema.required).toContain("endDate");
|
|
249
|
+
(0, bun_test_1.expect)(schema.required).not.toContain("includeDeleted");
|
|
250
|
+
return [2 /*return*/];
|
|
251
|
+
}
|
|
252
|
+
});
|
|
253
|
+
}); });
|
|
254
|
+
(0, bun_test_1.it)("includes response schema in OpenAPI spec", function () { return __awaiter(void 0, void 0, void 0, function () {
|
|
255
|
+
var res, statsPath, response200, schema;
|
|
256
|
+
return __generator(this, function (_a) {
|
|
257
|
+
switch (_a.label) {
|
|
258
|
+
case 0:
|
|
259
|
+
server = (0, supertest_1.default)(app);
|
|
260
|
+
return [4 /*yield*/, server.get("/openapi.json").expect(200)];
|
|
261
|
+
case 1:
|
|
262
|
+
res = _a.sent();
|
|
263
|
+
statsPath = res.body.paths["/food/stats"];
|
|
264
|
+
response200 = statsPath.get.responses["200"];
|
|
265
|
+
(0, bun_test_1.expect)(response200).toBeDefined();
|
|
266
|
+
(0, bun_test_1.expect)(response200.description).toBe("Success");
|
|
267
|
+
schema = response200.content["application/json"].schema;
|
|
268
|
+
(0, bun_test_1.expect)(schema.type).toBe("object");
|
|
269
|
+
(0, bun_test_1.expect)(schema.properties.count.type).toBe("number");
|
|
270
|
+
(0, bun_test_1.expect)(schema.properties.avgCalories.type).toBe("number");
|
|
271
|
+
return [2 /*return*/];
|
|
272
|
+
}
|
|
273
|
+
});
|
|
274
|
+
}); });
|
|
275
|
+
(0, bun_test_1.it)("includes array response schema in OpenAPI spec", function () { return __awaiter(void 0, void 0, void 0, function () {
|
|
276
|
+
var res, categoriesPath, response200, schema;
|
|
277
|
+
return __generator(this, function (_a) {
|
|
278
|
+
switch (_a.label) {
|
|
279
|
+
case 0:
|
|
280
|
+
server = (0, supertest_1.default)(app);
|
|
281
|
+
return [4 /*yield*/, server.get("/openapi.json").expect(200)];
|
|
282
|
+
case 1:
|
|
283
|
+
res = _a.sent();
|
|
284
|
+
categoriesPath = res.body.paths["/food/categories"];
|
|
285
|
+
(0, bun_test_1.expect)(categoriesPath).toBeDefined();
|
|
286
|
+
response200 = categoriesPath.get.responses["200"];
|
|
287
|
+
(0, bun_test_1.expect)(response200.description).toBe("List of categories");
|
|
288
|
+
schema = response200.content["application/json"].schema;
|
|
289
|
+
(0, bun_test_1.expect)(schema.type).toBe("array");
|
|
290
|
+
(0, bun_test_1.expect)(schema.items.type).toBe("object");
|
|
291
|
+
(0, bun_test_1.expect)(schema.items.properties.id.type).toBe("string");
|
|
292
|
+
(0, bun_test_1.expect)(schema.items.properties.name.type).toBe("string");
|
|
293
|
+
(0, bun_test_1.expect)(schema.items.properties.count.type).toBe("number");
|
|
294
|
+
return [2 /*return*/];
|
|
295
|
+
}
|
|
296
|
+
});
|
|
297
|
+
}); });
|
|
298
|
+
(0, bun_test_1.it)("includes path parameter in OpenAPI spec", function () { return __awaiter(void 0, void 0, void 0, function () {
|
|
299
|
+
var res, categoryPath, pathParam;
|
|
300
|
+
return __generator(this, function (_a) {
|
|
301
|
+
switch (_a.label) {
|
|
302
|
+
case 0:
|
|
303
|
+
server = (0, supertest_1.default)(app);
|
|
304
|
+
return [4 /*yield*/, server.get("/openapi.json").expect(200)];
|
|
305
|
+
case 1:
|
|
306
|
+
res = _a.sent();
|
|
307
|
+
categoryPath = res.body.paths["/food/categories/{categoryId}"];
|
|
308
|
+
(0, bun_test_1.expect)(categoryPath).toBeDefined();
|
|
309
|
+
pathParam = categoryPath.get.parameters.find(function (p) { return p.name === "categoryId"; });
|
|
310
|
+
(0, bun_test_1.expect)(pathParam).toBeDefined();
|
|
311
|
+
(0, bun_test_1.expect)(pathParam.in).toBe("path");
|
|
312
|
+
(0, bun_test_1.expect)(pathParam.required).toBe(true);
|
|
313
|
+
(0, bun_test_1.expect)(pathParam.schema.type).toBe("string");
|
|
314
|
+
(0, bun_test_1.expect)(pathParam.description).toBe("The category identifier");
|
|
315
|
+
return [2 /*return*/];
|
|
316
|
+
}
|
|
317
|
+
});
|
|
318
|
+
}); });
|
|
319
|
+
(0, bun_test_1.it)("includes custom response without body in OpenAPI spec", function () { return __awaiter(void 0, void 0, void 0, function () {
|
|
320
|
+
var res, reportsPath, response201;
|
|
321
|
+
return __generator(this, function (_a) {
|
|
322
|
+
switch (_a.label) {
|
|
323
|
+
case 0:
|
|
324
|
+
server = (0, supertest_1.default)(app);
|
|
325
|
+
return [4 /*yield*/, server.get("/openapi.json").expect(200)];
|
|
326
|
+
case 1:
|
|
327
|
+
res = _a.sent();
|
|
328
|
+
reportsPath = res.body.paths["/food/reports"];
|
|
329
|
+
response201 = reportsPath.post.responses["201"];
|
|
330
|
+
(0, bun_test_1.expect)(response201).toBeDefined();
|
|
331
|
+
(0, bun_test_1.expect)(response201.description).toBe("Report created successfully");
|
|
332
|
+
(0, bun_test_1.expect)(response201.content).toBeDefined();
|
|
333
|
+
return [2 /*return*/];
|
|
334
|
+
}
|
|
335
|
+
});
|
|
336
|
+
}); });
|
|
337
|
+
(0, bun_test_1.it)("includes default error responses", function () { return __awaiter(void 0, void 0, void 0, function () {
|
|
338
|
+
var res, statsPath, responses;
|
|
339
|
+
return __generator(this, function (_a) {
|
|
340
|
+
switch (_a.label) {
|
|
341
|
+
case 0:
|
|
342
|
+
server = (0, supertest_1.default)(app);
|
|
343
|
+
return [4 /*yield*/, server.get("/openapi.json").expect(200)];
|
|
344
|
+
case 1:
|
|
345
|
+
res = _a.sent();
|
|
346
|
+
statsPath = res.body.paths["/food/stats"];
|
|
347
|
+
responses = statsPath.get.responses;
|
|
348
|
+
// Default error responses should be merged
|
|
349
|
+
(0, bun_test_1.expect)(responses["400"]).toBeDefined();
|
|
350
|
+
(0, bun_test_1.expect)(responses["401"]).toBeDefined();
|
|
351
|
+
(0, bun_test_1.expect)(responses["403"]).toBeDefined();
|
|
352
|
+
(0, bun_test_1.expect)(responses["404"]).toBeDefined();
|
|
353
|
+
(0, bun_test_1.expect)(responses["405"]).toBeDefined();
|
|
354
|
+
return [2 /*return*/];
|
|
355
|
+
}
|
|
356
|
+
});
|
|
357
|
+
}); });
|
|
358
|
+
});
|
|
359
|
+
(0, bun_test_1.describe)("endpoint functionality", function () {
|
|
360
|
+
(0, bun_test_1.it)("stats endpoint returns correct data", function () { return __awaiter(void 0, void 0, void 0, function () {
|
|
361
|
+
var res;
|
|
362
|
+
return __generator(this, function (_a) {
|
|
363
|
+
switch (_a.label) {
|
|
364
|
+
case 0:
|
|
365
|
+
server = (0, supertest_1.default)(app);
|
|
366
|
+
return [4 /*yield*/, server.get("/food/stats").expect(200)];
|
|
367
|
+
case 1:
|
|
368
|
+
res = _a.sent();
|
|
369
|
+
(0, bun_test_1.expect)(res.body).toEqual({ avgCalories: 250, count: 10 });
|
|
370
|
+
return [2 /*return*/];
|
|
371
|
+
}
|
|
372
|
+
});
|
|
373
|
+
}); });
|
|
374
|
+
(0, bun_test_1.it)("reports endpoint returns correct data", function () { return __awaiter(void 0, void 0, void 0, function () {
|
|
375
|
+
var res;
|
|
376
|
+
return __generator(this, function (_a) {
|
|
377
|
+
switch (_a.label) {
|
|
378
|
+
case 0:
|
|
379
|
+
server = (0, supertest_1.default)(app);
|
|
380
|
+
return [4 /*yield*/, server
|
|
381
|
+
.post("/food/reports")
|
|
382
|
+
.send({ endDate: "2024-12-31", startDate: "2024-01-01" })
|
|
383
|
+
.expect(201)];
|
|
384
|
+
case 1:
|
|
385
|
+
res = _a.sent();
|
|
386
|
+
(0, bun_test_1.expect)(res.body).toEqual({ reportId: "report-123" });
|
|
387
|
+
return [2 /*return*/];
|
|
388
|
+
}
|
|
389
|
+
});
|
|
390
|
+
}); });
|
|
391
|
+
(0, bun_test_1.it)("categories endpoint returns array data", function () { return __awaiter(void 0, void 0, void 0, function () {
|
|
392
|
+
var res;
|
|
393
|
+
return __generator(this, function (_a) {
|
|
394
|
+
switch (_a.label) {
|
|
395
|
+
case 0:
|
|
396
|
+
server = (0, supertest_1.default)(app);
|
|
397
|
+
return [4 /*yield*/, server.get("/food/categories").expect(200)];
|
|
398
|
+
case 1:
|
|
399
|
+
res = _a.sent();
|
|
400
|
+
(0, bun_test_1.expect)(res.body).toHaveLength(2);
|
|
401
|
+
(0, bun_test_1.expect)(res.body[0]).toHaveProperty("id");
|
|
402
|
+
(0, bun_test_1.expect)(res.body[0]).toHaveProperty("name");
|
|
403
|
+
return [2 /*return*/];
|
|
404
|
+
}
|
|
405
|
+
});
|
|
406
|
+
}); });
|
|
407
|
+
(0, bun_test_1.it)("category by id endpoint returns correct data", function () { return __awaiter(void 0, void 0, void 0, function () {
|
|
408
|
+
var res;
|
|
409
|
+
return __generator(this, function (_a) {
|
|
410
|
+
switch (_a.label) {
|
|
411
|
+
case 0:
|
|
412
|
+
server = (0, supertest_1.default)(app);
|
|
413
|
+
return [4 /*yield*/, server.get("/food/categories/cat-123").expect(200)];
|
|
414
|
+
case 1:
|
|
415
|
+
res = _a.sent();
|
|
416
|
+
(0, bun_test_1.expect)(res.body).toEqual({ id: "cat-123", name: "Fruits" });
|
|
417
|
+
return [2 /*return*/];
|
|
418
|
+
}
|
|
419
|
+
});
|
|
420
|
+
}); });
|
|
421
|
+
});
|
|
422
|
+
(0, bun_test_1.describe)("snapshot tests", function () {
|
|
423
|
+
(0, bun_test_1.it)("matches OpenAPI spec snapshot", function () { return __awaiter(void 0, void 0, void 0, function () {
|
|
424
|
+
var res;
|
|
425
|
+
return __generator(this, function (_a) {
|
|
426
|
+
switch (_a.label) {
|
|
427
|
+
case 0:
|
|
428
|
+
server = (0, supertest_1.default)(app);
|
|
429
|
+
return [4 /*yield*/, server.get("/openapi.json").expect(200)];
|
|
430
|
+
case 1:
|
|
431
|
+
res = _a.sent();
|
|
432
|
+
(0, bun_test_1.expect)(res.body).toMatchSnapshot();
|
|
433
|
+
return [2 /*return*/];
|
|
434
|
+
}
|
|
435
|
+
});
|
|
436
|
+
}); });
|
|
437
|
+
});
|
|
438
|
+
});
|
|
439
|
+
(0, bun_test_1.describe)("OpenApiMiddlewareBuilder without OpenAPI", function () {
|
|
440
|
+
(0, bun_test_1.it)("build returns noop middleware when openApi.path is not configured", function () {
|
|
441
|
+
var builder = new openApiBuilder_1.OpenApiMiddlewareBuilder({});
|
|
442
|
+
var middleware = builder
|
|
443
|
+
.withTags(["test"])
|
|
444
|
+
.withSummary("Test")
|
|
445
|
+
.withResponse(200, { id: { type: "string" } })
|
|
446
|
+
.build();
|
|
447
|
+
// Middleware should be a function
|
|
448
|
+
(0, bun_test_1.expect)(typeof middleware).toBe("function");
|
|
449
|
+
// Should call next() without error
|
|
450
|
+
var nextCalled = false;
|
|
451
|
+
middleware({}, {}, function () {
|
|
452
|
+
nextCalled = true;
|
|
453
|
+
});
|
|
454
|
+
(0, bun_test_1.expect)(nextCalled).toBe(true);
|
|
455
|
+
});
|
|
456
|
+
(0, bun_test_1.it)("build returns noop middleware when options is empty", function () {
|
|
457
|
+
var middleware = (0, openApiBuilder_1.createOpenApiBuilder)({}).build();
|
|
458
|
+
var nextCalled = false;
|
|
459
|
+
middleware({}, {}, function () {
|
|
460
|
+
nextCalled = true;
|
|
461
|
+
});
|
|
462
|
+
(0, bun_test_1.expect)(nextCalled).toBe(true);
|
|
463
|
+
});
|
|
464
|
+
});
|
|
465
|
+
(0, bun_test_1.describe)("OpenApiMiddlewareBuilder configuration", function () {
|
|
466
|
+
(0, bun_test_1.it)("correctly extracts required fields from request body schema", function () {
|
|
467
|
+
// We can't easily test this without a mock openApi.path, but we can at least
|
|
468
|
+
// verify the builder accepts the configuration
|
|
469
|
+
var builder = (0, openApiBuilder_1.createOpenApiBuilder)({}).withRequestBody({
|
|
470
|
+
optional: { type: "string" },
|
|
471
|
+
required1: { required: true, type: "string" },
|
|
472
|
+
required2: { required: true, type: "string" },
|
|
473
|
+
});
|
|
474
|
+
(0, bun_test_1.expect)(builder).toBeInstanceOf(openApiBuilder_1.OpenApiMiddlewareBuilder);
|
|
475
|
+
});
|
|
476
|
+
(0, bun_test_1.it)("supports custom media types for request body", function () {
|
|
477
|
+
var builder = (0, openApiBuilder_1.createOpenApiBuilder)({}).withRequestBody({ data: { type: "string" } }, { mediaType: "application/xml" });
|
|
478
|
+
(0, bun_test_1.expect)(builder).toBeInstanceOf(openApiBuilder_1.OpenApiMiddlewareBuilder);
|
|
479
|
+
});
|
|
480
|
+
(0, bun_test_1.it)("supports custom media types for response", function () {
|
|
481
|
+
var builder = (0, openApiBuilder_1.createOpenApiBuilder)({}).withResponse(200, { data: { type: "string" } }, { mediaType: "text/plain" });
|
|
482
|
+
(0, bun_test_1.expect)(builder).toBeInstanceOf(openApiBuilder_1.OpenApiMiddlewareBuilder);
|
|
483
|
+
});
|
|
484
|
+
(0, bun_test_1.it)("supports optional request body", function () {
|
|
485
|
+
var builder = (0, openApiBuilder_1.createOpenApiBuilder)({}).withRequestBody({ data: { type: "string" } }, { required: false });
|
|
486
|
+
(0, bun_test_1.expect)(builder).toBeInstanceOf(openApiBuilder_1.OpenApiMiddlewareBuilder);
|
|
487
|
+
});
|
|
488
|
+
(0, bun_test_1.it)("supports multiple query parameters", function () {
|
|
489
|
+
var builder = (0, openApiBuilder_1.createOpenApiBuilder)({})
|
|
490
|
+
.withQueryParameter("limit", { type: "number" }, { required: false })
|
|
491
|
+
.withQueryParameter("offset", { type: "number" }, { required: false })
|
|
492
|
+
.withQueryParameter("search", { type: "string" });
|
|
493
|
+
(0, bun_test_1.expect)(builder).toBeInstanceOf(openApiBuilder_1.OpenApiMiddlewareBuilder);
|
|
494
|
+
});
|
|
495
|
+
(0, bun_test_1.it)("supports multiple path parameters", function () {
|
|
496
|
+
var builder = (0, openApiBuilder_1.createOpenApiBuilder)({})
|
|
497
|
+
.withPathParameter("userId", { type: "string" })
|
|
498
|
+
.withPathParameter("postId", { type: "string" });
|
|
499
|
+
(0, bun_test_1.expect)(builder).toBeInstanceOf(openApiBuilder_1.OpenApiMiddlewareBuilder);
|
|
500
|
+
});
|
|
501
|
+
(0, bun_test_1.it)("supports multiple responses", function () {
|
|
502
|
+
var builder = (0, openApiBuilder_1.createOpenApiBuilder)({})
|
|
503
|
+
.withResponse(200, { data: { type: "string" } })
|
|
504
|
+
.withResponse(201, { id: { type: "string" } })
|
|
505
|
+
.withResponse(204, "No content")
|
|
506
|
+
.withResponse(404, "Not found");
|
|
507
|
+
(0, bun_test_1.expect)(builder).toBeInstanceOf(openApiBuilder_1.OpenApiMiddlewareBuilder);
|
|
508
|
+
});
|
|
509
|
+
});
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
import type { NextFunction, Request, Response } from "express";
|
|
2
|
+
/**
|
|
3
|
+
* Middleware to add ETag support for OpenAPI JSON endpoint.
|
|
4
|
+
* This middleware should be added before the @wesleytodd/openapi middleware
|
|
5
|
+
* to intercept requests to /openapi.json and add conditional request support.
|
|
6
|
+
*/
|
|
7
|
+
export declare function openApiEtagMiddleware(req: Request, res: Response, next: NextFunction): void;
|
|
@@ -0,0 +1,38 @@
|
|
|
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
|
+
exports.openApiEtagMiddleware = openApiEtagMiddleware;
|
|
7
|
+
var node_crypto_1 = __importDefault(require("node:crypto"));
|
|
8
|
+
/**
|
|
9
|
+
* Middleware to add ETag support for OpenAPI JSON endpoint.
|
|
10
|
+
* This middleware should be added before the @wesleytodd/openapi middleware
|
|
11
|
+
* to intercept requests to /openapi.json and add conditional request support.
|
|
12
|
+
*/
|
|
13
|
+
function openApiEtagMiddleware(req, res, next) {
|
|
14
|
+
// Only handle GET requests to /openapi.json
|
|
15
|
+
if (req.method !== "GET" || req.path !== "/openapi.json") {
|
|
16
|
+
next();
|
|
17
|
+
return;
|
|
18
|
+
}
|
|
19
|
+
// Store original res.json to intercept the response
|
|
20
|
+
var originalJson = res.json.bind(res);
|
|
21
|
+
res.json = function (body) {
|
|
22
|
+
// Generate ETag based on the JSON content
|
|
23
|
+
var jsonString = JSON.stringify(body);
|
|
24
|
+
var etag = "\"".concat(node_crypto_1.default.createHash("sha256").update(jsonString).digest("hex").substring(0, 16), "\"");
|
|
25
|
+
// Set ETag header
|
|
26
|
+
res.set("ETag", etag);
|
|
27
|
+
// Check If-None-Match header for conditional requests
|
|
28
|
+
var ifNoneMatch = req.get("If-None-Match");
|
|
29
|
+
if (ifNoneMatch === etag) {
|
|
30
|
+
// Resource hasn't changed, return 304 Not Modified
|
|
31
|
+
res.status(304).end();
|
|
32
|
+
return res;
|
|
33
|
+
}
|
|
34
|
+
// Resource has changed or no conditional header, return the content
|
|
35
|
+
return originalJson(body);
|
|
36
|
+
};
|
|
37
|
+
next();
|
|
38
|
+
}
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
import type express from "express";
|
|
2
|
+
import type { NextFunction } from "express";
|
|
3
|
+
import { type Model } from "mongoose";
|
|
4
|
+
import { type modelRouterOptions, type RESTMethod } from "./api";
|
|
5
|
+
import type { User } from "./auth";
|
|
6
|
+
export type PermissionMethod<T> = (method: RESTMethod, user?: User, obj?: T) => boolean | Promise<boolean>;
|
|
7
|
+
export interface RESTPermissions<T> {
|
|
8
|
+
create: PermissionMethod<T>[];
|
|
9
|
+
list: PermissionMethod<T>[];
|
|
10
|
+
read: PermissionMethod<T>[];
|
|
11
|
+
update: PermissionMethod<T>[];
|
|
12
|
+
delete: PermissionMethod<T>[];
|
|
13
|
+
}
|
|
14
|
+
export declare const OwnerQueryFilter: (user?: User) => {
|
|
15
|
+
ownerId: string;
|
|
16
|
+
} | null;
|
|
17
|
+
export declare const Permissions: {
|
|
18
|
+
IsAdmin: (_method: RESTMethod, user?: User) => boolean;
|
|
19
|
+
IsAny: () => boolean;
|
|
20
|
+
IsAuthenticated: (_method: RESTMethod, user?: User) => boolean;
|
|
21
|
+
IsAuthenticatedOrReadOnly: (method: RESTMethod, user?: User) => boolean;
|
|
22
|
+
IsOwner: (_method: RESTMethod, user?: User, obj?: any) => any;
|
|
23
|
+
IsOwnerOrReadOnly: (method: RESTMethod, user?: User, obj?: any) => boolean;
|
|
24
|
+
};
|
|
25
|
+
export declare function checkPermissions<T>(method: RESTMethod, permissions: PermissionMethod<T>[], user?: User, obj?: T): Promise<boolean>;
|
|
26
|
+
export declare function permissionMiddleware<T>(baseModel: Model<T>, options: Pick<modelRouterOptions<T>, "permissions" | "populatePaths" | "discriminatorKey">): (req: express.Request, _res: express.Response, next: NextFunction) => Promise<void>;
|