From 8d85b5e613f819e3c5b9b8c43514fb8889ec43a2 Mon Sep 17 00:00:00 2001 From: Adam Mcgrath Date: Wed, 16 Aug 2023 11:22:11 +0100 Subject: [PATCH] [SDK-4485] Use native fetch, drop Node 16 support (#906) Co-authored-by: Frederik Prijck --- package-lock.json | 173 +++++++----------- package.json | 2 +- playground/handlers.ts | 2 +- src/lib/runtime.ts | 32 +--- .../__generated/managers/jobs-manager.ts | 2 +- src/management/__generated/models/index.ts | 4 +- test/lib/runtime.test.ts | 5 +- test/management/jobs.test.ts | 85 ++++----- test/setup.ts | 4 + v4_MIGRATION_GUIDE.md | 31 ++++ 10 files changed, 152 insertions(+), 188 deletions(-) diff --git a/package-lock.json b/package-lock.json index c3be94884..f54d489fd 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1394,29 +1394,15 @@ "dev": true }, "node_modules/@types/node-fetch": { - "version": "2.6.3", - "resolved": "https://registry.npmjs.org/@types/node-fetch/-/node-fetch-2.6.3.tgz", - "integrity": "sha512-ETTL1mOEdq/sxUtgtOhKjyB2Irra4cjxksvcMUR5Zr4n+PxVhsCD9WS46oPbHL3et9Zde7CNRr+WUNlcHvsX+w==", + "version": "2.6.4", + "resolved": "https://registry.npmjs.org/@types/node-fetch/-/node-fetch-2.6.4.tgz", + "integrity": "sha512-1ZX9fcN4Rvkvgv4E6PAY5WXUFWFcRWxZa3EW83UjycOB9ljJCedb2CupIP4RZMEwF/M3eTcCihbBRgwtGbg5Rg==", "dev": true, "dependencies": { "@types/node": "*", "form-data": "^3.0.0" } }, - "node_modules/@types/node-fetch/node_modules/form-data": { - "version": "3.0.1", - "resolved": "https://registry.npmjs.org/form-data/-/form-data-3.0.1.tgz", - "integrity": "sha512-RHkBKtLWUVwd7SqRIvCZMEvAMoGUp0XU+seQiZejj0COz3RI3hWP4sCv3gZWWLjJTd7rGwcsF5eKZGii0r/hbg==", - "dev": true, - "dependencies": { - "asynckit": "^0.4.0", - "combined-stream": "^1.0.8", - "mime-types": "^2.1.12" - }, - "engines": { - "node": ">= 6" - } - }, "node_modules/@types/normalize-package-data": { "version": "2.4.1", "resolved": "https://registry.npmjs.org/@types/normalize-package-data/-/normalize-package-data-2.4.1.tgz", @@ -3250,6 +3236,28 @@ "bser": "2.1.1" } }, + "node_modules/fetch-blob": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/fetch-blob/-/fetch-blob-3.2.0.tgz", + "integrity": "sha512-7yAQpD2UMJzLi1Dqv7qFYnPbaPx7ZfFK6PiIxQ4PfkGPyNyl2Ugx+a/umUonmKqjhM4DnfbMvdX6otXq83soQQ==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/jimmywarting" + }, + { + "type": "paypal", + "url": "https://paypal.me/jimmywarting" + } + ], + "dependencies": { + "node-domexception": "^1.0.0", + "web-streams-polyfill": "^3.0.3" + }, + "engines": { + "node": "^12.20 || >= 14.13" + } + }, "node_modules/file-entry-cache": { "version": "6.0.1", "resolved": "https://registry.npmjs.org/file-entry-cache/-/file-entry-cache-6.0.1.tgz", @@ -3331,6 +3339,20 @@ "node": ">=8.0.0" } }, + "node_modules/form-data": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/form-data/-/form-data-3.0.1.tgz", + "integrity": "sha512-RHkBKtLWUVwd7SqRIvCZMEvAMoGUp0XU+seQiZejj0COz3RI3hWP4sCv3gZWWLjJTd7rGwcsF5eKZGii0r/hbg==", + "dev": true, + "dependencies": { + "asynckit": "^0.4.0", + "combined-stream": "^1.0.8", + "mime-types": "^2.1.12" + }, + "engines": { + "node": ">= 6" + } + }, "node_modules/formdata-polyfill": { "version": "4.0.10", "resolved": "https://registry.npmjs.org/formdata-polyfill/-/formdata-polyfill-4.0.10.tgz", @@ -3342,28 +3364,6 @@ "node": ">=12.20.0" } }, - "node_modules/formdata-polyfill/node_modules/fetch-blob": { - "version": "3.2.0", - "resolved": "https://registry.npmjs.org/fetch-blob/-/fetch-blob-3.2.0.tgz", - "integrity": "sha512-7yAQpD2UMJzLi1Dqv7qFYnPbaPx7ZfFK6PiIxQ4PfkGPyNyl2Ugx+a/umUonmKqjhM4DnfbMvdX6otXq83soQQ==", - "funding": [ - { - "type": "github", - "url": "https://github.com/sponsors/jimmywarting" - }, - { - "type": "paypal", - "url": "https://paypal.me/jimmywarting" - } - ], - "dependencies": { - "node-domexception": "^1.0.0", - "web-streams-polyfill": "^3.0.3" - }, - "engines": { - "node": "^12.20 || >= 14.13" - } - }, "node_modules/fs.realpath": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/fs.realpath/-/fs.realpath-1.0.0.tgz", @@ -5359,9 +5359,9 @@ } }, "node_modules/node-fetch": { - "version": "3.3.1", - "resolved": "https://registry.npmjs.org/node-fetch/-/node-fetch-3.3.1.tgz", - "integrity": "sha512-cRVc/kyto/7E5shrWca1Wsea4y6tL9iYJE5FBCius3JQfb/4P4I295PfhgbJQBLTx6lATE4z+wK0rPM4VS2uow==", + "version": "3.3.2", + "resolved": "https://registry.npmjs.org/node-fetch/-/node-fetch-3.3.2.tgz", + "integrity": "sha512-dRB78srN/l6gqWulah9SrxeYnxeddIG30+GOqK/9OlLVyLg3HPnr6SqOWTWOXKRwC2eGYCkZ59NNuSgvSrpgOA==", "dependencies": { "data-uri-to-buffer": "^4.0.0", "fetch-blob": "^3.1.4", @@ -5375,28 +5375,6 @@ "url": "https://opencollective.com/node-fetch" } }, - "node_modules/node-fetch/node_modules/fetch-blob": { - "version": "3.2.0", - "resolved": "https://registry.npmjs.org/fetch-blob/-/fetch-blob-3.2.0.tgz", - "integrity": "sha512-7yAQpD2UMJzLi1Dqv7qFYnPbaPx7ZfFK6PiIxQ4PfkGPyNyl2Ugx+a/umUonmKqjhM4DnfbMvdX6otXq83soQQ==", - "funding": [ - { - "type": "github", - "url": "https://github.com/sponsors/jimmywarting" - }, - { - "type": "paypal", - "url": "https://paypal.me/jimmywarting" - } - ], - "dependencies": { - "node-domexception": "^1.0.0", - "web-streams-polyfill": "^3.0.3" - }, - "engines": { - "node": "^12.20 || >= 14.13" - } - }, "node_modules/node-int64": { "version": "0.4.0", "resolved": "https://registry.npmjs.org/node-int64/-/node-int64-0.4.0.tgz", @@ -8390,26 +8368,13 @@ "dev": true }, "@types/node-fetch": { - "version": "2.6.3", - "resolved": "https://registry.npmjs.org/@types/node-fetch/-/node-fetch-2.6.3.tgz", - "integrity": "sha512-ETTL1mOEdq/sxUtgtOhKjyB2Irra4cjxksvcMUR5Zr4n+PxVhsCD9WS46oPbHL3et9Zde7CNRr+WUNlcHvsX+w==", + "version": "2.6.4", + "resolved": "https://registry.npmjs.org/@types/node-fetch/-/node-fetch-2.6.4.tgz", + "integrity": "sha512-1ZX9fcN4Rvkvgv4E6PAY5WXUFWFcRWxZa3EW83UjycOB9ljJCedb2CupIP4RZMEwF/M3eTcCihbBRgwtGbg5Rg==", "dev": true, "requires": { "@types/node": "*", "form-data": "^3.0.0" - }, - "dependencies": { - "form-data": { - "version": "3.0.1", - "resolved": "https://registry.npmjs.org/form-data/-/form-data-3.0.1.tgz", - "integrity": "sha512-RHkBKtLWUVwd7SqRIvCZMEvAMoGUp0XU+seQiZejj0COz3RI3hWP4sCv3gZWWLjJTd7rGwcsF5eKZGii0r/hbg==", - "dev": true, - "requires": { - "asynckit": "^0.4.0", - "combined-stream": "^1.0.8", - "mime-types": "^2.1.12" - } - } } }, "@types/normalize-package-data": { @@ -9758,6 +9723,15 @@ "bser": "2.1.1" } }, + "fetch-blob": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/fetch-blob/-/fetch-blob-3.2.0.tgz", + "integrity": "sha512-7yAQpD2UMJzLi1Dqv7qFYnPbaPx7ZfFK6PiIxQ4PfkGPyNyl2Ugx+a/umUonmKqjhM4DnfbMvdX6otXq83soQQ==", + "requires": { + "node-domexception": "^1.0.0", + "web-streams-polyfill": "^3.0.3" + } + }, "file-entry-cache": { "version": "6.0.1", "resolved": "https://registry.npmjs.org/file-entry-cache/-/file-entry-cache-6.0.1.tgz", @@ -9821,23 +9795,23 @@ "signal-exit": "^3.0.2" } }, + "form-data": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/form-data/-/form-data-3.0.1.tgz", + "integrity": "sha512-RHkBKtLWUVwd7SqRIvCZMEvAMoGUp0XU+seQiZejj0COz3RI3hWP4sCv3gZWWLjJTd7rGwcsF5eKZGii0r/hbg==", + "dev": true, + "requires": { + "asynckit": "^0.4.0", + "combined-stream": "^1.0.8", + "mime-types": "^2.1.12" + } + }, "formdata-polyfill": { "version": "4.0.10", "resolved": "https://registry.npmjs.org/formdata-polyfill/-/formdata-polyfill-4.0.10.tgz", "integrity": "sha512-buewHzMvYL29jdeQTVILecSaZKnt/RJWjoZCF5OW60Z67/GmSLBkOFM7qh1PI3zFNtJbaZL5eQu1vLfazOwj4g==", "requires": { "fetch-blob": "^3.1.2" - }, - "dependencies": { - "fetch-blob": { - "version": "3.2.0", - "resolved": "https://registry.npmjs.org/fetch-blob/-/fetch-blob-3.2.0.tgz", - "integrity": "sha512-7yAQpD2UMJzLi1Dqv7qFYnPbaPx7ZfFK6PiIxQ4PfkGPyNyl2Ugx+a/umUonmKqjhM4DnfbMvdX6otXq83soQQ==", - "requires": { - "node-domexception": "^1.0.0", - "web-streams-polyfill": "^3.0.3" - } - } } }, "fs.realpath": { @@ -11305,24 +11279,13 @@ "integrity": "sha512-/jKZoMpw0F8GRwl4/eLROPA3cfcXtLApP0QzLmUT/HuPCZWyB7IY9ZrMeKw2O/nFIqPQB3PVM9aYm0F312AXDQ==" }, "node-fetch": { - "version": "3.3.1", - "resolved": "https://registry.npmjs.org/node-fetch/-/node-fetch-3.3.1.tgz", - "integrity": "sha512-cRVc/kyto/7E5shrWca1Wsea4y6tL9iYJE5FBCius3JQfb/4P4I295PfhgbJQBLTx6lATE4z+wK0rPM4VS2uow==", + "version": "3.3.2", + "resolved": "https://registry.npmjs.org/node-fetch/-/node-fetch-3.3.2.tgz", + "integrity": "sha512-dRB78srN/l6gqWulah9SrxeYnxeddIG30+GOqK/9OlLVyLg3HPnr6SqOWTWOXKRwC2eGYCkZ59NNuSgvSrpgOA==", "requires": { "data-uri-to-buffer": "^4.0.0", "fetch-blob": "^3.1.4", "formdata-polyfill": "^4.0.10" - }, - "dependencies": { - "fetch-blob": { - "version": "3.2.0", - "resolved": "https://registry.npmjs.org/fetch-blob/-/fetch-blob-3.2.0.tgz", - "integrity": "sha512-7yAQpD2UMJzLi1Dqv7qFYnPbaPx7ZfFK6PiIxQ4PfkGPyNyl2Ugx+a/umUonmKqjhM4DnfbMvdX6otXq83soQQ==", - "requires": { - "node-domexception": "^1.0.0", - "web-streams-polyfill": "^3.0.3" - } - } } }, "node-int64": { diff --git a/package.json b/package.json index f3b0c8e12..21f2fa2ba 100644 --- a/package.json +++ b/package.json @@ -50,7 +50,6 @@ "homepage": "https://github.com/auth0/node-auth0", "dependencies": { "jose": "^4.13.2", - "node-fetch": "^3.3.1", "uuid": "^9.0.0" }, "devDependencies": { @@ -72,6 +71,7 @@ "husky": "^3.0.1", "jest": "^29.5.0", "nock": "^13.2.7", + "node-fetch": "^3.3.1", "prettier": "^2.8.7", "pretty-quick": "^1.11.1", "ts-jest": "^29.1.0", diff --git a/playground/handlers.ts b/playground/handlers.ts index f26bf6ac2..ec8699453 100644 --- a/playground/handlers.ts +++ b/playground/handlers.ts @@ -731,7 +731,7 @@ export async function jobs() { const ids = []; const { data: createImportJob } = await mgmntClient.jobs.importUsers({ - users: fs.createReadStream(usersFilePath), + users: new Blob([fs.readFileSync(usersFilePath)], { type: 'application/json' }), connection_id: connection.id as string, }); diff --git a/src/lib/runtime.ts b/src/lib/runtime.ts index 2abcb4139..46573b862 100644 --- a/src/lib/runtime.ts +++ b/src/lib/runtime.ts @@ -1,4 +1,3 @@ -import type { ReadStream } from 'fs'; import { retry } from './retry.js'; import { FetchError, RequiredError, TimeoutError } from './errors.js'; import { @@ -10,17 +9,6 @@ import { FetchAPI, } from './models.js'; -/** - * @private - */ -const nodeFetch = (...args: Parameters) => - import('node-fetch').then(({ default: fetch }) => (fetch as FetchAPI)(...args)); - -/** - * @private - */ -export const getFormDataCls = async () => import('node-fetch').then(({ FormData }) => FormData); - export * from './models.js'; /** @@ -43,7 +31,7 @@ export class BaseAPI { } this.middleware = configuration.middleware || []; - this.fetchApi = configuration.fetchApi || nodeFetch; + this.fetchApi = configuration.fetchApi || fetch; this.parseError = configuration.parseError; this.timeoutDuration = typeof configuration.timeoutDuration === 'number' ? configuration.timeoutDuration : 10000; @@ -96,11 +84,10 @@ export class BaseAPI { })), }; - const { Blob } = await import('node-fetch'); const init: RequestInit = { ...overriddenInit, body: - (await isFormData(overriddenInit.body)) || + overriddenInit.body instanceof FormData || overriddenInit.body instanceof URLSearchParams || overriddenInit.body instanceof Blob ? overriddenInit.body @@ -184,11 +171,6 @@ export class BaseAPI { }; } -async function isFormData(value: unknown): Promise { - const FormData = await getFormDataCls(); - return typeof FormData !== 'undefined' && value instanceof FormData; -} - /** * @private */ @@ -312,17 +294,9 @@ export function applyQueryParams< * @private */ export async function parseFormParam( - originalValue: number | boolean | string | Blob | ReadStream + originalValue: number | boolean | string | Blob ): Promise { let value = originalValue; value = typeof value == 'number' || typeof value == 'boolean' ? '' + value : value; - if ( - typeof originalValue === 'object' && - 'path' in originalValue && - typeof originalValue.path === 'string' - ) { - const { fileFrom } = await import('node-fetch'); - value = await fileFrom(originalValue.path, 'application/json'); - } return value as string | Blob; } diff --git a/src/management/__generated/managers/jobs-manager.ts b/src/management/__generated/managers/jobs-manager.ts index 03ace761c..f1dc6a839 100644 --- a/src/management/__generated/managers/jobs-manager.ts +++ b/src/management/__generated/managers/jobs-manager.ts @@ -99,7 +99,7 @@ export class JobsManager extends BaseAPI { bodyParameters: PostUsersImportsData, initOverrides?: InitOverride ): Promise> { - const formParams = new (await runtime.getFormDataCls())(); + const formParams = new FormData(); if (bodyParameters.users !== undefined) { formParams.append('users', await runtime.parseFormParam(bodyParameters.users)); diff --git a/src/management/__generated/models/index.ts b/src/management/__generated/models/index.ts index 3139491a2..57bc9d98b 100644 --- a/src/management/__generated/models/index.ts +++ b/src/management/__generated/models/index.ts @@ -1,5 +1,3 @@ -import type { ReadStream } from 'fs'; - /** * */ @@ -11963,7 +11961,7 @@ export interface GetJobsByIdRequest { export interface PostUsersImportsData { /** */ - users: Blob | ReadStream; + users: Blob; /** * connection_id of the connection to which users will be imported. * diff --git a/test/lib/runtime.test.ts b/test/lib/runtime.test.ts index 519eae45b..495647212 100644 --- a/test/lib/runtime.test.ts +++ b/test/lib/runtime.test.ts @@ -11,7 +11,6 @@ import { } from '../../src/index.js'; import { InitOverrideFunction, RequestOpts } from '../../src/lib/models.js'; import { BaseAPI, applyQueryParams } from '../../src/lib/runtime.js'; -import { Response as NodeResponse } from 'node-fetch'; import * as utils from '../../src/utils.js'; import { base64url } from 'jose'; @@ -296,7 +295,7 @@ describe('Runtime', () => { middleware: [ { onError() { - return new NodeResponse(undefined, { status: 418 }) as Response; + return new Response(undefined, { status: 418 }) as Response; }, }, ], @@ -320,7 +319,7 @@ describe('Runtime', () => { middleware: [ { post() { - return new NodeResponse(JSON.stringify({ bar: 'foo' }), { + return new Response(JSON.stringify({ bar: 'foo' }), { status: 200, }) as Response; }, diff --git a/test/management/jobs.test.ts b/test/management/jobs.test.ts index 8706c9822..825f94ea8 100644 --- a/test/management/jobs.test.ts +++ b/test/management/jobs.test.ts @@ -174,9 +174,9 @@ describe('JobsManager', () => { let data: PostUsersImportsData; let request: nock.Scope; - beforeEach(() => { + beforeEach(async () => { data = { - users: fs.createReadStream(usersFilePath) as any, + users: new Blob([fs.readFileSync(usersFilePath)], { type: 'application/json' }), connection_id: 'con_test', }; request = nock(API_URL).post('/jobs/users-imports').reply(200, {}); @@ -384,15 +384,15 @@ describe('JobsManager', () => { describe('#importUsers with JSON data', () => { let data: PostUsersImportsData; - beforeEach(() => { + beforeEach(async () => { data = { - users: fs.createReadStream(usersFilePath) as any, + users: new Blob([fs.readFileSync(usersFilePath)], { type: 'application/json' }), connection_id: 'con_test', }; nock(API_URL).post('/jobs/users-imports').reply(200, {}); }); - it('should correctly include user JSON from ReadStream', (done) => { + it('should correctly include user JSON from Blob', (done) => { nock.cleanAll(); let boundary: string | null = null; @@ -423,46 +423,41 @@ describe('JobsManager', () => { }); }); - (typeof Blob === 'undefined' ? it.skip : it)( - 'should correctly include user JSON from Blob', - function (done) { - nock.cleanAll(); - let boundary: string | null = null; - - const request = nock(API_URL) - .matchHeader('Content-Type', (header) => { - boundary = `--${header.match(/boundary=([^\n]*)/)?.[1]}`; - - return true; - }) - .post('/jobs/users-imports', (body) => { - const parts = extractParts(body, boundary); - - expect(parts.users).toContain('Content-Type: application/json'); - - // Validate the content of the users JSON. - const users = JSON.parse(parts.users.split('\r\n').slice(-1)[0]); - expect(users.length).toBe(2); - expect(users[0].email).toBe('jane.doe@contoso.com'); - - return true; - }) - .reply(200, {}); - - jobs - .importUsers({ - ...data, - users: new Blob([fs.readFileSync(usersFilePath).toString()], { - type: 'application/json', - }), - }) - .then(() => { - expect(request.isDone()).toBe(true); - - done(); - }); - } - ); + it('should correctly include user JSON from Blob', async function () { + nock.cleanAll(); + let boundary: string | null = null; + + const request = nock(API_URL) + .matchHeader('Content-Type', (header) => { + boundary = `--${header.match(/boundary=([^\n]*)/)?.[1]}`; + + return true; + }) + .post('/jobs/users-imports', (body) => { + const parts = extractParts(body, boundary); + + expect(parts.users).toContain('Content-Type: application/json'); + + // Validate the content of the users JSON. + const users = JSON.parse(parts.users.split('\r\n').slice(-1)[0]); + expect(users.length).toBe(2); + expect(users[0].email).toBe('jane.doe@contoso.com'); + + return true; + }) + .reply(200, {}); + + await jobs + .importUsers({ + ...data, + users: new Blob([fs.readFileSync(usersFilePath)], { + type: 'application/json', + }), + }) + .then(() => { + expect(request.isDone()).toBe(true); + }); + }); }); describe('#exportUsers', () => { diff --git a/test/setup.ts b/test/setup.ts index 81d69886b..9eec5dd17 100644 --- a/test/setup.ts +++ b/test/setup.ts @@ -1,7 +1,11 @@ import path from 'path'; import { fileURLToPath } from 'url'; import nock from 'nock'; +import fetch from 'node-fetch'; const { back: nockBack } = nock; nockBack.fixtures = path.dirname(fileURLToPath(import.meta.url)); + +// Falling back to node-fetch because nock doesn't work with native fetch. +globalThis.fetch = fetch as any; diff --git a/v4_MIGRATION_GUIDE.md b/v4_MIGRATION_GUIDE.md index 0de208da1..a980892ba 100644 --- a/v4_MIGRATION_GUIDE.md +++ b/v4_MIGRATION_GUIDE.md @@ -356,3 +356,34 @@ await users.deleteRoles({ id: 'user' }, { roles: ['read:users'] }); | `organizations.removeMemberRoles` | `organizations.deleteMemberRoles` | + +### Import users now takes a Blob + +#### Before + +```js +await management.jobs.importUsers({ + users: fs.createReadStream('./myusers.json'), + connection_id: 'con_123', +}); +``` + +#### After + +```js +await management.jobs.importUsers({ + users: new Blob([fs.readFileSync('./myusers.json')], { type: 'application/json' }), + connection_id: 'con_123', +}); +``` + +If you don't want to read the whole file into memory, you can use a library like [fetch-blob](https://github.com/node-fetch/fetch-blob). + +```js +import { fileFrom } from 'fetch-blob/from.js'; + +await management.jobs.importUsers({ + users: await fileFrom('./myusers.json', 'application/json'), + connection_id: 'con_123', +}); +```