diff --git a/config/nodejs.yaml b/config/nodejs.yaml index 12af87d2..c906ea16 100644 --- a/config/nodejs.yaml +++ b/config/nodejs.yaml @@ -1,4 +1,6 @@ changelog: + - 2.5.7 (2024-04-23): + - Add *WithResponseHeaders() methods - 2.5.6 (2024-02-19): - Update VideoStatusIngest enum - 2.5.5 (2023-12-19): diff --git a/templates/nodejs/README.md.mustache b/templates/nodejs/README.md.mustache index 9a359d67..897ba90b 100644 --- a/templates/nodejs/README.md.mustache +++ b/templates/nodejs/README.md.mustache @@ -16,6 +16,7 @@ - [Documentation](#documentation) - [API Endpoints](#api-endpoints){{#apiInfo}}{{#apis}}{{^x-client-hidden}} - [{{classname}}](#{{#lower}}{{classname}}{{/lower}}){{/x-client-hidden}} {{/apis}}{{/apiInfo}} - [Models](#models) + - [Rate Limiting](#rate-limiting) - [Authorization](#authorization) - [API key](#api-key) - [Get the access token](#get-the-access-token) @@ -110,6 +111,24 @@ Method | Description | HTTP request {{#models}}{{#model}} - [{{classname}}](https://github.com/apivideo/{{gitRepoId}}/blob/main/{{modelDocFileFolder}}/{{classname}}.md) {{/model}}{{/models}} +### Rate Limiting + +api.video implements rate limiting to ensure fair usage and stability of the service. The API provides the rate limit values in the response headers for any API requests you make. The /auth endpoint is the only route without rate limitation. + +In this Node.js client, you can access these headers by using the `*WithResponseHeaders()` versions of the methods. These methods return both the response body and the headers, allowing you to check the `X-RateLimit-Limit`, `X-RateLimit-Remaining`, and `X-RateLimit-Retry-After` headers to understand your current rate limit status. + +Read more about these response headers in the [API reference](https://docs.api.video/reference#limitation). + +Here is an example of how to use these methods: + +```js +const client = new ApiVideoClient({ apiKey: "YOUR_API_KEY" }); +const { body: videos, headers } = await client.videos.listWithResponseHeaders(); +console.log('Rate Limit:', headers['x-ratelimit-limit']); +console.log('Rate Limit Remaining:', headers['x-ratelimit-remaining']); +console.log('Rate Limit Retry after:', headers['x-ratelimit-retry-after']); +``` + ### Authorization #### API key diff --git a/templates/nodejs/src/HttpClient.ts.mustache b/templates/nodejs/src/HttpClient.ts.mustache index 4291adb6..1a6171f8 100644 --- a/templates/nodejs/src/HttpClient.ts.mustache +++ b/templates/nodejs/src/HttpClient.ts.mustache @@ -24,6 +24,22 @@ export type QueryOptions = { onUploadProgress?: (progressEvent: AxiosProgressEvent) => void; }; + +export type ApiResponseHeaders = { + server: string + 'content-type': string + 'transfer-encoding': string + connection: string + 'cache-control': string + date: string + 'x-ratelimit-remaining': string + 'x-ratelimit-retry-after': string + 'x-ratelimit-limit': string + 'x-server': string + 'access-control-allow-origin': string + 'timing-allow-origin': string +} + export default class HttpClient { private apiKey?: string; private baseUri: string; diff --git a/templates/nodejs/src/api/api.ts.mustache b/templates/nodejs/src/api/api.ts.mustache index 9c06c88b..fe32802d 100644 --- a/templates/nodejs/src/api/api.ts.mustache +++ b/templates/nodejs/src/api/api.ts.mustache @@ -14,7 +14,7 @@ import { promisify } from 'util'; import { URLSearchParams } from 'url'; import FormData from 'form-data'; import ObjectSerializer from '../ObjectSerializer'; -import HttpClient, { QueryOptions } from '../HttpClient'; +import HttpClient, { QueryOptions, ApiResponseHeaders } from '../HttpClient'; import ProgressiveSession from '../model/ProgressiveSession'; {{#imports}} import {{classname}} from '../model/{{classname}}'; @@ -54,14 +54,22 @@ export default class {{classname}} { } uploadPart(file: string, progressListener?: (event: UploadProgressEvent) => void) { + return this.upload(file, false, progressListener).then((res) => res.body); + }; + + uploadPartWithResponseHeaders(file: string, progressListener?: (event: UploadProgressEvent) => void) { return this.upload(file, false, progressListener); }; uploadLastPart(file: string, progressListener?: (event: UploadProgressEvent) => void) { - return this.upload(file, true, progressListener); + return this.upload(file, true, progressListener).then((res) => res.body); }; - upload(file: string, isLast: boolean, progressListener?: (event: UploadProgressEvent) => void) { + uploadLastPartWithResponseHeaders(file: string, progressListener?: (event: UploadProgressEvent) => void) { + return this.upload(file, true, progressListener); + }; + + async upload(file: string, isLast: boolean, progressListener?: (event: UploadProgressEvent) => void) { const queryParams: QueryOptions = {}; queryParams.headers = {}; @@ -165,23 +173,30 @@ export default class {{classname}} { } } - const call = this.httpClient.call(localVarPath, queryParams); + const response = await this.httpClient.call(localVarPath, queryParams); this.currentPart++; - return call.then(response => ObjectSerializer.deserialize( + const responseBody = ObjectSerializer.deserialize( ObjectSerializer.parse(response.body, response.headers["content-type"]), "{{{returnType}}}", "{{returnFormat}}" - ) as Type){{#vendorExtensions.x-client-copy-from-response}}.then((res) => { - this.{{paramName}} = (res as any).{{paramName}}; - return res; - }){{/vendorExtensions.x-client-copy-from-response}}; + ) as Type; + + {{#vendorExtensions.x-client-copy-from-response}} + this.{{paramName}} = (responseBody as any).{{paramName}}; + {{/vendorExtensions.x-client-copy-from-response}} + + return { + body: responseBody, + headers: response.headers + } } } return new {{#titlecase}}{{nickname}}{{/titlecase}}ProgressiveSession<{{{returnType}}}>(this.httpClient); } {{/vendorExtensions.x-client-chunk-upload}} - {{#vendorExtensions.x-group-parameters}} + +{{#vendorExtensions.x-group-parameters}} /** {{#notes}} * {{¬es}} @@ -194,7 +209,9 @@ export default class {{classname}} { * @param { {{dataType}} } searchParams.{{paramName}} {{description}} {{/allParams}} */ - public async {{nickname}}({ {{#allParams}}{{paramName}}{{^-last}}, {{/-last}}{{/allParams}} }: { {{#allParams}}{{paramName}}{{^required}}?{{/required}}: {{{dataType}}}{{^-last}}, {{/-last}}{{/allParams}} }{{#vendorExtensions.x-optional-object}} = {}{{/vendorExtensions.x-optional-object}}): Promise<{{#returnType}}{{{returnType}}}{{/returnType}} {{^returnType}}void{{/returnType}}> { + public async {{nickname}}(args: { {{#allParams}}{{paramName}}{{^required}}?{{/required}}: {{{dataType}}}{{^-last}}, {{/-last}}{{/allParams}} }{{#vendorExtensions.x-optional-object}} = {}{{/vendorExtensions.x-optional-object}}): Promise<{{#returnType}}{{{returnType}}}{{/returnType}} {{^returnType}}void{{/returnType}}> { + return this.{{nickname}}WithResponseHeaders(args).then((res) => res.body); + } {{/vendorExtensions.x-group-parameters}} {{^vendorExtensions.x-group-parameters}} /** @@ -209,6 +226,39 @@ export default class {{classname}} { {{/allParams}} */ public async {{nickname}}({{#allParams}}{{paramName}}{{^required}}?{{/required}}: {{#isFile}}{{#vendorExtensions.x-client-chunk-upload}}string{{/vendorExtensions.x-client-chunk-upload}}{{^vendorExtensions.x-client-chunk-upload}}string | Readable | Buffer{{/vendorExtensions.x-client-chunk-upload}}{{/isFile}}{{^isFile}}{{{dataType}}}{{/isFile}}{{#vendorExtensions.x-optional-object}} = {}{{/vendorExtensions.x-optional-object}}{{^-last}}, {{/-last}}{{/allParams}}{{#vendorExtensions.x-client-chunk-upload}}, progressListener?: (event: UploadProgressEvent) => void{{/vendorExtensions.x-client-chunk-upload}}): Promise<{{#returnType}}{{{returnType}}}{{/returnType}} {{^returnType}}void{{/returnType}}> { + return this.{{nickname}}WithResponseHeaders({{#allParams}}{{paramName}}{{^-last}}, {{/-last}}{{/allParams}}{{#vendorExtensions.x-client-chunk-upload}}, progressListener{{/vendorExtensions.x-client-chunk-upload}}).then((res) => res.body);; + } + {{/vendorExtensions.x-group-parameters}} + + + {{#vendorExtensions.x-group-parameters}} + /** + {{#notes}} + * {{¬es}} + {{/notes}} + {{#summary}} + * {{&summary}} + {{/summary}} + * @param {Object} searchParams + {{#allParams}} + * @param { {{dataType}} } searchParams.{{paramName}} {{description}} + {{/allParams}} + */ + public async {{nickname}}WithResponseHeaders({ {{#allParams}}{{paramName}}{{^-last}}, {{/-last}}{{/allParams}} }: { {{#allParams}}{{paramName}}{{^required}}?{{/required}}: {{{dataType}}}{{^-last}}, {{/-last}}{{/allParams}} }{{#vendorExtensions.x-optional-object}} = {}{{/vendorExtensions.x-optional-object}}): Promise<{{#returnType}} {headers: ApiResponseHeaders, body:{{{returnType}}} } {{/returnType}} {{^returnType}}void{{/returnType}}> { + {{/vendorExtensions.x-group-parameters}} + {{^vendorExtensions.x-group-parameters}} + /** + {{#notes}} + * {{¬es}} + {{/notes}} + {{#summary}} + * {{&summary}} + {{/summary}} + {{#allParams}} + * @param {{paramName}} {{description}} + {{/allParams}} + */ + public async {{nickname}}WithResponseHeaders({{#allParams}}{{paramName}}{{^required}}?{{/required}}: {{#isFile}}{{#vendorExtensions.x-client-chunk-upload}}string{{/vendorExtensions.x-client-chunk-upload}}{{^vendorExtensions.x-client-chunk-upload}}string | Readable | Buffer{{/vendorExtensions.x-client-chunk-upload}}{{/isFile}}{{^isFile}}{{{dataType}}}{{/isFile}}{{#vendorExtensions.x-optional-object}} = {}{{/vendorExtensions.x-optional-object}}{{^-last}}, {{/-last}}{{/allParams}}{{#vendorExtensions.x-client-chunk-upload}}, progressListener?: (event: UploadProgressEvent) => void{{/vendorExtensions.x-client-chunk-upload}}): Promise<{{#returnType}} {headers: ApiResponseHeaders, body:{{{returnType}}} } {{/returnType}} {{^returnType}}void{{/returnType}}> { {{/vendorExtensions.x-group-parameters}} const queryParams: QueryOptions = {}; queryParams.headers = {}; @@ -364,13 +414,18 @@ export default class {{classname}} { const call = this.httpClient.call(localVarPath, queryParams); - return call.then(response => ObjectSerializer.deserialize( - ObjectSerializer.parse(response.body, response.headers["content-type"]), - "{{{returnType}}}", "{{returnFormat}}" - ) as {{{returnType}}}); + return call.then(response => { + return { + headers: response.headers, + body: ObjectSerializer.deserialize( + ObjectSerializer.parse(response.body, response.headers["content-type"]), + "{{{returnType}}}", "{{returnFormat}}" + ) as {{{returnType}}} + } + }); } let uploadChunkSize = chunkSize; - let lastBody; + let lastResponse; let stream; let chunkNumber = 0; @@ -409,15 +464,20 @@ export default class {{classname}} { } const call = this.httpClient.call(localVarPath, queryParams); - lastBody = await call.then(response => ObjectSerializer.deserialize( - ObjectSerializer.parse(response.body, response.headers["content-type"]), - "{{{returnType}}}", "{{returnFormat}}" - ) as {{{returnType}}}); + lastResponse = await call.then(response => { + return { + headers: response.headers, + body: ObjectSerializer.deserialize( + ObjectSerializer.parse(response.body, response.headers["content-type"]), + "{{{returnType}}}", "{{returnFormat}}" + ) as {{{returnType}}} + } + }); stream.close(); } - return Promise.resolve(lastBody as {{{returnType}}}); + return Promise.resolve(lastResponse!); {{/vendorExtensions.x-client-chunk-upload}} {{^vendorExtensions.x-client-chunk-upload}} {{#platforms}} @@ -438,10 +498,15 @@ export default class {{classname}} { queryParams.body = formData; {{/hasFormParams}} return this.httpClient.call(localVarPath, queryParams) - .then(response => ObjectSerializer.deserialize( + .then(response => { + return { + headers: response.headers, + body: ObjectSerializer.deserialize( ObjectSerializer.parse(response.body, response.headers["content-type"]), "{{{returnType}}}", "{{returnFormat}}" - ) as {{{returnType}}}); + ) as {{{returnType}}} + } + }); {{/vendorExtensions.x-client-chunk-upload}} } {{/operation}} diff --git a/templates/nodejs/src/model/ProgressiveSession.ts b/templates/nodejs/src/model/ProgressiveSession.ts index d4bebc0d..0d4aaf9a 100644 --- a/templates/nodejs/src/model/ProgressiveSession.ts +++ b/templates/nodejs/src/model/ProgressiveSession.ts @@ -1,4 +1,8 @@ +import { ApiResponseHeaders } from "../HttpClient"; + export default interface ProgressiveSession { uploadPart(file: string): Promise; + uploadPartWithResponseHeaders(file: string): Promise<{headers: ApiResponseHeaders, body: T}>; uploadLastPart(file: string): Promise; + uploadLastPartWithResponseHeaders(file: string): Promise<{headers: ApiResponseHeaders, body: T}>; } \ No newline at end of file diff --git a/templates/nodejs/test/sandbox.spec.ts.mustache b/templates/nodejs/test/sandbox.spec.ts.mustache index 1f682ec1..9258da7d 100644 --- a/templates/nodejs/test/sandbox.spec.ts.mustache +++ b/templates/nodejs/test/sandbox.spec.ts.mustache @@ -54,6 +54,13 @@ describe('ApiVideoClient', () => { ); }); }); + + describe("Response headers", () => { + it("should return response headers", async () => { + const videos = await client.videos.listWithResponseHeaders(); + expect(videos.headers).toHaveProperty("content-type"); + }); + }); describe('Watermarks', () => { let watermark: Watermark, watermarkVideo: Video;