@uphold/fastify-openapi-router-plugin 0.7.1 → 1.0.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/parser/index.js +9 -5
- package/src/parser/params.js +1 -1
- package/src/parser/security.test.js +24 -24
- package/src/plugin.js +42 -5
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/parser/index.js
CHANGED
|
@@ -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 applyParamsCoercingFn = 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
|
-
|
|
32
|
-
|
|
33
|
-
|
|
33
|
+
|
|
34
|
+
await securityFn?.(request);
|
|
35
|
+
applyParamsCoercingFn?.(request);
|
|
36
|
+
}
|
|
37
|
+
],
|
|
34
38
|
schema: {
|
|
35
39
|
headers: parseParams(operation.parameters, 'header'),
|
|
36
40
|
params: parseParams(operation.parameters, 'path'),
|
package/src/parser/params.js
CHANGED
|
@@ -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
|
|
106
|
+
const securityFn = applySecurity({ security: [] }, { security: [{ OAuth2: [] }] }, {});
|
|
107
107
|
|
|
108
|
-
expect(
|
|
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
|
|
138
|
+
const securityFn = applySecurity(operation, spec, securityHandlers);
|
|
139
139
|
|
|
140
|
-
await
|
|
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
|
|
193
|
+
const securityFn = applySecurity(operation, spec, securityHandlers);
|
|
194
194
|
|
|
195
|
-
await
|
|
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
|
|
252
|
+
const securityFn = applySecurity(operation, spec, securityHandlers);
|
|
253
253
|
|
|
254
254
|
try {
|
|
255
|
-
await
|
|
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
|
|
302
|
+
const securityFn = applySecurity(operation, spec, securityHandlers);
|
|
303
303
|
|
|
304
304
|
expect.assertions(2);
|
|
305
305
|
|
|
306
306
|
try {
|
|
307
|
-
await
|
|
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
|
|
357
|
+
const securityFn = applySecurity(operation, spec, securityHandlers);
|
|
358
358
|
|
|
359
|
-
await
|
|
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
|
|
388
|
+
const securityFn = applySecurity(operation, spec, securityHandlers);
|
|
389
389
|
|
|
390
390
|
expect.assertions(2);
|
|
391
391
|
|
|
392
392
|
try {
|
|
393
|
-
await
|
|
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
|
|
423
|
+
const securityFn = applySecurity(operation, spec, securityHandlers);
|
|
424
424
|
|
|
425
425
|
expect.assertions(3);
|
|
426
426
|
|
|
427
|
-
await
|
|
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
|
|
471
|
+
const securityFn = applySecurity(operation, spec, securityHandlers);
|
|
472
472
|
|
|
473
473
|
expect.assertions(2);
|
|
474
474
|
|
|
475
475
|
try {
|
|
476
|
-
await
|
|
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
|
|
516
|
+
const securityFn = applySecurity(operation, spec, securityHandlers);
|
|
517
517
|
|
|
518
|
-
await
|
|
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
|
|
557
|
+
const securityFn = applySecurity(operation, spec, securityHandlers);
|
|
558
558
|
|
|
559
559
|
expect.assertions(2);
|
|
560
560
|
|
|
561
561
|
try {
|
|
562
|
-
await
|
|
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
|
|
606
|
+
const securityFn = applySecurity(operation, spec, securityHandlers, securityErrorMapper);
|
|
607
607
|
|
|
608
608
|
expect.assertions(3);
|
|
609
609
|
|
|
610
610
|
try {
|
|
611
|
-
await
|
|
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
|
@@ -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.
|
|
@@ -19,6 +44,8 @@ const createRoute = (fastify, routes) => {
|
|
|
19
44
|
// Check if there is a routeOptions.onRequest hook.
|
|
20
45
|
if (typeof onRequest === 'function') {
|
|
21
46
|
route.onRequest.push(onRequest);
|
|
47
|
+
} else if (Array.isArray(onRequest)) {
|
|
48
|
+
route.onRequest.push(...onRequest);
|
|
22
49
|
}
|
|
23
50
|
|
|
24
51
|
// Register a new route.
|
|
@@ -26,6 +53,13 @@ const createRoute = (fastify, routes) => {
|
|
|
26
53
|
...routes[operationId],
|
|
27
54
|
...routeOptions
|
|
28
55
|
});
|
|
56
|
+
|
|
57
|
+
missingRoutes.delete(route);
|
|
58
|
+
};
|
|
59
|
+
|
|
60
|
+
return {
|
|
61
|
+
addMissingRoutes,
|
|
62
|
+
addRoute
|
|
29
63
|
};
|
|
30
64
|
};
|
|
31
65
|
|
|
@@ -34,10 +68,13 @@ const plugin = async (fastify, options) => {
|
|
|
34
68
|
|
|
35
69
|
const routes = await parse(options);
|
|
36
70
|
|
|
71
|
+
const { addMissingRoutes, addRoute } = createRoute(fastify, routes, options.notImplementedErrorMapper);
|
|
72
|
+
|
|
37
73
|
// Decorate fastify object.
|
|
38
74
|
fastify.decorate(DECORATOR_NAME, {
|
|
39
75
|
errors,
|
|
40
|
-
|
|
76
|
+
installNotImplementedRoutes: addMissingRoutes,
|
|
77
|
+
route: addRoute
|
|
41
78
|
});
|
|
42
79
|
|
|
43
80
|
// Avoid decorating the request with reference types.
|
|
@@ -45,7 +82,7 @@ const plugin = async (fastify, options) => {
|
|
|
45
82
|
fastify.decorateRequest(DECORATOR_NAME, null);
|
|
46
83
|
|
|
47
84
|
// Instead, decorate each incoming request.
|
|
48
|
-
fastify.addHook('onRequest', async request
|
|
85
|
+
fastify.addHook('onRequest', async function openApiRouterGlobalOnRequestHook(request) {
|
|
49
86
|
request[DECORATOR_NAME] = {
|
|
50
87
|
operation: {},
|
|
51
88
|
security: {},
|