@terreno/api 0.11.9 → 0.12.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.
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,311 @@
1
+ "use strict";
2
+ var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) {
3
+ function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); }
4
+ return new (P || (P = Promise))(function (resolve, reject) {
5
+ function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } }
6
+ function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } }
7
+ function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); }
8
+ step((generator = generator.apply(thisArg, _arguments || [])).next());
9
+ });
10
+ };
11
+ var __generator = (this && this.__generator) || function (thisArg, body) {
12
+ var _ = { label: 0, sent: function() { if (t[0] & 1) throw t[1]; return t[1]; }, trys: [], ops: [] }, f, y, t, g = Object.create((typeof Iterator === "function" ? Iterator : Object).prototype);
13
+ return g.next = verb(0), g["throw"] = verb(1), g["return"] = verb(2), typeof Symbol === "function" && (g[Symbol.iterator] = function() { return this; }), g;
14
+ function verb(n) { return function (v) { return step([n, v]); }; }
15
+ function step(op) {
16
+ if (f) throw new TypeError("Generator is already executing.");
17
+ while (g && (g = 0, op[0] && (_ = 0)), _) try {
18
+ if (f = 1, y && (t = op[0] & 2 ? y["return"] : op[0] ? y["throw"] || ((t = y["return"]) && t.call(y), 0) : y.next) && !(t = t.call(y, op[1])).done) return t;
19
+ if (y = 0, t) op = [op[0] & 2, t.value];
20
+ switch (op[0]) {
21
+ case 0: case 1: t = op; break;
22
+ case 4: _.label++; return { value: op[1], done: false };
23
+ case 5: _.label++; y = op[1]; op = [0]; continue;
24
+ case 7: op = _.ops.pop(); _.trys.pop(); continue;
25
+ default:
26
+ if (!(t = _.trys, t = t.length > 0 && t[t.length - 1]) && (op[0] === 6 || op[0] === 2)) { _ = 0; continue; }
27
+ if (op[0] === 3 && (!t || (op[1] > t[0] && op[1] < t[3]))) { _.label = op[1]; break; }
28
+ if (op[0] === 6 && _.label < t[1]) { _.label = t[1]; t = op; break; }
29
+ if (t && _.label < t[2]) { _.label = t[2]; _.ops.push(op); break; }
30
+ if (t[2]) _.ops.pop();
31
+ _.trys.pop(); continue;
32
+ }
33
+ op = body.call(thisArg, _);
34
+ } catch (e) { op = [6, e]; y = 0; } finally { f = t = 0; }
35
+ if (op[0] & 5) throw op[1]; return { value: op[0] ? op[1] : void 0, done: true };
36
+ }
37
+ };
38
+ var __values = (this && this.__values) || function(o) {
39
+ var s = typeof Symbol === "function" && Symbol.iterator, m = s && o[s], i = 0;
40
+ if (m) return m.call(o);
41
+ if (o && typeof o.length === "number") return {
42
+ next: function () {
43
+ if (o && i >= o.length) o = void 0;
44
+ return { value: o && o[i++], done: !o };
45
+ }
46
+ };
47
+ throw new TypeError(s ? "Object is not iterable." : "Symbol.iterator is not defined.");
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 express_1 = __importDefault(require("express"));
55
+ var supertest_1 = __importDefault(require("supertest"));
56
+ var openApiCompat_1 = require("../openApiCompat");
57
+ var getRouterStack = function (app) {
58
+ var _a;
59
+ var internal = app;
60
+ var router = (_a = internal._router) !== null && _a !== void 0 ? _a : internal.router;
61
+ if (!router) {
62
+ throw new Error("Express app has no router");
63
+ }
64
+ return router.stack;
65
+ };
66
+ var findLayer = function (stack, predicate) {
67
+ var e_1, _a;
68
+ var _b, _c;
69
+ try {
70
+ for (var stack_1 = __values(stack), stack_1_1 = stack_1.next(); !stack_1_1.done; stack_1_1 = stack_1.next()) {
71
+ var layer = stack_1_1.value;
72
+ if (predicate(layer)) {
73
+ return layer;
74
+ }
75
+ if ((_b = layer.handle) === null || _b === void 0 ? void 0 : _b.stack) {
76
+ var nested = findLayer(layer.handle.stack, predicate);
77
+ if (nested) {
78
+ return nested;
79
+ }
80
+ }
81
+ if ((_c = layer.route) === null || _c === void 0 ? void 0 : _c.stack) {
82
+ var nested = findLayer(layer.route.stack, predicate);
83
+ if (nested) {
84
+ return nested;
85
+ }
86
+ }
87
+ }
88
+ }
89
+ catch (e_1_1) { e_1 = { error: e_1_1 }; }
90
+ finally {
91
+ try {
92
+ if (stack_1_1 && !stack_1_1.done && (_a = stack_1.return)) _a.call(stack_1);
93
+ }
94
+ finally { if (e_1) throw e_1.error; }
95
+ }
96
+ return undefined;
97
+ };
98
+ var runMiddleware = function (app) {
99
+ var middlewareReq = { app: app };
100
+ var middlewareRes = {};
101
+ var called = false;
102
+ (0, openApiCompat_1.openApiCompatMiddleware)(middlewareReq, middlewareRes, function () {
103
+ called = true;
104
+ });
105
+ if (!called) {
106
+ throw new Error("next() was not called by openApiCompatMiddleware");
107
+ }
108
+ };
109
+ (0, bun_test_1.describe)("openApiCompat", function () {
110
+ (0, bun_test_1.describe)("patchAppUse", function () {
111
+ (0, bun_test_1.it)("annotates layers added via app.use(mountPath, ...) with the mount path", function () {
112
+ var app = (0, express_1.default)();
113
+ (0, openApiCompat_1.patchAppUse)(app);
114
+ var subRouter = express_1.default.Router();
115
+ subRouter.get("/list", function (_req, res) {
116
+ res.json({ ok: true });
117
+ });
118
+ app.use("/sub", subRouter);
119
+ var stack = getRouterStack(app);
120
+ var mountedLayer = stack.find(function (layer) { return layer.__openApiMountPath !== undefined; });
121
+ (0, bun_test_1.expect)(mountedLayer).toBeDefined();
122
+ (0, bun_test_1.expect)(mountedLayer === null || mountedLayer === void 0 ? void 0 : mountedLayer.__openApiMountPath).toBe("/sub");
123
+ });
124
+ (0, bun_test_1.it)("strips trailing slashes from the recorded mount path", function () {
125
+ var app = (0, express_1.default)();
126
+ (0, openApiCompat_1.patchAppUse)(app);
127
+ var subRouter = express_1.default.Router();
128
+ subRouter.get("/", function (_req, res) {
129
+ res.send("ok");
130
+ });
131
+ app.use("/api/v1///", subRouter);
132
+ var stack = getRouterStack(app);
133
+ var mountedLayer = stack.find(function (layer) { return layer.__openApiMountPath !== undefined; });
134
+ (0, bun_test_1.expect)(mountedLayer === null || mountedLayer === void 0 ? void 0 : mountedLayer.__openApiMountPath).toBe("/api/v1");
135
+ });
136
+ (0, bun_test_1.it)("does not annotate layers when mount path is missing or '/'", function () {
137
+ var e_2, _a;
138
+ var app = (0, express_1.default)();
139
+ (0, openApiCompat_1.patchAppUse)(app);
140
+ app.use(function (_req, _res, next) { return next(); });
141
+ var subRouter = express_1.default.Router();
142
+ subRouter.get("/x", function (_req, res) { return res.send("x"); });
143
+ app.use("/", subRouter);
144
+ var stack = getRouterStack(app);
145
+ try {
146
+ for (var stack_2 = __values(stack), stack_2_1 = stack_2.next(); !stack_2_1.done; stack_2_1 = stack_2.next()) {
147
+ var layer = stack_2_1.value;
148
+ (0, bun_test_1.expect)(layer.__openApiMountPath).toBeUndefined();
149
+ }
150
+ }
151
+ catch (e_2_1) { e_2 = { error: e_2_1 }; }
152
+ finally {
153
+ try {
154
+ if (stack_2_1 && !stack_2_1.done && (_a = stack_2.return)) _a.call(stack_2);
155
+ }
156
+ finally { if (e_2) throw e_2.error; }
157
+ }
158
+ });
159
+ (0, bun_test_1.it)("returns the underlying use() return value", function () {
160
+ var app = (0, express_1.default)();
161
+ (0, openApiCompat_1.patchAppUse)(app);
162
+ var result = app.use(function (_req, _res, next) { return next(); });
163
+ (0, bun_test_1.expect)(result).toBe(app);
164
+ });
165
+ });
166
+ (0, bun_test_1.describe)("openApiCompatMiddleware", function () {
167
+ (0, bun_test_1.it)("calls next() and is a no-op when the app has no router yet", function () {
168
+ var fakeReq = { app: {} };
169
+ var fakeRes = {};
170
+ var nextCalled = false;
171
+ (0, openApiCompat_1.openApiCompatMiddleware)(fakeReq, fakeRes, function () {
172
+ nextCalled = true;
173
+ });
174
+ (0, bun_test_1.expect)(nextCalled).toBe(true);
175
+ });
176
+ (0, bun_test_1.it)("sets fast_slash regexp on layers that use Express 5 .slash", function () {
177
+ var app = (0, express_1.default)();
178
+ (0, openApiCompat_1.patchAppUse)(app);
179
+ app.use(function (_req, _res, next) { return next(); });
180
+ var stack = getRouterStack(app);
181
+ var slashLayer = stack.find(function (layer) { return layer.slash === true; });
182
+ (0, bun_test_1.expect)(slashLayer).toBeDefined();
183
+ runMiddleware(app);
184
+ var patchedRegexp = slashLayer === null || slashLayer === void 0 ? void 0 : slashLayer.regexp;
185
+ (0, bun_test_1.expect)(patchedRegexp === null || patchedRegexp === void 0 ? void 0 : patchedRegexp.fast_slash).toBe(true);
186
+ });
187
+ (0, bun_test_1.it)("builds a regexp for sub-routers mounted at a non-root path", function () {
188
+ var app = (0, express_1.default)();
189
+ (0, openApiCompat_1.patchAppUse)(app);
190
+ var subRouter = express_1.default.Router();
191
+ subRouter.get("/foo", function (_req, res) { return res.send("foo"); });
192
+ app.use("/sub", subRouter);
193
+ runMiddleware(app);
194
+ var stack = getRouterStack(app);
195
+ var subLayer = stack.find(function (layer) { return layer.__openApiMountPath === "/sub"; });
196
+ (0, bun_test_1.expect)(subLayer).toBeDefined();
197
+ var regexp = subLayer === null || subLayer === void 0 ? void 0 : subLayer.regexp;
198
+ (0, bun_test_1.expect)(regexp).toBeInstanceOf(RegExp);
199
+ (0, bun_test_1.expect)(regexp.test("/sub/foo")).toBe(true);
200
+ });
201
+ (0, bun_test_1.it)("builds a regexp from layer.path and extracts :param keys", function () {
202
+ var app = (0, express_1.default)();
203
+ (0, openApiCompat_1.patchAppUse)(app);
204
+ var stack = getRouterStack(app);
205
+ stack.push({
206
+ name: "boundDispatch",
207
+ path: "/users/:userId",
208
+ });
209
+ runMiddleware(app);
210
+ var inserted = stack[stack.length - 1];
211
+ (0, bun_test_1.expect)(inserted.regexp).toBeInstanceOf(RegExp);
212
+ (0, bun_test_1.expect)(inserted.regexp.test("/users/abc")).toBe(true);
213
+ (0, bun_test_1.expect)(inserted.keys).toEqual([{ name: "userId", optional: false }]);
214
+ });
215
+ (0, bun_test_1.it)("builds a regexp from route.path for plain route layers", function () { return __awaiter(void 0, void 0, void 0, function () {
216
+ var app, stack, routeLayer, res;
217
+ return __generator(this, function (_a) {
218
+ switch (_a.label) {
219
+ case 0:
220
+ app = (0, express_1.default)();
221
+ (0, openApiCompat_1.patchAppUse)(app);
222
+ app.get("/items/:itemId", function (_req, res) {
223
+ res.json({ ok: true });
224
+ });
225
+ runMiddleware(app);
226
+ stack = getRouterStack(app);
227
+ routeLayer = findLayer(stack, function (layer) { var _a; return ((_a = layer.route) === null || _a === void 0 ? void 0 : _a.path) === "/items/:itemId"; });
228
+ (0, bun_test_1.expect)(routeLayer).toBeDefined();
229
+ (0, bun_test_1.expect)(routeLayer === null || routeLayer === void 0 ? void 0 : routeLayer.regexp).toBeInstanceOf(RegExp);
230
+ (0, bun_test_1.expect)((routeLayer === null || routeLayer === void 0 ? void 0 : routeLayer.regexp).test("/items/123")).toBe(true);
231
+ (0, bun_test_1.expect)(routeLayer === null || routeLayer === void 0 ? void 0 : routeLayer.keys).toEqual([{ name: "itemId", optional: false }]);
232
+ return [4 /*yield*/, (0, supertest_1.default)(app).get("/items/123").expect(200)];
233
+ case 1:
234
+ res = _a.sent();
235
+ (0, bun_test_1.expect)(res.body).toEqual({ ok: true });
236
+ return [2 /*return*/];
237
+ }
238
+ });
239
+ }); });
240
+ (0, bun_test_1.it)("falls back to /^\\/?$/ when no path information is available", function () {
241
+ var app = (0, express_1.default)();
242
+ (0, openApiCompat_1.patchAppUse)(app);
243
+ var stack = getRouterStack(app);
244
+ stack.push({ name: "anonymous" });
245
+ runMiddleware(app);
246
+ var inserted = stack[stack.length - 1];
247
+ (0, bun_test_1.expect)(inserted.regexp).toBeInstanceOf(RegExp);
248
+ (0, bun_test_1.expect)(inserted.regexp.source).toBe("^\\/?$");
249
+ (0, bun_test_1.expect)(inserted.keys).toEqual([]);
250
+ });
251
+ (0, bun_test_1.it)("skips layers that already have a regexp set", function () {
252
+ var app = (0, express_1.default)();
253
+ (0, openApiCompat_1.patchAppUse)(app);
254
+ var stack = getRouterStack(app);
255
+ var existing = /^prebuilt$/;
256
+ stack.push({
257
+ name: "preset",
258
+ path: "/should-be-ignored/:id",
259
+ regexp: existing,
260
+ });
261
+ runMiddleware(app);
262
+ var inserted = stack[stack.length - 1];
263
+ (0, bun_test_1.expect)(inserted.regexp).toBe(existing);
264
+ (0, bun_test_1.expect)(inserted.keys).toBeUndefined();
265
+ });
266
+ (0, bun_test_1.it)("converts Express 5 string keys arrays into {name, optional} objects", function () {
267
+ var app = (0, express_1.default)();
268
+ (0, openApiCompat_1.patchAppUse)(app);
269
+ var stack = getRouterStack(app);
270
+ stack.push({
271
+ keys: ["userId", "postId"],
272
+ name: "boundDispatch",
273
+ path: "/users/:userId/posts/:postId",
274
+ });
275
+ runMiddleware(app);
276
+ var inserted = stack[stack.length - 1];
277
+ (0, bun_test_1.expect)(inserted.keys).toEqual([
278
+ { name: "userId", optional: false },
279
+ { name: "postId", optional: false },
280
+ ]);
281
+ });
282
+ (0, bun_test_1.it)("recurses into nested router stacks added via patchAppUse", function () {
283
+ var app = (0, express_1.default)();
284
+ (0, openApiCompat_1.patchAppUse)(app);
285
+ var inner = express_1.default.Router();
286
+ inner.get("/widgets/:widgetId", function (_req, res) { return res.send("widget"); });
287
+ app.use("/api", inner);
288
+ runMiddleware(app);
289
+ var stack = getRouterStack(app);
290
+ var widgetLayer = findLayer(stack, function (layer) { var _a; return ((_a = layer.route) === null || _a === void 0 ? void 0 : _a.path) === "/widgets/:widgetId"; });
291
+ (0, bun_test_1.expect)(widgetLayer).toBeDefined();
292
+ (0, bun_test_1.expect)(widgetLayer === null || widgetLayer === void 0 ? void 0 : widgetLayer.regexp).toBeInstanceOf(RegExp);
293
+ (0, bun_test_1.expect)((widgetLayer === null || widgetLayer === void 0 ? void 0 : widgetLayer.regexp).test("/widgets/42")).toBe(true);
294
+ (0, bun_test_1.expect)(widgetLayer === null || widgetLayer === void 0 ? void 0 : widgetLayer.keys).toEqual([{ name: "widgetId", optional: false }]);
295
+ });
296
+ (0, bun_test_1.it)("escapes regex metacharacters in static path segments", function () {
297
+ var app = (0, express_1.default)();
298
+ (0, openApiCompat_1.patchAppUse)(app);
299
+ var stack = getRouterStack(app);
300
+ stack.push({
301
+ name: "boundDispatch",
302
+ path: "/foo.bar/baz",
303
+ });
304
+ runMiddleware(app);
305
+ var inserted = stack[stack.length - 1];
306
+ var regexp = inserted.regexp;
307
+ (0, bun_test_1.expect)(regexp.test("/foo.bar/baz")).toBe(true);
308
+ (0, bun_test_1.expect)(regexp.test("/fooXbar/baz")).toBe(false);
309
+ });
310
+ });
311
+ });
@@ -40,6 +40,7 @@ var __importDefault = (this && this.__importDefault) || function (mod) {
40
40
  };
41
41
  Object.defineProperty(exports, "__esModule", { value: true });
42
42
  exports.addGitHubAuthRoutes = exports.setupGitHubAuth = exports.githubUserPlugin = void 0;
43
+ var express_1 = require("express");
43
44
  var passport_1 = __importDefault(require("passport"));
44
45
  var passport_github2_1 = require("passport-github2");
45
46
  var auth_1 = require("./auth");
@@ -178,7 +179,7 @@ exports.setupGitHubAuth = setupGitHubAuth;
178
179
  * - DELETE /auth/github/unlink - Unlinks GitHub account from authenticated user (requires JWT auth)
179
180
  */
180
181
  var addGitHubAuthRoutes = function (app, userModel, githubOptions, authOptions) {
181
- var router = require("express").Router();
182
+ var router = (0, express_1.Router)();
182
183
  // Initiate GitHub OAuth flow
183
184
  router.get("/github", function (req, _res, next) {
184
185
  // Store the return URL in session or query for redirect after auth
@@ -8,6 +8,7 @@
8
8
  *
9
9
  * @see https://github.com/wesleytodd/express-openapi/issues/70
10
10
  */
11
+ import type { Application, NextFunction, Request, Response } from "express";
11
12
  /**
12
13
  * Wraps an Express app's `use` method to record the mount path on each
13
14
  * layer added to the router stack. This runs at setup time so that
@@ -15,9 +16,9 @@
15
16
  *
16
17
  * Must be called before any routes are registered.
17
18
  */
18
- export declare const patchAppUse: (app: any) => void;
19
+ export declare const patchAppUse: (app: Application) => void;
19
20
  /**
20
21
  * Express middleware that patches the router stack before OpenAPI doc
21
22
  * generation. Must be mounted before the openapi middleware.
22
23
  */
23
- export declare const openApiCompatMiddleware: (req: any, _res: any, next: () => void) => void;
24
+ export declare const openApiCompatMiddleware: (req: Request, _res: Response, next: NextFunction) => void;
@@ -1,14 +1,4 @@
1
1
  "use strict";
2
- /**
3
- * Patches the Express router stack to add `.regexp` on layers for
4
- * compatibility with @wesleytodd/openapi, which expects Express 4-style
5
- * layers with `.regexp.fast_slash`.
6
- *
7
- * In Express 5 (router@2.x), layers use `.slash` (boolean) and `.matchers`
8
- * (array of functions) instead of `.regexp`.
9
- *
10
- * @see https://github.com/wesleytodd/express-openapi/issues/70
11
- */
12
2
  var __values = (this && this.__values) || function(o) {
13
3
  var s = typeof Symbol === "function" && Symbol.iterator, m = s && o[s], i = 0;
14
4
  if (m) return m.call(o);
@@ -157,19 +147,20 @@ var patchRouterStack = function (stack) {
157
147
  * Must be called before any routes are registered.
158
148
  */
159
149
  var patchAppUse = function (app) {
150
+ var internal = app;
160
151
  var originalUse = app.use.bind(app);
161
152
  var patchedUse = function () {
162
- var _a, _b;
153
+ var _a, _b, _c, _d;
163
154
  var args = [];
164
155
  for (var _i = 0; _i < arguments.length; _i++) {
165
156
  args[_i] = arguments[_i];
166
157
  }
167
158
  // Track stack length before the call
168
- var router = app._router || app.router;
169
- var stackBefore = (_b = (_a = router === null || router === void 0 ? void 0 : router.stack) === null || _a === void 0 ? void 0 : _a.length) !== null && _b !== void 0 ? _b : 0;
159
+ var router = (_a = internal._router) !== null && _a !== void 0 ? _a : internal.router;
160
+ var stackBefore = (_c = (_b = router === null || router === void 0 ? void 0 : router.stack) === null || _b === void 0 ? void 0 : _b.length) !== null && _c !== void 0 ? _c : 0;
170
161
  var result = originalUse.apply(void 0, __spreadArray([], __read(args), false));
171
162
  // After use(), check if new layers were added and annotate them
172
- var routerAfter = app._router || app.router;
163
+ var routerAfter = (_d = internal._router) !== null && _d !== void 0 ? _d : internal.router;
173
164
  if (routerAfter === null || routerAfter === void 0 ? void 0 : routerAfter.stack) {
174
165
  var stackAfter = routerAfter.stack.length;
175
166
  // The first arg is the mount path if it's a string
@@ -190,7 +181,9 @@ exports.patchAppUse = patchAppUse;
190
181
  * generation. Must be mounted before the openapi middleware.
191
182
  */
192
183
  var openApiCompatMiddleware = function (req, _res, next) {
193
- var router = req.app._router || req.app.router;
184
+ var _a;
185
+ var internal = req.app;
186
+ var router = (_a = internal._router) !== null && _a !== void 0 ? _a : internal.router;
194
187
  if (router === null || router === void 0 ? void 0 : router.stack) {
195
188
  patchRouterStack(router.stack);
196
189
  }
@@ -197,6 +197,7 @@ function serialize(req, options, data) {
197
197
  */
198
198
  function defaultResponseHandler(doc, method, request, options) {
199
199
  return __awaiter(this, void 0, void 0, function () {
200
+ var errorObj;
200
201
  return __generator(this, function (_a) {
201
202
  if (!doc) {
202
203
  return [2 /*return*/, null];
@@ -205,10 +206,11 @@ function defaultResponseHandler(doc, method, request, options) {
205
206
  return [2 /*return*/, serialize(request, options, doc)];
206
207
  }
207
208
  catch (error) {
209
+ errorObj = error;
208
210
  throw new errors_1.APIError({
209
- error: error,
211
+ error: errorObj,
210
212
  status: 400,
211
- title: "Error serializing ".concat(method, " response: ").concat(error.message),
213
+ title: "Error serializing ".concat(method, " response: ").concat(errorObj.message),
212
214
  });
213
215
  }
214
216
  return [2 /*return*/];
package/package.json CHANGED
@@ -104,5 +104,5 @@
104
104
  "updateSnapshot": "bun test --update-snapshots"
105
105
  },
106
106
  "types": "dist/index.d.ts",
107
- "version": "0.11.9"
107
+ "version": "0.12.1"
108
108
  }
@@ -0,0 +1,299 @@
1
+ import {describe, expect, it} from "bun:test";
2
+ import express, {type Application, type Request, type Response} from "express";
3
+ import supertest from "supertest";
4
+
5
+ import {openApiCompatMiddleware, patchAppUse} from "../openApiCompat";
6
+
7
+ interface PatchedLayer {
8
+ name?: string;
9
+ regexp?: {fast_slash?: boolean} | RegExp;
10
+ slash?: boolean;
11
+ path?: string;
12
+ route?: {path?: string; stack?: PatchedLayer[]};
13
+ handle?: {stack?: PatchedLayer[]};
14
+ keys?: Array<{name: string; optional: boolean}> | string[];
15
+ __openApiMountPath?: string;
16
+ }
17
+
18
+ interface AppWithRouter {
19
+ _router?: {stack: PatchedLayer[]};
20
+ router?: {stack: PatchedLayer[]};
21
+ }
22
+
23
+ const getRouterStack = (app: Application): PatchedLayer[] => {
24
+ const internal = app as unknown as AppWithRouter;
25
+ const router = internal._router ?? internal.router;
26
+ if (!router) {
27
+ throw new Error("Express app has no router");
28
+ }
29
+ return router.stack as PatchedLayer[];
30
+ };
31
+
32
+ const findLayer = (
33
+ stack: PatchedLayer[],
34
+ predicate: (layer: PatchedLayer) => boolean
35
+ ): PatchedLayer | undefined => {
36
+ for (const layer of stack) {
37
+ if (predicate(layer)) {
38
+ return layer;
39
+ }
40
+ if (layer.handle?.stack) {
41
+ const nested = findLayer(layer.handle.stack, predicate);
42
+ if (nested) {
43
+ return nested;
44
+ }
45
+ }
46
+ if (layer.route?.stack) {
47
+ const nested = findLayer(layer.route.stack, predicate);
48
+ if (nested) {
49
+ return nested;
50
+ }
51
+ }
52
+ }
53
+ return undefined;
54
+ };
55
+
56
+ const runMiddleware = (app: Application): void => {
57
+ const middlewareReq = {app} as unknown as Request;
58
+ const middlewareRes = {} as Response;
59
+ let called = false;
60
+ openApiCompatMiddleware(middlewareReq, middlewareRes, () => {
61
+ called = true;
62
+ });
63
+ if (!called) {
64
+ throw new Error("next() was not called by openApiCompatMiddleware");
65
+ }
66
+ };
67
+
68
+ describe("openApiCompat", () => {
69
+ describe("patchAppUse", () => {
70
+ it("annotates layers added via app.use(mountPath, ...) with the mount path", () => {
71
+ const app = express();
72
+ patchAppUse(app);
73
+
74
+ const subRouter = express.Router();
75
+ subRouter.get("/list", (_req, res) => {
76
+ res.json({ok: true});
77
+ });
78
+ app.use("/sub", subRouter);
79
+
80
+ const stack = getRouterStack(app);
81
+ const mountedLayer = stack.find((layer) => layer.__openApiMountPath !== undefined) as
82
+ | PatchedLayer
83
+ | undefined;
84
+ expect(mountedLayer).toBeDefined();
85
+ expect(mountedLayer?.__openApiMountPath).toBe("/sub");
86
+ });
87
+
88
+ it("strips trailing slashes from the recorded mount path", () => {
89
+ const app = express();
90
+ patchAppUse(app);
91
+
92
+ const subRouter = express.Router();
93
+ subRouter.get("/", (_req, res) => {
94
+ res.send("ok");
95
+ });
96
+ app.use("/api/v1///", subRouter);
97
+
98
+ const stack = getRouterStack(app);
99
+ const mountedLayer = stack.find((layer) => layer.__openApiMountPath !== undefined) as
100
+ | PatchedLayer
101
+ | undefined;
102
+ expect(mountedLayer?.__openApiMountPath).toBe("/api/v1");
103
+ });
104
+
105
+ it("does not annotate layers when mount path is missing or '/'", () => {
106
+ const app = express();
107
+ patchAppUse(app);
108
+
109
+ app.use((_req, _res, next) => next());
110
+
111
+ const subRouter = express.Router();
112
+ subRouter.get("/x", (_req, res) => res.send("x"));
113
+ app.use("/", subRouter);
114
+
115
+ const stack = getRouterStack(app);
116
+ for (const layer of stack) {
117
+ expect(layer.__openApiMountPath).toBeUndefined();
118
+ }
119
+ });
120
+
121
+ it("returns the underlying use() return value", () => {
122
+ const app = express();
123
+ patchAppUse(app);
124
+ const result = app.use((_req, _res, next) => next());
125
+ expect(result).toBe(app);
126
+ });
127
+ });
128
+
129
+ describe("openApiCompatMiddleware", () => {
130
+ it("calls next() and is a no-op when the app has no router yet", () => {
131
+ const fakeReq = {app: {}} as unknown as Request;
132
+ const fakeRes = {} as Response;
133
+ let nextCalled = false;
134
+ openApiCompatMiddleware(fakeReq, fakeRes, () => {
135
+ nextCalled = true;
136
+ });
137
+ expect(nextCalled).toBe(true);
138
+ });
139
+
140
+ it("sets fast_slash regexp on layers that use Express 5 .slash", () => {
141
+ const app = express();
142
+ patchAppUse(app);
143
+ app.use((_req, _res, next) => next());
144
+
145
+ const stack = getRouterStack(app);
146
+ const slashLayer = stack.find((layer) => layer.slash === true) as PatchedLayer | undefined;
147
+ expect(slashLayer).toBeDefined();
148
+
149
+ runMiddleware(app);
150
+
151
+ const patchedRegexp = slashLayer?.regexp as {fast_slash?: boolean} | undefined;
152
+ expect(patchedRegexp?.fast_slash).toBe(true);
153
+ });
154
+
155
+ it("builds a regexp for sub-routers mounted at a non-root path", () => {
156
+ const app = express();
157
+ patchAppUse(app);
158
+
159
+ const subRouter = express.Router();
160
+ subRouter.get("/foo", (_req, res) => res.send("foo"));
161
+ app.use("/sub", subRouter);
162
+
163
+ runMiddleware(app);
164
+
165
+ const stack = getRouterStack(app);
166
+ const subLayer = stack.find((layer) => layer.__openApiMountPath === "/sub") as
167
+ | PatchedLayer
168
+ | undefined;
169
+ expect(subLayer).toBeDefined();
170
+ const regexp = subLayer?.regexp as RegExp;
171
+ expect(regexp).toBeInstanceOf(RegExp);
172
+ expect(regexp.test("/sub/foo")).toBe(true);
173
+ });
174
+
175
+ it("builds a regexp from layer.path and extracts :param keys", () => {
176
+ const app = express();
177
+ patchAppUse(app);
178
+
179
+ const stack = getRouterStack(app);
180
+ stack.push({
181
+ name: "boundDispatch",
182
+ path: "/users/:userId",
183
+ } as PatchedLayer);
184
+
185
+ runMiddleware(app);
186
+
187
+ const inserted = stack[stack.length - 1];
188
+ expect(inserted.regexp).toBeInstanceOf(RegExp);
189
+ expect((inserted.regexp as RegExp).test("/users/abc")).toBe(true);
190
+ expect(inserted.keys).toEqual([{name: "userId", optional: false}]);
191
+ });
192
+
193
+ it("builds a regexp from route.path for plain route layers", async () => {
194
+ const app = express();
195
+ patchAppUse(app);
196
+ app.get("/items/:itemId", (_req, res) => {
197
+ res.json({ok: true});
198
+ });
199
+
200
+ runMiddleware(app);
201
+
202
+ const stack = getRouterStack(app);
203
+ const routeLayer = findLayer(stack, (layer) => layer.route?.path === "/items/:itemId");
204
+ expect(routeLayer).toBeDefined();
205
+ expect(routeLayer?.regexp).toBeInstanceOf(RegExp);
206
+ expect((routeLayer?.regexp as RegExp).test("/items/123")).toBe(true);
207
+ expect(routeLayer?.keys).toEqual([{name: "itemId", optional: false}]);
208
+
209
+ const res = await supertest(app).get("/items/123").expect(200);
210
+ expect(res.body).toEqual({ok: true});
211
+ });
212
+
213
+ it("falls back to /^\\/?$/ when no path information is available", () => {
214
+ const app = express();
215
+ patchAppUse(app);
216
+ const stack = getRouterStack(app);
217
+ stack.push({name: "anonymous"} as PatchedLayer);
218
+
219
+ runMiddleware(app);
220
+
221
+ const inserted = stack[stack.length - 1];
222
+ expect(inserted.regexp).toBeInstanceOf(RegExp);
223
+ expect((inserted.regexp as RegExp).source).toBe("^\\/?$");
224
+ expect(inserted.keys).toEqual([]);
225
+ });
226
+
227
+ it("skips layers that already have a regexp set", () => {
228
+ const app = express();
229
+ patchAppUse(app);
230
+ const stack = getRouterStack(app);
231
+ const existing = /^prebuilt$/;
232
+ stack.push({
233
+ name: "preset",
234
+ path: "/should-be-ignored/:id",
235
+ regexp: existing,
236
+ } as PatchedLayer);
237
+
238
+ runMiddleware(app);
239
+
240
+ const inserted = stack[stack.length - 1];
241
+ expect(inserted.regexp).toBe(existing);
242
+ expect(inserted.keys).toBeUndefined();
243
+ });
244
+
245
+ it("converts Express 5 string keys arrays into {name, optional} objects", () => {
246
+ const app = express();
247
+ patchAppUse(app);
248
+ const stack = getRouterStack(app);
249
+ stack.push({
250
+ keys: ["userId", "postId"],
251
+ name: "boundDispatch",
252
+ path: "/users/:userId/posts/:postId",
253
+ } as PatchedLayer);
254
+
255
+ runMiddleware(app);
256
+
257
+ const inserted = stack[stack.length - 1];
258
+ expect(inserted.keys).toEqual([
259
+ {name: "userId", optional: false},
260
+ {name: "postId", optional: false},
261
+ ]);
262
+ });
263
+
264
+ it("recurses into nested router stacks added via patchAppUse", () => {
265
+ const app = express();
266
+ patchAppUse(app);
267
+
268
+ const inner = express.Router();
269
+ inner.get("/widgets/:widgetId", (_req, res) => res.send("widget"));
270
+ app.use("/api", inner);
271
+
272
+ runMiddleware(app);
273
+
274
+ const stack = getRouterStack(app);
275
+ const widgetLayer = findLayer(stack, (layer) => layer.route?.path === "/widgets/:widgetId");
276
+ expect(widgetLayer).toBeDefined();
277
+ expect(widgetLayer?.regexp).toBeInstanceOf(RegExp);
278
+ expect((widgetLayer?.regexp as RegExp).test("/widgets/42")).toBe(true);
279
+ expect(widgetLayer?.keys).toEqual([{name: "widgetId", optional: false}]);
280
+ });
281
+
282
+ it("escapes regex metacharacters in static path segments", () => {
283
+ const app = express();
284
+ patchAppUse(app);
285
+ const stack = getRouterStack(app);
286
+ stack.push({
287
+ name: "boundDispatch",
288
+ path: "/foo.bar/baz",
289
+ } as PatchedLayer);
290
+
291
+ runMiddleware(app);
292
+
293
+ const inserted = stack[stack.length - 1];
294
+ const regexp = inserted.regexp as RegExp;
295
+ expect(regexp.test("/foo.bar/baz")).toBe(true);
296
+ expect(regexp.test("/fooXbar/baz")).toBe(false);
297
+ });
298
+ });
299
+ });
package/src/githubAuth.ts CHANGED
@@ -1,4 +1,5 @@
1
1
  import type express from "express";
2
+ import {Router} from "express";
2
3
  import passport from "passport";
3
4
  import {Strategy as GitHubStrategy, type Profile} from "passport-github2";
4
5
  import {generateTokens, type UserModel} from "./auth";
@@ -213,7 +214,7 @@ export const addGitHubAuthRoutes = (
213
214
  githubOptions: GitHubAuthOptions,
214
215
  authOptions?: AuthOptions
215
216
  ): void => {
216
- const router = require("express").Router();
217
+ const router = Router();
217
218
 
218
219
  // Initiate GitHub OAuth flow
219
220
  router.get(
@@ -8,16 +8,38 @@
8
8
  *
9
9
  * @see https://github.com/wesleytodd/express-openapi/issues/70
10
10
  */
11
+ import type {Application, NextFunction, Request, Response} from "express";
11
12
 
12
13
  const MOUNT_PATH_KEY = "__openApiMountPath";
13
14
 
15
+ interface ExpressKey {
16
+ name: string;
17
+ optional: boolean;
18
+ }
19
+
20
+ interface PatchedLayer {
21
+ name?: string;
22
+ regexp?: {fast_slash?: boolean} | RegExp;
23
+ slash?: boolean;
24
+ path?: string;
25
+ route?: {path?: string; stack?: PatchedLayer[]};
26
+ handle?: {stack?: PatchedLayer[]};
27
+ keys?: ExpressKey[] | string[];
28
+ [MOUNT_PATH_KEY]?: string;
29
+ }
30
+
31
+ interface AppWithRouter {
32
+ _router?: {stack: PatchedLayer[]};
33
+ router?: {stack: PatchedLayer[]};
34
+ }
35
+
14
36
  /**
15
37
  * Extract Express 4-style keys from a path string.
16
38
  * Parses `:paramName` and `*paramName` segments into `{name, optional}` objects
17
39
  * that @wesleytodd/openapi expects.
18
40
  */
19
- const extractKeysFromPath = (path: string): Array<{name: string; optional: boolean}> => {
20
- const keys: Array<{name: string; optional: boolean}> = [];
41
+ const extractKeysFromPath = (path: string): ExpressKey[] => {
42
+ const keys: ExpressKey[] = [];
21
43
  const paramRegex = /[:*](\w+)\??/g;
22
44
  let match: RegExpExecArray | null;
23
45
  // biome-ignore lint/suspicious/noAssignInExpressions: standard regex exec loop
@@ -52,7 +74,7 @@ const buildRegexpForPath = (pathStr: string, isMount: boolean): RegExp => {
52
74
  return new RegExp(`^${pattern}\\/?$`);
53
75
  };
54
76
 
55
- const patchRouterStack = (stack: any[]): void => {
77
+ const patchRouterStack = (stack: PatchedLayer[]): void => {
56
78
  for (const layer of stack) {
57
79
  if (layer.regexp !== undefined) {
58
80
  continue;
@@ -66,13 +88,13 @@ const patchRouterStack = (stack: any[]): void => {
66
88
  // Express 5 layers use .slash instead of .regexp.fast_slash
67
89
  layer.regexp = {fast_slash: true};
68
90
  } else if (layer[MOUNT_PATH_KEY]) {
69
- pathStr = layer[MOUNT_PATH_KEY] as string;
91
+ pathStr = layer[MOUNT_PATH_KEY];
70
92
  layer.regexp = buildRegexpForPath(pathStr, isMount);
71
93
  } else if (layer.path && typeof layer.path === "string") {
72
- pathStr = layer.path as string;
94
+ pathStr = layer.path;
73
95
  layer.regexp = buildRegexpForPath(pathStr, false);
74
96
  } else if (layer.route?.path && typeof layer.route.path === "string") {
75
- pathStr = layer.route.path as string;
97
+ pathStr = layer.route.path;
76
98
  layer.regexp = buildRegexpForPath(pathStr, false);
77
99
  } else {
78
100
  layer.regexp = /^\/?$/;
@@ -88,7 +110,7 @@ const patchRouterStack = (stack: any[]): void => {
88
110
  }
89
111
  } else if (Array.isArray(layer.keys) && typeof layer.keys[0] === "string") {
90
112
  // Express 5 stores keys as plain strings after match() — convert to objects
91
- layer.keys = layer.keys.map((k: string) => ({name: k, optional: false}));
113
+ layer.keys = (layer.keys as string[]).map((k) => ({name: k, optional: false}));
92
114
  }
93
115
 
94
116
  // Recursively patch nested stacks
@@ -108,17 +130,18 @@ const patchRouterStack = (stack: any[]): void => {
108
130
  *
109
131
  * Must be called before any routes are registered.
110
132
  */
111
- export const patchAppUse = (app: any): void => {
133
+ export const patchAppUse = (app: Application): void => {
134
+ const internal = app as unknown as AppWithRouter;
112
135
  const originalUse = app.use.bind(app);
113
- const patchedUse = (...args: any[]): unknown => {
136
+ const patchedUse = (...args: unknown[]): unknown => {
114
137
  // Track stack length before the call
115
- const router = app._router || app.router;
138
+ const router = internal._router ?? internal.router;
116
139
  const stackBefore = router?.stack?.length ?? 0;
117
140
 
118
- const result = originalUse(...args);
141
+ const result = (originalUse as (...a: unknown[]) => unknown)(...args);
119
142
 
120
143
  // After use(), check if new layers were added and annotate them
121
- const routerAfter = app._router || app.router;
144
+ const routerAfter = internal._router ?? internal.router;
122
145
  if (routerAfter?.stack) {
123
146
  const stackAfter = routerAfter.stack.length;
124
147
  // The first arg is the mount path if it's a string
@@ -132,15 +155,16 @@ export const patchAppUse = (app: any): void => {
132
155
 
133
156
  return result;
134
157
  };
135
- app.use = patchedUse;
158
+ (app as unknown as {use: typeof patchedUse}).use = patchedUse;
136
159
  };
137
160
 
138
161
  /**
139
162
  * Express middleware that patches the router stack before OpenAPI doc
140
163
  * generation. Must be mounted before the openapi middleware.
141
164
  */
142
- export const openApiCompatMiddleware = (req: any, _res: any, next: () => void): void => {
143
- const router = req.app._router || req.app.router;
165
+ export const openApiCompatMiddleware = (req: Request, _res: Response, next: NextFunction): void => {
166
+ const internal = req.app as unknown as AppWithRouter;
167
+ const router = internal._router ?? internal.router;
144
168
  if (router?.stack) {
145
169
  patchRouterStack(router.stack);
146
170
  }
@@ -150,9 +150,9 @@ export function permissionMiddleware<T>(
150
150
  let data;
151
151
  try {
152
152
  data = await populatedQuery.exec();
153
- } catch (error: any) {
153
+ } catch (error: unknown) {
154
154
  throw new APIError({
155
- error,
155
+ error: error as Error,
156
156
  status: 500,
157
157
  title: `GET failed on ${req.params.id}`,
158
158
  });
@@ -160,11 +160,12 @@ export async function defaultResponseHandler<T>(
160
160
  }
161
161
  try {
162
162
  return serialize(request, options, doc);
163
- } catch (error: any) {
163
+ } catch (error: unknown) {
164
+ const errorObj = error as Error;
164
165
  throw new APIError({
165
- error,
166
+ error: errorObj,
166
167
  status: 400,
167
- title: `Error serializing ${method} response: ${error.message}`,
168
+ title: `Error serializing ${method} response: ${errorObj.message}`,
168
169
  });
169
170
  }
170
171
  }