Skip to content
Merged
Show file tree
Hide file tree
Changes from 8 commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
62 changes: 58 additions & 4 deletions packages/api/lib/api-client.ts
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ export interface ApiClientConfig {

export interface RequestOptions {
versionOverride?: string;
timeout?: number;
Comment thread
damian-rakus marked this conversation as resolved.
Outdated
}

/**
Expand Down Expand Up @@ -101,13 +102,28 @@ export class ApiClient {
* `QueryVariables` is a type alias for `Record<string, any>`, allowing specification
* of key-value pairs where the value can be any type. This parameter is used to provide
* dynamic values in the query or mutation.
* @param {RequestOptions} [options] - Optional request configuration including version override.
* @param {RequestOptions} [options] - Optional request configuration including version override and timeout.
* @returns {Promise<T>} A promise that resolves with the result of the query or mutation.
* @template T The expected type of the query or mutation result.
* @throws {Error} Throws an error if the request times out before receiving a response.
*/
public request = async <T>(query: string, variables?: QueryVariables, options?: RequestOptions): Promise<T> => {
const client = this.createClient(options);
return client.request<T>(query, variables);
const { timeout } = options || {};

if (!timeout) {
return client.request<T>(query, variables);
}

return this.requestWithTimeout<T>(
(signal) =>
client.request<T>({
document: query,
variables,
signal,
}),
timeout,
);
};

/**
Expand All @@ -122,19 +138,57 @@ export class ApiClient {
* `QueryVariables` is a type alias for `Record<string, any>`, allowing specification
* of key-value pairs where the value can be any type. This parameter is used to provide
* dynamic values in the query or mutation.
* @param {RequestOptions} [options] - Optional request configuration including version override.
* @param {RequestOptions} [options] - Optional request configuration including version override and timeout.
* @returns {Promise<T>} A promise that resolves with the result of the query or mutation.
* @template T The expected type of the query or mutation result.
* @throws {Error} Throws an error if the request times out before receiving a response.
*/
public rawRequest = async <T>(
query: string,
variables?: QueryVariables,
options?: RequestOptions,
): Promise<GraphQLClientResponse<T>> => {
const client = this.createClient(options);
return client.rawRequest<T>(query, variables);
const { timeout } = options || {};

if (!timeout) {
return client.rawRequest<T>(query, variables);
}

return this.requestWithTimeout<GraphQLClientResponse<T>>(
Comment thread
damian-rakus marked this conversation as resolved.
Outdated
(signal) =>
client.rawRequest<T>({
query,
variables,
signal,
}),
timeout,
);
};

/**
* Executes a request with timeout handling.
*
* @param {Function} requestExecutor - A function that performs the actual request, receiving an AbortSignal.
* @param {number} timeout - The timeout duration in milliseconds.
* @returns {Promise<T>} A promise that resolves with the result of the request.
* @template T The expected type of the request result.
* @throws {Error} Throws an error if the request times out before receiving a response.
*/
private async requestWithTimeout<T>(requestExecutor: (signal: AbortSignal) => Promise<T>, timeout: number): Promise<T> {
const controller = new AbortController();
const timeoutId = setTimeout(() => {
controller.abort();
}, timeout);

try {
const result = await requestExecutor(controller.signal);
return result;
} finally {
clearTimeout(timeoutId);
}
}

/**
* Validates the API version format (yyyy-mm), restricting mm to 01, 04, 07, or 10.
*
Expand Down
3 changes: 2 additions & 1 deletion packages/api/package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "@mondaydotcomorg/api",
"version": "12.0.1",
"version": "12.1.1",
"description": "monday.com API client",
"main": "dist/cjs/index.js",
"module": "dist/esm/index.js",
Expand Down Expand Up @@ -46,6 +46,7 @@
"@types/node": "^20.11.18",
"jest": "^29.7.0",
"moment": "^2.30.1",
"nock": "^13.5.0",
"rollup": "^2.79.1",
"rollup-plugin-delete": "^2.0.0",
"rollup-plugin-dts": "^4.2.3",
Expand Down
85 changes: 85 additions & 0 deletions packages/api/tests/api-client.integration.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,85 @@
import nock from 'nock';
import { ApiClient } from '../lib/api-client';
import { MONDAY_API_ENDPOINT } from '../lib/constants';

describe('ApiClient timeout integration', () => {
beforeAll(() => {
nock.disableNetConnect();
});

afterEach(() => {
nock.abortPendingRequests();
nock.cleanAll();
});

afterAll(() => {
nock.restore();
nock.enableNetConnect();
});
Comment thread
RomKadria marked this conversation as resolved.

describe('request method', () => {
it('should abort request when timeout is exceeded', async () => {
nock(MONDAY_API_ENDPOINT)
.post('')
.delay(2000)
.reply(200, { data: { users: [] } });

const apiClient = new ApiClient({ token: 'test-token' });
const query = '{ users { id } }';

await expect(apiClient.request(query, undefined, { timeout: 100 })).rejects.toThrow('The user aborted a request.');
});

it('should complete successfully when response arrives before timeout', async () => {
nock(MONDAY_API_ENDPOINT)
.post('')
.delay(50)
.reply(200, { data: { users: [{ id: '1' }] } });

const apiClient = new ApiClient({ token: 'test-token' });
const query = '{ users { id } }';

const result = await apiClient.request(query, undefined, { timeout: 500 });
expect(result).toEqual({ users: [{ id: '1' }] });
});

it('should work without timeout option', async () => {
nock(MONDAY_API_ENDPOINT)
.post('')
.reply(200, { data: { users: [{ id: '1', name: 'John' }] } });

const apiClient = new ApiClient({ token: 'test-token' });
const query = '{ users { id name } }';

const result = await apiClient.request(query);
expect(result).toEqual({ users: [{ id: '1', name: 'John' }] });
});
});

describe('rawRequest method', () => {
it('should abort rawRequest when timeout is exceeded', async () => {
nock(MONDAY_API_ENDPOINT)
.post('')
.delay(2000)
.reply(200, { data: { users: [] } });

const apiClient = new ApiClient({ token: 'test-token' });
const query = '{ users { id } }';

await expect(apiClient.rawRequest(query, undefined, { timeout: 100 })).rejects.toThrow('The user aborted a request.');
});

it('should complete rawRequest successfully when response arrives before timeout', async () => {
nock(MONDAY_API_ENDPOINT)
.post('')
.delay(50)
.reply(200, { data: { users: [{ id: '1' }] } });

const apiClient = new ApiClient({ token: 'test-token' });
const query = '{ users { id } }';

const result = await apiClient.rawRequest(query, undefined, { timeout: 500 });
expect(result.data).toEqual({ users: [{ id: '1' }] });
});
});
});
19 changes: 19 additions & 0 deletions yarn.lock
Original file line number Diff line number Diff line change
Expand Up @@ -2353,6 +2353,11 @@ json-stable-stringify-without-jsonify@^1.0.1:
resolved "https://registry.npmjs.org/json-stable-stringify-without-jsonify/-/json-stable-stringify-without-jsonify-1.0.1.tgz#9db7b59496ad3f3cfef30a75142d2d930ad72651"
integrity sha512-Bdboy+l7tA3OGW6FjyFHWkP5LuByj1Tk33Ljyq0axyzdk9//JSi2u3fP1QSmd1KNwq6VOKYGlAu87CisVir6Pw==

json-stringify-safe@^5.0.1:
version "5.0.1"
resolved "https://registry.yarnpkg.com/json-stringify-safe/-/json-stringify-safe-5.0.1.tgz#1296a2d58fd45f19a0f6ce01d65701e2c735b6eb"
integrity sha512-ZClg6AaYvamvYEE82d3Iyd3vSSIjQ+odgjaTzRuO3s7toCdFKczob2i0zCh7JE8kWn17yvAWhUVxvqGwUalsRA==

json5@^2.2.3:
version "2.2.3"
resolved "https://registry.npmjs.org/json5/-/json5-2.2.3.tgz#78cd6f1a19bdc12b73db5ad0c61efd66c1e29283"
Expand Down Expand Up @@ -2518,6 +2523,15 @@ natural-compare@^1.4.0:
resolved "https://registry.npmjs.org/natural-compare/-/natural-compare-1.4.0.tgz#4abebfeed7541f2c27acfb29bdbbd15c8d5ba4f7"
integrity sha512-OWND8ei3VtNC9h7V60qff3SVobHr996CTwgxubgyQYEpg290h9J0buyECNNJexkFm5sOajh5G116RYA1c8ZMSw==

nock@^13.5.0:
version "13.5.6"
resolved "https://registry.yarnpkg.com/nock/-/nock-13.5.6.tgz#5e693ec2300bbf603b61dae6df0225673e6c4997"
integrity sha512-o2zOYiCpzRqSzPj0Zt/dQ/DqZeYoaQ7TUonc/xUPjCGl9WeHpNbxgVvOquXYAaJzI0M9BXV3HTzG0p8IUAbBTQ==
dependencies:
debug "^4.1.0"
json-stringify-safe "^5.0.1"
propagate "^2.0.0"

node-fetch@^2.7.0:
version "2.7.0"
resolved "https://registry.npmjs.org/node-fetch/-/node-fetch-2.7.0.tgz#d0f0fa6e3e2dc1d27efcd8ad99d550bda94d187d"
Expand Down Expand Up @@ -2714,6 +2728,11 @@ prompts@^2.0.1:
kleur "^3.0.3"
sisteransi "^1.0.5"

propagate@^2.0.0:
version "2.0.1"
resolved "https://registry.yarnpkg.com/propagate/-/propagate-2.0.1.tgz#40cdedab18085c792334e64f0ac17256d38f9a45"
integrity sha512-vGrhOavPSTz4QVNuBNdcNXePNdNMaO1xj9yBeH1ScQPjk/rhg9sSlCXPhMkFuaNNW/syTvYqsnbIJxMBfRbbag==

punycode@^2.1.0:
version "2.3.1"
resolved "https://registry.npmjs.org/punycode/-/punycode-2.3.1.tgz#027422e2faec0b25e1549c3e1bd8309b9133b6e5"
Expand Down