From a1c81cf7b65902135fb541ce0274a3ecead5e840 Mon Sep 17 00:00:00 2001 From: Jon Ursenbach Date: Fri, 7 Aug 2020 22:33:22 -0700 Subject: [PATCH 1/9] feat: updating snippets to work with the new form-data syntax --- .../__fixtures__/output/multipart-data.js | 7 +- .../__fixtures__/output/multipart-file.js | 8 +- .../output/multipart-form-data.js | 2 +- .../request/multipart-data/definition.json | 34 ++++ .../request/multipart-file/definition.json | 34 ++++ .../multipart-form-data/definition.json | 33 +++ .../__tests__/index.test.js | 11 +- .../httpsnippet-client-api/package-lock.json | 188 ++++-------------- packages/httpsnippet-client-api/package.json | 4 +- packages/httpsnippet-client-api/src/index.js | 37 +--- 10 files changed, 160 insertions(+), 198 deletions(-) create mode 100644 packages/httpsnippet-client-api/__tests__/__fixtures__/request/multipart-data/definition.json create mode 100644 packages/httpsnippet-client-api/__tests__/__fixtures__/request/multipart-file/definition.json create mode 100644 packages/httpsnippet-client-api/__tests__/__fixtures__/request/multipart-form-data/definition.json diff --git a/packages/httpsnippet-client-api/__tests__/__fixtures__/output/multipart-data.js b/packages/httpsnippet-client-api/__tests__/__fixtures__/output/multipart-data.js index 7a0579f4..b456c3bf 100644 --- a/packages/httpsnippet-client-api/__tests__/__fixtures__/output/multipart-data.js +++ b/packages/httpsnippet-client-api/__tests__/__fixtures__/output/multipart-data.js @@ -1,11 +1,6 @@ const sdk = require('api')('https://example.com/openapi.json'); -sdk.post('/har', { - foo: { - value: 'Hello World', - options: {filename: 'hello.txt', contentType: 'text/plain'} - } -}, {'content-type': 'multipart/form-data; boundary=---011000010111000001101001'}) +sdk.post('/har', {foo: 'hello.txt'}) .then(res => res.json()) .then(res => { console.log(res); diff --git a/packages/httpsnippet-client-api/__tests__/__fixtures__/output/multipart-file.js b/packages/httpsnippet-client-api/__tests__/__fixtures__/output/multipart-file.js index 8e68bc88..42fb2703 100644 --- a/packages/httpsnippet-client-api/__tests__/__fixtures__/output/multipart-file.js +++ b/packages/httpsnippet-client-api/__tests__/__fixtures__/output/multipart-file.js @@ -1,12 +1,6 @@ -const fs = require("fs"); const sdk = require('api')('https://example.com/openapi.json'); -sdk.post('/har', { - foo: { - value: 'fs.createReadStream("test/fixtures/files/hello.txt")', - options: {filename: 'test/fixtures/files/hello.txt', contentType: 'text/plain'} - } -}, {'content-type': 'multipart/form-data; boundary=---011000010111000001101001'}) +sdk.post('/har', {foo: 'test/fixtures/files/hello.txt'}) .then(res => res.json()) .then(res => { console.log(res); diff --git a/packages/httpsnippet-client-api/__tests__/__fixtures__/output/multipart-form-data.js b/packages/httpsnippet-client-api/__tests__/__fixtures__/output/multipart-form-data.js index 42c408ac..d60c2408 100644 --- a/packages/httpsnippet-client-api/__tests__/__fixtures__/output/multipart-form-data.js +++ b/packages/httpsnippet-client-api/__tests__/__fixtures__/output/multipart-form-data.js @@ -1,6 +1,6 @@ const sdk = require('api')('https://example.com/openapi.json'); -sdk.post('/har', {foo: 'bar'}, {'content-type': 'multipart/form-data; boundary=---011000010111000001101001'}) +sdk.post('/har', {foo: 'bar'}) .then(res => res.json()) .then(res => { console.log(res); diff --git a/packages/httpsnippet-client-api/__tests__/__fixtures__/request/multipart-data/definition.json b/packages/httpsnippet-client-api/__tests__/__fixtures__/request/multipart-data/definition.json new file mode 100644 index 00000000..d4a36cf8 --- /dev/null +++ b/packages/httpsnippet-client-api/__tests__/__fixtures__/request/multipart-data/definition.json @@ -0,0 +1,34 @@ +{ + "openapi": "3.0.0", + "info": { + "version": "1.0", + "title": "application-json" + }, + "servers": [ + { + "url": "http://mockbin.com" + } + ], + "paths": { + "/har": { + "post": { + "requestBody": { + "required": true, + "content": { + "multipart/form-data": { + "schema": { + "type": "object", + "properties": { + "foo": { + "type": "string", + "format": "binary" + } + } + } + } + } + } + } + } + } +} diff --git a/packages/httpsnippet-client-api/__tests__/__fixtures__/request/multipart-file/definition.json b/packages/httpsnippet-client-api/__tests__/__fixtures__/request/multipart-file/definition.json new file mode 100644 index 00000000..d4a36cf8 --- /dev/null +++ b/packages/httpsnippet-client-api/__tests__/__fixtures__/request/multipart-file/definition.json @@ -0,0 +1,34 @@ +{ + "openapi": "3.0.0", + "info": { + "version": "1.0", + "title": "application-json" + }, + "servers": [ + { + "url": "http://mockbin.com" + } + ], + "paths": { + "/har": { + "post": { + "requestBody": { + "required": true, + "content": { + "multipart/form-data": { + "schema": { + "type": "object", + "properties": { + "foo": { + "type": "string", + "format": "binary" + } + } + } + } + } + } + } + } + } +} diff --git a/packages/httpsnippet-client-api/__tests__/__fixtures__/request/multipart-form-data/definition.json b/packages/httpsnippet-client-api/__tests__/__fixtures__/request/multipart-form-data/definition.json new file mode 100644 index 00000000..4cea45a6 --- /dev/null +++ b/packages/httpsnippet-client-api/__tests__/__fixtures__/request/multipart-form-data/definition.json @@ -0,0 +1,33 @@ +{ + "openapi": "3.0.0", + "info": { + "version": "1.0", + "title": "application-json" + }, + "servers": [ + { + "url": "http://mockbin.com" + } + ], + "paths": { + "/har": { + "post": { + "requestBody": { + "required": true, + "content": { + "multipart/form-data": { + "schema": { + "type": "object", + "properties": { + "foo": { + "type": "string" + } + } + } + } + } + } + } + } + } + } diff --git a/packages/httpsnippet-client-api/__tests__/index.test.js b/packages/httpsnippet-client-api/__tests__/index.test.js index bce5e955..196dbfd9 100644 --- a/packages/httpsnippet-client-api/__tests__/index.test.js +++ b/packages/httpsnippet-client-api/__tests__/index.test.js @@ -3,7 +3,7 @@ // Most of this has been copied over from the httpsnippet target unit test file. It'd be ideal if this were in a // helper library we could use instead. const fs = require('fs').promises; -const HTTPSnippet = require('httpsnippet'); +const HTTPSnippet = require('@readme/httpsnippet'); const path = require('path'); const client = require('../src'); @@ -63,12 +63,9 @@ describe('snippets', () => { ['issue-128'], ['jsonObj-multiline'], ['jsonObj-null-value'], - - // These tests need to be improved because the attachment handling isn't right. - // ['multipart-data'], - // ['multipart-file'], - // ['multipart-form-data'], - + ['multipart-data'], + ['multipart-file'], + ['multipart-form-data'], ['petstore'], ['query'], ['query-auth'], diff --git a/packages/httpsnippet-client-api/package-lock.json b/packages/httpsnippet-client-api/package-lock.json index 46bcb4e5..fa6ea33d 100644 --- a/packages/httpsnippet-client-api/package-lock.json +++ b/packages/httpsnippet-client-api/package-lock.json @@ -1603,6 +1603,17 @@ "eslint-plugin-unicorn": "^21.0.0" } }, + "@readme/httpsnippet": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/@readme/httpsnippet/-/httpsnippet-2.0.1.tgz", + "integrity": "sha512-yeQemCwfzF4ll4yqWaJ+Dv2m/srwAf4VxM9E1Kkz75A/q5+uqmX6ttSsK+NJl+s1G4btY30/D3OuEErtCUq6BQ==", + "requires": { + "event-stream": "4.0.1", + "form-data": "3.0.0", + "har-validator": "^5.0.0", + "stringify-object": "^3.3.0" + } + }, "@readme/oas-tooling": { "version": "3.5.6", "resolved": "https://registry.npmjs.org/@readme/oas-tooling/-/oas-tooling-3.5.6.tgz", @@ -2449,11 +2460,6 @@ "delayed-stream": "~1.0.0" } }, - "commander": { - "version": "2.20.3", - "resolved": "https://registry.npmjs.org/commander/-/commander-2.20.3.tgz", - "integrity": "sha512-GpVkmM8vF2vQUkj2LvZmD35JxeJOLCwJ9cUkugyk2nuhbv3+mJvpLYYt+0+USMxE+oj+ey/lJEnhZw75x/OMcQ==" - }, "component-emitter": { "version": "1.3.0", "resolved": "https://registry.npmjs.org/component-emitter/-/component-emitter-1.3.0.tgz", @@ -2574,6 +2580,7 @@ "version": "2.6.9", "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==", + "dev": true, "requires": { "ms": "2.0.0" } @@ -2789,7 +2796,8 @@ "escape-string-regexp": { "version": "1.0.5", "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-1.0.5.tgz", - "integrity": "sha1-G2HAViGQqN/2rjuyzwIAyhMLhtQ=" + "integrity": "sha1-G2HAViGQqN/2rjuyzwIAyhMLhtQ=", + "dev": true }, "escodegen": { "version": "1.14.3", @@ -3420,17 +3428,17 @@ "dev": true }, "event-stream": { - "version": "3.3.4", - "resolved": "https://registry.npmjs.org/event-stream/-/event-stream-3.3.4.tgz", - "integrity": "sha1-SrTJoPWlTbkzi0w02Gv86PSzVXE=", + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/event-stream/-/event-stream-4.0.1.tgz", + "integrity": "sha512-qACXdu/9VHPBzcyhdOWR5/IahhGMf0roTeZJfzz077GwylcDd90yOHLouhmv7GJ5XzPi6ekaQWd8AvPP2nOvpA==", "requires": { - "duplexer": "~0.1.1", - "from": "~0", - "map-stream": "~0.1.0", - "pause-stream": "0.0.11", - "split": "0.3", - "stream-combiner": "~0.0.4", - "through": "~2.3.1" + "duplexer": "^0.1.1", + "from": "^0.1.7", + "map-stream": "0.0.7", + "pause-stream": "^0.0.11", + "split": "^1.0.1", + "stream-combiner": "^0.2.2", + "through": "^2.3.8" } }, "exec-sh": { @@ -3790,33 +3798,6 @@ "resolved": "https://registry.npmjs.org/from/-/from-0.1.7.tgz", "integrity": "sha1-g8YK/Fi5xWmXAH7Rp2izqzA6RP4=" }, - "fs-readfile-promise": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/fs-readfile-promise/-/fs-readfile-promise-2.0.1.tgz", - "integrity": "sha1-gAI4I5gfn//+AWCei+Zo9prknnA=", - "requires": { - "graceful-fs": "^4.1.2" - } - }, - "fs-writefile-promise": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/fs-writefile-promise/-/fs-writefile-promise-1.0.3.tgz", - "integrity": "sha1-4C+bWP/CVe2CKtx6ARFPRF1I0GM=", - "requires": { - "mkdirp-promise": "^1.0.0", - "pinkie-promise": "^1.0.0" - }, - "dependencies": { - "pinkie-promise": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/pinkie-promise/-/pinkie-promise-1.0.0.tgz", - "integrity": "sha1-0dpn9UglY7t89X8oauKCLs+/NnA=", - "requires": { - "pinkie": "^1.0.0" - } - } - } - }, "fs.realpath": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/fs.realpath/-/fs.realpath-1.0.0.tgz", @@ -3930,7 +3911,8 @@ "graceful-fs": { "version": "4.2.4", "resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.2.4.tgz", - "integrity": "sha512-WjKPNJF79dtJAVniUlGGWHYGz2jWxT6VhN/4m1NdkbZ2nOsEF+cI1Edgql5zCRhs/VsQYRvrXctxktVXZUkixw==" + "integrity": "sha512-WjKPNJF79dtJAVniUlGGWHYGz2jWxT6VhN/4m1NdkbZ2nOsEF+cI1Edgql5zCRhs/VsQYRvrXctxktVXZUkixw==", + "dev": true }, "growly": { "version": "1.3.0", @@ -3962,21 +3944,6 @@ "function-bind": "^1.1.1" } }, - "has-ansi": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/has-ansi/-/has-ansi-2.0.0.tgz", - "integrity": "sha1-NPUEnOHs3ysGSa8+8k5F7TVBbZE=", - "requires": { - "ansi-regex": "^2.0.0" - }, - "dependencies": { - "ansi-regex": { - "version": "2.1.1", - "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-2.1.1.tgz", - "integrity": "sha1-w7M6te42DYbg5ijwRorn7yfWVN8=" - } - } - }, "has-flag": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-3.0.0.tgz", @@ -4073,60 +4040,6 @@ "sshpk": "^1.7.0" } }, - "httpsnippet": { - "version": "1.21.0", - "resolved": "https://registry.npmjs.org/httpsnippet/-/httpsnippet-1.21.0.tgz", - "integrity": "sha512-Gki7XjvHvo7bqA7b5bzXVAVG4FSTAQNTmprqMe2urLqtjKzxHiEuz5bQt9zAsg56pEGd6vGOR49cWjtXufBKKQ==", - "requires": { - "chalk": "^1.1.1", - "commander": "^2.9.0", - "debug": "^2.2.0", - "event-stream": "3.3.4", - "form-data": "3.0.0", - "fs-readfile-promise": "^2.0.1", - "fs-writefile-promise": "^1.0.3", - "har-validator": "^5.0.0", - "pinkie-promise": "^2.0.0", - "stringify-object": "^3.3.0" - }, - "dependencies": { - "ansi-regex": { - "version": "2.1.1", - "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-2.1.1.tgz", - "integrity": "sha1-w7M6te42DYbg5ijwRorn7yfWVN8=" - }, - "ansi-styles": { - "version": "2.2.1", - "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-2.2.1.tgz", - "integrity": "sha1-tDLdM1i2NM914eRmQ2gkBTPB3b4=" - }, - "chalk": { - "version": "1.1.3", - "resolved": "https://registry.npmjs.org/chalk/-/chalk-1.1.3.tgz", - "integrity": "sha1-qBFcVeSnAv5NFQq9OHKCKn4J/Jg=", - "requires": { - "ansi-styles": "^2.2.1", - "escape-string-regexp": "^1.0.2", - "has-ansi": "^2.0.0", - "strip-ansi": "^3.0.0", - "supports-color": "^2.0.0" - } - }, - "strip-ansi": { - "version": "3.0.1", - "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-3.0.1.tgz", - "integrity": "sha1-ajhfuIU9lS1f8F0Oiq+UJ43GPc8=", - "requires": { - "ansi-regex": "^2.0.0" - } - }, - "supports-color": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-2.0.0.tgz", - "integrity": "sha1-U10EXOa2Nj+kARcIRimZXp3zJMc=" - } - } - }, "human-signals": { "version": "1.1.1", "resolved": "https://registry.npmjs.org/human-signals/-/human-signals-1.1.1.tgz", @@ -6188,9 +6101,9 @@ "dev": true }, "map-stream": { - "version": "0.1.0", - "resolved": "https://registry.npmjs.org/map-stream/-/map-stream-0.1.0.tgz", - "integrity": "sha1-5WqpTEyAVaFkBKBnS3jyFffI4ZQ=" + "version": "0.0.7", + "resolved": "https://registry.npmjs.org/map-stream/-/map-stream-0.0.7.tgz", + "integrity": "sha1-ih8HiW2CsQkmvTdEokIACfiJdKg=" }, "map-visit": { "version": "1.0.0", @@ -6281,15 +6194,11 @@ "minimist": "^1.2.5" } }, - "mkdirp-promise": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/mkdirp-promise/-/mkdirp-promise-1.1.0.tgz", - "integrity": "sha1-LISJPtZ24NmPsY+5piEv0bK5qBk=" - }, "ms": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", - "integrity": "sha1-VgiurfwAvmwpAd9fmGF4jeDVl8g=" + "integrity": "sha1-VgiurfwAvmwpAd9fmGF4jeDVl8g=", + "dev": true }, "multimap": { "version": "1.1.0", @@ -6687,26 +6596,6 @@ "integrity": "sha1-7RQaasBDqEnqWISY59yosVMw6Qw=", "dev": true }, - "pinkie": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/pinkie/-/pinkie-1.0.0.tgz", - "integrity": "sha1-Wkfyi6EBXQIBvae/DzWOR77Ix+Q=" - }, - "pinkie-promise": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/pinkie-promise/-/pinkie-promise-2.0.1.tgz", - "integrity": "sha1-ITXW36ejWMBprJsXh3YogihFD/o=", - "requires": { - "pinkie": "^2.0.0" - }, - "dependencies": { - "pinkie": { - "version": "2.0.4", - "resolved": "https://registry.npmjs.org/pinkie/-/pinkie-2.0.4.tgz", - "integrity": "sha1-clVrgM+g1IqXToDnckjoDtT3+HA=" - } - } - }, "pirates": { "version": "4.0.1", "resolved": "https://registry.npmjs.org/pirates/-/pirates-4.0.1.tgz", @@ -7567,9 +7456,9 @@ "dev": true }, "split": { - "version": "0.3.3", - "resolved": "https://registry.npmjs.org/split/-/split-0.3.3.tgz", - "integrity": "sha1-zQ7qXmOiEd//frDwkcQTPi0N0o8=", + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/split/-/split-1.0.1.tgz", + "integrity": "sha512-mTyOoPbrivtXnwnIxZRFYRrPNtEFKlpB2fvjSnCQUiAA6qAZzqwna5envK4uk6OIeP17CsdF3rSBGYVBsU0Tkg==", "requires": { "through": "2" } @@ -7651,11 +7540,12 @@ "dev": true }, "stream-combiner": { - "version": "0.0.4", - "resolved": "https://registry.npmjs.org/stream-combiner/-/stream-combiner-0.0.4.tgz", - "integrity": "sha1-TV5DPBhSYd3mI8o/RMWGvPXErRQ=", + "version": "0.2.2", + "resolved": "https://registry.npmjs.org/stream-combiner/-/stream-combiner-0.2.2.tgz", + "integrity": "sha1-rsjLrBd7Vrb0+kec7YwZEs7lKFg=", "requires": { - "duplexer": "~0.1.1" + "duplexer": "~0.1.1", + "through": "~2.3.4" } }, "string-length": { diff --git a/packages/httpsnippet-client-api/package.json b/packages/httpsnippet-client-api/package.json index 5088a3cd..165792a8 100644 --- a/packages/httpsnippet-client-api/package.json +++ b/packages/httpsnippet-client-api/package.json @@ -23,14 +23,14 @@ "node": ">=10" }, "dependencies": { + "@readme/httpsnippet": "^2.0.1", "@readme/oas-tooling": "^3.4.7", "content-type": "^1.0.4", - "httpsnippet": "^1.20.0", "path-to-regexp": "^6.1.0", "stringify-object": "^3.3.0" }, "peerDependencies": { - "httpsnippet": "^1.20.0" + "@readme/httpsnippet": "^2.0.1" }, "devDependencies": { "@readme/eslint-config": "^3.2.0", diff --git a/packages/httpsnippet-client-api/src/index.js b/packages/httpsnippet-client-api/src/index.js index 300a63f5..d5d028ea 100644 --- a/packages/httpsnippet-client-api/src/index.js +++ b/packages/httpsnippet-client-api/src/index.js @@ -1,6 +1,6 @@ const { match } = require('path-to-regexp'); const stringifyObject = require('stringify-object'); -const CodeBuilder = require('httpsnippet/src/helpers/code-builder'); +const CodeBuilder = require('@readme/httpsnippet/src/helpers/code-builder'); const contentType = require('content-type'); const OAS = require('@readme/oas-tooling'); @@ -80,7 +80,6 @@ module.exports = function (source, options) { const authData = []; const authSources = getAuthSources(operation); - let includeFS = false; const code = new CodeBuilder(opts.indent); code.push(`const sdk = require('api')('${opts.apiDefinitionUri}');`); @@ -162,29 +161,19 @@ module.exports = function (source, options) { case 'multipart/form-data': body = {}; - source.postData.params.forEach(function (param) { - const attachment = {}; - - if (!param.fileName && !param.contentType) { - body[param.name] = param.value; - return; - } - - if (param.fileName && !param.value) { - includeFS = true; - attachment.value = `fs.createReadStream("${param.fileName}")`; - } else if (param.value) { - attachment.value = param.value; - } + // If there's a `Content-Type` header present in the metadata, but it's for the form-data + // request then dump it off the snippet. We shouldn't offload that unnecessary bloat to the + // user, instead letting the SDK handle it automatically. + if ('content-type' in metadata && metadata['content-type'].indexOf('multipart/form-data') === 0) { + delete metadata['content-type']; + } + source.postData.params.forEach(function (param) { if (param.fileName) { - attachment.options = { - filename: param.fileName, - contentType: param.contentType ? param.contentType : null, - }; + body[param.name] = param.fileName; + } else { + body[param.name] = param.value; } - - body[param.name] = attachment; }); break; @@ -211,10 +200,6 @@ module.exports = function (source, options) { args.push(stringifyObject(metadata, { indent: ' ', inlineCharacterLimit: 80 })); } - if (includeFS) { - code.unshift('const fs = require("fs");'); - } - if (authData.length) { code.push(authData.join('\n')); } From 6bc665ff76d6c8400af6077890e6f51e586a6b7e Mon Sep 17 00:00:00 2001 From: Jon Ursenbach Date: Thu, 13 Aug 2020 11:06:46 -0700 Subject: [PATCH 2/9] chore(deps): upgrading fetch-har to 4.0.0 for multipart/form-data support --- packages/api/package-lock.json | 6 +++--- packages/api/package.json | 2 +- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/packages/api/package-lock.json b/packages/api/package-lock.json index 6e20fa6e..065afcf7 100644 --- a/packages/api/package-lock.json +++ b/packages/api/package-lock.json @@ -3769,9 +3769,9 @@ } }, "fetch-har": { - "version": "3.0.2", - "resolved": "https://registry.npmjs.org/fetch-har/-/fetch-har-3.0.2.tgz", - "integrity": "sha512-O9zfOXvSB5NZwErivIPOrH/VfSbI9IqJJoWdjQE+UTqE5Sq2/xYECxGPBNIZEYiEUPXKsBhhx3e5nqixf2LacQ==" + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/fetch-har/-/fetch-har-4.0.0.tgz", + "integrity": "sha512-2C4Ibj6l/IOULFaJXHw7oXnm3nUBsmx/IKbf6G+1mFkNHhBdZ6tX/+IrbG4rc9yzyu6/0Wvw3GxCHGy3eAUD8A==" }, "file-entry-cache": { "version": "5.0.1", diff --git a/packages/api/package.json b/packages/api/package.json index 06e56a13..044d8b86 100644 --- a/packages/api/package.json +++ b/packages/api/package.json @@ -27,7 +27,7 @@ "@apidevtools/swagger-parser": "^10.0.1", "@readme/oas-to-har": "^6.9.6", "@readme/oas-tooling": "^3.4.1", - "fetch-har": "^3.0.0", + "fetch-har": "^4.0.0", "find-cache-dir": "^3.3.1", "js-yaml": "^3.14.0", "node-fetch": "^2.6.0" From ab3c61fc451e40253886fa568c36366d351175e2 Mon Sep 17 00:00:00 2001 From: Jon Ursenbach Date: Thu, 13 Aug 2020 17:42:52 -0700 Subject: [PATCH 3/9] feat: support for multipart handling in the sdk --- README.md | 14 ++- .../api/__tests__/lib/prepareParams.test.js | 103 +++++++++++------- packages/api/package-lock.json | 64 ++++++++--- packages/api/package.json | 5 +- packages/api/src/index.js | 20 ++-- packages/api/src/lib/prepareParams.js | 64 ++++++++++- .../httpsnippet-client-api/package-lock.json | 6 +- packages/httpsnippet-client-api/package.json | 2 +- 8 files changed, 208 insertions(+), 70 deletions(-) diff --git a/README.md b/README.md index 10959804..73658e59 100644 --- a/README.md +++ b/README.md @@ -76,6 +76,18 @@ sdk.updatePet({ name: 'Buster 2' }, { petId: 1234 }).then(...) Since we've supplied two objects here, the SDK automatically knows that you're supplying both a `body` and `metadata`, and can make a PUT request against `/pets/1234` for you. +What about a `multipart/form-data` request? That works too, and you don't even have to worry about the fun of multipart boundaries! + +```js +sdk.uploadFile({ file: '/path/to/a/file.txt' }).then(...) +``` + +You can also give it a stream and it'll handle all of the hard work for you. + +```js +sdk.uploadFile({ file: fs.createReadStream('/path/to/a/file.txt') }).then(...) +``` + ### HTTP requests If the API you're using doesn't have any documented operation IDs, you can make requests with HTTP verbs instead: @@ -112,7 +124,7 @@ Not yet, unfortunately. For APIs that use OAuth 2, you'll need a fully-qualified Not yet! This is something we're thinking about how to handle, but it's difficult with the simple nature of the `.auth()` method as it currently does not require the user to inform the SDK of what kind of authentication scheme the token they're supplying it should match up against. #### Will this work in browsers? -Not sure! If you'd like to help us out in making this compatible with browsers we'd love to help you out on a pull request. +Not at the moment as the library requires some filesystem handling in order to manage its cache state, but it's something we're actively thinking about. If you'd like to help us out in making this compatible with browsers we'd love to help you out on a pull request. #### Will this validate my data before it reaches the API? Not yet! This is something we've got planned down the road. diff --git a/packages/api/__tests__/lib/prepareParams.test.js b/packages/api/__tests__/lib/prepareParams.test.js index 78f66f99..c9fe91b8 100644 --- a/packages/api/__tests__/lib/prepareParams.test.js +++ b/packages/api/__tests__/lib/prepareParams.test.js @@ -1,3 +1,4 @@ +const fs = require('fs'); const Oas = require('@readme/oas-tooling'); const $RefParser = require('@apidevtools/json-schema-ref-parser'); const readmeExample = require('@readme/oas-examples/3.0/json/readme.json'); @@ -39,16 +40,16 @@ describe('#prepareParams', () => { usptoSpec = new Oas(schema); }); - it('should prepare nothing if nothing was supplied', () => { + it('should prepare nothing if nothing was supplied', async () => { const operation = readmeSpec.operation('/api-specification', 'post'); - expect(prepareParams(operation)).toStrictEqual({}); - expect(prepareParams(operation, null, null)).toStrictEqual({}); - expect(prepareParams(operation, [], [])).toStrictEqual({}); - expect(prepareParams(operation, {}, {})).toStrictEqual({}); + expect(await prepareParams(operation)).toStrictEqual({}); + expect(await prepareParams(operation, null, null)).toStrictEqual({}); + expect(await prepareParams(operation, [], [])).toStrictEqual({}); + expect(await prepareParams(operation, {}, {})).toStrictEqual({}); }); - it('should prepare body and metadata when both are supplied', () => { + it('should prepare body and metadata when both are supplied', async () => { const operation = readmeSpec.operation('/api-specification', 'post'); const body = { spec: 'this is the contents of an api specification', @@ -58,7 +59,7 @@ describe('#prepareParams', () => { 'x-readme-version': '1.0', }; - expect(prepareParams(operation, body, metadata)).toStrictEqual({ + expect(await prepareParams(operation, body, metadata)).toStrictEqual({ body: { spec: 'this is the contents of an api specification', }, @@ -68,7 +69,7 @@ describe('#prepareParams', () => { }); }); - it('should prepare body if body is a primitive', () => { + it('should prepare body if body is a primitive', async () => { const schema = createOas('put', '/', { requestBody: { content: { @@ -84,12 +85,12 @@ describe('#prepareParams', () => { const operation = new Oas(schema).operation('/', 'put'); const body = 'Brie cheeseburger ricotta.'; - expect(prepareParams(operation, body, {})).toStrictEqual({ + expect(await prepareParams(operation, body, {})).toStrictEqual({ body, }); }); - it('should prepare body if body is an array', () => { + it('should prepare body if body is an array', async () => { const operation = new Oas(arraySchema).operation('/', 'put'); const body = [ { @@ -97,55 +98,79 @@ describe('#prepareParams', () => { }, ]; - expect(prepareParams(operation, body, {})).toStrictEqual({ + expect(await prepareParams(operation, body, {})).toStrictEqual({ body, }); }); - it('should handle bodies when the content type is application/x-www-form-urlencoded', () => { - const operation = usptoSpec.operation('/{dataset}/{version}/records', 'post'); - const body = { - criteria: '*:*', - }; - - const metadata = { - dataset: 'v1', - version: 'oa_citations', - }; + describe('content types', () => { + it('should handle bodies when the content type is `application/x-www-form-urlencoded`', async () => { + const operation = usptoSpec.operation('/{dataset}/{version}/records', 'post'); + const body = { + criteria: '*:*', + }; - expect(prepareParams(operation, body, metadata)).toStrictEqual({ - path: { + const metadata = { dataset: 'v1', version: 'oa_citations', - }, - formData: { - criteria: '*:*', - }, + }; + + expect(await prepareParams(operation, body, metadata)).toStrictEqual({ + path: { + dataset: 'v1', + version: 'oa_citations', + }, + formData: { + criteria: '*:*', + }, + }); + }); + + describe('multipart/form-data', () => { + it('should handle a multipart body when a property is a file path', async () => { + const operation = readmeSpec.operation('/api-specification', 'post'); + const body = { + spec: require.resolve('@readme/oas-examples/3.0/json/readme.json'), + }; + + const params = await prepareParams(operation, body); + expect(params.body.spec).toContain('data:application/json;name=readme.json;base64,'); + }); + + it('should handle a multipart body when a property is a file stream', async () => { + const operation = readmeSpec.operation('/api-specification', 'post'); + const body = { + spec: fs.createReadStream(require.resolve('@readme/oas-examples/3.0/json/readme.json')), + }; + + const params = await prepareParams(operation, body); + expect(params.body.spec).toContain('data:application/json;name=readme.json;base64,'); + }); }); }); describe('supplying just a body or metadata', () => { - it('should handle if supplied is a body', () => { + it('should handle if supplied is a body', async () => { const operation = readmeSpec.operation('/api-specification', 'post'); const body = { spec: 'this is the contents of an api specification', }; - expect(prepareParams(operation, body)).toStrictEqual({ + expect(await prepareParams(operation, body)).toStrictEqual({ body, }); }); - it('should prepare a body if supplied is primitive', () => { + it('should prepare a body if supplied is primitive', async () => { const operation = readmeSpec.operation('/api-specification', 'post'); const body = 'this is a primitive value'; - expect(prepareParams(operation, body)).toStrictEqual({ + expect(await prepareParams(operation, body)).toStrictEqual({ body, }); }); - it('should prepare just a body if supplied argument is an array', () => { + it('should prepare just a body if supplied argument is an array', async () => { const operation = new Oas(arraySchema).operation('/', 'put'); const body = [ { @@ -153,12 +178,12 @@ describe('#prepareParams', () => { }, ]; - expect(prepareParams(operation, body)).toStrictEqual({ + expect(await prepareParams(operation, body)).toStrictEqual({ body, }); }); - it('should prepare metadata if more than 25% of the supplied argument lines up with known parameters', () => { + it('should prepare metadata if more than 25% of the supplied argument lines up with known parameters', async () => { const operation = usptoSpec.operation('/{dataset}/{version}/records', 'post'); const body = { version: 'v1', @@ -166,7 +191,7 @@ describe('#prepareParams', () => { randomUnknownParameter: true, }; - expect(prepareParams(operation, body)).toStrictEqual({ + expect(await prepareParams(operation, body)).toStrictEqual({ path: { version: 'v1', dataset: 'oa_citations', @@ -179,7 +204,7 @@ describe('#prepareParams', () => { }); }); - it('should prepare metadata if less than 25% of the supplied argument lines up with known parameters', () => { + it('should prepare metadata if less than 25% of the supplied argument lines up with known parameters', async () => { const operation = usptoSpec.operation('/{dataset}/{version}/records', 'post'); const body = { version: 'v1', // This a known parameter, but the others aren't and should be treated as body payload data. @@ -189,7 +214,7 @@ describe('#prepareParams', () => { randomUnknownParameter4: true, }; - expect(prepareParams(operation, body)).toStrictEqual({ + expect(await prepareParams(operation, body)).toStrictEqual({ formData: { version: 'v1', randomUnknownParameter: true, @@ -200,13 +225,13 @@ describe('#prepareParams', () => { }); }); - it('should prepare just metadata if supplied is metadata', () => { + it('should prepare just metadata if supplied is metadata', async () => { const operation = readmeSpec.operation('/api-specification', 'post'); const metadata = { 'x-readme-version': '1.0', }; - expect(prepareParams(operation, metadata)).toStrictEqual({ + expect(await prepareParams(operation, metadata)).toStrictEqual({ header: { 'x-readme-version': '1.0', }, diff --git a/packages/api/package-lock.json b/packages/api/package-lock.json index 9a2b3edb..a46d6926 100644 --- a/packages/api/package-lock.json +++ b/packages/api/package-lock.json @@ -1631,9 +1631,9 @@ } }, "@readme/oas-tooling": { - "version": "3.5.6", - "resolved": "https://registry.npmjs.org/@readme/oas-tooling/-/oas-tooling-3.5.6.tgz", - "integrity": "sha512-bTVAZcQVXvSEBobau6XA8Q1dfbV5/OsuPm/XvkAleBDNycvl2+LNKEF5nZC5Rf7t65yJq47ww3/+CHe6hC+jhw==", + "version": "3.5.8", + "resolved": "https://registry.npmjs.org/@readme/oas-tooling/-/oas-tooling-3.5.8.tgz", + "integrity": "sha512-Y70P0qGcz/3Kh1IpDv1hk20VctsiPt5BloXDbpmzyr3LsFTdCzzRIn9zgiE05gNOYNheVeFzXBzqI2SYV/9mNQ==", "requires": { "jsonpointer": "^4.0.1", "path-to-regexp": "^6.1.0" @@ -2605,6 +2605,15 @@ "whatwg-url": "^8.0.0" } }, + "datauri": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/datauri/-/datauri-3.0.0.tgz", + "integrity": "sha512-NeDFuUPV1YCpCn8MUIcDk1QnuyenUHs7f4Q5P0n9FFA0neKFrfEH9esR+YMW95BplbYfdmjbs0Pl/ZGAaM2QHQ==", + "requires": { + "image-size": "0.8.3", + "mimer": "1.1.0" + } + }, "debug": { "version": "2.6.9", "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", @@ -3488,6 +3497,15 @@ "which": "^1.2.9" } }, + "get-stream": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/get-stream/-/get-stream-4.1.0.tgz", + "integrity": "sha512-GMat4EJ5161kIy2HevLlr4luNjBgvmj413KaQA7jt4V8B4RDsfpHk7WQ9GVqfYyyx8OS/L66Kox+rJRNklLK7w==", + "dev": true, + "requires": { + "pump": "^3.0.0" + } + }, "path-key": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/path-key/-/path-key-2.0.1.tgz", @@ -3880,13 +3898,9 @@ "dev": true }, "get-stream": { - "version": "4.1.0", - "resolved": "https://registry.npmjs.org/get-stream/-/get-stream-4.1.0.tgz", - "integrity": "sha512-GMat4EJ5161kIy2HevLlr4luNjBgvmj413KaQA7jt4V8B4RDsfpHk7WQ9GVqfYyyx8OS/L66Kox+rJRNklLK7w==", - "dev": true, - "requires": { - "pump": "^3.0.0" - } + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/get-stream/-/get-stream-6.0.0.tgz", + "integrity": "sha512-A1B3Bh1UmL0bidM/YX2NsCOTnGJePL9rO/M+Mw3m9f2gUpfokS0hi5Eah0WSUEWZdZhIZtMjkIYS7mDfOqNHbg==" }, "get-value": { "version": "2.0.6", @@ -4104,6 +4118,14 @@ "integrity": "sha512-BMpfD7PpiETpBl/A6S498BaIJ6Y/ABT93ETbby2fP00v4EbvPBXWEoaR1UBPKs3iR53pJY7EtZk5KACI57i1Uw==", "dev": true }, + "image-size": { + "version": "0.8.3", + "resolved": "https://registry.npmjs.org/image-size/-/image-size-0.8.3.tgz", + "integrity": "sha512-SMtq1AJ+aqHB45c3FsB4ERK0UCiA2d3H1uq8s+8T0Pf8A3W4teyBQyaFaktH6xvZqh+npwlKU7i4fJo0r7TYTg==", + "requires": { + "queue": "6.0.1" + } + }, "import-fresh": { "version": "3.2.1", "resolved": "https://registry.npmjs.org/import-fresh/-/import-fresh-3.2.1.tgz", @@ -4157,8 +4179,7 @@ "inherits": { "version": "2.0.4", "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz", - "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==", - "dev": true + "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==" }, "internal-slot": { "version": "1.0.2", @@ -5819,9 +5840,9 @@ } }, "jsonpointer": { - "version": "4.0.1", - "resolved": "https://registry.npmjs.org/jsonpointer/-/jsonpointer-4.0.1.tgz", - "integrity": "sha1-T9kss04OnbPInIYi7PUfm5eMbLk=" + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/jsonpointer/-/jsonpointer-4.1.0.tgz", + "integrity": "sha512-CXcRvMyTlnR53xMcKnuMzfCA5i/nfblTnnr74CZb6C4vG39eu6w51t7nKmU5MfLfbTgGItliNyjO/ciNPDqClg==" }, "jsprim": { "version": "1.4.1", @@ -6031,6 +6052,11 @@ "mime-db": "1.44.0" } }, + "mimer": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/mimer/-/mimer-1.1.0.tgz", + "integrity": "sha512-y9dVfy2uiycQvDNiAYW6zp49ZhFlXDMr5wfdOiMbdzGM/0N5LNR6HTUn3un+WUQcM0koaw8FMTG1bt5EnHJdvQ==" + }, "mimic-fn": { "version": "2.1.0", "resolved": "https://registry.npmjs.org/mimic-fn/-/mimic-fn-2.1.0.tgz", @@ -6678,6 +6704,14 @@ "integrity": "sha512-N5ZAX4/LxJmF+7wN74pUD6qAh9/wnvdQcjq9TZjevvXzSUo7bfmw91saqMjzGS2xq91/odN2dW/WOl7qQHNDGA==", "dev": true }, + "queue": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/queue/-/queue-6.0.1.tgz", + "integrity": "sha512-AJBQabRCCNr9ANq8v77RJEv73DPbn55cdTb+Giq4X0AVnNVZvMHlYp7XlQiN+1npCZj1DuSmaA2hYVUUDgxFDg==", + "requires": { + "inherits": "~2.0.3" + } + }, "react-is": { "version": "16.13.1", "resolved": "https://registry.npmjs.org/react-is/-/react-is-16.13.1.tgz", diff --git a/packages/api/package.json b/packages/api/package.json index 99309caa..3f5a9712 100644 --- a/packages/api/package.json +++ b/packages/api/package.json @@ -26,10 +26,13 @@ "@apidevtools/json-schema-ref-parser": "^9.0.1", "@apidevtools/swagger-parser": "^10.0.1", "@readme/oas-to-har": "^6.9.6", - "@readme/oas-tooling": "^3.4.1", + "@readme/oas-tooling": "^3.5.8", + "datauri": "^3.0.0", "fetch-har": "^4.0.0", "find-cache-dir": "^3.3.1", + "get-stream": "^6.0.0", "js-yaml": "^3.14.0", + "mimer": "^1.1.0", "node-fetch": "^2.6.0" }, "devDependencies": { diff --git a/packages/api/src/index.js b/packages/api/src/index.js index 85e080e0..dbf83970 100644 --- a/packages/api/src/index.js +++ b/packages/api/src/index.js @@ -14,6 +14,7 @@ global.Headers = fetch.Headers; class Sdk { constructor(uri) { this.uri = uri; + this.userAgent = `${pkg.name} (node)/${pkg.version}`; } static getOperations(spec) { @@ -29,20 +30,25 @@ class Sdk { load() { const authKeys = []; const cache = new Cache(this.uri); + const self = this; let isLoaded = false; let isCached = cache.isCached(); let sdk = {}; function fetchOperation(spec, operation, body, metadata) { - const har = oasToHar(spec, operation, prepareParams(operation, body, metadata), prepareAuth(authKeys, operation)); - - return fetchHar(har, `${pkg.name} (node)/${pkg.version}`).then(res => { - if (res.status >= 400 && res.status <= 599) { - throw res; - } + return new Promise(resolve => { + resolve(prepareParams(operation, body, metadata)); + }).then(params => { + const har = oasToHar(spec, operation, params, prepareAuth(authKeys, operation)); + + return fetchHar(har, self.userAgent).then(res => { + if (res.status >= 400 && res.status <= 599) { + throw res; + } - return res; + return res; + }); }); } diff --git a/packages/api/src/lib/prepareParams.js b/packages/api/src/lib/prepareParams.js index badf5fca..e4cd8208 100644 --- a/packages/api/src/lib/prepareParams.js +++ b/packages/api/src/lib/prepareParams.js @@ -1,3 +1,11 @@ +const fs = require('fs'); +const stream = require('stream'); +const mimer = require('mimer'); +const getStream = require('get-stream'); +const path = require('path'); +const datauri = require('datauri'); +const { getSchema } = require('@readme/oas-tooling/utils'); + function digestParameters(parameters) { return parameters.reduce((prev, param) => { if ('$ref' in param || 'allOf' in param || 'anyOf' in param || 'oneOf' in param) { @@ -17,7 +25,7 @@ function isEmpty(obj) { return [Object, Array].includes((obj || {}).constructor) && !Object.entries(obj || {}).length; } -module.exports = function (operation, body, metadata) { +module.exports = async (operation, body, metadata) => { // If no data was supplied, just return immediately. if (isEmpty(body) && isEmpty(metadata)) { return {}; @@ -25,7 +33,6 @@ module.exports = function (operation, body, metadata) { const params = {}; let shouldDigestParams = false; - const contentType = operation.getContentType(); if (Array.isArray(body)) { // If the body param is an array, then it's absolutely a body and not something we need to do analysis against. @@ -77,6 +84,57 @@ module.exports = function (operation, body, metadata) { } } + // @todo support content types like `image/png` where the request body is the binary + + // If the operation is `multipart/form-data`, or one of our recognized variants, we need to look at the incoming + // body payload to see if anything in there is either a file path or a file stream so we can translate those into a + // data URL for `@readme/oas-to-har` to make a request. + if ('body' in params && operation.isMultipart()) { + const schema = getSchema(operation, operation.oas) || { schema: {} }; + const bodyKeys = Object.keys(params.body); + + // Loop thorugh the schema to look for `binary` properties so we know what we need to convert. + const conversions = []; + Object.keys(schema.schema.properties) + .filter(key => schema.schema.properties[key].format === 'binary') + .filter(x => bodyKeys.includes(x)) + .forEach(async prop => { + const file = params.body[prop]; + if (typeof file === 'string') { + if (fs.existsSync(file)) { + conversions.push( + new Promise(resolve => resolve(datauri(file))).then(dataurl => { + // Doing this manually for now until when/if https://github.com/data-uri/datauri/pull/29 is accepted. + params.body[prop] = dataurl.replace( + ';base64', + `;name=${encodeURIComponent(path.basename(file))};base64` + ); + + return Promise.resolve(true); + }) + ); + } + } else if (file instanceof stream.Readable) { + conversions.push( + new Promise(resolve => resolve(getStream.buffer(file))).then(buffer => { + // This logic was taken from the `datauri` package, and ideally it should be able to accept the content + // of a file, or a file stream, but I'll PR that later to that package. + // @todo + const base64 = buffer.toString('base64'); + const mimeType = mimer(file.path); + const filepath = path.basename(file.path); + + params.body[prop] = `data:${mimeType};name=${encodeURIComponent(filepath)};base64,${base64 || ''}`; + + return Promise.resolve(true); + }) + ); + } + }); + + await Promise.all(conversions); + } + // @todo add in a debug mode that would run jsonschema validation against request bodies and parameters and throw back errors if what's supplied isn't up to spec. // Only spend time trying to organize metadata into parameters if we were able to digest parameters out of the @@ -111,7 +169,7 @@ module.exports = function (operation, body, metadata) { } // Form data should be placed inside `formData` instead of `body` for it to properly get picked up. - if (contentType === 'application/x-www-form-urlencoded') { + if (operation.isFormUrlEncoded()) { params.formData = body; delete params.body; } diff --git a/packages/httpsnippet-client-api/package-lock.json b/packages/httpsnippet-client-api/package-lock.json index 66443a40..3518cbb4 100644 --- a/packages/httpsnippet-client-api/package-lock.json +++ b/packages/httpsnippet-client-api/package-lock.json @@ -1615,9 +1615,9 @@ } }, "@readme/oas-tooling": { - "version": "3.5.6", - "resolved": "https://registry.npmjs.org/@readme/oas-tooling/-/oas-tooling-3.5.6.tgz", - "integrity": "sha512-bTVAZcQVXvSEBobau6XA8Q1dfbV5/OsuPm/XvkAleBDNycvl2+LNKEF5nZC5Rf7t65yJq47ww3/+CHe6hC+jhw==", + "version": "3.5.8", + "resolved": "https://registry.npmjs.org/@readme/oas-tooling/-/oas-tooling-3.5.8.tgz", + "integrity": "sha512-Y70P0qGcz/3Kh1IpDv1hk20VctsiPt5BloXDbpmzyr3LsFTdCzzRIn9zgiE05gNOYNheVeFzXBzqI2SYV/9mNQ==", "requires": { "jsonpointer": "^4.0.1", "path-to-regexp": "^6.1.0" diff --git a/packages/httpsnippet-client-api/package.json b/packages/httpsnippet-client-api/package.json index e9355622..1199bf2a 100644 --- a/packages/httpsnippet-client-api/package.json +++ b/packages/httpsnippet-client-api/package.json @@ -24,7 +24,7 @@ }, "dependencies": { "@readme/httpsnippet": "^2.0.1", - "@readme/oas-tooling": "^3.4.7", + "@readme/oas-tooling": "^3.5.8", "content-type": "^1.0.4", "path-to-regexp": "^6.1.0", "stringify-object": "^3.3.0" From 22352ce42229ad43d8266933022b1e1a3b00d404 Mon Sep 17 00:00:00 2001 From: Jon Ursenbach Date: Thu, 13 Aug 2020 17:52:17 -0700 Subject: [PATCH 4/9] feat: supporting relative paths from being used in uploads --- packages/api/__tests__/__fixtures__/owlbert.png | Bin 0 -> 400 bytes packages/api/__tests__/lib/prepareParams.test.js | 10 ++++++++++ packages/api/src/lib/prepareParams.js | 5 +++-- 3 files changed, 13 insertions(+), 2 deletions(-) create mode 100644 packages/api/__tests__/__fixtures__/owlbert.png diff --git a/packages/api/__tests__/__fixtures__/owlbert.png b/packages/api/__tests__/__fixtures__/owlbert.png new file mode 100644 index 0000000000000000000000000000000000000000..7b9200986fd6e78c2a64079b7dcb6eb93e5a82ef GIT binary patch literal 400 zcmV;B0dM|^P)BHapC z2vSrYX!Rq+O6y5ksPn4gV8{7U4hE-lyv<*F8>`f!D6CEJ|KFG$_rE6G?mvtj?PT)b zQA;ijuCO>OW$D&U8~&G+l>E<)^7+4Z>4N`FP0jz~!bASUtOh%{BsuEn#J=ADZZ1~; zr#G1XUpBAre{g_57~5+rYz3(S`?x&V{squFuxV}SqW=@U41NLGV5>py2B~4tRS@3> za@V9Z*Z)27PAIs<&*FcGh30>d8U|x!sjnb``9(f(Jgd+POal!7E7VgE|HdFI%-;ht z9;yJ2r{#Ho6}lQKy#uKM``T1pII1An@F&oExT2;6d$3!z { expect(params.body.spec).toContain('data:application/json;name=readme.json;base64,'); }); + it('should handle when the file path is relative', async () => { + const operation = readmeSpec.operation('/api-specification', 'post'); + const body = { + spec: './__tests__/__fixtures__/owlbert.png', + }; + + const params = await prepareParams(operation, body); + expect(params.body.spec).toContain('data:image/png;name=owlbert.png;base64,'); + }); + it('should handle a multipart body when a property is a file stream', async () => { const operation = readmeSpec.operation('/api-specification', 'post'); const body = { diff --git a/packages/api/src/lib/prepareParams.js b/packages/api/src/lib/prepareParams.js index e4cd8208..f824f249 100644 --- a/packages/api/src/lib/prepareParams.js +++ b/packages/api/src/lib/prepareParams.js @@ -1,8 +1,8 @@ const fs = require('fs'); +const path = require('path'); const stream = require('stream'); const mimer = require('mimer'); const getStream = require('get-stream'); -const path = require('path'); const datauri = require('datauri'); const { getSchema } = require('@readme/oas-tooling/utils'); @@ -99,8 +99,9 @@ module.exports = async (operation, body, metadata) => { .filter(key => schema.schema.properties[key].format === 'binary') .filter(x => bodyKeys.includes(x)) .forEach(async prop => { - const file = params.body[prop]; + let file = params.body[prop]; if (typeof file === 'string') { + file = path.resolve(file); if (fs.existsSync(file)) { conversions.push( new Promise(resolve => resolve(datauri(file))).then(dataurl => { From d6904328e93ea2d12b1f68d8c1b1f8744c4d8ea6 Mon Sep 17 00:00:00 2001 From: Jon Ursenbach Date: Fri, 14 Aug 2020 09:54:30 -0700 Subject: [PATCH 5/9] docs: adding some comments --- packages/api/src/lib/prepareParams.js | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/packages/api/src/lib/prepareParams.js b/packages/api/src/lib/prepareParams.js index f824f249..e07a04a7 100644 --- a/packages/api/src/lib/prepareParams.js +++ b/packages/api/src/lib/prepareParams.js @@ -93,7 +93,7 @@ module.exports = async (operation, body, metadata) => { const schema = getSchema(operation, operation.oas) || { schema: {} }; const bodyKeys = Object.keys(params.body); - // Loop thorugh the schema to look for `binary` properties so we know what we need to convert. + // Loop through the schema to look for `binary` properties so we know what we need to convert. const conversions = []; Object.keys(schema.schema.properties) .filter(key => schema.schema.properties[key].format === 'binary') @@ -101,6 +101,8 @@ module.exports = async (operation, body, metadata) => { .forEach(async prop => { let file = params.body[prop]; if (typeof file === 'string') { + // In order to support relative pathed files, we need to attempt to resolve them. Thankfully `path.resolve()` + // doesn't munge absolute paths. file = path.resolve(file); if (fs.existsSync(file)) { conversions.push( From 50afb77f77221b5fdc5cf2ab6d70d195625f22bf Mon Sep 17 00:00:00 2001 From: Jon Ursenbach Date: Fri, 14 Aug 2020 10:25:20 -0700 Subject: [PATCH 6/9] docs: updating the package readme --- packages/api/README.md | 14 +++++++++++++- 1 file changed, 13 insertions(+), 1 deletion(-) diff --git a/packages/api/README.md b/packages/api/README.md index 10959804..73658e59 100644 --- a/packages/api/README.md +++ b/packages/api/README.md @@ -76,6 +76,18 @@ sdk.updatePet({ name: 'Buster 2' }, { petId: 1234 }).then(...) Since we've supplied two objects here, the SDK automatically knows that you're supplying both a `body` and `metadata`, and can make a PUT request against `/pets/1234` for you. +What about a `multipart/form-data` request? That works too, and you don't even have to worry about the fun of multipart boundaries! + +```js +sdk.uploadFile({ file: '/path/to/a/file.txt' }).then(...) +``` + +You can also give it a stream and it'll handle all of the hard work for you. + +```js +sdk.uploadFile({ file: fs.createReadStream('/path/to/a/file.txt') }).then(...) +``` + ### HTTP requests If the API you're using doesn't have any documented operation IDs, you can make requests with HTTP verbs instead: @@ -112,7 +124,7 @@ Not yet, unfortunately. For APIs that use OAuth 2, you'll need a fully-qualified Not yet! This is something we're thinking about how to handle, but it's difficult with the simple nature of the `.auth()` method as it currently does not require the user to inform the SDK of what kind of authentication scheme the token they're supplying it should match up against. #### Will this work in browsers? -Not sure! If you'd like to help us out in making this compatible with browsers we'd love to help you out on a pull request. +Not at the moment as the library requires some filesystem handling in order to manage its cache state, but it's something we're actively thinking about. If you'd like to help us out in making this compatible with browsers we'd love to help you out on a pull request. #### Will this validate my data before it reaches the API? Not yet! This is something we've got planned down the road. From 39c476ba1190e419253358e589498b8f0c265be6 Mon Sep 17 00:00:00 2001 From: Jon Ursenbach Date: Mon, 17 Aug 2020 12:14:31 -0700 Subject: [PATCH 7/9] chore(deps): upgrading fetch-har to 4.x --- packages/api/package-lock.json | 53 ++++++++++++++++++++++++---------- packages/api/package.json | 3 +- 2 files changed, 39 insertions(+), 17 deletions(-) diff --git a/packages/api/package-lock.json b/packages/api/package-lock.json index 9821220b..b13fb5fb 100644 --- a/packages/api/package-lock.json +++ b/packages/api/package-lock.json @@ -2042,8 +2042,7 @@ "asynckit": { "version": "0.4.0", "resolved": "https://registry.npmjs.org/asynckit/-/asynckit-0.4.0.tgz", - "integrity": "sha1-x57Zf380y48robyXkLzDZkdLS3k=", - "dev": true + "integrity": "sha1-x57Zf380y48robyXkLzDZkdLS3k=" }, "atob": { "version": "2.1.2", @@ -2477,7 +2476,6 @@ "version": "1.0.8", "resolved": "https://registry.npmjs.org/combined-stream/-/combined-stream-1.0.8.tgz", "integrity": "sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg==", - "dev": true, "requires": { "delayed-stream": "~1.0.0" } @@ -2705,8 +2703,7 @@ "delayed-stream": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/delayed-stream/-/delayed-stream-1.0.0.tgz", - "integrity": "sha1-3zrhmayt+31ECqrgsp4icrJOxhk=", - "dev": true + "integrity": "sha1-3zrhmayt+31ECqrgsp4icrJOxhk=" }, "detect-newline": { "version": "3.1.0", @@ -3757,9 +3754,12 @@ } }, "fetch-har": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/fetch-har/-/fetch-har-4.0.0.tgz", - "integrity": "sha512-2C4Ibj6l/IOULFaJXHw7oXnm3nUBsmx/IKbf6G+1mFkNHhBdZ6tX/+IrbG4rc9yzyu6/0Wvw3GxCHGy3eAUD8A==" + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/fetch-har/-/fetch-har-4.0.1.tgz", + "integrity": "sha512-dW9fzMq8F7hcc4Y24ypCwr7oZv7rAleSuoTydXyFxRRPgD32sbnk1gAU7l8O2gV8kxPe81IlQ2M/KA4L+5VMzg==", + "requires": { + "parse-data-url": "^2.0.0" + } }, "file-entry-cache": { "version": "5.0.1", @@ -3828,13 +3828,12 @@ "dev": true }, "form-data": { - "version": "2.3.3", - "resolved": "https://registry.npmjs.org/form-data/-/form-data-2.3.3.tgz", - "integrity": "sha512-1lLKB2Mu3aGP1Q/2eCOx0fNbRMe7XdwktwOruhfqqd0rIJWwN4Dh+E3hrPSlDCXnSR7UtZ1N38rVXm+6+MEhJQ==", - "dev": true, + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/form-data/-/form-data-3.0.0.tgz", + "integrity": "sha512-CKMFDglpbMi6PyN+brwB9Q/GOw0eAnsrEZDgcsH5Krhz5Od/haKHAX0NmQfha2zPPz0JpWzA7GJHGSnvCRLWsg==", "requires": { "asynckit": "^0.4.0", - "combined-stream": "^1.0.6", + "combined-stream": "^1.0.8", "mime-types": "^2.1.12" } }, @@ -6025,14 +6024,12 @@ "mime-db": { "version": "1.44.0", "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.44.0.tgz", - "integrity": "sha512-/NOTfLrsPBVeH7YtFPgsVWveuL+4SjjYxaQ1xtM1KMFj7HdxlBlxeyNLzhyJVx7r4rZGJAZ/6lkKCitSc/Nmpg==", - "dev": true + "integrity": "sha512-/NOTfLrsPBVeH7YtFPgsVWveuL+4SjjYxaQ1xtM1KMFj7HdxlBlxeyNLzhyJVx7r4rZGJAZ/6lkKCitSc/Nmpg==" }, "mime-types": { "version": "2.1.27", "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.27.tgz", "integrity": "sha512-JIhqnCasI9yD+SsmkquHBxTSEuZdQX5BuQnS2Vc7puQQQ+8yiP5AY5uWhpdv4YL4VM5c6iliiYWPgJ/nJQLp7w==", - "dev": true, "requires": { "mime-db": "1.44.0" } @@ -6460,6 +6457,14 @@ "callsites": "^3.0.0" } }, + "parse-data-url": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/parse-data-url/-/parse-data-url-2.0.0.tgz", + "integrity": "sha512-6iXM6OBCHADCN9Bzv5QbWm1v41xSH15kIWE5hAJ9+sdkVM6pJFg+FlLm8n7gZ17pmZv6Wdr3+leXB2Uifxm7kw==", + "requires": { + "valid-data-url": "^2.0.0" + } + }, "parse-json": { "version": "5.0.0", "resolved": "https://registry.npmjs.org/parse-json/-/parse-json-5.0.0.tgz", @@ -6837,6 +6842,17 @@ "uuid": "^3.3.2" }, "dependencies": { + "form-data": { + "version": "2.3.3", + "resolved": "https://registry.npmjs.org/form-data/-/form-data-2.3.3.tgz", + "integrity": "sha512-1lLKB2Mu3aGP1Q/2eCOx0fNbRMe7XdwktwOruhfqqd0rIJWwN4Dh+E3hrPSlDCXnSR7UtZ1N38rVXm+6+MEhJQ==", + "dev": true, + "requires": { + "asynckit": "^0.4.0", + "combined-stream": "^1.0.6", + "mime-types": "^2.1.12" + } + }, "tough-cookie": { "version": "2.5.0", "resolved": "https://registry.npmjs.org/tough-cookie/-/tough-cookie-2.5.0.tgz", @@ -7974,6 +7990,11 @@ } } }, + "valid-data-url": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/valid-data-url/-/valid-data-url-2.0.0.tgz", + "integrity": "sha512-dyCZnv3aCey7yfTgIqdZanKl7xWAEEKCbgmR7SKqyK6QT/Z07ROactrgD1eA37C69ODRj7rNOjzKWVPh0EUjBA==" + }, "validate-npm-package-license": { "version": "3.0.4", "resolved": "https://registry.npmjs.org/validate-npm-package-license/-/validate-npm-package-license-3.0.4.tgz", diff --git a/packages/api/package.json b/packages/api/package.json index 3f5a9712..a194d7b8 100644 --- a/packages/api/package.json +++ b/packages/api/package.json @@ -28,8 +28,9 @@ "@readme/oas-to-har": "^6.9.6", "@readme/oas-tooling": "^3.5.8", "datauri": "^3.0.0", - "fetch-har": "^4.0.0", + "fetch-har": "^4.0.1", "find-cache-dir": "^3.3.1", + "form-data": "^3.0.0", "get-stream": "^6.0.0", "js-yaml": "^3.14.0", "mimer": "^1.1.0", From b75729e3f792bc7e6810d4d671e1aef9b0f5dc2a Mon Sep 17 00:00:00 2001 From: Jon Ursenbach Date: Mon, 17 Aug 2020 13:22:09 -0700 Subject: [PATCH 8/9] fix: loading in `form-data` as a polyfill for FormData --- packages/api/src/index.js | 1 + 1 file changed, 1 insertion(+) diff --git a/packages/api/src/index.js b/packages/api/src/index.js index dbf83970..1520e7ef 100644 --- a/packages/api/src/index.js +++ b/packages/api/src/index.js @@ -10,6 +10,7 @@ const { prepareAuth, prepareParams } = require('./lib/index'); global.fetch = fetch; global.Request = fetch.Request; global.Headers = fetch.Headers; +global.FormData = require('form-data'); class Sdk { constructor(uri) { From 2719e1e61c53e4f4f9a451dd5946ffb8734adfef Mon Sep 17 00:00:00 2001 From: Jon Ursenbach Date: Mon, 17 Aug 2020 14:57:43 -0700 Subject: [PATCH 9/9] chore(deps): upgrading @readme/oas-to-har to v7 --- packages/api/package-lock.json | 17 +++++++++-------- packages/api/package.json | 2 +- 2 files changed, 10 insertions(+), 9 deletions(-) diff --git a/packages/api/package-lock.json b/packages/api/package-lock.json index b13fb5fb..ceeba810 100644 --- a/packages/api/package-lock.json +++ b/packages/api/package-lock.json @@ -1617,17 +1617,18 @@ "dev": true }, "@readme/oas-extensions": { - "version": "6.16.1", - "resolved": "https://registry.npmjs.org/@readme/oas-extensions/-/oas-extensions-6.16.1.tgz", - "integrity": "sha512-C8YTgAcJxhjrUFMLUn2zOgBXQJsHYybQXqwmN9FjaUWHCNlqdyi4xlgg8DSfAs/zsW8dY+WnZE5+iNcMFF2wcw==" + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/@readme/oas-extensions/-/oas-extensions-7.0.0.tgz", + "integrity": "sha512-Z6PxFOZNaM9yM+p/bsXb12pRQcz0+KfvCZBnEGRpALUzpQNjPHK7KzQja3vI0Xeq5vi2fufQd+WPfRH+LqaXpQ==" }, "@readme/oas-to-har": { - "version": "6.16.1", - "resolved": "https://registry.npmjs.org/@readme/oas-to-har/-/oas-to-har-6.16.1.tgz", - "integrity": "sha512-c5ZM2PzyWJMimBOvExii3QgnWpuE4WLAQ1dr5nMQjyOA1ERd0zXKttcf/DkSi0Fvl+nbGMaqPFGCbas+kqItBw==", + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/@readme/oas-to-har/-/oas-to-har-7.0.0.tgz", + "integrity": "sha512-Ugpb03zGufgx3biuiiEVq6bJgUvkhMsIhxggtRNO+Kt3kHYYzNrEgsCZT0aV5T34vEBqt9GIo/MS5i9yo170zQ==", "requires": { - "@readme/oas-extensions": "^6.16.1", - "@readme/oas-tooling": "^3.5.4" + "@readme/oas-extensions": "^7.0.0", + "@readme/oas-tooling": "^3.5.8", + "parse-data-url": "^2.0.0" } }, "@readme/oas-tooling": { diff --git a/packages/api/package.json b/packages/api/package.json index a194d7b8..ddb0dd8b 100644 --- a/packages/api/package.json +++ b/packages/api/package.json @@ -25,7 +25,7 @@ "dependencies": { "@apidevtools/json-schema-ref-parser": "^9.0.1", "@apidevtools/swagger-parser": "^10.0.1", - "@readme/oas-to-har": "^6.9.6", + "@readme/oas-to-har": "^7.0.0", "@readme/oas-tooling": "^3.5.8", "datauri": "^3.0.0", "fetch-har": "^4.0.1",