Skip to content

Commit 180c291

Browse files
Include ajvFilePlugin into source and follow OpenAPI convention. (#443)
1 parent 0c4772a commit 180c291

File tree

6 files changed

+175
-27
lines changed

6 files changed

+175
-27
lines changed

README.md

Lines changed: 6 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -379,28 +379,18 @@ The shared schema, that is added, will look like this:
379379
If you want to use `@fastify/multipart` with `@fastify/swagger` and `@fastify/swagger-ui` you must add a new type called `isFile` and use custom instance of validator compiler [Docs](https://www.fastify.io/docs/latest/Reference/Validation-and-Serialization/#validator-compiler).
380380

381381
```js
382-
383-
const ajvFilePlugin = (ajv, options = {}) => {
384-
return ajv.addKeyword({
385-
keyword: "isFile",
386-
compile: (_schema, parent, _it) => {
387-
parent.type = "file";
388-
delete parent.isFile;
389-
return () => true;
390-
},
391-
});
392-
};
382+
393383
const fastify = require('fastify')({
394384
// ...
395385
ajv: {
396-
// add the new ajv plugin
397-
plugins: [/*...*/ ajvFilePlugin]
386+
// Adds the file plugin to help @fastify/swagger schema generation
387+
plugins: [require('@fastify/multipart').ajvFilePlugin]
398388
}
399389
})
400-
const opts = {
390+
391+
fastify.register(require("@fastify/multipart"), {
401392
attachFieldsToBody: true,
402-
};
403-
fastify.register(require(".."), opts);
393+
});
404394

405395
fastify.post(
406396
"/upload/files",

examples/example-with-swagger.js

Lines changed: 6 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -1,26 +1,21 @@
11
'use strict'
2-
const ajvFilePlugin = (ajv, options = {}) => {
3-
return ajv.addKeyword({
4-
keyword: 'isFile',
5-
compile: (_schema, parent, _it) => {
6-
parent.type = 'file'
7-
delete parent.isFile
8-
return () => true
9-
}
10-
})
11-
}
2+
123
const fastify = require('fastify')({
4+
// ...
135
logger: true,
146
ajv: {
15-
plugins: [ajvFilePlugin]
7+
// Adds the file plugin to help @fastify/swagger schema generation
8+
plugins: [require('..').ajvFilePlugin]
169
}
1710
})
1811

1912
fastify.register(require('..'), {
2013
attachFieldsToBody: true
2114
})
15+
2216
fastify.register(require('fastify-swagger'))
2317
fastify.register(require('@fastify/swagger-ui'))
18+
2419
fastify.post(
2520
'/upload/files',
2621
{

index.d.ts

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -205,6 +205,11 @@ declare namespace fastifyMultipart {
205205
onFile?: (this: FastifyRequest, part: MultipartFile) => void | Promise<void>;
206206
}
207207

208+
/**
209+
* Adds a new type `isFile` to help @fastify/swagger generate the correct schema.
210+
*/
211+
export function ajvFilePlugin(ajv: any): void;
212+
208213
export const fastifyMultipart: FastifyMultipartPlugin;
209214
export { fastifyMultipart as default };
210215
}

index.js

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -625,6 +625,26 @@ function fastifyMultipart (fastify, options, done) {
625625
done()
626626
}
627627

628+
/**
629+
* Adds a new type `isFile` to help @fastify/swagger generate the correct schema.
630+
*/
631+
function ajvFilePlugin (ajv) {
632+
return ajv.addKeyword({
633+
keyword: 'isFile',
634+
compile: (_schema, parent) => {
635+
// Updates the schema to match the file type
636+
parent.type = 'string'
637+
parent.format = 'binary'
638+
delete parent.isFile
639+
640+
return (field /* MultipartFile */) => !!field.file
641+
},
642+
error: {
643+
message: 'should be a file'
644+
}
645+
})
646+
}
647+
628648
/**
629649
* These export configurations enable JS and TS developers
630650
* to consumer fastify in whatever way best suits their needs.
@@ -635,3 +655,4 @@ module.exports = fp(fastifyMultipart, {
635655
})
636656
module.exports.default = fastifyMultipart
637657
module.exports.fastifyMultipart = fastifyMultipart
658+
module.exports.ajvFilePlugin = ajvFilePlugin

test/avj-plugin.test-d.ts

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
import fastify from 'fastify'
2+
import { fastifyMultipart, ajvFilePlugin } from '..'
3+
4+
const app = fastify({
5+
ajv: {
6+
plugins: [
7+
ajvFilePlugin,
8+
(await import('..')).ajvFilePlugin
9+
]
10+
}
11+
})
12+
13+
app.register(fastifyMultipart)

test/multipart-ajv-file.test.js

Lines changed: 124 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,124 @@
1+
'use strict'
2+
3+
const test = require('tap').test
4+
const Fastify = require('fastify')
5+
const FormData = require('form-data')
6+
const http = require('http')
7+
const multipart = require('..')
8+
const { once } = require('events')
9+
const fs = require('fs')
10+
const path = require('path')
11+
12+
const filePath = path.join(__dirname, '../README.md')
13+
14+
test('show modify the generated schema', async function (t) {
15+
t.plan(4)
16+
17+
const fastify = Fastify({
18+
ajv: {
19+
plugins: [multipart.ajvFilePlugin]
20+
}
21+
})
22+
23+
t.teardown(fastify.close.bind(fastify))
24+
25+
await fastify.register(multipart, { attachFieldsToBody: true })
26+
await fastify.register(require('@fastify/swagger'), {
27+
mode: 'dynamic',
28+
29+
openapi: {
30+
openapi: '3.1.0'
31+
}
32+
})
33+
34+
await fastify.post(
35+
'/',
36+
{
37+
schema: {
38+
operationId: 'test',
39+
consumes: ['multipart/form-data'],
40+
body: {
41+
type: 'object',
42+
properties: {
43+
field: { isFile: true }
44+
}
45+
}
46+
}
47+
},
48+
async function (req, reply) {
49+
reply.code(200).send()
50+
}
51+
)
52+
53+
await fastify.ready()
54+
55+
t.match(fastify.swagger(), {
56+
paths: {
57+
'/': {
58+
post: {
59+
operationId: 'test',
60+
requestBody: {
61+
content: {
62+
'multipart/form-data': {
63+
schema: {
64+
type: 'object',
65+
properties: {
66+
field: { type: 'string', format: 'binary' }
67+
}
68+
}
69+
}
70+
}
71+
},
72+
responses: {
73+
200: { description: 'Default Response' }
74+
}
75+
}
76+
}
77+
}
78+
})
79+
80+
await fastify.listen({ port: 0 })
81+
82+
// request without file
83+
{
84+
const form = new FormData()
85+
const req = http.request({
86+
protocol: 'http:',
87+
hostname: 'localhost',
88+
port: fastify.server.address().port,
89+
path: '/',
90+
headers: form.getHeaders(),
91+
method: 'POST'
92+
})
93+
94+
form.append('field', JSON.stringify({}), { contentType: 'application/json' })
95+
form.pipe(req)
96+
97+
const [res] = await once(req, 'response')
98+
res.resume()
99+
await once(res, 'end')
100+
t.equal(res.statusCode, 400) // body/field should be a file
101+
}
102+
103+
// request with file
104+
{
105+
const form = new FormData()
106+
const req = http.request({
107+
protocol: 'http:',
108+
hostname: 'localhost',
109+
port: fastify.server.address().port,
110+
path: '/',
111+
headers: form.getHeaders(),
112+
method: 'POST'
113+
})
114+
115+
form.append('field', fs.createReadStream(filePath), { contentType: 'multipart/form-data' })
116+
form.pipe(req)
117+
118+
const [res] = await once(req, 'response')
119+
res.resume()
120+
await once(res, 'end')
121+
t.equal(res.statusCode, 200)
122+
}
123+
t.pass('res ended successfully')
124+
})

0 commit comments

Comments
 (0)