Skip to content

Commit

Permalink
Include ajvFilePlugin into source and follow OpenAPI convention. (#443
Browse files Browse the repository at this point in the history
)
  • Loading branch information
arthurfiorette committed Jun 18, 2023
1 parent 0c4772a commit 180c291
Show file tree
Hide file tree
Showing 6 changed files with 175 additions and 27 deletions.
22 changes: 6 additions & 16 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -379,28 +379,18 @@ The shared schema, that is added, will look like this:
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).

```js

const ajvFilePlugin = (ajv, options = {}) => {
return ajv.addKeyword({
keyword: "isFile",
compile: (_schema, parent, _it) => {
parent.type = "file";
delete parent.isFile;
return () => true;
},
});
};

const fastify = require('fastify')({
// ...
ajv: {
// add the new ajv plugin
plugins: [/*...*/ ajvFilePlugin]
// Adds the file plugin to help @fastify/swagger schema generation
plugins: [require('@fastify/multipart').ajvFilePlugin]
}
})
const opts = {

fastify.register(require("@fastify/multipart"), {
attachFieldsToBody: true,
};
fastify.register(require(".."), opts);
});

fastify.post(
"/upload/files",
Expand Down
17 changes: 6 additions & 11 deletions examples/example-with-swagger.js
Original file line number Diff line number Diff line change
@@ -1,26 +1,21 @@
'use strict'
const ajvFilePlugin = (ajv, options = {}) => {
return ajv.addKeyword({
keyword: 'isFile',
compile: (_schema, parent, _it) => {
parent.type = 'file'
delete parent.isFile
return () => true
}
})
}

const fastify = require('fastify')({
// ...
logger: true,
ajv: {
plugins: [ajvFilePlugin]
// Adds the file plugin to help @fastify/swagger schema generation
plugins: [require('..').ajvFilePlugin]
}
})

fastify.register(require('..'), {
attachFieldsToBody: true
})

fastify.register(require('fastify-swagger'))
fastify.register(require('@fastify/swagger-ui'))

fastify.post(
'/upload/files',
{
Expand Down
5 changes: 5 additions & 0 deletions index.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -205,6 +205,11 @@ declare namespace fastifyMultipart {
onFile?: (this: FastifyRequest, part: MultipartFile) => void | Promise<void>;
}

/**
* Adds a new type `isFile` to help @fastify/swagger generate the correct schema.
*/
export function ajvFilePlugin(ajv: any): void;

export const fastifyMultipart: FastifyMultipartPlugin;
export { fastifyMultipart as default };
}
Expand Down
21 changes: 21 additions & 0 deletions index.js
Original file line number Diff line number Diff line change
Expand Up @@ -625,6 +625,26 @@ function fastifyMultipart (fastify, options, done) {
done()
}

/**
* Adds a new type `isFile` to help @fastify/swagger generate the correct schema.
*/
function ajvFilePlugin (ajv) {
return ajv.addKeyword({
keyword: 'isFile',
compile: (_schema, parent) => {
// Updates the schema to match the file type
parent.type = 'string'
parent.format = 'binary'
delete parent.isFile

return (field /* MultipartFile */) => !!field.file
},
error: {
message: 'should be a file'
}
})
}

/**
* These export configurations enable JS and TS developers
* to consumer fastify in whatever way best suits their needs.
Expand All @@ -635,3 +655,4 @@ module.exports = fp(fastifyMultipart, {
})
module.exports.default = fastifyMultipart
module.exports.fastifyMultipart = fastifyMultipart
module.exports.ajvFilePlugin = ajvFilePlugin
13 changes: 13 additions & 0 deletions test/avj-plugin.test-d.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
import fastify from 'fastify'
import { fastifyMultipart, ajvFilePlugin } from '..'

const app = fastify({
ajv: {
plugins: [
ajvFilePlugin,
(await import('..')).ajvFilePlugin
]
}
})

app.register(fastifyMultipart)
124 changes: 124 additions & 0 deletions test/multipart-ajv-file.test.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,124 @@
'use strict'

const test = require('tap').test
const Fastify = require('fastify')
const FormData = require('form-data')
const http = require('http')
const multipart = require('..')
const { once } = require('events')
const fs = require('fs')
const path = require('path')

const filePath = path.join(__dirname, '../README.md')

test('show modify the generated schema', async function (t) {
t.plan(4)

const fastify = Fastify({
ajv: {
plugins: [multipart.ajvFilePlugin]
}
})

t.teardown(fastify.close.bind(fastify))

await fastify.register(multipart, { attachFieldsToBody: true })
await fastify.register(require('@fastify/swagger'), {
mode: 'dynamic',

openapi: {
openapi: '3.1.0'
}
})

await fastify.post(
'/',
{
schema: {
operationId: 'test',
consumes: ['multipart/form-data'],
body: {
type: 'object',
properties: {
field: { isFile: true }
}
}
}
},
async function (req, reply) {
reply.code(200).send()
}
)

await fastify.ready()

t.match(fastify.swagger(), {
paths: {
'/': {
post: {
operationId: 'test',
requestBody: {
content: {
'multipart/form-data': {
schema: {
type: 'object',
properties: {
field: { type: 'string', format: 'binary' }
}
}
}
}
},
responses: {
200: { description: 'Default Response' }
}
}
}
}
})

await fastify.listen({ port: 0 })

// request without file
{
const form = new FormData()
const req = http.request({
protocol: 'http:',
hostname: 'localhost',
port: fastify.server.address().port,
path: '/',
headers: form.getHeaders(),
method: 'POST'
})

form.append('field', JSON.stringify({}), { contentType: 'application/json' })
form.pipe(req)

const [res] = await once(req, 'response')
res.resume()
await once(res, 'end')
t.equal(res.statusCode, 400) // body/field should be a file
}

// request with file
{
const form = new FormData()
const req = http.request({
protocol: 'http:',
hostname: 'localhost',
port: fastify.server.address().port,
path: '/',
headers: form.getHeaders(),
method: 'POST'
})

form.append('field', fs.createReadStream(filePath), { contentType: 'multipart/form-data' })
form.pipe(req)

const [res] = await once(req, 'response')
res.resume()
await once(res, 'end')
t.equal(res.statusCode, 200)
}
t.pass('res ended successfully')
})

0 comments on commit 180c291

Please sign in to comment.