@uphold/fastify-openapi-router-plugin 0.7.1 → 0.8.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -59,6 +59,7 @@ await fastify.register(import('@fastify/fastify-openapi-router-plugin'), {
59
59
  | `spec` | `string` or `object` | **REQUIRED**. A file path or object of your OpenAPI specification. |
60
60
  | `securityHandlers` | `object` | An object containing the security handlers that match [Security Schemes](https://github.com/OAI/OpenAPI-Specification/blob/main/versions/3.1.0.md#security-scheme-object) described in your OpenAPI specification. |
61
61
  | `securityErrorMapper` | `function` | A function that allows mapping the default `UnauthorizedError` to a custom error. |
62
+ | `notImplementedErrorMapper` | `function` | A function that allows mapping the default `NotImplementedError` to a custom error. |
62
63
 
63
64
  #### `spec`
64
65
 
@@ -146,7 +147,7 @@ Any error thrown by the security handler will be internally wrapped in a `Securi
146
147
 
147
148
  #### `securityErrorMapper`
148
149
 
149
- The plugin will throw an `UnauthorizedError` when none of the `security` blocks succeed. By default, this error originates a `401` reply with `{ code: 'FST_OAS_UNAUTHORIZED', 'message': 'Unauthorized' }` as the payload. You can override this behavior by leveraging the `securityErrorMapper` option:
150
+ The plugin will throw an `UnauthorizedError` when none of the `security` blocks succeed. By default, this error originates a `401` reply with `{ code: 'FST_OAS_UNAUTHORIZED', message: 'Unauthorized' }` as the payload. You can override this behavior by leveraging the `securityErrorMapper` option:
150
151
 
151
152
  ```js
152
153
  await fastify.register(import('@fastify/fastify-openapi-router-plugin'), {
@@ -173,7 +174,8 @@ The `securityReport` property of the unauthorized error contains an array of obj
173
174
  schemes: {
174
175
  OAuth2: {
175
176
  ok: false,
176
- // The error will be either be a `fastify.oas.errors.SecurityHandlerError` or a `fastify.oas.errors.ScopesMismatchError` if the scopes were not satisfied.
177
+ // The error will be either be a `fastify.oas.errors.SecurityHandlerError` or a
178
+ // `fastify.oas.errors.ScopesMismatchError` if the scopes were not satisfied.
177
179
  error: <Error>,
178
180
  }
179
181
  }
@@ -183,6 +185,21 @@ The `securityReport` property of the unauthorized error contains an array of obj
183
185
 
184
186
  If you don't define a `securityErrorMapper`, you can still catch the `UnauthorizedError` in your fastify error handler.
185
187
 
188
+ #### `notImplementedErrorMapper`
189
+
190
+ The plugin will throw an `NotImplementedError` when you install a handler for not implemented specs through `fastify.oas.installNotImplementedRoutes()` function. By default, this error originates a `501` reply with `{ code: 'FST_OAS_NOT_IMPLEMENTED', message: 'Not implemented' }` as the payload. You can override this behavior by leveraging the `notImplementedErrorMapper` option:
191
+
192
+ ```js
193
+ await fastify.register(import('@fastify/fastify-openapi-router-plugin'), {
194
+ spec: './petstore.json',
195
+ notImplementedErrorMapper: (notImplementedError) => {
196
+ return MyNotImplementedError();
197
+ },
198
+ });
199
+ ```
200
+
201
+ If you don't define a `notImplementedErrorMapper`, you can still catch the `NotImplementedError` in your fastify error handler.
202
+
186
203
  ### Decorators
187
204
 
188
205
  #### `fastify.oas.route(options)`
@@ -204,12 +221,42 @@ fastify.oas.route({
204
221
  });
205
222
  ```
206
223
 
224
+ #### `fastify.oas.installNotImplementedRoutes()`
225
+
226
+ This function will register handlers with fastify for the routes in the spec that were not registered. You can use this, for example, when you want to give a more specific error for the consumer for operations that you still do not have an implementation.
227
+
228
+ > [!IMPORTANT]
229
+ > Make sure you `await` the calls to `fastify.register()` for your routes before calling this function to make sure that fastify executes them before the call to `fastify.oas.installNotImplementedRoutes()`.
230
+
231
+ **Example**
232
+
233
+ ```js
234
+ await fastify.register(import('@fastify/fastify-openapi-router-plugin'), {
235
+ spec: './petstore.json'
236
+ });
237
+
238
+ fastify.oas.route({
239
+ operationId: 'getPetById',
240
+ handler: (request, reply) => {}
241
+ });
242
+
243
+ // Routes in spec that were not registered through `fastify.oas.route` will get a 'not implemented' handler.
244
+ fastify.oas.installNotImplementedRoutes();
245
+
246
+ // Finish fastify setup.
247
+ await fastify.ready();
248
+ ```
249
+
250
+ > [!TIP]
251
+ > If you need to customize the `NotImplementedError` that is thrown by default, you can use the [notImplementedErrorMapper](#notimplementederrormapper) in order to change the default behavior.
252
+
207
253
  #### `fastify.oas.errors`
208
254
 
209
- This object contains all error classes that can be thrown by the plugin:
255
+ This object contains all error classes used by the plugin:
210
256
 
211
- - `UnauthorizedError`: Thrown when all security schemes verification failed.
212
- - `ScopesMismatchError`: Thrown when the scopes returned by the security handler do not satisfy the scopes defined in the API operation.
257
+ - `SecurityHandlerError`: Used to wrap a security handler error.
258
+ - `ScopesMismatchError`: Used when the scopes returned by the security handler do not satisfy the scopes defined in the API operation.
259
+ - `UnauthorizedError`: Used to indicate that the request is unauthorized, containing a `securityReport`. Check the [`securityErrorMapper`](#securityerrormapper) section for more information.
213
260
 
214
261
  #### `request.oas`
215
262
 
@@ -217,7 +264,7 @@ For your convenience, the object `request.oas` is populated with data related to
217
264
 
218
265
  - `operation` is the raw API operation that activated the Fastify route.
219
266
  - `security` is an object where keys are security scheme names and values the returned `data` field from security handlers.
220
- - `securityReport`: A detailed report of the security verification process. Check the [`securityErrorMapper`](#security-error-mapper) section for more information.
267
+ - `securityReport`: A detailed report of the security verification process. Check the [`securityErrorMapper`](#securityerrormapper) section for more information.
221
268
 
222
269
  **Example**
223
270
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@uphold/fastify-openapi-router-plugin",
3
- "version": "0.7.1",
3
+ "version": "0.8.0",
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",
@@ -1,11 +1,19 @@
1
+ import { NotImplementedError, createNotImplementedError } from './not-implemented-error.js';
1
2
  import { ScopesMismatchError, createScopesMismatchError } from './scopes-mismatch-error.js';
2
3
  import { SecurityHandlerError, createSecurityHandlerError } from './security-handler-error.js';
3
4
  import { UnauthorizedError, createUnauthorizedError } from './unauthorized-error.js';
4
5
 
5
6
  const errors = {
7
+ NotImplementedError,
6
8
  ScopesMismatchError,
7
9
  SecurityHandlerError,
8
10
  UnauthorizedError
9
11
  };
10
12
 
11
- export { createScopesMismatchError, createUnauthorizedError, createSecurityHandlerError, errors };
13
+ export {
14
+ createScopesMismatchError,
15
+ createUnauthorizedError,
16
+ createNotImplementedError,
17
+ createSecurityHandlerError,
18
+ errors
19
+ };
@@ -0,0 +1,11 @@
1
+ import createError from '@fastify/error';
2
+
3
+ const NotImplementedError = createError('FST_OAS_NOT_IMPLEMENTED', 'Not implemented', 501);
4
+
5
+ const createNotImplementedError = () => {
6
+ const err = new NotImplementedError();
7
+
8
+ return err;
9
+ };
10
+
11
+ export { NotImplementedError, createNotImplementedError };
package/src/index.test.js CHANGED
@@ -13,6 +13,9 @@ describe('Fastify plugin', () => {
13
13
  '/pets': {
14
14
  get: {
15
15
  operationId: 'getPets'
16
+ },
17
+ post: {
18
+ operationId: 'postPets'
16
19
  }
17
20
  }
18
21
  }
@@ -46,48 +49,119 @@ describe('Fastify plugin', () => {
46
49
  });
47
50
  });
48
51
 
49
- it('should throw registering a route if `operationId` is not in spec', async () => {
50
- const app = fastify({ logger: false });
52
+ describe('route()', () => {
53
+ it('should throw registering a route if `operationId` is not in spec', async () => {
54
+ const app = fastify({ logger: false });
51
55
 
52
- await app.register(OpenAPIRouter, { spec });
56
+ await app.register(OpenAPIRouter, { spec });
53
57
 
54
- expect(() =>
55
- app.oas.route({
56
- handler: () => {},
57
- operationId: 'getUnknown'
58
- })
59
- ).toThrowError(`Missing 'getUnknown' in OpenAPI spec.`);
60
- });
58
+ expect(() =>
59
+ app.oas.route({
60
+ handler: () => {},
61
+ operationId: 'getUnknown'
62
+ })
63
+ ).toThrowError(`Missing 'getUnknown' in OpenAPI spec.`);
64
+ });
61
65
 
62
- it('should set a route-level onRequest hook', async () => {
63
- const app = fastify({ logger: false });
64
- const onRequest = vi.fn(async () => {});
66
+ it('should set a route-level onRequest hook', async () => {
67
+ const app = fastify({ logger: false });
68
+ const onRequest = vi.fn(async () => {});
65
69
 
66
- await app.register(OpenAPIRouter, { spec });
70
+ await app.register(OpenAPIRouter, { spec });
67
71
 
68
- app.oas.route({
69
- handler: async () => {},
70
- onRequest,
71
- operationId: 'getPets'
72
+ app.oas.route({
73
+ handler: async () => {},
74
+ onRequest,
75
+ operationId: 'getPets'
76
+ });
77
+
78
+ await app.inject({ url: '/pets' });
79
+
80
+ expect(onRequest).toHaveBeenCalledTimes(1);
72
81
  });
73
82
 
74
- await app.inject({ url: '/pets' });
83
+ it('should not override internal route options', async () => {
84
+ const app = fastify({ logger: false });
75
85
 
76
- expect(onRequest).toHaveBeenCalledTimes(1);
86
+ await app.register(OpenAPIRouter, { spec });
87
+
88
+ expect(() =>
89
+ app.oas.route({
90
+ handler: async () => {},
91
+ method: 'PUT',
92
+ operationId: 'getPets',
93
+ url: '/pets/:id'
94
+ })
95
+ ).toThrowError(`Not allowed to override 'method', 'schema' or 'url' for operation 'getPets'.`);
96
+ });
77
97
  });
78
98
 
79
- it('should not override internal route options', async () => {
80
- const app = fastify({ logger: false });
99
+ describe('installNotImplementedRoutes()', () => {
100
+ it('should install handlers for not registered routes that will return a not implemented error', async () => {
101
+ const app = fastify({ logger: false });
81
102
 
82
- await app.register(OpenAPIRouter, { spec });
103
+ await app.register(OpenAPIRouter, { spec });
83
104
 
84
- expect(() =>
85
105
  app.oas.route({
86
- handler: async () => {},
87
- method: 'PUT',
88
- operationId: 'getPets',
89
- url: '/pets/:id'
90
- })
91
- ).toThrowError(`Not allowed to override 'method', 'schema' or 'url' for operation 'getPets'.`);
106
+ handler: async () => {
107
+ return 'pets';
108
+ },
109
+ operationId: 'getPets'
110
+ });
111
+
112
+ vi.spyOn(app, 'route');
113
+
114
+ app.oas.installNotImplementedRoutes();
115
+
116
+ expect(app.route).toHaveBeenCalledWith({
117
+ handler: expect.any(Function),
118
+ method: 'POST',
119
+ onRequest: [expect.any(Function)],
120
+ schema: {
121
+ headers: {
122
+ properties: {},
123
+ required: [],
124
+ type: 'object'
125
+ },
126
+ params: {
127
+ properties: {},
128
+ required: [],
129
+ type: 'object'
130
+ },
131
+ query: {
132
+ properties: {},
133
+ required: [],
134
+ type: 'object'
135
+ },
136
+ response: {}
137
+ },
138
+ url: '/pets'
139
+ });
140
+
141
+ const getPetsResult = await app.inject({ url: '/pets' });
142
+
143
+ expect(getPetsResult.body).toMatchInlineSnapshot('"pets"');
144
+
145
+ const postPetsResult = await app.inject({ method: 'POST', url: '/pets' });
146
+
147
+ expect(postPetsResult.body).toMatchInlineSnapshot(
148
+ `"{"statusCode":501,"code":"FST_OAS_NOT_IMPLEMENTED","error":"Not Implemented","message":"Not implemented"}"`
149
+ );
150
+ });
151
+
152
+ it('should call `notImplementedErrorMapper` option if provided', async () => {
153
+ const app = fastify({ logger: false });
154
+ const notImplementedErrorMapper = vi.fn(() => new Error('Foo'));
155
+
156
+ await app.register(OpenAPIRouter, { notImplementedErrorMapper, spec });
157
+
158
+ app.oas.installNotImplementedRoutes();
159
+
160
+ const postPetsResult = await app.inject({ method: 'POST', url: '/pets' });
161
+
162
+ expect(postPetsResult.body).toMatchInlineSnapshot(
163
+ `"{"statusCode":500,"error":"Internal Server Error","message":"Foo"}"`
164
+ );
165
+ });
92
166
  });
93
167
  });
package/src/plugin.js CHANGED
@@ -1,9 +1,34 @@
1
1
  import { DECORATOR_NAME } from './utils/constants.js';
2
- import { errors } from './errors/index.js';
2
+ import { createNotImplementedError, errors } from './errors/index.js';
3
3
  import { parse } from './parser/index.js';
4
4
 
5
- const createRoute = (fastify, routes) => {
6
- return ({ method, onRequest, operationId, schema, url, ...routeOptions }) => {
5
+ const createRoute = (fastify, routes, notImplementedErrorMapper) => {
6
+ const missingRoutes = new Set(Object.values(routes));
7
+
8
+ const addMissingRoutes = () => {
9
+ missingRoutes.forEach(route => {
10
+ fastify.route({
11
+ ...route,
12
+ handler: () => {
13
+ if (typeof notImplementedErrorMapper === 'function') {
14
+ const error = notImplementedErrorMapper(createNotImplementedError());
15
+
16
+ if (error instanceof Error) {
17
+ throw error;
18
+ }
19
+
20
+ throw createNotImplementedError();
21
+ }
22
+
23
+ throw createNotImplementedError();
24
+ }
25
+ });
26
+ });
27
+
28
+ missingRoutes.clear();
29
+ };
30
+
31
+ const addRoute = ({ method, onRequest, operationId, schema, url, ...routeOptions }) => {
7
32
  const route = routes[operationId];
8
33
 
9
34
  // Throw an error if the operation is unknown.
@@ -26,6 +51,13 @@ const createRoute = (fastify, routes) => {
26
51
  ...routes[operationId],
27
52
  ...routeOptions
28
53
  });
54
+
55
+ missingRoutes.delete(route);
56
+ };
57
+
58
+ return {
59
+ addMissingRoutes,
60
+ addRoute
29
61
  };
30
62
  };
31
63
 
@@ -34,10 +66,13 @@ const plugin = async (fastify, options) => {
34
66
 
35
67
  const routes = await parse(options);
36
68
 
69
+ const { addMissingRoutes, addRoute } = createRoute(fastify, routes, options.notImplementedErrorMapper);
70
+
37
71
  // Decorate fastify object.
38
72
  fastify.decorate(DECORATOR_NAME, {
39
73
  errors,
40
- route: createRoute(fastify, routes)
74
+ installNotImplementedRoutes: addMissingRoutes,
75
+ route: addRoute
41
76
  });
42
77
 
43
78
  // Avoid decorating the request with reference types.