diff --git a/.eslintignore b/.eslintignore new file mode 100644 index 0000000..face8be --- /dev/null +++ b/.eslintignore @@ -0,0 +1,121 @@ +# Logs +logs +*.log +npm-debug.log* +yarn-debug.log* +yarn-error.log* +lerna-debug.log* + +# Diagnostic reports (https://nodejs.org/api/report.html) +report.[0-9]*.[0-9]*.[0-9]*.[0-9]*.json + +# Runtime data +pids +*.pid +*.seed +*.pid.lock + +# Directory for instrumented libs generated by jscoverage/JSCover +lib-cov + +# Coverage directory used by tools like istanbul +coverage +*.lcov + +# nyc test coverage +.nyc_output + +# Grunt intermediate storage (https://gruntjs.com/creating-plugins#storing-task-files) +.grunt + +# Bower dependency directory (https://bower.io/) +bower_components + +# node-waf configuration +.lock-wscript + +# Compiled binary addons (https://nodejs.org/api/addons.html) +build/Release + +# Dependency directories +node_modules/ +jspm_packages/ + +# Snowpack dependency directory (https://snowpack.dev/) +web_modules/ + +# TypeScript cache +*.tsbuildinfo + +# Optional npm cache directory +.npm + +# Optional eslint cache +.eslintcache + +# Microbundle cache +.rpt2_cache/ +.rts2_cache_cjs/ +.rts2_cache_es/ +.rts2_cache_umd/ + +# Optional REPL history +.node_repl_history + +# Output of 'npm pack' +*.tgz + +# Yarn Integrity file +.yarn-integrity + +# dotenv environment variables file +.env +.env.test + +# parcel-bundler cache (https://parceljs.org/) +.cache +.parcel-cache + +# Next.js build output +.next +out + +# Nuxt.js build / generate output +.nuxt +dist + +# Gatsby files +.cache/ +# Comment in the public line in if your project uses Gatsby and not Next.js +# https://nextjs.org/blog/next-9-1#public-directory-support +# public + +# vuepress build output +.vuepress/dist + +# Serverless directories +.serverless/ + +# FuseBox cache +.fusebox/ + +# DynamoDB Local files +.dynamodb/ + +# TernJS port file +.tern-port + +# Stores VSCode versions used for testing VSCode extensions +.vscode-test + +# yarn v2 +.yarn/cache +.yarn/unplugged +.yarn/build-state.yml +.yarn/install-state.gz +.pnp.* + +# TypeScript Build +*.js.map +*.js +*.d.ts \ No newline at end of file diff --git a/.eslintrc b/.eslintrc new file mode 100644 index 0000000..587e81d --- /dev/null +++ b/.eslintrc @@ -0,0 +1,6 @@ +{ + "extends": "standard-with-typescript", + "parserOptions": { + "project": "./tsconfig.json" + } +} \ No newline at end of file diff --git a/.gitattributes b/.gitattributes new file mode 100644 index 0000000..c7e451c --- /dev/null +++ b/.gitattributes @@ -0,0 +1,3 @@ +* text=auto eol=lf +*.{cmd,[cC][mM][dD]} text eol=lf +*.{bat,[bB][aA][tT]} text eol=lf \ No newline at end of file diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml new file mode 100644 index 0000000..d925de2 --- /dev/null +++ b/.github/workflows/ci.yml @@ -0,0 +1,80 @@ +name: Continuous Integration +on: + push: + paths-ignore: + - "docs/**" + - "*.md" + pull_request: + paths-ignore: + - "docs/**" + - "*.md" + +env: + CI: true + COVERALLS: 0 + +jobs: + build: + runs-on: ${{ matrix.os }} + strategy: + matrix: + # Maintenance and active LTS + node-version: [12, 14, 16] + os: [ubuntu-latest, windows-latest, macOS-latest] + name: Node ${{ matrix.node-version }} + + steps: + - uses: actions/checkout@v2 + + - name: Use Node.js + id: setup_node + uses: actions/setup-node@v2.1.4 + with: + node-version: ${{ matrix.node-version }} + + - name: Install Dependencies + id: install + run: npm install --ignore-scripts + + - name: Check licenses + id: license_check + run: | + npm run license-checker --if-present + # Unit and lint tests + - name: Tests + id: test + run: npm run test + + - name: coverage + id: coverage + run: npm run coverage + + - name: Coveralls Parallel + uses: coverallsapp/github-action@v1.1.2 + with: + github-token: ${{ secrets.GITHUB_TOKEN }} + parallel: true + flag-name: run-${{ matrix.node-version }}-${{ matrix.os }} + + - name: Typescript + id: typescript_test + run: npm run typescript --if-present + + coverage: + needs: build + runs-on: ubuntu-latest + steps: + - name: Coveralls Finished + uses: coverallsapp/github-action@v1.1.2 + with: + github-token: ${{ secrets.GITHUB_TOKEN }} + parallel-finished: true + + automerge: + needs: build + runs-on: ubuntu-latest + steps: + - uses: fastify/github-action-merge-dependabot@v1.2.0 + if: ${{ github.actor == 'dependabot[bot]' && github.event_name == 'pull_request' }} + with: + github-token: ${{secrets.GITHUB_TOKEN}} \ No newline at end of file diff --git a/.github/workflows/package-manager-ci.yml b/.github/workflows/package-manager-ci.yml new file mode 100644 index 0000000..9c2e49f --- /dev/null +++ b/.github/workflows/package-manager-ci.yml @@ -0,0 +1,62 @@ +name: Package Manager CI + +on: + push: + branches: + - master + +jobs: + pnpm: + runs-on: ${{ matrix.os }} + + strategy: + matrix: + # Maintenance and active LTS + node-version: [12, 14, 16] + os: [ubuntu-latest] + + steps: + - uses: actions/checkout@v2 + + - name: Use Node.js + uses: actions/setup-node@v2.1.4 + id: setup_node + with: + node-version: ${{ matrix.node-version }} + + - name: Install with pnpm + id: install_package_manager + run: | + curl -L https://unpkg.com/@pnpm/self-installer | node + pnpm install + - name: Run tests + id: test + run: | + pnpm run test + yarn: + runs-on: ${{ matrix.os }} + + strategy: + matrix: + # Maintenance and active LTS + node-version: [12, 14, 16] + os: [ubuntu-latest] + + steps: + - uses: actions/checkout@v2 + + - name: Use Node.js + id: setup_node + uses: actions/setup-node@v2.1.4 + with: + node-version: ${{ matrix.node-version }} + + - name: Install with yarn + id: install_package_manager + run: | + curl -o- -L https://yarnpkg.com/install.sh | bash + yarn install --ignore-engines + - name: Run tests + id: test + run: | + yarn run test \ No newline at end of file diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..fab8089 --- /dev/null +++ b/.gitignore @@ -0,0 +1,127 @@ +# Logs +logs +*.log +npm-debug.log* +yarn-debug.log* +yarn-error.log* +lerna-debug.log* + +# Diagnostic reports (https://nodejs.org/api/report.html) +report.[0-9]*.[0-9]*.[0-9]*.[0-9]*.json + +# Runtime data +pids +*.pid +*.seed +*.pid.lock + +# Directory for instrumented libs generated by jscoverage/JSCover +lib-cov + +# Coverage directory used by tools like istanbul +coverage +*.lcov + +# nyc test coverage +.nyc_output + +# Grunt intermediate storage (https://gruntjs.com/creating-plugins#storing-task-files) +.grunt + +# Bower dependency directory (https://bower.io/) +bower_components + +# node-waf configuration +.lock-wscript + +# Compiled binary addons (https://nodejs.org/api/addons.html) +build/Release + +# Dependency directories +node_modules/ +jspm_packages/ + +# Snowpack dependency directory (https://snowpack.dev/) +web_modules/ + +# TypeScript cache +*.tsbuildinfo + +# Optional npm cache directory +.npm + +# Optional eslint cache +.eslintcache + +# Microbundle cache +.rpt2_cache/ +.rts2_cache_cjs/ +.rts2_cache_es/ +.rts2_cache_umd/ + +# Optional REPL history +.node_repl_history + +# Output of 'npm pack' +*.tgz + +# Yarn Integrity file +.yarn-integrity + +# dotenv environment variables file +.env +.env.test + +# parcel-bundler cache (https://parceljs.org/) +.cache +.parcel-cache + +# Next.js build output +.next +out + +# Nuxt.js build / generate output +.nuxt +dist + +# Gatsby files +.cache/ +# Comment in the public line in if your project uses Gatsby and not Next.js +# https://nextjs.org/blog/next-9-1#public-directory-support +# public + +# vuepress build output +.vuepress/dist + +# Serverless directories +.serverless/ + +# FuseBox cache +.fusebox/ + +# DynamoDB Local files +.dynamodb/ + +# TernJS port file +.tern-port + +# Stores VSCode versions used for testing VSCode extensions +.vscode-test + +# yarn v2 +.yarn.lock +.yarn/cache +.yarn/unplugged +.yarn/build-state.yml +.yarn/install-state.gz +.pnp.* + +# TypeScript Build +*.js.map +*.js +*.d.ts + +# Others +yarn.lock +!jest.config.js +!rollup.config.js \ No newline at end of file diff --git a/.npmignore b/.npmignore new file mode 100644 index 0000000..0e6ddba --- /dev/null +++ b/.npmignore @@ -0,0 +1,131 @@ +# Logs +logs +*.log +npm-debug.log* +yarn-debug.log* +yarn-error.log* +lerna-debug.log* + +# Diagnostic reports (https://nodejs.org/api/report.html) +report.[0-9]*.[0-9]*.[0-9]*.[0-9]*.json + +# Runtime data +pids +*.pid +*.seed +*.pid.lock + +# Directory for instrumented libs generated by jscoverage/JSCover +lib-cov + +# Coverage directory used by tools like istanbul +coverage +*.lcov + +# nyc test coverage +.nyc_output + +# Grunt intermediate storage (https://gruntjs.com/creating-plugins#storing-task-files) +.grunt + +# Bower dependency directory (https://bower.io/) +bower_components + +# node-waf configuration +.lock-wscript + +# Compiled binary addons (https://nodejs.org/api/addons.html) +build/Release + +# Dependency directories +node_modules/ +jspm_packages/ + +# Snowpack dependency directory (https://snowpack.dev/) +web_modules/ + +# TypeScript cache +*.tsbuildinfo + +# Optional npm cache directory +.npm + +# Optional eslint cache +.eslintcache + +# Microbundle cache +.rpt2_cache/ +.rts2_cache_cjs/ +.rts2_cache_es/ +.rts2_cache_umd/ + +# Optional REPL history +.node_repl_history + +# Output of 'npm pack' +*.tgz + +# Yarn Integrity file +.yarn-integrity + +# dotenv environment variables file +.env +.env.test + +# parcel-bundler cache (https://parceljs.org/) +.cache +.parcel-cache + +# Next.js build output +.next +out + +# Nuxt.js build / generate output +.nuxt +dist + +# Gatsby files +.cache/ +# Comment in the public line in if your project uses Gatsby and not Next.js +# https://nextjs.org/blog/next-9-1#public-directory-support +# public + +# vuepress build output +.vuepress/dist + +# Serverless directories +.serverless/ + +# FuseBox cache +.fusebox/ + +# DynamoDB Local files +.dynamodb/ + +# TernJS port file +.tern-port + +# Stores VSCode versions used for testing VSCode extensions +.vscode-test + +# yarn v2 +.yarn/cache +.yarn/unplugged +.yarn/build-state.yml +.yarn/install-state.gz +.pnp.* + +# Project Source Files +.yarn +test/ +bin/ +.vscode/ +.github/ + +.eslint* +.git* +.prettier* +jest* + +lib/**/*.ts +!lib/**/*.d.ts \ No newline at end of file diff --git a/.vscode/settings.json b/.vscode/settings.json new file mode 100644 index 0000000..4fc3674 --- /dev/null +++ b/.vscode/settings.json @@ -0,0 +1,14 @@ +{ + "files.exclude": { + "**/*.d.ts": { "when": "$(basename).ts" }, + "**/*.js.map": { "when": "$(basename)" }, + "**/*.js": { "when": "$(basename).ts" }, + "node_modules": true + }, + "editor.formatOnSave": false, + "todo-tree.tree.showCountsInTree": true, + "typescript.updateImportsOnFileMove.enabled": "always", + "[typescript]": { + "editor.defaultFormatter": "dbaeumer.vscode-eslint" + }, +} diff --git a/README.md b/README.md new file mode 100644 index 0000000..d9abc7a --- /dev/null +++ b/README.md @@ -0,0 +1,87 @@ +# fastify-formidable + +[![Continuous Integration](https://github.com/climba03003/fastify-formidable/actions/workflows/ci.yml/badge.svg)](https://github.com/climba03003/fastify-formidable/actions/workflows/ci.yml) +[![Package Manager CI](https://github.com/climba03003/fastify-formidable/actions/workflows/package-manager-ci.yml/badge.svg)](https://github.com/climba03003/fastify-formidable/actions/workflows/package-manager-ci.yml) +[![NPM version](https://img.shields.io/npm/v/@climba03003/fastify-formidable.svg?style=flat)](https://www.npmjs.com/package/@climba03003/fastify-formidable) +[![GitHub package.json version](https://img.shields.io/github/package-json/v/climba03003/fastify-formidable)](https://github.com/climba03003/fastify-formidable) +[![Coverage Status](https://coveralls.io/repos/github/climba03003/fastify-formidable/badge.svg?branch=main)](https://coveralls.io/github/climba03003/fastify-formidable?branch=master) +[![GitHub](https://img.shields.io/github/license/climba03003/fastify-formidable)](https://github.com/climba03003/fastify-formidable) + +This plugin add a handy parser for `multipart/form-data` by using `formidable` and provide a better integration between `multipart/form-data` and `fastify-swagger` + +## Install +``` +npm install fastify-formidable --save + +yarn add fastify-formidable +``` + +## Usage + +```ts +import FastifyFormidable from 'fastify-formidable' + +fastify.register(FastifyFormidable) + +fastify.post('/', async function(request, reply) { + // you need to call the parser if you do not pass any option through plugin registration + await request.parseMultipart() + + // access files + request.files + + // access body + // note that file fields will exist in body and it will becomes the file path saved on disk + request.body +}) + +// add content type parser which will automatic parse all `multipart/form-data` found +fastify.register(FastifyFormidable, { + addContentTypeParser: true +}) + +// add `preValidation` hook which will automatic parse all `multipart/form-data` found +fastify.register(FastifyFormidable, { + addHooks: true +}) +``` + +### Options + +#### options.formidable + +The options which will be directly passed to `formidable`. + +```ts +import FastifyFormidable from 'fastify-formidable' + +fastify.register(FastifyFormidable, { + formidable: { + maxFileSize: 1000 + } +}) +``` + +See: [`formidable`](https://github.com/node-formidable/formidable#options) + +### Integration + +It is a known limitation for `fastify-multipart` integrate with `fastify-swagger` and this plugin provide a relatively simple solution for the integration. + +```ts +import Fastify from 'fastify' +import FastifyFormidable, { ajvBinaryFormat } from 'fastify-formidable' +import FastifySwagger from 'fastify-swagger' + +const fastify = Fastify({ + ajv: { + plugins: [ ajvBinaryFormat ] + } +}) + +fastify.register(FastifyFormidable, { + addContentTypeParser: true +}) + +fastify.register(FastifySwagger) +``` \ No newline at end of file diff --git a/jest.config.js b/jest.config.js new file mode 100644 index 0000000..251220e --- /dev/null +++ b/jest.config.js @@ -0,0 +1,6 @@ +module.exports = { + verbose: true, + preset: 'ts-jest', + testEnvironment: 'node', + collectCoverageFrom: ['lib/**/*'] +} diff --git a/lib/index.ts b/lib/index.ts new file mode 100644 index 0000000..d97e647 --- /dev/null +++ b/lib/index.ts @@ -0,0 +1,120 @@ +import { Ajv } from 'ajv' +import { FastifyPluginAsync, FastifyRequest } from 'fastify' +import FastifyPlugin from 'fastify-plugin' +import { Fields, File, Files, IncomingForm, Options } from 'formidable' +const kIsMultipart = Symbol.for('[isMultipart]') + +declare module 'fastify' { + interface FastifyRequest { + files: Files | null + parseMultipart: (this: FastifyRequest, options?: Options) => Promise + [kIsMultipart]: boolean + } +} + +export interface FastifyFormidableOptions { + addContentTypeParser?: boolean + addHooks?: boolean + formidable?: Options +} + +const plugin: FastifyPluginAsync = async function (fastify, options) { + const formidable = new IncomingForm(options.formidable) + + fastify.decorateRequest(kIsMultipart, false) + fastify.decorateRequest('files', null) + + fastify.decorateRequest('parseMultipart', async function (this: FastifyRequest, options?: Options) { + // eslint-disable-next-line @typescript-eslint/no-this-alias + const request = this + + let requestFormidable = formidable + + if (typeof options === 'object' && options !== null && !Array.isArray(options)) { + requestFormidable = new IncomingForm(options) + } + + return await new Promise(function (resolve, reject) { + // skip if it is not multipart + if (!request[kIsMultipart]) return reject(new Error('Cannot handle non-multipart request')) + requestFormidable.parse(request.raw, function (err, fields, files) { + if (err as true) reject(err) + request.body = Object.assign({}, fields) + Object.keys(files).forEach(function (key) { + (request.body as any)[key] = Array.isArray(files[key]) + ? (files[key] as File[]).map(function (file) { + return file.path + }) + : (files[key] as File).path + }) + request.files = files + resolve(request.body) + }) + }) + }) + + if (options.addContentTypeParser === true && options.addHooks === true) { + throw new Error('Cannot enable `addContentTypeParser` togather with `addHooks`') + } + + if (options.addContentTypeParser === true) { + fastify.addContentTypeParser('multipart', function (request, _, done) { + request[kIsMultipart] = true + formidable.parse(request.raw, function (err, fields, files) { + if (err as true) done(err) + const body = Object.assign({}, fields) + Object.keys(files).forEach(function (key) { + (body as any)[key] = Array.isArray(files[key]) + ? (files[key] as File[]).map(function (file) { + return file.path + }) + : (files[key] as File).path + }) + request.files = files + done(null, body) + }) + }) + } else { + fastify.addContentTypeParser('multipart', function (request, _, done) { + request[kIsMultipart] = true + done(null) + }) + } + + if (options.addHooks === true) { + fastify.addHook('preValidation', function (request, reply, done) { + // skip if it is not multipart + if (!request[kIsMultipart]) return done() + + formidable.parse(request.raw, function (err, fields, files) { + if (err as true) done(err) + request.body = Object.assign({}, fields) + Object.keys(files).forEach(function (key) { + (request.body as any)[key] = Array.isArray(files[key]) + ? (files[key] as File[]).map(function (file) { + return file.path + }) + : (files[key] as File).path + }) + request.files = files + done() + }) + }) + } +} + +export const ajvBinaryFormat = function (ajv: Ajv): void { + ajv.addFormat('binary', { + type: 'string', + validate (o: unknown) { + // it must be string because we parse the file / binary and return the filepath + return typeof o === 'string' + } + }) +} +export const FastifyFormidable = FastifyPlugin(plugin, { + fastify: '3.x', + name: 'fastify-formidable', + dependencies: [] +}) +export default FastifyFormidable diff --git a/package.json b/package.json new file mode 100644 index 0000000..19b51c5 --- /dev/null +++ b/package.json @@ -0,0 +1,69 @@ +{ + "name": "fastify-formidable", + "version": "0.0.0", + "description": "", + "main": "lib/index.js", + "types": "lib/index.d.ts", + "exports": { + ".": { + "import": "./lib/mjs/index.js", + "require": "./lib/index.js" + } + }, + "directories": { + "lib": "lib" + }, + "scripts": { + "lint": "eslint --ext .ts lib test", + "lint:fix": "npm run lint -- --fix", + "build": "rollup -c", + "build:tsc": "tsc -b", + "unit": "jest", + "test": "npm run lint && npm run unit", + "coverage": "jest --coverage", + "prepublishOnly": "npm run build" + }, + "publishConfig": { + "access": "public", + "registry": "https://registry.npmjs.org" + }, + "repository": { + "type": "git", + "url": "https://github.com/climba03003/fastify-formidable" + }, + "author": "KaKa", + "license": "GPL-3.0", + "dependencies": { + "@types/formidable": "^1.2.3", + "fastify-plugin": "^3.0.0", + "formidable": "^1.2.2" + }, + "devDependencies": { + "@rollup/plugin-typescript": "^8.2.1", + "@types/jest": "^26.0.23", + "@types/nodemailer": "^6.4.2", + "@typescript-eslint/eslint-plugin": "4", + "eslint": "7", + "eslint-config-standard-with-typescript": "^20.0.0", + "eslint-plugin-import": "2", + "eslint-plugin-node": "11", + "eslint-plugin-promise": "5", + "eslint-plugin-standard": "5", + "fastify": "^3.15.1", + "fastify-swagger": "^4.8.2", + "form-data": "^4.0.0", + "husky": "^6.0.0", + "jest": "^26.6.3", + "prettier": "^2.3.0", + "rollup": "^2.47.0", + "ts-jest": "^26.5.6", + "typescript": "^4.2.4", + "undici": "^4.1.0" + }, + "husky": { + "hooks": { + "pre-commit": "npm test", + "pre-push": "npm test" + } + } +} diff --git a/rollup.config.js b/rollup.config.js new file mode 100644 index 0000000..b43a198 --- /dev/null +++ b/rollup.config.js @@ -0,0 +1,18 @@ +import TypeScript from '@rollup/plugin-typescript' + +export default [ + { + input: 'lib/index.ts', + output: [ + { dir: 'lib', format: 'cjs', sourcemap: true }, + ], + plugins: [TypeScript({ module: 'ESNext', outDir: 'lib', include: ['lib/**/*'] })] + }, + { + input: 'lib/index.ts', + output: [ + { dir: 'lib/mjs', format: 'esm', sourcemap: true }, + ], + plugins: [TypeScript({ module: 'ESNext', outDir: 'lib/mjs', include: ['lib/**/*'] })] + }, +] \ No newline at end of file diff --git a/test/addContentTypeParser.test.ts b/test/addContentTypeParser.test.ts new file mode 100644 index 0000000..bfc23ba --- /dev/null +++ b/test/addContentTypeParser.test.ts @@ -0,0 +1,50 @@ +import Fastify, { FastifyInstance } from 'fastify' +import * as fs from 'fs' +import { AddressInfo } from 'net' +import * as path from 'path' +import FastifyFormidable from '../lib' +import { request } from './request' +import FormData = require('form-data') + +const filePath = path.join(__dirname, '../package.json') + +describe('addContentTypeParser', function () { + let fastify: FastifyInstance + + beforeEach(async function () { + fastify = Fastify() + await fastify.register(FastifyFormidable, { + addContentTypeParser: true + }) + + fastify.post<{ Body: { foo: String, file: string } }>('/', async function (request, reply) { + return await reply.code(200).send({ + body: request.body, + files: request.files + }) + }) + + await fastify.listen(0) + }) + + test('single file', async function () { + const form = new FormData() + form.append('foo', 'bar') + form.append('file', fs.createReadStream(filePath)) + + const response = await request(`http://localhost:${(fastify.server.address() as AddressInfo).port}`, form) + + expect(response.status).toStrictEqual(200) + + const json = await response.json() + + expect(json.body.foo).toStrictEqual('bar') + expect(/\/tmp\/upload_/.test(json.body.file)).toStrictEqual(true) + expect(json.files.file).toBeDefined() + expect(json.files.file.name).toStrictEqual('package.json') + }) + + afterEach(async function () { + await fastify.close() + }) +}) diff --git a/test/addHooks.test.ts b/test/addHooks.test.ts new file mode 100644 index 0000000..2c8f694 --- /dev/null +++ b/test/addHooks.test.ts @@ -0,0 +1,50 @@ +import Fastify, { FastifyInstance } from 'fastify' +import * as fs from 'fs' +import { AddressInfo } from 'net' +import * as path from 'path' +import FastifyFormidable from '../lib' +import { request } from './request' +import FormData = require('form-data') + +const filePath = path.join(__dirname, '../package.json') + +describe('addContentTypeParser', function () { + let fastify: FastifyInstance + + beforeEach(async function () { + fastify = Fastify() + await fastify.register(FastifyFormidable, { + addHooks: true + }) + + fastify.post<{ Body: { foo: String, file: string } }>('/', async function (request, reply) { + return await reply.code(200).send({ + body: request.body, + files: request.files + }) + }) + + await fastify.listen(0) + }) + + test('single file', async function () { + const form = new FormData() + form.append('foo', 'bar') + form.append('file', fs.createReadStream(filePath)) + + const response = await request(`http://localhost:${(fastify.server.address() as AddressInfo).port}`, form) + + expect(response.status).toStrictEqual(200) + + const json = await response.json() + + expect(json.body.foo).toStrictEqual('bar') + expect(/\/tmp\/upload_/.test(json.body.file)).toStrictEqual(true) + expect(json.files.file).toBeDefined() + expect(json.files.file.name).toStrictEqual('package.json') + }) + + afterEach(async function () { + await fastify.close() + }) +}) diff --git a/test/integration.test.ts b/test/integration.test.ts new file mode 100644 index 0000000..7c25446 --- /dev/null +++ b/test/integration.test.ts @@ -0,0 +1,67 @@ +import Fastify, { FastifyInstance } from 'fastify' +import FastifySwagger from 'fastify-swagger' +import * as fs from 'fs' +import { AddressInfo } from 'net' +import * as path from 'path' +import FastifyFormidable, { ajvBinaryFormat } from '../lib' +import { request } from './request' +import FormData = require('form-data') + +const filePath = path.join(__dirname, '../package.json') + +describe('integration', function () { + let fastify: FastifyInstance + + beforeEach(async function () { + fastify = Fastify({ + ajv: { + plugins: [ajvBinaryFormat] + } + }) + await fastify.register(FastifyFormidable, { + addContentTypeParser: true + }) + await fastify.register(FastifySwagger) + + fastify.post<{ Body: { foo: String, file: string } }>('/', { + schema: { + body: { + type: 'object', + properties: { + foo: { type: 'string' }, + file: { type: 'string', format: 'binary' } + } + } + } + }, async function (request, reply) { + return await reply.code(200).send({ + body: request.body, + files: request.files + }) + }) + + await fastify.listen(0) + }) + + test('single file', async function () { + const form = new FormData() + form.append('foo', 'bar') + form.append('file', fs.createReadStream(filePath)) + + const response = await request(`http://localhost:${(fastify.server.address() as AddressInfo).port}`, form) + + console.log(response.body) + expect(response.status).toStrictEqual(200) + + const json = await response.json() + + expect(json.body.foo).toStrictEqual('bar') + expect(/\/tmp\/upload_/.test(json.body.file)).toStrictEqual(true) + expect(json.files.file).toBeDefined() + expect(json.files.file.name).toStrictEqual('package.json') + }) + + afterEach(async function () { + await fastify.close() + }) +}) diff --git a/test/parseMultipart.test.ts b/test/parseMultipart.test.ts new file mode 100644 index 0000000..e28acfb --- /dev/null +++ b/test/parseMultipart.test.ts @@ -0,0 +1,49 @@ +import Fastify, { FastifyInstance } from 'fastify' +import * as fs from 'fs' +import { AddressInfo } from 'net' +import * as path from 'path' +import FastifyFormidable from '../lib' +import { request } from './request' +import FormData = require('form-data') + +const filePath = path.join(__dirname, '../package.json') + +describe('addContentTypeParser', function () { + let fastify: FastifyInstance + + beforeEach(async function () { + fastify = Fastify() + await fastify.register(FastifyFormidable) + + fastify.post<{ Body: { foo: String, file: string } }>('/', async function (request, reply) { + const body = await request.parseMultipart() + return await reply.code(200).send({ + body: body, + files: request.files + }) + }) + + await fastify.listen(0) + }) + + test('single file', async function () { + const form = new FormData() + form.append('foo', 'bar') + form.append('file', fs.createReadStream(filePath)) + + const response = await request(`http://localhost:${(fastify.server.address() as AddressInfo).port}`, form) + + expect(response.status).toStrictEqual(200) + + const json = await response.json() + + expect(json.body.foo).toStrictEqual('bar') + expect(/\/tmp\/upload_/.test(json.body.file)).toStrictEqual(true) + expect(json.files.file).toBeDefined() + expect(json.files.file.name).toStrictEqual('package.json') + }) + + afterEach(async function () { + await fastify.close() + }) +}) diff --git a/test/request.ts b/test/request.ts new file mode 100644 index 0000000..5d58cff --- /dev/null +++ b/test/request.ts @@ -0,0 +1,36 @@ +import * as stream from 'stream' +import undici from 'undici' +import * as util from 'util' +import FormData = require('form-data') +const pump = util.promisify(stream.pipeline) + +async function resolve (source: any): Promise { + let data = '' + await pump(source, new stream.Transform({ + write (chunk: Buffer, _, done) { + data += chunk.toString() + done() + } + })) + return data +} + +export async function request (url: string, formData: FormData): Promise<{ status: number, body: string, json: () => any }> { + const requestBody = await resolve(formData) + + const response = await undici.request(url, { + method: 'POST', + body: requestBody, + headers: formData.getHeaders() + }) + + const responseBody = await resolve(response.body) + + return { + status: response.statusCode, + body: responseBody, + json () { + return JSON.parse(responseBody) + } + } +} diff --git a/tsconfig.json b/tsconfig.json new file mode 100644 index 0000000..fb1ecbc --- /dev/null +++ b/tsconfig.json @@ -0,0 +1,26 @@ +{ + "compilerOptions": { + "lib": ["ES5", "ES6", "ES2016", "ES2017", "ES2018", "ES2019"], + "module": "CommonJS", + "target": "ES2015", + "moduleResolution": "Node", + + "resolveJsonModule": true, + + "removeComments": true, + "preserveConstEnums": true, + + "sourceMap": true, + + "declaration": true, + + "strict": true, + "noImplicitAny": true, + "noImplicitThis": true, + + "skipLibCheck": true, + "esModuleInterop": true + }, + "include": ["lib/**/*", "test/**/*"], + "exclude": ["node_modules"] +}