@uphold/fastify-openapi-router-plugin 0.8.0 → 1.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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@uphold/fastify-openapi-router-plugin",
3
- "version": "0.8.0",
3
+ "version": "1.0.1",
4
4
  "description": "A plugin for Fastify to connect routes with a OpenAPI 3.x specification",
5
5
  "main": "./src/index.js",
6
6
  "types": "./types/index.d.ts",
package/src/index.test.js CHANGED
@@ -164,4 +164,58 @@ describe('Fastify plugin', () => {
164
164
  );
165
165
  });
166
166
  });
167
+
168
+ describe('encapsulation', () => {
169
+ it('should use the correct fastify instance when calling oas.route() from a child plugin', async () => {
170
+ const app = fastify({ logger: false });
171
+
172
+ await app.register(OpenAPIRouter, { spec });
173
+
174
+ await app.register(async childFastify => {
175
+ childFastify.decorateRequest('customProperty', null);
176
+ childFastify.addHook('onRequest', async request => {
177
+ request.customProperty = 'decorated-value';
178
+ });
179
+
180
+ childFastify.oas.route({
181
+ handler: async request => {
182
+ return { customProperty: request.customProperty };
183
+ },
184
+ operationId: 'getPets'
185
+ });
186
+ });
187
+
188
+ const result = await app.inject({ url: '/pets' });
189
+
190
+ expect(result.json()).toMatchObject({
191
+ customProperty: 'decorated-value'
192
+ });
193
+ });
194
+
195
+ it('should use the correct fastify instance when calling oas.installNotImplementedRoutes() from a child plugin', async () => {
196
+ const app = fastify({ logger: false });
197
+
198
+ await app.register(OpenAPIRouter, { spec });
199
+
200
+ await app.register(async childFastify => {
201
+ childFastify.decorateRequest('customProperty', null);
202
+ childFastify.addHook('onRequest', async request => {
203
+ request.customProperty = 'decorated-value';
204
+ });
205
+
206
+ childFastify.oas.route({
207
+ handler: async request => {
208
+ return { customProperty: request.customProperty };
209
+ },
210
+ operationId: 'getPets'
211
+ });
212
+
213
+ childFastify.oas.installNotImplementedRoutes();
214
+ });
215
+
216
+ const postPetsResult = await app.inject({ method: 'POST', url: '/pets' });
217
+
218
+ expect(postPetsResult.statusCode).toBe(501);
219
+ });
220
+ });
167
221
  });
@@ -21,16 +21,20 @@ export const parse = async options => {
21
21
  for (const method in methods) {
22
22
  const operation = methods[method];
23
23
 
24
+ const securityFn = applySecurity(operation, spec, options.securityHandlers, options.securityErrorMapper);
25
+ const paramsCoercingFn = applyParamsCoercing(operation);
26
+
24
27
  // Build fastify route.
25
28
  const route = {
26
29
  method: method.toUpperCase(),
27
30
  onRequest: [
28
- async function (request) {
31
+ async function openApiRouterOnRequestHook(request) {
29
32
  request[DECORATOR_NAME].operation = operation;
30
- },
31
- applySecurity(operation, spec, options.securityHandlers, options.securityErrorMapper),
32
- applyParamsCoercing(operation)
33
- ].filter(Boolean),
33
+
34
+ await securityFn?.(request);
35
+ paramsCoercingFn?.(request);
36
+ }
37
+ ],
34
38
  schema: {
35
39
  headers: parseParams(operation.parameters, 'header'),
36
40
  params: parseParams(operation.parameters, 'path'),
@@ -87,7 +87,7 @@ export const applyParamsCoercing = operation => {
87
87
  })
88
88
  .filter(Boolean);
89
89
 
90
- return async request => {
90
+ return request => {
91
91
  coerceArrayParametersFns.forEach(fn => fn(request));
92
92
  };
93
93
  };
@@ -103,9 +103,9 @@ describe('applySecurity()', () => {
103
103
  });
104
104
 
105
105
  it('should return undefined if `security` is disabled in operation', async () => {
106
- const onRequest = applySecurity({ security: [] }, { security: [{ OAuth2: [] }] }, {});
106
+ const securityFn = applySecurity({ security: [] }, { security: [{ OAuth2: [] }] }, {});
107
107
 
108
- expect(onRequest).toBeUndefined();
108
+ expect(securityFn).toBeUndefined();
109
109
  });
110
110
 
111
111
  it('should stop at the first successful security block', async () => {
@@ -135,9 +135,9 @@ describe('applySecurity()', () => {
135
135
  OAuth2: vi.fn(async () => ({ data: 'OAuth2 data', scopes: [] }))
136
136
  };
137
137
 
138
- const onRequest = applySecurity(operation, spec, securityHandlers);
138
+ const securityFn = applySecurity(operation, spec, securityHandlers);
139
139
 
140
- await onRequest(request);
140
+ await securityFn(request);
141
141
 
142
142
  expect(securityHandlers.ApiKey).toHaveBeenCalledTimes(1);
143
143
  expect(securityHandlers.ApiKey).toHaveBeenCalledWith('api key', request);
@@ -190,9 +190,9 @@ describe('applySecurity()', () => {
190
190
  OAuth2: vi.fn(async () => ({ data: 'OAuth2 data', scopes: [] }))
191
191
  };
192
192
 
193
- const onRequest = applySecurity(operation, spec, securityHandlers);
193
+ const securityFn = applySecurity(operation, spec, securityHandlers);
194
194
 
195
- await onRequest(request);
195
+ await securityFn(request);
196
196
 
197
197
  expect(securityHandlers.ApiKey).toHaveBeenCalledTimes(1);
198
198
  expect(securityHandlers.ApiKey).toHaveBeenCalledWith('api key', request);
@@ -249,10 +249,10 @@ describe('applySecurity()', () => {
249
249
  OAuth2: vi.fn(async () => ({ data: 'OAuth2 data', scopes: [] }))
250
250
  };
251
251
 
252
- const onRequest = applySecurity(operation, spec, securityHandlers);
252
+ const securityFn = applySecurity(operation, spec, securityHandlers);
253
253
 
254
254
  try {
255
- await onRequest(request);
255
+ await securityFn(request);
256
256
  } catch (error) {
257
257
  expect(error).toBeInstanceOf(errors.UnauthorizedError);
258
258
  expect(error.securityReport).toMatchInlineSnapshot(`
@@ -299,12 +299,12 @@ describe('applySecurity()', () => {
299
299
  })
300
300
  };
301
301
 
302
- const onRequest = applySecurity(operation, spec, securityHandlers);
302
+ const securityFn = applySecurity(operation, spec, securityHandlers);
303
303
 
304
304
  expect.assertions(2);
305
305
 
306
306
  try {
307
- await onRequest(request);
307
+ await securityFn(request);
308
308
  } catch (err) {
309
309
  expect(err).toBeInstanceOf(errors.UnauthorizedError);
310
310
  expect(err.securityReport).toMatchInlineSnapshot(`
@@ -354,9 +354,9 @@ describe('applySecurity()', () => {
354
354
  OAuth2: vi.fn(async () => ({ data: 'OAuth2 data', scopes: ['write'] }))
355
355
  };
356
356
 
357
- const onRequest = applySecurity(operation, spec, securityHandlers);
357
+ const securityFn = applySecurity(operation, spec, securityHandlers);
358
358
 
359
- await onRequest(request);
359
+ await securityFn(request);
360
360
 
361
361
  expect(securityHandlers.OAuth2).toHaveBeenCalledTimes(1);
362
362
  });
@@ -385,12 +385,12 @@ describe('applySecurity()', () => {
385
385
  })
386
386
  };
387
387
 
388
- const onRequest = applySecurity(operation, spec, securityHandlers);
388
+ const securityFn = applySecurity(operation, spec, securityHandlers);
389
389
 
390
390
  expect.assertions(2);
391
391
 
392
392
  try {
393
- await onRequest(request);
393
+ await securityFn(request);
394
394
  } catch (err) {
395
395
  expect(err).toBeInstanceOf(errors.UnauthorizedError);
396
396
  expect(securityHandlers.OAuth2).toHaveBeenCalledTimes(1);
@@ -420,11 +420,11 @@ describe('applySecurity()', () => {
420
420
  OAuth2: vi.fn(async () => ({ data: 'OAuth2 data', scopes: [] }))
421
421
  };
422
422
 
423
- const onRequest = applySecurity(operation, spec, securityHandlers);
423
+ const securityFn = applySecurity(operation, spec, securityHandlers);
424
424
 
425
425
  expect.assertions(3);
426
426
 
427
- await onRequest(request);
427
+ await securityFn(request);
428
428
 
429
429
  expect(securityHandlers.ApiKey).not.toHaveBeenCalled();
430
430
  expect(securityHandlers.OAuth2).toHaveBeenCalledTimes(1);
@@ -468,12 +468,12 @@ describe('applySecurity()', () => {
468
468
  OAuth2: vi.fn(() => ({ data: 'OAuth2 data', scopes: ['read'] }))
469
469
  };
470
470
 
471
- const onRequest = applySecurity(operation, spec, securityHandlers);
471
+ const securityFn = applySecurity(operation, spec, securityHandlers);
472
472
 
473
473
  expect.assertions(2);
474
474
 
475
475
  try {
476
- await onRequest(request);
476
+ await securityFn(request);
477
477
  } catch (err) {
478
478
  expect(err).toBeInstanceOf(errors.UnauthorizedError);
479
479
  expect(err.securityReport).toMatchInlineSnapshot(`
@@ -513,9 +513,9 @@ describe('applySecurity()', () => {
513
513
  OAuth2: vi.fn(() => {})
514
514
  };
515
515
 
516
- const onRequest = applySecurity(operation, spec, securityHandlers);
516
+ const securityFn = applySecurity(operation, spec, securityHandlers);
517
517
 
518
- await onRequest(request);
518
+ await securityFn(request);
519
519
 
520
520
  expect(request[DECORATOR_NAME].security).toMatchObject({ OAuth2: undefined });
521
521
  expect(request[DECORATOR_NAME].securityReport).toMatchInlineSnapshot(`
@@ -554,12 +554,12 @@ describe('applySecurity()', () => {
554
554
  OAuth2: vi.fn(() => {})
555
555
  };
556
556
 
557
- const onRequest = applySecurity(operation, spec, securityHandlers);
557
+ const securityFn = applySecurity(operation, spec, securityHandlers);
558
558
 
559
559
  expect.assertions(2);
560
560
 
561
561
  try {
562
- await onRequest(request);
562
+ await securityFn(request);
563
563
  } catch (err) {
564
564
  expect(err).toBeInstanceOf(errors.UnauthorizedError);
565
565
  expect(err.securityReport).toMatchInlineSnapshot(`
@@ -603,12 +603,12 @@ describe('applySecurity()', () => {
603
603
  const customError = new Error('Mapped error');
604
604
  const securityErrorMapper = vi.fn(() => customError);
605
605
 
606
- const onRequest = applySecurity(operation, spec, securityHandlers, securityErrorMapper);
606
+ const securityFn = applySecurity(operation, spec, securityHandlers, securityErrorMapper);
607
607
 
608
608
  expect.assertions(3);
609
609
 
610
610
  try {
611
- await onRequest(request);
611
+ await securityFn(request);
612
612
  } catch (err) {
613
613
  expect(err).toBe(customError);
614
614
  expect(securityErrorMapper).toHaveBeenCalledTimes(1);
package/src/plugin.js CHANGED
@@ -2,10 +2,10 @@ import { DECORATOR_NAME } from './utils/constants.js';
2
2
  import { createNotImplementedError, errors } from './errors/index.js';
3
3
  import { parse } from './parser/index.js';
4
4
 
5
- const createRoute = (fastify, routes, notImplementedErrorMapper) => {
5
+ const createRouting = (routes, notImplementedErrorMapper) => {
6
6
  const missingRoutes = new Set(Object.values(routes));
7
7
 
8
- const addMissingRoutes = () => {
8
+ const addMissingRoutes = fastify => {
9
9
  missingRoutes.forEach(route => {
10
10
  fastify.route({
11
11
  ...route,
@@ -28,7 +28,7 @@ const createRoute = (fastify, routes, notImplementedErrorMapper) => {
28
28
  missingRoutes.clear();
29
29
  };
30
30
 
31
- const addRoute = ({ method, onRequest, operationId, schema, url, ...routeOptions }) => {
31
+ const addRoute = (fastify, { method, onRequest, operationId, schema, url, ...routeOptions }) => {
32
32
  const route = routes[operationId];
33
33
 
34
34
  // Throw an error if the operation is unknown.
@@ -44,6 +44,8 @@ const createRoute = (fastify, routes, notImplementedErrorMapper) => {
44
44
  // Check if there is a routeOptions.onRequest hook.
45
45
  if (typeof onRequest === 'function') {
46
46
  route.onRequest.push(onRequest);
47
+ } else if (Array.isArray(onRequest)) {
48
+ route.onRequest.push(...onRequest);
47
49
  }
48
50
 
49
51
  // Register a new route.
@@ -66,13 +68,19 @@ const plugin = async (fastify, options) => {
66
68
 
67
69
  const routes = await parse(options);
68
70
 
69
- const { addMissingRoutes, addRoute } = createRoute(fastify, routes, options.notImplementedErrorMapper);
71
+ const { addMissingRoutes, addRoute } = createRouting(routes, options.notImplementedErrorMapper);
70
72
 
71
73
  // Decorate fastify object.
72
74
  fastify.decorate(DECORATOR_NAME, {
73
- errors,
74
- installNotImplementedRoutes: addMissingRoutes,
75
- route: addRoute
75
+ getter: function () {
76
+ const fastify = this;
77
+
78
+ return {
79
+ errors,
80
+ installNotImplementedRoutes: () => addMissingRoutes(fastify),
81
+ route: route => addRoute(fastify, route)
82
+ };
83
+ }
76
84
  });
77
85
 
78
86
  // Avoid decorating the request with reference types.
@@ -80,7 +88,7 @@ const plugin = async (fastify, options) => {
80
88
  fastify.decorateRequest(DECORATOR_NAME, null);
81
89
 
82
90
  // Instead, decorate each incoming request.
83
- fastify.addHook('onRequest', async request => {
91
+ fastify.addHook('onRequest', async function openApiRouterGlobalOnRequestHook(request) {
84
92
  request[DECORATOR_NAME] = {
85
93
  operation: {},
86
94
  security: {},