diff --git a/package.json b/package.json index 88df0ff..edc2907 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { - "name": "fileforge", - "version": "0.0.9", + "name": "@fileforge/client", + "version": "0.1.0", "private": false, "repository": "https://github.com/OnedocLabs/fileforge-node-sdk", "license": "MIT", @@ -15,11 +15,11 @@ "dependencies": { "url-join": "4.0.1", "form-data": "4.0.0", + "formdata-node": "^6.0.3", "node-fetch": "2.7.0", "qs": "6.11.2", - "formdata-node": "^6.0.3", "form-data-encoder": "^4.0.2", - "js-base64": "3.7.2" + "mime-types": "^2.1.35" }, "devDependencies": { "@types/url-join": "4.0.1", @@ -31,6 +31,7 @@ "jest-environment-jsdom": "29.7.0", "@types/node": "17.0.33", "prettier": "2.7.1", - "typescript": "4.6.4" + "typescript": "4.6.4", + "@types/mime-types": "^2.1.4" } } diff --git a/src/Client.ts b/src/Client.ts index 572ccb8..1bf57cf 100644 --- a/src/Client.ts +++ b/src/Client.ts @@ -5,93 +5,215 @@ import * as environments from "./environments"; import * as core from "./core"; import * as fs from "fs"; -import * as FileForge from "./api/index"; +import * as Fileforge from "./api/index"; import * as stream from "stream"; import urlJoin from "url-join"; import * as errors from "./errors/index"; import * as serializers from "./serialization/index"; -export declare namespace FileForgeClient { +export declare namespace FileforgeClient { interface Options { - environment?: core.Supplier; + environment?: core.Supplier; apiKey: core.Supplier; } interface RequestOptions { timeoutInSeconds?: number; maxRetries?: number; + abortSignal?: AbortSignal; } } -export class FileForgeClient { - constructor(protected readonly _options: FileForgeClient.Options) {} +export class FileforgeClient { + constructor(protected readonly _options: FileforgeClient.Options) {} /** * Generates a PDF document from web assets. - * @throws {@link FileForge.BadRequestError} - * @throws {@link FileForge.UnauthorizedError} - * @throws {@link FileForge.InternalServerError} - * @throws {@link FileForge.BadGatewayError} + * @throws {@link Fileforge.BadRequestError} + * @throws {@link Fileforge.UnauthorizedError} + * @throws {@link Fileforge.InternalServerError} + * @throws {@link Fileforge.BadGatewayError} */ public async generate( files: File[] | fs.ReadStream[], - request: FileForge.GenerateRequest, - requestOptions?: FileForgeClient.RequestOptions + request: Fileforge.GenerateRequest, + requestOptions?: FileforgeClient.RequestOptions ): Promise { - const _request = core.newFormData(); - const options = await serializers.GenerateRequestOptions.jsonOrThrow(request.options, { - unrecognizedObjectKeys: "passthrough", - allowUnrecognizedUnionMembers: false, - allowUnrecognizedEnumValues: false, - breadcrumbsPrefix: [""], - }); - await _request.append("options", new Blob([JSON.stringify(options)], { type: "application/json" })); + const _request = new core.FormDataWrapper(); + await _request.append("options", JSON.stringify(request.options)); for (const _file of files) { await _request.append("files", _file); } + + const _maybeEncodedRequest = _request.getRequest(); const _response = await core.fetcher({ url: urlJoin( - (await core.Supplier.get(this._options.environment)) ?? environments.FileForgeEnvironment.Default, + (await core.Supplier.get(this._options.environment)) ?? environments.FileforgeEnvironment.Default, "pdf/generate/" ), method: "POST", headers: { - "X-API-Key": await core.Supplier.get(this._options.apiKey), "X-Fern-Language": "JavaScript", - "X-Fern-SDK-Name": "fileforge", - "X-Fern-SDK-Version": "0.0.1", + "X-Fern-SDK-Name": "@fileforge/client", + "X-Fern-SDK-Version": "0.1.0", + "X-Fern-Runtime": core.RUNTIME.type, + "X-Fern-Runtime-Version": core.RUNTIME.version, + ...(await this._getCustomAuthorizationHeaders()), + ...(await _maybeEncodedRequest.getHeaders()), + }, + body: await _maybeEncodedRequest.getBody(), + responseType: "streaming", + timeoutMs: requestOptions?.timeoutInSeconds != null ? requestOptions.timeoutInSeconds * 1000 : 60000, + maxRetries: requestOptions?.maxRetries, + abortSignal: requestOptions?.abortSignal, + }); + if (_response.ok) { + return _response.body; + } + + if (_response.error.reason === "status-code") { + switch (_response.error.statusCode) { + case 400: + throw new Fileforge.BadRequestError( + await serializers.ErrorSchema.parseOrThrow(_response.error.body, { + unrecognizedObjectKeys: "passthrough", + allowUnrecognizedUnionMembers: true, + allowUnrecognizedEnumValues: true, + breadcrumbsPrefix: ["response"], + }) + ); + case 401: + throw new Fileforge.UnauthorizedError( + await serializers.ErrorSchema.parseOrThrow(_response.error.body, { + unrecognizedObjectKeys: "passthrough", + allowUnrecognizedUnionMembers: true, + allowUnrecognizedEnumValues: true, + breadcrumbsPrefix: ["response"], + }) + ); + case 500: + throw new Fileforge.InternalServerError(_response.error.body); + case 502: + throw new Fileforge.BadGatewayError( + await serializers.ErrorSchema.parseOrThrow(_response.error.body, { + unrecognizedObjectKeys: "passthrough", + allowUnrecognizedUnionMembers: true, + allowUnrecognizedEnumValues: true, + breadcrumbsPrefix: ["response"], + }) + ); + default: + throw new errors.FileforgeError({ + statusCode: _response.error.statusCode, + body: _response.error.body, + }); + } + } + + switch (_response.error.reason) { + case "non-json": + throw new errors.FileforgeError({ + statusCode: _response.error.statusCode, + body: _response.error.rawBody, + }); + case "timeout": + throw new errors.FileforgeTimeoutError(); + case "unknown": + throw new errors.FileforgeError({ + message: _response.error.errorMessage, + }); + } + } + + /** + * @throws {@link Fileforge.BadRequestError} + * @throws {@link Fileforge.UnauthorizedError} + * @throws {@link Fileforge.InternalServerError} + */ + public async merge( + files: File[] | fs.ReadStream[], + request: Fileforge.MergeRequest, + requestOptions?: FileforgeClient.RequestOptions + ): Promise { + const _request = new core.FormDataWrapper(); + await _request.append("options", JSON.stringify(request.options)); + for (const _file of files) { + await _request.append("files", _file); + } + + const _maybeEncodedRequest = _request.getRequest(); + const _response = await core.fetcher({ + url: urlJoin( + (await core.Supplier.get(this._options.environment)) ?? environments.FileforgeEnvironment.Default, + "pdf/merge/" + ), + method: "POST", + headers: { + "X-Fern-Language": "JavaScript", + "X-Fern-SDK-Name": "@fileforge/client", + "X-Fern-SDK-Version": "0.1.0", "X-Fern-Runtime": core.RUNTIME.type, "X-Fern-Runtime-Version": core.RUNTIME.version, - ...(await _request.getHeaders()), + ...(await this._getCustomAuthorizationHeaders()), + ...(await _maybeEncodedRequest.getHeaders()), }, - body: await _request.getBody(), + body: await _maybeEncodedRequest.getBody(), responseType: "streaming", timeoutMs: requestOptions?.timeoutInSeconds != null ? requestOptions.timeoutInSeconds * 1000 : 60000, maxRetries: requestOptions?.maxRetries, + abortSignal: requestOptions?.abortSignal, }); if (_response.ok) { return _response.body; } if (_response.error.reason === "status-code") { - throw new errors.FileForgeError({ - statusCode: _response.error.statusCode, - body: _response.error.body, - }); + switch (_response.error.statusCode) { + case 400: + throw new Fileforge.BadRequestError( + await serializers.ErrorSchema.parseOrThrow(_response.error.body, { + unrecognizedObjectKeys: "passthrough", + allowUnrecognizedUnionMembers: true, + allowUnrecognizedEnumValues: true, + breadcrumbsPrefix: ["response"], + }) + ); + case 401: + throw new Fileforge.UnauthorizedError( + await serializers.ErrorSchema.parseOrThrow(_response.error.body, { + unrecognizedObjectKeys: "passthrough", + allowUnrecognizedUnionMembers: true, + allowUnrecognizedEnumValues: true, + breadcrumbsPrefix: ["response"], + }) + ); + case 500: + throw new Fileforge.InternalServerError(_response.error.body); + default: + throw new errors.FileforgeError({ + statusCode: _response.error.statusCode, + body: _response.error.body, + }); + } } switch (_response.error.reason) { case "non-json": - throw new errors.FileForgeError({ + throw new errors.FileforgeError({ statusCode: _response.error.statusCode, body: _response.error.rawBody, }); case "timeout": - throw new errors.FileForgeTimeoutError(); + throw new errors.FileforgeTimeoutError(); case "unknown": - throw new errors.FileForgeError({ + throw new errors.FileforgeError({ message: _response.error.errorMessage, }); } } + + protected async _getCustomAuthorizationHeaders() { + const apiKeyValue = await core.Supplier.get(this._options.apiKey); + return { "X-API-Key": apiKeyValue }; + } } diff --git a/src/core/form-data-utils/FormDataWrapper.ts b/src/core/form-data-utils/FormDataWrapper.ts new file mode 100644 index 0000000..4974501 --- /dev/null +++ b/src/core/form-data-utils/FormDataWrapper.ts @@ -0,0 +1,73 @@ +import { RUNTIME } from "../runtime"; + +interface CrossPlatformFormData { + append(key: string, value: any): void; +} + +class FormDataRequestBody { + private fd: any; + private encoder: any; + + constructor(fd: any) { + this.fd = fd; + } + + async setup(): Promise { + if (this.encoder == null && RUNTIME.type === "node") { + this.encoder = new (await import("form-data-encoder")).FormDataEncoder(this.fd); + } + } + + /** + * @returns the multipart form data request + */ + public async getBody(): Promise { + if (RUNTIME.type !== "node") { + return this.fd; + } else { + if (this.encoder == null) { + await this.setup(); + } + return (await import("node:stream")).Readable.from(this.encoder); + } + } + + /** + * @returns headers that need to be added to the multipart form data request + */ + public async getHeaders(): Promise> { + if (RUNTIME.type !== "node") { + return {}; + } else { + if (this.encoder == null) { + await this.setup(); + } + return { + "Content-Length": this.encoder.length, + }; + } + } +} + +/** + * FormDataWrapper is a utility to make form data + * requests across both Browser and Node.js runtimes. + */ +export class FormDataWrapper { + private fd: CrossPlatformFormData | undefined; + + public async append(name: string, value: any): Promise { + if (this.fd == null) { + if (RUNTIME.type === "node") { + this.fd = new (await import("formdata-node")).FormData(); + } else { + this.fd = new (await import("form-data")).default(); + } + } + this.fd.append(name, value); + } + + public getRequest(): FormDataRequestBody { + return new FormDataRequestBody(this.fd); + } +} diff --git a/src/core/form-data-utils/index.ts b/src/core/form-data-utils/index.ts index 7ecf250..f210ac4 100644 --- a/src/core/form-data-utils/index.ts +++ b/src/core/form-data-utils/index.ts @@ -1 +1 @@ -export { newFormData } from "./FormData"; +export * from "./FormDataWrapper"; diff --git a/src/core/index.ts b/src/core/index.ts index ed49c5c..d405074 100644 --- a/src/core/index.ts +++ b/src/core/index.ts @@ -1,5 +1,4 @@ export * from "./fetcher"; -export * from "./runtime"; -export * from "./auth"; export * from "./form-data-utils"; +export * from "./runtime"; export * as serialization from "./schemas"; diff --git a/yarn.lock b/yarn.lock index 47b111e..796b06a 100644 --- a/yarn.lock +++ b/yarn.lock @@ -637,6 +637,11 @@ "@types/tough-cookie" "*" parse5 "^7.0.0" +"@types/mime-types@^2.1.4": + version "2.1.4" + resolved "https://registry.yarnpkg.com/@types/mime-types/-/mime-types-2.1.4.tgz#93a1933e24fed4fb9e4adc5963a63efcbb3317a2" + integrity sha512-lfU4b34HOri+kAY5UheuFMWPDOI+OPceBSHZKp69gEyTL/mmJ4cnU6Y/rlme3UL3GyOn6Y42hyIEw0/q8sWx5w== + "@types/node-fetch@2.6.9": version "2.6.9" resolved "https://registry.yarnpkg.com/@types/node-fetch/-/node-fetch-2.6.9.tgz#15f529d247f1ede1824f7e7acdaa192d5f28071e" @@ -1903,11 +1908,6 @@ jest@29.7.0: import-local "^3.0.2" jest-cli "^29.7.0" -js-base64@3.7.2: - version "3.7.2" - resolved "https://registry.yarnpkg.com/js-base64/-/js-base64-3.7.2.tgz#816d11d81a8aff241603d19ce5761e13e41d7745" - integrity sha512-NnRs6dsyqUXejqk/yv2aiXlAvOs56sLkX6nUdeaNezI5LFFLlsZjOThmwnrcwh5ZZRwZlCMnVAY3CvhIhoVEKQ== - js-tokens@^4.0.0: version "4.0.0" resolved "https://registry.yarnpkg.com/js-tokens/-/js-tokens-4.0.0.tgz#19203fb59991df98e3a287050d4647cdeaf32499" @@ -2039,7 +2039,7 @@ mime-db@1.52.0: resolved "https://registry.yarnpkg.com/mime-db/-/mime-db-1.52.0.tgz#bbabcdc02859f4987301c856e3387ce5ec43bf70" integrity sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg== -mime-types@^2.1.12: +mime-types@^2.1.12, mime-types@^2.1.35: version "2.1.35" resolved "https://registry.yarnpkg.com/mime-types/-/mime-types-2.1.35.tgz#381a871b62a734450660ae3deee44813f70d959a" integrity sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==