From 71a57bcd39cc62596d82f2db43173b4a655ee68b Mon Sep 17 00:00:00 2001 From: Jon Ursenbach Date: Mon, 11 Sep 2023 16:55:54 -0700 Subject: [PATCH 1/7] feat: moving httpsnippet-client-api to native fetch --- packages/api/src/.sink.d.ts | 1 - packages/api/tsconfig.json | 14 +---------- packages/httpsnippet-client-api/package.json | 4 ++-- .../test/__datasets__/multipart-data/index.ts | 24 ++++++++++++------- .../test/__datasets__/multipart-file/index.ts | 24 ++++++++++++------- .../__datasets__/multipart-form-data/index.ts | 24 ++++++++++++------- .../test/helpers/fetch-mock.ts | 11 --------- .../httpsnippet-client-api/test/index.test.ts | 1 - .../httpsnippet-client-api/test/tsconfig.json | 4 +++- 9 files changed, 51 insertions(+), 56 deletions(-) delete mode 100644 packages/api/src/.sink.d.ts delete mode 100644 packages/httpsnippet-client-api/test/helpers/fetch-mock.ts diff --git a/packages/api/src/.sink.d.ts b/packages/api/src/.sink.d.ts deleted file mode 100644 index 7e0dca49..00000000 --- a/packages/api/src/.sink.d.ts +++ /dev/null @@ -1 +0,0 @@ -declare module 'form-data-encoder'; diff --git a/packages/api/tsconfig.json b/packages/api/tsconfig.json index 72554f64..513f48fa 100644 --- a/packages/api/tsconfig.json +++ b/packages/api/tsconfig.json @@ -6,19 +6,7 @@ "esModuleInterop": true, "lib": ["dom", "dom.iterable", "es2020"], "noImplicitAny": true, - "outDir": "dist/", - "paths": { - // Because this library uses ES2015+ `#private` syntax that would require us to make this - // library ESM-only we're overloading its types with a `paths` config with this empty file. - // This isn't a great solution as we're losing type checks where this library is used, but - // it's far too early in the ESM lifecycle for us to make API an ESM-only library. - // - // And though TS offers an unstable `node12` module resolution that lets us manage this in - // another way that module resolution requires TS nightlies to be installed, which no thanks! - // - // https://github.com/microsoft/TypeScript/issues/17042 - "form-data-encoder": [".sink.d.ts"] - } + "outDir": "dist/" }, "include": ["./src/**/*"] } diff --git a/packages/httpsnippet-client-api/package.json b/packages/httpsnippet-client-api/package.json index c7794007..2da181db 100644 --- a/packages/httpsnippet-client-api/package.json +++ b/packages/httpsnippet-client-api/package.json @@ -22,7 +22,7 @@ "author": "Jon Ursenbach ", "license": "MIT", "engines": { - "node": ">=16" + "node": ">=18" }, "dependencies": { "content-type": "^1.0.5", @@ -40,7 +40,7 @@ "@vitest/coverage-v8": "^0.34.1", "api": "file:../api", "fetch-mock": "^9.11.0", - "isomorphic-fetch": "^3.0.0", + "formdata-to-string": "^1.0.0", "typescript": "^5.2.2", "vitest": "^0.34.1" }, diff --git a/packages/httpsnippet-client-api/test/__datasets__/multipart-data/index.ts b/packages/httpsnippet-client-api/test/__datasets__/multipart-data/index.ts index 4d8b0564..fc6e7c77 100644 --- a/packages/httpsnippet-client-api/test/__datasets__/multipart-data/index.ts +++ b/packages/httpsnippet-client-api/test/__datasets__/multipart-data/index.ts @@ -1,7 +1,7 @@ import type { SnippetMock } from '../../index.test'; import type { OASDocument } from 'oas/dist/rmoas.types'; -import { streamToString } from '../../helpers/fetch-mock'; +import formDataToString from 'formdata-to-string'; import definition from './openapi.json'; @@ -54,16 +54,22 @@ const mock: SnippetMock = { req: { scope: 'https://httpbin.org/anything', method: 'post', - /* headers: { // `fetch-mock` doesn't support regex matching on headers - 'content-type': /multipart\/form-data; boundary=form-data-boundary-(.*)/, - }, */ // @ts-expect-error Types don't reflect it but `fetch-mock` supports async function matchers. - functionMatcher: async (url, opts) => { - const body = await streamToString(opts.body); + functionMatcher: async (url, { body }: { body: FormData }) => { + const content = await formDataToString(body); - return /--form-data-boundary-(.*)\r\nContent-Disposition: form-data; name="foo"; filename="hello.txt"\r\nContent-Type: text\/plain\r\n\r\nHello world!\n\r\n--form-data-boundary-(.*)--\r\n\r\n/.test( - body, - ); + // A fun thing about `fetch-mock` and these undocumented async matchers is that it doesn't + // look at what you're returning so we could return `false` here and it would think that + // the request was matched. Very cool. + if ( + !/------formdata-undici-(.*)\r\nContent-Disposition: form-data; name="foo"; filename="hello.txt"\r\nContent-Type: text\/plain\r\n\r\nHello world!\n\r\n------formdata-undici-(.*)--/.test( + content, + ) + ) { + throw new Error('The FormData payload does not match what was expected.'); + } + + return true; }, }, res: { diff --git a/packages/httpsnippet-client-api/test/__datasets__/multipart-file/index.ts b/packages/httpsnippet-client-api/test/__datasets__/multipart-file/index.ts index be96159b..e39097d2 100644 --- a/packages/httpsnippet-client-api/test/__datasets__/multipart-file/index.ts +++ b/packages/httpsnippet-client-api/test/__datasets__/multipart-file/index.ts @@ -1,7 +1,7 @@ import type { SnippetMock } from '../../index.test'; import type { OASDocument } from 'oas/dist/rmoas.types'; -import { streamToString } from '../../helpers/fetch-mock'; +import formDataToString from 'formdata-to-string'; import definition from './openapi.json'; @@ -36,16 +36,22 @@ const mock: SnippetMock = { req: { url: 'https://httpbin.org/anything', method: 'post', - /* headers: { // `fetch-mock` doesn't support regex matching on headers - 'content-type': /multipart\/form-data; boundary=form-data-boundary-(.*)/, - }, */ // @ts-expect-error Types don't reflect it but `fetch-mock` supports async function matchers. - functionMatcher: async (url, opts) => { - const body = await streamToString(opts.body); + functionMatcher: async (url, { body }: { body: FormData }) => { + const content = await formDataToString(body); - return /--form-data-boundary-(.*)\r\nContent-Disposition: form-data; name="foo"; filename="hello.txt"\r\nContent-Type: text\/plain\r\n\r\nHello world!\n\r\n--form-data-boundary-(.*)--\r\n\r\n/.test( - body, - ); + // A fun thing about `fetch-mock` and these undocumented async matchers is that it doesn't + // look at what you're returning so we could return `false` here and it would think that + // the request was matched. Very cool. + if ( + !/------formdata-undici-(.*)\r\nContent-Disposition: form-data; name="foo"; filename="hello.txt"\r\nContent-Type: text\/plain\r\n\r\nHello world!\n\r\n------formdata-undici-(.*)--/.test( + content, + ) + ) { + throw new Error('The FormData payload does not match what was expected.'); + } + + return true; }, }, res: { diff --git a/packages/httpsnippet-client-api/test/__datasets__/multipart-form-data/index.ts b/packages/httpsnippet-client-api/test/__datasets__/multipart-form-data/index.ts index 4f3bbd81..e552da72 100644 --- a/packages/httpsnippet-client-api/test/__datasets__/multipart-form-data/index.ts +++ b/packages/httpsnippet-client-api/test/__datasets__/multipart-form-data/index.ts @@ -1,7 +1,7 @@ import type { SnippetMock } from '../../index.test'; import type { OASDocument } from 'oas/dist/rmoas.types'; -import { streamToString } from '../../helpers/fetch-mock'; +import formDataToString from 'formdata-to-string'; import definition from './openapi.json'; @@ -35,16 +35,22 @@ const mock: SnippetMock = { req: { url: 'https://httpbin.org/anything', method: 'POST', - /* headers: { // `fetch-mock` doesn't support regex matching on headers - 'content-type': /multipart\/form-data; boundary=form-data-boundary-(.*)/, - }, */ // @ts-expect-error Types don't reflect it but `fetch-mock` supports async function matchers. - functionMatcher: async (url, opts) => { - const body = await streamToString(opts.body); + functionMatcher: async (url, { body }: { body: FormData }) => { + const content = await formDataToString(body); - return /--form-data-boundary-(.*)\r\nContent-Disposition: form-data; name="foo"\r\n\r\nbar\r\n--form-data-boundary-(.*)--\r\n\r\n/.test( - body, - ); + // A fun thing about `fetch-mock` and these undocumented async matchers is that it doesn't + // look at what you're returning so we could return `false` here and it would think that + // the request was matched. Very cool. + if ( + !/------formdata-undici-(.*)\r\nContent-Disposition: form-data; name="foo"\r\n\r\nbar\r\n------formdata-undici-(.*)--/.test( + content, + ) + ) { + throw new Error('The FormData payload does not match what was expected.'); + } + + return true; }, }, res: { diff --git a/packages/httpsnippet-client-api/test/helpers/fetch-mock.ts b/packages/httpsnippet-client-api/test/helpers/fetch-mock.ts deleted file mode 100644 index f264819f..00000000 --- a/packages/httpsnippet-client-api/test/helpers/fetch-mock.ts +++ /dev/null @@ -1,11 +0,0 @@ -/** - * @see {@link https://stackoverflow.com/questions/10623798/how-do-i-read-the-contents-of-a-node-js-stream-into-a-string-variable} - */ -export function streamToString(stream): Promise { - const chunks = []; - return new Promise((resolve, reject) => { - stream.on('data', chunk => chunks.push(Buffer.from(chunk))); - stream.on('error', err => reject(err)); - stream.on('end', () => resolve(Buffer.concat(chunks).toString('utf8'))); - }); -} diff --git a/packages/httpsnippet-client-api/test/index.test.ts b/packages/httpsnippet-client-api/test/index.test.ts index 30e741fe..0081419e 100644 --- a/packages/httpsnippet-client-api/test/index.test.ts +++ b/packages/httpsnippet-client-api/test/index.test.ts @@ -13,7 +13,6 @@ import { HTTPSnippet, addTargetClient } from '@readme/httpsnippet'; import readme from '@readme/oas-examples/3.0/json/readme.json'; import openapiParser from '@readme/openapi-parser'; import fetchMock from 'fetch-mock'; -import 'isomorphic-fetch'; import rimraf from 'rimraf'; import { describe, afterEach, beforeEach, expect, it, vi } from 'vitest'; diff --git a/packages/httpsnippet-client-api/test/tsconfig.json b/packages/httpsnippet-client-api/test/tsconfig.json index 72079533..59c11cc5 100644 --- a/packages/httpsnippet-client-api/test/tsconfig.json +++ b/packages/httpsnippet-client-api/test/tsconfig.json @@ -2,7 +2,9 @@ "extends": "../tsconfig.json", "compilerOptions": { "noImplicitAny": false, - "resolveJsonModule": true + "module": "Node16", + "resolveJsonModule": true, + "target": "ES2015" }, "include": ["../src/**/*", "*.ts", "**/*"], "exclude": ["__fixtures__/sdk/"] From 9a10aae3d39c26db3e159bf62cebdc1abcc9c34e Mon Sep 17 00:00:00 2001 From: Jon Ursenbach Date: Mon, 11 Sep 2023 17:08:54 -0700 Subject: [PATCH 2/7] feat: moving to native fetch in api --- .github/dependabot.yml | 6 - package-lock.json | 129 ++++++++----------- packages/api/package.json | 9 +- packages/api/src/cache.ts | 1 - packages/api/src/core/index.ts | 7 - packages/api/src/fetcher.ts | 1 - packages/api/test/cli/storage.test.ts | 13 +- packages/api/test/core/parseResponse.test.ts | 1 - packages/api/test/datasets/refresh-dataset | 3 +- packages/api/test/fetcher.test.ts | 12 +- packages/api/test/helpers/fetch-mock.ts | 16 +-- packages/api/test/integration.test.ts | 24 ++-- 12 files changed, 76 insertions(+), 146 deletions(-) diff --git a/.github/dependabot.yml b/.github/dependabot.yml index a76fcb62..33374ef1 100644 --- a/.github/dependabot.yml +++ b/.github/dependabot.yml @@ -48,12 +48,6 @@ updates: - dependency-name: find-cache-dir versions: - '>= 4' - - dependency-name: form-data-encoder - versions: - - '>= 2' - - dependency-name: formdata-node - versions: - - '>= 5' - dependency-name: get-stream versions: - '>= 7' diff --git a/package-lock.json b/package-lock.json index 73efea61..9ae8eeda 100644 --- a/package-lock.json +++ b/package-lock.json @@ -3734,9 +3734,9 @@ } }, "node_modules/@types/har-format": { - "version": "1.2.8", - "resolved": "https://registry.npmjs.org/@types/har-format/-/har-format-1.2.8.tgz", - "integrity": "sha512-OP6L9VuZNdskgNN3zFQQ54ceYD8OLq5IbqO4VK91ORLfOm7WdT/CiT/pHEBSQEqCInJ2y3O6iCm/zGtPElpgJQ==" + "version": "1.2.12", + "resolved": "https://registry.npmjs.org/@types/har-format/-/har-format-1.2.12.tgz", + "integrity": "sha512-P20p/YBrqUBmzD6KhIQ8EiY4/RRzlekL4eCvfQnulFPfjmiGxKIoyCeI7qam5I7oKH3P8EU4ptEi0EfyGoLysw==" }, "node_modules/@types/hast": { "version": "2.3.4", @@ -5845,6 +5845,17 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/busboy": { + "version": "1.6.0", + "resolved": "https://registry.npmjs.org/busboy/-/busboy-1.6.0.tgz", + "integrity": "sha512-8SFQbg/0hQ9xy3UNTB0YEnsNBbWfhf7RtnzpL7TkBiTBRfrQ9Fxcnz7VJsleJpyp6rVLvXiuORqjlHi5q+PYuA==", + "dependencies": { + "streamsearch": "^1.1.0" + }, + "engines": { + "node": ">=10.16.0" + } + }, "node_modules/byte-size": { "version": "8.1.1", "resolved": "https://registry.npmjs.org/byte-size/-/byte-size-8.1.1.tgz", @@ -9284,19 +9295,17 @@ } }, "node_modules/fetch-har": { - "version": "8.1.5", - "resolved": "https://registry.npmjs.org/fetch-har/-/fetch-har-8.1.5.tgz", - "integrity": "sha512-c9WDro4RWC+suOVRJFNW21cgqTOELRZpvFJgfENvOM7Yt/VA4QeFtRax795SyOpTisdpcl5XNQlQZdAE6HERDA==", + "version": "10.0.0", + "resolved": "https://registry.npmjs.org/fetch-har/-/fetch-har-10.0.0.tgz", + "integrity": "sha512-GTs1e4JttuyyVn7V0liRR5WbzyCxsjs0mUfrw3UMt+mqvTR5GS5zdGWaOr4tFHIuAkgCEGDZ/1hObTU430MT6Q==", "dependencies": { "@readme/data-urls": "^1.0.1", - "@types/har-format": "^1.2.8", - "readable-stream": "^3.6.0" + "@types/har-format": "^1.2.12", + "readable-stream": "^3.6.0", + "undici": "^5.24.0" }, "engines": { - "node": ">=14" - }, - "optionalDependencies": { - "formdata-node": "^4.3.2" + "node": ">=18" } }, "node_modules/fetch-mock": { @@ -9543,11 +9552,6 @@ "node": ">= 6" } }, - "node_modules/form-data-encoder": { - "version": "1.7.2", - "resolved": "https://registry.npmjs.org/form-data-encoder/-/form-data-encoder-1.7.2.tgz", - "integrity": "sha512-qfqtYan3rxrnCk1VYaA4H+Ms9xdpPqvLZa6xmMgFvhO32x7/3J/ExcTd6qpxM0vH2GdMI+poehyBZvqfMTto8A==" - }, "node_modules/format": { "version": "0.2.2", "resolved": "https://registry.npmjs.org/format/-/format-0.2.2.tgz", @@ -9557,16 +9561,16 @@ "node": ">=0.4.x" } }, - "node_modules/formdata-node": { - "version": "4.3.2", - "resolved": "https://registry.npmjs.org/formdata-node/-/formdata-node-4.3.2.tgz", - "integrity": "sha512-k7lYJyzDOSL6h917favP8j1L0/wNyylzU+x+1w4p5haGVHNlP58dbpdJhiCUsDbWsa9HwEtLp89obQgXl2e0qg==", + "node_modules/formdata-to-string": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/formdata-to-string/-/formdata-to-string-1.0.0.tgz", + "integrity": "sha512-VTseQXEHo086wgchDG6priy+GSUE6CGjdJ+02vQbDshiHZ70FmsheGfYVTF47NGMDX27zsH9tvVvcf+SENP4Rw==", + "dev": true, "dependencies": { - "node-domexception": "1.0.0", - "web-streams-polyfill": "4.0.0-beta.1" + "undici": "^5.24.0" }, "engines": { - "node": ">= 12.20" + "node": ">=18" } }, "node_modules/from": { @@ -11573,15 +11577,6 @@ "node": ">=0.10.0" } }, - "node_modules/isomorphic-fetch": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/isomorphic-fetch/-/isomorphic-fetch-3.0.0.tgz", - "integrity": "sha512-qvUtwJ3j6qwsF3jLxkZ72qCgjMysPzDfeV240JHiGZsANBYd+EEuu35v7dfrJ9Up0Ak07D7GGSkGhCHTqg/5wA==", - "dependencies": { - "node-fetch": "^2.6.1", - "whatwg-fetch": "^3.4.1" - } - }, "node_modules/istanbul-lib-coverage": { "version": "3.2.0", "resolved": "https://registry.npmjs.org/istanbul-lib-coverage/-/istanbul-lib-coverage-3.2.0.tgz", @@ -15339,35 +15334,12 @@ "url": "https://opencollective.com/unified" } }, - "node_modules/node-abort-controller": { - "version": "3.1.1", - "resolved": "https://registry.npmjs.org/node-abort-controller/-/node-abort-controller-3.1.1.tgz", - "integrity": "sha512-AGK2yQKIjRuqnc6VkX2Xj5d+QW8xZ87pa1UK6yA6ouUyuxfHuMP6umE5QK7UmTeOAymo+Zx1Fxiuw9rVx8taHQ==" - }, "node_modules/node-addon-api": { "version": "3.2.1", "resolved": "https://registry.npmjs.org/node-addon-api/-/node-addon-api-3.2.1.tgz", "integrity": "sha512-mmcei9JghVNDYydghQmeDX8KoAm0FAiYyIcUt/N4nhyAipB17pllZQDOJD2fotxABnt4Mdz+dKTO7eftLg4d0A==", "dev": true }, - "node_modules/node-domexception": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/node-domexception/-/node-domexception-1.0.0.tgz", - "integrity": "sha512-/jKZoMpw0F8GRwl4/eLROPA3cfcXtLApP0QzLmUT/HuPCZWyB7IY9ZrMeKw2O/nFIqPQB3PVM9aYm0F312AXDQ==", - "funding": [ - { - "type": "github", - "url": "https://github.com/sponsors/jimmywarting" - }, - { - "type": "github", - "url": "https://paypal.me/jimmywarting" - } - ], - "engines": { - "node": ">=10.5.0" - } - }, "node_modules/node-fetch": { "version": "2.6.7", "resolved": "https://registry.npmjs.org/node-fetch/-/node-fetch-2.6.7.tgz", @@ -18869,6 +18841,14 @@ "through": "~2.3.4" } }, + "node_modules/streamsearch": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/streamsearch/-/streamsearch-1.1.0.tgz", + "integrity": "sha512-Mcc5wHehp9aXz1ax6bZUyY5afg9u2rv5cqQI3mRrYkGC8rW2hM02jWuwjtL++LS5qinSyhj2QfLyNsuc+VsExg==", + "engines": { + "node": ">=10.0.0" + } + }, "node_modules/string_decoder": { "version": "1.3.0", "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.3.0.tgz", @@ -19779,6 +19759,17 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/undici": { + "version": "5.24.0", + "resolved": "https://registry.npmjs.org/undici/-/undici-5.24.0.tgz", + "integrity": "sha512-OKlckxBjFl0oXxcj9FU6oB8fDAaiRUq+D8jrFWGmOfI/gIyjk/IeS75LMzgYKUaeHzLUcYvf9bbJGSrUwTfwwQ==", + "dependencies": { + "busboy": "^1.6.0" + }, + "engines": { + "node": ">=14.0" + } + }, "node_modules/unherit": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/unherit/-/unherit-3.0.0.tgz", @@ -20753,25 +20744,12 @@ "url": "https://github.com/sponsors/wooorm" } }, - "node_modules/web-streams-polyfill": { - "version": "4.0.0-beta.1", - "resolved": "https://registry.npmjs.org/web-streams-polyfill/-/web-streams-polyfill-4.0.0-beta.1.tgz", - "integrity": "sha512-3ux37gEX670UUphBF9AMCq8XM6iQ8Ac6A+DSRRjDoRBm1ufCkaCDdNVbaqq60PsEkdNlLKrGtv/YBP4EJXqNtQ==", - "engines": { - "node": ">= 12" - } - }, "node_modules/webidl-conversions": { "version": "4.0.2", "resolved": "https://registry.npmjs.org/webidl-conversions/-/webidl-conversions-4.0.2.tgz", "integrity": "sha512-YQ+BmxuTgd6UXZW3+ICGfyqRyHXVlD5GtQr5+qjiNW7bF0cqrzX500HVXPBOvgXb5YnzDd+h0zqyv61KUD7+Sg==", "dev": true }, - "node_modules/whatwg-fetch": { - "version": "3.6.2", - "resolved": "https://registry.npmjs.org/whatwg-fetch/-/whatwg-fetch-3.6.2.tgz", - "integrity": "sha512-bJlen0FcuU/0EMLrdbJ7zOnW6ITZLrZMIarMUVmdKtsGvZna8vxKYaexICWPfZ8qwf9fzNq+UEIZrnSaApt6RA==" - }, "node_modules/whatwg-url": { "version": "6.5.0", "resolved": "https://registry.npmjs.org/whatwg-url/-/whatwg-url-6.5.0.tgz", @@ -21202,13 +21180,10 @@ "commander": "^11.0.0", "datauri": "^4.1.0", "execa": "^5.1.1", - "fetch-har": "^8.1.5", + "fetch-har": "^10.0.0", "figures": "^3.2.0", "find-cache-dir": "^3.3.1", - "form-data-encoder": "^1.7.2", - "formdata-node": "^4.3.2", "get-stream": "^6.0.1", - "isomorphic-fetch": "^3.0.0", "js-yaml": "^4.1.0", "json-schema-to-ts": "^2.9.2", "json-schema-traverse": "^1.0.0", @@ -21218,7 +21193,6 @@ "lodash.setwith": "^4.3.2", "lodash.startcase": "^4.4.0", "make-dir": "^3.1.0", - "node-abort-controller": "^3.1.1", "oas": "^20.4.0", "ora": "^5.4.1", "prompts": "^2.4.2", @@ -21248,6 +21222,7 @@ "@types/validate-npm-package-name": "^4.0.0", "@vitest/coverage-v8": "^0.34.1", "fetch-mock": "^9.11.0", + "formdata-to-string": "^1.0.0", "oas-normalize": "^8.3.2", "type-fest": "^4.3.1", "typescript": "^5.2.2", @@ -21255,7 +21230,7 @@ "vitest": "^0.34.1" }, "engines": { - "node": ">=16" + "node": ">=18" } }, "packages/api/node_modules/@types/find-cache-dir": { @@ -21294,12 +21269,12 @@ "@vitest/coverage-v8": "^0.34.1", "api": "file:../api", "fetch-mock": "^9.11.0", - "isomorphic-fetch": "^3.0.0", + "formdata-to-string": "^1.0.0", "typescript": "^5.2.2", "vitest": "^0.34.1" }, "engines": { - "node": ">=16" + "node": ">=18" }, "peerDependencies": { "@readme/httpsnippet": ">=4", diff --git a/packages/api/package.json b/packages/api/package.json index 0aac4446..65840b7c 100644 --- a/packages/api/package.json +++ b/packages/api/package.json @@ -29,7 +29,7 @@ "author": "Jon Ursenbach ", "license": "MIT", "engines": { - "node": ">=16" + "node": ">=18" }, "keywords": [ "api", @@ -45,13 +45,10 @@ "commander": "^11.0.0", "datauri": "^4.1.0", "execa": "^5.1.1", - "fetch-har": "^8.1.5", + "fetch-har": "^10.0.0", "figures": "^3.2.0", "find-cache-dir": "^3.3.1", - "form-data-encoder": "^1.7.2", - "formdata-node": "^4.3.2", "get-stream": "^6.0.1", - "isomorphic-fetch": "^3.0.0", "js-yaml": "^4.1.0", "json-schema-to-ts": "^2.9.2", "json-schema-traverse": "^1.0.0", @@ -61,7 +58,6 @@ "lodash.setwith": "^4.3.2", "lodash.startcase": "^4.4.0", "make-dir": "^3.1.0", - "node-abort-controller": "^3.1.1", "oas": "^20.4.0", "ora": "^5.4.1", "prompts": "^2.4.2", @@ -88,6 +84,7 @@ "@types/validate-npm-package-name": "^4.0.0", "@vitest/coverage-v8": "^0.34.1", "fetch-mock": "^9.11.0", + "formdata-to-string": "^1.0.0", "oas-normalize": "^8.3.2", "type-fest": "^4.3.1", "typescript": "^5.2.2", diff --git a/packages/api/src/cache.ts b/packages/api/src/cache.ts index 6857b387..0be32885 100644 --- a/packages/api/src/cache.ts +++ b/packages/api/src/cache.ts @@ -6,7 +6,6 @@ import os from 'os'; import path from 'path'; import findCacheDir from 'find-cache-dir'; -import 'isomorphic-fetch'; import makeDir from 'make-dir'; import Fetcher from './fetcher'; diff --git a/packages/api/src/core/index.ts b/packages/api/src/core/index.ts index eb7ab2cd..09c2a71e 100644 --- a/packages/api/src/core/index.ts +++ b/packages/api/src/core/index.ts @@ -4,10 +4,6 @@ import type { HttpMethods } from 'oas/dist/rmoas.types'; import oasToHar from '@readme/oas-to-har'; import fetchHar from 'fetch-har'; -import { FormDataEncoder } from 'form-data-encoder'; -import 'isomorphic-fetch'; -// `AbortController` was shipped in Node 15 so when Node 14 is EOL'd we can drop this dependency. -import { AbortController } from 'node-abort-controller'; import FetchError from './errors/fetchError'; import getJSONSchemaDefaults from './getJSONSchemaDefaults'; @@ -113,15 +109,12 @@ export default class APICore { if (this.config.timeout) { const controller = new AbortController(); timeoutSignal = setTimeout(() => controller.abort(), this.config.timeout); - // @todo Typing on `AbortController` coming out of `node-abort-controler` isn't right so when - // we eventually drop that dependency we can remove the `as any` here. init.signal = controller.signal as any; } return fetchHar(har as any, { files: data.files || {}, init, - multipartEncoder: FormDataEncoder, userAgent: this.userAgent, }) .then(async (res: Response) => { diff --git a/packages/api/src/fetcher.ts b/packages/api/src/fetcher.ts index 7e0cef72..1f189c3b 100644 --- a/packages/api/src/fetcher.ts +++ b/packages/api/src/fetcher.ts @@ -4,7 +4,6 @@ import fs from 'fs'; import path from 'path'; import OpenAPIParser from '@readme/openapi-parser'; -import 'isomorphic-fetch'; import yaml from 'js-yaml'; export default class Fetcher { diff --git a/packages/api/test/cli/storage.test.ts b/packages/api/test/cli/storage.test.ts index 86b7b3d2..6111fe46 100644 --- a/packages/api/test/cli/storage.test.ts +++ b/packages/api/test/cli/storage.test.ts @@ -5,7 +5,6 @@ import fs from 'fs/promises'; import path from 'path'; import fetchMock from 'fetch-mock'; -import 'isomorphic-fetch'; import uniqueTempDir from 'unique-temp-dir'; import { describe, beforeAll, beforeEach, afterEach, it, expect } from 'vitest'; @@ -131,16 +130,8 @@ describe('storage', () => { .load() .then(() => assert.fail()) .catch(err => { - // The native `fetch` implementation in Node 18 returns a different error, with the new - // `Error.cause` data, so we should make sure that we're catching that case instead of - // the `node-fetch` error message. - const isNode18 = Number(process.versions.node.split('.')[0]) >= 18; - if (isNode18) { - expect(err.message).toBe('fetch failed'); - expect(err.cause.message).toBe('unknown scheme'); - } else { - expect(err.message).toBe('Only HTTP(S) protocols are supported'); - } + expect(err.message).toBe('fetch failed'); + expect(err.cause.message).toBe('unknown scheme'); }); }); diff --git a/packages/api/test/core/parseResponse.test.ts b/packages/api/test/core/parseResponse.test.ts index bdbfa71b..42e9fc89 100644 --- a/packages/api/test/core/parseResponse.test.ts +++ b/packages/api/test/core/parseResponse.test.ts @@ -1,4 +1,3 @@ -import 'isomorphic-fetch'; import { describe, beforeEach, it, expect } from 'vitest'; import parseResponse from '../../src/core/parseResponse'; diff --git a/packages/api/test/datasets/refresh-dataset b/packages/api/test/datasets/refresh-dataset index c8343bd3..d2d875b6 100755 --- a/packages/api/test/datasets/refresh-dataset +++ b/packages/api/test/datasets/refresh-dataset @@ -7,10 +7,9 @@ */ /* eslint-disable @typescript-eslint/no-var-requires */ /* eslint-disable no-param-reassign */ +/* eslint-disable vitest/require-hook */ const fs = require('fs'); -require('isomorphic-fetch'); - fetch('https://api.apis.guru/v2/list.json') .then(response => { if (!response.ok) { diff --git a/packages/api/test/fetcher.test.ts b/packages/api/test/fetcher.test.ts index 7a7a77db..d281c673 100644 --- a/packages/api/test/fetcher.test.ts +++ b/packages/api/test/fetcher.test.ts @@ -80,16 +80,8 @@ describe('fetcher', () => { .load() .then(() => assert.fail()) .catch(err => { - // The native `fetch` implementation in Node 18 returns a different error, with the new - // `Error.cause` data, so we should make sure that we're catching that case instead of - // the `node-fetch` error message. - const isNode18 = Number(process.versions.node.split('.')[0]) >= 18; - if (isNode18) { - expect(err.message).toBe('fetch failed'); - expect(err.cause.message).toBe('unknown scheme'); - } else { - expect(err.message).toBe('Only HTTP(S) protocols are supported'); - } + expect(err.message).toBe('fetch failed'); + expect(err.cause.message).toBe('unknown scheme'); }); }); diff --git a/packages/api/test/helpers/fetch-mock.ts b/packages/api/test/helpers/fetch-mock.ts index 491f400b..e43d764f 100644 --- a/packages/api/test/helpers/fetch-mock.ts +++ b/packages/api/test/helpers/fetch-mock.ts @@ -1,4 +1,5 @@ import DatauriParser from 'datauri/parser'; +import formDataToString from 'formdata-to-string'; function objectifyHeaders(headers: []) { return Object.fromEntries(headers); @@ -42,23 +43,14 @@ export const responses = { }, multipart: async (url, opts) => { - // https://stackoverflow.com/questions/10623798/how-do-i-read-the-contents-of-a-node-js-stream-into-a-string-variable - function streamToString(stream) { - const chunks = []; - return new Promise((resolve, reject) => { - stream.on('data', chunk => chunks.push(Buffer.from(chunk))); - stream.on('error', err => reject(err)); - stream.on('end', () => resolve(Buffer.concat(chunks).toString('utf8'))); - }); - } - const headers = objectifyHeaders(opts.headers); + const payload = await formDataToString(opts.body); return { uri: new URL(url).pathname, - requestBody: await streamToString(opts.body), + requestBody: payload, headers, - boundary: headers['content-type'].split('boundary=')[1], + boundary: payload.split('\r\n')[0], }; }, diff --git a/packages/api/test/integration.test.ts b/packages/api/test/integration.test.ts index af88a1e9..3321d6e0 100644 --- a/packages/api/test/integration.test.ts +++ b/packages/api/test/integration.test.ts @@ -206,15 +206,15 @@ describe('integration tests', () => { const { data } = await api(parametersStyle as unknown as OASDocument).formData_form_nonExploded(body); expect(data.uri).toBe('/anything/form-data/form'); - expect(data.requestBody.split(`--${data.boundary}`).filter(Boolean)).toStrictEqual([ + expect(data.requestBody.split(`${data.boundary}`).filter(Boolean)).toStrictEqual([ '\r\nContent-Disposition: form-data; name="primitive"\r\n\r\nstring\r\n', '\r\nContent-Disposition: form-data; name="array"\r\n\r\nstring\r\n', '\r\nContent-Disposition: form-data; name="object"\r\n\r\nfoo,foo-string,bar,bar-string\r\n', - '--\r\n\r\n', + '--', ]); - expect(data.headers).toHaveProperty('content-type', `multipart/form-data; boundary=${data.boundary}`); - expect(data.headers).toHaveProperty('content-length', '356'); + // expect(data.headers).toHaveProperty('content-type', `multipart/form-data; boundary=${data.boundary}`); + // expect(data.headers).toHaveProperty('content-length', '356'); expect(data.headers).toHaveCustomUserAgent(); }); @@ -230,15 +230,15 @@ describe('integration tests', () => { const { data } = await api(fileUploads as unknown as OASDocument).postAnythingMultipartFormdata(body); expect(data.uri).toBe('/anything/multipart-formdata'); - expect(data.requestBody.split(`--${data.boundary}`).filter(Boolean)).toStrictEqual([ + expect(data.requestBody.split(`${data.boundary}`).filter(Boolean)).toStrictEqual([ '\r\nContent-Disposition: form-data; name="orderId"\r\n\r\n1234\r\n', '\r\nContent-Disposition: form-data; name="userId"\r\n\r\n5678\r\n', '\r\nContent-Disposition: form-data; name="documentFile"; filename="hello.txt"\r\nContent-Type: text/plain\r\n\r\nHello world!\n\r\n', - '--\r\n\r\n', + '--', ]); - expect(data.headers).toHaveProperty('content-type', `multipart/form-data; boundary=${data.boundary}`); - expect(data.headers).toHaveProperty('content-length', '389'); + // expect(data.headers).toHaveProperty('content-type', `multipart/form-data; boundary=${data.boundary}`); + // expect(data.headers).toHaveProperty('content-length', '389'); expect(data.headers).toHaveCustomUserAgent(); }); @@ -251,13 +251,13 @@ describe('integration tests', () => { const { data } = await api(fileUploads as unknown as OASDocument).postAnythingMultipartFormdata(body); expect(data.uri).toBe('/anything/multipart-formdata'); - expect(data.requestBody.split(`--${data.boundary}`).filter(Boolean)).toStrictEqual([ + expect(data.requestBody.split(`${data.boundary}`).filter(Boolean)).toStrictEqual([ '\r\nContent-Disposition: form-data; name="documentFile"; filename="hello.jp.txt"\r\nContent-Type: text/plain\r\n\r\n速い茶色のキツネは怠惰な犬を飛び越えます\n\r\n', - '--\r\n\r\n', + '--', ]); - expect(data.headers).toHaveProperty('content-type', `multipart/form-data; boundary=${data.boundary}`); - expect(data.headers).toHaveProperty('content-length', '251'); + // expect(data.headers).toHaveProperty('content-type', `multipart/form-data; boundary=${data.boundary}`); + // expect(data.headers).toHaveProperty('content-length', '251'); expect(data.headers).toHaveCustomUserAgent(); }); }); From 42373f338543bf5c56948c226ade1ae23d60aabb Mon Sep 17 00:00:00 2001 From: Jon Ursenbach Date: Mon, 11 Sep 2023 17:13:15 -0700 Subject: [PATCH 3/7] fix: stop testing node 16 --- .github/workflows/ci.yml | 3 +-- .github/workflows/smoketest.yml | 2 +- 2 files changed, 2 insertions(+), 3 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 56d464d3..4a4fa287 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -13,7 +13,7 @@ jobs: - uses: actions/checkout@v4 - uses: actions/setup-node@v3 with: - node-version: 16 + node-version: 18 - run: npm ci - run: npm run build @@ -28,7 +28,6 @@ jobs: strategy: matrix: node-version: - - 16 - 18 - 20 diff --git a/.github/workflows/smoketest.yml b/.github/workflows/smoketest.yml index 34b27ab4..51209fad 100644 --- a/.github/workflows/smoketest.yml +++ b/.github/workflows/smoketest.yml @@ -18,7 +18,7 @@ jobs: - uses: actions/checkout@v4 - uses: actions/setup-node@v3 with: - node-version: 16 + node-version: 18 - run: npm ci - run: npm run build From 387320a2aaf9c08c38a96185de28615f9165db99 Mon Sep 17 00:00:00 2001 From: Jon Ursenbach Date: Mon, 11 Sep 2023 17:50:23 -0700 Subject: [PATCH 4/7] feat: deprecating the dynamic `api` entrypoint --- .github/dependabot.yml | 6 - package-lock.json | 36 +--- packages/api/example.js | 13 -- packages/api/package.json | 4 - packages/api/src/index.ts | 203 ----------------- packages/api/test/auth.test.ts | 225 ------------------- packages/api/test/cache-custom.test.ts | 59 ----- packages/api/test/cache-tmp.test.ts | 26 --- packages/api/test/cache.test.ts | 273 ----------------------- packages/api/test/config.test.ts | 53 ----- packages/api/test/core/index.test.ts | 146 +++++++++++++ packages/api/test/dist.test.ts | 50 ----- packages/api/test/index.test.ts | 287 ------------------------- packages/api/test/integration.test.ts | 276 ------------------------ packages/api/test/server.test.ts | 63 ------ 15 files changed, 153 insertions(+), 1567 deletions(-) delete mode 100644 packages/api/example.js delete mode 100644 packages/api/src/index.ts delete mode 100644 packages/api/test/auth.test.ts delete mode 100644 packages/api/test/cache-custom.test.ts delete mode 100644 packages/api/test/cache-tmp.test.ts delete mode 100644 packages/api/test/cache.test.ts delete mode 100644 packages/api/test/config.test.ts delete mode 100644 packages/api/test/dist.test.ts delete mode 100644 packages/api/test/index.test.ts delete mode 100644 packages/api/test/integration.test.ts delete mode 100644 packages/api/test/server.test.ts diff --git a/.github/dependabot.yml b/.github/dependabot.yml index 33374ef1..0a22f21e 100644 --- a/.github/dependabot.yml +++ b/.github/dependabot.yml @@ -33,9 +33,6 @@ updates: ignore: # The following are packages that we're ignoring updates from because they've moved to being # ESM-only packages and we can't yet upgrade them. - - dependency-name: '@types/find-cache-dir' - versions: - - '>= 4' - dependency-name: chalk versions: - '>= 5' @@ -45,9 +42,6 @@ updates: - dependency-name: figures versions: - '>= 4' - - dependency-name: find-cache-dir - versions: - - '>= 4' - dependency-name: get-stream versions: - '>= 7' diff --git a/package-lock.json b/package-lock.json index 9ae8eeda..3a4baa44 100644 --- a/package-lock.json +++ b/package-lock.json @@ -6427,11 +6427,6 @@ "node": ">=16" } }, - "node_modules/commondir": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/commondir/-/commondir-1.0.1.tgz", - "integrity": "sha1-3dgA2gxmEnOTzKWVDqloo6rxJTs=" - }, "node_modules/compare-func": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/compare-func/-/compare-func-2.0.0.tgz", @@ -9422,22 +9417,6 @@ "node": ">=8" } }, - "node_modules/find-cache-dir": { - "version": "3.3.2", - "resolved": "https://registry.npmjs.org/find-cache-dir/-/find-cache-dir-3.3.2.tgz", - "integrity": "sha512-wXZV5emFEjrridIgED11OoUKLxiYjAcqot/NJdAkOhlJ+vGzwhOAfcG5OX1jP+S0PcjEn8bdMJv+g2jwQ3Onig==", - "dependencies": { - "commondir": "^1.0.1", - "make-dir": "^3.0.2", - "pkg-dir": "^4.1.0" - }, - "engines": { - "node": ">=8" - }, - "funding": { - "url": "https://github.com/avajs/find-cache-dir?sponsor=1" - } - }, "node_modules/find-up": { "version": "5.0.0", "resolved": "https://registry.npmjs.org/find-up/-/find-up-5.0.0.tgz", @@ -16486,6 +16465,7 @@ "version": "2.2.0", "resolved": "https://registry.npmjs.org/p-try/-/p-try-2.2.0.tgz", "integrity": "sha512-R4nPAVTAU0B9D35/Gk3uJf/7XYbQcyohSKdvAxIRSNghFl4e71hVoGnBNQz9cWaXxO2I10KTC+3jMdvvoKw6dQ==", + "dev": true, "engines": { "node": ">=6" } @@ -16782,6 +16762,7 @@ "version": "4.0.0", "resolved": "https://registry.npmjs.org/path-exists/-/path-exists-4.0.0.tgz", "integrity": "sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w==", + "dev": true, "engines": { "node": ">=8" } @@ -16915,6 +16896,7 @@ "version": "4.2.0", "resolved": "https://registry.npmjs.org/pkg-dir/-/pkg-dir-4.2.0.tgz", "integrity": "sha512-HRDzbaKjC+AOWVXxAU/x54COGeIv9eb+6CkDSQoNTt4XyWoIJvuPsXizxu/Fr23EiekbtZwmh1IcIG/l/a10GQ==", + "dev": true, "dependencies": { "find-up": "^4.0.0" }, @@ -16926,6 +16908,7 @@ "version": "4.1.0", "resolved": "https://registry.npmjs.org/find-up/-/find-up-4.1.0.tgz", "integrity": "sha512-PpOwAdQ/YlXQ2vj8a3h8IipDuYRi3wceVQQGYWxNINccq40Anw7BlsEXCMbt1Zt+OLA6Fq9suIpIWD0OsnISlw==", + "dev": true, "dependencies": { "locate-path": "^5.0.0", "path-exists": "^4.0.0" @@ -16938,6 +16921,7 @@ "version": "5.0.0", "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-5.0.0.tgz", "integrity": "sha512-t7hw9pI+WvuwNJXwk5zVHpyhIqzg2qTlklJOf0mVxGSbe3Fp2VieZcduNYjaLDoy6p9uGpQEGWG87WpMKlNq8g==", + "dev": true, "dependencies": { "p-locate": "^4.1.0" }, @@ -16949,6 +16933,7 @@ "version": "2.3.0", "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-2.3.0.tgz", "integrity": "sha512-//88mFWSJx8lxCzwdAABTJL2MyWB12+eIY7MDL2SqLmAkeKU9qxRvWuSyTjm3FUmpBEMuFfckAIqEaVGUDxb6w==", + "dev": true, "dependencies": { "p-try": "^2.0.0" }, @@ -16963,6 +16948,7 @@ "version": "4.1.0", "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-4.1.0.tgz", "integrity": "sha512-R79ZZ/0wAxKGu3oYMlz8jy/kbhsNrS7SKZ7PxEHBgJ5+F2mtFW2fK2cOtBh1cHYkQsbzFV7I+EoRKe6Yt0oK7A==", + "dev": true, "dependencies": { "p-limit": "^2.2.0" }, @@ -21182,7 +21168,6 @@ "execa": "^5.1.1", "fetch-har": "^10.0.0", "figures": "^3.2.0", - "find-cache-dir": "^3.3.1", "get-stream": "^6.0.1", "js-yaml": "^4.1.0", "json-schema-to-ts": "^2.9.2", @@ -21208,7 +21193,6 @@ "devDependencies": { "@readme/oas-examples": "^5.12.0", "@types/caseless": "^0.12.3", - "@types/find-cache-dir": "^3.2.1", "@types/js-yaml": "^4.0.5", "@types/lodash.camelcase": "^4.3.7", "@types/lodash.deburr": "^4.1.7", @@ -21233,12 +21217,6 @@ "node": ">=18" } }, - "packages/api/node_modules/@types/find-cache-dir": { - "version": "3.2.1", - "resolved": "https://registry.npmjs.org/@types/find-cache-dir/-/find-cache-dir-3.2.1.tgz", - "integrity": "sha512-frsJrz2t/CeGifcu/6uRo4b+SzAwT4NYCVPu1GN8IB9XTzrpPkGuV0tmh9mN+/L0PklAlsC3u5Fxt0ju00LXIw==", - "dev": true - }, "packages/api/node_modules/@types/prettier": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/@types/prettier/-/prettier-3.0.0.tgz", diff --git a/packages/api/example.js b/packages/api/example.js deleted file mode 100644 index ad2928e4..00000000 --- a/packages/api/example.js +++ /dev/null @@ -1,13 +0,0 @@ -// const path = require('path'); -const sdk = require('./src')('https://raw.githubusercontent.com/readmeio/oas-examples/main/3.0/yaml/readme.yaml', { - // Uncomment this line to load the SDK with a custom caching directory. - // cacheDir: path.join(process.cwd(), '.api'), -}); - -sdk - .getOpenRoles() - .then(res => { - console.log(`there are ${res.length} open roles`); - console.log(res[0]); - }) - .catch(console.error); diff --git a/packages/api/package.json b/packages/api/package.json index 65840b7c..ae383a7a 100644 --- a/packages/api/package.json +++ b/packages/api/package.json @@ -2,8 +2,6 @@ "name": "api", "version": "6.1.1", "description": "Magical SDK generation from an OpenAPI definition 🪄", - "main": "./dist/index.js", - "types": "./dist/index.d.ts", "bin": { "api": "./bin/api" }, @@ -47,7 +45,6 @@ "execa": "^5.1.1", "fetch-har": "^10.0.0", "figures": "^3.2.0", - "find-cache-dir": "^3.3.1", "get-stream": "^6.0.1", "js-yaml": "^4.1.0", "json-schema-to-ts": "^2.9.2", @@ -70,7 +67,6 @@ "devDependencies": { "@readme/oas-examples": "^5.12.0", "@types/caseless": "^0.12.3", - "@types/find-cache-dir": "^3.2.1", "@types/js-yaml": "^4.0.5", "@types/lodash.camelcase": "^4.3.7", "@types/lodash.deburr": "^4.1.7", diff --git a/packages/api/src/index.ts b/packages/api/src/index.ts deleted file mode 100644 index c33fff6f..00000000 --- a/packages/api/src/index.ts +++ /dev/null @@ -1,203 +0,0 @@ -import type { ConfigOptions } from './core'; -import type { Operation } from 'oas'; -import type { OASDocument } from 'oas/dist/rmoas.types'; - -import Oas from 'oas'; - -import Cache from './cache'; -import APICore from './core'; -import { PACKAGE_NAME, PACKAGE_VERSION } from './packageInfo'; - -interface SDKOptions { - cacheDir?: string; -} - -class Sdk { - uri: string | OASDocument; - - userAgent: string; - - cacheDir: string | false; - - constructor(uri: string | OASDocument, opts: SDKOptions = {}) { - this.uri = uri; - this.userAgent = `${PACKAGE_NAME} (node)/${PACKAGE_VERSION}`; - - this.cacheDir = opts.cacheDir ? opts.cacheDir : false; - } - - load() { - const cache = new Cache(this.uri, this.cacheDir); - const userAgent = this.userAgent; - - const core = new APICore(); - core.setUserAgent(userAgent); - - let isLoaded = false; - let isCached = cache.isCached(); - let sdk = {}; - - /** - * Create dynamic accessors for every operation with a defined operation ID. If an operation - * does not have an operation ID it can be accessed by its `.method('/path')` accessor instead. - * - */ - function loadOperations(spec: Oas) { - return Object.entries(spec.getPaths()) - .map(([, operations]) => Object.values(operations)) - .reduce((prev, next) => prev.concat(next), []) - .reduce((prev, next) => { - // `getOperationId()` creates dynamic operation IDs when one isn't available but we need - // to know here if we actually have one present or not. The `camelCase` option here also - // cleans up any `operationId` that we might have into something that can be used as a - // valid JS method. - const originalOperationId = next.getOperationId(); - const operationId = next.getOperationId({ camelCase: true }); - - const op = { - [operationId]: ((operation: Operation, ...args: unknown[]) => { - return core.fetchOperation(operation, ...args); - }).bind(null, next), - }; - - if (operationId !== originalOperationId) { - // If we cleaned up their operation ID into a friendly method accessor (`findPetById` - // versus `find pet by id`) we should still let them use the non-friendly version if - // they want. - // - // This work is to maintain backwards compatibility with `api@4` and does not exist - // within our code generated SDKs -- those only allow the cleaner camelCase - // `operationId` to be used. - op[originalOperationId] = ((operation: Operation, ...args: unknown[]) => { - return core.fetchOperation(operation, ...args); - }).bind(null, next); - } - - return Object.assign(prev, op); - }, {}); - } - - async function loadFromCache() { - let cachedSpec; - if (isCached) { - cachedSpec = await cache.get(); - } else { - cachedSpec = await cache.load(); - isCached = true; - } - - const spec = new Oas(cachedSpec); - - core.setSpec(spec); - - sdk = Object.assign(sdk, loadOperations(spec)); - isLoaded = true; - } - - const sdkProxy = { - // @give this a better type than any - get(target: any, method: string) { - // Since auth returns a self-proxy, we **do not** want it to fall through into the async - // function below as when that'll happen, instead of returning a self-proxy, it'll end up - // returning a Promise. When that happens, chaining `sdk.auth().operationId()` will fail. - if (['auth', 'config'].includes(method)) { - // @todo split this up so we have better types for `auth` and `config` - return function authAndConfigHandler(...args: any) { - return target[method].apply(this, args); - }; - } - - return async function accessorHandler(...args: unknown[]) { - if (!(method in target)) { - // If this method doesn't exist on the proxy, have we loaded the SDK? If we have, then - // this method isn't valid. - if (isLoaded) { - throw new Error(`Sorry, \`${method}\` does not appear to be a valid operation on this API.`); - } - - await loadFromCache(); - - // If after loading the SDK and this method still doesn't exist, then it's not real! - if (!(method in sdk)) { - throw new Error(`Sorry, \`${method}\` does not appear to be a valid operation on this API.`); - } - - // @todo give sdk a better type - return (sdk as any)[method].apply(this, args); - } - - return target[method].apply(this, args); - }; - }, - }; - - sdk = { - /** - * If the API you're using requires authentication you can supply the required credentials - * through this method and the library will magically determine how they should be used - * within your API request. - * - * With the exception of OpenID and MutualTLS, it supports all forms of authentication - * supported by the OpenAPI specification. - * - * @example HTTP Basic auth - * sdk.auth('username', 'password'); - * - * @example Bearer tokens (HTTP or OAuth 2) - * sdk.auth('myBearerToken'); - * - * @example API Keys - * sdk.auth('myApiKey'); - * - * @see {@link https://spec.openapis.org/oas/v3.0.3#fixed-fields-22} - * @see {@link https://spec.openapis.org/oas/v3.1.0#fixed-fields-22} - * @param values Your auth credentials for the API. Can specify up to two strings or numbers. - */ - auth: (...values: string[] | number[]) => { - core.setAuth(...values); - }, - - /** - * Optionally configure various options that the SDK allows. - * - * @param config Object of supported SDK options and toggles. - * @param config.timeout Override the default `fetch` request timeout of 30 seconds (30000ms). - */ - config: (config: ConfigOptions) => { - core.setConfig(config); - }, - - /** - * If the API you're using offers alternate server URLs, and server variables, you can tell - * the SDK which one to use with this method. To use it you can supply either one of the - * server URLs that are contained within the OpenAPI definition (along with any server - * variables), or you can pass it a fully qualified URL to use (that may or may not exist - * within the OpenAPI definition). - * - * @example Server URL with server variables - * sdk.server('https://{region}.api.example.com/{basePath}', { - * name: 'eu', - * basePath: 'v14', - * }); - * - * @example Fully qualified server URL - * sdk.server('https://eu.api.example.com/v14'); - * - * @param url Server URL - * @param variables An object of variables to replace into the server URL. - */ - server: (url: string, variables = {}) => { - core.setServer(url, variables); - }, - }; - - return new Proxy(sdk, sdkProxy); - } -} - -// Why `export` vs `export default`? If we leave this as `export` then TS will transpile it into -// a `module.exports` export so that when folks load this they don't need to load it as -// `require('api').default`. -export = (uri: string | OASDocument, opts: SDKOptions = {}) => { - return new Sdk(uri, opts).load(); -}; diff --git a/packages/api/test/auth.test.ts b/packages/api/test/auth.test.ts deleted file mode 100644 index 50b68ffd..00000000 --- a/packages/api/test/auth.test.ts +++ /dev/null @@ -1,225 +0,0 @@ -import type { OASDocument } from 'oas/dist/rmoas.types'; - -import fetchMock from 'fetch-mock'; -import uniqueTempDir from 'unique-temp-dir'; -import { describe, beforeAll, beforeEach, afterEach, it, expect, vi } from 'vitest'; - -import api from '../src'; -import Cache from '../src/cache'; - -import { responses as mockResponses } from './helpers/fetch-mock'; -import loadSpec from './helpers/load-spec'; - -let sdk; - -const apiKey = '123457890'; -const user = 'buster'; -const pass = 'hunter1'; - -describe('#auth()', () => { - beforeAll(() => { - // Set a unique cache dir so these tests won't collide with other tests and we don't need to go - // through the trouble of mocking out the filesystem. - Cache.setCacheDir(uniqueTempDir()); - }); - - beforeEach(async () => { - const securityOas = await loadSpec('@readme/oas-examples/3.0/json/security.json'); - sdk = api(securityOas as unknown as OASDocument); - }); - - afterEach(() => { - fetchMock.restore(); - }); - - describe('API keys', () => { - describe('in: query', () => { - it('should allow you to supply auth', async () => { - fetchMock.get( - { - url: 'https://httpbin.org/anything/apiKey', - query: { apiKey }, - }, - mockResponses.searchParams, - ); - - sdk.auth(apiKey); - - await sdk.getAnythingApikey().then(({ data }) => { - expect(data).toBe('/anything/apiKey?apiKey=123457890'); - }); - }); - - it('should throw if you supply multiple auth keys', async () => { - sdk.auth(apiKey, apiKey); - - await expect(sdk.getAnythingApikey()).rejects.toThrow(/only a single token is needed/i); - }); - }); - - describe('in: header', () => { - it('should allow you to supply auth', async () => { - fetchMock.put('https://httpbin.org/anything/apiKey', mockResponses.headers); - - sdk.auth(apiKey); - await sdk.putAnythingApikey().then(({ data }) => { - expect(data).toHaveProperty('x-api-key', '123457890'); - }); - }); - - it('should throw if you supply multiple auth keys', async () => { - sdk.auth(apiKey, apiKey); - - await expect(sdk.putAnythingApikey()).rejects.toThrow(/only a single token is needed/i); - }); - }); - }); - - describe('HTTP', () => { - describe('scheme: basic', () => { - it('should allow you to supply auth', async () => { - const authHeader = `Basic ${Buffer.from(`${user}:${pass}`).toString('base64')}`; - - fetchMock.post('https://httpbin.org/anything/basic', mockResponses.headers); - - sdk.auth(user, pass); - - await sdk.postAnythingBasic().then(({ data }) => { - expect(data).toHaveProperty('authorization', authHeader); - }); - }); - - it('should allow you to not pass in a password', async () => { - fetchMock.post('https://httpbin.org/anything/basic', mockResponses.headers); - - sdk.auth(user); - - await sdk.postAnythingBasic().then(({ data }) => { - expect(data).toHaveProperty('authorization', `Basic ${Buffer.from(`${user}:`).toString('base64')}`); - }); - }); - }); - - describe('scheme: bearer', () => { - it('should allow you to supply auth', async () => { - fetchMock.post('https://httpbin.org/anything/bearer', mockResponses.headers); - - sdk.auth(apiKey); - - await sdk.postAnythingBearer().then(({ data }) => { - expect(data).toHaveProperty('authorization', `Bearer ${apiKey}`); - }); - }); - - it('should throw if you pass in multiple bearer tokens', async () => { - sdk.auth(apiKey, apiKey); - await expect(sdk.postAnythingBearer()).rejects.toThrow(/only a single token is needed/i); - }); - }); - }); - - describe('OAuth 2', () => { - it('should allow you to supply auth', async () => { - fetchMock.post('https://httpbin.org/anything/oauth2', mockResponses.headers); - - sdk.auth(apiKey); - - await sdk.postAnythingOauth2().then(({ data }) => { - expect(data).toHaveProperty('authorization', `Bearer ${apiKey}`); - }); - }); - - it('should throw if you pass in multiple bearer tokens', async () => { - sdk.auth(apiKey, apiKey); - await expect(sdk.postAnythingOauth2()).rejects.toThrow(/only a single token is needed/i); - }); - }); - - it('should allow multiple calls to share an API key', async () => { - const endpointCall = vi.fn(); - fetchMock.get( - { - url: 'https://httpbin.org/anything/apiKey', - query: { apiKey }, - }, - () => { - endpointCall(); - return {}; - }, - ); - - sdk.auth(apiKey); - - await sdk.getAnythingApikey().then(() => expect(endpointCall).toHaveBeenCalledTimes(1)); - await sdk.getAnythingApikey().then(() => expect(endpointCall).toHaveBeenCalledTimes(2)); - }); - - it('should allow auth to be called again to change the key', async () => { - const apiKey1 = '12345'; - const apiKey2 = '67890'; - - fetchMock.get( - { - name: `fetch ${apiKey1}`, - url: 'https://httpbin.org/anything/apiKey', - query: { apiKey: apiKey1 }, - }, - mockResponses.searchParams, - ); - - fetchMock.get( - { - name: `fetch ${apiKey2}`, - url: 'https://httpbin.org/anything/apiKey', - query: { apiKey: apiKey2 }, - }, - mockResponses.searchParams, - ); - - sdk.auth(apiKey1); - await sdk.getAnythingApikey().then(({ data }) => expect(data).toBe('/anything/apiKey?apiKey=12345')); - - sdk.auth(apiKey2); - await sdk.getAnythingApikey().then(({ data }) => expect(data).toBe('/anything/apiKey?apiKey=67890')); - }); - - describe('quirks', () => { - let authQuirksOas; - let quirks; - - beforeAll(async () => { - authQuirksOas = await loadSpec(require.resolve('./__fixtures__/definitions/auth-quirks.json')); - quirks = api(authQuirksOas as unknown as OASDocument); - }); - - // Because the `POST /anything` operation allows either an OAuth2 token or Basic Auth the - // quirks case we're testing is that you should be able to supply either a single OAuth2 token - // or a username+password and it should be able to intelligently handle both. - it('should have an expected security setting definition for this quirks case', () => { - expect(authQuirksOas.paths['/anything'].post.security).toStrictEqual([ - { oauth2: ['write:things'] }, - { basicAuth: [] }, - ]); - }); - - it('should support an operation that has OR auth requirements (supplying Basic Auth)', async () => { - const authHeader = `Basic ${Buffer.from(`${user}:${pass}`).toString('base64')}`; - - fetchMock.post('https://httpbin.org/anything', mockResponses.headers); - - quirks.auth(user, pass); - await quirks.postAnything().then(({ data }) => { - expect(data).toHaveProperty('authorization', authHeader); - }); - }); - - it('should support an operation that has OR auth requirements (supplying an OAuth2 token)', async () => { - fetchMock.post('https://httpbin.org/anything', mockResponses.headers); - - quirks.auth(apiKey); - await quirks.postAnything().then(({ data }) => { - expect(data).toHaveProperty('authorization', `Bearer ${apiKey}`); - }); - }); - }); -}); diff --git a/packages/api/test/cache-custom.test.ts b/packages/api/test/cache-custom.test.ts deleted file mode 100644 index f4d8baa0..00000000 --- a/packages/api/test/cache-custom.test.ts +++ /dev/null @@ -1,59 +0,0 @@ -import fs from 'fs/promises'; -import path from 'path'; - -import fetchMock from 'fetch-mock'; -import { describe, beforeEach, afterEach, it, expect } from 'vitest'; - -import api from '../src'; -import Cache from '../src/cache'; - -import loadSpec from './helpers/load-spec'; - -describe('cache (custom directory)', () => { - let cacheDir; - let originalCacheDir; - - beforeEach(async () => { - originalCacheDir = Cache.dir; - - cacheDir = path.join(__dirname, '..', '.api-test'); - - await fs.mkdir(cacheDir, { recursive: true }); - }); - - afterEach(async () => { - await fs.rm(cacheDir, { recursive: true }); - - Cache.setCacheDir(originalCacheDir); - - fetchMock.restore(); - }); - - it('should support supplying a custom cache directory', async () => { - const uspto = await loadSpec('@readme/oas-examples/3.0/json/uspto.json'); - - // Our custom caching directory should be empty. - await expect(fs.readdir(cacheDir)).resolves.toHaveLength(0); - - fetchMock.get('https://example.com/openapi.json', uspto); - fetchMock.get('https://developer.uspto.gov/ds-api/', { status: 200 }); - - const sdk = api('https://example.com/openapi.json', { cacheDir }); - - await sdk.listDataSets(); - - // Our custom caching directory should have our cached spec in it. - const files = [...(await fs.readdir(cacheDir)), ...(await fs.readdir(path.join(cacheDir, 'specs')))]; - expect(files).toStrictEqual(['cache.json', 'specs', 'f2068ebf6ce28b51a8467bf7ac53bbae.json']); - - const cache = await fs.readFile(path.join(cacheDir, 'cache.json'), 'utf8').then(JSON.parse); - expect(cache).toStrictEqual({ - bcace67532514a49e663e471602b7be6: { - hash: 'f2068ebf6ce28b51a8467bf7ac53bbae', - original: 'https://example.com/openapi.json', - title: 'USPTO Data Set API', - version: '1.0.0', - }, - }); - }); -}); diff --git a/packages/api/test/cache-tmp.test.ts b/packages/api/test/cache-tmp.test.ts deleted file mode 100644 index 523b0c34..00000000 --- a/packages/api/test/cache-tmp.test.ts +++ /dev/null @@ -1,26 +0,0 @@ -import os from 'os'; - -import { describe, it, expect, vi } from 'vitest'; - -import Cache from '../src/cache'; - -vi.mock('find-cache-dir', () => ({ - default: () => undefined, -})); - -describe('cache (temp dir handling)', () => { - // Since this test is mocking out the `find-cache-dir` module for a single test, it needs to be - // run separately from the rest of the cache tests, otherwise all of those tests would use this - // mocked out version. - it('should fallback to an os-level temp directory if a cache directory cannot be determined', async () => { - await Cache.reset(); - - const dir = os.tmpdir(); - // eslint-disable-next-line no-new - new Cache('http://example.com/readme.json'); - - expect(Cache.dir).toMatch(new RegExp(dir)); - expect(Cache.cacheStore).toMatch(new RegExp(dir)); - expect(Cache.specsCache).toMatch(new RegExp(dir)); - }); -}); diff --git a/packages/api/test/cache.test.ts b/packages/api/test/cache.test.ts deleted file mode 100644 index 7bda63d7..00000000 --- a/packages/api/test/cache.test.ts +++ /dev/null @@ -1,273 +0,0 @@ -import type { OASDocument } from 'oas/dist/rmoas.types'; - -import fs from 'fs/promises'; -import path from 'path'; - -import fetchMock from 'fetch-mock'; -import uniqueTempDir from 'unique-temp-dir'; -import { describe, beforeAll, it, expect } from 'vitest'; - -import Cache from '../src/cache'; - -import loadSpec from './helpers/load-spec'; - -let petstoreSpec; -let readmeSpec; - -describe('cache', () => { - beforeAll(async () => { - petstoreSpec = await loadSpec('@readme/oas-examples/3.0/json/petstore.json'); - readmeSpec = await loadSpec('@readme/oas-examples/3.0/json/readme.json'); - - // Set a unique cache dir so these tests won't collide with other tests and we don't need to go - // through the trouble of mocking out the filesystem. - Cache.setCacheDir(uniqueTempDir()); - }); - - describe('#load', () => { - describe('raw object', () => { - it('should return a raw object if a JSON object was initially supplied', async () => { - const res = await new Cache(readmeSpec as unknown as OASDocument).load(); - expect(res).toStrictEqual(readmeSpec); - }); - - it('should return a raw object if supplied, but still validate and dereference it', async () => { - const res = await new Cache(petstoreSpec as unknown as OASDocument).load(); - expect(Object.keys((res.paths['/pet'].post as any).requestBody.content)).toStrictEqual([ - 'application/json', - 'application/xml', - ]); - }); - }); - - describe('ReadMe registry UUID', () => { - it('should resolve the shorthand `@petstore/v1.0#uuid` syntax to the ReadMe API', () => { - expect(new Cache('@petstore/v1.0#n6kvf10vakpemvplx').uri).toBe( - 'https://dash.readme.com/api/v1/api-registry/n6kvf10vakpemvplx', - ); - }); - - it('should resolve the shorthand `@petstore#uuid` syntax to the ReadMe API', () => { - expect(new Cache('@petstore#n6kvf10vakpemvplx').uri).toBe( - 'https://dash.readme.com/api/v1/api-registry/n6kvf10vakpemvplx', - ); - }); - - it("shouldn't try to resolve improperly formatted shorthand accessors to the ReadMe API", () => { - expect(new Cache('n6kvf10vakpemvplx').uri).toBe('n6kvf10vakpemvplx'); - }); - - it('should be able to load a definition', async () => { - fetchMock.get('https://dash.readme.com/api/v1/api-registry/n6kvf10vakpemvplxn', readmeSpec); - - const cacheStore = new Cache('@readme/v1.0#n6kvf10vakpemvplxn'); - - expect(cacheStore.isCached()).toBe(false); - - await expect(cacheStore.load()).resolves.toHaveProperty('info', { - description: 'Create beautiful product and API documentation with our developer friendly platform.', - version: '4.355.0', - title: 'ReadMe API 🦉', - contact: { - name: 'API Support', - url: 'https://docs.readme.com/main/docs/need-more-support', - email: 'support@readme.io', - }, - }); - - expect(cacheStore.get().paths['/api-specification'].get.parameters).toBeDereferenced(); - expect(cacheStore.isCached()).toBe(true); - fetchMock.restore(); - }); - }); - - describe('URL', () => { - it('should be able to load a definition', async () => { - fetchMock.get('http://example.com/readme.json', readmeSpec); - const cacheStore = new Cache('http://example.com/readme.json'); - - expect(cacheStore.isCached()).toBe(false); - - await expect(cacheStore.load()).resolves.toHaveProperty('info', { - description: 'Create beautiful product and API documentation with our developer friendly platform.', - version: '4.355.0', - title: 'ReadMe API 🦉', - contact: { - name: 'API Support', - url: 'https://docs.readme.com/main/docs/need-more-support', - email: 'support@readme.io', - }, - }); - - expect(cacheStore.get().paths['/api-specification'].get.parameters).toBeDereferenced(); - expect(cacheStore.isCached()).toBe(true); - fetchMock.restore(); - }); - - it('should error if the url cannot be reached', async () => { - fetchMock.get('https://example.com/unknown.json', { status: 404 }); - - await expect(new Cache('https://example.com/unknown.json').load()).rejects.toThrow( - 'Unable to retrieve URL (https://example.com/unknown.json). Reason: Not Found', - ); - - fetchMock.restore(); - }); - - it('should convert yaml to json', async () => { - const spec = await fs.readFile(require.resolve('@readme/oas-examples/3.0/yaml/readme.yaml'), 'utf8'); - fetchMock.get('https://example.com/readme.yaml', spec); - - const definition = 'https://example.com/readme.yaml'; - const cacheStore = new Cache(definition); - const hash = Cache.getCacheHash(definition); - - expect(cacheStore.isCached()).toBe(false); - - await cacheStore.load(); - expect(cacheStore.get().paths['/api-specification'].get.parameters).toBeDereferenced(); - expect(cacheStore.isCached()).toBe(true); - fetchMock.restore(); - - const cached = cacheStore.getCache(); - expect(cached).toHaveProperty(hash); - }); - }); - - describe('file', () => { - it('should be able to load a definition', async () => { - const cacheStore = new Cache(require.resolve('@readme/oas-examples/3.0/json/readme.json')); - - expect(cacheStore.isCached()).toBe(false); - - await cacheStore.load(); - expect(cacheStore.get().paths['/api-specification'].get.parameters).toBeDereferenced(); - expect(cacheStore.isCached()).toBe(true); - }); - - it('should be able to handle a relative path', async () => { - const cacheStore = new Cache('../api/test/__fixtures__/oas.json'); - - expect(cacheStore.isCached()).toBe(false); - - await cacheStore.load(); - expect(cacheStore.isCached()).toBe(true); - }); - - it('should convert yaml to json', async () => { - const file = require.resolve('@readme/oas-examples/3.0/yaml/readme.yaml'); - const cacheStore = new Cache(file); - const hash = Cache.getCacheHash(file); - - expect(cacheStore.isCached()).toBe(false); - - await cacheStore.load(); - expect(cacheStore.get().paths['/api-specification'].get.parameters).toBeDereferenced(); - expect(cacheStore.isCached()).toBe(true); - - const cached = cacheStore.getCache(); - expect(cached).toHaveProperty(hash); - }); - }); - }); - - describe('#save()', () => { - it('should error if definition is a swagger file', async () => { - await expect(new Cache(require.resolve('@readme/oas-examples/2.0/json/petstore.json')).load()).rejects.toThrow( - 'Sorry, this module only supports OpenAPI definitions.', - ); - }); - - it('should error if definition is not a valid openapi file', async () => { - await expect(new Cache(require.resolve('../package.json')).load()).rejects.toThrow( - "Sorry, that doesn't look like a valid OpenAPI definition.", - ); - }); - - it('should cache a new file', async () => { - await Cache.reset(); - - const file = require.resolve('@readme/oas-examples/3.0/json/readme.json'); - const cacheStore = new Cache(file); - - expect(cacheStore.isCached()).toBe(false); - - await cacheStore.load(); - - expect(cacheStore.isCached()).toBe(true); - }); - - it('should be able to cache a definition that contains a circular reference', async () => { - const file = require.resolve('@readme/oas-examples/3.0/json/circular'); - const cacheStore = new Cache(file); - - expect(cacheStore.isCached()).toBe(false); - - await cacheStore.load(); - - expect(cacheStore.isCached()).toBe(true); - }); - - it('should be able to load and cache an OpenAPI 3.1 definition', async () => { - const file = require.resolve('@readme/oas-examples/3.1/json/petstore.json'); - const cacheStore = new Cache(file); - - expect(cacheStore.isCached()).toBe(false); - - await cacheStore.load(); - - expect(cacheStore.isCached()).toBe(true); - }); - }); - - describe('#get', () => { - it('should return an object if the current uri is an object (used for unit testing)', () => { - const loaded = new Cache(readmeSpec as unknown as OASDocument).get(); - - expect(loaded).toStrictEqual(readmeSpec); - }); - - it('should load a file out of cache', async () => { - const file = require.resolve('@readme/oas-examples/3.0/json/readme.json'); - const cacheStore = new Cache(file); - await cacheStore.load(); - - const loaded = cacheStore.get(); - expect(loaded).toHaveProperty('components'); - expect(loaded).toHaveProperty('info'); - expect(loaded).toHaveProperty('paths'); - expect(loaded).toHaveProperty('servers'); - }); - - it('should support the legacy `path` property in the cache store', async () => { - await Cache.reset(); - - const file = require.resolve('@readme/oas-examples/3.0/yaml/readme.yaml'); - const cacheStore = new Cache(file); - await cacheStore.load(); - - const cache = cacheStore.getCache(); - const cacheKey = Object.keys(cache)[0]; - cache[cacheKey].path = path.join(Cache.specsCache, `${cache[cacheKey].hash}.json`); - - delete cache[cacheKey].hash; - - expect(Object.keys(cache)).toHaveLength(1); - - const loaded = cacheStore.get(); - expect(loaded).toHaveProperty('components'); - expect(loaded).toHaveProperty('info'); - expect(loaded).toHaveProperty('paths'); - expect(loaded).toHaveProperty('servers'); - }); - - it('should error if the file is not cached', () => { - const file = require.resolve('@readme/oas-examples/3.0/json/security.json'); - const cacheStore = new Cache(file); - - expect(() => { - return cacheStore.get(); - }).toThrow(`${file} has not been cached yet and must do so before being retrieved.`); - }); - }); -}); diff --git a/packages/api/test/config.test.ts b/packages/api/test/config.test.ts deleted file mode 100644 index b333859a..00000000 --- a/packages/api/test/config.test.ts +++ /dev/null @@ -1,53 +0,0 @@ -import type { OASDocument } from 'oas/dist/rmoas.types'; - -import fetchMock from 'fetch-mock'; -import uniqueTempDir from 'unique-temp-dir'; -import { describe, beforeAll, beforeEach, afterEach, it, expect } from 'vitest'; - -import api from '../src'; -import Cache from '../src/cache'; - -import { response, responses as mockResponses } from './helpers/fetch-mock'; -import loadSpec from './helpers/load-spec'; - -let petstore; -let sdk; -const petId = 123; - -describe('#config()', () => { - beforeAll(async () => { - petstore = await loadSpec('@readme/oas-examples/3.0/json/petstore.json'); - - // Set a unique cache dir so these tests won't collide with other tests and we don't need to go - // through the trouble of mocking out the filesystem. - Cache.setCacheDir(uniqueTempDir()); - }); - - describe('timeout', () => { - beforeEach(() => { - sdk = api(petstore as unknown as OASDocument); - }); - - afterEach(() => { - fetchMock.restore(); - }); - - it('should override the default `fetch` timeout if present and fail if request takes too long', async () => { - fetchMock.delete(`http://petstore.swagger.io/v2/pet/${petId}`, mockResponses.delay(response, 500)); - - sdk.config({ timeout: 100 }); - - await expect(sdk.deletePet({ petId })).rejects.toThrow('The operation was aborted.'); - }); - - it('should override the default `fetch` timeout and return if request is quick', async () => { - fetchMock.delete(`http://petstore.swagger.io/v2/pet/${petId}`, mockResponses.delay(response, 100)); - - sdk.config({ timeout: 500 }); - - await sdk.deletePet({ petId }).then(({ data }) => { - expect(data).toStrictEqual(response); - }); - }); - }); -}); diff --git a/packages/api/test/core/index.test.ts b/packages/api/test/core/index.test.ts index dc0de896..b889571a 100644 --- a/packages/api/test/core/index.test.ts +++ b/packages/api/test/core/index.test.ts @@ -1,5 +1,6 @@ import assert from 'assert'; +import datauri from 'datauri'; import fetchMock from 'fetch-mock'; import Oas from 'oas'; import { describe, beforeEach, afterEach, it, expect } from 'vitest'; @@ -10,6 +11,8 @@ import { responses as mockResponse } from '../helpers/fetch-mock'; import loadSpec from '../helpers/load-spec'; describe('APICore', () => { + let fileUploads: APICore; + let parametersStyle: APICore; let petstore: APICore; let readme: APICore; let security: APICore; @@ -22,6 +25,14 @@ describe('APICore', () => { }; beforeEach(async () => { + fileUploads = await loadSpec('@readme/oas-examples/3.0/json/file-uploads.json') + .then(Oas.init) + .then(oas => new APICore(oas)); + + parametersStyle = await loadSpec('@readme/oas-examples/3.1/json/parameters-style.json') + .then(Oas.init) + .then(oas => new APICore(oas)); + petstore = await loadSpec('@readme/oas-examples/3.0/json/petstore-expanded.json') .then(Oas.init) .then(oas => new APICore(oas)); @@ -193,6 +204,107 @@ describe('APICore', () => { }); }); }); + + describe('application/x-www-form-urlencoded', () => { + it('should support `application/x-www-form-urlencoded` requests', async () => { + const usptoSpec = await loadSpec('@readme/oas-examples/3.0/json/uspto.json') + .then(spec => { + // eslint-disable-next-line no-param-reassign + spec.servers[0].url = '{scheme}://httpbin.org/anything'; + return spec; + }) + .then(Oas.init) + .then(oas => new APICore(oas)); + + fetchMock.post('https://httpbin.org/anything/v1/oa_citations/records', mockResponse.all); + + const body = { + criteria: 'propertyName:value', + }; + + const metadata = { + dataset: 'v1', + version: 'oa_citations', + }; + + const { data } = await usptoSpec.fetch('/{dataset}/{version}/records', 'post', body, metadata); + expect(data.uri).toBe('/anything/v1/oa_citations/records'); + expect(data.requestBody).toBe('criteria=propertyName%3Avalue'); + expect(data.headers).toHaveProperty('content-type', 'application/x-www-form-urlencoded'); + }); + }); + + describe('multipart/form-data', () => { + it('should support `image/png` requests', async () => { + fetchMock.post('https://httpbin.org/anything/image-png', mockResponse.datauri); + + const file = `${__dirname}/../__fixtures__/owlbert.png`; + + const { data } = await fileUploads.fetch('/anything/image-png', 'post', file); + + expect(data.uri).toBe('/anything/image-png'); + expect(data.requestBody).toBe(await datauri(file)); + expect(data.headers).toHaveProperty('content-type', 'image/png'); + }); + + it('should support `multipart/form-data` requests', async () => { + fetchMock.post('https://httpbin.org/anything/form-data/form', mockResponse.multipart); + + const body = { + primitive: 'string', + array: ['string'], + object: { + foo: 'foo-string', + bar: 'bar-string', + }, + }; + + const { data } = await parametersStyle.fetch('/anything/form-data/form', 'post', body); + expect(data.uri).toBe('/anything/form-data/form'); + expect(data.requestBody.split(`${data.boundary}`).filter(Boolean)).toStrictEqual([ + '\r\nContent-Disposition: form-data; name="primitive"\r\n\r\nstring\r\n', + '\r\nContent-Disposition: form-data; name="array"\r\n\r\nstring\r\n', + '\r\nContent-Disposition: form-data; name="object"\r\n\r\nfoo,foo-string,bar,bar-string\r\n', + '--', + ]); + }); + + describe('files', () => { + it('should support plaintext files', async () => { + fetchMock.post('https://httpbin.org/anything/multipart-formdata', mockResponse.multipart); + + const body = { + orderId: 1234, + userId: 5678, + documentFile: `${__dirname}/../__fixtures__/hello.txt`, + }; + + const { data } = await fileUploads.fetch('/anything/multipart-formdata', 'post', body); + expect(data.uri).toBe('/anything/multipart-formdata'); + expect(data.requestBody.split(`${data.boundary}`).filter(Boolean)).toStrictEqual([ + '\r\nContent-Disposition: form-data; name="orderId"\r\n\r\n1234\r\n', + '\r\nContent-Disposition: form-data; name="userId"\r\n\r\n5678\r\n', + '\r\nContent-Disposition: form-data; name="documentFile"; filename="hello.txt"\r\nContent-Type: text/plain\r\n\r\nHello world!\n\r\n', + '--', + ]); + }); + + it('should support plaintext files containing unicode characters', async () => { + fetchMock.post('https://httpbin.org/anything/multipart-formdata', mockResponse.multipart); + + const body = { + documentFile: `${__dirname}/../__fixtures__/hello.jp.txt`, + }; + + const { data } = await fileUploads.fetch('/anything/multipart-formdata', 'post', body); + expect(data.uri).toBe('/anything/multipart-formdata'); + expect(data.requestBody.split(`${data.boundary}`).filter(Boolean)).toStrictEqual([ + '\r\nContent-Disposition: form-data; name="documentFile"; filename="hello.jp.txt"\r\nContent-Type: text/plain\r\n\r\n速い茶色のキツネは怠惰な犬を飛び越えます\n\r\n', + '--', + ]); + }); + }); + }); }); }); @@ -252,4 +364,38 @@ describe('APICore', () => { it.todo("should be able to supply a url that doesn't match any defined server"); }); + + describe('#setConfig()', () => { + describe('timeout', () => { + let petstoreTimeout: APICore; + + beforeEach(async () => { + petstoreTimeout = await loadSpec('@readme/oas-examples/3.0/json/petstore.json') + .then(Oas.init) + .then(oas => new APICore(oas)); + }); + + afterEach(() => { + fetchMock.restore(); + }); + + it('should override the default `fetch` timeout if present and fail if request takes too long', async () => { + fetchMock.delete(`http://petstore.swagger.io/v2/pet/${petId}`, mockResponse.delay(response, 500)); + + petstoreTimeout.setConfig({ timeout: 100 }); + + await expect(petstoreTimeout.fetch(`/pet/${petId}`, 'delete')).rejects.toThrow('The operation was aborted.'); + }); + + it('should override the default `fetch` timeout and return if request is quick', async () => { + fetchMock.delete(`http://petstore.swagger.io/v2/pet/${petId}`, mockResponse.delay(response, 100)); + + petstoreTimeout.setConfig({ timeout: 500 }); + + await petstoreTimeout.fetch(`/pet/${petId}`, 'delete').then(({ data }) => { + expect(data).toStrictEqual(response); + }); + }); + }); + }); }); diff --git a/packages/api/test/dist.test.ts b/packages/api/test/dist.test.ts deleted file mode 100644 index b06bbc08..00000000 --- a/packages/api/test/dist.test.ts +++ /dev/null @@ -1,50 +0,0 @@ -import type { OASDocument } from 'oas/dist/rmoas.types'; - -import fetchMock from 'fetch-mock'; -import uniqueTempDir from 'unique-temp-dir'; -import { describe, beforeAll, afterEach, it, expect } from 'vitest'; - -import api from '../dist'; -import Cache from '../src/cache'; - -import { responses as mockResponses } from './helpers/fetch-mock'; -import loadSpec from './helpers/load-spec'; - -describe('typescript dist verification', () => { - beforeAll(() => { - // Set a unique cache dir so these tests won't collide with other tests and we don't need to go - // through the trouble of mocking out the filesystem. - Cache.setCacheDir(uniqueTempDir()); - }); - - afterEach(() => { - fetchMock.restore(); - }); - - it('should be able to use the transpiled dist', async () => { - fetchMock.post('https://developer.uspto.gov/ds-api/oa_citations/v1/records', mockResponses.url('pathname')); - - const uspto = await loadSpec('@readme/oas-examples/3.0/json/uspto.json'); - const sdk = api(uspto as unknown as OASDocument); - - await sdk.performSearch().then(({ data }) => { - expect(data).toBe('/ds-api/oa_citations/v1/records'); - }); - }); - - it('should be able to set an auth token', async () => { - const user = 'buster'; - const pass = 'hunter1'; - - const authHeader = `Basic ${Buffer.from(`${user}:${pass}`).toString('base64')}`; - fetchMock.post('https://httpbin.org/anything/basic', mockResponses.headers); - - const securityOas = await loadSpec('@readme/oas-examples/3.0/json/security.json'); - const sdk = api(securityOas as unknown as OASDocument); - - sdk.auth(user, pass); - await sdk.postAnythingBasic().then(({ data }) => { - expect(data).toHaveProperty('authorization', authHeader); - }); - }); -}); diff --git a/packages/api/test/index.test.ts b/packages/api/test/index.test.ts deleted file mode 100644 index 24856ca9..00000000 --- a/packages/api/test/index.test.ts +++ /dev/null @@ -1,287 +0,0 @@ -import type { OASDocument } from 'oas/dist/rmoas.types'; - -import assert from 'assert'; - -import fetchMock from 'fetch-mock'; -import uniqueTempDir from 'unique-temp-dir'; -import { describe, beforeAll, beforeEach, afterEach, it, expect, expectTypeOf } from 'vitest'; - -import pkg from '../package.json'; -import api from '../src'; -import Cache from '../src/cache'; - -import { responses as mockResponses } from './helpers/fetch-mock'; -import loadSpec from './helpers/load-spec'; - -let petstoreSDK; -let readmeSDK; -let operationIDQuirksSDK; -const petstoreServerURL = 'http://petstore.swagger.io/api'; - -describe('api', () => { - beforeAll(() => { - // Set a unique cache dir so these tests won't collide with other tests and we don't need to go - // through the trouble of mocking out the filesystem. - Cache.setCacheDir(uniqueTempDir()); - }); - - beforeEach(async () => { - const petstore = require.resolve('@readme/oas-examples/3.0/json/petstore-expanded.json'); - await new Cache(petstore).load(); - petstoreSDK = api(petstore); - - const readme = require.resolve('@readme/oas-examples/3.0/json/readme.json'); - await new Cache(readme).load(); - readmeSDK = api(readme); - - const operationIDQuirks = require.resolve('./__fixtures__/definitions/operationid-quirks.json'); - await new Cache(readme).load(); - operationIDQuirksSDK = api(operationIDQuirks); - }); - - afterEach(() => { - fetchMock.restore(); - }); - - describe('#preloading', () => { - let uspto; - - beforeEach(() => { - uspto = require.resolve('@readme/oas-examples/3.0/json/uspto.json'); - }); - - it('should proxy an sdk for the first time', async () => { - fetchMock.get('https://developer.uspto.gov/ds-api/', mockResponses.url('pathname')); - fetchMock.get('https://developer.uspto.gov/ds-api/two', mockResponses.url('pathname')); - - // Asserting that we have not previously loaded this API. - expect(new Cache(uspto).isCached()).toBe(false); - - const sdk = api(uspto); - - // SDK should still not be loaded since we haven't officially called it yet. - expect(new Cache(uspto).isCached()).toBe(false); - expect(Object.keys(sdk)).toStrictEqual(['auth', 'config', 'server']); - - await sdk.listDataSets().then(({ data }) => expect(data).toBe('/ds-api/')); - - // Now that we've called something on the SDK, it should now be fully loaded. - expect(new Cache(uspto).isCached()).toBe(true); - expect(Object.keys(sdk)).toStrictEqual([ - 'auth', - 'config', - 'server', - 'listDataSets', - 'list-data-sets', - 'listSearchableFields', - 'list-searchable-fields', - 'performSearch', - 'perform-search', - ]); - - // Calling the same method again should also work as expected. - await sdk.listDataSets().then(({ data }) => expect(data).toBe('/ds-api/')); - }); - }); - - describe('#accessors', () => { - it('should have a function for each http method', () => { - ['get', 'put', 'post', 'delete', 'options', 'head', 'patch', 'trace'].forEach(method => { - expectTypeOf(petstoreSDK[method]).toBeFunction(); - }); - }); - - describe('#operationId()', () => { - it('should work for operationId', async () => { - fetchMock.get(`${petstoreServerURL}/pets`, mockResponses.real('it worked!')); - - await petstoreSDK.findPets().then(({ data }) => expect(data).toBe('it worked!')); - }); - - it('should work with operationIds that have contain spaces', async () => { - fetchMock.get(`${petstoreServerURL}/pets/1234`, mockResponses.real('it worked!')); - - await petstoreSDK['find pet by id']({ id: 1234 }).then(({ data }) => expect(data).toBe('it worked!')); - - // Because we don't want people using ugly `operationID` accessors like the above we - // transform them into JS-friendly method accessors also. - await petstoreSDK.findPetById({ id: 1234 }).then(({ data }) => expect(data).toBe('it worked!')); - }); - - it('should work with operationIds that contain hyphens', async () => { - fetchMock.get('https://httpbin.org/anything/hyphenated-operation-id', mockResponses.real('it worked!')); - - await operationIDQuirksSDK['hyphenated-operation-id']().then(({ data }) => expect(data).toBe('it worked!')); - await operationIDQuirksSDK.hyphenatedOperationId().then(({ data }) => expect(data).toBe('it worked!')); - }); - - it('should support an operationId that was dynamically cleaned up within `Operation.getOperationId', async () => { - const petstore = await loadSpec('@readme/oas-examples/3.0/json/petstore-expanded.json'); - - // `GET /pets/{id}` in this petstore SDK has an operationID of `find pet by id` but the - // `camelCase` option on `Operation.getOperationId()` should transform it into - // `findPetById`. - expect(petstore.paths['/pets/{id}'].get.operationId).toBe('find pet by id'); - expectTypeOf(petstoreSDK.findPetById).toBeFunction(); - }); - - it('should work for other methods', async () => { - fetchMock.post(`${petstoreServerURL}/pets`, mockResponses.real('it worked!')); - - await petstoreSDK.addPet().then(({ data }) => expect(data).toBe('it worked!')); - }); - - it.todo('should allow operationId to be the same as a http method'); - - it('should error if an operationId does not exist', async () => { - await expect(petstoreSDK.findPetz()).rejects.toThrow(/does not appear to be a valid operation/); - }); - }); - }); - - describe('#fetch', () => { - const petId = 123; - - it('should reject for error-level status codes', async () => { - const response = { - error: 'ENDPOINT_NOTFOUND', - message: `The endpoint you called (GET /pets/${petId}) doesn't exist`, - }; - - fetchMock.delete(`${petstoreServerURL}/pets/${petId}`, { body: response, status: 404 }); - - await petstoreSDK - .deletePet({ id: petId }) - .then(() => assert.fail()) - .catch(({ data, status }) => { - expect(status).toBe(404); - expect(data).toStrictEqual(response); - }); - }); - - it('should contain a custom user agent for the library in requests', async () => { - const userAgent = `${pkg.name} (node)/${pkg.version}`; - fetchMock.delete(`${petstoreServerURL}/pets/${petId}`, mockResponses.headers, { - headers: { - 'User-Agent': userAgent, - }, - }); - - await petstoreSDK.deletePet({ id: petId }).then(({ data }) => { - expect(data).toHaveProperty('user-agent', userAgent); - }); - }); - - describe('operationId', () => { - it('should pass through parameters for operationId', async () => { - const response = { - id: petId, - name: 'Buster', - }; - - fetchMock.delete(`${petstoreServerURL}/pets/${petId}`, response); - - await petstoreSDK.deletePet({ id: petId }).then(({ data }) => expect(data).toStrictEqual(response)); - }); - - it('should pass through body for operationId', async () => { - const body = { name: 'Buster' }; - fetchMock.post(`${petstoreServerURL}/pets`, body, { body }); - - await petstoreSDK.addPet(body).then(({ data }) => expect(data).toStrictEqual(body)); - }); - - it('should pass through parameters and body for operationId', async () => { - const slug = 'new-release'; - const body = { - title: 'revised title', - body: 'updated body', - }; - - fetchMock.put(`https://dash.readme.com/api/v1/changelogs/${slug}`, mockResponses.requestBody, { body }); - - readmeSDK.server('https://dash.readme.com/api/v1'); - await readmeSDK.updateChangelog(body, { slug }).then(({ data }) => { - expect(data).toStrictEqual({ - requestBody: body, - uri: '/api/v1/changelogs/new-release', - }); - }); - }); - }); - - describe('query parameter encoding', () => { - let queryEncoding; - - beforeEach(() => { - queryEncoding = api({ - openapi: '3.1.0', - info: { - version: '1.0.0', - title: '', - }, - servers: [{ url: 'https://httpbin.org/' }], - paths: { - '/anything': { - get: { - operationId: 'getAnything', - parameters: [ - { name: 'stringPound', in: 'query', schema: { type: 'string' } }, - { name: 'stringPound2', in: 'query', schema: { type: 'string' } }, - { name: 'stringHash', in: 'query', schema: { type: 'string' } }, - { name: 'stringArray', in: 'query', schema: { type: 'string' } }, - { name: 'stringWeird', in: 'query', schema: { type: 'string' } }, - { name: 'array', in: 'query', schema: { type: 'array', items: { type: 'string' } } }, - ], - }, - }, - }, - } as unknown as OASDocument); - }); - - it('should encode query parameters', async () => { - const params = { - stringPound: 'something¬hing=true', - stringHash: 'hash#data', - stringArray: 'where[4]=10', - stringWeird: 'properties["$email"] == "testing"', - array: [ - encodeURIComponent('something¬hing=true'), // This is already encoded so it shouldn't be double encoded. - 'nothing&something=false', - 'another item', - ], - }; - - fetchMock.get('glob:https://*.*', mockResponses.searchParams); - - await queryEncoding.getAnything(params).then(({ data }) => { - expect(data).toBe( - '/anything?stringPound=something%26nothing%3Dtrue&stringHash=hash%23data&stringArray=where%5B4%5D%3D10&stringWeird=properties%5B%22%24email%22%5D%20%3D%3D%20%22testing%22&array=something%26nothing%3Dtrue&array=nothing%26something%3Dfalse&array=another%20item', - ); - }); - }); - - it("should not double encode query params if they're already encoded", async () => { - const params = { - stringPound: encodeURIComponent('something¬hing=true'), - stringHash: encodeURIComponent('hash#data'), - stringArray: encodeURIComponent('where[4]=10'), - stringWeird: encodeURIComponent('properties["$email"] == "testing"'), - array: [ - 'something¬hing=true', // Should still encode this one eventhrough the others are already encoded. - encodeURIComponent('nothing&something=false'), - encodeURIComponent('another item'), - ], - }; - - fetchMock.get('glob:https://*.*', mockResponses.searchParams); - - await queryEncoding.getAnything(params).then(({ data }) => { - expect(data).toBe( - '/anything?stringPound=something%26nothing%3Dtrue&stringHash=hash%23data&stringArray=where%5B4%5D%3D10&stringWeird=properties%5B%22%24email%22%5D%20%3D%3D%20%22testing%22&array=something%26nothing%3Dtrue&array=nothing%26something%3Dfalse&array=another%20item', - ); - }); - }); - }); - }); -}); diff --git a/packages/api/test/integration.test.ts b/packages/api/test/integration.test.ts deleted file mode 100644 index 3321d6e0..00000000 --- a/packages/api/test/integration.test.ts +++ /dev/null @@ -1,276 +0,0 @@ -import type { OASDocument } from 'oas/dist/rmoas.types'; - -import datauri from 'datauri'; -import fetchMock from 'fetch-mock'; -import uniqueTempDir from 'unique-temp-dir'; -import { describe, beforeAll, beforeEach, afterEach, it, expect } from 'vitest'; - -import api from '../src'; -import Cache from '../src/cache'; - -import { responses as mockResponse } from './helpers/fetch-mock'; -import loadSpec from './helpers/load-spec'; - -let fileUploads; -let parametersStyle; -let petstore; -let security; - -describe('integration tests', () => { - beforeAll(async () => { - fileUploads = await loadSpec('@readme/oas-examples/3.0/json/file-uploads.json'); - parametersStyle = await loadSpec('@readme/oas-examples/3.1/json/parameters-style.json'); - petstore = await loadSpec('@readme/oas-examples/3.0/json/petstore.json'); - security = await loadSpec('@readme/oas-examples/3.0/json/security.json'); - - // Set a unique cache dir so these tests won't collide with other tests and we don't need to go - // through the trouble of mocking out the filesystem. - Cache.setCacheDir(uniqueTempDir()); - }); - - afterEach(() => { - fetchMock.restore(); - }); - - describe('`application/x-www-form-urlencoded`', () => { - let usptoSpec; - - beforeEach(async () => { - usptoSpec = await loadSpec('@readme/oas-examples/3.0/json/uspto.json').then(spec => { - // eslint-disable-next-line no-param-reassign - spec.servers[0].url = '{scheme}://httpbin.org/anything'; - return spec; - }); - }); - - it('should support `application/x-www-form-urlencoded` requests', async () => { - fetchMock.post('https://httpbin.org/anything/v1/oa_citations/records', mockResponse.all); - - const body = { - criteria: 'propertyName:value', - }; - - const metadata = { - dataset: 'v1', - version: 'oa_citations', - }; - - const { data } = await api(usptoSpec).performSearch(body, metadata); - expect(data.uri).toBe('/anything/v1/oa_citations/records'); - expect(data.requestBody).toBe('criteria=propertyName%3Avalue'); - expect(data.headers).toHaveProperty('content-type', 'application/x-www-form-urlencoded'); - expect(data.headers).toHaveCustomUserAgent(); - }); - - // it.skip('should send along required parameters if not supplied', async () => { - // const metadata = { - // dataset: 'v1', - // version: 'oa_citations', - // }; - - // const { data } = await api(usptoSpec).post('/{dataset}/{version}/records', metadata); - // await expect(data).resolves.toStrictEqual({ - // args: {}, - // data: '', - // files: {}, - // form: { - // criteria: '*/*', // @todo should include this because it's a default - // }, - // headers: expect.objectContaining({ - // 'Content-Type': 'application/x-www-form-urlencoded', - // 'User-Agent': expect.stringMatching(/api \(node\)\/\d+.\d+.\d+/), - // }), - // json: null, - // method: 'POST', - // origin: expect.any(String), - // url: 'https://httpbin.org/anything/v1/oa_citations/records', - // }); - // }); - }); - - it('should support `image/png` requests', async () => { - fetchMock.post('https://httpbin.org/anything/image-png', mockResponse.datauri); - - const file = `${__dirname}/__fixtures__/owlbert.png`; - - const { data } = await api(fileUploads as unknown as OASDocument).postAnythingImagePng(file); - expect(data.uri).toBe('/anything/image-png'); - expect(data.requestBody).toBe(await datauri(file)); - expect(data.headers).toHaveProperty('content-type', 'image/png'); - expect(data.headers).toHaveCustomUserAgent(); - }); - - describe('header handling', () => { - describe('`authorization`', () => { - it('should support supplying an `authorization` header', async () => { - fetchMock.post('http://petstore.swagger.io/v2/pet', mockResponse.all); - - const body = { - id: 1234, - name: 'buster', - }; - - const metadata = { - authorization: 'bearer 12345', - }; - - const sdk = api(petstore as unknown as OASDocument); - - sdk.auth('buster'); - - const { data } = await sdk.addPet(body, metadata); - expect(data.uri).toBe('/v2/pet'); - expect(data.requestBody).toBe('{"id":1234,"name":"buster"}'); - expect(data.headers).toHaveProperty('authorization', 'bearer 12345'); - expect(data.headers).toHaveCustomUserAgent(); - }); - - it('should support supplying an `authorization` header (on an operation that has no params)', async () => { - fetchMock.post('https://httpbin.org/anything/bearer', mockResponse.headers); - - const metadata = { - authorization: 'bearer 12345', - }; - - const securityApi = api(security as unknown as OASDocument); - - securityApi.auth('buster'); - - await securityApi.postAnythingBearer(metadata).then(({ data }) => { - // `authorization: bearer buster` should not be here because we manually supplied - // `authorization: bearer 12345` to the metadata. - expect(data).toHaveProperty('authorization', 'bearer 12345'); - }); - }); - }); - - describe('`accept`', () => { - it('should support supplying an `accept` header', async () => { - fetchMock.post('http://petstore.swagger.io/v2/pet', mockResponse.all); - - const body = { - id: 1234, - name: 'buster', - }; - - const metadata = { - accept: 'text/xml', - }; - - const { data } = await api(petstore as unknown as OASDocument).addPet(body, metadata); - expect(data.uri).toBe('/v2/pet'); - expect(data.requestBody).toBe('{"id":1234,"name":"buster"}'); - expect(data.headers).toHaveProperty('accept', 'text/xml'); - expect(data.headers).toHaveCustomUserAgent(); - }); - - it('should support supplying **only** an `accept` header', async () => { - fetchMock.post('http://petstore.swagger.io/v2/pet', mockResponse.all); - - const metadata = { - accept: 'text/xml', - }; - - const { data } = await api(petstore as unknown as OASDocument).addPet(metadata); - expect(data.uri).toBe('/v2/pet'); - expect(data.requestBody).toBeUndefined(); - expect(data.headers).toHaveProperty('accept', 'text/xml'); - expect(data.headers).toHaveCustomUserAgent(); - }); - - it('should support supplying an `authorization` header (on an operation that has no params)', async () => { - fetchMock.post('https://httpbin.org/anything/bearer', mockResponse.headers); - - const metadata = { - accept: 'application/buster+json', - }; - - const { data } = await api(security as unknown as OASDocument).postAnythingBearer(metadata); - expect(data).toHaveProperty('accept', 'application/buster+json'); - }); - }); - }); - - describe('multipart/form-data', () => { - it('should support `multipart/form-data` requests', async () => { - fetchMock.post('https://httpbin.org/anything/form-data/form', mockResponse.multipart); - - const body = { - primitive: 'string', - array: ['string'], - object: { - foo: 'foo-string', - bar: 'bar-string', - }, - }; - - const { data } = await api(parametersStyle as unknown as OASDocument).formData_form_nonExploded(body); - expect(data.uri).toBe('/anything/form-data/form'); - expect(data.requestBody.split(`${data.boundary}`).filter(Boolean)).toStrictEqual([ - '\r\nContent-Disposition: form-data; name="primitive"\r\n\r\nstring\r\n', - '\r\nContent-Disposition: form-data; name="array"\r\n\r\nstring\r\n', - '\r\nContent-Disposition: form-data; name="object"\r\n\r\nfoo,foo-string,bar,bar-string\r\n', - '--', - ]); - - // expect(data.headers).toHaveProperty('content-type', `multipart/form-data; boundary=${data.boundary}`); - // expect(data.headers).toHaveProperty('content-length', '356'); - expect(data.headers).toHaveCustomUserAgent(); - }); - - describe('files', () => { - it('should support plaintext files', async () => { - fetchMock.post('https://httpbin.org/anything/multipart-formdata', mockResponse.multipart); - - const body = { - orderId: 1234, - userId: 5678, - documentFile: `${__dirname}/__fixtures__/hello.txt`, - }; - - const { data } = await api(fileUploads as unknown as OASDocument).postAnythingMultipartFormdata(body); - expect(data.uri).toBe('/anything/multipart-formdata'); - expect(data.requestBody.split(`${data.boundary}`).filter(Boolean)).toStrictEqual([ - '\r\nContent-Disposition: form-data; name="orderId"\r\n\r\n1234\r\n', - '\r\nContent-Disposition: form-data; name="userId"\r\n\r\n5678\r\n', - '\r\nContent-Disposition: form-data; name="documentFile"; filename="hello.txt"\r\nContent-Type: text/plain\r\n\r\nHello world!\n\r\n', - '--', - ]); - - // expect(data.headers).toHaveProperty('content-type', `multipart/form-data; boundary=${data.boundary}`); - // expect(data.headers).toHaveProperty('content-length', '389'); - expect(data.headers).toHaveCustomUserAgent(); - }); - - it('should support plaintext files containing unicode characters', async () => { - fetchMock.post('https://httpbin.org/anything/multipart-formdata', mockResponse.multipart); - - const body = { - documentFile: `${__dirname}/__fixtures__/hello.jp.txt`, - }; - - const { data } = await api(fileUploads as unknown as OASDocument).postAnythingMultipartFormdata(body); - expect(data.uri).toBe('/anything/multipart-formdata'); - expect(data.requestBody.split(`${data.boundary}`).filter(Boolean)).toStrictEqual([ - '\r\nContent-Disposition: form-data; name="documentFile"; filename="hello.jp.txt"\r\nContent-Type: text/plain\r\n\r\n速い茶色のキツネは怠惰な犬を飛び越えます\n\r\n', - '--', - ]); - - // expect(data.headers).toHaveProperty('content-type', `multipart/form-data; boundary=${data.boundary}`); - // expect(data.headers).toHaveProperty('content-length', '251'); - expect(data.headers).toHaveCustomUserAgent(); - }); - }); - - // it.skip('should support `multipart/form-data` requests with an array of files', async () => { - // const body = [ - // `${__dirname}/__fixtures__/owlbert.png`, - // `${__dirname}/__fixtures__/owlbert-shrub.png`, - // ]; - - // await api(fileUploads).post('/anything/form-data', body).then({ data } => { - // console.log(data) - // }) - // }); - }); -}); diff --git a/packages/api/test/server.test.ts b/packages/api/test/server.test.ts deleted file mode 100644 index 5d4e62fe..00000000 --- a/packages/api/test/server.test.ts +++ /dev/null @@ -1,63 +0,0 @@ -import fetchMock from 'fetch-mock'; -import uniqueTempDir from 'unique-temp-dir'; -import { describe, beforeAll, beforeEach, afterEach, it, expect } from 'vitest'; - -import api from '../src'; -import Cache from '../src/cache'; - -import loadSpec from './helpers/load-spec'; - -let serverVariables; -let sdk; -const petId = 123; -const response = { - id: petId, - name: 'Buster', -}; - -describe('#server()', () => { - beforeAll(async () => { - serverVariables = await loadSpec('@readme/oas-examples/3.0/json/server-variables.json'); - - // Set a unique cache dir so these tests won't collide with other tests and we don't need to go - // through the trouble of mocking out the filesystem. - Cache.setCacheDir(uniqueTempDir()); - }); - - beforeEach(() => { - sdk = api(serverVariables); - }); - - afterEach(() => { - fetchMock.restore(); - }); - - it('should use server variable defaults if no server or variables are supplied', async () => { - fetchMock.post('https://demo.example.com:443/v2/global', response); - - await sdk.postGlobal().then(({ data }) => expect(data).toStrictEqual(response)); - }); - - it('should support supplying a full server url', async () => { - fetchMock.post('https://buster.example.com:3000/v14/global', response); - - sdk.server('https://buster.example.com:3000/v14'); - - await sdk.postGlobal().then(({ data }) => expect(data).toStrictEqual(response)); - }); - - it('should support supplying a server url with server variables', async () => { - fetchMock.post('http://dev.local/v14/global', response); - - sdk.server('http://{name}.local/{basePath}', { - name: 'dev', - basePath: 'v14', - }); - - await sdk.postGlobal().then(({ data }) => expect(data).toStrictEqual(response)); - }); - - it.todo('should be able to supply a url on an OAS that has no servers defined'); - - it.todo("should be able to supply a url that doesn't match any defined server"); -}); From bdffa014be7c737adf9f071917293cafd2ce83f1 Mon Sep 17 00:00:00 2001 From: Jon Ursenbach Date: Mon, 11 Sep 2023 18:27:54 -0700 Subject: [PATCH 5/7] fix: cleaning up some remnants of the dynamic api --- docs/faq.md | 6 +- docs/how-it-works.md | 57 ------ docs/installation.md | 6 - docs/making-requests.md | 6 - docs/upgrading-from-v4.md | 2 +- docs/usage.md | 35 ---- package-lock.json | 3 - packages/api/README.md | 12 -- packages/api/src/cache.ts | 192 ------------------ packages/api/src/cli/commands/install.ts | 2 +- packages/api/src/{ => cli}/fetcher.ts | 0 packages/api/src/cli/storage.ts | 3 +- packages/api/src/typings.d.ts | 2 - packages/api/test/{ => cli}/fetcher.test.ts | 5 +- packages/httpsnippet-client-api/package.json | 3 - .../__datasets__/alternate-server/index.ts | 9 - .../application-form-encoded/index.ts | 15 -- .../__datasets__/application-json/index.ts | 24 --- .../__datasets__/auth-apikey-cookie/index.ts | 12 -- .../__datasets__/auth-apikey-header/index.ts | 12 -- .../__datasets__/auth-basic-full/index.ts | 16 -- .../auth-basic-password-only/index.ts | 16 -- .../auth-basic-username-only/index.ts | 16 -- .../test/__datasets__/auth-bearer/index.ts | 12 -- .../test/__datasets__/auth-query/index.ts | 12 -- .../test/__datasets__/cookies/index.ts | 12 -- .../full-many-query-params/index.ts | 21 -- .../test/__datasets__/full/index.ts | 21 -- .../test/__datasets__/headers/index.ts | 13 -- .../test/__datasets__/http-insecure/index.ts | 9 - .../test/__datasets__/issue-128/index.ts | 12 -- .../test/__datasets__/issue-76/index.ts | 13 -- .../issue-78-operationid/index.ts | 9 - .../test/__datasets__/issue-78/index.ts | 9 - .../__datasets__/jsonObj-multiline/index.ts | 15 -- .../__datasets__/jsonObj-null-value/index.ts | 15 -- .../test/__datasets__/multipart-data/index.ts | 28 --- .../test/__datasets__/multipart-file/index.ts | 28 --- .../multipart-form-data-no-params/index.ts | 12 -- .../__datasets__/multipart-form-data/index.ts | 28 --- .../operationid-non-alphanumerical/index.ts | 15 -- .../operationid-with-underscores/index.ts | 9 - .../parameter-special-characters/index.ts | 9 - .../test/__datasets__/petstore/index.ts | 16 -- .../test/__datasets__/query/index.ts | 14 -- .../test/__datasets__/short/index.ts | 9 - .../test/__datasets__/text-plain/index.ts | 15 -- .../httpsnippet-client-api/test/index.test.ts | 75 ------- 48 files changed, 8 insertions(+), 877 deletions(-) delete mode 100644 packages/api/src/cache.ts rename packages/api/src/{ => cli}/fetcher.ts (100%) delete mode 100644 packages/api/src/typings.d.ts rename packages/api/test/{ => cli}/fetcher.test.ts (98%) diff --git a/docs/faq.md b/docs/faq.md index a8f5e9d5..fecdea54 100644 --- a/docs/faq.md +++ b/docs/faq.md @@ -5,7 +5,7 @@ category: 5d4c940cae4e610060475769 ### Does this support YAML definitions? -Yes! YAML definitions will be automatically converted to JSON when they're fetched for either SDK code generation or dynamic `api` usage. +Yes! YAML definitions will be automatically converted to JSON when they're fetched. ### Does this support Swagger 2.0 definitions? @@ -31,9 +31,7 @@ If you have ideas on how to handle this [we'd love to hear them](https://github. ### Will this work in browsers? -If you generate an SDK with the CLI installation process then yes! If you're having trouble getting autogenerated SDKs working in a browser, [please let us know](https://github.com/readmeio/api/issues)! - -Unfortunately the dynamic version of `api` will **not** work in browsers as it requires access to the filesystem for handling and managing its cache state. +If you generate an SDK with the CLI installation process then yes! However because `api` supports `multipart/form-data` requests and uploading files to APIs you may need to polyfill the `fs` module in your toolchain. If you're having trouble getting autogenerated SDKs working in a browser [please let us know](https://github.com/readmeio/api/issues)! ### Will this validate my data before it reaches the API? diff --git a/docs/how-it-works.md b/docs/how-it-works.md index 2a20fcdb..9c01b536 100644 --- a/docs/how-it-works.md +++ b/docs/how-it-works.md @@ -9,7 +9,6 @@ Behind the scenes, `api` will: 2. Dereference the definition so it's easier for us to handle. 3. Cache the definition so we don't need to re-fetch it. 4. Process the definition into chainable methods for HTTP verbs and operation IDs. - - If you're using the code generation offering this only done at compilation time. If you're using the dynamic `api` offering this is done whenever you call `api` by way of a JavaScript [Proxy](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Proxy). ## Fetching @@ -17,11 +16,6 @@ At its core, `api` is a syntactical sugar wrapper for the [Fetch API](https://de ## Caching -Depending on the way you're using `api`, either dynamically or with code generation, there are two caching mechanisms in place: - -- CLI code generation: `.api/` -- Dynamic usage: `node_modules/.cache/api` - ### `.api/` directory The `.api/` directory is where the CLI installation process stores all of its information: installation records, OpenAPI definitions, and code generated libraries. Its structure is as such: @@ -66,54 +60,3 @@ The `api.json` file within `.api/` is where the CLI keeps track of everything th ``` In the future, commands will be added to the CLI that will take advantage of this information, but for now it's only used to determine if you've already installed an API to the same `identifier` (say, to prevent you from installing `@developers/v2.0#nysezql0wwo236` twice). - -### `node_modules/.cache/api` - -Because the dynamic version of `api` cannot have access to the `identifier` system that the CLI installation process does, the dynamic version of `api` has a slightly different caching mechanism and stores its data within `node_modules/.cache/api`: - -``` -.node_modules/ -└── .cache/ - └── api/ - ├── cache.json // Similar to `api.json` this is a record of - | // everything installed. - └── specs/ - └── cbb821db3609f8983ce1a372dadd122c.json -``` - -> ⚠️ -> -> Note that because the dynamic version of `api` requires a filesystem and the Node `crypto` module, `api` cannot be used in the browser. If you need to use it in a browser we recommend you use the code generation avenue instead. - -#### `cache.json` - -The `cache.json` file in `node_modules/.cache/api` is where the dynamic version of API stores and pulls all of its data from. OpenAPI definitions are indexed within this file by their original acessor (`require('api')('http://example.com/some-accessor')`). - -The `hash` within this is an `md5` of the full OpenAPI definition that we retrieved. - -If for some reason this file gets lost, or the accessor you're supplying to `api` changes for whatever reason `api` will re-retrieve the OpenAPI definition at run-time. - -```json -{ - "d6b93e95fa1a7efdce6d1406dc599923": { - "hash": "cbb821db3609f8983ce1a372dadd122c", - "original": "https://dash.readme.com/api/v1/api-registry/nysezql0wwo236", - "title": "API Endpoints", - "version": "2.0.0" - } -} -``` - -#### Custom cache directory - -By default the cache is configured with the [find-cache-dir](https://npm.im/find-cache-dir) library so the cache will be in `node_modules/.cache/api`. If placing this cache within the `node_modules/` directory is a problem for your environment (e.g. if you use `npm prune`), you can customize this by supplying an additional argument to the `api` instantiator: - -```js -const sdk = require('api')('https://raw.githubusercontent.com/readmeio/oas-examples/main/3.0/json/petstore.json', { - cacheDir: './path/to/my/custom/cache/dir', -}); - -sdk.listPets().then(({ data })) => { - console.log(`My pets name is ${data[0].name}!`); -}); -``` diff --git a/docs/installation.md b/docs/installation.md index 135091e8..705ebcc3 100644 --- a/docs/installation.md +++ b/docs/installation.md @@ -17,9 +17,3 @@ The `api` installer will guide you through several prompts about what kind of pr - URLs - You can also access and supply the URL for your ReadMe API Registry entry by using https://dash.readme.com/api/v1/api-registry/{uuid}, where `{uuid}` is the last part of your registry entry string. So in `@developers/v2.0#nysezql0wwo236` that would be `nysezql0wwo236`. - Local file paths - -To use the dynamic version of `api`, install the package to your project dependencies: - -```shell -$ npm install api --save -``` diff --git a/docs/making-requests.md b/docs/making-requests.md index a07088d5..9192b1cf 100644 --- a/docs/making-requests.md +++ b/docs/making-requests.md @@ -5,12 +5,6 @@ category: 62cc6ce22b8b6601da6cb12d If the API you're using doesn't have any documented [operation IDs](https://github.com/OAI/OpenAPI-Specification/blob/main/versions/3.1.0.md#user-content-operationid), `api` will generate some for you to use. -If you're using code generation, these will be immediately available to use in your generated library. However, due to the nature of the [Proxy](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Proxy) architecture in the dynamic version of the library, this isn't the case for dynamically generated libraries. For these cases, you can check out the documentation for the API you're using. - -> ⚠️ -> -> We recommend using code generation as it'll give you the additional benefit of TypeScript type assistance and autocompletion (even if you aren't using TypeScript in your codebase). - With an instance of your SDK, you make an HTTP request against an operation on the API like so: ```js diff --git a/docs/upgrading-from-v4.md b/docs/upgrading-from-v4.md index 941d664d..fc71f9da 100644 --- a/docs/upgrading-from-v4.md +++ b/docs/upgrading-from-v4.md @@ -1,5 +1,5 @@ --- -title: Upgrading from v4 +title: Upgrading from v4 to v5 category: 5d4c940cae4e610060475769 --- diff --git a/docs/usage.md b/docs/usage.md index 0c719821..a038737e 100644 --- a/docs/usage.md +++ b/docs/usage.md @@ -3,8 +3,6 @@ title: Usage category: 5d4c940cae4e610060475769 --- -`api` can be used in two ways: a locally-installed code-generated library or dynamically. - ## Code generation The code generation installation process that `api` offers comes in the form of an `api` CLI that will: @@ -31,36 +29,3 @@ petstore.listPets().then(({ data, status, headers, res }) => { And if you use an IDE with TypeScript support (like [Visual Studio Code](https://code.visualstudio.com/)), you get the benefit of having autogenerated TypeScript types to help you out—regardless if you're actually using TypeScript! ![TypeScript types in action](https://raw.githubusercontent.com/readmeio/api/main/docs/images/ts-types.png) - -## Dynamically - -If you don't wish to use code generation, you can load `api` and supply it an OpenAPI definition directly: - -```js -const petstore = require('api')('https://raw.githubusercontent.com/readmeio/oas-examples/main/3.0/json/petstore.json'); - -petstore.listPets().then(({ data, status, headers, res }) => { - console.log(`My pets name is ${data[0].name}!`); -}); -``` - -Alternatively, you can use the ESM syntax: - -```js -import api from 'api'; -const petstore = api('@petstore/v1.0#tl1e4kl1cl8eg8'); - -petstore.listPets().then(({ data, status, headers, res }) => { - console.log(`My pets name is ${data[0].name}!`); -}); -``` - -The OpenAPI definition is automatically downloaded, cached, and transformed into a chainable [`fetch`](https://developer.mozilla.org/en-US/docs/Web/API/Fetch_API) Promise that you can use to make API requests. - -> 📘 -> -> By using the dynamic, non-code-generated version of `api`, you will not have access to any TypeScript types to assist you in using an API. - -> ⚠️ -> -> Using the dynamic version version of `api` will also gate you off you from using the library in a browser as the dynamic version of `api` requires access to a filesystem for the library to work. diff --git a/package-lock.json b/package-lock.json index 3a4baa44..d0ba0787 100644 --- a/package-lock.json +++ b/package-lock.json @@ -21245,9 +21245,6 @@ "@types/content-type": "^1.1.6", "@types/stringify-object": "^4.0.2", "@vitest/coverage-v8": "^0.34.1", - "api": "file:../api", - "fetch-mock": "^9.11.0", - "formdata-to-string": "^1.0.0", "typescript": "^5.2.2", "vitest": "^0.34.1" }, diff --git a/packages/api/README.md b/packages/api/README.md index 0290d1c6..f6e0a8b0 100644 --- a/packages/api/README.md +++ b/packages/api/README.md @@ -36,18 +36,6 @@ petstore.listPets().then(({ data }) => { }); ``` -Or you can use it dynamically (though you won't have fancy TypeScript types to help you out!): - -```js -const petstore = require('api')( - 'https://raw.githubusercontent.com/OAI/OpenAPI-Specification/main/examples/v3.0/petstore.json', -); - -petstore.listPets().then(({ data }) => { - console.log(`My pets name is ${data[0].name}!`); -}); -``` - The ESM syntax is supported as well: ```js diff --git a/packages/api/src/cache.ts b/packages/api/src/cache.ts deleted file mode 100644 index 0be32885..00000000 --- a/packages/api/src/cache.ts +++ /dev/null @@ -1,192 +0,0 @@ -import type { OASDocument } from 'oas/dist/rmoas.types'; - -import crypto from 'crypto'; -import fs from 'fs'; -import os from 'os'; -import path from 'path'; - -import findCacheDir from 'find-cache-dir'; -import makeDir from 'make-dir'; - -import Fetcher from './fetcher'; -import { PACKAGE_NAME } from './packageInfo'; - -type CacheStore = Record< - string, - { - hash: string; - original: string | OASDocument; - /** - * @deprecated Deprecated in v4.5.0 in favor of `hash`. - */ - path?: string; - title?: string; - version?: string; - } ->; - -export default class Cache { - static dir: string; - - static cacheStore: string; - - static specsCache: string; - - uri: string | OASDocument; - - uriHash: string; - - cached: false | CacheStore; - - fetcher: Fetcher; - - constructor(uri: string | OASDocument, cacheDir: string | false = false) { - Cache.setCacheDir(cacheDir); - Cache.cacheStore = path.join(Cache.dir, 'cache.json'); - Cache.specsCache = path.join(Cache.dir, 'specs'); - - this.fetcher = new Fetcher(uri); - - this.uri = this.fetcher.uri; - this.uriHash = Cache.getCacheHash(this.uri); - - // This should default to false so we have awareness if we've looked at the cache yet. - this.cached = false; - } - - static getCacheHash(file: string | OASDocument) { - let data: string; - if (typeof file === 'object') { - // Under certain unit testing circumstances, we might be supplying the class with a raw JSON - // object so we'll need to convert it to a string in order to hand it off to the crypto - // module. - data = JSON.stringify(file); - } else { - data = file; - } - - return crypto.createHash('md5').update(data).digest('hex'); - } - - static setCacheDir(dir?: string | false) { - if (dir) { - Cache.dir = dir; - return; - } else if (Cache.dir) { - // If we already have a cache dir set and aren't explicitly it to something new then we - // shouldn't overwrite what we've already got. - return; - } - - Cache.dir = findCacheDir({ name: PACKAGE_NAME }); - if (typeof Cache.dir === 'undefined') { - // The `find-cache-dir` module returns `undefined` if the `node_modules/` directory isn't - // writable, or there's no `package.json` in the root-most directory. If this happens, we can - // instead adhoc create a cache directory in the users OS temp directory and store our data - // there. - // - // @link https://github.com/avajs/find-cache-dir/issues/29 - Cache.dir = makeDir.sync(path.join(os.tmpdir(), PACKAGE_NAME)); - } - } - - static async reset() { - if (Cache.cacheStore) { - await fs.promises.rm(Cache.cacheStore).catch(() => { - // no-op - }); - } - - if (Cache.specsCache) { - await fs.promises.rm(Cache.specsCache, { recursive: true }).catch(() => { - // no-op - }); - } - } - - isCached() { - const cache = this.getCache(); - return cache && this.uriHash in cache; - } - - getCache() { - if (typeof this.cached === 'object') { - return this.cached; - } - - this.cached = {}; - - if (fs.existsSync(Cache.cacheStore)) { - this.cached = JSON.parse(fs.readFileSync(Cache.cacheStore, 'utf8')) as CacheStore; - } - - return this.cached; - } - - get() { - // If the class was supplied a raw object, just go ahead and bypass the caching system and - // return that. - if (typeof this.uri === 'object') { - return this.uri; - } - - if (!this.isCached()) { - throw new Error(`${this.uri} has not been cached yet and must do so before being retrieved.`); - } - - const cache = this.getCache(); - - // Prior to v4.5.0 we were putting a fully resolved path to the API definition in the cache - // store but if you had specified a custom caching directory and would generate the cache on - // your system, that filepath would obviously not be the same in other environments. For this - // reason the `path` was removed from the cache store in favor of storing the `hash` instead. - // - // If we still have `path` in the config cache for backwards compatibility we should use it. - if ('path' in cache[this.uriHash]) { - return JSON.parse(fs.readFileSync(cache[this.uriHash].path, 'utf8')); - } - - return JSON.parse(fs.readFileSync(path.join(Cache.specsCache, `${cache[this.uriHash].hash}.json`), 'utf8')); - } - - async load() { - // If the class was supplied a raw object we should still validate and make sure that it's - // dereferenced in order for everything to function, but we shouldn't worry about saving it - // into the cache directory architecture. - if (typeof this.uri === 'object') { - return Fetcher.validate(this.uri); - } - - return this.fetcher.load().then(async spec => this.save(spec)); - } - - save(spec: OASDocument) { - if (!fs.existsSync(Cache.dir)) { - fs.mkdirSync(Cache.dir, { recursive: true }); - } - - if (!fs.existsSync(Cache.specsCache)) { - fs.mkdirSync(Cache.specsCache, { recursive: true }); - } - - const cache = this.getCache(); - if (!(this.uriHash in cache)) { - const saved = JSON.stringify(spec, null, 2); - const fileHash = crypto.createHash('md5').update(saved).digest('hex'); - - cache[this.uriHash] = { - hash: fileHash, - original: this.uri, - title: 'title' in spec.info ? spec.info.title : undefined, - version: 'version' in spec.info ? spec.info.version : undefined, - }; - - fs.writeFileSync(path.join(Cache.specsCache, `${fileHash}.json`), saved); - fs.writeFileSync(Cache.cacheStore, JSON.stringify(cache, null, 2)); - - this.cached = cache; - } - - return spec; - } -} diff --git a/packages/api/src/cli/commands/install.ts b/packages/api/src/cli/commands/install.ts index b469ff45..19a9e893 100644 --- a/packages/api/src/cli/commands/install.ts +++ b/packages/api/src/cli/commands/install.ts @@ -5,8 +5,8 @@ import figures from 'figures'; import Oas from 'oas'; import ora from 'ora'; -import Fetcher from '../../fetcher'; import codegen from '../codegen'; +import Fetcher from '../fetcher'; import promptTerminal from '../lib/prompt'; import logger from '../logger'; import Storage from '../storage'; diff --git a/packages/api/src/fetcher.ts b/packages/api/src/cli/fetcher.ts similarity index 100% rename from packages/api/src/fetcher.ts rename to packages/api/src/cli/fetcher.ts diff --git a/packages/api/src/cli/storage.ts b/packages/api/src/cli/storage.ts index c8993894..55f0a882 100644 --- a/packages/api/src/cli/storage.ts +++ b/packages/api/src/cli/storage.ts @@ -7,9 +7,10 @@ import makeDir from 'make-dir'; import ssri from 'ssri'; import validateNPMPackageName from 'validate-npm-package-name'; -import Fetcher from '../fetcher'; import { PACKAGE_VERSION } from '../packageInfo'; +import Fetcher from './fetcher'; + export default class Storage { static dir: string; diff --git a/packages/api/src/typings.d.ts b/packages/api/src/typings.d.ts deleted file mode 100644 index 41052d81..00000000 --- a/packages/api/src/typings.d.ts +++ /dev/null @@ -1,2 +0,0 @@ -// These libraries don't have any types so we need to let TS know so we can use them. -declare module 'fetch-har'; diff --git a/packages/api/test/fetcher.test.ts b/packages/api/test/cli/fetcher.test.ts similarity index 98% rename from packages/api/test/fetcher.test.ts rename to packages/api/test/cli/fetcher.test.ts index d281c673..4dd32f80 100644 --- a/packages/api/test/fetcher.test.ts +++ b/packages/api/test/cli/fetcher.test.ts @@ -4,9 +4,8 @@ import fs from 'fs/promises'; import fetchMock from 'fetch-mock'; import { describe, beforeAll, it, expect } from 'vitest'; -import Fetcher from '../src/fetcher'; - -import loadSpec from './helpers/load-spec'; +import Fetcher from '../../src/cli/fetcher'; +import loadSpec from '../helpers/load-spec'; let readmeSpec; diff --git a/packages/httpsnippet-client-api/package.json b/packages/httpsnippet-client-api/package.json index 2da181db..4c9e2127 100644 --- a/packages/httpsnippet-client-api/package.json +++ b/packages/httpsnippet-client-api/package.json @@ -38,9 +38,6 @@ "@types/content-type": "^1.1.6", "@types/stringify-object": "^4.0.2", "@vitest/coverage-v8": "^0.34.1", - "api": "file:../api", - "fetch-mock": "^9.11.0", - "formdata-to-string": "^1.0.0", "typescript": "^5.2.2", "vitest": "^0.34.1" }, diff --git a/packages/httpsnippet-client-api/test/__datasets__/alternate-server/index.ts b/packages/httpsnippet-client-api/test/__datasets__/alternate-server/index.ts index cc732149..69aa8240 100644 --- a/packages/httpsnippet-client-api/test/__datasets__/alternate-server/index.ts +++ b/packages/httpsnippet-client-api/test/__datasets__/alternate-server/index.ts @@ -17,15 +17,6 @@ const mock: SnippetMock = { url: 'http://dev.local/v2/global', }, definition, - fetch: { - req: { - url: 'http://dev.local/v2/global', - method: 'POST', - }, - res: { - status: 200, - }, - }, }; export default mock; diff --git a/packages/httpsnippet-client-api/test/__datasets__/application-form-encoded/index.ts b/packages/httpsnippet-client-api/test/__datasets__/application-form-encoded/index.ts index bb9829e1..dba290fb 100644 --- a/packages/httpsnippet-client-api/test/__datasets__/application-form-encoded/index.ts +++ b/packages/httpsnippet-client-api/test/__datasets__/application-form-encoded/index.ts @@ -33,21 +33,6 @@ const mock: SnippetMock = { url: 'http://httpbin.org/anything', }, definition: definition as OASDocument, - fetch: { - req: { - url: 'http://httpbin.org/anything', - method: 'post', - headers: { - 'content-type': 'application/x-www-form-urlencoded', - }, - functionMatcher: (url, opts) => { - return opts.body === 'foo=bar&hello=world'; - }, - }, - res: { - status: 200, - }, - }, }; export default mock; diff --git a/packages/httpsnippet-client-api/test/__datasets__/application-json/index.ts b/packages/httpsnippet-client-api/test/__datasets__/application-json/index.ts index 6d9ee532..5976ad9d 100644 --- a/packages/httpsnippet-client-api/test/__datasets__/application-json/index.ts +++ b/packages/httpsnippet-client-api/test/__datasets__/application-json/index.ts @@ -38,30 +38,6 @@ const mock: SnippetMock = { url: 'http://httpbin.org/anything', }, definition: definition as OASDocument, - fetch: { - req: { - url: 'http://httpbin.org/anything', - method: 'post', - functionMatcher: (url, opts) => { - return ( - opts.body === - JSON.stringify({ - number: 1, - string: 'f"oo', - arr: [1, 2, 3], - nested: { - a: 'b', - }, - arr_mix: [1, 'a'], - boolean: false, - }) - ); - }, - }, - res: { - status: 200, - }, - }, }; export default mock; diff --git a/packages/httpsnippet-client-api/test/__datasets__/auth-apikey-cookie/index.ts b/packages/httpsnippet-client-api/test/__datasets__/auth-apikey-cookie/index.ts index c98cca35..ff267282 100644 --- a/packages/httpsnippet-client-api/test/__datasets__/auth-apikey-cookie/index.ts +++ b/packages/httpsnippet-client-api/test/__datasets__/auth-apikey-cookie/index.ts @@ -18,18 +18,6 @@ const mock: SnippetMock = { url: 'https://httpbin.org/anything/apiKey', }, definition: definition as OASDocument, - fetch: { - req: { - url: 'https://httpbin.org/anything/apiKey', - method: 'post', - headers: { - cookie: 'api_key=buster', - }, - }, - res: { - status: 200, - }, - }, }; export default mock; diff --git a/packages/httpsnippet-client-api/test/__datasets__/auth-apikey-header/index.ts b/packages/httpsnippet-client-api/test/__datasets__/auth-apikey-header/index.ts index 960e8744..dff7962c 100644 --- a/packages/httpsnippet-client-api/test/__datasets__/auth-apikey-header/index.ts +++ b/packages/httpsnippet-client-api/test/__datasets__/auth-apikey-header/index.ts @@ -23,18 +23,6 @@ const mock: SnippetMock = { url: 'https://httpbin.org/anything/apiKey', }, definition: definition as OASDocument, - fetch: { - req: { - url: 'https://httpbin.org/anything/apiKey', - method: 'put', - headers: { - 'x-api-key': 'a5a220e', - }, - }, - res: { - status: 200, - }, - }, }; export default mock; diff --git a/packages/httpsnippet-client-api/test/__datasets__/auth-basic-full/index.ts b/packages/httpsnippet-client-api/test/__datasets__/auth-basic-full/index.ts index 82ffe6d6..aab69b3d 100644 --- a/packages/httpsnippet-client-api/test/__datasets__/auth-basic-full/index.ts +++ b/packages/httpsnippet-client-api/test/__datasets__/auth-basic-full/index.ts @@ -28,22 +28,6 @@ const mock: SnippetMock = { url: 'https://dash.readme.com/api/v1/api-specification', }, definition: definition as OASDocument, - fetch: { - req: { - url: 'https://dash.readme.com/api/v1/api-specification', - method: 'get', - query: { - perPage: 10, - page: 1, - }, - headers: { - authorization: 'Basic YnVzdGVyOnB1Zw==', - }, - }, - res: { - status: 200, - }, - }, }; export default mock; diff --git a/packages/httpsnippet-client-api/test/__datasets__/auth-basic-password-only/index.ts b/packages/httpsnippet-client-api/test/__datasets__/auth-basic-password-only/index.ts index 8280f771..a25a86d9 100644 --- a/packages/httpsnippet-client-api/test/__datasets__/auth-basic-password-only/index.ts +++ b/packages/httpsnippet-client-api/test/__datasets__/auth-basic-password-only/index.ts @@ -28,22 +28,6 @@ const mock: SnippetMock = { url: 'https://dash.readme.com/api/v1/api-specification', }, definition: definition as OASDocument, - fetch: { - req: { - url: 'https://dash.readme.com/api/v1/api-specification', - method: 'get', - query: { - perPage: 10, - page: 1, - }, - headers: { - authorization: 'Basic OnB1Zw==', - }, - }, - res: { - status: 200, - }, - }, }; export default mock; diff --git a/packages/httpsnippet-client-api/test/__datasets__/auth-basic-username-only/index.ts b/packages/httpsnippet-client-api/test/__datasets__/auth-basic-username-only/index.ts index 191d7abb..07eb7aef 100644 --- a/packages/httpsnippet-client-api/test/__datasets__/auth-basic-username-only/index.ts +++ b/packages/httpsnippet-client-api/test/__datasets__/auth-basic-username-only/index.ts @@ -28,22 +28,6 @@ const mock: SnippetMock = { url: 'https://dash.readme.com/api/v1/api-specification', }, definition: definition as OASDocument, - fetch: { - req: { - url: 'https://dash.readme.com/api/v1/api-specificatio', - method: 'get', - query: { - perPage: 10, - page: 1, - }, - headers: { - authorization: 'Basic YnVzdGVyOg==', - }, - }, - res: { - status: 200, - }, - }, }; export default mock; diff --git a/packages/httpsnippet-client-api/test/__datasets__/auth-bearer/index.ts b/packages/httpsnippet-client-api/test/__datasets__/auth-bearer/index.ts index ae420947..774cefb9 100644 --- a/packages/httpsnippet-client-api/test/__datasets__/auth-bearer/index.ts +++ b/packages/httpsnippet-client-api/test/__datasets__/auth-bearer/index.ts @@ -23,18 +23,6 @@ const mock: SnippetMock = { url: 'https://httpbin.org/anything/bearer', }, definition: definition as OASDocument, - fetch: { - req: { - url: 'https://httpbin.org/anything/bearer', - method: 'post', - headers: { - authorization: 'Bearer myBearerToken', - }, - }, - res: { - status: 200, - }, - }, }; export default mock; diff --git a/packages/httpsnippet-client-api/test/__datasets__/auth-query/index.ts b/packages/httpsnippet-client-api/test/__datasets__/auth-query/index.ts index d8a58897..b89c4468 100644 --- a/packages/httpsnippet-client-api/test/__datasets__/auth-query/index.ts +++ b/packages/httpsnippet-client-api/test/__datasets__/auth-query/index.ts @@ -23,18 +23,6 @@ const mock: SnippetMock = { url: 'https://httpbin.org/anything/apiKey', }, definition: definition as OASDocument, - fetch: { - req: { - url: 'https://httpbin.org/anything/apiKey', - method: 'get', - query: { - apiKey: 'a5a220e', - }, - }, - res: { - status: 200, - }, - }, }; export default mock; diff --git a/packages/httpsnippet-client-api/test/__datasets__/cookies/index.ts b/packages/httpsnippet-client-api/test/__datasets__/cookies/index.ts index 6791a806..9a462403 100644 --- a/packages/httpsnippet-client-api/test/__datasets__/cookies/index.ts +++ b/packages/httpsnippet-client-api/test/__datasets__/cookies/index.ts @@ -27,18 +27,6 @@ const mock: SnippetMock = { url: 'https://httpbin.org/anything', }, definition: definition as OASDocument, - fetch: { - req: { - url: 'https://httpbin.org/anything', - method: 'post', - headers: { - cookie: 'foo=bar; bar=baz', - }, - }, - res: { - status: 200, - }, - }, }; export default mock; diff --git a/packages/httpsnippet-client-api/test/__datasets__/full-many-query-params/index.ts b/packages/httpsnippet-client-api/test/__datasets__/full-many-query-params/index.ts index 2a235501..e06d352f 100644 --- a/packages/httpsnippet-client-api/test/__datasets__/full-many-query-params/index.ts +++ b/packages/httpsnippet-client-api/test/__datasets__/full-many-query-params/index.ts @@ -67,27 +67,6 @@ const mock: SnippetMock = { url: 'https://httpbin.org/anything?key=value', }, definition: definition as OASDocument, - fetch: { - req: { - url: 'https://httpbin.org/anything', - method: 'post', - query: { - key: 'value', - foo: ['bar', 'baz'], - baz: 'abc', - }, - headers: { - 'content-type': 'application/x-www-form-urlencoded', - cookie: 'bar-cookie=baz; foo-cookie=bar', - }, - functionMatcher: (url, opts) => { - return opts.body === 'foo=bar&foo2=bar2&foo3=bar3&foo4=bar4'; - }, - }, - res: { - status: 200, - }, - }, }; export default mock; diff --git a/packages/httpsnippet-client-api/test/__datasets__/full/index.ts b/packages/httpsnippet-client-api/test/__datasets__/full/index.ts index dfffc830..dab92b9f 100644 --- a/packages/httpsnippet-client-api/test/__datasets__/full/index.ts +++ b/packages/httpsnippet-client-api/test/__datasets__/full/index.ts @@ -55,27 +55,6 @@ const mock: SnippetMock = { url: 'https://httpbin.org/anything?key=value', }, definition: definition as OASDocument, - fetch: { - req: { - url: 'https://httpbin.org/anything', - method: 'post', - query: { - key: 'value', - foo: ['bar', 'baz'], - baz: 'abc', - }, - headers: { - 'content-type': 'application/x-www-form-urlencoded', - cookie: 'bar-cookie=baz; foo-cookie=bar', - }, - functionMatcher: (url, opts) => { - return opts.body === 'foo=bar'; - }, - }, - res: { - status: 200, - }, - }, }; export default mock; diff --git a/packages/httpsnippet-client-api/test/__datasets__/headers/index.ts b/packages/httpsnippet-client-api/test/__datasets__/headers/index.ts index a1858766..49eac112 100644 --- a/packages/httpsnippet-client-api/test/__datasets__/headers/index.ts +++ b/packages/httpsnippet-client-api/test/__datasets__/headers/index.ts @@ -31,19 +31,6 @@ const mock: SnippetMock = { url: 'https://httpbin.org/anything', }, definition: definition as OASDocument, - fetch: { - req: { - url: 'https://httpbin.org/anything', - method: 'GET', - headers: { - 'x-bar': 'foo', - 'x-foo': 'Bar', - }, - }, - res: { - status: 200, - }, - }, }; export default mock; diff --git a/packages/httpsnippet-client-api/test/__datasets__/http-insecure/index.ts b/packages/httpsnippet-client-api/test/__datasets__/http-insecure/index.ts index f9f0d2cd..1d9d6ae4 100644 --- a/packages/httpsnippet-client-api/test/__datasets__/http-insecure/index.ts +++ b/packages/httpsnippet-client-api/test/__datasets__/http-insecure/index.ts @@ -18,15 +18,6 @@ const mock: SnippetMock = { url: 'http://httpbin.org/anything', }, definition: definition as OASDocument, - fetch: { - req: { - url: 'http://httpbin.org/anything', - method: 'get', - }, - res: { - status: 200, - }, - }, }; export default mock; diff --git a/packages/httpsnippet-client-api/test/__datasets__/issue-128/index.ts b/packages/httpsnippet-client-api/test/__datasets__/issue-128/index.ts index 7df45ca3..c718e3df 100644 --- a/packages/httpsnippet-client-api/test/__datasets__/issue-128/index.ts +++ b/packages/httpsnippet-client-api/test/__datasets__/issue-128/index.ts @@ -23,18 +23,6 @@ const mock: SnippetMock = { url: 'https://httpbin.org/anything', }, definition: definition as OASDocument, - fetch: { - req: { - url: 'https://httpbin.org/anything', - method: 'get', - query: { - api_key: "authKey'With'Apostrophes", - }, - }, - res: { - status: 200, - }, - }, }; export default mock; diff --git a/packages/httpsnippet-client-api/test/__datasets__/issue-76/index.ts b/packages/httpsnippet-client-api/test/__datasets__/issue-76/index.ts index 5cbe2fb8..3ea21886 100644 --- a/packages/httpsnippet-client-api/test/__datasets__/issue-76/index.ts +++ b/packages/httpsnippet-client-api/test/__datasets__/issue-76/index.ts @@ -27,19 +27,6 @@ const mock: SnippetMock = { url: 'http://petstore.swagger.io/v2/pet/findByStatus', }, definition: definition as OASDocument, - fetch: { - req: { - url: 'http://petstore.swagger.io/v2/pet/findByStatus', - method: 'get', - query: { - status: 'available', - api_key: 'a5a220e', - }, - }, - res: { - status: 200, - }, - }, }; export default mock; diff --git a/packages/httpsnippet-client-api/test/__datasets__/issue-78-operationid/index.ts b/packages/httpsnippet-client-api/test/__datasets__/issue-78-operationid/index.ts index aba1d0c9..a473a9af 100644 --- a/packages/httpsnippet-client-api/test/__datasets__/issue-78-operationid/index.ts +++ b/packages/httpsnippet-client-api/test/__datasets__/issue-78-operationid/index.ts @@ -18,15 +18,6 @@ const mock: SnippetMock = { url: 'http://petstore.swagger.io/v2/store/order/1234', }, definition: definition as OASDocument, - fetch: { - req: { - url: 'http://petstore.swagger.io/v2/store/order/1234', - method: 'get', - }, - res: { - status: 200, - }, - }, }; export default mock; diff --git a/packages/httpsnippet-client-api/test/__datasets__/issue-78/index.ts b/packages/httpsnippet-client-api/test/__datasets__/issue-78/index.ts index fd22c752..e9a6f14c 100644 --- a/packages/httpsnippet-client-api/test/__datasets__/issue-78/index.ts +++ b/packages/httpsnippet-client-api/test/__datasets__/issue-78/index.ts @@ -18,15 +18,6 @@ const mock: SnippetMock = { url: 'http://petstore.swagger.io/v2/store/order/1234/tracking/5678', }, definition: definition as OASDocument, - fetch: { - req: { - url: 'http://petstore.swagger.io/v2/store/order/1234/tracking/5678', - method: 'get', - }, - res: { - status: 200, - }, - }, }; export default mock; diff --git a/packages/httpsnippet-client-api/test/__datasets__/jsonObj-multiline/index.ts b/packages/httpsnippet-client-api/test/__datasets__/jsonObj-multiline/index.ts index 1db53efb..08a7b950 100644 --- a/packages/httpsnippet-client-api/test/__datasets__/jsonObj-multiline/index.ts +++ b/packages/httpsnippet-client-api/test/__datasets__/jsonObj-multiline/index.ts @@ -24,21 +24,6 @@ const mock: SnippetMock = { url: 'https://httpbin.org/anything', }, definition: definition as OASDocument, - fetch: { - req: { - url: 'https://httpbin.org/anything', - method: 'post', - headers: { - 'content-type': 'application/json', - }, - functionMatcher: (url, opts) => { - return opts.body === JSON.stringify({ foo: 'bar' }); - }, - }, - res: { - status: 200, - }, - }, }; export default mock; diff --git a/packages/httpsnippet-client-api/test/__datasets__/jsonObj-null-value/index.ts b/packages/httpsnippet-client-api/test/__datasets__/jsonObj-null-value/index.ts index c4c4ffc0..10e79255 100644 --- a/packages/httpsnippet-client-api/test/__datasets__/jsonObj-null-value/index.ts +++ b/packages/httpsnippet-client-api/test/__datasets__/jsonObj-null-value/index.ts @@ -25,21 +25,6 @@ const mock: SnippetMock = { url: 'https://httpbin.org/anything', }, definition: definition as OASDocument, - fetch: { - req: { - url: 'https://httpbin.org/anything', - method: 'post', - headers: { - 'content-type': 'application/json', - }, - functionMatcher: (url, opts) => { - return opts.body === JSON.stringify({ foo: null }); - }, - }, - res: { - status: 200, - }, - }, }; export default mock; diff --git a/packages/httpsnippet-client-api/test/__datasets__/multipart-data/index.ts b/packages/httpsnippet-client-api/test/__datasets__/multipart-data/index.ts index fc6e7c77..d32d8226 100644 --- a/packages/httpsnippet-client-api/test/__datasets__/multipart-data/index.ts +++ b/packages/httpsnippet-client-api/test/__datasets__/multipart-data/index.ts @@ -1,8 +1,6 @@ import type { SnippetMock } from '../../index.test'; import type { OASDocument } from 'oas/dist/rmoas.types'; -import formDataToString from 'formdata-to-string'; - import definition from './openapi.json'; /** @@ -50,32 +48,6 @@ const mock: SnippetMock = { url: 'https://httpbin.org/anything', }, definition: definition as OASDocument, - fetch: { - req: { - scope: 'https://httpbin.org/anything', - method: 'post', - // @ts-expect-error Types don't reflect it but `fetch-mock` supports async function matchers. - functionMatcher: async (url, { body }: { body: FormData }) => { - const content = await formDataToString(body); - - // A fun thing about `fetch-mock` and these undocumented async matchers is that it doesn't - // look at what you're returning so we could return `false` here and it would think that - // the request was matched. Very cool. - if ( - !/------formdata-undici-(.*)\r\nContent-Disposition: form-data; name="foo"; filename="hello.txt"\r\nContent-Type: text\/plain\r\n\r\nHello world!\n\r\n------formdata-undici-(.*)--/.test( - content, - ) - ) { - throw new Error('The FormData payload does not match what was expected.'); - } - - return true; - }, - }, - res: { - status: 200, - }, - }, }; export default mock; diff --git a/packages/httpsnippet-client-api/test/__datasets__/multipart-file/index.ts b/packages/httpsnippet-client-api/test/__datasets__/multipart-file/index.ts index e39097d2..0dae441b 100644 --- a/packages/httpsnippet-client-api/test/__datasets__/multipart-file/index.ts +++ b/packages/httpsnippet-client-api/test/__datasets__/multipart-file/index.ts @@ -1,8 +1,6 @@ import type { SnippetMock } from '../../index.test'; import type { OASDocument } from 'oas/dist/rmoas.types'; -import formDataToString from 'formdata-to-string'; - import definition from './openapi.json'; const mock: SnippetMock = { @@ -32,32 +30,6 @@ const mock: SnippetMock = { url: 'https://httpbin.org/anything', }, definition: definition as OASDocument, - fetch: { - req: { - url: 'https://httpbin.org/anything', - method: 'post', - // @ts-expect-error Types don't reflect it but `fetch-mock` supports async function matchers. - functionMatcher: async (url, { body }: { body: FormData }) => { - const content = await formDataToString(body); - - // A fun thing about `fetch-mock` and these undocumented async matchers is that it doesn't - // look at what you're returning so we could return `false` here and it would think that - // the request was matched. Very cool. - if ( - !/------formdata-undici-(.*)\r\nContent-Disposition: form-data; name="foo"; filename="hello.txt"\r\nContent-Type: text\/plain\r\n\r\nHello world!\n\r\n------formdata-undici-(.*)--/.test( - content, - ) - ) { - throw new Error('The FormData payload does not match what was expected.'); - } - - return true; - }, - }, - res: { - status: 200, - }, - }, }; export default mock; diff --git a/packages/httpsnippet-client-api/test/__datasets__/multipart-form-data-no-params/index.ts b/packages/httpsnippet-client-api/test/__datasets__/multipart-form-data-no-params/index.ts index a5999601..587c1192 100644 --- a/packages/httpsnippet-client-api/test/__datasets__/multipart-form-data-no-params/index.ts +++ b/packages/httpsnippet-client-api/test/__datasets__/multipart-form-data-no-params/index.ts @@ -23,18 +23,6 @@ const mock: SnippetMock = { url: 'https://httpbin.org/anything', }, definition: definition as OASDocument, - fetch: { - req: { - url: 'https://httpbin.org/anything', - method: 'post', - headers: { - 'content-type': 'multipart/form-data', - }, - }, - res: { - status: 200, - }, - }, }; export default mock; diff --git a/packages/httpsnippet-client-api/test/__datasets__/multipart-form-data/index.ts b/packages/httpsnippet-client-api/test/__datasets__/multipart-form-data/index.ts index e552da72..67d0b25e 100644 --- a/packages/httpsnippet-client-api/test/__datasets__/multipart-form-data/index.ts +++ b/packages/httpsnippet-client-api/test/__datasets__/multipart-form-data/index.ts @@ -1,8 +1,6 @@ import type { SnippetMock } from '../../index.test'; import type { OASDocument } from 'oas/dist/rmoas.types'; -import formDataToString from 'formdata-to-string'; - import definition from './openapi.json'; const mock: SnippetMock = { @@ -31,32 +29,6 @@ const mock: SnippetMock = { url: 'https://httpbin.org/anything', }, definition: definition as OASDocument, - fetch: { - req: { - url: 'https://httpbin.org/anything', - method: 'POST', - // @ts-expect-error Types don't reflect it but `fetch-mock` supports async function matchers. - functionMatcher: async (url, { body }: { body: FormData }) => { - const content = await formDataToString(body); - - // A fun thing about `fetch-mock` and these undocumented async matchers is that it doesn't - // look at what you're returning so we could return `false` here and it would think that - // the request was matched. Very cool. - if ( - !/------formdata-undici-(.*)\r\nContent-Disposition: form-data; name="foo"\r\n\r\nbar\r\n------formdata-undici-(.*)--/.test( - content, - ) - ) { - throw new Error('The FormData payload does not match what was expected.'); - } - - return true; - }, - }, - res: { - status: 200, - }, - }, }; export default mock; diff --git a/packages/httpsnippet-client-api/test/__datasets__/operationid-non-alphanumerical/index.ts b/packages/httpsnippet-client-api/test/__datasets__/operationid-non-alphanumerical/index.ts index 9f60dcd8..0b730227 100644 --- a/packages/httpsnippet-client-api/test/__datasets__/operationid-non-alphanumerical/index.ts +++ b/packages/httpsnippet-client-api/test/__datasets__/operationid-non-alphanumerical/index.ts @@ -28,21 +28,6 @@ const mock: SnippetMock = { url: 'http://petstore.swagger.io/v2/pet/findByStatus', }, definition: definition as OASDocument, - fetch: { - req: { - url: 'http://petstore.swagger.io/v2/pet/findByStatus', - method: 'GET', - query: { - status: 'available', - }, - headers: { - authorization: 'Bearer 123', - }, - }, - res: { - status: 200, - }, - }, }; export default mock; diff --git a/packages/httpsnippet-client-api/test/__datasets__/operationid-with-underscores/index.ts b/packages/httpsnippet-client-api/test/__datasets__/operationid-with-underscores/index.ts index 126b28b8..958a9e08 100644 --- a/packages/httpsnippet-client-api/test/__datasets__/operationid-with-underscores/index.ts +++ b/packages/httpsnippet-client-api/test/__datasets__/operationid-with-underscores/index.ts @@ -18,15 +18,6 @@ const mock: SnippetMock = { url: 'https://httpbin.org/anything', }, definition: definition as unknown as OASDocument, - fetch: { - req: { - url: 'https://httpbin.org/anything', - method: 'get', - }, - res: { - status: 200, - }, - }, }; export default mock; diff --git a/packages/httpsnippet-client-api/test/__datasets__/parameter-special-characters/index.ts b/packages/httpsnippet-client-api/test/__datasets__/parameter-special-characters/index.ts index 43d81fe0..e6a87ca4 100644 --- a/packages/httpsnippet-client-api/test/__datasets__/parameter-special-characters/index.ts +++ b/packages/httpsnippet-client-api/test/__datasets__/parameter-special-characters/index.ts @@ -18,15 +18,6 @@ const mock: SnippetMock = { url: 'https://httpbin.org/anything/1234/5678/installs_report/v5', }, definition: definition as unknown as OASDocument, - fetch: { - req: { - url: 'https://httpbin.org/anything/1234/5678/installs_report/v5', - method: 'get', - }, - res: { - status: 200, - }, - }, }; export default mock; diff --git a/packages/httpsnippet-client-api/test/__datasets__/petstore/index.ts b/packages/httpsnippet-client-api/test/__datasets__/petstore/index.ts index 82379adc..54091392 100644 --- a/packages/httpsnippet-client-api/test/__datasets__/petstore/index.ts +++ b/packages/httpsnippet-client-api/test/__datasets__/petstore/index.ts @@ -32,22 +32,6 @@ const mock: SnippetMock = { url: 'http://petstore.swagger.io/v2/pet/findByStatus', }, definition: definition as OASDocument, - fetch: { - req: { - url: 'http://petstore.swagger.io/v2/pet/findByStatus', - method: 'GET', - query: { - status: 'available', - }, - headers: { - accept: 'application/xml', - authorization: 'Bearer 123', - }, - }, - res: { - status: 200, - }, - }, }; export default mock; diff --git a/packages/httpsnippet-client-api/test/__datasets__/query/index.ts b/packages/httpsnippet-client-api/test/__datasets__/query/index.ts index f3c36f7a..f86dd163 100644 --- a/packages/httpsnippet-client-api/test/__datasets__/query/index.ts +++ b/packages/httpsnippet-client-api/test/__datasets__/query/index.ts @@ -31,20 +31,6 @@ const mock: SnippetMock = { url: 'https://httpbin.org/anything?key=value', }, definition: definition as OASDocument, - fetch: { - req: { - url: 'https://httpbin.org/anything', - method: 'GET', - query: { - key: 'value', - foo: ['bar', 'baz'], - baz: 'abc', - }, - }, - res: { - status: 200, - }, - }, }; export default mock; diff --git a/packages/httpsnippet-client-api/test/__datasets__/short/index.ts b/packages/httpsnippet-client-api/test/__datasets__/short/index.ts index ebdf8cd6..3aed36b2 100644 --- a/packages/httpsnippet-client-api/test/__datasets__/short/index.ts +++ b/packages/httpsnippet-client-api/test/__datasets__/short/index.ts @@ -18,15 +18,6 @@ const mock: SnippetMock = { url: 'https://httpbin.org/anything', }, definition: definition as OASDocument, - fetch: { - req: { - url: 'https://httpbin.org/anything', - method: 'get', - }, - res: { - status: 200, - }, - }, }; export default mock; diff --git a/packages/httpsnippet-client-api/test/__datasets__/text-plain/index.ts b/packages/httpsnippet-client-api/test/__datasets__/text-plain/index.ts index 4dc2ee57..d543e7ab 100644 --- a/packages/httpsnippet-client-api/test/__datasets__/text-plain/index.ts +++ b/packages/httpsnippet-client-api/test/__datasets__/text-plain/index.ts @@ -24,21 +24,6 @@ const mock: SnippetMock = { url: 'https://httpbin.org/anything', }, definition: definition as OASDocument, - fetch: { - req: { - url: 'https://httpbin.org/anything', - method: 'post', - headers: { - 'content-type': 'text/plain', - }, - functionMatcher: (url, opts) => { - return opts.body === 'Hello World'; - }, - }, - res: { - status: 200, - }, - }, }; export default mock; diff --git a/packages/httpsnippet-client-api/test/index.test.ts b/packages/httpsnippet-client-api/test/index.test.ts index 0081419e..6faa4d85 100644 --- a/packages/httpsnippet-client-api/test/index.test.ts +++ b/packages/httpsnippet-client-api/test/index.test.ts @@ -1,18 +1,14 @@ -/* eslint-disable global-require */ import type { HarRequest, Request } from '@readme/httpsnippet'; import type { Client } from '@readme/httpsnippet/dist/targets/targets'; -import type { MockMatcher, MockOptions } from 'fetch-mock'; import type { OASDocument } from 'oas/dist/rmoas.types'; import { readdirSync } from 'fs'; import fs from 'fs/promises'; import path from 'path'; -import vm from 'vm'; import { HTTPSnippet, addTargetClient } from '@readme/httpsnippet'; import readme from '@readme/oas-examples/3.0/json/readme.json'; import openapiParser from '@readme/openapi-parser'; -import fetchMock from 'fetch-mock'; import rimraf from 'rimraf'; import { describe, afterEach, beforeEach, expect, it, vi } from 'vitest'; @@ -23,10 +19,6 @@ const SNIPPETS = readdirSync(DATASETS_DIR); export interface SnippetMock { definition: OASDocument; - fetch: { - req: MockMatcher | MockOptions; - res: Record; - }; har: HarRequest; } @@ -124,7 +116,6 @@ describe('httpsnippet-client-api', () => { afterEach(() => { consoleStub.mockRestore(); - fetchMock.restore(); }); it('should generate the expected snippet', async () => { @@ -137,72 +128,6 @@ describe('httpsnippet-client-api', () => { expect(`${code}\n`).toStrictEqual(expected); }); - - it('should generate a functional snippet', async () => { - if (!mock.fetch.req || !mock.fetch.res) { - throw new Error(`The mock definition for ${snippet} must include required \`req\` and \`res\` expectations.`); - } - - fetchMock.get(`https://api.example.com/${snippet}.json`, { status: 200, body: mock.definition }); - fetchMock.mock(mock.fetch.req, { - ...mock.fetch.res, - body: `The ${snippet} request works properly!`, - }); - - const code = await fs.readFile(path.join(DATASETS_DIR, snippet, 'output.js'), 'utf-8').then(str => { - // So we can test these snippets within a Node VM environment we need to remove the api - // require statement off (as we'll be handling that ourselves), and also set up the - // promise within the snippet to be returned so that we can containerize it within - // another promise. - const lines = str.split('\n').slice(2); - - // If the first non-require statement in our snippet is an `sdk.auth()` or - // `sdk.server()` call then we should add our `return` statement to the following. We - // currently don't have any tests that test both auth and alternate servers so we don't - // need to worry about that. - if (lines[0].startsWith('sdk.auth(') || lines[0].startsWith('sdk.server')) { - lines[1] = `return ${lines[1]}`; - } else { - lines[0] = `return ${lines[0]}`; - } - - return lines.join('\n'); - }); - - await new Promise((resolve, reject) => { - const sandbox = { - // eslint-disable-next-line @typescript-eslint/no-var-requires - sdk: require('api')(`https://api.example.com/${snippet}.json`), - - // So we can access logged data from our snippets within the VM we need to set the - // global `console` object within it to our current one so that the stub we're - // creating above will have visibility into what the VM is doing. - console, - - // So we can await for responses from the async snippets we've got in the VM we need - // to set some global `resolve` and `reject` methods inside the VM to the current - // of the same in this Promise so that when they're called from within the context of - // the VM environment the this Promise will also be resolved/rejected. - // https://stackoverflow.com/a/60216761 - resolve, - reject, - }; - - const vmCode = `Promise.resolve().then(() => { - ${code} - }).then(res => { - resolve(); - }).catch(err => { - reject(err.message); - })`; - - const script = new vm.Script(vmCode); - const context = vm.createContext(sandbox); - script.runInContext(context); - }); - - expect(consoleStub).toHaveBeenCalledWith(`The ${snippet} request works properly!`); - }); }); }); }); From fd4297546ac6c661339822b49acd07d56edb3254 Mon Sep 17 00:00:00 2001 From: Jon Ursenbach Date: Tue, 12 Sep 2023 10:22:39 -0700 Subject: [PATCH 6/7] Update docs/faq.md Co-authored-by: Kanad Gupta <8854718+kanadgupta@users.noreply.github.com> --- docs/faq.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/faq.md b/docs/faq.md index fecdea54..c4f53c18 100644 --- a/docs/faq.md +++ b/docs/faq.md @@ -31,7 +31,7 @@ If you have ideas on how to handle this [we'd love to hear them](https://github. ### Will this work in browsers? -If you generate an SDK with the CLI installation process then yes! However because `api` supports `multipart/form-data` requests and uploading files to APIs you may need to polyfill the `fs` module in your toolchain. If you're having trouble getting autogenerated SDKs working in a browser [please let us know](https://github.com/readmeio/api/issues)! +Yes! However because `api` supports `multipart/form-data` requests and uploading files to APIs you may need to polyfill the `fs` module in your toolchain. If you're having trouble getting autogenerated SDKs working in a browser [please let us know](https://github.com/readmeio/api/issues)! ### Will this validate my data before it reaches the API? From f2092434c63ac56849a1bb8be921c2c6d4000d4b Mon Sep 17 00:00:00 2001 From: Jon Ursenbach Date: Tue, 12 Sep 2023 12:48:40 -0700 Subject: [PATCH 7/7] fix: pr feedback --- packages/httpsnippet-client-api/test/tsconfig.json | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/packages/httpsnippet-client-api/test/tsconfig.json b/packages/httpsnippet-client-api/test/tsconfig.json index 59c11cc5..72079533 100644 --- a/packages/httpsnippet-client-api/test/tsconfig.json +++ b/packages/httpsnippet-client-api/test/tsconfig.json @@ -2,9 +2,7 @@ "extends": "../tsconfig.json", "compilerOptions": { "noImplicitAny": false, - "module": "Node16", - "resolveJsonModule": true, - "target": "ES2015" + "resolveJsonModule": true }, "include": ["../src/**/*", "*.ts", "**/*"], "exclude": ["__fixtures__/sdk/"]