From a6d48c512e2f95ef6bd18dd16c735696cc820503 Mon Sep 17 00:00:00 2001 From: Remco Haszing Date: Mon, 12 Aug 2024 11:11:09 +0200 Subject: [PATCH 01/23] Convert to TypeScript --- .github/workflows/ci.yml | 4 +- .gitignore | 2 + package.json | 18 +- ...eError.js => InconsistentResponseError.ts} | 2 +- ...aginationStream.js => PaginationStream.ts} | 23 +- ...TimeoutError.js => PollingTimeoutError.ts} | 3 +- src/{Transloadit.js => Transloadit.ts} | 512 +++++++++++++----- src/TransloaditError.js | 10 - src/TransloaditError.ts | 13 + src/{tus.js => tus.ts} | 49 +- .../{live-api.test.js => live-api.test.ts} | 186 ++++--- test/{testserver.js => testserver.ts} | 43 +- test/{tunnel.js => tunnel.ts} | 50 +- .../{mock-http.test.js => mock-http.test.ts} | 30 +- ...test.js => test-pagination-stream.test.ts} | 35 +- ...est.js => test-transloadit-client.test.ts} | 141 +++-- tsconfig.build.json | 15 + tsconfig.json | 12 + types/index.d.ts | 223 -------- types/index.test-d.ts | 98 ---- vitest.config.mjs | 4 + yarn.lock | 481 ++-------------- 22 files changed, 850 insertions(+), 1104 deletions(-) rename src/{InconsistentResponseError.js => InconsistentResponseError.ts} (67%) rename src/{PaginationStream.js => PaginationStream.ts} (55%) rename src/{PollingTimeoutError.js => PollingTimeoutError.ts} (73%) rename src/{Transloadit.js => Transloadit.ts} (56%) delete mode 100644 src/TransloaditError.js create mode 100644 src/TransloaditError.ts rename src/{tus.js => tus.ts} (70%) rename test/integration/{live-api.test.js => live-api.test.ts} (82%) rename test/{testserver.js => testserver.ts} (71%) rename test/{tunnel.js => tunnel.ts} (75%) rename test/unit/{mock-http.test.js => mock-http.test.ts} (90%) rename test/unit/{test-pagination-stream.test.js => test-pagination-stream.test.ts} (55%) rename test/unit/{test-transloadit-client.test.js => test-transloadit-client.test.ts} (67%) create mode 100644 tsconfig.build.json create mode 100644 tsconfig.json delete mode 100644 types/index.d.ts delete mode 100644 types/index.test-d.ts diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 4c8f7e7..89fba58 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -27,7 +27,7 @@ jobs: - run: corepack yarn - run: corepack yarn prettier --check . - tsd: + typescript: runs-on: ubuntu-latest steps: - uses: actions/checkout@v4 @@ -35,7 +35,7 @@ jobs: with: node-version: 22 - run: corepack yarn - - run: corepack yarn tsd + - run: corepack yarn tsc --build vitest: runs-on: ubuntu-latest diff --git a/.gitignore b/.gitignore index 33be172..d6fc5fa 100644 --- a/.gitignore +++ b/.gitignore @@ -1,8 +1,10 @@ +dist/ node_modules cloudflared* credentials.js sample.js +*.tsbuildinfo npm-debug.log env.sh /coverage diff --git a/package.json b/package.json index dba68ed..2d69bdf 100644 --- a/package.json +++ b/package.json @@ -28,6 +28,8 @@ "@babel/core": "^7.12.3", "@babel/eslint-parser": "^7.15.8", "@babel/eslint-plugin": "^7.13.10", + "@types/debug": "^4.1.12", + "@types/temp": "^0.9.4", "@vitest/coverage-v8": "^2.0.5", "badge-maker": "^3.3.0", "eslint": "^7.18.0", @@ -46,7 +48,7 @@ "p-retry": "^4.2.0", "prettier": "^2.8.6", "temp": "^0.9.1", - "tsd": "^0.25.0", + "typescript": "^5.5.4", "vitest": "^2.0.5" }, "repository": { @@ -62,20 +64,22 @@ "lint": "npm-run-all --parallel 'lint:*'", "fix": "npm-run-all --serial 'fix:*'", "next:update": "next-update --keep true --tldr", + "prepack": "tsc --build", "test-unit": "vitest run --coverage ./test/unit", "test-integration": "vitest run ./test/integration", - "tsd": "tsd", "test-all": "npm run tsd && vitest run --coverage", "test": "npm run tsd && npm run test-unit", "fix:formatting": "prettier --write .", "lint:formatting": "prettier --check ." }, "license": "MIT", - "main": "./index", - "types": "types/index.d.ts", + "main": "./dist/Transloadit.js", + "exports": { + ".": "./dist/Transloadit.js", + "./package.json": "./package.json" + }, "files": [ - "index.js", - "src", - "types/index.d.ts" + "dist", + "src" ] } diff --git a/src/InconsistentResponseError.js b/src/InconsistentResponseError.ts similarity index 67% rename from src/InconsistentResponseError.js rename to src/InconsistentResponseError.ts index b9dea30..0d0646b 100644 --- a/src/InconsistentResponseError.js +++ b/src/InconsistentResponseError.ts @@ -2,4 +2,4 @@ class InconsistentResponseError extends Error { name = 'InconsistentResponseError' } -module.exports = InconsistentResponseError +export = InconsistentResponseError diff --git a/src/PaginationStream.js b/src/PaginationStream.ts similarity index 55% rename from src/PaginationStream.js rename to src/PaginationStream.ts index b614e61..c04a62f 100644 --- a/src/PaginationStream.js +++ b/src/PaginationStream.ts @@ -1,12 +1,18 @@ -const stream = require('stream') +import stream = require('stream') +import type { PaginationList } from './Transloadit' -class PaginationStream extends stream.Readable { - constructor(_fetchPage) { +type FetchPage = (pageno: number) => PaginationList | PromiseLike> + +class PaginationStream extends stream.Readable { + private _fetchPage: FetchPage + private _nitems?: number + private _pageno = 0 + private _items: T[] = [] + private _itemsRead = 0 + + constructor(fetchPage: FetchPage) { super({ objectMode: true }) - this._fetchPage = _fetchPage - this._pageno = 0 - this._items = [] - this._itemsRead = 0 + this._fetchPage = fetchPage } async _read() { @@ -29,11 +35,10 @@ class PaginationStream extends stream.Readable { this._items.reverse() this._read() - return } catch (err) { this.emit('error', err) } } } -module.exports = PaginationStream +export = PaginationStream diff --git a/src/PollingTimeoutError.js b/src/PollingTimeoutError.ts similarity index 73% rename from src/PollingTimeoutError.js rename to src/PollingTimeoutError.ts index 1a497ff..5ba2adb 100644 --- a/src/PollingTimeoutError.js +++ b/src/PollingTimeoutError.ts @@ -1,7 +1,6 @@ class PollingTimeoutError extends Error { name = 'PollingTimeoutError' - code = 'POLLING_TIMED_OUT' } -module.exports = PollingTimeoutError +export = PollingTimeoutError diff --git a/src/Transloadit.js b/src/Transloadit.ts similarity index 56% rename from src/Transloadit.js rename to src/Transloadit.ts index 84a7ec2..90dea2c 100644 --- a/src/Transloadit.js +++ b/src/Transloadit.ts @@ -1,25 +1,40 @@ -const crypto = require('crypto') -const got = require('got') -const FormData = require('form-data') -const fs = require('fs') -const fsPromises = require('fs/promises') -const debug = require('debug') -const intoStream = require('into-stream') -const isStream = require('is-stream') -const assert = require('assert') -const pMap = require('p-map') - -const InconsistentResponseError = require('./InconsistentResponseError') -const PaginationStream = require('./PaginationStream') -const PollingTimeoutError = require('./PollingTimeoutError') -const TransloaditError = require('./TransloaditError') -const pkg = require('../package.json') -const tus = require('./tus') +import crypto = require('crypto') +import got = require('got') +import FormData = require('form-data') +import fs = require('fs') +import fsPromises = require('fs/promises') +import debug = require('debug') +import intoStream = require('into-stream') +import isStream = require('is-stream') +import assert = require('assert') +import pMap = require('p-map') +import InconsistentResponseError = require('./InconsistentResponseError') +import PaginationStream = require('./PaginationStream') +import PollingTimeoutError = require('./PollingTimeoutError') +import TransloaditError = require('./TransloaditError') +import pkg = require('../package.json') +import tus = require('./tus') + +import type { Readable } from 'stream' const log = debug('transloadit') const logWarn = debug('transloadit:warn') -function decorateHttpError(err, body) { +interface RequestOptions { + urlSuffix?: string + url?: string + timeout?: number + method?: 'delete' | 'get' | 'post' | 'put' + params?: TransloaditClient.KeyVal + fields?: Record + headers?: got.Headers +} + +interface CreateAssemblyPromise extends Promise { + assemblyId: string +} + +function decorateHttpError(err: TransloaditError, body: any): TransloaditError { if (!body) return err let newMessage = err.message @@ -48,20 +63,25 @@ function decorateHttpError(err, body) { } // Not sure if this is still a problem with the API, but throw a special error type so the user can retry if needed -function checkAssemblyUrls(result) { +function checkAssemblyUrls(result: TransloaditClient.Assembly) { if (result.assembly_url == null || result.assembly_ssl_url == null) { throw new InconsistentResponseError('Server returned an incomplete assembly response (no URL)') } } -function getHrTimeMs() { +function getHrTimeMs(): number { return Number(process.hrtime.bigint() / 1000000n) } -function checkResult(result) { +function checkResult(result: T | { error: string }): asserts result is T { // In case server returned a successful HTTP status code, but an `error` in the JSON object // This happens sometimes when createAssembly with an invalid file (IMPORT_FILE_ERROR) - if (typeof result === 'object' && result !== null && typeof result.error === 'string') { + if ( + typeof result === 'object' && + result !== null && + 'error' in result && + typeof result.error === 'string' + ) { throw decorateHttpError(new TransloaditError('Error in response', result), result) } } @@ -85,8 +105,16 @@ class TransloaditClient { static InconsistentResponseError = InconsistentResponseError - constructor(opts = {}) { - if (opts.authKey == null) { + private _authKey: string + private _authSecret: string + private _endpoint: string + private _maxRetries: number + private _defaultTimeout: number + private _gotRetry: got.RequiredRetryOptions | number + private _lastUsedAssemblyUrl = '' + + constructor(opts: TransloaditClient.TransloaditClientOptions) { + if (opts?.authKey == null) { throw new Error('Please provide an authKey') } @@ -106,25 +134,25 @@ class TransloaditClient { // Passed on to got https://github.com/sindresorhus/got/blob/main/documentation/7-retry.md this._gotRetry = opts.gotRetry != null ? opts.gotRetry : 0 - - this._lastUsedAssemblyUrl = '' } - getLastUsedAssemblyUrl() { + getLastUsedAssemblyUrl(): string { return this._lastUsedAssemblyUrl } - setDefaultTimeout(timeout) { + setDefaultTimeout(timeout: number): void { this._defaultTimeout = timeout } /** * Create an Assembly * - * @param {object} opts assembly options - * @returns {Promise} + * @param opts assembly options */ - createAssembly(opts = {}, arg2) { + createAssembly( + opts: TransloaditClient.CreateAssemblyOptions = {}, + arg2?: void + ): CreateAssemblyPromise { // Warn users of old callback API if (typeof arg2 === 'function') { throw new TypeError( @@ -192,7 +220,7 @@ class TransloaditClient { ) // Wrap in object structure (so we can know if it's a pathless stream or not) - const allStreamsMap = Object.fromEntries( + const allStreamsMap = Object.fromEntries( Object.entries(streamsMap).map(([label, stream]) => [label, { stream }]) ) @@ -208,12 +236,12 @@ class TransloaditClient { allStreams.forEach(({ stream }) => stream.pause()) // If any stream emits error, we want to handle this and exit with error - const streamErrorPromise = new Promise((resolve, reject) => { + const streamErrorPromise = new Promise((resolve, reject) => { allStreams.forEach(({ stream }) => stream.on('error', reject)) }) const createAssemblyAndUpload = async () => { - const requestOpts = { + const requestOpts: RequestOptions = { urlSuffix, method: 'post', timeout, @@ -227,9 +255,13 @@ class TransloaditClient { } // upload as form multipart or tus? - const formUploadStreamsMap = isResumable ? {} : allStreamsMap + const formUploadStreamsMap: Record = isResumable ? {} : allStreamsMap - const result = await this._remoteJson(requestOpts, formUploadStreamsMap, onUploadProgress) + const result = await this._remoteJson( + requestOpts, + formUploadStreamsMap, + onUploadProgress + ) checkResult(result) if (isResumable && Object.keys(allStreamsMap).length > 0) { @@ -253,7 +285,7 @@ class TransloaditClient { } return Promise.race([createAssemblyAndUpload(), streamErrorPromise]) - })() + })() as CreateAssemblyPromise // This allows the user to use or log the assemblyId even before it has been created for easier debugging promise.assemblyId = effectiveAssemblyId @@ -261,16 +293,25 @@ class TransloaditClient { } async awaitAssemblyCompletion( - assemblyId, - { onAssemblyProgress = () => {}, timeout, startTimeMs = getHrTimeMs(), interval = 1000 } = {} - ) { + assemblyId: string, + { + onAssemblyProgress = () => {}, + timeout, + startTimeMs = getHrTimeMs(), + interval = 1000, + }: TransloaditClient.AwaitAssemblyCompletionOptions = {} + ): Promise { assert(assemblyId) // eslint-disable-next-line no-constant-condition while (true) { const result = await this.getAssembly(assemblyId) - if (!['ASSEMBLY_UPLOADING', 'ASSEMBLY_EXECUTING', 'ASSEMBLY_REPLAYING'].includes(result.ok)) { + if ( + result.ok !== 'ASSEMBLY_UPLOADING' && + result.ok !== 'ASSEMBLY_EXECUTING' && + result.ok !== 'ASSEMBLY_REPLAYING' + ) { return result // Done! } @@ -291,16 +332,16 @@ class TransloaditClient { /** * Cancel the assembly * - * @param {string} assemblyId assembly ID - * @returns {Promise} after the assembly is deleted + * @param assemblyId assembly ID + * @returns after the assembly is deleted */ - async cancelAssembly(assemblyId) { + async cancelAssembly(assemblyId: string): Promise { // You may wonder why do we need to call getAssembly first: // If we use the default base URL (instead of the one returned in assembly_url_ssl), // the delete call will hang in certain cases // See test "should stop the assembly from reaching completion" const { assembly_ssl_url: url } = await this.getAssembly(assemblyId) - const opts = { + const opts: RequestOptions = { url, // urlSuffix: `/assemblies/${assemblyId}`, // Cannot simply do this, see above method: 'delete', @@ -312,17 +353,20 @@ class TransloaditClient { /** * Replay an Assembly * - * @param {string} assemblyId of the assembly to replay - * @param {object} optional params - * @returns {Promise} after the replay is started + * @param assemblyId of the assembly to replay + * @param optional params + * @returns after the replay is started */ - async replayAssembly(assemblyId, params = {}) { - const requestOpts = { + async replayAssembly( + assemblyId: string, + params: TransloaditClient.KeyVal = {} + ): Promise { + const requestOpts: RequestOptions = { urlSuffix: `/assemblies/${assemblyId}/replay`, method: 'post', } if (Object.keys(params).length > 0) requestOpts.params = params - const result = await this._remoteJson(requestOpts) + const result = await this._remoteJson(requestOpts) checkResult(result) return result } @@ -330,12 +374,15 @@ class TransloaditClient { /** * Replay an Assembly notification * - * @param {string} assemblyId of the assembly whose notification to replay - * @param {object} optional params - * @returns {Promise} after the replay is started + * @param assemblyId of the assembly whose notification to replay + * @param optional params + * @returns after the replay is started */ - async replayAssemblyNotification(assemblyId, params = {}) { - const requestOpts = { + async replayAssemblyNotification( + assemblyId: string, + params: TransloaditClient.KeyVal = {} + ): Promise<{ ok: string; success: boolean }> { + const requestOpts: RequestOptions = { urlSuffix: `/assembly_notifications/${assemblyId}/replay`, method: 'post', } @@ -346,11 +393,13 @@ class TransloaditClient { /** * List all assembly notifications * - * @param {object} params optional request options - * @returns {Promise} the list of Assembly notifications + * @param params optional request options + * @returns the list of Assembly notifications */ - async listAssemblyNotifications(params) { - const requestOpts = { + async listAssemblyNotifications( + params: object + ): Promise> { + const requestOpts: RequestOptions = { urlSuffix: '/assembly_notifications', method: 'get', params: params || {}, @@ -359,18 +408,20 @@ class TransloaditClient { return this._remoteJson(requestOpts) } - streamAssemblyNotifications(params) { + streamAssemblyNotifications(params: object): PaginationStream { return new PaginationStream(async (page) => this.listAssemblyNotifications({ ...params, page })) } /** * List all assemblies * - * @param {object} params optional request options - * @returns {Promise} list of Assemblies + * @param params optional request options + * @returns list of Assemblies */ - async listAssemblies(params) { - const requestOpts = { + async listAssemblies( + params?: TransloaditClient.KeyVal + ): Promise> { + const requestOpts: RequestOptions = { urlSuffix: '/assemblies', method: 'get', params: params || {}, @@ -379,18 +430,20 @@ class TransloaditClient { return this._remoteJson(requestOpts) } - streamAssemblies(params) { + streamAssemblies(params: TransloaditClient.KeyVal): Readable { return new PaginationStream(async (page) => this.listAssemblies({ ...params, page })) } /** * Get an Assembly * - * @param {string} assemblyId the Assembly Id - * @returns {Promise} the retrieved Assembly + * @param assemblyId the Assembly Id + * @returns the retrieved Assembly */ - async getAssembly(assemblyId) { - const result = await this._remoteJson({ urlSuffix: `/assemblies/${assemblyId}` }) + async getAssembly(assemblyId: string): Promise { + const result = await this._remoteJson({ + urlSuffix: `/assemblies/${assemblyId}`, + }) checkAssemblyUrls(result) return result } @@ -398,11 +451,11 @@ class TransloaditClient { /** * Create a Credential * - * @param {object} params optional request options - * @returns {Promise} when the Credential is created + * @param params optional request options + * @returns when the Credential is created */ - async createTemplateCredential(params) { - const requestOpts = { + async createTemplateCredential(params: object): Promise { + const requestOpts: RequestOptions = { urlSuffix: '/template_credentials', method: 'post', params: params || {}, @@ -414,12 +467,12 @@ class TransloaditClient { /** * Edit a Credential * - * @param {string} credentialId the Credential ID - * @param {object} params optional request options - * @returns {Promise} when the Credential is edited + * @param credentialId the Credential ID + * @param params optional request options + * @returns when the Credential is edited */ - async editTemplateCredential(credentialId, params) { - const requestOpts = { + async editTemplateCredential(credentialId: string, params: object): Promise { + const requestOpts: RequestOptions = { urlSuffix: `/template_credentials/${credentialId}`, method: 'put', params: params || {}, @@ -431,11 +484,11 @@ class TransloaditClient { /** * Delete a Credential * - * @param {string} credentialId the Credential ID - * @returns {Promise} when the Credential is deleted + * @param credentialId the Credential ID + * @returns when the Credential is deleted */ - async deleteTemplateCredential(credentialId) { - const requestOpts = { + async deleteTemplateCredential(credentialId: string): Promise { + const requestOpts: RequestOptions = { urlSuffix: `/template_credentials/${credentialId}`, method: 'delete', } @@ -446,11 +499,11 @@ class TransloaditClient { /** * Get a Credential * - * @param {string} credentialId the Credential ID - * @returns {Promise} when the Credential is retrieved + * @param credentialId the Credential ID + * @returns when the Credential is retrieved */ - async getTemplateCredential(credentialId) { - const requestOpts = { + async getTemplateCredential(credentialId: string): Promise { + const requestOpts: RequestOptions = { urlSuffix: `/template_credentials/${credentialId}`, method: 'get', } @@ -461,11 +514,11 @@ class TransloaditClient { /** * List all TemplateCredentials * - * @param {object} params optional request options - * @returns {Promise} the list of templates + * @param params optional request options + * @returns the list of templates */ - async listTemplateCredentials(params) { - const requestOpts = { + async listTemplateCredentials(params?: object): Promise { + const requestOpts: RequestOptions = { urlSuffix: '/template_credentials', method: 'get', params: params || {}, @@ -474,18 +527,20 @@ class TransloaditClient { return this._remoteJson(requestOpts) } - streamTemplateCredentials(params) { + streamTemplateCredentials(params: object): PaginationStream { return new PaginationStream(async (page) => this.listTemplateCredentials({ ...params, page })) } /** * Create an Assembly Template * - * @param {object} params optional request options - * @returns {Promise} when the template is created + * @param params optional request options + * @returns when the template is created */ - async createTemplate(params) { - const requestOpts = { + async createTemplate( + params: TransloaditClient.KeyVal = {} + ): Promise { + const requestOpts: RequestOptions = { urlSuffix: '/templates', method: 'post', params: params || {}, @@ -497,12 +552,15 @@ class TransloaditClient { /** * Edit an Assembly Template * - * @param {string} templateId the template ID - * @param {object} params optional request options - * @returns {Promise} when the template is edited + * @param templateId the template ID + * @param params optional request options + * @returns when the template is edited */ - async editTemplate(templateId, params) { - const requestOpts = { + async editTemplate( + templateId: string, + params: TransloaditClient.KeyVal + ): Promise { + const requestOpts: RequestOptions = { urlSuffix: `/templates/${templateId}`, method: 'put', params: params || {}, @@ -514,11 +572,11 @@ class TransloaditClient { /** * Delete an Assembly Template * - * @param {string} templateId the template ID - * @returns {Promise} when the template is deleted + * @param templateId the template ID + * @returns when the template is deleted */ - async deleteTemplate(templateId) { - const requestOpts = { + async deleteTemplate(templateId: string): Promise<{ ok: string; message: string }> { + const requestOpts: RequestOptions = { urlSuffix: `/templates/${templateId}`, method: 'delete', } @@ -529,11 +587,11 @@ class TransloaditClient { /** * Get an Assembly Template * - * @param {string} templateId the template ID - * @returns {Promise} when the template is retrieved + * @param templateId the template ID + * @returns when the template is retrieved */ - async getTemplate(templateId) { - const requestOpts = { + async getTemplate(templateId: string): Promise { + const requestOpts: RequestOptions = { urlSuffix: `/templates/${templateId}`, method: 'get', } @@ -544,11 +602,13 @@ class TransloaditClient { /** * List all Assembly Templates * - * @param {object} params optional request options - * @returns {Promise} the list of templates + * @param params optional request options + * @returns the list of templates */ - async listTemplates(params) { - const requestOpts = { + async listTemplates( + params?: TransloaditClient.KeyVal + ): Promise> { + const requestOpts: RequestOptions = { urlSuffix: '/templates', method: 'get', params: params || {}, @@ -557,19 +617,22 @@ class TransloaditClient { return this._remoteJson(requestOpts) } - streamTemplates(params) { + streamTemplates( + params?: TransloaditClient.KeyVal + ): PaginationStream { return new PaginationStream(async (page) => this.listTemplates({ ...params, page })) } /** * Get account Billing details for a specific month * - * @param {string} month the date for the required billing in the format yyyy-mm - * @returns {Promise} with billing data + * @param month the date for the required billing in the format yyyy-mm + * @returns with billing data + * @see https://transloadit.com/docs/api/bill-date-get/ */ - async getBill(month) { + async getBill(month: string): Promise { assert(month, 'month is required') - const requestOpts = { + const requestOpts: RequestOptions = { urlSuffix: `/bill/${month}`, method: 'get', } @@ -577,14 +640,14 @@ class TransloaditClient { return this._remoteJson(requestOpts) } - calcSignature(params) { + calcSignature(params: TransloaditClient.KeyVal): { signature: string; params: string } { const jsonParams = this._prepareParams(params) const signature = this._calcSignature(jsonParams) return { signature, params: jsonParams } } - _calcSignature(toSign, algorithm = 'sha384') { + private _calcSignature(toSign: string, algorithm = 'sha384'): string { return `${algorithm}:${crypto .createHmac(algorithm, this._authSecret) .update(Buffer.from(toSign, 'utf-8')) @@ -593,7 +656,12 @@ class TransloaditClient { // Sets the multipart/form-data for POST, PUT and DELETE requests, including // the streams, the signed params, and any additional fields. - _appendForm(form, params, streamsMap, fields) { + private _appendForm( + form: FormData, + params: TransloaditClient.KeyVal, + streamsMap?: Record, + fields?: Record + ): void { const sigData = this.calcSignature(params) const jsonParams = sigData.params const { signature } = sigData @@ -618,7 +686,7 @@ class TransloaditClient { // Implements HTTP GET query params, handling the case where the url already // has params. - _appendParamsToUrl(url, params) { + private _appendParamsToUrl(url: string, params: TransloaditClient.KeyVal): string { const { signature, params: jsonParams } = this.calcSignature(params) const prefix = url.indexOf('?') === -1 ? '?' : '&' @@ -627,7 +695,7 @@ class TransloaditClient { } // Responsible for including auth parameters in all requests - _prepareParams(paramsIn) { + private _prepareParams(paramsIn: TransloaditClient.KeyVal): string { let params = paramsIn if (params == null) { params = {} @@ -647,7 +715,7 @@ class TransloaditClient { // We want to mock this method // eslint-disable-next-line class-methods-use-this - _getExpiresDate() { + private _getExpiresDate(): string { const expiresDate = new Date() expiresDate.setDate(expiresDate.getDate() + 1) return expiresDate.toISOString() @@ -656,7 +724,11 @@ class TransloaditClient { // Responsible for making API calls. Automatically sends streams with any POST, // PUT or DELETE requests. Automatically adds signature parameters to all // requests. Also automatically parses the JSON response. - async _remoteJson(opts, streamsMap, onProgress = () => {}) { + private async _remoteJson( + opts: RequestOptions, + streamsMap?: Record, + onProgress: TransloaditClient.CreateAssemblyOptions['onUploadProgress'] = () => {} + ): Promise { const { urlSuffix, url: urlInput, @@ -689,7 +761,7 @@ class TransloaditClient { const isUploadingStreams = streamsMap && Object.keys(streamsMap).length > 0 - const requestOpts = { + const requestOpts: got.OptionsOfJSONResponseBody = { retry: this._gotRetry, body: form, timeout, @@ -704,10 +776,10 @@ class TransloaditClient { // For non-file streams transfer encoding does not get set, and the uploaded files will not get accepted // https://github.com/transloadit/node-sdk/issues/86 // https://github.com/form-data/form-data/issues/394#issuecomment-573595015 - if (isUploadingStreams) requestOpts.headers['transfer-encoding'] = 'chunked' + if (isUploadingStreams) requestOpts.headers!['transfer-encoding'] = 'chunked' try { - const request = got[method](url, requestOpts) + const request = got.default[method](url, requestOpts) if (isUploadingStreams) { request.on('uploadProgress', ({ transferred, total }) => onProgress({ uploadedBytes: transferred, totalBytes: total }) @@ -723,15 +795,21 @@ class TransloaditClient { const shouldRetry = statusCode === 413 && + typeof body === 'object' && + body != null && + 'error' in body && body.error === 'RATE_LIMIT_REACHED' && - body.info && - body.info.retryIn && + 'info' in body && + typeof body.info === 'object' && + body.info != null && + 'retryIn' in body.info && + Boolean(body.info.retryIn) && retryCount < this._maxRetries // https://transloadit.com/blog/2012/04/introducing-rate-limiting/ if (!shouldRetry) throw decorateHttpError(err, body) - const { retryIn: retryInSec } = body.info + const { retryIn: retryInSec } = body.info as { retryIn: number } logWarn(`Rate limit reached, retrying request in approximately ${retryInSec} seconds.`) const retryInMs = 1000 * (retryInSec * (1 + 0.1 * Math.random())) await new Promise((resolve) => setTimeout(resolve, retryInMs)) @@ -741,4 +819,174 @@ class TransloaditClient { } } -module.exports = TransloaditClient +namespace TransloaditClient { + export interface CreateAssemblyOptions { + params?: CreateAssemblyParams + files?: { + [name: string]: string + } + uploads?: { + [name: string]: Readable | intoStream.Input + } + waitForCompletion?: boolean + isResumable?: boolean + chunkSize?: number + uploadConcurrency?: number + timeout?: number + onUploadProgress?: (uploadProgress: UploadProgress) => void + onAssemblyProgress?: AssemblyProgress + assemblyId?: string + } + + export type AssemblyProgress = (assembly: Assembly) => void + + export interface CreateAssemblyParams { + /** See https://transloadit.com/docs/topics/assembly-instructions/ */ + steps?: KeyVal + template_id?: string + notify_url?: string + fields?: KeyVal + allow_steps_override?: boolean + } + + // TODO + /** Object with properties. See https://transloadit.com/docs/api/ */ + export interface KeyVal { + [key: string]: any + } + + export interface UploadProgress { + uploadedBytes?: number + totalBytes?: number + } + + /** https://transloadit.com/docs/api/assembly-status-response/#explanation-of-fields */ + export interface Assembly { + ok?: string + message?: string + assembly_id: string + parent_id?: string + account_id: string + template_id?: string + instance: string + assembly_url: string + assembly_ssl_url: string + uppyserver_url: string + companion_url: string + websocket_url: string + tus_url: string + bytes_received: number + bytes_expected: number + upload_duration: number + client_agent?: string + client_ip?: string + client_referer?: string + transloadit_client: string + start_date: string + upload_meta_data_extracted: boolean + warnings: any[] + is_infinite: boolean + has_dupe_jobs: boolean + execution_start: string + execution_duration: number + queue_duration: number + jobs_queue_duration: number + notify_start?: any + notify_url?: string + notify_status?: any + notify_response_code?: any + notify_duration?: any + last_job_completed?: string + fields: KeyVal + running_jobs: any[] + bytes_usage: number + executing_jobs: any[] + started_jobs: string[] + parent_assembly_status: any + params: string + template?: any + merged_params: string + uploads: any[] + results: any + build_id: string + error?: string + stderr?: string + stdout?: string + reason?: string + } + + /** See https://transloadit.com/docs/api/assemblies-assembly-id-get/ */ + export interface ListedAssembly { + id?: string + parent_id?: string + account_id: string + template_id?: string + instance: string + notify_url?: string + redirect_url?: string + files: string + warning_count: number + execution_duration: number + execution_start: string + ok?: string + error?: string + created: string + } + + export interface ReplayedAssembly { + ok?: string + message?: string + success: boolean + assembly_id: string + assembly_url: string + assembly_ssl_url: string + notify_url?: string + } + + export interface ListedTemplate { + id: string + name: string + encryption_version: number + require_signature_auth: number + last_used?: string + created: string + modified: string + content: TemplateContent + } + + export interface TemplateResponse { + ok: string + message: string + id: string + content: TemplateContent + name: string + require_signature_auth: number + } + + export interface TemplateContent { + steps: KeyVal + } + + export interface TransloaditClientOptions { + authKey: string + authSecret: string + endpoint?: string + maxRetries?: number + timeout?: number + gotRetry?: got.RequiredRetryOptions + } + + export interface AwaitAssemblyCompletionOptions { + onAssemblyProgress?: AssemblyProgress + timeout?: number + interval?: number + startTimeMs?: number + } + + export interface PaginationList { + count: number + items: T[] + } +} + +export = TransloaditClient diff --git a/src/TransloaditError.js b/src/TransloaditError.js deleted file mode 100644 index d2b4097..0000000 --- a/src/TransloaditError.js +++ /dev/null @@ -1,10 +0,0 @@ -class TransloaditError extends Error { - name = 'TransloaditError' - - constructor(message, body) { - super(message) - this.response = { body } - } -} - -module.exports = TransloaditError diff --git a/src/TransloaditError.ts b/src/TransloaditError.ts new file mode 100644 index 0000000..cddcb1c --- /dev/null +++ b/src/TransloaditError.ts @@ -0,0 +1,13 @@ +class TransloaditError extends Error { + name = 'TransloaditError' + response: { body: unknown } + assemblyId?: string + transloaditErrorCode?: string + + constructor(message: string, body: unknown) { + super(message) + this.response = { body } + } +} + +export = TransloaditError diff --git a/src/tus.js b/src/tus.ts similarity index 70% rename from src/tus.js rename to src/tus.ts index 1a18ee1..f2be1c7 100644 --- a/src/tus.js +++ b/src/tus.ts @@ -1,24 +1,34 @@ -const debug = require('debug') -const nodePath = require('path') -const tus = require('tus-js-client') -const fsPromises = require('fs/promises') -const pMap = require('p-map') +import debug = require('debug') +import p = require('path') +import tus = require('tus-js-client') +import fsPromises = require('fs/promises') +import pMap = require('p-map') +import type { Readable } from 'stream' +import type { Assembly, UploadProgress } from './Transloadit' const log = debug('transloadit') +interface SendTusRequestOptions { + streamsMap: Record + assembly: Assembly + requestedChunkSize: number + uploadConcurrency: number + onProgress: (options: UploadProgress) => void +} + async function sendTusRequest({ streamsMap, assembly, requestedChunkSize, uploadConcurrency, onProgress, -}) { +}: SendTusRequestOptions) { const streamLabels = Object.keys(streamsMap) let totalBytes = 0 let lastEmittedProgress = 0 - const sizes = {} + const sizes: Record = {} const haveUnknownLengthStreams = streamLabels.some((label) => !streamsMap[label].path) @@ -37,16 +47,16 @@ async function sendTusRequest({ { concurrency: 5 } ) - const uploadProgresses = {} + const uploadProgresses: Record = {} - async function uploadSingleStream(label) { + async function uploadSingleStream(label: string) { uploadProgresses[label] = 0 const { stream, path } = streamsMap[label] const size = sizes[label] let chunkSize = requestedChunkSize - let uploadLengthDeferred + let uploadLengthDeferred: boolean const isStreamLengthKnown = !!path if (!isStreamLengthKnown) { // tus-js-client requires these options to be set for unknown size streams @@ -55,7 +65,7 @@ async function sendTusRequest({ if (chunkSize === Infinity) chunkSize = 50e6 } - const onTusProgress = (bytesUploaded) => { + const onTusProgress = (bytesUploaded: number): void => { uploadProgresses[label] = bytesUploaded // get all uploaded bytes for all files @@ -76,10 +86,10 @@ async function sendTusRequest({ } } - const filename = path ? nodePath.basename(path) : label + const filename = path ? p.basename(path) : label - await new Promise((resolve, reject) => { - const tusOptions = { + await new Promise((resolve, reject) => { + const tusOptions: tus.UploadOptions = { endpoint: assembly.tus_url, metadata: { assembly_url: assembly.assembly_ssl_url, @@ -106,6 +116,15 @@ async function sendTusRequest({ await pMap(streamLabels, uploadSingleStream, { concurrency: uploadConcurrency }) } -module.exports = { +const export_ = { sendTusRequest, } + +namespace export_ { + export interface Stream { + path?: string + stream: Readable + } +} + +export = export_ diff --git a/test/integration/live-api.test.js b/test/integration/live-api.test.ts similarity index 82% rename from test/integration/live-api.test.js rename to test/integration/live-api.test.ts index 90c84b9..ea63e0e 100644 --- a/test/integration/live-api.test.js +++ b/test/integration/live-api.test.ts @@ -1,22 +1,23 @@ -const crypto = require('crypto') -const querystring = require('querystring') -const temp = require('temp') -const fs = require('fs') -const nodePath = require('path') -const nodeStream = require('stream/promises') -const got = require('got') -const intoStream = require('into-stream') -const debug = require('debug') +import crypto = require('crypto') +import querystring = require('querystring') +import temp = require('temp') +import fs = require('fs') +import http = require('http') +import nodePath = require('path') +import nodeStream = require('stream') +import got = require('got') +import intoStream = require('into-stream') +import debug = require('debug') const log = debug('transloadit:live-api') -const Transloadit = require('../../src/Transloadit') +import Transloadit = require('../../src/Transloadit.ts') -const { createTestServer } = require('../testserver') +import createTestServer = require('../testserver.ts') -async function downloadTmpFile(url) { +async function downloadTmpFile(url: string) { const { path } = await temp.open('transloadit') - await nodeStream.pipeline(got.stream(url), fs.createWriteStream(path)) + await nodeStream.pipeline(got.default.stream(url), fs.createWriteStream(path)) return path } @@ -28,7 +29,7 @@ function createClient(opts = {}) { } // https://github.com/sindresorhus/got/blob/main/documentation/7-retry.md#retry - const gotRetry = { + const gotRetry: got.RequiredRetryOptions = { limit: 2, methods: [ 'GET', @@ -39,6 +40,7 @@ function createClient(opts = {}) { 'TRACE', 'POST', // Normally we shouldn't retry on POST, as it is not idempotent, but for tests we can do it ], + calculateDelay: () => 0, statusCodes: [], errorCodes: [ 'ETIMEDOUT', @@ -55,7 +57,7 @@ function createClient(opts = {}) { return new Transloadit({ authKey, authSecret, gotRetry, ...opts }) } -function createAssembly(client, params) { +function createAssembly(client: Transloadit, params: Transloadit.CreateAssemblyOptions) { const promise = client.createAssembly(params) const { assemblyId } = promise console.log(expect.getState().currentTestName, 'createAssembly', assemblyId) // For easier debugging @@ -100,19 +102,19 @@ const genericOptions = { const handlers = new Map() -let testServer +let testServer: createTestServer.Result beforeAll(async () => { // cloudflared tunnels are a bit unstable, so we share one cloudflared tunnel between all tests // we do this by prefixing each "virtual" server under a uuid subpath testServer = await createTestServer((req, res) => { const regex = /^\/([^/]+)/ - const match = req.url.match(regex) + const match = req.url?.match(regex) if (match) { const [, id] = match const handler = handlers.get(id) if (handler) { - req.url = req.url.replace(regex, '') + req.url = req.url?.replace(regex, '') if (req.url === '') req.url = '/' handler(req, res) } else { @@ -128,7 +130,12 @@ afterAll(async () => { await testServer?.close() }) -async function createVirtualTestServer(handler) { +interface VirtualTestServer { + close: () => void + url: string +} + +async function createVirtualTestServer(handler: http.RequestListener): Promise { const id = crypto.randomUUID() log('Adding virtual server handler', id) const url = `${testServer.url}/${id}` @@ -150,7 +157,7 @@ describe('API integration', { timeout: 30000 }, () => { const client = createClient() let uploadProgressCalled - const options = { + const options: Transloadit.CreateAssemblyOptions = { ...genericOptions, onUploadProgress: (uploadProgress) => { uploadProgressCalled = uploadProgress @@ -246,7 +253,7 @@ describe('API integration', { timeout: 30000 }, () => { file1: intoStream(sampleSvg), file2: sampleSvg, file3: buf, - file4: got.stream(genericImg), + file4: got.default.stream(genericImg), }, params: { steps: { @@ -258,7 +265,7 @@ describe('API integration', { timeout: 30000 }, () => { const result = await createAssembly(client, params) // console.log(result) - const getMatchObject = ({ name }) => ({ + const getMatchObject = ({ name }: { name: string }) => ({ name, basename: name, ext: 'svg', @@ -328,11 +335,11 @@ describe('API integration', { timeout: 30000 }, () => { expect(result.assembly_id).toMatch(promise.assemblyId) }) - async function testUploadProgress(isResumable) { + async function testUploadProgress(isResumable: boolean) { const client = createClient() let progressCalled = false - function onUploadProgress({ uploadedBytes, totalBytes }) { + function onUploadProgress({ uploadedBytes, totalBytes }: Transloadit.UploadProgress) { // console.log(uploadedBytes) expect(uploadedBytes).toBeDefined() if (isResumable) { @@ -342,7 +349,7 @@ describe('API integration', { timeout: 30000 }, () => { progressCalled = true } - const params = { + const params: Transloadit.CreateAssemblyOptions = { isResumable, params: { steps: { @@ -414,13 +421,13 @@ describe('API integration', { timeout: 30000 }, () => { // request // Async book-keeping for delaying the response - let sendServerResponse + let sendServerResponse: () => void - const promise = new Promise((resolve) => { + const promise = new Promise((resolve) => { sendServerResponse = resolve }) - const handleRequest = async (req, res) => { + const handleRequest: http.RequestListener = async (req, res) => { // console.log('handler', req.url) expect(req.url).toBe('/') @@ -430,7 +437,7 @@ describe('API integration', { timeout: 30000 }, () => { // console.log('sending response') res.setHeader('Content-type', 'image/jpeg') res.writeHead(200) - got.stream(genericImg).pipe(res) + got.default.stream(genericImg).pipe(res) } const server = await createVirtualTestServer(handleRequest) @@ -475,7 +482,7 @@ describe('API integration', { timeout: 30000 }, () => { // console.log('canceled', id) // Allow the upload to finish - sendServerResponse() + sendServerResponse!() // Successful cancel requests get ASSEMBLY_CANCELED even when it // completed, so we now request the assembly status to check the @@ -486,7 +493,7 @@ describe('API integration', { timeout: 30000 }, () => { // Check that awaitAssemblyCompletion gave the correct response too const awaitCompletionResponse = await awaitCompletionPromise - expect(awaitCompletionResponse.ok).toBe('ASSEMBLY_CANCELED') + expect(awaitCompletionResponse?.ok).toBe('ASSEMBLY_CANCELED') } finally { server.close() } @@ -526,7 +533,7 @@ describe('API integration', { timeout: 30000 }, () => { let n = 0 let isDone = false - await new Promise((resolve) => { + await new Promise((resolve) => { assemblies.on('readable', () => { const assembly = assemblies.read() @@ -551,29 +558,40 @@ describe('API integration', { timeout: 30000 }, () => { }) describe('assembly notification', () => { - let server + type OnNotification = (params: { + path?: string + client: Transloadit + assemblyId: string + }) => void + + let server: VirtualTestServer afterEach(() => { server?.close() }) // helper function - const streamToString = (stream) => - new Promise((resolve, reject) => { - const chunks = [] + const streamToString = (stream: http.IncomingMessage) => + new Promise((resolve, reject) => { + const chunks: string[] = [] stream.on('data', (chunk) => chunks.push(chunk)) stream.on('error', (err) => reject(err)) stream.on('end', () => resolve(chunks.join(''))) }) - const runNotificationTest = async (onNotification, onError) => { + const runNotificationTest = async ( + onNotification: OnNotification, + onError: (error: unknown) => void + ) => { const client = createClient() // listens for notifications - const onNotificationRequest = async (req, res) => { + const onNotificationRequest: http.RequestListener = async (req, res) => { try { expect(req.method).toBe('POST') const body = await streamToString(req) - const result = JSON.parse(querystring.parse(body).transloadit) + const result = JSON.parse( + (querystring.parse(body) as { transloadit: string }).transloadit + ) expect(result).toHaveProperty('ok') if (result.ok !== 'ASSEMBLY_COMPLETED') { onError(new Error(`result.ok was ${result.ok}`)) @@ -598,8 +616,8 @@ describe('API integration', { timeout: 30000 }, () => { } it('should send a notification upon assembly completion', async () => { - await new Promise((resolve, reject) => { - const onNotification = async ({ path }) => { + await new Promise((resolve, reject) => { + const onNotification: OnNotification = async ({ path }) => { try { expect(path).toBe('/') resolve() @@ -611,52 +629,54 @@ describe('API integration', { timeout: 30000 }, () => { }) }) - it('should replay the notification when requested', (done) => { + it('should replay the notification when requested', async () => { let secondNotification = false - const onNotification = async ({ path, client, assemblyId }) => { - const newPath = '/newPath' - const newUrl = `${server.url}${newPath}` + await new Promise((resolve, reject) => { + const onNotification: OnNotification = async ({ path, client, assemblyId }) => { + const newPath = '/newPath' + const newUrl = `${server.url}${newPath}` - // I think there are some eventual consistency issues here - await new Promise((resolve) => setTimeout(resolve, 1000)) + // I think there are some eventual consistency issues here + await new Promise((resolve) => setTimeout(resolve, 1000)) - const result = await client.getAssembly(assemblyId) + const result = await client.getAssembly(assemblyId) - expect(['successful', 'processing']).toContain(result.notify_status) - expect(result.notify_response_code).toBe(200) + expect(['successful', 'processing']).toContain(result.notify_status) + expect(result.notify_response_code).toBe(200) - if (secondNotification) { - expect(path).toBe(newPath) + if (secondNotification) { + expect(path).toBe(newPath) - // notify_url will not get updated to new URL - expect(result.notify_url).toBe(server.url) + // notify_url will not get updated to new URL + expect(result.notify_url).toBe(server.url) - try { - // If we quit immediately, things will not get cleaned up and jest will hang - await new Promise((resolve) => setTimeout(resolve, 2000)) - done() - } catch (err) { - done(err) - } + try { + // If we quit immediately, things will not get cleaned up and jest will hang + await new Promise((resolve) => setTimeout(resolve, 2000)) + resolve() + } catch (err) { + reject(err) + } - return - } + return + } - secondNotification = true + secondNotification = true - try { - expect(path).toBe('/') - expect(result.notify_url).toBe(server.url) + try { + expect(path).toBe('/') + expect(result.notify_url).toBe(server.url) - await new Promise((resolve) => setTimeout(resolve, 2000)) - await client.replayAssemblyNotification(assemblyId, { notify_url: newUrl }) - } catch (err) { - done(err) + await new Promise((resolve) => setTimeout(resolve, 2000)) + await client.replayAssemblyNotification(assemblyId, { notify_url: newUrl }) + } catch (err) { + reject(err) + } } - } - runNotificationTest(onNotification, (err) => done(err)) + runNotificationTest(onNotification, reject) + }) }) }) @@ -666,7 +686,7 @@ describe('API integration', { timeout: 30000 }, () => { .toISOString() .toLocaleLowerCase('en-US') .replace(/[^0-9a-z-]/g, '-')}` - let templId = null + let templId: string | null = null const client = createClient() it('should allow listing templates', async () => { @@ -682,7 +702,7 @@ describe('API integration', { timeout: 30000 }, () => { it("should be able to fetch a template's definition", async () => { expect(templId).toBeDefined() - const template = await client.getTemplate(templId) + const template = await client.getTemplate(templId!) const { name, content } = template expect(name).toBe(templName) expect(content).toEqual(genericParams) @@ -696,7 +716,7 @@ describe('API integration', { timeout: 30000 }, () => { } const editedName = `${templName}-edited` - const editResult = await client.editTemplate(templId, { + const editResult = await client.editTemplate(templId!, { name: editedName, template: editedTemplate, }) @@ -709,10 +729,10 @@ describe('API integration', { timeout: 30000 }, () => { it('should delete the template successfully', async () => { expect(templId).toBeDefined() - const template = await client.deleteTemplate(templId) + const template = await client.deleteTemplate(templId!) const { ok } = template expect(ok).toBe('TEMPLATE_DELETED') - await expect(client.getTemplate(templId)).rejects.toThrow( + await expect(client.getTemplate(templId!)).rejects.toThrow( expect.objectContaining({ transloaditErrorCode: 'TEMPLATE_NOT_FOUND' }) ) }) @@ -724,7 +744,7 @@ describe('API integration', { timeout: 30000 }, () => { .toISOString() .toLocaleLowerCase('en-US') .replace(/[^0-9a-z-]/g, '-')}` - let credId = null + let credId: string | null = null const client = createClient() it('should allow listing credentials', async () => { @@ -750,7 +770,7 @@ describe('API integration', { timeout: 30000 }, () => { it("should be able to fetch a credential's definition", async () => { expect(credId).toBeDefined() - const readResult = await client.getTemplateCredential(credId) + const readResult = await client.getTemplateCredential(credId!) const { name, content } = readResult.credential expect(name).toBe(credName) expect(content.bucket).toEqual('mybucket.example.com') @@ -759,7 +779,7 @@ describe('API integration', { timeout: 30000 }, () => { it('should allow editing a credential', async () => { expect(credId).toBeDefined() const editedName = `${credName}-edited` - const editResult = await client.editTemplateCredential(credId, { + const editResult = await client.editTemplateCredential(credId!, { name: editedName, type: 's3', content: { @@ -778,10 +798,10 @@ describe('API integration', { timeout: 30000 }, () => { it('should delete the credential successfully', async () => { expect(credId).toBeDefined() - const credential = await client.deleteTemplateCredential(credId) + const credential = await client.deleteTemplateCredential(credId!) const { ok } = credential expect(ok).toBe('TEMPLATE_CREDENTIALS_DELETED') - await expect(client.getTemplateCredential(credId)).rejects.toThrow( + await expect(client.getTemplateCredential(credId!)).rejects.toThrow( expect.objectContaining({ transloaditErrorCode: 'TEMPLATE_CREDENTIALS_NOT_READ' }) ) }) diff --git a/test/testserver.js b/test/testserver.ts similarity index 71% rename from test/testserver.js rename to test/testserver.ts index 04cab10..20398b8 100644 --- a/test/testserver.js +++ b/test/testserver.ts @@ -1,12 +1,17 @@ -const http = require('http') -const got = require('got') -const debug = require('debug') +import http = require('http') +import got = require('got') +import debug = require('debug') const log = debug('transloadit:testserver') -const createTunnel = require('./tunnel') +import createTunnel = require('./tunnel.ts') -async function createHttpServer(handler) { +interface HttpServer { + server: http.Server + port: number +} + +async function createHttpServer(handler: http.RequestListener): Promise { return new Promise((resolve, reject) => { const server = http.createServer(handler) @@ -20,7 +25,7 @@ async function createHttpServer(handler) { }) } server.on('error', (err) => { - if (err.code === 'EADDRINUSE') { + if ((err as NodeJS.ErrnoException).code === 'EADDRINUSE') { if (++port >= 65535) { server.close() reject(new Error('Failed to find any free port to listen on')) @@ -36,17 +41,17 @@ async function createHttpServer(handler) { }) } -async function createTestServer(onRequest) { +async function createTestServer(onRequest: http.RequestListener): Promise { if (!process.env.CLOUDFLARED_PATH) { throw new Error('CLOUDFLARED_PATH environment variable not set') } - let expectedPath + let expectedPath: string let initialized = false - let onTunnelOperational - let tunnel + let onTunnelOperational: () => void + let tunnel: createTunnel.Result - const handleHttpRequest = (req, res) => { + const handleHttpRequest: http.RequestListener = (req, res) => { log('HTTP request handler', req.method, req.url) if (!initialized) { @@ -63,7 +68,7 @@ async function createTestServer(onRequest) { async function close() { if (tunnel) await tunnel.close() - await new Promise((resolve) => server.close(() => resolve())) + await new Promise((resolve) => server.close(() => resolve())) log('closed tunnel') } @@ -84,7 +89,7 @@ async function createTestServer(onRequest) { expectedPath = `/initialize-test${i}` try { - await got(`${tunnelPublicUrl}${expectedPath}`, { timeout: { request: 2000 } }) + await got.default(`${tunnelPublicUrl}${expectedPath}`, { timeout: { request: 2000 } }) return } catch (err) { // console.error(err.message) @@ -96,7 +101,7 @@ async function createTestServer(onRequest) { } await Promise.all([ - new Promise((resolve) => { + new Promise((resolve) => { onTunnelOperational = resolve }), sendTunnelRequest(), @@ -115,6 +120,12 @@ async function createTestServer(onRequest) { } } -module.exports = { - createTestServer, +namespace createTestServer { + export interface Result { + port: number + close: () => void + url: string + } } + +export = createTestServer diff --git a/test/tunnel.js b/test/tunnel.ts similarity index 75% rename from test/tunnel.js rename to test/tunnel.ts index d20c464..d8e928a 100644 --- a/test/tunnel.js +++ b/test/tunnel.ts @@ -1,12 +1,25 @@ -const execa = require('execa') -const readline = require('readline') -const dns = require('dns/promises') -const debug = require('debug') -const pRetry = require('p-retry') +import execa = require('execa') +import readline = require('readline') +import dns = require('dns/promises') +import debug = require('debug') +import pRetry = require('p-retry') const log = debug('transloadit:cloudflared-tunnel') -async function startTunnel({ cloudFlaredPath, port }) { +interface CreateTunnelParams { + cloudFlaredPath: string + port: number +} + +interface StartTunnelResult { + url: string + process: execa.ExecaChildProcess +} + +async function startTunnel({ + cloudFlaredPath, + port, +}: CreateTunnelParams): Promise { const process = execa( cloudFlaredPath, ['tunnel', '--url', `http://localhost:${port}`, '--no-autoupdate'], @@ -17,7 +30,7 @@ async function startTunnel({ cloudFlaredPath, port }) { return await new Promise((resolve, reject) => { const timeout = setTimeout(() => reject(new Error('Timed out trying to start tunnel')), 30000) - const rl = readline.createInterface({ input: process.stderr }) + const rl = readline.createInterface({ input: process.stderr as NodeJS.ReadStream }) process.on('error', (err) => { console.error(err) @@ -25,7 +38,7 @@ async function startTunnel({ cloudFlaredPath, port }) { }) let fullStderr = '' - let foundUrl + let foundUrl: string rl.on('error', (err) => { reject( @@ -73,8 +86,11 @@ async function startTunnel({ cloudFlaredPath, port }) { } } -function createTunnel({ cloudFlaredPath = 'cloudflared', port }) { - let process +function createTunnel({ + cloudFlaredPath = 'cloudflared', + port, +}: CreateTunnelParams): createTunnel.Result { + let process: execa.ExecaChildProcess | undefined const urlPromise = (async () => { const tunnel = await pRetry(async () => startTunnel({ cloudFlaredPath, port }), { retries: 1 }) @@ -96,7 +112,7 @@ function createTunnel({ cloudFlaredPath = 'cloudflared', port }) { await resolver.resolve4(host) return url } catch (err) { - log('dns err', err.message) + log('dns err', (err as Error).message) await new Promise((resolve) => setTimeout(resolve, 3000)) } } @@ -106,7 +122,7 @@ function createTunnel({ cloudFlaredPath = 'cloudflared', port }) { async function close() { if (!process) return - const promise = new Promise((resolve) => process.on('close', resolve)) + const promise = new Promise((resolve) => process!.on('close', resolve)) process.kill() await promise } @@ -118,4 +134,12 @@ function createTunnel({ cloudFlaredPath = 'cloudflared', port }) { } } -module.exports = createTunnel +namespace createTunnel { + export interface Result { + process?: execa.ExecaChildProcess + urlPromise: Promise + close: () => Promise + } +} + +export = createTunnel diff --git a/test/unit/mock-http.test.js b/test/unit/mock-http.test.ts similarity index 90% rename from test/unit/mock-http.test.js rename to test/unit/mock-http.test.ts index d78ea69..4bd1130 100644 --- a/test/unit/mock-http.test.js +++ b/test/unit/mock-http.test.ts @@ -1,9 +1,10 @@ -const nock = require('nock') +import nock = require('nock') -const Transloadit = require('../../src/Transloadit') +import Transloadit = require('../../src/Transloadit.ts') -const getLocalClient = (opts) => - new Transloadit({ authKey: '', authSecret: '', endpoint: 'http://localhost', ...opts }) +const getLocalClient = ( + opts?: Omit +) => new Transloadit({ authKey: '', authSecret: '', endpoint: 'http://localhost', ...opts }) const createAssemblyRegex = /\/assemblies\/[0-9a-f]{32}/ @@ -38,7 +39,7 @@ describe('Mocked API tests', () => { .delay(100) .reply(200, { ok: 'ASSEMBLY_EXECUTING', assembly_url: '', assembly_ssl_url: '' }) - await expect(client.awaitAssemblyCompletion(1, { timeout: 1, interval: 1 })).rejects.toThrow( + await expect(client.awaitAssemblyCompletion('1', { timeout: 1, interval: 1 })).rejects.toThrow( expect.objectContaining({ code: 'POLLING_TIMED_OUT', message: 'Polling timed out' }) ) scope.done() @@ -56,7 +57,7 @@ describe('Mocked API tests', () => { await client.createAssembly() - const result = await client.awaitAssemblyCompletion(1) + const result = await client.awaitAssemblyCompletion('1') expect(result.ok).toBe('REQUEST_ABORTED') scope.done() }) @@ -73,7 +74,7 @@ describe('Mocked API tests', () => { .reply(200, { ok: 'ASSEMBLY_COMPLETED', assembly_url: '', assembly_ssl_url: '' }) await expect( - client.awaitAssemblyCompletion(1, { timeout: 100, interval: 1 }) + client.awaitAssemblyCompletion('1', { timeout: 100, interval: 1 }) ).resolves.toMatchObject({ ok: 'ASSEMBLY_COMPLETED' }) scope.done() }) @@ -115,8 +116,7 @@ describe('Mocked API tests', () => { }) it('should retry correctly on RATE_LIMIT_REACHED', async () => { - const client = getLocalClient() - client._maxRetries = 1 + const client = getLocalClient({ maxRetries: 1 }) // https://transloadit.com/blog/2012/04/introducing-rate-limiting/ @@ -158,7 +158,7 @@ describe('Mocked API tests', () => { .query(() => true) .reply(500) - const promise = client.getAssembly(1) + const promise = client.getAssembly('1') await expect(promise).rejects.toThrow( expect.not.objectContaining({ code: 'ERR_NOCK_NO_MATCH' }) ) // Make sure that it was called only once @@ -183,10 +183,10 @@ describe('Mocked API tests', () => { .reply(200, {}) // Success - await client.getAssembly(1) + await client.getAssembly('1') // Failure - const promise = client.getAssembly(1) + const promise = client.getAssembly('1') await expect(promise).rejects.toThrow(Transloadit.InconsistentResponseError) await expect(promise).rejects.toThrow( expect.objectContaining({ @@ -207,10 +207,10 @@ describe('Mocked API tests', () => { .query(() => true) .reply(200, { error: 'IMPORT_FILE_ERROR', assembly_url: '', assembly_ssl_url: '' }) - const assembly = await client.getAssembly(1) + const assembly = await client.getAssembly('1') expect(assembly).toMatchObject({ error: 'IMPORT_FILE_ERROR' }) - const assembly2 = await client.awaitAssemblyCompletion(1) + const assembly2 = await client.awaitAssemblyCompletion('1') expect(assembly2).toMatchObject({ error: 'IMPORT_FILE_ERROR' }) scope.done() @@ -239,7 +239,7 @@ describe('Mocked API tests', () => { .post('/assemblies/1/replay') .reply(200, { error: 'IMPORT_FILE_ERROR' }) - await expect(client.replayAssembly(1)).rejects.toThrow( + await expect(client.replayAssembly('1')).rejects.toThrow( expect.objectContaining({ transloaditErrorCode: 'IMPORT_FILE_ERROR' }) ) scope.done() diff --git a/test/unit/test-pagination-stream.test.js b/test/unit/test-pagination-stream.test.ts similarity index 55% rename from test/unit/test-pagination-stream.test.js rename to test/unit/test-pagination-stream.test.ts index 72975dd..27152a2 100644 --- a/test/unit/test-pagination-stream.test.js +++ b/test/unit/test-pagination-stream.test.ts @@ -1,15 +1,21 @@ -const { Writable } = require('stream') +import stream = require('stream') -const PaginationStream = require('../../src/PaginationStream') +import PaginationStream = require('../../src/PaginationStream.ts') -const toArray = (callback) => { - const stream = new Writable({ objectMode: true }) - const list = [] - stream.write = (chunk) => list.push(chunk) +const toArray = (callback: (list: number[]) => void) => { + const writable = new stream.Writable({ objectMode: true }) + const list: number[] = [] + writable.write = (chunk) => { + list.push(chunk) + return false + } - stream.end = () => callback(list) + writable.end = () => { + callback(list) + return writable + } - return stream + return writable } describe('PaginationStream', () => { @@ -21,9 +27,9 @@ describe('PaginationStream', () => { { count, items: [7, 8, 9] }, ] - const stream = new PaginationStream(async (pageno) => pages[pageno - 1]) + const stream = new PaginationStream(async (pageno) => pages[pageno - 1]) - await new Promise((resolve) => { + await new Promise((resolve) => { stream.pipe( toArray((array) => { const expected = pages.flatMap(({ items }) => items) @@ -45,11 +51,14 @@ describe('PaginationStream', () => { { count, items: [7, 8, 9] }, ] - const stream = new PaginationStream( - async (pageno) => new Promise((resolve) => process.nextTick(() => resolve(pages[pageno - 1]))) + const stream = new PaginationStream( + async (pageno) => + new Promise((resolve) => { + process.nextTick(() => resolve(pages[pageno - 1])) + }) ) - await new Promise((resolve) => { + await new Promise((resolve) => { stream.pipe( toArray((array) => { const expected = pages.flatMap(({ items }) => items) diff --git a/test/unit/test-transloadit-client.test.js b/test/unit/test-transloadit-client.test.ts similarity index 67% rename from test/unit/test-transloadit-client.test.js rename to test/unit/test-transloadit-client.test.ts index fa0edb3..eea0745 100644 --- a/test/unit/test-transloadit-client.test.js +++ b/test/unit/test-transloadit-client.test.ts @@ -1,21 +1,25 @@ -const stream = require('stream') -const FormData = require('form-data') -const got = require('got') +import stream = require('stream') +import FormData = require('form-data') +import got = require('got') -const tus = require('../../src/tus') -const Transloadit = require('../../src/Transloadit') -const pkg = require('../../package.json') +import tus = require('../../src/tus.ts') +import Transloadit = require('../../src/Transloadit.ts') +import pkg = require('transloadit/package.json') const mockedExpiresDate = '2021-01-06T21:11:07.883Z' -const mockGetExpiresDate = (client) => +const mockGetExpiresDate = (client: Transloadit) => + // @ts-expect-error This mocks private internals vi.spyOn(client, '_getExpiresDate').mockReturnValue(mockedExpiresDate) -const mockGot = (method) => - vi.spyOn(got, method).mockImplementation(() => { - const mockPromise = Promise.resolve({ body: '' }) - mockPromise.on = vi.fn(() => {}) +const mockGot = (method: 'get') => + vi.spyOn(got.default, method).mockImplementation(() => { + const mockPromise = Promise.resolve({ + body: '', + }) as got.CancelableRequest + ;(mockPromise as any).on = vi.fn(() => {}) return mockPromise }) -const mockRemoteJson = (client) => +const mockRemoteJson = (client: Transloadit) => + // @ts-expect-error This mocks private internals vi.spyOn(client, '_remoteJson').mockImplementation(() => ({ body: {} })) describe('Transloadit', () => { @@ -23,7 +27,7 @@ describe('Transloadit', () => { const client = new Transloadit({ authKey: 'foo_key', authSecret: 'foo_secret' }) // mimic Stream object returned from `request` (which is not a stream v3) - const req = { pipe: () => {} } + const req = { pipe: () => {} } as Partial as stream.Readable const promise = client.createAssembly({ uploads: { file: req } }) await expect(promise).rejects.toThrow( @@ -39,14 +43,32 @@ describe('Transloadit', () => { maxRetries: 0, } const client = new Transloadit(opts) - expect(client._authKey).toBe('foo_key') - expect(client._authSecret).toBe('foo_secret') - expect(client._endpoint).toBe('https://api2.transloadit.com') - expect(client._maxRetries).toBe(0) - expect(client._defaultTimeout).toBe(60000) + expect( + // @ts-expect-error This tests private internals + client._authKey + ).toBe('foo_key') + expect( + // @ts-expect-error This tests private internals + client._authSecret + ).toBe('foo_secret') + expect( + // @ts-expect-error This tests private internals + client._endpoint + ).toBe('https://api2.transloadit.com') + expect( + // @ts-expect-error This tests private internals + client._maxRetries + ).toBe(0) + expect( + // @ts-expect-error This tests private internals + client._defaultTimeout + ).toBe(60000) client.setDefaultTimeout(10000) - expect(client._defaultTimeout).toBe(10000) + expect( + // @ts-expect-error This tests private internals + client._defaultTimeout + ).toBe(10000) }) it('should throw when sending a trailing slash in endpoint', () => { @@ -59,11 +81,23 @@ describe('Transloadit', () => { }) it('should give error when no authSecret', () => { - expect(() => new Transloadit({ authSecret: '' })).toThrow() + expect( + () => + new Transloadit( + // @ts-expect-error This tests invalid types + { authSecret: '' } + ) + ).toThrow() }) it('should give error when no authKey', () => { - expect(() => new Transloadit({ authKey: '' })).toThrow() + expect( + () => + new Transloadit( + // @ts-expect-error This tests invalid types + { authKey: '' } + ) + ).toThrow() }) it('should allow overwriting some properties', () => { @@ -74,27 +108,36 @@ describe('Transloadit', () => { } const client = new Transloadit(opts) - expect(client._authKey).toBe('foo_key') - expect(client._authSecret).toBe('foo_secret') - expect(client._endpoint).toBe('http://foo') + expect( + // @ts-expect-error This tests private internals + client._authKey + ).toBe('foo_key') + expect( + // @ts-expect-error This tests private internals + client._authSecret + ).toBe('foo_secret') + expect( + // @ts-expect-error This tests private internals + client._endpoint + ).toBe('http://foo') }) }) describe('add stream', () => { it('should pause streams', async () => { - vi.spyOn(tus, 'sendTusRequest').mockImplementation(() => {}) + vi.spyOn(tus, 'sendTusRequest').mockImplementation(() => Promise.resolve()) const client = new Transloadit({ authKey: 'foo_key', authSecret: 'foo_secret' }) const name = 'foo_name' - const pause = vi.fn(() => {}) + const pause = vi.fn(() => mockStream) const mockStream = { pause, - pipe: () => {}, - _read: () => {}, + pipe: () => undefined, + _read: () => undefined, _readableState: {}, - on: () => {}, + on: () => mockStream, readable: true, - } + } as Partial as stream.Readable mockRemoteJson(client) @@ -126,6 +169,7 @@ describe('Transloadit', () => { const calcSignatureSpy = vi.spyOn(client, 'calcSignature') const formAppendSpy = vi.spyOn(form, 'append') + // @ts-expect-error This tests private internals client._appendForm(form, params, streamsMap, fields) expect(calcSignatureSpy).toHaveBeenCalledWith(params) @@ -157,6 +201,7 @@ describe('Transloadit', () => { mockGetExpiresDate(client) + // @ts-expect-error This tests private internals const fullUrl = client._appendParamsToUrl(url, params) const expected = `${url}&signature=${signature}¶ms=${encodeURIComponent(jsonParams)}` @@ -168,6 +213,7 @@ describe('Transloadit', () => { it('should add the auth key, secret and expires parameters', () => { let client = new Transloadit({ authKey: 'foo_key', authSecret: 'foo_secret' }) + // @ts-expect-error This tests private internals let r = JSON.parse(client._prepareParams()) expect(r.auth.key).toBe('foo_key') expect(r.auth.expires).not.toBeNull() @@ -178,6 +224,7 @@ describe('Transloadit', () => { } client = new Transloadit(opts) + // @ts-expect-error This tests private internals r = JSON.parse(client._prepareParams()) expect(r.auth.key).toBe('foo') return expect(r.auth.expires).not.toBeNull() @@ -193,6 +240,7 @@ describe('Transloadit', () => { }, } + // @ts-expect-error This tests private internals const r = JSON.parse(client._prepareParams(PARAMS)) expect(r.auth.key).toBe('foo_key') return expect(r.auth.expires).toBe('foo_expires') @@ -203,12 +251,14 @@ describe('Transloadit', () => { it('should calc _prepareParams and _calcSignature', () => { const client = new Transloadit({ authKey: 'foo_key', authSecret: 'foo_secret' }) + // @ts-expect-error This tests private internals client._authSecret = '13123123123' const params = { foo: 'bar' } mockGetExpiresDate(client) + // @ts-expect-error This tests private internals const prepareParamsSpy = vi.spyOn(client, '_prepareParams') const r = client.calcSignature(params) @@ -240,32 +290,52 @@ describe('Transloadit', () => { it('should crash if attempt to use callback', async () => { const client = new Transloadit({ authKey: 'foo_key', authSecret: 'foo_secret' }) const cb = () => {} - expect(() => client.createAssembly({}, cb)).toThrow(TypeError) + expect(() => + client.createAssembly( + {}, + // @ts-expect-error This tests bad input + cb + ) + ).toThrow(TypeError) }) describe('_calcSignature', () => { it('should calculate the signature properly', () => { const client = new Transloadit({ authKey: 'foo_key', authSecret: 'foo_secret' }) + // @ts-expect-error This tests private internals client._authSecret = '13123123123' let expected = 'sha384:8b90663d4b7d14ac7d647c74cb53c529198dee4689d0f8faae44f0df1c2a157acce5cb8c55a375218bc331897cf92e9d' - expect(client._calcSignature('foo')).toBe(expected) + expect( + // @ts-expect-error This tests private internals + client._calcSignature('foo') + ).toBe(expected) expected = 'sha384:3595c177fc09c9cc46672cef90685257838a0a4295056dcfd45b5d5c255e8f987e1c1ca8800b9c21ee03e4ada7485e9d' - expect(client._calcSignature('akjdkadskjads')).toBe(expected) + expect( + // @ts-expect-error This tests private internals + client._calcSignature('akjdkadskjads') + ).toBe(expected) + // @ts-expect-error This tests private internals client._authSecret = '90191902390123' expected = 'sha384:b6f967f8bd659652c6c2093bc52045becbd6e8fbd96d8ef419e07bbc9fb411c56316e75f03dfc2a6613dbe896bbad20f' - expect(client._calcSignature('foo')).toBe(expected) + expect( + // @ts-expect-error This tests private internals + client._calcSignature('foo') + ).toBe(expected) expected = 'sha384:fc75f6a4bbb06340653c0f7efff013e94eb8e402e0e45cf40ad4bc95f45a3ae3263032000727359c595a433364a84f96' - return expect(client._calcSignature('akjdkadskjads')).toBe(expected) + return expect( + // @ts-expect-error This tests private internals + client._calcSignature('akjdkadskjads') + ).toBe(expected) }) }) @@ -276,6 +346,7 @@ describe('Transloadit', () => { const get = mockGot('get') const url = '/some-url' + // @ts-expect-error This tests private internals await client._remoteJson({ url, method: 'get' }) expect(get).toHaveBeenCalledWith( diff --git a/tsconfig.build.json b/tsconfig.build.json new file mode 100644 index 0000000..4b124dd --- /dev/null +++ b/tsconfig.build.json @@ -0,0 +1,15 @@ +{ + "include": ["src"], + "compilerOptions": { + "composite": true, + "declaration": true, + "declarationMap": true, + "module": "node16", + "outDir": "dist", + "resolveJsonModule": true, + "rootDir": "src", + "sourceMap": true, + "strict": true, + "verbatimModuleSyntax": true + } +} diff --git a/tsconfig.json b/tsconfig.json new file mode 100644 index 0000000..adf32b0 --- /dev/null +++ b/tsconfig.json @@ -0,0 +1,12 @@ +{ + "exclude": ["src"], + "references": [{ "path": "./tsconfig.build.json" }], + "compilerOptions": { + "allowImportingTsExtensions": true, + "module": "node16", + "noEmit": true, + "resolveJsonModule": true, + "strict": true, + "types": ["vitest/globals"] + } +} diff --git a/types/index.d.ts b/types/index.d.ts deleted file mode 100644 index b3e8030..0000000 --- a/types/index.d.ts +++ /dev/null @@ -1,223 +0,0 @@ -// Type definitions for transloadit - -import { Readable } from 'stream' -import * as intoStream from 'into-stream' - -import { - RequestError, - ReadError, - ParseError, - UploadError, - HTTPError, - MaxRedirectsError, - TimeoutError, - RequiredRetryOptions, -} from 'got' - -export interface CreateAssemblyOptions { - params?: CreateAssemblyParams - files?: { - [name: string]: string - } - uploads?: { - [name: string]: Readable | intoStream.Input - } - waitForCompletion?: boolean - isResumable?: boolean - chunkSize?: number - uploadConcurrency?: number - timeout?: number - onUploadProgress?: (uploadProgress: UploadProgress) => void - onAssemblyProgress?: AssemblyProgress - assemblyId?: string -} - -export default class Transloadit { - constructor(options: { - authKey: string - authSecret: string - endpoint?: string - maxRetries?: number - timeout?: number - gotRetry?: RequiredRetryOptions - }) - - setDefaultTimeout(timeout: number): void - - createAssembly(options: CreateAssemblyOptions): Promise - - replayAssembly(assemblyId: string, params?: KeyVal): Promise - cancelAssembly(assemblyId: string): Promise - listAssemblies(params?: KeyVal): Promise<{ count: number; items: ListedAssembly[] }> - getAssembly(assemblyId: string): Promise - streamAssemblies(params?: KeyVal): Readable - - awaitAssemblyCompletion( - assemblyId: string, - options: { - onAssemblyProgress?: AssemblyProgress - timeout?: number - interval?: number - } - ): Promise - - replayAssemblyNotification( - assemblyId: string, - params?: KeyVal - ): Promise<{ ok: string; success: boolean }> - - getLastUsedAssemblyUrl(): string - - createTemplate(params: KeyVal): Promise - editTemplate(templateId: string, params: KeyVal): Promise - deleteTemplate(templateId: string): Promise<{ ok: string; message: string }> - listTemplates(params: KeyVal): Promise<{ count: number; items: ListedTemplate[] }> - getTemplate(templateId: string): Promise - streamTemplates(params?: KeyVal): Readable - - /** https://transloadit.com/docs/api/bill-date-get/ */ - getBill(month: string): Promise - - calcSignature(params: KeyVal): { signature: string; params: string } -} - -export type AssemblyProgress = (assembly: Assembly) => void - -export interface CreateAssemblyParams { - /** See https://transloadit.com/docs/topics/assembly-instructions/ */ - steps?: KeyVal - template_id?: string - notify_url?: string - fields?: KeyVal - allow_steps_override?: boolean -} - -// TODO -/** Object with properties. See https://transloadit.com/docs/api/ */ -export interface KeyVal { - [key: string]: any -} - -export interface UploadProgress { - uploadedBytes?: number - totalBytes?: number -} - -/** https://transloadit.com/docs/api/assembly-status-response/#explanation-of-fields */ -export interface Assembly { - ok?: string - message?: string - assembly_id: string - parent_id?: string - account_id: string - template_id?: string - instance: string - assembly_url: string - assembly_ssl_url: string - uppyserver_url: string - companion_url: string - websocket_url: string - tus_url: string - bytes_received: number - bytes_expected: number - upload_duration: number - client_agent?: string - client_ip?: string - client_referer?: string - transloadit_client: string - start_date: string - upload_meta_data_extracted: boolean - warnings: any[] - is_infinite: boolean - has_dupe_jobs: boolean - execution_start: string - execution_duration: number - queue_duration: number - jobs_queue_duration: number - notify_start?: any - notify_url?: string - notify_status?: any - notify_response_code?: any - notify_duration?: any - last_job_completed?: string - fields: KeyVal - running_jobs: any[] - bytes_usage: number - executing_jobs: any[] - started_jobs: string[] - parent_assembly_status: any - params: string - template?: any - merged_params: string - uploads: any[] - results: any - build_id: string - error?: string - stderr?: string - stdout?: string - reason?: string -} - -/** See https://transloadit.com/docs/api/assemblies-assembly-id-get/ */ -export interface ListedAssembly { - id?: string - parent_id?: string - account_id: string - template_id?: string - instance: string - notify_url?: string - redirect_url?: string - files: string - warning_count: number - execution_duration: number - execution_start: string - ok?: string - error?: string - created: string -} - -export interface ReplayedAssembly { - ok?: string - message?: string - success: boolean - assembly_id: string - assembly_url: string - assembly_ssl_url: string - notify_url?: string -} - -export interface ListedTemplate { - id: string - name: string - encryption_version: number - require_signature_auth: number - last_used?: string - created: string - modified: string - content: TemplateContent -} - -export interface TemplateResponse { - ok: string - message: string - id: string - content: TemplateContent - name: string - require_signature_auth: number -} - -export interface TemplateContent { - steps: KeyVal -} - -export class InconsistentResponseError extends Error {} - -export { - RequestError, - ReadError, - ParseError, - UploadError, - HTTPError, - MaxRedirectsError, - TimeoutError, -} diff --git a/types/index.test-d.ts b/types/index.test-d.ts deleted file mode 100644 index 0fc5835..0000000 --- a/types/index.test-d.ts +++ /dev/null @@ -1,98 +0,0 @@ -import { expectType } from 'tsd' - -import intoStream from 'into-stream' -import { Readable } from 'stream' - -import Transloadit, { - Assembly, - ListedAssembly, - ReplayedAssembly, - TemplateResponse, - ListedTemplate, - KeyVal, -} from '../' - -const transloadit = new Transloadit({ - authKey: '123', - authSecret: '456', - endpoint: 'http://localhost', - maxRetries: 1, - gotRetry: { - limit: 2, - methods: ['GET'], - statusCodes: [404], - errorCodes: ['ERROR'], - calculateDelay: () => 1, - maxRetryAfter: 1, - }, -}) - -expectType(transloadit.getLastUsedAssemblyUrl()) -expectType(transloadit.setDefaultTimeout(1)) - -expectType>( - transloadit.createAssembly({ - params: { - steps: { foo: 'bar' }, - template_id: 'template', - notify_url: 'url', - fields: { a: 'b', c: 1 }, - allow_steps_override: false, - }, - files: { - file1: '/path/to/file', - }, - uploads: { - file2: 'string', - file3: intoStream('string'), - file4: Buffer.from('string'), - }, - isResumable: true, - chunkSize: Infinity, - uploadConcurrency: 5, - timeout: 1, - waitForCompletion: true, - onAssemblyProgress: (assembly) => { - expectType(assembly) - }, - onUploadProgress: ({ uploadedBytes, totalBytes }) => { - expectType(uploadedBytes) - expectType(totalBytes) - }, - assemblyId: '123', - }) -) - -expectType>( - transloadit.awaitAssemblyCompletion('1', { - onAssemblyProgress: (assembly) => { - expectType(assembly) - }, - timeout: 1, - interval: 1, - }) -) - -expectType>(transloadit.cancelAssembly('1')) -expectType>(transloadit.replayAssembly('1', { param1: { a: 1 } })) -expectType>( - transloadit.replayAssemblyNotification('1', { param1: { a: 1 } }) -) -expectType>( - transloadit.listAssemblies({ param1: { a: 1 } }) -) -expectType(transloadit.streamAssemblies({ param1: { a: 1 } })) -expectType>(transloadit.getAssembly('1')) - -expectType>(transloadit.createTemplate({ param1: { a: 1 } })) -expectType>(transloadit.editTemplate('1', { param1: { a: 1 } })) -expectType>(transloadit.deleteTemplate('1')) -expectType>(transloadit.getTemplate('1')) -expectType>( - transloadit.listTemplates({ param1: { a: 1 } }) -) -expectType(transloadit.streamTemplates({ param1: { a: 1 } })) - -expectType>(transloadit.getBill('2020-01')) - -expectType<{ signature: string; params: string }>(transloadit.calcSignature({ param1: { a: 1 } })) diff --git a/vitest.config.mjs b/vitest.config.mjs index 109cde7..c97b4e0 100644 --- a/vitest.config.mjs +++ b/vitest.config.mjs @@ -1,6 +1,10 @@ import { defineConfig } from 'vitest/config' export default defineConfig({ + esbuild: { + target: 'node14', + format: 'cjs', + }, test: { coverage: { include: 'src', diff --git a/yarn.lock b/yarn.lock index f0499c8..31d09f1 100644 --- a/yarn.lock +++ b/yarn.lock @@ -41,7 +41,7 @@ __metadata: languageName: node linkType: hard -"@babel/code-frame@npm:^7.0.0, @babel/code-frame@npm:^7.22.13": +"@babel/code-frame@npm:^7.22.13": version: 7.22.13 resolution: "@babel/code-frame@npm:7.22.13" dependencies: @@ -762,13 +762,6 @@ __metadata: languageName: node linkType: hard -"@tsd/typescript@npm:~4.9.3": - version: 4.9.5 - resolution: "@tsd/typescript@npm:4.9.5" - checksum: ec32c2a990c75646f3b8bc50e8a6b9b2fbd43752882a3b8d302a54d7e8eccaefc269114920681bf6f2509c2db3f48629b85ac93170fe2557bcb64169976f510d - languageName: node - linkType: hard - "@types/cacheable-request@npm:^6.0.1": version: 6.0.3 resolution: "@types/cacheable-request@npm:6.0.3" @@ -781,17 +774,16 @@ __metadata: languageName: node linkType: hard -"@types/eslint@npm:^7.2.13": - version: 7.29.0 - resolution: "@types/eslint@npm:7.29.0" +"@types/debug@npm:^4.1.12": + version: 4.1.12 + resolution: "@types/debug@npm:4.1.12" dependencies: - "@types/estree": "npm:*" - "@types/json-schema": "npm:*" - checksum: 780ea3f4abba77a577a9ca5c4b66f74acc0f5ff5162b9a361ca931763ed65bca062389fc26027b416ed0a54d390e2206412db6c682f565e523d2b82159e6c46f + "@types/ms": "npm:*" + checksum: 5dcd465edbb5a7f226e9a5efd1f399c6172407ef5840686b73e3608ce135eeca54ae8037dcd9f16bdb2768ac74925b820a8b9ecc588a58ca09eca6acabe33e2f languageName: node linkType: hard -"@types/estree@npm:*, @types/estree@npm:1.0.5, @types/estree@npm:^1.0.0": +"@types/estree@npm:1.0.5, @types/estree@npm:^1.0.0": version: 1.0.5 resolution: "@types/estree@npm:1.0.5" checksum: b3b0e334288ddb407c7b3357ca67dbee75ee22db242ca7c56fe27db4e1a31989cb8af48a84dd401deb787fe10cc6b2ab1ee82dc4783be87ededbe3d53c79c70d @@ -805,7 +797,7 @@ __metadata: languageName: node linkType: hard -"@types/json-schema@npm:*, @types/json-schema@npm:^7.0.7": +"@types/json-schema@npm:^7.0.7": version: 7.0.14 resolution: "@types/json-schema@npm:7.0.14" checksum: da68689ccd44cb93ca4c9a4af3b25c6091ecf45fb370d1ed0d0ac5b780e235bf0b9bdc1f7e28f19e6713b22567c3db11fefcbcc6d48ac6b356d035a8f9f4ea30 @@ -828,10 +820,10 @@ __metadata: languageName: node linkType: hard -"@types/minimist@npm:^1.2.0": - version: 1.2.4 - resolution: "@types/minimist@npm:1.2.4" - checksum: 01403652c09de17b8c6d7d9959cb7a244deccf31e9e7a1a7011fba73fa2724c14fe935718e0fdc48dcd30403fd76a916cb991d4c0ddf229748ccc6c4920c3371 +"@types/ms@npm:*": + version: 0.7.34 + resolution: "@types/ms@npm:0.7.34" + checksum: ac80bd90012116ceb2d188fde62d96830ca847823e8ca71255616bc73991aa7d9f057b8bfab79e8ee44ffefb031ddd1bcce63ea82f9e66f7c31ec02d2d823ccc languageName: node linkType: hard @@ -844,13 +836,6 @@ __metadata: languageName: node linkType: hard -"@types/normalize-package-data@npm:^2.4.0": - version: 2.4.3 - resolution: "@types/normalize-package-data@npm:2.4.3" - checksum: 9ad94568b53f65d0c7fffed61c74e4a7b8625b1ebbc549f1de25287c2d20e6bca9d9cdc5826e508c9d95e02a48ac69d0282121c300667071661f37090224416b - languageName: node - linkType: hard - "@types/responselike@npm:^1.0.0": version: 1.0.2 resolution: "@types/responselike@npm:1.0.2" @@ -867,6 +852,15 @@ __metadata: languageName: node linkType: hard +"@types/temp@npm:^0.9.4": + version: 0.9.4 + resolution: "@types/temp@npm:0.9.4" + dependencies: + "@types/node": "npm:*" + checksum: 901fc8e7815b4bcdc47d1ed4273f13f2e3353ef57b33a81ee325e975989fbb32264f398a190b337360192b27b130eeceedfebd06a4eb195965affd3ff45698b7 + languageName: node + linkType: hard + "@typescript-eslint/experimental-utils@npm:^4.0.1": version: 4.33.0 resolution: "@typescript-eslint/experimental-utils@npm:4.33.0" @@ -1097,15 +1091,6 @@ __metadata: languageName: node linkType: hard -"ansi-escapes@npm:^4.2.1": - version: 4.3.2 - resolution: "ansi-escapes@npm:4.3.2" - dependencies: - type-fest: "npm:^0.21.3" - checksum: da917be01871525a3dfcf925ae2977bc59e8c513d4423368645634bf5d4ceba5401574eb705c1e92b79f7292af5a656f78c5725a4b0e1cec97c4b413705c1d50 - languageName: node - linkType: hard - "ansi-regex@npm:^5.0.1": version: 5.0.1 resolution: "ansi-regex@npm:5.0.1" @@ -1258,13 +1243,6 @@ __metadata: languageName: node linkType: hard -"arrify@npm:^1.0.1": - version: 1.0.1 - resolution: "arrify@npm:1.0.1" - checksum: c35c8d1a81bcd5474c0c57fe3f4bad1a4d46a5fa353cedcff7a54da315df60db71829e69104b859dff96c5d68af46bd2be259fe5e50dc6aa9df3b36bea0383ab - languageName: node - linkType: hard - "assertion-error@npm:^2.0.1": version: 2.0.1 resolution: "assertion-error@npm:2.0.1" @@ -1467,24 +1445,6 @@ __metadata: languageName: node linkType: hard -"camelcase-keys@npm:^6.2.2": - version: 6.2.2 - resolution: "camelcase-keys@npm:6.2.2" - dependencies: - camelcase: "npm:^5.3.1" - map-obj: "npm:^4.0.0" - quick-lru: "npm:^4.0.1" - checksum: bf1a28348c0f285c6c6f68fb98a9d088d3c0269fed0cdff3ea680d5a42df8a067b4de374e7a33e619eb9d5266a448fe66c2dd1f8e0c9209ebc348632882a3526 - languageName: node - linkType: hard - -"camelcase@npm:^5.3.1": - version: 5.3.1 - resolution: "camelcase@npm:5.3.1" - checksum: 92ff9b443bfe8abb15f2b1513ca182d16126359ad4f955ebc83dc4ddcc4ef3fdd2c078bc223f2673dc223488e75c99b16cc4d056624374b799e6a1555cf61b23 - languageName: node - linkType: hard - "caniuse-lite@npm:^1.0.30001541": version: 1.0.30001559 resolution: "caniuse-lite@npm:1.0.30001559" @@ -1516,7 +1476,7 @@ __metadata: languageName: node linkType: hard -"chalk@npm:^4.0.0, chalk@npm:^4.1.0": +"chalk@npm:^4.0.0": version: 4.1.2 resolution: "chalk@npm:4.1.2" dependencies: @@ -1728,23 +1688,6 @@ __metadata: languageName: node linkType: hard -"decamelize-keys@npm:^1.1.0": - version: 1.1.1 - resolution: "decamelize-keys@npm:1.1.1" - dependencies: - decamelize: "npm:^1.1.0" - map-obj: "npm:^1.0.0" - checksum: 4ca385933127437658338c65fb9aead5f21b28d3dd3ccd7956eb29aab0953b5d3c047fbc207111672220c71ecf7a4d34f36c92851b7bbde6fca1a02c541bdd7d - languageName: node - linkType: hard - -"decamelize@npm:^1.1.0, decamelize@npm:^1.2.0": - version: 1.2.0 - resolution: "decamelize@npm:1.2.0" - checksum: 85c39fe8fbf0482d4a1e224ef0119db5c1897f8503bcef8b826adff7a1b11414972f6fef2d7dec2ee0b4be3863cf64ac1439137ae9e6af23a3d8dcbe26a5b4b2 - languageName: node - linkType: hard - "decompress-response@npm:^6.0.0": version: 6.0.0 resolution: "decompress-response@npm:6.0.0" @@ -2181,22 +2124,6 @@ __metadata: languageName: node linkType: hard -"eslint-formatter-pretty@npm:^4.1.0": - version: 4.1.0 - resolution: "eslint-formatter-pretty@npm:4.1.0" - dependencies: - "@types/eslint": "npm:^7.2.13" - ansi-escapes: "npm:^4.2.1" - chalk: "npm:^4.1.0" - eslint-rule-docs: "npm:^1.1.5" - log-symbols: "npm:^4.0.0" - plur: "npm:^4.0.0" - string-width: "npm:^4.2.0" - supports-hyperlinks: "npm:^2.0.0" - checksum: 7cc55b873d3e9a5049cf0db65cef873abfc299639e7527bed52dea61f0742661b68e48018cf05de4c9cd8fb9362badc20f22c50fd5f36d745a346fa17bac8b60 - languageName: node - linkType: hard - "eslint-import-resolver-node@npm:^0.3.9": version: 0.3.9 resolution: "eslint-import-resolver-node@npm:0.3.9" @@ -2367,13 +2294,6 @@ __metadata: languageName: node linkType: hard -"eslint-rule-docs@npm:^1.1.5": - version: 1.1.235 - resolution: "eslint-rule-docs@npm:1.1.235" - checksum: 76a735c1e13a511ddff1017d5913b2526643827c8fdc86a23467f680b8dcbdfd07806cb092c82dd8d0e99789f23c8a38b9d2b838cd1cd62cc1932612ed606b8e - languageName: node - linkType: hard - "eslint-scope@npm:5.1.1, eslint-scope@npm:^5.1.1": version: 5.1.1 resolution: "eslint-scope@npm:5.1.1" @@ -2639,16 +2559,6 @@ __metadata: languageName: node linkType: hard -"find-up@npm:^4.1.0": - version: 4.1.0 - resolution: "find-up@npm:4.1.0" - dependencies: - locate-path: "npm:^5.0.0" - path-exists: "npm:^4.0.0" - checksum: 0406ee89ebeefa2d507feb07ec366bebd8a6167ae74aa4e34fb4c4abd06cf782a3ce26ae4194d70706f72182841733f00551c209fe575cb00bd92104056e78c1 - languageName: node - linkType: hard - "flat-cache@npm:^3.0.4": version: 3.1.1 resolution: "flat-cache@npm:3.1.1" @@ -2922,7 +2832,7 @@ __metadata: languageName: node linkType: hard -"globby@npm:^11.0.1, globby@npm:^11.0.3": +"globby@npm:^11.0.3": version: 11.1.0 resolution: "globby@npm:11.1.0" dependencies: @@ -2971,13 +2881,6 @@ __metadata: languageName: node linkType: hard -"hard-rejection@npm:^2.1.0": - version: 2.1.0 - resolution: "hard-rejection@npm:2.1.0" - checksum: febc3343a1ad575aedcc112580835b44a89a89e01f400b4eda6e8110869edfdab0b00cd1bd4c3bfec9475a57e79e0b355aecd5be46454b6a62b9a359af60e564 - languageName: node - linkType: hard - "has-bigints@npm:^1.0.1, has-bigints@npm:^1.0.2": version: 1.0.2 resolution: "has-bigints@npm:1.0.2" @@ -3047,15 +2950,6 @@ __metadata: languageName: node linkType: hard -"hosted-git-info@npm:^4.0.1": - version: 4.1.0 - resolution: "hosted-git-info@npm:4.1.0" - dependencies: - lru-cache: "npm:^6.0.0" - checksum: 150fbcb001600336d17fdbae803264abed013548eea7946c2264c49ebe2ebd8c4441ba71dd23dd8e18c65de79d637f98b22d4760ba5fb2e0b15d62543d0fff07 - languageName: node - linkType: hard - "html-escaper@npm:^2.0.0": version: 2.0.2 resolution: "html-escaper@npm:2.0.2" @@ -3206,13 +3100,6 @@ __metadata: languageName: node linkType: hard -"irregular-plurals@npm:^3.2.0": - version: 3.5.0 - resolution: "irregular-plurals@npm:3.5.0" - checksum: 7c033bbe7325e5a6e0a26949cc6863b6ce273403d4cd5b93bd99b33fecb6605b0884097c4259c23ed0c52c2133bf7d1cdcdd7a0630e8c325161fe269b3447918 - languageName: node - linkType: hard - "is-array-buffer@npm:^3.0.1, is-array-buffer@npm:^3.0.2": version: 3.0.2 resolution: "is-array-buffer@npm:3.0.2" @@ -3266,7 +3153,7 @@ __metadata: languageName: node linkType: hard -"is-core-module@npm:^2.13.0, is-core-module@npm:^2.13.1, is-core-module@npm:^2.5.0": +"is-core-module@npm:^2.13.0, is-core-module@npm:^2.13.1": version: 2.13.1 resolution: "is-core-module@npm:2.13.1" dependencies: @@ -3362,13 +3249,6 @@ __metadata: languageName: node linkType: hard -"is-plain-obj@npm:^1.1.0": - version: 1.1.0 - resolution: "is-plain-obj@npm:1.1.0" - checksum: daaee1805add26f781b413fdf192fc91d52409583be30ace35c82607d440da63cc4cac0ac55136716688d6c0a2c6ef3edb2254fecbd1fe06056d6bd15975ee8c - languageName: node - linkType: hard - "is-regex@npm:^1.1.4": version: 1.1.4 resolution: "is-regex@npm:1.1.4" @@ -3436,13 +3316,6 @@ __metadata: languageName: node linkType: hard -"is-unicode-supported@npm:^0.1.0": - version: 0.1.0 - resolution: "is-unicode-supported@npm:0.1.0" - checksum: 00cbe3455c3756be68d2542c416cab888aebd5012781d6819749fefb15162ff23e38501fe681b3d751c73e8ff561ac09a5293eba6f58fdf0178462ce6dcb3453 - languageName: node - linkType: hard - "is-weakmap@npm:^2.0.1": version: 2.0.1 resolution: "is-weakmap@npm:2.0.1" @@ -3631,13 +3504,6 @@ __metadata: languageName: node linkType: hard -"json-parse-even-better-errors@npm:^2.3.0": - version: 2.3.1 - resolution: "json-parse-even-better-errors@npm:2.3.1" - checksum: 140932564c8f0b88455432e0f33c4cb4086b8868e37524e07e723f4eaedb9425bdc2bafd71bd1d9765bd15fd1e2d126972bc83990f55c467168c228c24d665f3 - languageName: node - linkType: hard - "json-schema-traverse@npm:^0.4.1": version: 0.4.1 resolution: "json-schema-traverse@npm:0.4.1" @@ -3707,13 +3573,6 @@ __metadata: languageName: node linkType: hard -"kind-of@npm:^6.0.3": - version: 6.0.3 - resolution: "kind-of@npm:6.0.3" - checksum: 61cdff9623dabf3568b6445e93e31376bee1cdb93f8ba7033d86022c2a9b1791a1d9510e026e6465ebd701a6dd2f7b0808483ad8838341ac52f003f512e0b4c4 - languageName: node - linkType: hard - "language-subtag-registry@npm:^0.3.20": version: 0.3.22 resolution: "language-subtag-registry@npm:0.3.22" @@ -3740,13 +3599,6 @@ __metadata: languageName: node linkType: hard -"lines-and-columns@npm:^1.1.6": - version: 1.2.4 - resolution: "lines-and-columns@npm:1.2.4" - checksum: 3da6ee62d4cd9f03f5dc90b4df2540fb85b352081bee77fe4bbcd12c9000ead7f35e0a38b8d09a9bb99b13223446dd8689ff3c4959807620726d788701a83d2d - languageName: node - linkType: hard - "load-json-file@npm:^4.0.0": version: 4.0.0 resolution: "load-json-file@npm:4.0.0" @@ -3759,15 +3611,6 @@ __metadata: languageName: node linkType: hard -"locate-path@npm:^5.0.0": - version: 5.0.0 - resolution: "locate-path@npm:5.0.0" - dependencies: - p-locate: "npm:^4.1.0" - checksum: 33a1c5247e87e022f9713e6213a744557a3e9ec32c5d0b5efb10aa3a38177615bf90221a5592674857039c1a0fd2063b82f285702d37b792d973e9e72ace6c59 - languageName: node - linkType: hard - "lodash._baseiteratee@npm:~4.7.0": version: 4.7.0 resolution: "lodash._baseiteratee@npm:4.7.0" @@ -3848,16 +3691,6 @@ __metadata: languageName: node linkType: hard -"log-symbols@npm:^4.0.0": - version: 4.1.0 - resolution: "log-symbols@npm:4.1.0" - dependencies: - chalk: "npm:^4.1.0" - is-unicode-supported: "npm:^0.1.0" - checksum: 67f445a9ffa76db1989d0fa98586e5bc2fd5247260dafb8ad93d9f0ccd5896d53fb830b0e54dade5ad838b9de2006c826831a3c528913093af20dff8bd24aca6 - languageName: node - linkType: hard - "loose-envify@npm:^1.4.0": version: 1.4.0 resolution: "loose-envify@npm:1.4.0" @@ -3958,20 +3791,6 @@ __metadata: languageName: node linkType: hard -"map-obj@npm:^1.0.0": - version: 1.0.1 - resolution: "map-obj@npm:1.0.1" - checksum: ccca88395e7d38671ed9f5652ecf471ecd546924be2fb900836b9da35e068a96687d96a5f93dcdfa94d9a27d649d2f10a84595590f89a347fb4dda47629dcc52 - languageName: node - linkType: hard - -"map-obj@npm:^4.0.0": - version: 4.3.0 - resolution: "map-obj@npm:4.3.0" - checksum: 1c19e1c88513c8abdab25c316367154c6a0a6a0f77e3e8c391bb7c0e093aefed293f539d026dc013d86219e5e4c25f23b0003ea588be2101ccd757bacc12d43b - languageName: node - linkType: hard - "memorystream@npm:^0.3.1": version: 0.3.1 resolution: "memorystream@npm:0.3.1" @@ -3979,26 +3798,6 @@ __metadata: languageName: node linkType: hard -"meow@npm:^9.0.0": - version: 9.0.0 - resolution: "meow@npm:9.0.0" - dependencies: - "@types/minimist": "npm:^1.2.0" - camelcase-keys: "npm:^6.2.2" - decamelize: "npm:^1.2.0" - decamelize-keys: "npm:^1.1.0" - hard-rejection: "npm:^2.1.0" - minimist-options: "npm:4.1.0" - normalize-package-data: "npm:^3.0.0" - read-pkg-up: "npm:^7.0.1" - redent: "npm:^3.0.0" - trim-newlines: "npm:^3.0.0" - type-fest: "npm:^0.18.0" - yargs-parser: "npm:^20.2.3" - checksum: 998955ecff999dc3f3867ef3b51999218212497f27d75b9cbe10bdb73aac4ee308d484f7801fd1b3cfa4172819065f65f076ca018c1412fab19d0ea486648722 - languageName: node - linkType: hard - "merge-stream@npm:^2.0.0": version: 2.0.0 resolution: "merge-stream@npm:2.0.0" @@ -4067,13 +3866,6 @@ __metadata: languageName: node linkType: hard -"min-indent@npm:^1.0.0": - version: 1.0.1 - resolution: "min-indent@npm:1.0.1" - checksum: 7e207bd5c20401b292de291f02913230cb1163abca162044f7db1d951fa245b174dc00869d40dd9a9f32a885ad6a5f3e767ee104cf278f399cb4e92d3f582d5c - languageName: node - linkType: hard - "minimatch@npm:^3.0.4, minimatch@npm:^3.1.1, minimatch@npm:^3.1.2": version: 3.1.2 resolution: "minimatch@npm:3.1.2" @@ -4092,17 +3884,6 @@ __metadata: languageName: node linkType: hard -"minimist-options@npm:4.1.0": - version: 4.1.0 - resolution: "minimist-options@npm:4.1.0" - dependencies: - arrify: "npm:^1.0.1" - is-plain-obj: "npm:^1.1.0" - kind-of: "npm:^6.0.3" - checksum: 7871f9cdd15d1e7374e5b013e2ceda3d327a06a8c7b38ae16d9ef941e07d985e952c589e57213f7aa90a8744c60aed9524c0d85e501f5478382d9181f2763f54 - languageName: node - linkType: hard - "minimist@npm:^1.2.0, minimist@npm:^1.2.6": version: 1.2.8 resolution: "minimist@npm:1.2.8" @@ -4314,7 +4095,7 @@ __metadata: languageName: node linkType: hard -"normalize-package-data@npm:^2.3.2, normalize-package-data@npm:^2.5.0": +"normalize-package-data@npm:^2.3.2": version: 2.5.0 resolution: "normalize-package-data@npm:2.5.0" dependencies: @@ -4326,18 +4107,6 @@ __metadata: languageName: node linkType: hard -"normalize-package-data@npm:^3.0.0": - version: 3.0.3 - resolution: "normalize-package-data@npm:3.0.3" - dependencies: - hosted-git-info: "npm:^4.0.1" - is-core-module: "npm:^2.5.0" - semver: "npm:^7.3.4" - validate-npm-package-license: "npm:^3.0.1" - checksum: e5d0f739ba2c465d41f77c9d950e291ea4af78f8816ddb91c5da62257c40b76d8c83278b0d08ffbcd0f187636ebddad20e181e924873916d03e6e5ea2ef026be - languageName: node - linkType: hard - "normalize-url@npm:^6.0.1": version: 6.1.0 resolution: "normalize-url@npm:6.1.0" @@ -4527,24 +4296,6 @@ __metadata: languageName: node linkType: hard -"p-limit@npm:^2.2.0": - version: 2.3.0 - resolution: "p-limit@npm:2.3.0" - dependencies: - p-try: "npm:^2.0.0" - checksum: 8da01ac53efe6a627080fafc127c873da40c18d87b3f5d5492d465bb85ec7207e153948df6b9cbaeb130be70152f874229b8242ee2be84c0794082510af97f12 - languageName: node - linkType: hard - -"p-locate@npm:^4.1.0": - version: 4.1.0 - resolution: "p-locate@npm:4.1.0" - dependencies: - p-limit: "npm:^2.2.0" - checksum: 1b476ad69ad7f6059744f343b26d51ce091508935c1dbb80c4e0a2f397ffce0ca3a1f9f5cd3c7ce19d7929a09719d5c65fe70d8ee289c3f267cd36f2881813e9 - languageName: node - linkType: hard - "p-map@npm:^4.0.0": version: 4.0.0 resolution: "p-map@npm:4.0.0" @@ -4564,13 +4315,6 @@ __metadata: languageName: node linkType: hard -"p-try@npm:^2.0.0": - version: 2.2.0 - resolution: "p-try@npm:2.2.0" - checksum: c36c19907734c904b16994e6535b02c36c2224d433e01a2f1ab777237f4d86e6289fd5fd464850491e940379d4606ed850c03e0f9ab600b0ebddb511312e177f - languageName: node - linkType: hard - "package-json-from-dist@npm:^1.0.0": version: 1.0.0 resolution: "package-json-from-dist@npm:1.0.0" @@ -4597,25 +4341,6 @@ __metadata: languageName: node linkType: hard -"parse-json@npm:^5.0.0": - version: 5.2.0 - resolution: "parse-json@npm:5.2.0" - dependencies: - "@babel/code-frame": "npm:^7.0.0" - error-ex: "npm:^1.3.1" - json-parse-even-better-errors: "npm:^2.3.0" - lines-and-columns: "npm:^1.1.6" - checksum: 77947f2253005be7a12d858aedbafa09c9ae39eb4863adf330f7b416ca4f4a08132e453e08de2db46459256fb66afaac5ee758b44fe6541b7cdaf9d252e59585 - languageName: node - linkType: hard - -"path-exists@npm:^4.0.0": - version: 4.0.0 - resolution: "path-exists@npm:4.0.0" - checksum: 8c0bd3f5238188197dc78dced15207a4716c51cc4e3624c44fc97acf69558f5ebb9a2afff486fe1b4ee148e0c133e96c5e11a9aa5c48a3006e3467da070e5e1b - languageName: node - linkType: hard - "path-is-absolute@npm:^1.0.0": version: 1.0.1 resolution: "path-is-absolute@npm:1.0.1" @@ -4721,15 +4446,6 @@ __metadata: languageName: node linkType: hard -"plur@npm:^4.0.0": - version: 4.0.0 - resolution: "plur@npm:4.0.0" - dependencies: - irregular-plurals: "npm:^3.2.0" - checksum: 4d3010843dac60b980c9b83d4cdecc2c6bb4bf0a1d7620ba7229b5badcbc3c3767fddfdb61746f6253c3bd33ada3523375983cbe0b3f94868f4a475b9b6bd7d7 - languageName: node - linkType: hard - "postcss@npm:^8.4.41": version: 8.4.41 resolution: "postcss@npm:8.4.41" @@ -4848,13 +4564,6 @@ __metadata: languageName: node linkType: hard -"quick-lru@npm:^4.0.1": - version: 4.0.1 - resolution: "quick-lru@npm:4.0.1" - checksum: f9b1596fa7595a35c2f9d913ac312fede13d37dc8a747a51557ab36e11ce113bbe88ef4c0154968845559a7709cb6a7e7cbe75f7972182451cd45e7f057a334d - languageName: node - linkType: hard - "quick-lru@npm:^5.1.1": version: 5.1.1 resolution: "quick-lru@npm:5.1.1" @@ -4869,17 +4578,6 @@ __metadata: languageName: node linkType: hard -"read-pkg-up@npm:^7.0.0, read-pkg-up@npm:^7.0.1": - version: 7.0.1 - resolution: "read-pkg-up@npm:7.0.1" - dependencies: - find-up: "npm:^4.1.0" - read-pkg: "npm:^5.2.0" - type-fest: "npm:^0.8.1" - checksum: 82b3ac9fd7c6ca1bdc1d7253eb1091a98ff3d195ee0a45386582ce3e69f90266163c34121e6a0a02f1630073a6c0585f7880b3865efcae9c452fa667f02ca385 - languageName: node - linkType: hard - "read-pkg@npm:^3.0.0": version: 3.0.0 resolution: "read-pkg@npm:3.0.0" @@ -4891,18 +4589,6 @@ __metadata: languageName: node linkType: hard -"read-pkg@npm:^5.2.0": - version: 5.2.0 - resolution: "read-pkg@npm:5.2.0" - dependencies: - "@types/normalize-package-data": "npm:^2.4.0" - normalize-package-data: "npm:^2.5.0" - parse-json: "npm:^5.0.0" - type-fest: "npm:^0.6.0" - checksum: b51a17d4b51418e777029e3a7694c9bd6c578a5ab99db544764a0b0f2c7c0f58f8a6bc101f86a6fceb8ba6d237d67c89acf6170f6b98695d0420ddc86cf109fb - languageName: node - linkType: hard - "readable-stream@npm:^2.0.0": version: 2.3.8 resolution: "readable-stream@npm:2.3.8" @@ -4918,16 +4604,6 @@ __metadata: languageName: node linkType: hard -"redent@npm:^3.0.0": - version: 3.0.0 - resolution: "redent@npm:3.0.0" - dependencies: - indent-string: "npm:^4.0.0" - strip-indent: "npm:^3.0.0" - checksum: d64a6b5c0b50eb3ddce3ab770f866658a2b9998c678f797919ceb1b586bab9259b311407280bd80b804e2a7c7539b19238ae6a2a20c843f1a7fcff21d48c2eae - languageName: node - linkType: hard - "reflect.getprototypeof@npm:^1.0.4": version: 1.0.4 resolution: "reflect.getprototypeof@npm:1.0.4" @@ -5226,7 +4902,7 @@ __metadata: languageName: node linkType: hard -"semver@npm:^7.2.1, semver@npm:^7.3.4, semver@npm:^7.3.5, semver@npm:^7.5.3": +"semver@npm:^7.2.1, semver@npm:^7.3.5, semver@npm:^7.5.3": version: 7.5.4 resolution: "semver@npm:7.5.4" dependencies: @@ -5448,7 +5124,7 @@ __metadata: languageName: node linkType: hard -"string-width-cjs@npm:string-width@^4.2.0, string-width@npm:^4.1.0, string-width@npm:^4.2.0, string-width@npm:^4.2.3": +"string-width-cjs@npm:string-width@^4.2.0, string-width@npm:^4.1.0, string-width@npm:^4.2.3": version: 4.2.3 resolution: "string-width@npm:4.2.3" dependencies: @@ -5579,15 +5255,6 @@ __metadata: languageName: node linkType: hard -"strip-indent@npm:^3.0.0": - version: 3.0.0 - resolution: "strip-indent@npm:3.0.0" - dependencies: - min-indent: "npm:^1.0.0" - checksum: ae0deaf41c8d1001c5d4fbe16cb553865c1863da4fae036683b474fa926af9fc121e155cb3fc57a68262b2ae7d5b8420aa752c97a6428c315d00efe2a3875679 - languageName: node - linkType: hard - "strip-json-comments@npm:^3.1.0, strip-json-comments@npm:^3.1.1": version: 3.1.1 resolution: "strip-json-comments@npm:3.1.1" @@ -5604,7 +5271,7 @@ __metadata: languageName: node linkType: hard -"supports-color@npm:^7.0.0, supports-color@npm:^7.1.0": +"supports-color@npm:^7.1.0": version: 7.2.0 resolution: "supports-color@npm:7.2.0" dependencies: @@ -5613,16 +5280,6 @@ __metadata: languageName: node linkType: hard -"supports-hyperlinks@npm:^2.0.0": - version: 2.3.0 - resolution: "supports-hyperlinks@npm:2.3.0" - dependencies: - has-flag: "npm:^4.0.0" - supports-color: "npm:^7.0.0" - checksum: 4057f0d86afb056cd799602f72d575b8fdd79001c5894bcb691176f14e870a687e7981e50bc1484980e8b688c6d5bcd4931e1609816abb5a7dc1486b7babf6a1 - languageName: node - linkType: hard - "supports-preserve-symlinks-flag@npm:^1.0.0": version: 1.0.0 resolution: "supports-preserve-symlinks-flag@npm:1.0.0" @@ -5736,6 +5393,8 @@ __metadata: "@babel/core": "npm:^7.12.3" "@babel/eslint-parser": "npm:^7.15.8" "@babel/eslint-plugin": "npm:^7.13.10" + "@types/debug": "npm:^4.1.12" + "@types/temp": "npm:^0.9.4" "@vitest/coverage-v8": "npm:^2.0.5" badge-maker: "npm:^3.3.0" debug: "npm:^4.3.1" @@ -5760,19 +5419,12 @@ __metadata: p-retry: "npm:^4.2.0" prettier: "npm:^2.8.6" temp: "npm:^0.9.1" - tsd: "npm:^0.25.0" tus-js-client: "npm:^3.0.1" + typescript: "npm:^5.5.4" vitest: "npm:^2.0.5" languageName: unknown linkType: soft -"trim-newlines@npm:^3.0.0": - version: 3.0.1 - resolution: "trim-newlines@npm:3.0.1" - checksum: 03cfefde6c59ff57138412b8c6be922ecc5aec30694d784f2a65ef8dcbd47faef580b7de0c949345abdc56ec4b4abf64dd1e5aea619b200316e471a3dd5bf1f6 - languageName: node - linkType: hard - "tsconfig-paths@npm:^3.14.2": version: 3.14.2 resolution: "tsconfig-paths@npm:3.14.2" @@ -5785,22 +5437,6 @@ __metadata: languageName: node linkType: hard -"tsd@npm:^0.25.0": - version: 0.25.0 - resolution: "tsd@npm:0.25.0" - dependencies: - "@tsd/typescript": "npm:~4.9.3" - eslint-formatter-pretty: "npm:^4.1.0" - globby: "npm:^11.0.1" - meow: "npm:^9.0.0" - path-exists: "npm:^4.0.0" - read-pkg-up: "npm:^7.0.0" - bin: - tsd: dist/cli.js - checksum: 760dcbeff20f057ac515264124db3224e7deeb8fc8a9dcf393e87abab1fa3e9b6e9a2850bbed118e7c6353440e2e27ad5b7392621d100132d76b94c2a30169e5 - languageName: node - linkType: hard - "tslib@npm:^1.8.1": version: 1.14.1 resolution: "tslib@npm:1.14.1" @@ -5843,13 +5479,6 @@ __metadata: languageName: node linkType: hard -"type-fest@npm:^0.18.0": - version: 0.18.1 - resolution: "type-fest@npm:0.18.1" - checksum: 303f5ecf40d03e1d5b635ce7660de3b33c18ed8ebc65d64920c02974d9e684c72483c23f9084587e9dd6466a2ece1da42ddc95b412a461794dd30baca95e2bac - languageName: node - linkType: hard - "type-fest@npm:^0.20.2": version: 0.20.2 resolution: "type-fest@npm:0.20.2" @@ -5857,27 +5486,6 @@ __metadata: languageName: node linkType: hard -"type-fest@npm:^0.21.3": - version: 0.21.3 - resolution: "type-fest@npm:0.21.3" - checksum: 902bd57bfa30d51d4779b641c2bc403cdf1371fb9c91d3c058b0133694fcfdb817aef07a47f40faf79039eecbaa39ee9d3c532deff244f3a19ce68cea71a61e8 - languageName: node - linkType: hard - -"type-fest@npm:^0.6.0": - version: 0.6.0 - resolution: "type-fest@npm:0.6.0" - checksum: 0c585c26416fce9ecb5691873a1301b5aff54673c7999b6f925691ed01f5b9232db408cdbb0bd003d19f5ae284322523f44092d1f81ca0a48f11f7cf0be8cd38 - languageName: node - linkType: hard - -"type-fest@npm:^0.8.1": - version: 0.8.1 - resolution: "type-fest@npm:0.8.1" - checksum: dffbb99329da2aa840f506d376c863bd55f5636f4741ad6e65e82f5ce47e6914108f44f340a0b74009b0cb5d09d6752ae83203e53e98b1192cf80ecee5651636 - languageName: node - linkType: hard - "typed-array-buffer@npm:^1.0.0": version: 1.0.0 resolution: "typed-array-buffer@npm:1.0.0" @@ -5925,6 +5533,26 @@ __metadata: languageName: node linkType: hard +"typescript@npm:^5.5.4": + version: 5.5.4 + resolution: "typescript@npm:5.5.4" + bin: + tsc: bin/tsc + tsserver: bin/tsserver + checksum: 422be60f89e661eab29ac488c974b6cc0a660fb2228003b297c3d10c32c90f3bcffc1009b43876a082515a3c376b1eefcce823d6e78982e6878408b9a923199c + languageName: node + linkType: hard + +"typescript@patch:typescript@npm%3A^5.5.4#optional!builtin": + version: 5.5.4 + resolution: "typescript@patch:typescript@npm%3A5.5.4#optional!builtin::version=5.5.4&hash=29ae49" + bin: + tsc: bin/tsc + tsserver: bin/tsserver + checksum: 10dd9881baba22763de859e8050d6cb6e2db854197495c6f1929b08d1eb2b2b00d0b5d9b0bcee8472f1c3f4a7ef6a5d7ebe0cfd703f853aa5ae465b8404bc1ba + languageName: node + linkType: hard + "unbox-primitive@npm:^1.0.2": version: 1.0.2 resolution: "unbox-primitive@npm:1.0.2" @@ -6271,10 +5899,3 @@ __metadata: checksum: 2286b5e8dbfe22204ab66e2ef5cc9bbb1e55dfc873bbe0d568aa943eb255d131890dfd5bf243637273d31119b870f49c18fcde2c6ffbb7a7a092b870dc90625a languageName: node linkType: hard - -"yargs-parser@npm:^20.2.3": - version: 20.2.9 - resolution: "yargs-parser@npm:20.2.9" - checksum: 0685a8e58bbfb57fab6aefe03c6da904a59769bd803a722bb098bd5b0f29d274a1357762c7258fb487512811b8063fb5d2824a3415a0a4540598335b3b086c72 - languageName: node - linkType: hard From cd59974b77c4f3eb7dd24fe0272059fddd586341 Mon Sep 17 00:00:00 2001 From: Remco Haszing Date: Fri, 13 Sep 2024 16:46:04 +0200 Subject: [PATCH 02/23] Use named exports This converts the use of `import =` and `export =` to ESM syntax. The ESM syntax is compiled to CJS. --- README.md | 10 +- src/InconsistentResponseError.ts | 4 +- src/PaginationStream.ts | 8 +- src/PollingTimeoutError.ts | 4 +- src/Transloadit.ts | 505 ++++++++++------------ src/TransloaditError.ts | 4 +- src/tus.ts | 36 +- test/integration/live-api.test.ts | 59 ++- test/testserver.ts | 36 +- test/tunnel.ts | 32 +- test/unit/mock-http.test.ts | 28 +- test/unit/test-pagination-stream.test.ts | 8 +- test/unit/test-transloadit-client.test.ts | 64 +-- tsconfig.build.json | 1 - tsconfig.json | 1 - vitest.config.mjs | 4 - 16 files changed, 377 insertions(+), 427 deletions(-) diff --git a/README.md b/README.md index 8983366..11321e5 100644 --- a/README.md +++ b/README.md @@ -48,9 +48,9 @@ npm install --save transloadit The following code will upload an image and resize it to a thumbnail: ```javascript -const Transloadit = require('transloadit') +const { TransloaditClient } = require('transloadit') -const transloadit = new Transloadit({ +const transloadit = new TransloaditClient({ authKey: 'YOUR_TRANSLOADIT_KEY', authSecret: 'YOUR_TRANSLOADIT_SECRET', }) @@ -113,7 +113,7 @@ For more example use cases and information about the available robots and their ## API -These are the public methods on the `Transloadit` object and their descriptions. The methods are based on the [Transloadit API](https://transloadit.com/docs/api/). See also [TypeScript definitions](types/index.d.ts). +These are the public methods on the `TransloaditClient` object and their descriptions. The methods are based on the [Transloadit API](https://transloadit.com/docs/api/). Table of contents: @@ -391,7 +391,7 @@ This function returns an object with the key `signature` (containing the calcula ### Errors -Errors from Node.js will be passed on and we use [GOT](https://github.com/sindresorhus/got) for HTTP requests and errors from there will also be passed on. When the HTTP response code is not 200, the error will be a `Transloadit.HTTPError`, which is a [got.HTTPError](https://github.com/sindresorhus/got#errors)) with some additional properties: +Errors from Node.js will be passed on and we use [GOT](https://github.com/sindresorhus/got) for HTTP requests and errors from there will also be passed on. When the HTTP response code is not 200, the error will be an `HTTPError`, which is a [got.HTTPError](https://github.com/sindresorhus/got#errors)) with some additional properties: - `HTTPError.response?.body` the JSON object returned by the server along with the error response (**note**: `HTTPError.response` will be `undefined` for non-server errors) - `HTTPError.transloaditErrorCode` alias for `HTTPError.response.body.error` ([View all error codes](https://transloadit.com/docs/api/response-codes/#error-codes)) @@ -401,7 +401,7 @@ To identify errors you can either check its props or use `instanceof`, e.g.: ```js catch (err) { - if (err instanceof Transloadit.TimeoutError) { + if (err instanceof TimeoutError) { return console.error('The request timed out', err) } if (err.code === 'ENOENT') { diff --git a/src/InconsistentResponseError.ts b/src/InconsistentResponseError.ts index 0d0646b..3b32efc 100644 --- a/src/InconsistentResponseError.ts +++ b/src/InconsistentResponseError.ts @@ -1,5 +1,3 @@ -class InconsistentResponseError extends Error { +export class InconsistentResponseError extends Error { name = 'InconsistentResponseError' } - -export = InconsistentResponseError diff --git a/src/PaginationStream.ts b/src/PaginationStream.ts index c04a62f..d153efe 100644 --- a/src/PaginationStream.ts +++ b/src/PaginationStream.ts @@ -1,9 +1,9 @@ -import stream = require('stream') -import type { PaginationList } from './Transloadit' +import { Readable } from 'stream' +import { PaginationList } from './Transloadit' type FetchPage = (pageno: number) => PaginationList | PromiseLike> -class PaginationStream extends stream.Readable { +export class PaginationStream extends Readable { private _fetchPage: FetchPage private _nitems?: number private _pageno = 0 @@ -40,5 +40,3 @@ class PaginationStream extends stream.Readable { } } } - -export = PaginationStream diff --git a/src/PollingTimeoutError.ts b/src/PollingTimeoutError.ts index 5ba2adb..1c87004 100644 --- a/src/PollingTimeoutError.ts +++ b/src/PollingTimeoutError.ts @@ -1,6 +1,4 @@ -class PollingTimeoutError extends Error { +export class PollingTimeoutError extends Error { name = 'PollingTimeoutError' code = 'POLLING_TIMED_OUT' } - -export = PollingTimeoutError diff --git a/src/Transloadit.ts b/src/Transloadit.ts index 90dea2c..05a8738 100644 --- a/src/Transloadit.ts +++ b/src/Transloadit.ts @@ -1,22 +1,35 @@ -import crypto = require('crypto') -import got = require('got') -import FormData = require('form-data') -import fs = require('fs') -import fsPromises = require('fs/promises') -import debug = require('debug') -import intoStream = require('into-stream') -import isStream = require('is-stream') -import assert = require('assert') -import pMap = require('p-map') -import InconsistentResponseError = require('./InconsistentResponseError') -import PaginationStream = require('./PaginationStream') -import PollingTimeoutError = require('./PollingTimeoutError') -import TransloaditError = require('./TransloaditError') -import pkg = require('../package.json') -import tus = require('./tus') +import { createHmac } from 'crypto' +import got, { RequiredRetryOptions, Headers, OptionsOfJSONResponseBody } from 'got' +import FormData from 'form-data' +import { constants, createReadStream } from 'fs' +import { access } from 'fs/promises' +import debug from 'debug' +import intoStream from 'into-stream' +import isStream from 'is-stream' +import * as assert from 'assert' +import pMap from 'p-map' +import { InconsistentResponseError } from './InconsistentResponseError' +import { PaginationStream } from './PaginationStream' +import { PollingTimeoutError } from './PollingTimeoutError' +import { TransloaditError } from './TransloaditError' +import { version } from '../package.json' +import { sendTusRequest, Stream } from './tus' import type { Readable } from 'stream' +// See https://github.com/sindresorhus/got#errors +// Expose relevant errors +export { + RequestError, + ReadError, + ParseError, + UploadError, + HTTPError, + MaxRedirectsError, + TimeoutError, +} from 'got' +export { InconsistentResponseError } + const log = debug('transloadit') const logWarn = debug('transloadit:warn') @@ -25,12 +38,12 @@ interface RequestOptions { url?: string timeout?: number method?: 'delete' | 'get' | 'post' | 'put' - params?: TransloaditClient.KeyVal + params?: KeyVal fields?: Record - headers?: got.Headers + headers?: Headers } -interface CreateAssemblyPromise extends Promise { +interface CreateAssemblyPromise extends Promise { assemblyId: string } @@ -63,7 +76,7 @@ function decorateHttpError(err: TransloaditError, body: any): TransloaditError { } // Not sure if this is still a problem with the API, but throw a special error type so the user can retry if needed -function checkAssemblyUrls(result: TransloaditClient.Assembly) { +function checkAssemblyUrls(result: Assembly) { if (result.assembly_url == null || result.assembly_ssl_url == null) { throw new InconsistentResponseError('Server returned an incomplete assembly response (no URL)') } @@ -86,34 +99,16 @@ function checkResult(result: T | { error: string }): asserts result is T { } } -class TransloaditClient { - // See https://github.com/sindresorhus/got#errors - // Expose relevant errors - static RequestError = got.RequestError - - static ReadError = got.ReadError - - static ParseError = got.ParseError - - static UploadError = got.UploadError - - static HTTPError = got.HTTPError - - static MaxRedirectsError = got.MaxRedirectsError - - static TimeoutError = got.TimeoutError - - static InconsistentResponseError = InconsistentResponseError - +export class TransloaditClient { private _authKey: string private _authSecret: string private _endpoint: string private _maxRetries: number private _defaultTimeout: number - private _gotRetry: got.RequiredRetryOptions | number + private _gotRetry: RequiredRetryOptions | number private _lastUsedAssemblyUrl = '' - constructor(opts: TransloaditClient.TransloaditClientOptions) { + constructor(opts: TransloaditClientOptions) { if (opts?.authKey == null) { throw new Error('Please provide an authKey') } @@ -149,10 +144,7 @@ class TransloaditClient { * * @param opts assembly options */ - createAssembly( - opts: TransloaditClient.CreateAssemblyOptions = {}, - arg2?: void - ): CreateAssemblyPromise { + createAssembly(opts: CreateAssemblyOptions = {}, arg2?: void): CreateAssemblyPromise { // Warn users of old callback API if (typeof arg2 === 'function') { throw new TypeError( @@ -202,7 +194,7 @@ class TransloaditClient { await pMap( Object.entries(files), // eslint-disable-next-line no-bitwise - async ([, path]) => fsPromises.access(path, fs.constants.F_OK | fs.constants.R_OK), + async ([, path]) => access(path, constants.F_OK | constants.R_OK), { concurrency: 5 } ) @@ -220,13 +212,13 @@ class TransloaditClient { ) // Wrap in object structure (so we can know if it's a pathless stream or not) - const allStreamsMap = Object.fromEntries( + const allStreamsMap = Object.fromEntries( Object.entries(streamsMap).map(([label, stream]) => [label, { stream }]) ) // Create streams from files too for (const [label, path] of Object.entries(files)) { - const stream = fs.createReadStream(path) + const stream = createReadStream(path) allStreamsMap[label] = { stream, path } // File streams have path } @@ -236,7 +228,7 @@ class TransloaditClient { allStreams.forEach(({ stream }) => stream.pause()) // If any stream emits error, we want to handle this and exit with error - const streamErrorPromise = new Promise((resolve, reject) => { + const streamErrorPromise = new Promise((resolve, reject) => { allStreams.forEach(({ stream }) => stream.on('error', reject)) }) @@ -255,9 +247,9 @@ class TransloaditClient { } // upload as form multipart or tus? - const formUploadStreamsMap: Record = isResumable ? {} : allStreamsMap + const formUploadStreamsMap: Record = isResumable ? {} : allStreamsMap - const result = await this._remoteJson( + const result = await this._remoteJson( requestOpts, formUploadStreamsMap, onUploadProgress @@ -265,7 +257,7 @@ class TransloaditClient { checkResult(result) if (isResumable && Object.keys(allStreamsMap).length > 0) { - await tus.sendTusRequest({ + await sendTusRequest({ streamsMap: allStreamsMap, assembly: result, onProgress: onUploadProgress, @@ -299,9 +291,9 @@ class TransloaditClient { timeout, startTimeMs = getHrTimeMs(), interval = 1000, - }: TransloaditClient.AwaitAssemblyCompletionOptions = {} - ): Promise { - assert(assemblyId) + }: AwaitAssemblyCompletionOptions = {} + ): Promise { + assert.ok(assemblyId) // eslint-disable-next-line no-constant-condition while (true) { @@ -335,7 +327,7 @@ class TransloaditClient { * @param assemblyId assembly ID * @returns after the assembly is deleted */ - async cancelAssembly(assemblyId: string): Promise { + async cancelAssembly(assemblyId: string): Promise { // You may wonder why do we need to call getAssembly first: // If we use the default base URL (instead of the one returned in assembly_url_ssl), // the delete call will hang in certain cases @@ -357,16 +349,13 @@ class TransloaditClient { * @param optional params * @returns after the replay is started */ - async replayAssembly( - assemblyId: string, - params: TransloaditClient.KeyVal = {} - ): Promise { + async replayAssembly(assemblyId: string, params: KeyVal = {}): Promise { const requestOpts: RequestOptions = { urlSuffix: `/assemblies/${assemblyId}/replay`, method: 'post', } if (Object.keys(params).length > 0) requestOpts.params = params - const result = await this._remoteJson(requestOpts) + const result = await this._remoteJson(requestOpts) checkResult(result) return result } @@ -380,7 +369,7 @@ class TransloaditClient { */ async replayAssemblyNotification( assemblyId: string, - params: TransloaditClient.KeyVal = {} + params: KeyVal = {} ): Promise<{ ok: string; success: boolean }> { const requestOpts: RequestOptions = { urlSuffix: `/assembly_notifications/${assemblyId}/replay`, @@ -396,9 +385,7 @@ class TransloaditClient { * @param params optional request options * @returns the list of Assembly notifications */ - async listAssemblyNotifications( - params: object - ): Promise> { + async listAssemblyNotifications(params: object): Promise> { const requestOpts: RequestOptions = { urlSuffix: '/assembly_notifications', method: 'get', @@ -418,9 +405,7 @@ class TransloaditClient { * @param params optional request options * @returns list of Assemblies */ - async listAssemblies( - params?: TransloaditClient.KeyVal - ): Promise> { + async listAssemblies(params?: KeyVal): Promise> { const requestOpts: RequestOptions = { urlSuffix: '/assemblies', method: 'get', @@ -430,7 +415,7 @@ class TransloaditClient { return this._remoteJson(requestOpts) } - streamAssemblies(params: TransloaditClient.KeyVal): Readable { + streamAssemblies(params: KeyVal): Readable { return new PaginationStream(async (page) => this.listAssemblies({ ...params, page })) } @@ -440,8 +425,8 @@ class TransloaditClient { * @param assemblyId the Assembly Id * @returns the retrieved Assembly */ - async getAssembly(assemblyId: string): Promise { - const result = await this._remoteJson({ + async getAssembly(assemblyId: string): Promise { + const result = await this._remoteJson({ urlSuffix: `/assemblies/${assemblyId}`, }) checkAssemblyUrls(result) @@ -537,9 +522,7 @@ class TransloaditClient { * @param params optional request options * @returns when the template is created */ - async createTemplate( - params: TransloaditClient.KeyVal = {} - ): Promise { + async createTemplate(params: KeyVal = {}): Promise { const requestOpts: RequestOptions = { urlSuffix: '/templates', method: 'post', @@ -556,10 +539,7 @@ class TransloaditClient { * @param params optional request options * @returns when the template is edited */ - async editTemplate( - templateId: string, - params: TransloaditClient.KeyVal - ): Promise { + async editTemplate(templateId: string, params: KeyVal): Promise { const requestOpts: RequestOptions = { urlSuffix: `/templates/${templateId}`, method: 'put', @@ -590,7 +570,7 @@ class TransloaditClient { * @param templateId the template ID * @returns when the template is retrieved */ - async getTemplate(templateId: string): Promise { + async getTemplate(templateId: string): Promise { const requestOpts: RequestOptions = { urlSuffix: `/templates/${templateId}`, method: 'get', @@ -605,9 +585,7 @@ class TransloaditClient { * @param params optional request options * @returns the list of templates */ - async listTemplates( - params?: TransloaditClient.KeyVal - ): Promise> { + async listTemplates(params?: KeyVal): Promise> { const requestOpts: RequestOptions = { urlSuffix: '/templates', method: 'get', @@ -617,9 +595,7 @@ class TransloaditClient { return this._remoteJson(requestOpts) } - streamTemplates( - params?: TransloaditClient.KeyVal - ): PaginationStream { + streamTemplates(params?: KeyVal): PaginationStream { return new PaginationStream(async (page) => this.listTemplates({ ...params, page })) } @@ -630,8 +606,8 @@ class TransloaditClient { * @returns with billing data * @see https://transloadit.com/docs/api/bill-date-get/ */ - async getBill(month: string): Promise { - assert(month, 'month is required') + async getBill(month: string): Promise { + assert.ok(month, 'month is required') const requestOpts: RequestOptions = { urlSuffix: `/bill/${month}`, method: 'get', @@ -640,7 +616,7 @@ class TransloaditClient { return this._remoteJson(requestOpts) } - calcSignature(params: TransloaditClient.KeyVal): { signature: string; params: string } { + calcSignature(params: KeyVal): { signature: string; params: string } { const jsonParams = this._prepareParams(params) const signature = this._calcSignature(jsonParams) @@ -648,8 +624,7 @@ class TransloaditClient { } private _calcSignature(toSign: string, algorithm = 'sha384'): string { - return `${algorithm}:${crypto - .createHmac(algorithm, this._authSecret) + return `${algorithm}:${createHmac(algorithm, this._authSecret) .update(Buffer.from(toSign, 'utf-8')) .digest('hex')}` } @@ -658,8 +633,8 @@ class TransloaditClient { // the streams, the signed params, and any additional fields. private _appendForm( form: FormData, - params: TransloaditClient.KeyVal, - streamsMap?: Record, + params: KeyVal, + streamsMap?: Record, fields?: Record ): void { const sigData = this.calcSignature(params) @@ -686,7 +661,7 @@ class TransloaditClient { // Implements HTTP GET query params, handling the case where the url already // has params. - private _appendParamsToUrl(url: string, params: TransloaditClient.KeyVal): string { + private _appendParamsToUrl(url: string, params: KeyVal): string { const { signature, params: jsonParams } = this.calcSignature(params) const prefix = url.indexOf('?') === -1 ? '?' : '&' @@ -695,7 +670,7 @@ class TransloaditClient { } // Responsible for including auth parameters in all requests - private _prepareParams(paramsIn: TransloaditClient.KeyVal): string { + private _prepareParams(paramsIn: KeyVal): string { let params = paramsIn if (params == null) { params = {} @@ -726,8 +701,8 @@ class TransloaditClient { // requests. Also automatically parses the JSON response. private async _remoteJson( opts: RequestOptions, - streamsMap?: Record, - onProgress: TransloaditClient.CreateAssemblyOptions['onUploadProgress'] = () => {} + streamsMap?: Record, + onProgress: CreateAssemblyOptions['onUploadProgress'] = () => {} ): Promise { const { urlSuffix, @@ -761,12 +736,12 @@ class TransloaditClient { const isUploadingStreams = streamsMap && Object.keys(streamsMap).length > 0 - const requestOpts: got.OptionsOfJSONResponseBody = { + const requestOpts: OptionsOfJSONResponseBody = { retry: this._gotRetry, body: form, timeout, headers: { - 'Transloadit-Client': `node-sdk:${pkg.version}`, + 'Transloadit-Client': `node-sdk:${version}`, 'User-Agent': undefined, // Remove got's user-agent ...headers, }, @@ -779,7 +754,7 @@ class TransloaditClient { if (isUploadingStreams) requestOpts.headers!['transfer-encoding'] = 'chunked' try { - const request = got.default[method](url, requestOpts) + const request = got[method](url, requestOpts) if (isUploadingStreams) { request.on('uploadProgress', ({ transferred, total }) => onProgress({ uploadedBytes: transferred, totalBytes: total }) @@ -819,174 +794,170 @@ class TransloaditClient { } } -namespace TransloaditClient { - export interface CreateAssemblyOptions { - params?: CreateAssemblyParams - files?: { - [name: string]: string - } - uploads?: { - [name: string]: Readable | intoStream.Input - } - waitForCompletion?: boolean - isResumable?: boolean - chunkSize?: number - uploadConcurrency?: number - timeout?: number - onUploadProgress?: (uploadProgress: UploadProgress) => void - onAssemblyProgress?: AssemblyProgress - assemblyId?: string - } - - export type AssemblyProgress = (assembly: Assembly) => void - - export interface CreateAssemblyParams { - /** See https://transloadit.com/docs/topics/assembly-instructions/ */ - steps?: KeyVal - template_id?: string - notify_url?: string - fields?: KeyVal - allow_steps_override?: boolean - } - - // TODO - /** Object with properties. See https://transloadit.com/docs/api/ */ - export interface KeyVal { - [key: string]: any - } - - export interface UploadProgress { - uploadedBytes?: number - totalBytes?: number - } - - /** https://transloadit.com/docs/api/assembly-status-response/#explanation-of-fields */ - export interface Assembly { - ok?: string - message?: string - assembly_id: string - parent_id?: string - account_id: string - template_id?: string - instance: string - assembly_url: string - assembly_ssl_url: string - uppyserver_url: string - companion_url: string - websocket_url: string - tus_url: string - bytes_received: number - bytes_expected: number - upload_duration: number - client_agent?: string - client_ip?: string - client_referer?: string - transloadit_client: string - start_date: string - upload_meta_data_extracted: boolean - warnings: any[] - is_infinite: boolean - has_dupe_jobs: boolean - execution_start: string - execution_duration: number - queue_duration: number - jobs_queue_duration: number - notify_start?: any - notify_url?: string - notify_status?: any - notify_response_code?: any - notify_duration?: any - last_job_completed?: string - fields: KeyVal - running_jobs: any[] - bytes_usage: number - executing_jobs: any[] - started_jobs: string[] - parent_assembly_status: any - params: string - template?: any - merged_params: string - uploads: any[] - results: any - build_id: string - error?: string - stderr?: string - stdout?: string - reason?: string - } - - /** See https://transloadit.com/docs/api/assemblies-assembly-id-get/ */ - export interface ListedAssembly { - id?: string - parent_id?: string - account_id: string - template_id?: string - instance: string - notify_url?: string - redirect_url?: string - files: string - warning_count: number - execution_duration: number - execution_start: string - ok?: string - error?: string - created: string - } - - export interface ReplayedAssembly { - ok?: string - message?: string - success: boolean - assembly_id: string - assembly_url: string - assembly_ssl_url: string - notify_url?: string - } - - export interface ListedTemplate { - id: string - name: string - encryption_version: number - require_signature_auth: number - last_used?: string - created: string - modified: string - content: TemplateContent - } - - export interface TemplateResponse { - ok: string - message: string - id: string - content: TemplateContent - name: string - require_signature_auth: number - } - - export interface TemplateContent { - steps: KeyVal - } - - export interface TransloaditClientOptions { - authKey: string - authSecret: string - endpoint?: string - maxRetries?: number - timeout?: number - gotRetry?: got.RequiredRetryOptions - } - - export interface AwaitAssemblyCompletionOptions { - onAssemblyProgress?: AssemblyProgress - timeout?: number - interval?: number - startTimeMs?: number - } - - export interface PaginationList { - count: number - items: T[] +export interface CreateAssemblyOptions { + params?: CreateAssemblyParams + files?: { + [name: string]: string + } + uploads?: { + [name: string]: Readable | intoStream.Input } + waitForCompletion?: boolean + isResumable?: boolean + chunkSize?: number + uploadConcurrency?: number + timeout?: number + onUploadProgress?: (uploadProgress: UploadProgress) => void + onAssemblyProgress?: AssemblyProgress + assemblyId?: string +} + +export type AssemblyProgress = (assembly: Assembly) => void + +export interface CreateAssemblyParams { + /** See https://transloadit.com/docs/topics/assembly-instructions/ */ + steps?: KeyVal + template_id?: string + notify_url?: string + fields?: KeyVal + allow_steps_override?: boolean +} + +// TODO +/** Object with properties. See https://transloadit.com/docs/api/ */ +export interface KeyVal { + [key: string]: any +} + +export interface UploadProgress { + uploadedBytes?: number + totalBytes?: number +} + +/** https://transloadit.com/docs/api/assembly-status-response/#explanation-of-fields */ +export interface Assembly { + ok?: string + message?: string + assembly_id: string + parent_id?: string + account_id: string + template_id?: string + instance: string + assembly_url: string + assembly_ssl_url: string + uppyserver_url: string + companion_url: string + websocket_url: string + tus_url: string + bytes_received: number + bytes_expected: number + upload_duration: number + client_agent?: string + client_ip?: string + client_referer?: string + transloadit_client: string + start_date: string + upload_meta_data_extracted: boolean + warnings: any[] + is_infinite: boolean + has_dupe_jobs: boolean + execution_start: string + execution_duration: number + queue_duration: number + jobs_queue_duration: number + notify_start?: any + notify_url?: string + notify_status?: any + notify_response_code?: any + notify_duration?: any + last_job_completed?: string + fields: KeyVal + running_jobs: any[] + bytes_usage: number + executing_jobs: any[] + started_jobs: string[] + parent_assembly_status: any + params: string + template?: any + merged_params: string + uploads: any[] + results: any + build_id: string + error?: string + stderr?: string + stdout?: string + reason?: string +} + +/** See https://transloadit.com/docs/api/assemblies-assembly-id-get/ */ +export interface ListedAssembly { + id?: string + parent_id?: string + account_id: string + template_id?: string + instance: string + notify_url?: string + redirect_url?: string + files: string + warning_count: number + execution_duration: number + execution_start: string + ok?: string + error?: string + created: string +} + +export interface ReplayedAssembly { + ok?: string + message?: string + success: boolean + assembly_id: string + assembly_url: string + assembly_ssl_url: string + notify_url?: string +} + +export interface ListedTemplate { + id: string + name: string + encryption_version: number + require_signature_auth: number + last_used?: string + created: string + modified: string + content: TemplateContent +} + +export interface TemplateResponse { + ok: string + message: string + id: string + content: TemplateContent + name: string + require_signature_auth: number +} + +export interface TemplateContent { + steps: KeyVal +} + +export interface TransloaditClientOptions { + authKey: string + authSecret: string + endpoint?: string + maxRetries?: number + timeout?: number + gotRetry?: RequiredRetryOptions } -export = TransloaditClient +export interface AwaitAssemblyCompletionOptions { + onAssemblyProgress?: AssemblyProgress + timeout?: number + interval?: number + startTimeMs?: number +} + +export interface PaginationList { + count: number + items: T[] +} diff --git a/src/TransloaditError.ts b/src/TransloaditError.ts index cddcb1c..dc82a1a 100644 --- a/src/TransloaditError.ts +++ b/src/TransloaditError.ts @@ -1,4 +1,4 @@ -class TransloaditError extends Error { +export class TransloaditError extends Error { name = 'TransloaditError' response: { body: unknown } assemblyId?: string @@ -9,5 +9,3 @@ class TransloaditError extends Error { this.response = { body } } } - -export = TransloaditError diff --git a/src/tus.ts b/src/tus.ts index f2be1c7..9b284a7 100644 --- a/src/tus.ts +++ b/src/tus.ts @@ -1,22 +1,22 @@ -import debug = require('debug') -import p = require('path') -import tus = require('tus-js-client') -import fsPromises = require('fs/promises') -import pMap = require('p-map') +import debug from 'debug' +import { basename } from 'path' +import { Upload, UploadOptions } from 'tus-js-client' +import { stat } from 'fs/promises' +import pMap from 'p-map' import type { Readable } from 'stream' import type { Assembly, UploadProgress } from './Transloadit' const log = debug('transloadit') interface SendTusRequestOptions { - streamsMap: Record + streamsMap: Record assembly: Assembly requestedChunkSize: number uploadConcurrency: number onProgress: (options: UploadProgress) => void } -async function sendTusRequest({ +export async function sendTusRequest({ streamsMap, assembly, requestedChunkSize, @@ -39,7 +39,7 @@ async function sendTusRequest({ const { path } = streamsMap[label] if (path) { - const { size } = await fsPromises.stat(path) + const { size } = await stat(path) sizes[label] = size totalBytes += size } @@ -86,10 +86,10 @@ async function sendTusRequest({ } } - const filename = path ? p.basename(path) : label + const filename = path ? basename(path) : label await new Promise((resolve, reject) => { - const tusOptions: tus.UploadOptions = { + const tusOptions: UploadOptions = { endpoint: assembly.tus_url, metadata: { assembly_url: assembly.assembly_ssl_url, @@ -105,7 +105,7 @@ async function sendTusRequest({ if (chunkSize) tusOptions.chunkSize = chunkSize if (uploadLengthDeferred) tusOptions.uploadLengthDeferred = uploadLengthDeferred - const tusUpload = new tus.Upload(stream, tusOptions) + const tusUpload = new Upload(stream, tusOptions) tusUpload.start() }) @@ -116,15 +116,7 @@ async function sendTusRequest({ await pMap(streamLabels, uploadSingleStream, { concurrency: uploadConcurrency }) } -const export_ = { - sendTusRequest, +export interface Stream { + path?: string + stream: Readable } - -namespace export_ { - export interface Stream { - path?: string - stream: Readable - } -} - -export = export_ diff --git a/test/integration/live-api.test.ts b/test/integration/live-api.test.ts index ea63e0e..edfd6f2 100644 --- a/test/integration/live-api.test.ts +++ b/test/integration/live-api.test.ts @@ -1,23 +1,22 @@ -import crypto = require('crypto') -import querystring = require('querystring') -import temp = require('temp') -import fs = require('fs') -import http = require('http') -import nodePath = require('path') -import nodeStream = require('stream') -import got = require('got') +import * as crypto from 'crypto' +import { parse } from 'querystring' +import * as temp from 'temp' +import { createWriteStream } from 'fs' +import { IncomingMessage, RequestListener } from 'http' +import { join } from 'path' +import { pipeline } from 'stream' +import got, { RequiredRetryOptions } from 'got' import intoStream = require('into-stream') import debug = require('debug') -const log = debug('transloadit:live-api') - -import Transloadit = require('../../src/Transloadit.ts') +import { CreateAssemblyOptions, TransloaditClient, UploadProgress } from '../../src/Transloadit' +import { createTestServer, CreateTestServerResult } from '../testserver' -import createTestServer = require('../testserver.ts') +const log = debug('transloadit:live-api') async function downloadTmpFile(url: string) { const { path } = await temp.open('transloadit') - await nodeStream.pipeline(got.default.stream(url), fs.createWriteStream(path)) + await pipeline(got.stream(url), createWriteStream(path)) return path } @@ -29,7 +28,7 @@ function createClient(opts = {}) { } // https://github.com/sindresorhus/got/blob/main/documentation/7-retry.md#retry - const gotRetry: got.RequiredRetryOptions = { + const gotRetry: RequiredRetryOptions = { limit: 2, methods: [ 'GET', @@ -54,10 +53,10 @@ function createClient(opts = {}) { ], } - return new Transloadit({ authKey, authSecret, gotRetry, ...opts }) + return new TransloaditClient({ authKey, authSecret, gotRetry, ...opts }) } -function createAssembly(client: Transloadit, params: Transloadit.CreateAssemblyOptions) { +function createAssembly(client: TransloaditClient, params: CreateAssemblyOptions) { const promise = client.createAssembly(params) const { assemblyId } = promise console.log(expect.getState().currentTestName, 'createAssembly', assemblyId) // For easier debugging @@ -102,7 +101,7 @@ const genericOptions = { const handlers = new Map() -let testServer: createTestServer.Result +let testServer: CreateTestServerResult beforeAll(async () => { // cloudflared tunnels are a bit unstable, so we share one cloudflared tunnel between all tests @@ -135,7 +134,7 @@ interface VirtualTestServer { url: string } -async function createVirtualTestServer(handler: http.RequestListener): Promise { +async function createVirtualTestServer(handler: RequestListener): Promise { const id = crypto.randomUUID() log('Adding virtual server handler', id) const url = `${testServer.url}/${id}` @@ -157,7 +156,7 @@ describe('API integration', { timeout: 30000 }, () => { const client = createClient() let uploadProgressCalled - const options: Transloadit.CreateAssemblyOptions = { + const options: CreateAssemblyOptions = { ...genericOptions, onUploadProgress: (uploadProgress) => { uploadProgressCalled = uploadProgress @@ -253,7 +252,7 @@ describe('API integration', { timeout: 30000 }, () => { file1: intoStream(sampleSvg), file2: sampleSvg, file3: buf, - file4: got.default.stream(genericImg), + file4: got.stream(genericImg), }, params: { steps: { @@ -339,7 +338,7 @@ describe('API integration', { timeout: 30000 }, () => { const client = createClient() let progressCalled = false - function onUploadProgress({ uploadedBytes, totalBytes }: Transloadit.UploadProgress) { + function onUploadProgress({ uploadedBytes, totalBytes }: UploadProgress) { // console.log(uploadedBytes) expect(uploadedBytes).toBeDefined() if (isResumable) { @@ -349,7 +348,7 @@ describe('API integration', { timeout: 30000 }, () => { progressCalled = true } - const params: Transloadit.CreateAssemblyOptions = { + const params: CreateAssemblyOptions = { isResumable, params: { steps: { @@ -395,7 +394,7 @@ describe('API integration', { timeout: 30000 }, () => { }, }, files: { - file: nodePath.join(__dirname, './fixtures/zerobytes.jpg'), + file: join(__dirname, './fixtures/zerobytes.jpg'), }, waitForCompletion: true, } @@ -427,7 +426,7 @@ describe('API integration', { timeout: 30000 }, () => { sendServerResponse = resolve }) - const handleRequest: http.RequestListener = async (req, res) => { + const handleRequest: RequestListener = async (req, res) => { // console.log('handler', req.url) expect(req.url).toBe('/') @@ -437,7 +436,7 @@ describe('API integration', { timeout: 30000 }, () => { // console.log('sending response') res.setHeader('Content-type', 'image/jpeg') res.writeHead(200) - got.default.stream(genericImg).pipe(res) + got.stream(genericImg).pipe(res) } const server = await createVirtualTestServer(handleRequest) @@ -560,7 +559,7 @@ describe('API integration', { timeout: 30000 }, () => { describe('assembly notification', () => { type OnNotification = (params: { path?: string - client: Transloadit + client: TransloaditClient assemblyId: string }) => void @@ -570,7 +569,7 @@ describe('API integration', { timeout: 30000 }, () => { }) // helper function - const streamToString = (stream: http.IncomingMessage) => + const streamToString = (stream: IncomingMessage) => new Promise((resolve, reject) => { const chunks: string[] = [] stream.on('data', (chunk) => chunks.push(chunk)) @@ -585,13 +584,11 @@ describe('API integration', { timeout: 30000 }, () => { const client = createClient() // listens for notifications - const onNotificationRequest: http.RequestListener = async (req, res) => { + const onNotificationRequest: RequestListener = async (req, res) => { try { expect(req.method).toBe('POST') const body = await streamToString(req) - const result = JSON.parse( - (querystring.parse(body) as { transloadit: string }).transloadit - ) + const result = JSON.parse((parse(body) as { transloadit: string }).transloadit) expect(result).toHaveProperty('ok') if (result.ok !== 'ASSEMBLY_COMPLETED') { onError(new Error(`result.ok was ${result.ok}`)) diff --git a/test/testserver.ts b/test/testserver.ts index 20398b8..21194a5 100644 --- a/test/testserver.ts +++ b/test/testserver.ts @@ -1,19 +1,19 @@ -import http = require('http') -import got = require('got') -import debug = require('debug') +import { createServer, RequestListener, Server } from 'http' +import got from 'got' +import debug from 'debug' const log = debug('transloadit:testserver') -import createTunnel = require('./tunnel.ts') +import { createTunnel, CreateTunnelResult } from './tunnel' interface HttpServer { - server: http.Server + server: Server port: number } -async function createHttpServer(handler: http.RequestListener): Promise { +async function createHttpServer(handler: RequestListener): Promise { return new Promise((resolve, reject) => { - const server = http.createServer(handler) + const server = createServer(handler) let port = 8000 @@ -41,7 +41,9 @@ async function createHttpServer(handler: http.RequestListener): Promise { +export async function createTestServer( + onRequest: RequestListener +): Promise { if (!process.env.CLOUDFLARED_PATH) { throw new Error('CLOUDFLARED_PATH environment variable not set') } @@ -49,9 +51,9 @@ async function createTestServer(onRequest: http.RequestListener): Promise void - let tunnel: createTunnel.Result + let tunnel: CreateTunnelResult - const handleHttpRequest: http.RequestListener = (req, res) => { + const handleHttpRequest: RequestListener = (req, res) => { log('HTTP request handler', req.method, req.url) if (!initialized) { @@ -89,7 +91,7 @@ async function createTestServer(onRequest: http.RequestListener): Promise void - url: string - } +export interface CreateTestServerResult { + port: number + close: () => void + url: string } - -export = createTestServer diff --git a/test/tunnel.ts b/test/tunnel.ts index d8e928a..baae590 100644 --- a/test/tunnel.ts +++ b/test/tunnel.ts @@ -1,8 +1,8 @@ -import execa = require('execa') -import readline = require('readline') -import dns = require('dns/promises') -import debug = require('debug') -import pRetry = require('p-retry') +import execa, { ExecaChildProcess } from 'execa' +import { createInterface } from 'readline' +import { Resolver } from 'dns/promises' +import debug from 'debug' +import pRetry from 'p-retry' const log = debug('transloadit:cloudflared-tunnel') @@ -13,7 +13,7 @@ interface CreateTunnelParams { interface StartTunnelResult { url: string - process: execa.ExecaChildProcess + process: ExecaChildProcess } async function startTunnel({ @@ -30,7 +30,7 @@ async function startTunnel({ return await new Promise((resolve, reject) => { const timeout = setTimeout(() => reject(new Error('Timed out trying to start tunnel')), 30000) - const rl = readline.createInterface({ input: process.stderr as NodeJS.ReadStream }) + const rl = createInterface({ input: process.stderr as NodeJS.ReadStream }) process.on('error', (err) => { console.error(err) @@ -86,10 +86,10 @@ async function startTunnel({ } } -function createTunnel({ +export function createTunnel({ cloudFlaredPath = 'cloudflared', port, -}: CreateTunnelParams): createTunnel.Result { +}: CreateTunnelParams): CreateTunnelResult { let process: execa.ExecaChildProcess | undefined const urlPromise = (async () => { @@ -102,7 +102,7 @@ function createTunnel({ // We need to wait for DNS to be resolvable. // If we don't, the operating system's dns cache will be poisoned by the not yet valid resolved entry // and it will forever fail for that subdomain name... - const resolver = new dns.Resolver() + const resolver = new Resolver() resolver.setServers(['1.1.1.1']) // use cloudflare's dns server. if we don't explicitly specify DNS server, it will also poison our OS' dns cache for (let i = 0; i < 10; i += 1) { @@ -134,12 +134,8 @@ function createTunnel({ } } -namespace createTunnel { - export interface Result { - process?: execa.ExecaChildProcess - urlPromise: Promise - close: () => Promise - } +export interface CreateTunnelResult { + process?: execa.ExecaChildProcess + urlPromise: Promise + close: () => Promise } - -export = createTunnel diff --git a/test/unit/mock-http.test.ts b/test/unit/mock-http.test.ts index 4bd1130..f6fbb5e 100644 --- a/test/unit/mock-http.test.ts +++ b/test/unit/mock-http.test.ts @@ -1,10 +1,16 @@ -import nock = require('nock') +import nock from 'nock' -import Transloadit = require('../../src/Transloadit.ts') +import { + HTTPError, + InconsistentResponseError, + TimeoutError, + TransloaditClient, + TransloaditClientOptions, +} from '../../src/Transloadit' const getLocalClient = ( - opts?: Omit -) => new Transloadit({ authKey: '', authSecret: '', endpoint: 'http://localhost', ...opts }) + opts?: Omit +) => new TransloaditClient({ authKey: '', authSecret: '', endpoint: 'http://localhost', ...opts }) const createAssemblyRegex = /\/assemblies\/[0-9a-f]{32}/ @@ -15,11 +21,15 @@ describe('Mocked API tests', () => { }) it('should time out createAssembly with a custom timeout', async () => { - const client = new Transloadit({ authKey: '', authSecret: '', endpoint: 'http://localhost' }) + const client = new TransloaditClient({ + authKey: '', + authSecret: '', + endpoint: 'http://localhost', + }) nock('http://localhost').post(createAssemblyRegex).delay(100).reply(200) - await expect(client.createAssembly({ timeout: 10 })).rejects.toThrow(Transloadit.TimeoutError) + await expect(client.createAssembly({ timeout: 10 })).rejects.toThrow(TimeoutError) }) it('should time out other requests with a custom timeout', async () => { @@ -27,7 +37,7 @@ describe('Mocked API tests', () => { nock('http://localhost').post('/templates').delay(100).reply(200) - await expect(client.createTemplate()).rejects.toThrow(Transloadit.TimeoutError) + await expect(client.createTemplate()).rejects.toThrow(TimeoutError) }) it('should time out awaitAssemblyCompletion polling', async () => { @@ -187,7 +197,7 @@ describe('Mocked API tests', () => { // Failure const promise = client.getAssembly('1') - await expect(promise).rejects.toThrow(Transloadit.InconsistentResponseError) + await expect(promise).rejects.toThrow(InconsistentResponseError) await expect(promise).rejects.toThrow( expect.objectContaining({ message: 'Server returned an incomplete assembly response (no URL)', @@ -267,7 +277,7 @@ describe('Mocked API tests', () => { .query(() => true) .reply(404, { error: 'SERVER_404', message: 'unknown method / url' }) - await expect(client.getAssembly('invalid')).rejects.toThrow(Transloadit.HTTPError) + await expect(client.getAssembly('invalid')).rejects.toThrow(HTTPError) scope.done() }) }) diff --git a/test/unit/test-pagination-stream.test.ts b/test/unit/test-pagination-stream.test.ts index 27152a2..857fc58 100644 --- a/test/unit/test-pagination-stream.test.ts +++ b/test/unit/test-pagination-stream.test.ts @@ -1,13 +1,13 @@ -import stream = require('stream') +import { Writable } from 'stream' -import PaginationStream = require('../../src/PaginationStream.ts') +import { PaginationStream } from '../../src/PaginationStream' const toArray = (callback: (list: number[]) => void) => { - const writable = new stream.Writable({ objectMode: true }) + const writable = new Writable({ objectMode: true }) const list: number[] = [] writable.write = (chunk) => { list.push(chunk) - return false + return true } writable.end = () => { diff --git a/test/unit/test-transloadit-client.test.ts b/test/unit/test-transloadit-client.test.ts index eea0745..d817e9c 100644 --- a/test/unit/test-transloadit-client.test.ts +++ b/test/unit/test-transloadit-client.test.ts @@ -1,33 +1,33 @@ -import stream = require('stream') -import FormData = require('form-data') -import got = require('got') +import { Readable } from 'stream' +import FormData from 'form-data' +import got, { CancelableRequest } from 'got' -import tus = require('../../src/tus.ts') -import Transloadit = require('../../src/Transloadit.ts') -import pkg = require('transloadit/package.json') +import * as tus from '../../src/tus' +import { TransloaditClient } from '../../src/Transloadit' +import { version } from 'transloadit/package.json' const mockedExpiresDate = '2021-01-06T21:11:07.883Z' -const mockGetExpiresDate = (client: Transloadit) => +const mockGetExpiresDate = (client: TransloaditClient) => // @ts-expect-error This mocks private internals vi.spyOn(client, '_getExpiresDate').mockReturnValue(mockedExpiresDate) const mockGot = (method: 'get') => - vi.spyOn(got.default, method).mockImplementation(() => { + vi.spyOn(got, method).mockImplementation(() => { const mockPromise = Promise.resolve({ body: '', - }) as got.CancelableRequest + }) as CancelableRequest ;(mockPromise as any).on = vi.fn(() => {}) return mockPromise }) -const mockRemoteJson = (client: Transloadit) => +const mockRemoteJson = (client: TransloaditClient) => // @ts-expect-error This mocks private internals vi.spyOn(client, '_remoteJson').mockImplementation(() => ({ body: {} })) describe('Transloadit', () => { it('should throw a proper error for request stream', async () => { - const client = new Transloadit({ authKey: 'foo_key', authSecret: 'foo_secret' }) + const client = new TransloaditClient({ authKey: 'foo_key', authSecret: 'foo_secret' }) // mimic Stream object returned from `request` (which is not a stream v3) - const req = { pipe: () => {} } as Partial as stream.Readable + const req = { pipe: () => {} } as Partial as Readable const promise = client.createAssembly({ uploads: { file: req } }) await expect(promise).rejects.toThrow( @@ -42,7 +42,7 @@ describe('Transloadit', () => { authSecret: 'foo_secret', maxRetries: 0, } - const client = new Transloadit(opts) + const client = new TransloaditClient(opts) expect( // @ts-expect-error This tests private internals client._authKey @@ -77,13 +77,13 @@ describe('Transloadit', () => { authSecret: 'foo_secret', endpoint: 'https://api2.transloadit.com/', } - expect(() => new Transloadit(opts)).toThrow('Trailing slash in endpoint is not allowed') + expect(() => new TransloaditClient(opts)).toThrow('Trailing slash in endpoint is not allowed') }) it('should give error when no authSecret', () => { expect( () => - new Transloadit( + new TransloaditClient( // @ts-expect-error This tests invalid types { authSecret: '' } ) @@ -93,7 +93,7 @@ describe('Transloadit', () => { it('should give error when no authKey', () => { expect( () => - new Transloadit( + new TransloaditClient( // @ts-expect-error This tests invalid types { authKey: '' } ) @@ -107,7 +107,7 @@ describe('Transloadit', () => { endpoint: 'http://foo', } - const client = new Transloadit(opts) + const client = new TransloaditClient(opts) expect( // @ts-expect-error This tests private internals client._authKey @@ -126,7 +126,7 @@ describe('Transloadit', () => { describe('add stream', () => { it('should pause streams', async () => { vi.spyOn(tus, 'sendTusRequest').mockImplementation(() => Promise.resolve()) - const client = new Transloadit({ authKey: 'foo_key', authSecret: 'foo_secret' }) + const client = new TransloaditClient({ authKey: 'foo_key', authSecret: 'foo_secret' }) const name = 'foo_name' const pause = vi.fn(() => mockStream) @@ -137,7 +137,7 @@ describe('Transloadit', () => { _readableState: {}, on: () => mockStream, readable: true, - } as Partial as stream.Readable + } as Partial as Readable mockRemoteJson(client) @@ -149,10 +149,10 @@ describe('Transloadit', () => { describe('_appendForm', () => { it('should append all required fields to the request form', () => { - const client = new Transloadit({ authKey: 'foo_key', authSecret: 'foo_secret' }) + const client = new TransloaditClient({ authKey: 'foo_key', authSecret: 'foo_secret' }) - const stream1 = new stream.Readable() - const stream2 = new stream.Readable() + const stream1 = new Readable() + const stream2 = new Readable() const streamsMap = { stream1: { stream: stream1 }, @@ -189,7 +189,7 @@ describe('Transloadit', () => { describe('_appendParamsToUrl', () => { it('should append params and signature to the given url', () => { - const client = new Transloadit({ authKey: 'foo_key', authSecret: 'foo_secret' }) + const client = new TransloaditClient({ authKey: 'foo_key', authSecret: 'foo_secret' }) // URL can have question mark also inside parameter const url = 'https://example.com/foo_url?param=12?3' @@ -211,7 +211,7 @@ describe('Transloadit', () => { describe('_prepareParams', () => { it('should add the auth key, secret and expires parameters', () => { - let client = new Transloadit({ authKey: 'foo_key', authSecret: 'foo_secret' }) + let client = new TransloaditClient({ authKey: 'foo_key', authSecret: 'foo_secret' }) // @ts-expect-error This tests private internals let r = JSON.parse(client._prepareParams()) @@ -222,7 +222,7 @@ describe('Transloadit', () => { authKey: 'foo', authSecret: 'foo_secret', } - client = new Transloadit(opts) + client = new TransloaditClient(opts) // @ts-expect-error This tests private internals r = JSON.parse(client._prepareParams()) @@ -231,7 +231,7 @@ describe('Transloadit', () => { }) it('should not add anything if the params are already present', () => { - const client = new Transloadit({ authKey: 'foo_key', authSecret: 'foo_secret' }) + const client = new TransloaditClient({ authKey: 'foo_key', authSecret: 'foo_secret' }) const PARAMS = { auth: { @@ -249,7 +249,7 @@ describe('Transloadit', () => { describe('calcSignature', () => { it('should calc _prepareParams and _calcSignature', () => { - const client = new Transloadit({ authKey: 'foo_key', authSecret: 'foo_secret' }) + const client = new TransloaditClient({ authKey: 'foo_key', authSecret: 'foo_secret' }) // @ts-expect-error This tests private internals client._authSecret = '13123123123' @@ -274,7 +274,7 @@ describe('Transloadit', () => { }) it('should set 1 day timeout by default for createAssembly', async () => { - const client = new Transloadit({ authKey: 'foo_key', authSecret: 'foo_secret' }) + const client = new TransloaditClient({ authKey: 'foo_key', authSecret: 'foo_secret' }) const spy = mockRemoteJson(client) @@ -288,7 +288,7 @@ describe('Transloadit', () => { }) it('should crash if attempt to use callback', async () => { - const client = new Transloadit({ authKey: 'foo_key', authSecret: 'foo_secret' }) + const client = new TransloaditClient({ authKey: 'foo_key', authSecret: 'foo_secret' }) const cb = () => {} expect(() => client.createAssembly( @@ -301,7 +301,7 @@ describe('Transloadit', () => { describe('_calcSignature', () => { it('should calculate the signature properly', () => { - const client = new Transloadit({ authKey: 'foo_key', authSecret: 'foo_secret' }) + const client = new TransloaditClient({ authKey: 'foo_key', authSecret: 'foo_secret' }) // @ts-expect-error This tests private internals client._authSecret = '13123123123' @@ -341,7 +341,7 @@ describe('Transloadit', () => { describe('_remoteJson', () => { it('should add "Transloadit-Client" header to requests', async () => { - const client = new Transloadit({ authKey: 'foo_key', authSecret: 'foo_secret' }) + const client = new TransloaditClient({ authKey: 'foo_key', authSecret: 'foo_secret' }) const get = mockGot('get') @@ -351,7 +351,7 @@ describe('Transloadit', () => { expect(get).toHaveBeenCalledWith( expect.any(String), - expect.objectContaining({ headers: { 'Transloadit-Client': `node-sdk:${pkg.version}` } }) + expect.objectContaining({ headers: { 'Transloadit-Client': `node-sdk:${version}` } }) ) }) }) diff --git a/tsconfig.build.json b/tsconfig.build.json index 4b124dd..e52a99b 100644 --- a/tsconfig.build.json +++ b/tsconfig.build.json @@ -10,6 +10,5 @@ "rootDir": "src", "sourceMap": true, "strict": true, - "verbatimModuleSyntax": true } } diff --git a/tsconfig.json b/tsconfig.json index adf32b0..7467df7 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -2,7 +2,6 @@ "exclude": ["src"], "references": [{ "path": "./tsconfig.build.json" }], "compilerOptions": { - "allowImportingTsExtensions": true, "module": "node16", "noEmit": true, "resolveJsonModule": true, diff --git a/vitest.config.mjs b/vitest.config.mjs index c97b4e0..109cde7 100644 --- a/vitest.config.mjs +++ b/vitest.config.mjs @@ -1,10 +1,6 @@ import { defineConfig } from 'vitest/config' export default defineConfig({ - esbuild: { - target: 'node14', - format: 'cjs', - }, test: { coverage: { include: 'src', From 5dc7cc30246349a33b20e6aabc4c366f229511ef Mon Sep 17 00:00:00 2001 From: Remco Haszing Date: Fri, 13 Sep 2024 17:00:18 +0200 Subject: [PATCH 03/23] Fix linting issues --- .eslintignore | 2 ++ .eslintrc.js | 1 + .prettierignore | 1 + index.js | 1 - tsconfig.build.json | 2 +- 5 files changed, 5 insertions(+), 2 deletions(-) delete mode 100644 index.js diff --git a/.eslintignore b/.eslintignore index e55628a..e876629 100644 --- a/.eslintignore +++ b/.eslintignore @@ -1,2 +1,4 @@ +coverage/ +dist/ node_modules/ fixture/ diff --git a/.eslintrc.js b/.eslintrc.js index f9fe152..807e7bf 100644 --- a/.eslintrc.js +++ b/.eslintrc.js @@ -19,6 +19,7 @@ module.exports = { }, rules: { 'no-console': 'off', + 'import/extensions': 'off', }, }, ], diff --git a/.prettierignore b/.prettierignore index d1e1584..272265b 100644 --- a/.prettierignore +++ b/.prettierignore @@ -1,3 +1,4 @@ coverage/ +dist/ fixture/ node_modules/ diff --git a/index.js b/index.js deleted file mode 100644 index 3338e8c..0000000 --- a/index.js +++ /dev/null @@ -1 +0,0 @@ -module.exports = require('./src/Transloadit') diff --git a/tsconfig.build.json b/tsconfig.build.json index e52a99b..c42a657 100644 --- a/tsconfig.build.json +++ b/tsconfig.build.json @@ -9,6 +9,6 @@ "resolveJsonModule": true, "rootDir": "src", "sourceMap": true, - "strict": true, + "strict": true } } From 3df58dbb6866eabf550021eac30c182674d2edbd Mon Sep 17 00:00:00 2001 From: Remco Haszing Date: Fri, 13 Sep 2024 17:05:55 +0200 Subject: [PATCH 04/23] Fix ESLint configuration --- .eslintrc.js | 11 ++++++++++- 1 file changed, 10 insertions(+), 1 deletion(-) diff --git a/.eslintrc.js b/.eslintrc.js index 807e7bf..76899e3 100644 --- a/.eslintrc.js +++ b/.eslintrc.js @@ -5,6 +5,16 @@ module.exports = { ecmaVersion: 11, requireConfigFile: false, }, + settings: { + 'import/resolver': { + node: { + extensions: ['.js', '.ts'], + }, + }, + }, + rules: { + 'import/extensions': 'off', + }, overrides: [ { files: 'test/**', @@ -19,7 +29,6 @@ module.exports = { }, rules: { 'no-console': 'off', - 'import/extensions': 'off', }, }, ], From 18aa73a43ea5baac5be46a0f36aa72f90bcdc7d0 Mon Sep 17 00:00:00 2001 From: Remco Haszing Date: Fri, 13 Sep 2024 17:08:54 +0200 Subject: [PATCH 05/23] Fix Node.js 18 compatibility --- src/Transloadit.ts | 4 ++-- test/integration/live-api.test.ts | 6 +++--- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/src/Transloadit.ts b/src/Transloadit.ts index 05a8738..d3f31a7 100644 --- a/src/Transloadit.ts +++ b/src/Transloadit.ts @@ -1,4 +1,4 @@ -import { createHmac } from 'crypto' +import { createHmac, randomUUID } from 'crypto' import got, { RequiredRetryOptions, Headers, OptionsOfJSONResponseBody } from 'got' import FormData from 'form-data' import { constants, createReadStream } from 'fs' @@ -183,7 +183,7 @@ export class TransloaditClient { if (assemblyId != null) { effectiveAssemblyId = assemblyId } else { - effectiveAssemblyId = crypto.randomUUID().replace(/-/g, '') + effectiveAssemblyId = randomUUID().replace(/-/g, '') } const urlSuffix = `/assemblies/${effectiveAssemblyId}` diff --git a/test/integration/live-api.test.ts b/test/integration/live-api.test.ts index edfd6f2..44f488b 100644 --- a/test/integration/live-api.test.ts +++ b/test/integration/live-api.test.ts @@ -1,4 +1,4 @@ -import * as crypto from 'crypto' +import { randomUUID } from 'crypto' import { parse } from 'querystring' import * as temp from 'temp' import { createWriteStream } from 'fs' @@ -135,7 +135,7 @@ interface VirtualTestServer { } async function createVirtualTestServer(handler: RequestListener): Promise { - const id = crypto.randomUUID() + const id = randomUUID() log('Adding virtual server handler', id) const url = `${testServer.url}/${id}` handlers.set(id, handler) @@ -302,7 +302,7 @@ describe('API integration', { timeout: 30000 }, () => { it('should allow setting an explicit assemblyId on createAssembly', async () => { const client = createClient() - const assemblyId = crypto.randomUUID().replace(/-/g, '') + const assemblyId = randomUUID().replace(/-/g, '') const params = { assemblyId, waitForCompletion: true, From e955bbb0662a982708de55c09f223162c4cb6e17 Mon Sep 17 00:00:00 2001 From: Remco Haszing Date: Mon, 16 Sep 2024 15:43:34 +0200 Subject: [PATCH 06/23] Rename TransloaditClient to Transloadit Also rename `TransloaditClientOptions` to `Transloadit.Options`. --- README.md | 6 ++-- src/Transloadit.ts | 24 +++++++------- test/integration/live-api.test.ts | 8 ++--- test/unit/mock-http.test.ts | 10 +++--- test/unit/test-transloadit-client.test.ts | 40 +++++++++++------------ 5 files changed, 44 insertions(+), 44 deletions(-) diff --git a/README.md b/README.md index 11321e5..8cb41a0 100644 --- a/README.md +++ b/README.md @@ -48,9 +48,9 @@ npm install --save transloadit The following code will upload an image and resize it to a thumbnail: ```javascript -const { TransloaditClient } = require('transloadit') +const { Transloadit } = require('transloadit') -const transloadit = new TransloaditClient({ +const transloadit = new Transloadit({ authKey: 'YOUR_TRANSLOADIT_KEY', authSecret: 'YOUR_TRANSLOADIT_SECRET', }) @@ -113,7 +113,7 @@ For more example use cases and information about the available robots and their ## API -These are the public methods on the `TransloaditClient` object and their descriptions. The methods are based on the [Transloadit API](https://transloadit.com/docs/api/). +These are the public methods on the `Transloadit` object and their descriptions. The methods are based on the [Transloadit API](https://transloadit.com/docs/api/). Table of contents: diff --git a/src/Transloadit.ts b/src/Transloadit.ts index d3f31a7..13f6e04 100644 --- a/src/Transloadit.ts +++ b/src/Transloadit.ts @@ -99,7 +99,18 @@ function checkResult(result: T | { error: string }): asserts result is T { } } -export class TransloaditClient { +export declare namespace Transloadit { + interface Options { + authKey: string + authSecret: string + endpoint?: string + maxRetries?: number + timeout?: number + gotRetry?: RequiredRetryOptions + } +} + +export class Transloadit { private _authKey: string private _authSecret: string private _endpoint: string @@ -108,7 +119,7 @@ export class TransloaditClient { private _gotRetry: RequiredRetryOptions | number private _lastUsedAssemblyUrl = '' - constructor(opts: TransloaditClientOptions) { + constructor(opts: Transloadit.Options) { if (opts?.authKey == null) { throw new Error('Please provide an authKey') } @@ -941,15 +952,6 @@ export interface TemplateContent { steps: KeyVal } -export interface TransloaditClientOptions { - authKey: string - authSecret: string - endpoint?: string - maxRetries?: number - timeout?: number - gotRetry?: RequiredRetryOptions -} - export interface AwaitAssemblyCompletionOptions { onAssemblyProgress?: AssemblyProgress timeout?: number diff --git a/test/integration/live-api.test.ts b/test/integration/live-api.test.ts index 44f488b..971d71c 100644 --- a/test/integration/live-api.test.ts +++ b/test/integration/live-api.test.ts @@ -9,7 +9,7 @@ import got, { RequiredRetryOptions } from 'got' import intoStream = require('into-stream') import debug = require('debug') -import { CreateAssemblyOptions, TransloaditClient, UploadProgress } from '../../src/Transloadit' +import { CreateAssemblyOptions, Transloadit, UploadProgress } from '../../src/Transloadit' import { createTestServer, CreateTestServerResult } from '../testserver' const log = debug('transloadit:live-api') @@ -53,10 +53,10 @@ function createClient(opts = {}) { ], } - return new TransloaditClient({ authKey, authSecret, gotRetry, ...opts }) + return new Transloadit({ authKey, authSecret, gotRetry, ...opts }) } -function createAssembly(client: TransloaditClient, params: CreateAssemblyOptions) { +function createAssembly(client: Transloadit, params: CreateAssemblyOptions) { const promise = client.createAssembly(params) const { assemblyId } = promise console.log(expect.getState().currentTestName, 'createAssembly', assemblyId) // For easier debugging @@ -559,7 +559,7 @@ describe('API integration', { timeout: 30000 }, () => { describe('assembly notification', () => { type OnNotification = (params: { path?: string - client: TransloaditClient + client: Transloadit assemblyId: string }) => void diff --git a/test/unit/mock-http.test.ts b/test/unit/mock-http.test.ts index f6fbb5e..4946d4a 100644 --- a/test/unit/mock-http.test.ts +++ b/test/unit/mock-http.test.ts @@ -4,13 +4,11 @@ import { HTTPError, InconsistentResponseError, TimeoutError, - TransloaditClient, - TransloaditClientOptions, + Transloadit, } from '../../src/Transloadit' -const getLocalClient = ( - opts?: Omit -) => new TransloaditClient({ authKey: '', authSecret: '', endpoint: 'http://localhost', ...opts }) +const getLocalClient = (opts?: Omit) => + new Transloadit({ authKey: '', authSecret: '', endpoint: 'http://localhost', ...opts }) const createAssemblyRegex = /\/assemblies\/[0-9a-f]{32}/ @@ -21,7 +19,7 @@ describe('Mocked API tests', () => { }) it('should time out createAssembly with a custom timeout', async () => { - const client = new TransloaditClient({ + const client = new Transloadit({ authKey: '', authSecret: '', endpoint: 'http://localhost', diff --git a/test/unit/test-transloadit-client.test.ts b/test/unit/test-transloadit-client.test.ts index d817e9c..23b9307 100644 --- a/test/unit/test-transloadit-client.test.ts +++ b/test/unit/test-transloadit-client.test.ts @@ -3,11 +3,11 @@ import FormData from 'form-data' import got, { CancelableRequest } from 'got' import * as tus from '../../src/tus' -import { TransloaditClient } from '../../src/Transloadit' +import { Transloadit } from '../../src/Transloadit' import { version } from 'transloadit/package.json' const mockedExpiresDate = '2021-01-06T21:11:07.883Z' -const mockGetExpiresDate = (client: TransloaditClient) => +const mockGetExpiresDate = (client: Transloadit) => // @ts-expect-error This mocks private internals vi.spyOn(client, '_getExpiresDate').mockReturnValue(mockedExpiresDate) const mockGot = (method: 'get') => @@ -18,13 +18,13 @@ const mockGot = (method: 'get') => ;(mockPromise as any).on = vi.fn(() => {}) return mockPromise }) -const mockRemoteJson = (client: TransloaditClient) => +const mockRemoteJson = (client: Transloadit) => // @ts-expect-error This mocks private internals vi.spyOn(client, '_remoteJson').mockImplementation(() => ({ body: {} })) describe('Transloadit', () => { it('should throw a proper error for request stream', async () => { - const client = new TransloaditClient({ authKey: 'foo_key', authSecret: 'foo_secret' }) + const client = new Transloadit({ authKey: 'foo_key', authSecret: 'foo_secret' }) // mimic Stream object returned from `request` (which is not a stream v3) const req = { pipe: () => {} } as Partial as Readable @@ -42,7 +42,7 @@ describe('Transloadit', () => { authSecret: 'foo_secret', maxRetries: 0, } - const client = new TransloaditClient(opts) + const client = new Transloadit(opts) expect( // @ts-expect-error This tests private internals client._authKey @@ -77,13 +77,13 @@ describe('Transloadit', () => { authSecret: 'foo_secret', endpoint: 'https://api2.transloadit.com/', } - expect(() => new TransloaditClient(opts)).toThrow('Trailing slash in endpoint is not allowed') + expect(() => new Transloadit(opts)).toThrow('Trailing slash in endpoint is not allowed') }) it('should give error when no authSecret', () => { expect( () => - new TransloaditClient( + new Transloadit( // @ts-expect-error This tests invalid types { authSecret: '' } ) @@ -93,7 +93,7 @@ describe('Transloadit', () => { it('should give error when no authKey', () => { expect( () => - new TransloaditClient( + new Transloadit( // @ts-expect-error This tests invalid types { authKey: '' } ) @@ -107,7 +107,7 @@ describe('Transloadit', () => { endpoint: 'http://foo', } - const client = new TransloaditClient(opts) + const client = new Transloadit(opts) expect( // @ts-expect-error This tests private internals client._authKey @@ -126,7 +126,7 @@ describe('Transloadit', () => { describe('add stream', () => { it('should pause streams', async () => { vi.spyOn(tus, 'sendTusRequest').mockImplementation(() => Promise.resolve()) - const client = new TransloaditClient({ authKey: 'foo_key', authSecret: 'foo_secret' }) + const client = new Transloadit({ authKey: 'foo_key', authSecret: 'foo_secret' }) const name = 'foo_name' const pause = vi.fn(() => mockStream) @@ -149,7 +149,7 @@ describe('Transloadit', () => { describe('_appendForm', () => { it('should append all required fields to the request form', () => { - const client = new TransloaditClient({ authKey: 'foo_key', authSecret: 'foo_secret' }) + const client = new Transloadit({ authKey: 'foo_key', authSecret: 'foo_secret' }) const stream1 = new Readable() const stream2 = new Readable() @@ -189,7 +189,7 @@ describe('Transloadit', () => { describe('_appendParamsToUrl', () => { it('should append params and signature to the given url', () => { - const client = new TransloaditClient({ authKey: 'foo_key', authSecret: 'foo_secret' }) + const client = new Transloadit({ authKey: 'foo_key', authSecret: 'foo_secret' }) // URL can have question mark also inside parameter const url = 'https://example.com/foo_url?param=12?3' @@ -211,7 +211,7 @@ describe('Transloadit', () => { describe('_prepareParams', () => { it('should add the auth key, secret and expires parameters', () => { - let client = new TransloaditClient({ authKey: 'foo_key', authSecret: 'foo_secret' }) + let client = new Transloadit({ authKey: 'foo_key', authSecret: 'foo_secret' }) // @ts-expect-error This tests private internals let r = JSON.parse(client._prepareParams()) @@ -222,7 +222,7 @@ describe('Transloadit', () => { authKey: 'foo', authSecret: 'foo_secret', } - client = new TransloaditClient(opts) + client = new Transloadit(opts) // @ts-expect-error This tests private internals r = JSON.parse(client._prepareParams()) @@ -231,7 +231,7 @@ describe('Transloadit', () => { }) it('should not add anything if the params are already present', () => { - const client = new TransloaditClient({ authKey: 'foo_key', authSecret: 'foo_secret' }) + const client = new Transloadit({ authKey: 'foo_key', authSecret: 'foo_secret' }) const PARAMS = { auth: { @@ -249,7 +249,7 @@ describe('Transloadit', () => { describe('calcSignature', () => { it('should calc _prepareParams and _calcSignature', () => { - const client = new TransloaditClient({ authKey: 'foo_key', authSecret: 'foo_secret' }) + const client = new Transloadit({ authKey: 'foo_key', authSecret: 'foo_secret' }) // @ts-expect-error This tests private internals client._authSecret = '13123123123' @@ -274,7 +274,7 @@ describe('Transloadit', () => { }) it('should set 1 day timeout by default for createAssembly', async () => { - const client = new TransloaditClient({ authKey: 'foo_key', authSecret: 'foo_secret' }) + const client = new Transloadit({ authKey: 'foo_key', authSecret: 'foo_secret' }) const spy = mockRemoteJson(client) @@ -288,7 +288,7 @@ describe('Transloadit', () => { }) it('should crash if attempt to use callback', async () => { - const client = new TransloaditClient({ authKey: 'foo_key', authSecret: 'foo_secret' }) + const client = new Transloadit({ authKey: 'foo_key', authSecret: 'foo_secret' }) const cb = () => {} expect(() => client.createAssembly( @@ -301,7 +301,7 @@ describe('Transloadit', () => { describe('_calcSignature', () => { it('should calculate the signature properly', () => { - const client = new TransloaditClient({ authKey: 'foo_key', authSecret: 'foo_secret' }) + const client = new Transloadit({ authKey: 'foo_key', authSecret: 'foo_secret' }) // @ts-expect-error This tests private internals client._authSecret = '13123123123' @@ -341,7 +341,7 @@ describe('Transloadit', () => { describe('_remoteJson', () => { it('should add "Transloadit-Client" header to requests', async () => { - const client = new TransloaditClient({ authKey: 'foo_key', authSecret: 'foo_secret' }) + const client = new Transloadit({ authKey: 'foo_key', authSecret: 'foo_secret' }) const get = mockGot('get') From 2e8072d1d815d3241fabf7e8836771919fe2cf88 Mon Sep 17 00:00:00 2001 From: Remco Haszing Date: Mon, 30 Sep 2024 15:33:31 +0200 Subject: [PATCH 07/23] Split complex logical expression Instead of coercing a lot of checks into a single boolean, we now throw early. --- src/Transloadit.ts | 44 +++++++++++++++++++++++++++----------------- 1 file changed, 27 insertions(+), 17 deletions(-) diff --git a/src/Transloadit.ts b/src/Transloadit.ts index 13f6e04..b036ac8 100644 --- a/src/Transloadit.ts +++ b/src/Transloadit.ts @@ -75,6 +75,16 @@ function decorateHttpError(err: TransloaditError, body: any): TransloaditError { return err } +function assertHttpError( + err: TransloaditError, + body: unknown, + assertion: unknown +): asserts assertion { + if (!assertion) { + throw decorateHttpError(err, body) + } +} + // Not sure if this is still a problem with the API, but throw a special error type so the user can retry if needed function checkAssemblyUrls(result: Assembly) { if (result.assembly_url == null || result.assembly_ssl_url == null) { @@ -779,23 +789,23 @@ export class Transloadit { const { statusCode, body } = err.response logWarn('HTTP error', statusCode, body) - const shouldRetry = - statusCode === 413 && - typeof body === 'object' && - body != null && - 'error' in body && - body.error === 'RATE_LIMIT_REACHED' && - 'info' in body && - typeof body.info === 'object' && - body.info != null && - 'retryIn' in body.info && - Boolean(body.info.retryIn) && - retryCount < this._maxRetries - - // https://transloadit.com/blog/2012/04/introducing-rate-limiting/ - if (!shouldRetry) throw decorateHttpError(err, body) - - const { retryIn: retryInSec } = body.info as { retryIn: number } + assertHttpError(err, body, statusCode === 413) + assertHttpError(err, body, typeof body === 'object') + assertHttpError(err, body, body) + assertHttpError(err, body, 'error' in body) + assertHttpError(err, body, body.error === 'RATE_LIMIT_REACHED') + assertHttpError(err, body, 'info' in body) + + const { info } = body + assertHttpError(err, body, typeof info === 'object') + assertHttpError(err, body, info) + assertHttpError(err, body, 'retryIn' in info) + const { retryIn: retryInSec } = info + + assertHttpError(err, body, retryInSec) + assertHttpError(err, body, typeof retryInSec === 'number') + assertHttpError(err, body, retryCount >= this._maxRetries) + logWarn(`Rate limit reached, retrying request in approximately ${retryInSec} seconds.`) const retryInMs = 1000 * (retryInSec * (1 + 0.1 * Math.random())) await new Promise((resolve) => setTimeout(resolve, retryInMs)) From e911c9bf0a831a904a3c38af4c88646bb342289c Mon Sep 17 00:00:00 2001 From: Remco Haszing Date: Mon, 30 Sep 2024 15:37:17 +0200 Subject: [PATCH 08/23] Fix CI job name --- .github/workflows/ci.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index e1eef21..9d24223 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -73,7 +73,7 @@ jobs: - eslint - pack - prettier - - tsd + - typescript - vitest if: startsWith(github.ref, 'refs/tags/') permissions: From 7ab50a4365cd91fc589c4b6add48ddf8bd2c813c Mon Sep 17 00:00:00 2001 From: Remco Haszing Date: Thu, 3 Oct 2024 13:17:23 +0200 Subject: [PATCH 09/23] Revert "Split complex logical expression" This reverts commit 2e8072d1d815d3241fabf7e8836771919fe2cf88. --- src/Transloadit.ts | 44 +++++++++++++++++--------------------------- 1 file changed, 17 insertions(+), 27 deletions(-) diff --git a/src/Transloadit.ts b/src/Transloadit.ts index b036ac8..13f6e04 100644 --- a/src/Transloadit.ts +++ b/src/Transloadit.ts @@ -75,16 +75,6 @@ function decorateHttpError(err: TransloaditError, body: any): TransloaditError { return err } -function assertHttpError( - err: TransloaditError, - body: unknown, - assertion: unknown -): asserts assertion { - if (!assertion) { - throw decorateHttpError(err, body) - } -} - // Not sure if this is still a problem with the API, but throw a special error type so the user can retry if needed function checkAssemblyUrls(result: Assembly) { if (result.assembly_url == null || result.assembly_ssl_url == null) { @@ -789,23 +779,23 @@ export class Transloadit { const { statusCode, body } = err.response logWarn('HTTP error', statusCode, body) - assertHttpError(err, body, statusCode === 413) - assertHttpError(err, body, typeof body === 'object') - assertHttpError(err, body, body) - assertHttpError(err, body, 'error' in body) - assertHttpError(err, body, body.error === 'RATE_LIMIT_REACHED') - assertHttpError(err, body, 'info' in body) - - const { info } = body - assertHttpError(err, body, typeof info === 'object') - assertHttpError(err, body, info) - assertHttpError(err, body, 'retryIn' in info) - const { retryIn: retryInSec } = info - - assertHttpError(err, body, retryInSec) - assertHttpError(err, body, typeof retryInSec === 'number') - assertHttpError(err, body, retryCount >= this._maxRetries) - + const shouldRetry = + statusCode === 413 && + typeof body === 'object' && + body != null && + 'error' in body && + body.error === 'RATE_LIMIT_REACHED' && + 'info' in body && + typeof body.info === 'object' && + body.info != null && + 'retryIn' in body.info && + Boolean(body.info.retryIn) && + retryCount < this._maxRetries + + // https://transloadit.com/blog/2012/04/introducing-rate-limiting/ + if (!shouldRetry) throw decorateHttpError(err, body) + + const { retryIn: retryInSec } = body.info as { retryIn: number } logWarn(`Rate limit reached, retrying request in approximately ${retryInSec} seconds.`) const retryInMs = 1000 * (retryInSec * (1 + 0.1 * Math.random())) await new Promise((resolve) => setTimeout(resolve, retryInMs)) From da4d70d0312022d4df8c389e2ee67a0f5967bd27 Mon Sep 17 00:00:00 2001 From: Mikael Finstad Date: Sat, 5 Oct 2024 11:29:27 +0200 Subject: [PATCH 10/23] fix "as CreateAssemblyPromise" --- src/Transloadit.ts | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/src/Transloadit.ts b/src/Transloadit.ts index 13f6e04..70c52a1 100644 --- a/src/Transloadit.ts +++ b/src/Transloadit.ts @@ -288,11 +288,10 @@ export class Transloadit { } return Promise.race([createAssemblyAndUpload(), streamErrorPromise]) - })() as CreateAssemblyPromise + })() // This allows the user to use or log the assemblyId even before it has been created for easier debugging - promise.assemblyId = effectiveAssemblyId - return promise + return Object.assign(promise, { assemblyId: effectiveAssemblyId }) } async awaitAssemblyCompletion( From 7ee6abb71759359ecd50725838b210fab28a183b Mon Sep 17 00:00:00 2001 From: Mikael Finstad Date: Sat, 5 Oct 2024 11:33:56 +0200 Subject: [PATCH 11/23] fix "as { retryIn: number }" --- src/Transloadit.ts | 13 ++++++++----- 1 file changed, 8 insertions(+), 5 deletions(-) diff --git a/src/Transloadit.ts b/src/Transloadit.ts index 70c52a1..9881440 100644 --- a/src/Transloadit.ts +++ b/src/Transloadit.ts @@ -778,7 +778,9 @@ export class Transloadit { const { statusCode, body } = err.response logWarn('HTTP error', statusCode, body) - const shouldRetry = + // check whether we should retry + // https://transloadit.com/blog/2012/04/introducing-rate-limiting/ + if (!( statusCode === 413 && typeof body === 'object' && body != null && @@ -788,13 +790,14 @@ export class Transloadit { typeof body.info === 'object' && body.info != null && 'retryIn' in body.info && + typeof body.info.retryIn === 'number' && Boolean(body.info.retryIn) && retryCount < this._maxRetries + )) { + throw decorateHttpError(err, body) + } - // https://transloadit.com/blog/2012/04/introducing-rate-limiting/ - if (!shouldRetry) throw decorateHttpError(err, body) - - const { retryIn: retryInSec } = body.info as { retryIn: number } + const { retryIn: retryInSec } = body.info logWarn(`Rate limit reached, retrying request in approximately ${retryInSec} seconds.`) const retryInMs = 1000 * (retryInSec * (1 + 0.1 * Math.random())) await new Promise((resolve) => setTimeout(resolve, retryInMs)) From 5abb090453491d9415fcc251d5136dc3171f6295 Mon Sep 17 00:00:00 2001 From: Mikael Finstad Date: Sat, 5 Oct 2024 11:34:05 +0200 Subject: [PATCH 12/23] fix test server type --- test/integration/live-api.test.ts | 4 ++-- test/testserver.ts | 8 ++------ 2 files changed, 4 insertions(+), 8 deletions(-) diff --git a/test/integration/live-api.test.ts b/test/integration/live-api.test.ts index 971d71c..23e1063 100644 --- a/test/integration/live-api.test.ts +++ b/test/integration/live-api.test.ts @@ -10,7 +10,7 @@ import intoStream = require('into-stream') import debug = require('debug') import { CreateAssemblyOptions, Transloadit, UploadProgress } from '../../src/Transloadit' -import { createTestServer, CreateTestServerResult } from '../testserver' +import { createTestServer, TestServer } from '../testserver' const log = debug('transloadit:live-api') @@ -101,7 +101,7 @@ const genericOptions = { const handlers = new Map() -let testServer: CreateTestServerResult +let testServer: TestServer beforeAll(async () => { // cloudflared tunnels are a bit unstable, so we share one cloudflared tunnel between all tests diff --git a/test/testserver.ts b/test/testserver.ts index 21194a5..c421d50 100644 --- a/test/testserver.ts +++ b/test/testserver.ts @@ -43,7 +43,7 @@ async function createHttpServer(handler: RequestListener): Promise { export async function createTestServer( onRequest: RequestListener -): Promise { +) { if (!process.env.CLOUDFLARED_PATH) { throw new Error('CLOUDFLARED_PATH environment variable not set') } @@ -122,8 +122,4 @@ export async function createTestServer( } } -export interface CreateTestServerResult { - port: number - close: () => void - url: string -} +export type TestServer = Awaited> From 667355f5605772123209e15da7b8d71396a655d1 Mon Sep 17 00:00:00 2001 From: Mikael Finstad Date: Sat, 5 Oct 2024 11:34:40 +0200 Subject: [PATCH 13/23] prettier --- src/Transloadit.ts | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/src/Transloadit.ts b/src/Transloadit.ts index 9881440..f4f4c6e 100644 --- a/src/Transloadit.ts +++ b/src/Transloadit.ts @@ -170,8 +170,8 @@ export class Transloadit { chunkSize: requestedChunkSize = Infinity, uploadConcurrency = 10, timeout = 24 * 60 * 60 * 1000, // 1 day - onUploadProgress = () => {}, - onAssemblyProgress = () => {}, + onUploadProgress = () => { }, + onAssemblyProgress = () => { }, files = {}, uploads = {}, assemblyId, @@ -297,7 +297,7 @@ export class Transloadit { async awaitAssemblyCompletion( assemblyId: string, { - onAssemblyProgress = () => {}, + onAssemblyProgress = () => { }, timeout, startTimeMs = getHrTimeMs(), interval = 1000, @@ -712,7 +712,7 @@ export class Transloadit { private async _remoteJson( opts: RequestOptions, streamsMap?: Record, - onProgress: CreateAssemblyOptions['onUploadProgress'] = () => {} + onProgress: CreateAssemblyOptions['onUploadProgress'] = () => { } ): Promise { const { urlSuffix, From 22011e51eaf257a158d83f14d9c7bb1d808a56d8 Mon Sep 17 00:00:00 2001 From: Mikael Finstad Date: Sat, 5 Oct 2024 12:07:54 +0200 Subject: [PATCH 14/23] fix bug --- test/integration/live-api.test.ts | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/test/integration/live-api.test.ts b/test/integration/live-api.test.ts index 23e1063..8962cf9 100644 --- a/test/integration/live-api.test.ts +++ b/test/integration/live-api.test.ts @@ -4,7 +4,7 @@ import * as temp from 'temp' import { createWriteStream } from 'fs' import { IncomingMessage, RequestListener } from 'http' import { join } from 'path' -import { pipeline } from 'stream' +import { pipeline } from 'stream/promises' import got, { RequiredRetryOptions } from 'got' import intoStream = require('into-stream') import debug = require('debug') @@ -14,6 +14,7 @@ import { createTestServer, TestServer } from '../testserver' const log = debug('transloadit:live-api') + async function downloadTmpFile(url: string) { const { path } = await temp.open('transloadit') await pipeline(got.stream(url), createWriteStream(path)) From 48f7a15ae26d57ec607b208c972c5ab82313f54d Mon Sep 17 00:00:00 2001 From: Mikael Finstad Date: Sat, 5 Oct 2024 13:02:53 +0200 Subject: [PATCH 15/23] add timeout to tunnel --- test/tunnel.ts | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/test/tunnel.ts b/test/tunnel.ts index baae590..7d2d51f 100644 --- a/test/tunnel.ts +++ b/test/tunnel.ts @@ -3,6 +3,7 @@ import { createInterface } from 'readline' import { Resolver } from 'dns/promises' import debug from 'debug' import pRetry from 'p-retry' +import * as timers from 'timers/promises'; const log = debug('transloadit:cloudflared-tunnel') @@ -68,7 +69,7 @@ async function startTunnel({ if (!foundUrl) { const match = line.match(/(https:\/\/[^.]+\.trycloudflare\.com)/) if (!match) return - ;[, foundUrl] = match + ;[, foundUrl] = match } else { const match = line.match( /Connection [^\s+] registered connIndex=[^\s+] ip=[^\s+] location=[^\s+]/ @@ -94,11 +95,13 @@ export function createTunnel({ const urlPromise = (async () => { const tunnel = await pRetry(async () => startTunnel({ cloudFlaredPath, port }), { retries: 1 }) - ;({ process } = tunnel) + ; ({ process } = tunnel) const { url } = tunnel log('Found url', url) + await timers.setTimeout(3000); // seems to help to prevent timeouts (I think tunnel is not actually ready when cloudflared reports it to be) + // We need to wait for DNS to be resolvable. // If we don't, the operating system's dns cache will be poisoned by the not yet valid resolved entry // and it will forever fail for that subdomain name... From d7409c9390a285b4545d9e2f42e0ca2bd064f67a Mon Sep 17 00:00:00 2001 From: Mikael Finstad Date: Sat, 5 Oct 2024 13:03:18 +0200 Subject: [PATCH 16/23] improve commend --- src/Transloadit.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Transloadit.ts b/src/Transloadit.ts index f4f4c6e..05a8c59 100644 --- a/src/Transloadit.ts +++ b/src/Transloadit.ts @@ -222,7 +222,7 @@ export class Transloadit { }) ) - // Wrap in object structure (so we can know if it's a pathless stream or not) + // Wrap in object structure (so we can store whether it's a pathless stream or not) const allStreamsMap = Object.fromEntries( Object.entries(streamsMap).map(([label, stream]) => [label, { stream }]) ) From abe0d9d9e53bec758e975d6c5691c98fb0f62d93 Mon Sep 17 00:00:00 2001 From: Mikael Finstad Date: Sat, 5 Oct 2024 13:41:40 +0200 Subject: [PATCH 17/23] improve check for easier debugging --- test/integration/live-api.test.ts | 42 ++++++++++++++++--------------- 1 file changed, 22 insertions(+), 20 deletions(-) diff --git a/test/integration/live-api.test.ts b/test/integration/live-api.test.ts index 8962cf9..fd2ce4a 100644 --- a/test/integration/live-api.test.ts +++ b/test/integration/live-api.test.ts @@ -151,7 +151,7 @@ async function createVirtualTestServer(handler: RequestListener): Promise { +describe('API integration', { timeout: 60000 }, () => { describe('assembly creation', () => { it('should create a retrievable assembly on the server', async () => { const client = createClient() @@ -280,25 +280,27 @@ describe('API integration', { timeout: 30000 }, () => { original_md5hash: '1b199e02dd833b2278ce2a0e75480b14', }) // Because order is not same as input - const uploadsKeyed = Object.fromEntries(result.uploads.map((upload) => [upload.name, upload])) - expect(uploadsKeyed.file1).toMatchObject(getMatchObject({ name: 'file1' })) - expect(uploadsKeyed.file2).toMatchObject(getMatchObject({ name: 'file2' })) - expect(uploadsKeyed.file3).toMatchObject(getMatchObject({ name: 'file3' })) - expect(uploadsKeyed.file4).toMatchObject({ - name: 'file4', - basename: 'file4', - ext: 'jpg', - size: 133788, - mime: 'image/jpeg', - type: 'image', - field: 'file4', - md5hash: '42f29c0d9d5f3ea807ef3c327f8c5890', - original_basename: 'file4', - original_name: 'file4', - original_path: '/', - original_md5hash: '42f29c0d9d5f3ea807ef3c327f8c5890', - }) - }) + const uploadsMap = Object.fromEntries(result.uploads.map((upload) => [upload.name, upload])) + expect(uploadsMap).toEqual({ + file1: expect.objectContaining(getMatchObject({ name: 'file1' })), + file2: expect.objectContaining(getMatchObject({ name: 'file2' })), + file3: expect.objectContaining(getMatchObject({ name: 'file3' })), + file4: expect.objectContaining({ + name: 'file4', + basename: 'file4', + ext: 'jpg', + size: 133788, + mime: 'image/jpeg', + type: 'image', + field: 'file4', + md5hash: '42f29c0d9d5f3ea807ef3c327f8c5890', + original_basename: 'file4', + original_name: 'file4', + original_path: '/', + original_md5hash: '42f29c0d9d5f3ea807ef3c327f8c5890', + }), + }); + }); it('should allow setting an explicit assemblyId on createAssembly', async () => { const client = createClient() From 79b18eb2900817d2020bb8fb5b91bc9c5a862428 Mon Sep 17 00:00:00 2001 From: Mikael Finstad Date: Sat, 5 Oct 2024 13:49:19 +0200 Subject: [PATCH 18/23] enable ts rules "exactOptionalPropertyTypes": true, "noPropertyAccessFromIndexSignature": true, "noUncheckedIndexedAccess": true, --- src/Transloadit.ts | 20 ++++++++++---------- src/tus.ts | 8 ++++---- tsconfig.build.json | 5 ++++- 3 files changed, 18 insertions(+), 15 deletions(-) diff --git a/src/Transloadit.ts b/src/Transloadit.ts index 05a8c59..6bb86e6 100644 --- a/src/Transloadit.ts +++ b/src/Transloadit.ts @@ -67,7 +67,7 @@ function decorateHttpError(err: TransloaditError, body: any): TransloaditError { /* eslint-disable no-param-reassign */ err.message = newMessage - err.stack = newStack + if (newStack != null) err.stack = newStack if (body.assembly_id) err.assemblyId = body.assembly_id if (body.error) err.transloaditErrorCode = body.error /* eslint-enable no-param-reassign */ @@ -685,14 +685,14 @@ export class Transloadit { if (params == null) { params = {} } - if (params.auth == null) { - params.auth = {} + if (params['auth'] == null) { + params['auth'] = {} } - if (params.auth.key == null) { - params.auth.key = this._authKey + if (params['auth'].key == null) { + params['auth'].key = this._authKey } - if (params.auth.expires == null) { - params.auth.expires = this._getExpiresDate() + if (params['auth'].expires == null) { + params['auth'].expires = this._getExpiresDate() } return JSON.stringify(params) @@ -748,7 +748,7 @@ export class Transloadit { const requestOpts: OptionsOfJSONResponseBody = { retry: this._gotRetry, - body: form, + body: form as FormData, timeout, headers: { 'Transloadit-Client': `node-sdk:${version}`, @@ -843,8 +843,8 @@ export interface KeyVal { } export interface UploadProgress { - uploadedBytes?: number - totalBytes?: number + uploadedBytes?: number | undefined + totalBytes?: number | undefined } /** https://transloadit.com/docs/api/assembly-status-response/#explanation-of-fields */ diff --git a/src/tus.ts b/src/tus.ts index 9b284a7..1280496 100644 --- a/src/tus.ts +++ b/src/tus.ts @@ -30,13 +30,13 @@ export async function sendTusRequest({ const sizes: Record = {} - const haveUnknownLengthStreams = streamLabels.some((label) => !streamsMap[label].path) + const haveUnknownLengthStreams = streamLabels.some((label) => !streamsMap[label]!.path) // Initialize size data await pMap( streamLabels, async (label) => { - const { path } = streamsMap[label] + const { path } = streamsMap[label]! if (path) { const { size } = await stat(path) @@ -52,7 +52,7 @@ export async function sendTusRequest({ async function uploadSingleStream(label: string) { uploadProgresses[label] = 0 - const { stream, path } = streamsMap[label] + const { stream, path } = streamsMap[label]! const size = sizes[label] let chunkSize = requestedChunkSize @@ -71,7 +71,7 @@ export async function sendTusRequest({ // get all uploaded bytes for all files let uploadedBytes = 0 for (const l of streamLabels) { - uploadedBytes += uploadProgresses[l] + uploadedBytes += uploadProgresses[l] ?? 0 } // don't send redundant progress diff --git a/tsconfig.build.json b/tsconfig.build.json index c42a657..5162c96 100644 --- a/tsconfig.build.json +++ b/tsconfig.build.json @@ -9,6 +9,9 @@ "resolveJsonModule": true, "rootDir": "src", "sourceMap": true, - "strict": true + "strict": true, + "exactOptionalPropertyTypes": true, + "noPropertyAccessFromIndexSignature": true, + "noUncheckedIndexedAccess": true, } } From 7f18d79fdd938a44ad7dad3de6294b22dd4c45ed Mon Sep 17 00:00:00 2001 From: Mikael Finstad Date: Sat, 5 Oct 2024 13:49:49 +0200 Subject: [PATCH 19/23] run prettier --- src/Transloadit.ts | 38 ++++++++++++++++--------------- test/integration/live-api.test.ts | 5 ++-- test/testserver.ts | 4 +--- test/tunnel.ts | 8 +++---- tsconfig.build.json | 2 +- 5 files changed, 28 insertions(+), 29 deletions(-) diff --git a/src/Transloadit.ts b/src/Transloadit.ts index 6bb86e6..1afd45b 100644 --- a/src/Transloadit.ts +++ b/src/Transloadit.ts @@ -170,8 +170,8 @@ export class Transloadit { chunkSize: requestedChunkSize = Infinity, uploadConcurrency = 10, timeout = 24 * 60 * 60 * 1000, // 1 day - onUploadProgress = () => { }, - onAssemblyProgress = () => { }, + onUploadProgress = () => {}, + onAssemblyProgress = () => {}, files = {}, uploads = {}, assemblyId, @@ -297,7 +297,7 @@ export class Transloadit { async awaitAssemblyCompletion( assemblyId: string, { - onAssemblyProgress = () => { }, + onAssemblyProgress = () => {}, timeout, startTimeMs = getHrTimeMs(), interval = 1000, @@ -712,7 +712,7 @@ export class Transloadit { private async _remoteJson( opts: RequestOptions, streamsMap?: Record, - onProgress: CreateAssemblyOptions['onUploadProgress'] = () => { } + onProgress: CreateAssemblyOptions['onUploadProgress'] = () => {} ): Promise { const { urlSuffix, @@ -780,20 +780,22 @@ export class Transloadit { // check whether we should retry // https://transloadit.com/blog/2012/04/introducing-rate-limiting/ - if (!( - statusCode === 413 && - typeof body === 'object' && - body != null && - 'error' in body && - body.error === 'RATE_LIMIT_REACHED' && - 'info' in body && - typeof body.info === 'object' && - body.info != null && - 'retryIn' in body.info && - typeof body.info.retryIn === 'number' && - Boolean(body.info.retryIn) && - retryCount < this._maxRetries - )) { + if ( + !( + statusCode === 413 && + typeof body === 'object' && + body != null && + 'error' in body && + body.error === 'RATE_LIMIT_REACHED' && + 'info' in body && + typeof body.info === 'object' && + body.info != null && + 'retryIn' in body.info && + typeof body.info.retryIn === 'number' && + Boolean(body.info.retryIn) && + retryCount < this._maxRetries + ) + ) { throw decorateHttpError(err, body) } diff --git a/test/integration/live-api.test.ts b/test/integration/live-api.test.ts index fd2ce4a..d7f20eb 100644 --- a/test/integration/live-api.test.ts +++ b/test/integration/live-api.test.ts @@ -14,7 +14,6 @@ import { createTestServer, TestServer } from '../testserver' const log = debug('transloadit:live-api') - async function downloadTmpFile(url: string) { const { path } = await temp.open('transloadit') await pipeline(got.stream(url), createWriteStream(path)) @@ -299,8 +298,8 @@ describe('API integration', { timeout: 60000 }, () => { original_path: '/', original_md5hash: '42f29c0d9d5f3ea807ef3c327f8c5890', }), - }); - }); + }) + }) it('should allow setting an explicit assemblyId on createAssembly', async () => { const client = createClient() diff --git a/test/testserver.ts b/test/testserver.ts index c421d50..dca05c1 100644 --- a/test/testserver.ts +++ b/test/testserver.ts @@ -41,9 +41,7 @@ async function createHttpServer(handler: RequestListener): Promise { }) } -export async function createTestServer( - onRequest: RequestListener -) { +export async function createTestServer(onRequest: RequestListener) { if (!process.env.CLOUDFLARED_PATH) { throw new Error('CLOUDFLARED_PATH environment variable not set') } diff --git a/test/tunnel.ts b/test/tunnel.ts index 7d2d51f..e378abf 100644 --- a/test/tunnel.ts +++ b/test/tunnel.ts @@ -3,7 +3,7 @@ import { createInterface } from 'readline' import { Resolver } from 'dns/promises' import debug from 'debug' import pRetry from 'p-retry' -import * as timers from 'timers/promises'; +import * as timers from 'timers/promises' const log = debug('transloadit:cloudflared-tunnel') @@ -69,7 +69,7 @@ async function startTunnel({ if (!foundUrl) { const match = line.match(/(https:\/\/[^.]+\.trycloudflare\.com)/) if (!match) return - ;[, foundUrl] = match + ;[, foundUrl] = match } else { const match = line.match( /Connection [^\s+] registered connIndex=[^\s+] ip=[^\s+] location=[^\s+]/ @@ -95,12 +95,12 @@ export function createTunnel({ const urlPromise = (async () => { const tunnel = await pRetry(async () => startTunnel({ cloudFlaredPath, port }), { retries: 1 }) - ; ({ process } = tunnel) + ;({ process } = tunnel) const { url } = tunnel log('Found url', url) - await timers.setTimeout(3000); // seems to help to prevent timeouts (I think tunnel is not actually ready when cloudflared reports it to be) + await timers.setTimeout(3000) // seems to help to prevent timeouts (I think tunnel is not actually ready when cloudflared reports it to be) // We need to wait for DNS to be resolvable. // If we don't, the operating system's dns cache will be poisoned by the not yet valid resolved entry diff --git a/tsconfig.build.json b/tsconfig.build.json index 5162c96..95a5419 100644 --- a/tsconfig.build.json +++ b/tsconfig.build.json @@ -12,6 +12,6 @@ "strict": true, "exactOptionalPropertyTypes": true, "noPropertyAccessFromIndexSignature": true, - "noUncheckedIndexedAccess": true, + "noUncheckedIndexedAccess": true } } From 702d1036ac8f42590be28e958cffbc1c4f8215de Mon Sep 17 00:00:00 2001 From: Mikael Finstad Date: Sat, 5 Oct 2024 14:32:40 +0200 Subject: [PATCH 20/23] fix ts error --- src/tus.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/tus.ts b/src/tus.ts index 1280496..98eea8d 100644 --- a/src/tus.ts +++ b/src/tus.ts @@ -96,12 +96,12 @@ export async function sendTusRequest({ fieldname: label, filename, }, - uploadSize: size, onError: reject, onProgress: onTusProgress, onSuccess: resolve, } // tus-js-client doesn't like undefined/null + if (size != null) tusOptions.uploadSize = size if (chunkSize) tusOptions.chunkSize = chunkSize if (uploadLengthDeferred) tusOptions.uploadLengthDeferred = uploadLengthDeferred From b10c396f660b15e70ab39e7ccf15a41272bf4c1f Mon Sep 17 00:00:00 2001 From: Mikael Finstad Date: Sat, 5 Oct 2024 14:50:15 +0200 Subject: [PATCH 21/23] add rules "isolatedModules": true, "esModuleInterop": true, "skipLibCheck": true, "noImplicitOverride": true, "noImplicitReturns": true, --- src/InconsistentResponseError.ts | 2 +- src/PaginationStream.ts | 2 +- src/PollingTimeoutError.ts | 2 +- src/TransloaditError.ts | 2 +- tsconfig.build.json | 7 ++++++- 5 files changed, 10 insertions(+), 5 deletions(-) diff --git a/src/InconsistentResponseError.ts b/src/InconsistentResponseError.ts index 3b32efc..fbc5fe9 100644 --- a/src/InconsistentResponseError.ts +++ b/src/InconsistentResponseError.ts @@ -1,3 +1,3 @@ export class InconsistentResponseError extends Error { - name = 'InconsistentResponseError' + override name = 'InconsistentResponseError' } diff --git a/src/PaginationStream.ts b/src/PaginationStream.ts index d153efe..9dad02f 100644 --- a/src/PaginationStream.ts +++ b/src/PaginationStream.ts @@ -15,7 +15,7 @@ export class PaginationStream extends Readable { this._fetchPage = fetchPage } - async _read() { + override async _read() { if (this._items.length > 0) { this._itemsRead++ process.nextTick(() => this.push(this._items.pop())) diff --git a/src/PollingTimeoutError.ts b/src/PollingTimeoutError.ts index 1c87004..016a9b2 100644 --- a/src/PollingTimeoutError.ts +++ b/src/PollingTimeoutError.ts @@ -1,4 +1,4 @@ export class PollingTimeoutError extends Error { - name = 'PollingTimeoutError' + override name = 'PollingTimeoutError' code = 'POLLING_TIMED_OUT' } diff --git a/src/TransloaditError.ts b/src/TransloaditError.ts index dc82a1a..a67897a 100644 --- a/src/TransloaditError.ts +++ b/src/TransloaditError.ts @@ -1,5 +1,5 @@ export class TransloaditError extends Error { - name = 'TransloaditError' + override name = 'TransloaditError' response: { body: unknown } assemblyId?: string transloaditErrorCode?: string diff --git a/tsconfig.build.json b/tsconfig.build.json index 95a5419..67a02f7 100644 --- a/tsconfig.build.json +++ b/tsconfig.build.json @@ -12,6 +12,11 @@ "strict": true, "exactOptionalPropertyTypes": true, "noPropertyAccessFromIndexSignature": true, - "noUncheckedIndexedAccess": true + "noUncheckedIndexedAccess": true, + "isolatedModules": true, + "esModuleInterop": true, + "skipLibCheck": true, + "noImplicitOverride": true, + "noImplicitReturns": true } } From 366b6bdf0c861098eb79ad50446dd593b13a8c37 Mon Sep 17 00:00:00 2001 From: Remco Haszing Date: Fri, 11 Oct 2024 11:49:23 +0200 Subject: [PATCH 22/23] Update TypeScript compiler options --- tsconfig.build.json | 14 +++++--------- tsconfig.json | 4 ++++ 2 files changed, 9 insertions(+), 9 deletions(-) diff --git a/tsconfig.build.json b/tsconfig.build.json index 67a02f7..fe9903d 100644 --- a/tsconfig.build.json +++ b/tsconfig.build.json @@ -4,19 +4,15 @@ "composite": true, "declaration": true, "declarationMap": true, + "exactOptionalPropertyTypes": true, + "isolatedModules": true, "module": "node16", + "noImplicitOverride": true, + "noUncheckedIndexedAccess": true, "outDir": "dist", "resolveJsonModule": true, "rootDir": "src", "sourceMap": true, - "strict": true, - "exactOptionalPropertyTypes": true, - "noPropertyAccessFromIndexSignature": true, - "noUncheckedIndexedAccess": true, - "isolatedModules": true, - "esModuleInterop": true, - "skipLibCheck": true, - "noImplicitOverride": true, - "noImplicitReturns": true + "strict": true } } diff --git a/tsconfig.json b/tsconfig.json index 7467df7..aa6f95a 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -2,7 +2,11 @@ "exclude": ["src"], "references": [{ "path": "./tsconfig.build.json" }], "compilerOptions": { + "exactOptionalPropertyTypes": true, + "isolatedModules": true, "module": "node16", + "noImplicitOverride": true, + "noUncheckedIndexedAccess": true, "noEmit": true, "resolveJsonModule": true, "strict": true, From 8140cbc972ab56f6a684c362da4906492e29fba8 Mon Sep 17 00:00:00 2001 From: Remco Haszing Date: Fri, 11 Oct 2024 11:52:26 +0200 Subject: [PATCH 23/23] Remove TypeScript option exactOptionalPropertyTypes Our code is not compatible with this. --- tsconfig.build.json | 2 -- tsconfig.json | 2 -- 2 files changed, 4 deletions(-) diff --git a/tsconfig.build.json b/tsconfig.build.json index fe9903d..6e1531c 100644 --- a/tsconfig.build.json +++ b/tsconfig.build.json @@ -4,11 +4,9 @@ "composite": true, "declaration": true, "declarationMap": true, - "exactOptionalPropertyTypes": true, "isolatedModules": true, "module": "node16", "noImplicitOverride": true, - "noUncheckedIndexedAccess": true, "outDir": "dist", "resolveJsonModule": true, "rootDir": "src", diff --git a/tsconfig.json b/tsconfig.json index aa6f95a..c6a629d 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -2,11 +2,9 @@ "exclude": ["src"], "references": [{ "path": "./tsconfig.build.json" }], "compilerOptions": { - "exactOptionalPropertyTypes": true, "isolatedModules": true, "module": "node16", "noImplicitOverride": true, - "noUncheckedIndexedAccess": true, "noEmit": true, "resolveJsonModule": true, "strict": true,