diff --git a/.codegen.json b/.codegen.json index 300e7426..9faf0a42 100644 --- a/.codegen.json +++ b/.codegen.json @@ -1 +1 @@ -{ "engineHash": "80d1f7a", "specHash": "e798cb1", "version": "1.7.0" } +{ "engineHash": "2efc8ab", "specHash": "e798cb1", "version": "1.7.0" } diff --git a/docs/downloads.md b/docs/downloads.md index 6e9c2c85..0224e7a1 100644 --- a/docs/downloads.md +++ b/docs/downloads.md @@ -1,25 +1,41 @@ -# Downloads +# DownloadsManager -Downloads module is used to download files from Box. +- [Download file](#download-file) - - +## Download file -- [Download a File](#download-a-file) +Returns the contents of a file in binary format. - +This operation is performed by calling function `downloadFile`. -## Download a File - -To get the entire contents of the file as `ArrayBuffer`, call `downloadFile` method. -This method returns a `ArrayBuffer` object which contains the file content. +See the endpoint docs at +[API Reference](https://developer.box.com/reference/get-files-id-content/). -```js -const fs = require('fs'); - -const fileContent = await client.downloads.downloadFile('123456789'); -const fileWriteStream = fs.createWriteStream('file.pdf'); -fileContent.pipe(fileWriteStream); +```ts +await clientWithInterceptor.downloads.downloadFile(uploadedFile.id); ``` + +### Arguments + +- fileId `string` + - The unique identifier that represents a file. The ID for any file can be determined by visiting a file in the web application and copying the ID from the URL. For example, for the URL `https://*.app.box.com/files/123` the `file_id` is `123`. Example: "12345" +- optionalsInput `DownloadFileOptionalsInput` + - + +### Returns + +This function returns a value of type `ByteStream`. + +Returns the requested file if the client has the **follow +redirects** setting enabled to automatically +follow HTTP `3xx` responses as redirects. If not, the request +will return `302` instead. +For details, see +the [download file guide](g://downloads/file#download-url).If the file is not ready to be downloaded yet `Retry-After` header will +be returned indicating the time in seconds after which the file will +be available for the client to download. + +This response can occur when the file was uploaded immediately before the +download request. diff --git a/docs/uploads.md b/docs/uploads.md index 7ca985e5..e204544e 100644 --- a/docs/uploads.md +++ b/docs/uploads.md @@ -1,29 +1,114 @@ -# Uploads +# UploadsManager -Uploads module is used to upload files to Box. It supports uploading files from a readable stream. For now, it only supports uploading small files without chunked upload. +- [Upload file version](#upload-file-version) +- [Preflight check before upload](#preflight-check-before-upload) +- [Upload file](#upload-file) - - +## Upload file version -- [Upload a File](#upload-a-file) +Update a file's content. For file sizes over 50MB we recommend +using the Chunk Upload APIs. - +The `attributes` part of the body must come **before** the +`file` part. Requests that do not follow this format when +uploading the file will receive a HTTP `400` error with a +`metadata_after_file_contents` error code. -## Upload a File +This operation is performed by calling function `uploadFileVersion`. -To upload a small file from a readable stream, call `uploadFile` method. This method returns a `Files` object which contains information about the uploaded files. +See the endpoint docs at +[API Reference](https://developer.box.com/reference/post-files-id-content/). + + + +```ts +await client.uploads.uploadFileVersion(file.id, { + attributes: { + name: file.name!, + } satisfies UploadFileVersionRequestBodyAttributesField, + file: generateByteStream(20), +} satisfies UploadFileVersionRequestBody); +``` + +### Arguments + +- fileId `string` + - The unique identifier that represents a file. The ID for any file can be determined by visiting a file in the web application and copying the ID from the URL. For example, for the URL `https://*.app.box.com/files/123` the `file_id` is `123`. Example: "12345" +- requestBody `UploadFileVersionRequestBody` + - Request body of uploadFileVersion method +- optionalsInput `UploadFileVersionOptionalsInput` + - + +### Returns + +This function returns a value of type `Files`. + +Returns the new file object in a list. + +## Preflight check before upload + +Performs a check to verify that a file will be accepted by Box +before you upload the entire file. + +This operation is performed by calling function `preflightFileUploadCheck`. + +See the endpoint docs at +[API Reference](https://developer.box.com/reference/options-files-content/). + +_Currently we don't have an example for calling `preflightFileUploadCheck` in integration tests_ + +### Arguments + +- requestBody `PreflightFileUploadCheckRequestBody` + - Request body of preflightFileUploadCheck method +- headersInput `PreflightFileUploadCheckHeadersInput` + - Headers of preflightFileUploadCheck method +- cancellationToken `undefined | CancellationToken` + - Token used for request cancellation. + +### Returns + +This function returns a value of type `UploadUrl`. + +If the check passed, the response will include a session URL that +can be used to upload the file to. + +## Upload file + +Uploads a small file to Box. For file sizes over 50MB we recommend +using the Chunk Upload APIs. + +The `attributes` part of the body must come **before** the +`file` part. Requests that do not follow this format when +uploading the file will receive a HTTP `400` error with a +`metadata_after_file_contents` error code. + +This operation is performed by calling function `uploadFile`. + +See the endpoint docs at +[API Reference](https://developer.box.com/reference/post-files-content/). -```js -const fs = require('fs'); - -const attrs = { name: 'filename.txt', parent: { id: '0' } }; -const body = { - attributes: attrs, - file: fs.createReadStream('filename.txt'), -}; -const files = await client.uploads.uploadFile(body); -const file = files.entries[0]; -console.log(`File uploaded with id ${file.id}, name ${file.name}`); +```ts +await parentClient.uploads.uploadFile({ + attributes: { + name: getUuid(), + parent: { id: '0' } satisfies UploadFileRequestBodyAttributesParentField, + } satisfies UploadFileRequestBodyAttributesField, + file: generateByteStream(1024 * 1024), +} satisfies UploadFileRequestBody); ``` + +### Arguments + +- requestBody `UploadFileRequestBody` + - Request body of uploadFile method +- optionalsInput `UploadFileOptionalsInput` + - + +### Returns + +This function returns a value of type `Files`. + +Returns the new file object in a list. diff --git a/package-lock.json b/package-lock.json index f45bbcbf..1dac9272 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1088,14 +1088,14 @@ } }, "node_modules/@rollup/pluginutils": { - "version": "5.1.2", - "resolved": "https://registry.npmjs.org/@rollup/pluginutils/-/pluginutils-5.1.2.tgz", - "integrity": "sha512-/FIdS3PyZ39bjZlwqFnWqCOVnW7o963LtKMwQOD0NhQqw22gSr2YY1afu3FxRip4ZCZNsD5jq6Aaz6QV3D/Njw==", + "version": "5.1.3", + "resolved": "https://registry.npmjs.org/@rollup/pluginutils/-/pluginutils-5.1.3.tgz", + "integrity": "sha512-Pnsb6f32CD2W3uCaLZIzDmeFyQ2b8UWMFI7xtwUezpcGBDVDW6y9XgAWIlARiGAo6eNF5FK5aQTr0LFyNyqq5A==", "dev": true, "dependencies": { "@types/estree": "^1.0.0", "estree-walker": "^2.0.2", - "picomatch": "^2.3.1" + "picomatch": "^4.0.2" }, "engines": { "node": ">=14.0.0" @@ -1565,6 +1565,18 @@ "node": ">= 8" } }, + "node_modules/anymatch/node_modules/picomatch": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.1.tgz", + "integrity": "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==", + "dev": true, + "engines": { + "node": ">=8.6" + }, + "funding": { + "url": "https://github.com/sponsors/jonschlinkert" + } + }, "node_modules/argparse": { "version": "1.0.10", "resolved": "https://registry.npmjs.org/argparse/-/argparse-1.0.10.tgz", @@ -2140,9 +2152,9 @@ } }, "node_modules/electron-to-chromium": { - "version": "1.5.43", - "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.5.43.tgz", - "integrity": "sha512-NxnmFBHDl5Sachd2P46O7UJiMaMHMLSofoIWVJq3mj8NJgG0umiSeljAVP9lGzjI0UDLJJ5jjoGjcrB8RSbjLQ==", + "version": "1.5.45", + "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.5.45.tgz", + "integrity": "sha512-vOzZS6uZwhhbkZbcRyiy99Wg+pYFV5hk+5YaECvx0+Z31NR3Tt5zS6dze2OepT6PCTzVzT0dIJItti+uAW5zmw==", "dev": true }, "node_modules/emittery": { @@ -3398,6 +3410,18 @@ "node": "^14.15.0 || ^16.10.0 || >=18.0.0" } }, + "node_modules/jest-util/node_modules/picomatch": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.1.tgz", + "integrity": "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==", + "dev": true, + "engines": { + "node": ">=8.6" + }, + "funding": { + "url": "https://github.com/sponsors/jonschlinkert" + } + }, "node_modules/jest-validate": { "version": "29.7.0", "resolved": "https://registry.npmjs.org/jest-validate/-/jest-validate-29.7.0.tgz", @@ -3670,6 +3694,18 @@ "node": ">=8.6" } }, + "node_modules/micromatch/node_modules/picomatch": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.1.tgz", + "integrity": "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==", + "dev": true, + "engines": { + "node": ">=8.6" + }, + "funding": { + "url": "https://github.com/sponsors/jonschlinkert" + } + }, "node_modules/mime-db": { "version": "1.52.0", "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.52.0.tgz", @@ -3944,12 +3980,12 @@ "dev": true }, "node_modules/picomatch": { - "version": "2.3.1", - "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.1.tgz", - "integrity": "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==", + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.2.tgz", + "integrity": "sha512-M7BAV6Rlcy5u+m6oPhAPFgJTzAioX/6B0DxyvDlo9l8+T3nLKbrczg2WLUyzd45L8RqfUMyGPzekbMvX2Ldkwg==", "dev": true, "engines": { - "node": ">=8.6" + "node": ">=12" }, "funding": { "url": "https://github.com/sponsors/jonschlinkert" diff --git a/src/internal/utils.test.ts b/src/internal/utils.test.ts new file mode 100644 index 00000000..f13601ff --- /dev/null +++ b/src/internal/utils.test.ts @@ -0,0 +1,39 @@ +import { + dateFromString, + dateTimeFromString, + dateTimeToString, + dateToString, + generateByteStream, + hexStrToBase64, + readByteStream, +} from './utils'; + +test('stream', async () => { + const size = 1024 * 1024; + const buffer = await readByteStream(generateByteStream(size)); + expect(buffer.length).toBe(size); +}); + +test('hexStrToBase64', () => { + const hexStr = 'a94a8fe5ccb19ba61c4c0873d391e987982fbbd3'; + const base64 = 'qUqP5cyxm6YcTAhz05Hph5gvu9M='; + expect(hexStrToBase64(hexStr)).toBe(base64); +}); + +describe('date conversions', () => { + test('dateFromString and dateToString', () => { + const dateString = '2024-04-25'; + const dateWrapper = dateFromString(dateString); + const result = dateToString(dateWrapper); + expect(result).toBe(dateString); + }); +}); + +describe('datetime conversions', () => { + test('dateTimeFromString and dateTimeToString', () => { + const dateTimeString = '2024-04-25T12:30:00+00:00'; + const dateTimeWrapper = dateTimeFromString(dateTimeString); + const result = dateTimeToString(dateTimeWrapper); + expect(result).toBe('2024-04-25T12:30:00+00:00'); + }); +}); diff --git a/src/networking/fetch.test.ts b/src/networking/fetch.test.ts new file mode 100644 index 00000000..8233cd36 --- /dev/null +++ b/src/networking/fetch.test.ts @@ -0,0 +1,52 @@ +import fetch from 'node-fetch'; +import { fetch as builtInFetch, userAgentHeader, xBoxUaHeader } from './fetch'; +import { prepareParams, toString } from '../internal/utils'; + +jest.mock('node-fetch', () => + jest.fn(async () => ({ + text: async () => '', + arrayBuffer: async () => new ArrayBuffer(0), + })) +); + +// The fetch method is using NetworkSession, but NetworkSession using BaseUrls +// which is generated during the build process. +// Skip this test for now +test.skip('fetch parses headers correctly and adds analytic headers', async () => { + // The request will fail but we ignore that + // await expect( + // builtInFetch('url', { + // params: prepareParams( + // Object.fromEntries([ + // ['key1', toString('value1')], + // ['key2', toString(null)], + // ['key3', toString('value3')], + // ['key4', toString(void 0)], + // ['key5', toString(42)], + // ['key6', toString(true)], + // ]) + // ), + // headers: prepareParams( + // Object.fromEntries([ + // ['key1', toString('value1')], + // ['key2', toString(null)], + // ['key3', toString('value3')], + // ['key4', toString(void 0)], + // ]) + // ), + // }) + // ).rejects.toThrow(); + // expect(fetch as jest.Mock).toBeCalledWith( + // 'url?key1=value1&key3=value3&key5=42&key6=true', + // { + // method: 'GET', + // headers: { + // 'Content-Type': 'application/json', + // key1: 'value1', + // key3: 'value3', + // 'User-Agent': userAgentHeader, + // 'X-Box-UA': xBoxUaHeader, + // }, + // } + // ); +}); diff --git a/src/run.test.ts b/src/run.test.ts new file mode 100644 index 00000000..8835f99e --- /dev/null +++ b/src/run.test.ts @@ -0,0 +1,19 @@ +import { Readable } from 'stream'; +import run from './run'; + +test('Uses standard input/output', async () => { + const args = [2, 2]; + + process = Object.create(process, { + stdin: { + value: Readable.from(Buffer.from(JSON.stringify(args), 'utf-8')), + }, + }); + + jest.spyOn(global.console, 'log').mockImplementation(); + + await run(async (a: number, b: number) => 2 + 2); + + expect(console.log).toBeCalledTimes(1); + expect(console.log).toBeCalledWith(4); +}); diff --git a/src/serialization/json.test.ts b/src/serialization/json.test.ts new file mode 100644 index 00000000..905ec76d --- /dev/null +++ b/src/serialization/json.test.ts @@ -0,0 +1,29 @@ +import { + sdToJson, + jsonToSerializedData, + sdIsNumber, + sdIsBoolean, + sdIsMap, +} from './json'; + +test('sdToJson', () => + expect( + sdToJson({ + firstName: 'John', + lastName: 'Smith', + }) + ).toBe('{"firstName":"John","lastName":"Smith"}')); + +test('jsonToSerializedData', () => + expect( + jsonToSerializedData('{"firstName":"John","lastName":"Smith"}') + ).toEqual({ + firstName: 'John', + lastName: 'Smith', + })); + +test('isJSON', () => { + expect(sdIsNumber(5)).toBe(true); + expect(sdIsBoolean(5)).toBe(false); + expect(sdIsMap({})).toBe(true); +});