Skip to content

Commit 2d6e923

Browse files
authored
feat(client-core): Add baseRequestId to the client public methods (#10212)
* feat(client-core): Add baseRequestId to the client public methods * feat(client-react): add baseRequestId to public methods * add tests
1 parent 52fe97f commit 2d6e923

File tree

3 files changed

+242
-8
lines changed

3 files changed

+242
-8
lines changed

packages/cubejs-client-core/src/index.ts

Lines changed: 19 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -50,6 +50,11 @@ export type LoadMethodOptions = {
5050
* AbortSignal to cancel requests
5151
*/
5252
signal?: AbortSignal;
53+
54+
/**
55+
* Client provided request ID, if client wants to track request onb their own
56+
*/
57+
baseRequestId?: string;
5358
};
5459

5560
export type DeeplyReadonly<T> = {
@@ -277,8 +282,8 @@ class CubeApi {
277282

278283
protected request(method: string, params?: any) {
279284
return this.transport.request(method, {
280-
baseRequestId: uuidv4(),
281-
...params
285+
...params,
286+
baseRequestId: params?.baseRequestId || uuidv4(),
282287
});
283288
}
284289

@@ -574,7 +579,8 @@ class CubeApi {
574579
() => this.request('load', {
575580
query,
576581
queryType: 'multi',
577-
signal: options?.signal
582+
signal: options?.signal,
583+
baseRequestId: options?.baseRequestId,
578584
}),
579585
(response: any) => this.loadResponseInternal(response, options),
580586
options,
@@ -640,7 +646,8 @@ class CubeApi {
640646
() => this.request('subscribe', {
641647
query,
642648
queryType: 'multi',
643-
signal: options?.signal
649+
signal: options?.signal,
650+
baseRequestId: options?.baseRequestId,
644651
}),
645652
(response: any) => this.loadResponseInternal(response, options),
646653
{ ...options, subscribe: true },
@@ -659,7 +666,8 @@ class CubeApi {
659666
return this.loadMethod(
660667
() => this.request('sql', {
661668
query,
662-
signal: options?.signal
669+
signal: options?.signal,
670+
baseRequestId: options?.baseRequestId,
663671
}),
664672
(response: any) => (Array.isArray(response) ? response.map((body) => new SqlQuery(body)) : new SqlQuery(response)),
665673
options,
@@ -677,7 +685,8 @@ class CubeApi {
677685
public meta(options?: LoadMethodOptions, callback?: LoadMethodCallback<Meta>): Promise<Meta> | UnsubscribeObj {
678686
return this.loadMethod(
679687
() => this.request('meta', {
680-
signal: options?.signal
688+
signal: options?.signal,
689+
baseRequestId: options?.baseRequestId,
681690
}),
682691
(body: MetaResponse) => new Meta(body),
683692
options,
@@ -696,7 +705,8 @@ class CubeApi {
696705
return this.loadMethod(
697706
() => this.request('dry-run', {
698707
query,
699-
signal: options?.signal
708+
signal: options?.signal,
709+
baseRequestId: options?.baseRequestId,
700710
}),
701711
(response: DryRunResponse) => response,
702712
options,
@@ -719,7 +729,8 @@ class CubeApi {
719729
cache: options?.cache,
720730
method: 'POST',
721731
signal: options?.signal,
722-
fetchTimeout: options?.timeout
732+
fetchTimeout: options?.timeout,
733+
baseRequestId: options?.baseRequestId,
723734
});
724735

725736
return request;

packages/cubejs-client-core/test/CubeApi.test.ts

Lines changed: 222 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -358,3 +358,225 @@ describe('CubeApi with Abort Signal', () => {
358358
expect(requestSpy.mock.calls[0]?.[1]?.signal).toBe(signal);
359359
});
360360
});
361+
362+
describe('CubeApi with baseRequestId', () => {
363+
afterEach(() => {
364+
jest.clearAllMocks();
365+
jest.restoreAllMocks();
366+
});
367+
368+
test('should pass baseRequestId from options to request', async () => {
369+
const baseRequestId = 'custom-request-id-123';
370+
371+
const requestSpy = jest.spyOn(HttpTransport.prototype, 'request').mockImplementation(() => ({
372+
subscribe: (cb) => Promise.resolve(cb({
373+
status: 200,
374+
text: () => Promise.resolve('{"results":[]}'),
375+
json: () => Promise.resolve({ results: [] })
376+
} as any,
377+
async () => undefined as any))
378+
}));
379+
380+
const cubeApi = new CubeApi('token', {
381+
apiUrl: 'http://localhost:4000/cubejs-api/v1'
382+
});
383+
384+
await cubeApi.load(
385+
{ measures: ['Orders.count'] },
386+
{ baseRequestId }
387+
);
388+
389+
expect(requestSpy).toHaveBeenCalled();
390+
expect(requestSpy.mock.calls[0]?.[1]?.baseRequestId).toBe(baseRequestId);
391+
});
392+
393+
test('should generate baseRequestId if not provided', async () => {
394+
const requestSpy = jest.spyOn(HttpTransport.prototype, 'request').mockImplementation(() => ({
395+
subscribe: (cb) => Promise.resolve(cb({
396+
status: 200,
397+
text: () => Promise.resolve('{"results":[]}'),
398+
json: () => Promise.resolve({ results: [] })
399+
} as any,
400+
async () => undefined as any))
401+
}));
402+
403+
const cubeApi = new CubeApi('token', {
404+
apiUrl: 'http://localhost:4000/cubejs-api/v1'
405+
});
406+
407+
await cubeApi.load(
408+
{ measures: ['Orders.count'] }
409+
);
410+
411+
expect(requestSpy).toHaveBeenCalled();
412+
// Should have a baseRequestId (generated via uuidv4)
413+
expect(requestSpy.mock.calls[0]?.[1]?.baseRequestId).toBeDefined();
414+
expect(typeof requestSpy.mock.calls[0]?.[1]?.baseRequestId).toBe('string');
415+
});
416+
417+
test('should pass baseRequestId to sql request', async () => {
418+
const baseRequestId = 'sql-request-id-456';
419+
420+
const requestSpy = jest.spyOn(HttpTransport.prototype, 'request').mockImplementation(() => ({
421+
subscribe: (cb) => Promise.resolve(cb({
422+
status: 200,
423+
text: () => Promise.resolve('{"sql":{"sql":"SELECT * FROM orders"}}'),
424+
json: () => Promise.resolve({ sql: { sql: 'SELECT * FROM orders' } })
425+
} as any,
426+
async () => undefined as any))
427+
}));
428+
429+
const cubeApi = new CubeApi('token', {
430+
apiUrl: 'http://localhost:4000/cubejs-api/v1'
431+
});
432+
433+
await cubeApi.sql(
434+
{ measures: ['Orders.count'] },
435+
{ baseRequestId }
436+
);
437+
438+
expect(requestSpy).toHaveBeenCalled();
439+
expect(requestSpy.mock.calls[0]?.[1]?.baseRequestId).toBe(baseRequestId);
440+
});
441+
442+
test('should pass baseRequestId to dryRun request', async () => {
443+
const baseRequestId = 'dryrun-request-id-789';
444+
445+
const requestSpy = jest.spyOn(HttpTransport.prototype, 'request').mockImplementation(() => ({
446+
subscribe: (cb) => Promise.resolve(cb({
447+
status: 200,
448+
text: () => Promise.resolve('{"queryType":"regular"}'),
449+
json: () => Promise.resolve({ queryType: 'regular' })
450+
} as any,
451+
async () => undefined as any))
452+
}));
453+
454+
const cubeApi = new CubeApi('token', {
455+
apiUrl: 'http://localhost:4000/cubejs-api/v1'
456+
});
457+
458+
await cubeApi.dryRun(
459+
{ measures: ['Orders.count'] },
460+
{ baseRequestId }
461+
);
462+
463+
expect(requestSpy).toHaveBeenCalled();
464+
expect(requestSpy.mock.calls[0]?.[1]?.baseRequestId).toBe(baseRequestId);
465+
});
466+
467+
test('should pass baseRequestId to subscribe request', async () => {
468+
const baseRequestId = 'subscribe-request-id-abc';
469+
470+
const requestSpy = jest.spyOn(HttpTransport.prototype, 'request').mockImplementation(() => ({
471+
subscribe: (cb) => Promise.resolve(cb({
472+
status: 200,
473+
text: () => Promise.resolve('{"results":[]}'),
474+
json: () => Promise.resolve({ results: [] })
475+
} as any,
476+
async () => undefined as any))
477+
}));
478+
479+
const cubeApi = new CubeApi('token', {
480+
apiUrl: 'http://localhost:4000/cubejs-api/v1'
481+
});
482+
483+
const subscription = cubeApi.subscribe(
484+
{ measures: ['Orders.count'] },
485+
{ baseRequestId },
486+
// eslint-disable-next-line @typescript-eslint/no-empty-function
487+
() => {}
488+
);
489+
490+
// Wait for the subscription to be initiated
491+
await new Promise(resolve => setTimeout(resolve, 0));
492+
493+
expect(requestSpy).toHaveBeenCalled();
494+
expect(requestSpy.mock.calls[0]?.[1]?.baseRequestId).toBe(baseRequestId);
495+
496+
subscription.unsubscribe();
497+
});
498+
499+
test('should pass baseRequestId with multiple queries', async () => {
500+
const baseRequestId = 'multi-query-request-id';
501+
502+
const requestSpy = jest.spyOn(HttpTransport.prototype, 'request').mockImplementation(() => ({
503+
subscribe: (cb) => Promise.resolve(cb({
504+
status: 200,
505+
text: () => Promise.resolve(JSON.stringify(DescriptiveQueryResponse)),
506+
json: () => Promise.resolve(DescriptiveQueryResponse)
507+
} as any,
508+
async () => undefined as any))
509+
}));
510+
511+
const cubeApi = new CubeApi('token', {
512+
apiUrl: 'http://localhost:4000/cubejs-api/v1'
513+
});
514+
515+
await cubeApi.load(
516+
[
517+
{ measures: ['Orders.count'] },
518+
{ measures: ['Users.count'] }
519+
],
520+
{ baseRequestId }
521+
);
522+
523+
expect(requestSpy).toHaveBeenCalled();
524+
expect(requestSpy.mock.calls[0]?.[1]?.baseRequestId).toBe(baseRequestId);
525+
});
526+
527+
test('should pass baseRequestId to meta request', async () => {
528+
const baseRequestId = 'meta-request-id-def';
529+
530+
const requestSpy = jest.spyOn(HttpTransport.prototype, 'request').mockImplementation(() => ({
531+
subscribe: (cb) => Promise.resolve(cb({
532+
status: 200,
533+
text: () => Promise.resolve(JSON.stringify({
534+
cubes: [{
535+
name: 'Orders',
536+
title: 'Orders',
537+
measures: [{
538+
name: 'count',
539+
title: 'Count',
540+
shortTitle: 'Count',
541+
type: 'number'
542+
}],
543+
dimensions: [{
544+
name: 'status',
545+
title: 'Status',
546+
type: 'string'
547+
}],
548+
segments: []
549+
}]
550+
})),
551+
json: () => Promise.resolve({
552+
cubes: [{
553+
name: 'Orders',
554+
title: 'Orders',
555+
measures: [{
556+
name: 'count',
557+
title: 'Count',
558+
shortTitle: 'Count',
559+
type: 'number'
560+
}],
561+
dimensions: [{
562+
name: 'status',
563+
title: 'Status',
564+
type: 'string'
565+
}],
566+
segments: []
567+
}]
568+
})
569+
} as any,
570+
async () => undefined as any))
571+
}));
572+
573+
const cubeApi = new CubeApi('token', {
574+
apiUrl: 'http://localhost:4000/cubejs-api/v1'
575+
});
576+
577+
await cubeApi.meta({ baseRequestId });
578+
579+
expect(requestSpy).toHaveBeenCalled();
580+
expect(requestSpy.mock.calls[0]?.[1]?.baseRequestId).toBe(baseRequestId);
581+
});
582+
});

packages/cubejs-client-react/src/hooks/cube-fetch.js

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -33,6 +33,7 @@ export function useCubeFetch(method, options = {}) {
3333
const coreOptions = {
3434
mutexObj: mutexRef.current,
3535
mutexKey: method,
36+
...(options.baseRequestId ? { baseRequestId: options.baseRequestId } : {})
3637
};
3738
const args = method === 'meta' ? [coreOptions] : [query, coreOptions];
3839

0 commit comments

Comments
 (0)