Skip to content

Commit 45d1278

Browse files
authored
feat(cubejs-client): Add lastRefreshTime to CubeSqlResult and CubeSqlStreamChunk types; enhance CubeApi to parse and return lastRefreshTime from responses (cube-js#10425)
1 parent 1756cdb commit 45d1278

File tree

2 files changed

+82
-3
lines changed

2 files changed

+82
-3
lines changed

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

Lines changed: 9 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -128,11 +128,13 @@ export type CubeSqlSchemaColumn = {
128128
export type CubeSqlResult = {
129129
schema: CubeSqlSchemaColumn[];
130130
data: (string | number | boolean | null)[][];
131+
lastRefreshTime?: string;
131132
};
132133

133134
export type CubeSqlStreamChunk = {
134135
type: 'schema';
135136
schema: CubeSqlSchemaColumn[];
137+
lastRefreshTime?: string;
136138
} | {
137139
type: 'data';
138140
data: (string | number | boolean | null)[];
@@ -754,12 +756,14 @@ class CubeApi {
754756
const [schema, ...data] = response.error.split('\n');
755757

756758
try {
759+
const parsedSchema = JSON.parse(schema);
757760
return {
758-
schema: JSON.parse(schema).schema,
761+
schema: parsedSchema.schema,
759762
data: data
760763
.filter((d: string) => d.trim().length)
761764
.map((d: string) => JSON.parse(d).data)
762765
.reduce((a: any, b: any) => a.concat(b), []),
766+
...(parsedSchema.lastRefreshTime ? { lastRefreshTime: parsedSchema.lastRefreshTime } : {}),
763767
};
764768
} catch (err) {
765769
throw new Error(response.error);
@@ -810,7 +814,8 @@ class CubeApi {
810814
if (parsed.schema) {
811815
yield {
812816
type: 'schema' as const,
813-
schema: parsed.schema
817+
schema: parsed.schema,
818+
...(parsed.lastRefreshTime ? { lastRefreshTime: parsed.lastRefreshTime } : {}),
814819
};
815820
} else if (parsed.data) {
816821
yield {
@@ -840,7 +845,8 @@ class CubeApi {
840845
if (parsed.schema) {
841846
yield {
842847
type: 'schema' as const,
843-
schema: parsed.schema
848+
schema: parsed.schema,
849+
...(parsed.lastRefreshTime ? { lastRefreshTime: parsed.lastRefreshTime } : {}),
844850
};
845851
} else if (parsed.data) {
846852
yield {

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

Lines changed: 73 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -359,6 +359,79 @@ describe('CubeApi with Abort Signal', () => {
359359
});
360360
});
361361

362+
describe('CubeApi cubeSql', () => {
363+
afterEach(() => {
364+
jest.clearAllMocks();
365+
jest.restoreAllMocks();
366+
});
367+
368+
const cubeSqlResponseBody = [
369+
JSON.stringify({
370+
schema: [
371+
{ name: 'status', column_type: 'String' },
372+
{ name: 'measure(orders_transactions.count)', column_type: 'Int64' }
373+
],
374+
lastRefreshTime: '2026-02-24T00:34:01.594Z'
375+
}),
376+
JSON.stringify({ data: [['Cancelled', '27090'], ['Returned', '18232']] }),
377+
JSON.stringify({ data: [['Shipped', '45102']] }),
378+
].join('\n');
379+
380+
const cubeSqlResponseBodyNoRefreshTime = [
381+
JSON.stringify({
382+
schema: [
383+
{ name: 'status', column_type: 'String' },
384+
],
385+
}),
386+
JSON.stringify({ data: [['Active']] }),
387+
].join('\n');
388+
389+
test('should parse lastRefreshTime from response', async () => {
390+
jest.spyOn(HttpTransport.prototype, 'request').mockImplementation(() => ({
391+
subscribe: (cb) => Promise.resolve(cb({
392+
status: 200,
393+
text: () => Promise.resolve(JSON.stringify({ error: cubeSqlResponseBody })),
394+
} as any,
395+
async () => undefined as any))
396+
}));
397+
398+
const cubeApi = new CubeApi('token', {
399+
apiUrl: 'http://localhost:4000/cubejs-api/v1',
400+
});
401+
402+
const res = await cubeApi.cubeSql('SELECT status, measure(count) FROM orders_transactions');
403+
expect(res.lastRefreshTime).toBe('2026-02-24T00:34:01.594Z');
404+
expect(res.schema).toEqual([
405+
{ name: 'status', column_type: 'String' },
406+
{ name: 'measure(orders_transactions.count)', column_type: 'Int64' }
407+
]);
408+
expect(res.data).toEqual([
409+
['Cancelled', '27090'],
410+
['Returned', '18232'],
411+
['Shipped', '45102'],
412+
]);
413+
});
414+
415+
test('should omit lastRefreshTime when not present in response', async () => {
416+
jest.spyOn(HttpTransport.prototype, 'request').mockImplementation(() => ({
417+
subscribe: (cb) => Promise.resolve(cb({
418+
status: 200,
419+
text: () => Promise.resolve(JSON.stringify({ error: cubeSqlResponseBodyNoRefreshTime })),
420+
} as any,
421+
async () => undefined as any))
422+
}));
423+
424+
const cubeApi = new CubeApi('token', {
425+
apiUrl: 'http://localhost:4000/cubejs-api/v1',
426+
});
427+
428+
const res = await cubeApi.cubeSql('SELECT status FROM users');
429+
expect(res.lastRefreshTime).toBeUndefined();
430+
expect(res.schema).toEqual([{ name: 'status', column_type: 'String' }]);
431+
expect(res.data).toEqual([['Active']]);
432+
});
433+
});
434+
362435
describe('CubeApi with baseRequestId', () => {
363436
afterEach(() => {
364437
jest.clearAllMocks();

0 commit comments

Comments
 (0)