@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.
Files changed (119) hide show
  1. package/LICENSE +202 -0
  2. package/README.md +170 -0
  3. package/biome.jsonc +22 -0
  4. package/bunfig.toml +4 -0
  5. package/dist/api.d.ts +227 -0
  6. package/dist/api.js +1024 -0
  7. package/dist/api.test.d.ts +1 -0
  8. package/dist/api.test.js +2143 -0
  9. package/dist/auth.d.ts +50 -0
  10. package/dist/auth.js +512 -0
  11. package/dist/auth.test.d.ts +1 -0
  12. package/dist/auth.test.js +778 -0
  13. package/dist/errors.d.ts +75 -0
  14. package/dist/errors.js +216 -0
  15. package/dist/example.d.ts +1 -0
  16. package/dist/example.js +118 -0
  17. package/dist/expressServer.d.ts +35 -0
  18. package/dist/expressServer.js +436 -0
  19. package/dist/index.d.ts +14 -0
  20. package/dist/index.js +30 -0
  21. package/dist/logger.d.ts +23 -0
  22. package/dist/logger.js +249 -0
  23. package/dist/middleware.d.ts +10 -0
  24. package/dist/middleware.js +52 -0
  25. package/dist/notifiers/googleChatNotifier.d.ts +5 -0
  26. package/dist/notifiers/googleChatNotifier.js +130 -0
  27. package/dist/notifiers/googleChatNotifier.test.d.ts +1 -0
  28. package/dist/notifiers/googleChatNotifier.test.js +260 -0
  29. package/dist/notifiers/index.d.ts +3 -0
  30. package/dist/notifiers/index.js +19 -0
  31. package/dist/notifiers/slackNotifier.d.ts +5 -0
  32. package/dist/notifiers/slackNotifier.js +130 -0
  33. package/dist/notifiers/slackNotifier.test.d.ts +1 -0
  34. package/dist/notifiers/slackNotifier.test.js +259 -0
  35. package/dist/notifiers/zoomNotifier.d.ts +34 -0
  36. package/dist/notifiers/zoomNotifier.js +181 -0
  37. package/dist/notifiers/zoomNotifier.test.d.ts +1 -0
  38. package/dist/notifiers/zoomNotifier.test.js +370 -0
  39. package/dist/openApi.d.ts +60 -0
  40. package/dist/openApi.js +441 -0
  41. package/dist/openApi.test.d.ts +1 -0
  42. package/dist/openApi.test.js +445 -0
  43. package/dist/openApiBuilder.d.ts +419 -0
  44. package/dist/openApiBuilder.js +424 -0
  45. package/dist/openApiBuilder.test.d.ts +1 -0
  46. package/dist/openApiBuilder.test.js +509 -0
  47. package/dist/openApiEtag.d.ts +7 -0
  48. package/dist/openApiEtag.js +38 -0
  49. package/dist/permissions.d.ts +26 -0
  50. package/dist/permissions.js +331 -0
  51. package/dist/permissions.test.d.ts +1 -0
  52. package/dist/permissions.test.js +413 -0
  53. package/dist/plugins.d.ts +67 -0
  54. package/dist/plugins.js +315 -0
  55. package/dist/plugins.test.d.ts +1 -0
  56. package/dist/plugins.test.js +639 -0
  57. package/dist/populate.d.ts +14 -0
  58. package/dist/populate.js +315 -0
  59. package/dist/populate.test.d.ts +1 -0
  60. package/dist/populate.test.js +133 -0
  61. package/dist/response.d.ts +0 -0
  62. package/dist/response.js +1 -0
  63. package/dist/tests/bunSetup.d.ts +1 -0
  64. package/dist/tests/bunSetup.js +297 -0
  65. package/dist/tests/index.d.ts +1 -0
  66. package/dist/tests/index.js +17 -0
  67. package/dist/tests.d.ts +99 -0
  68. package/dist/tests.js +273 -0
  69. package/dist/transformers.d.ts +25 -0
  70. package/dist/transformers.js +217 -0
  71. package/dist/transformers.test.d.ts +1 -0
  72. package/dist/transformers.test.js +370 -0
  73. package/dist/utils.d.ts +11 -0
  74. package/dist/utils.js +143 -0
  75. package/dist/utils.test.d.ts +1 -0
  76. package/dist/utils.test.js +14 -0
  77. package/index.ts +1 -0
  78. package/package.json +88 -0
  79. package/src/__snapshots__/openApi.test.ts.snap +4814 -0
  80. package/src/__snapshots__/openApiBuilder.test.ts.snap +1485 -0
  81. package/src/api.test.ts +1661 -0
  82. package/src/api.ts +1036 -0
  83. package/src/auth.test.ts +550 -0
  84. package/src/auth.ts +408 -0
  85. package/src/errors.ts +225 -0
  86. package/src/example.ts +99 -0
  87. package/src/express.d.ts +5 -0
  88. package/src/expressServer.ts +387 -0
  89. package/src/index.ts +14 -0
  90. package/src/logger.ts +190 -0
  91. package/src/middleware.ts +18 -0
  92. package/src/notifiers/googleChatNotifier.test.ts +114 -0
  93. package/src/notifiers/googleChatNotifier.ts +47 -0
  94. package/src/notifiers/index.ts +3 -0
  95. package/src/notifiers/slackNotifier.test.ts +113 -0
  96. package/src/notifiers/slackNotifier.ts +55 -0
  97. package/src/notifiers/zoomNotifier.test.ts +207 -0
  98. package/src/notifiers/zoomNotifier.ts +111 -0
  99. package/src/openApi.test.ts +331 -0
  100. package/src/openApi.ts +494 -0
  101. package/src/openApiBuilder.test.ts +442 -0
  102. package/src/openApiBuilder.ts +636 -0
  103. package/src/openApiEtag.ts +40 -0
  104. package/src/permissions.test.ts +219 -0
  105. package/src/permissions.ts +228 -0
  106. package/src/plugins.test.ts +390 -0
  107. package/src/plugins.ts +289 -0
  108. package/src/populate.test.ts +65 -0
  109. package/src/populate.ts +258 -0
  110. package/src/response.ts +0 -0
  111. package/src/tests/bunSetup.ts +234 -0
  112. package/src/tests/index.ts +1 -0
  113. package/src/tests.ts +218 -0
  114. package/src/transformers.test.ts +202 -0
  115. package/src/transformers.ts +170 -0
  116. package/src/utils.test.ts +14 -0
  117. package/src/utils.ts +47 -0
  118. package/tsconfig.json +60 -0
  119. 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>;