Skip to content

Commit

Permalink
Merge pull request #376 from apivideo/feature/nodejs_rate_limit
Browse files Browse the repository at this point in the history
Add *WithResponseHeaders() methods
  • Loading branch information
olivierapivideo authored Apr 24, 2024
2 parents 9589008 + 8cbc066 commit 12211ad
Show file tree
Hide file tree
Showing 6 changed files with 136 additions and 23 deletions.
2 changes: 2 additions & 0 deletions config/nodejs.yaml
Original file line number Diff line number Diff line change
@@ -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):
Expand Down
19 changes: 19 additions & 0 deletions templates/nodejs/README.md.mustache
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down Expand Up @@ -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
Expand Down
16 changes: 16 additions & 0 deletions templates/nodejs/src/HttpClient.ts.mustache
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down
111 changes: 88 additions & 23 deletions templates/nodejs/src/api/api.ts.mustache
Original file line number Diff line number Diff line change
Expand Up @@ -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}}';
Expand Down Expand Up @@ -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 = {};

Expand Down Expand Up @@ -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}}
* {{&notes}}
Expand All @@ -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}}
/**
Expand All @@ -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}}
* {{&notes}}
{{/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}}
* {{&notes}}
{{/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 = {};
Expand Down Expand Up @@ -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;

Expand Down Expand Up @@ -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}}
Expand All @@ -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}}
Expand Down
4 changes: 4 additions & 0 deletions templates/nodejs/src/model/ProgressiveSession.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,8 @@
import { ApiResponseHeaders } from "../HttpClient";

export default interface ProgressiveSession<T> {
uploadPart(file: string): Promise<T>;
uploadPartWithResponseHeaders(file: string): Promise<{headers: ApiResponseHeaders, body: T}>;
uploadLastPart(file: string): Promise<T>;
uploadLastPartWithResponseHeaders(file: string): Promise<{headers: ApiResponseHeaders, body: T}>;
}
7 changes: 7 additions & 0 deletions templates/nodejs/test/sandbox.spec.ts.mustache
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down

0 comments on commit 12211ad

Please sign in to comment.