@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 +53 -6
- package/package.json +1 -1
- package/src/errors/index.js +9 -1
- package/src/errors/not-implemented-error.js +11 -0
- package/src/index.test.js +104 -30
- package/src/plugin.js +39 -4
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',
|
|
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
|
|
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
|
|
255
|
+
This object contains all error classes used by the plugin:
|
|
210
256
|
|
|
211
|
-
- `
|
|
212
|
-
- `ScopesMismatchError`:
|
|
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`](#
|
|
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
package/src/errors/index.js
CHANGED
|
@@ -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 {
|
|
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
|
-
|
|
50
|
-
|
|
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
|
-
|
|
56
|
+
await app.register(OpenAPIRouter, { spec });
|
|
53
57
|
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
58
|
+
expect(() =>
|
|
59
|
+
app.oas.route({
|
|
60
|
+
handler: () => {},
|
|
61
|
+
operationId: 'getUnknown'
|
|
62
|
+
})
|
|
63
|
+
).toThrowError(`Missing 'getUnknown' in OpenAPI spec.`);
|
|
64
|
+
});
|
|
61
65
|
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
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
|
-
|
|
70
|
+
await app.register(OpenAPIRouter, { spec });
|
|
67
71
|
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
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
|
-
|
|
83
|
+
it('should not override internal route options', async () => {
|
|
84
|
+
const app = fastify({ logger: false });
|
|
75
85
|
|
|
76
|
-
|
|
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
|
-
|
|
80
|
-
|
|
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
|
-
|
|
103
|
+
await app.register(OpenAPIRouter, { spec });
|
|
83
104
|
|
|
84
|
-
expect(() =>
|
|
85
105
|
app.oas.route({
|
|
86
|
-
handler: async () => {
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
})
|
|
91
|
-
|
|
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
|
-
|
|
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
|
-
|
|
74
|
+
installNotImplementedRoutes: addMissingRoutes,
|
|
75
|
+
route: addRoute
|
|
41
76
|
});
|
|
42
77
|
|
|
43
78
|
// Avoid decorating the request with reference types.
|