From 667f88530b071b9a7e92a83bd46caa6058230597 Mon Sep 17 00:00:00 2001 From: Jake Date: Sat, 4 Apr 2026 12:05:32 -0400 Subject: [PATCH 1/7] feat(meta): add extended flag --- packages/cubejs-client-core/src/index.ts | 493 ++++++++++++++--------- 1 file changed, 313 insertions(+), 180 deletions(-) diff --git a/packages/cubejs-client-core/src/index.ts b/packages/cubejs-client-core/src/index.ts index 821deabe68fa4..719f9ae8557d3 100644 --- a/packages/cubejs-client-core/src/index.ts +++ b/packages/cubejs-client-core/src/index.ts @@ -1,10 +1,14 @@ -import { v4 as uuidv4 } from 'uuid'; -import ResultSet from './ResultSet'; -import SqlQuery from './SqlQuery'; -import Meta from './Meta'; -import ProgressResult from './ProgressResult'; -import HttpTransport, { ErrorResponse, ITransport, TransportOptions } from './HttpTransport'; -import RequestError from './RequestError'; +import { v4 as uuidv4 } from "uuid"; +import ResultSet from "./ResultSet"; +import SqlQuery from "./SqlQuery"; +import Meta from "./Meta"; +import ProgressResult from "./ProgressResult"; +import HttpTransport, { + ErrorResponse, + ITransport, + TransportOptions, +} from "./HttpTransport"; +import RequestError from "./RequestError"; import { CacheMode, DimensionFormat, @@ -17,8 +21,8 @@ import { Query, QueryOrder, QueryType, - TransformedQuery -} from './types'; + TransformedQuery, +} from "./types"; export type LoadMethodCallback = (error: Error | null, resultSet: T) => void; @@ -63,29 +67,44 @@ export type LoadMethodOptions = { baseRequestId?: string; }; +export type MetaMethodOptions = LoadMethodOptions & { + /** + * When `true`, requests extended meta from the API (joins, pre-aggregations, SQL snippets, etc.). + * Only pass this property when the value is `true`. Omit it when extended meta is not needed — + * the gateway treats the presence of any `extended` query parameter as extended mode, so + * sending `extended=false` would still select extended meta. + */ + extended?: boolean; +}; + export type DeeplyReadonly = { readonly [K in keyof T]: DeeplyReadonly; }; export type ExtractMembers> = - | (T extends { dimensions: readonly (infer Names)[]; } ? Names : never) - | (T extends { measures: readonly (infer Names)[]; } ? Names : never) - | (T extends { timeDimensions: (infer U); } ? ExtractTimeMembers : never); + | (T extends { dimensions: readonly (infer Names)[] } ? Names : never) + | (T extends { measures: readonly (infer Names)[] } ? Names : never) + | (T extends { timeDimensions: infer U } ? ExtractTimeMembers : never); // If we can't infer any members at all, then return any. -export type SingleQueryRecordType> = ExtractMembers extends never - ? any - : { [K in string & ExtractMembers]: string | number | boolean | null }; +export type SingleQueryRecordType> = + ExtractMembers extends never + ? any + : { [K in string & ExtractMembers]: string | number | boolean | null }; export type QueryArrayRecordType> = T extends readonly [infer First, ...infer Rest] - ? SingleQueryRecordType> | QueryArrayRecordType> + ? + | SingleQueryRecordType> + | QueryArrayRecordType> : never; export type QueryRecordType> = - T extends DeeplyReadonly ? QueryArrayRecordType : - T extends DeeplyReadonly ? SingleQueryRecordType : - never; + T extends DeeplyReadonly + ? QueryArrayRecordType + : T extends DeeplyReadonly + ? SingleQueryRecordType + : never; export interface UnsubscribeObj { /** @@ -134,17 +153,20 @@ export type CubeSqlResult = { lastRefreshTime?: string; }; -export type CubeSqlStreamChunk = { - type: 'schema'; - schema: CubeSqlSchemaColumn[]; - lastRefreshTime?: string; -} | { - type: 'data'; - data: (string | number | boolean | null)[]; -} | { - type: 'error'; - error: string; -}; +export type CubeSqlStreamChunk = + | { + type: "schema"; + schema: CubeSqlSchemaColumn[]; + lastRefreshTime?: string; + } + | { + type: "data"; + data: (string | number | boolean | null)[]; + } + | { + type: "error"; + error: string; + }; interface BodyResponse { error?: string; @@ -153,7 +175,7 @@ interface BodyResponse { let mutexCounter = 0; -const MUTEX_ERROR = 'Mutex has been changed'; +const MUTEX_ERROR = "Mutex has been changed"; function mutexPromise(promise: Promise): Promise { return promise @@ -167,7 +189,7 @@ function mutexPromise(promise: Promise): Promise { }); } -export type ResponseFormat = 'compact' | 'default' | undefined; +export type ResponseFormat = "compact" | "default" | undefined; export type CubeApiOptions = { /** @@ -178,12 +200,12 @@ export type CubeApiOptions = { * Transport implementation to use. [HttpTransport](#http-transport) will be used by default. */ transport?: ITransport; - method?: TransportOptions['method']; - headers?: TransportOptions['headers']; + method?: TransportOptions["method"]; + headers?: TransportOptions["headers"]; pollInterval?: number; - credentials?: TransportOptions['credentials']; + credentials?: TransportOptions["credentials"]; parseDateMeasures?: boolean; - resType?: 'default' | 'compact'; + resType?: "default" | "compact"; castNumerics?: boolean; /** * How many network errors would be retried before returning to users. Default to 0. @@ -203,15 +225,19 @@ export type CubeApiOptions = { * Main class for accessing Cube API */ class CubeApi { - private readonly apiToken: string | (() => Promise) | (CubeApiOptions & any[]) | undefined; + private readonly apiToken: + | string + | (() => Promise) + | (CubeApiOptions & any[]) + | undefined; private readonly apiUrl: string; - private readonly method: TransportOptions['method']; + private readonly method: TransportOptions["method"]; - private readonly headers: TransportOptions['headers']; + private readonly headers: TransportOptions["headers"]; - private readonly credentials: TransportOptions['credentials']; + private readonly credentials: TransportOptions["credentials"]; protected readonly transport: ITransport; @@ -225,7 +251,10 @@ class CubeApi { private updateAuthorizationPromise: Promise | null; - public constructor(apiToken: string | (() => Promise) | undefined, options: CubeApiOptions); + public constructor( + apiToken: string | (() => Promise) | undefined, + options: CubeApiOptions + ); public constructor(options: CubeApiOptions); @@ -254,13 +283,13 @@ class CubeApi { apiToken: string | (() => Promise) | undefined | CubeApiOptions, options?: CubeApiOptions ) { - if (apiToken && !Array.isArray(apiToken) && typeof apiToken === 'object') { + if (apiToken && !Array.isArray(apiToken) && typeof apiToken === "object") { options = apiToken; apiToken = undefined; } if (!options || (!options.transport && !options.apiUrl)) { - throw new Error('The `apiUrl` option is required'); + throw new Error("The `apiUrl` option is required"); } this.apiToken = apiToken; @@ -269,19 +298,22 @@ class CubeApi { this.headers = options.headers || {}; this.credentials = options.credentials; - this.transport = options.transport || new HttpTransport({ - authorization: typeof apiToken === 'string' ? apiToken : undefined, - apiUrl: this.apiUrl, - method: this.method, - headers: this.headers, - credentials: this.credentials, - fetchTimeout: options.fetchTimeout, - signal: options.signal - }); + this.transport = + options.transport || + new HttpTransport({ + authorization: typeof apiToken === "string" ? apiToken : undefined, + apiUrl: this.apiUrl, + method: this.method, + headers: this.headers, + credentials: this.credentials, + fetchTimeout: options.fetchTimeout, + signal: options.signal, + }); this.pollInterval = options.pollInterval || 5; this.parseDateMeasures = options.parseDateMeasures; - this.castNumerics = typeof options.castNumerics === 'boolean' ? options.castNumerics : false; + this.castNumerics = + typeof options.castNumerics === "boolean" ? options.castNumerics : false; this.networkErrorRetries = options.networkErrorRetries || 0; this.updateAuthorizationPromise = null; @@ -294,23 +326,28 @@ class CubeApi { }); } - private loadMethod(request: CallableFunction, toResult: CallableFunction, options?: LoadMethodOptions, callback?: CallableFunction) { + private loadMethod( + request: CallableFunction, + toResult: CallableFunction, + options?: LoadMethodOptions, + callback?: CallableFunction + ) { const mutexValue = ++mutexCounter; - if (typeof options === 'function' && !callback) { + if (typeof options === "function" && !callback) { callback = options; options = undefined; } options = options || {}; - const mutexKey = options.mutexKey || 'default'; + const mutexKey = options.mutexKey || "default"; if (options.mutexObj) { options.mutexObj[mutexKey] = mutexValue; } - const requestPromise = this - .updateTransportAuthorization() - .then(() => request()); + const requestPromise = this.updateTransportAuthorization().then(() => + request() + ); let skipAuthorizationUpdate = true; let unsubscribed = false; @@ -318,9 +355,10 @@ class CubeApi { const checkMutex = async () => { const requestInstance = await requestPromise; - if (options && + if ( + options && options.mutexObj && - options.mutexObj[mutexKey] !== mutexValue + options.mutexObj[mutexKey] !== mutexValue ) { unsubscribed = true; if (requestInstance.unsubscribe) { @@ -332,7 +370,10 @@ class CubeApi { let networkRetries = this.networkErrorRetries; - const loadImpl = async (response: Response | ErrorResponse, next: CallableFunction) => { + const loadImpl = async ( + response: Response | ErrorResponse, + next: CallableFunction + ) => { const requestInstance = await requestPromise; const subscribeNext = async () => { @@ -340,7 +381,9 @@ class CubeApi { if (requestInstance.unsubscribe) { return next(); } else { - await new Promise(resolve => setTimeout(() => resolve(), this.pollInterval * 1000)); + await new Promise((resolve) => + setTimeout(() => resolve(), this.pollInterval * 1000) + ); return next(); } } @@ -350,7 +393,9 @@ class CubeApi { const continueWait = async (wait: boolean = false) => { if (!unsubscribed) { if (wait) { - await new Promise(resolve => setTimeout(() => resolve(), this.pollInterval * 1000)); + await new Promise((resolve) => + setTimeout(() => resolve(), this.pollInterval * 1000) + ); } return next(); } @@ -363,18 +408,20 @@ class CubeApi { skipAuthorizationUpdate = false; - if (('status' in response && response.status === 502) || - ('error' in response && response.error?.toLowerCase() === 'network error') && - --networkRetries >= 0 + if ( + ("status" in response && response.status === 502) || + ("error" in response && + response.error?.toLowerCase() === "network error" && + --networkRetries >= 0) ) { await checkMutex(); return continueWait(true); } // From here we're sure that response is only fetch Response - response = (response as Response); + response = response as Response; let body: BodyResponse = {}; - let text = ''; + let text = ""; try { text = await response.text(); body = JSON.parse(text); @@ -382,10 +429,12 @@ class CubeApi { body.error = text; } - if (body.error?.includes('Continue wait')) { + if (body.error?.includes("Continue wait")) { await checkMutex(); if (options?.progressCallback) { - options.progressCallback(new ProgressResult(body as ProgressResponse)); + options.progressCallback( + new ProgressResult(body as ProgressResponse) + ); } return continueWait(); } @@ -396,7 +445,11 @@ class CubeApi { await requestInstance.unsubscribe(); } - const error = new RequestError(body.error || (response as any).error || '', body, response.status); + const error = new RequestError( + body.error || (response as any).error || "", + body, + response.status + ); if (callback) { callback(error); } else { @@ -419,8 +472,9 @@ class CubeApi { return subscribeNext(); }; - const promise: Promise = requestPromise - .then(requestInstance => mutexPromise(requestInstance.subscribe(loadImpl))); + const promise: Promise = requestPromise.then((requestInstance) => + mutexPromise(requestInstance.subscribe(loadImpl)) + ); if (callback) { return { @@ -432,7 +486,7 @@ class CubeApi { return requestInstance.unsubscribe(); } return null; - } + }, }; } else { return promise; @@ -447,7 +501,7 @@ class CubeApi { const tokenFetcher = this.apiToken; - if (typeof tokenFetcher === 'function') { + if (typeof tokenFetcher === "function") { const promise = (async () => { try { const token = await tokenFetcher(); @@ -468,14 +522,14 @@ class CubeApi { /** * Add system properties to a query object. */ - private patchQueryInternal(query: DeeplyReadonly, responseFormat: ResponseFormat): DeeplyReadonly { - if ( - responseFormat === 'compact' && - query.responseFormat !== 'compact' - ) { + private patchQueryInternal( + query: DeeplyReadonly, + responseFormat: ResponseFormat + ): DeeplyReadonly { + if (responseFormat === "compact" && query.responseFormat !== "compact") { return { ...query, - responseFormat: 'compact', + responseFormat: "compact", }; } else { return query; @@ -486,17 +540,18 @@ class CubeApi { * Process result fetched from the gateway#load method according * to the network protocol. */ - protected loadResponseInternal(response: LoadResponse, options: LoadMethodOptions | null = {}): ResultSet { - if ( - response.results.length - ) { + protected loadResponseInternal( + response: LoadResponse, + options: LoadMethodOptions | null = {} + ): ResultSet { + if (response.results.length) { if (options?.castNumerics) { response.results.forEach((result) => { const numericMembers = Object.entries({ ...result.annotation.measures, ...result.annotation.dimensions, }).reduce((acc, [k, v]) => { - if (v.type === 'number') { + if (v.type === "number") { acc.push(k); } return acc; @@ -514,11 +569,16 @@ class CubeApi { }); } - if (response.results[0].query.responseFormat && - response.results[0].query.responseFormat === 'compact') { + if ( + response.results[0].query.responseFormat && + response.results[0].query.responseFormat === "compact" + ) { response.results.forEach((result, j) => { const data: Record[] = []; - const { dataset, members } = result.data as unknown as { dataset: any[]; members: string[] }; + const { dataset, members } = result.data as unknown as { + dataset: any[]; + members: string[]; + }; dataset.forEach((r) => { const row: Record = {}; members.forEach((m, i) => { @@ -532,19 +592,19 @@ class CubeApi { } return new ResultSet(response, { - parseDateMeasures: this.parseDateMeasures + parseDateMeasures: this.parseDateMeasures, }); } public load>( query: QueryType, - options?: LoadMethodOptions, + options?: LoadMethodOptions ): Promise>>; public load>( query: QueryType, options?: LoadMethodOptions, - callback?: LoadMethodCallback>>, + callback?: LoadMethodCallback>> ): UnsubscribeObj; public load>( @@ -581,12 +641,17 @@ class CubeApi { * @param callback * @param responseFormat */ - public load>(query: QueryType, options?: LoadMethodOptions, callback?: CallableFunction, responseFormat: ResponseFormat = 'default') { + public load>( + query: QueryType, + options?: LoadMethodOptions, + callback?: CallableFunction, + responseFormat: ResponseFormat = "default" + ) { [query, options] = this.prepareQueryOptions(query, options, responseFormat); const params: Record = { query, - queryType: 'multi', + queryType: "multi", signal: options?.signal, baseRequestId: options?.baseRequestId, }; @@ -596,25 +661,34 @@ class CubeApi { } return this.loadMethod( - () => this.request('load', params), + () => this.request("load", params), (response: any) => this.loadResponseInternal(response, options), options, callback ); } - private prepareQueryOptions>(query: QueryType, options?: LoadMethodOptions | null, responseFormat: ResponseFormat = 'default'): [query: QueryType, options: LoadMethodOptions] { + private prepareQueryOptions< + QueryType extends DeeplyReadonly + >( + query: QueryType, + options?: LoadMethodOptions | null, + responseFormat: ResponseFormat = "default" + ): [query: QueryType, options: LoadMethodOptions] { options = { castNumerics: this.castNumerics, - ...options + ...options, }; - if (responseFormat === 'compact') { + if (responseFormat === "compact") { if (Array.isArray(query)) { - const patched = query.map((q) => this.patchQueryInternal(q, 'compact')); + const patched = query.map((q) => this.patchQueryInternal(q, "compact")); return [patched as unknown as QueryType, options]; } else { - const patched = this.patchQueryInternal(query as DeeplyReadonly, 'compact'); + const patched = this.patchQueryInternal( + query as DeeplyReadonly, + "compact" + ); return [patched as QueryType, options]; } } @@ -654,94 +728,141 @@ class CubeApi { query: QueryType, options: LoadMethodOptions | null, callback: LoadMethodCallback>>, - responseFormat: ResponseFormat = 'default' + responseFormat: ResponseFormat = "default" ): UnsubscribeObj { [query, options] = this.prepareQueryOptions(query, options, responseFormat); return this.loadMethod( - () => this.request('subscribe', { - query, - queryType: 'multi', - signal: options?.signal, - baseRequestId: options?.baseRequestId, - }), + () => + this.request("subscribe", { + query, + queryType: "multi", + signal: options?.signal, + baseRequestId: options?.baseRequestId, + }), (response: any) => this.loadResponseInternal(response, options), { ...options, subscribe: true }, callback ) as UnsubscribeObj; } - public sql(query: DeeplyReadonly, options?: LoadMethodOptions): Promise; + public sql( + query: DeeplyReadonly, + options?: LoadMethodOptions + ): Promise; - public sql(query: DeeplyReadonly, options?: LoadMethodOptions, callback?: LoadMethodCallback): UnsubscribeObj; + public sql( + query: DeeplyReadonly, + options?: LoadMethodOptions, + callback?: LoadMethodCallback + ): UnsubscribeObj; /** * Get generated SQL string for the given `query`. */ - public sql(query: DeeplyReadonly, options?: LoadMethodOptions, callback?: LoadMethodCallback): Promise | UnsubscribeObj { + public sql( + query: DeeplyReadonly, + options?: LoadMethodOptions, + callback?: LoadMethodCallback + ): Promise | UnsubscribeObj { return this.loadMethod( - () => this.request('sql', { - query, - signal: options?.signal, - baseRequestId: options?.baseRequestId, - }), - (response: any) => (Array.isArray(response) ? response.map((body) => new SqlQuery(body)) : new SqlQuery(response)), + () => + this.request("sql", { + query, + signal: options?.signal, + baseRequestId: options?.baseRequestId, + }), + (response: any) => + Array.isArray(response) + ? response.map((body) => new SqlQuery(body)) + : new SqlQuery(response), options, callback ); } - public meta(options?: LoadMethodOptions): Promise; + public meta(options?: MetaMethodOptions): Promise; - public meta(options?: LoadMethodOptions, callback?: LoadMethodCallback): UnsubscribeObj; + public meta( + options?: MetaMethodOptions, + callback?: LoadMethodCallback + ): UnsubscribeObj; /** * Get meta description of cubes available for querying. */ - public meta(options?: LoadMethodOptions, callback?: LoadMethodCallback): Promise | UnsubscribeObj { + public meta( + options?: MetaMethodOptions, + callback?: LoadMethodCallback + ): Promise | UnsubscribeObj { return this.loadMethod( - () => this.request('meta', { - signal: options?.signal, - baseRequestId: options?.baseRequestId, - }), + () => + this.request("meta", { + signal: options?.signal, + baseRequestId: options?.baseRequestId, + ...(options?.extended === true ? { extended: true } : {}), + }), (body: MetaResponse) => new Meta(body), options, callback ); } - public dryRun(query: DeeplyReadonly, options?: LoadMethodOptions): Promise; + public dryRun( + query: DeeplyReadonly, + options?: LoadMethodOptions + ): Promise; - public dryRun(query: DeeplyReadonly, options: LoadMethodOptions, callback?: LoadMethodCallback): UnsubscribeObj; + public dryRun( + query: DeeplyReadonly, + options: LoadMethodOptions, + callback?: LoadMethodCallback + ): UnsubscribeObj; /** * Get query related meta without query execution */ - public dryRun(query: DeeplyReadonly, options?: LoadMethodOptions, callback?: LoadMethodCallback): Promise | UnsubscribeObj { + public dryRun( + query: DeeplyReadonly, + options?: LoadMethodOptions, + callback?: LoadMethodCallback + ): Promise | UnsubscribeObj { return this.loadMethod( - () => this.request('dry-run', { - query, - signal: options?.signal, - baseRequestId: options?.baseRequestId, - }), + () => + this.request("dry-run", { + query, + signal: options?.signal, + baseRequestId: options?.baseRequestId, + }), (response: DryRunResponse) => response, options, callback ); } - public cubeSql(sqlQuery: string, options?: CubeSqlOptions): Promise; + public cubeSql( + sqlQuery: string, + options?: CubeSqlOptions + ): Promise; - public cubeSql(sqlQuery: string, options?: CubeSqlOptions, callback?: LoadMethodCallback): UnsubscribeObj; + public cubeSql( + sqlQuery: string, + options?: CubeSqlOptions, + callback?: LoadMethodCallback + ): UnsubscribeObj; /** * Execute a Cube SQL query against Cube SQL interface and return the results. */ - public cubeSql(sqlQuery: string, options?: CubeSqlOptions, callback?: LoadMethodCallback): Promise | UnsubscribeObj { + public cubeSql( + sqlQuery: string, + options?: CubeSqlOptions, + callback?: LoadMethodCallback + ): Promise | UnsubscribeObj { return this.loadMethod( () => { const cubesqlParams: Record = { query: sqlQuery, - method: 'POST', + method: "POST", signal: options?.signal, fetchTimeout: options?.timeout, baseRequestId: options?.baseRequestId, @@ -752,27 +873,27 @@ class CubeApi { cubesqlParams.cache = options.cache; } - const request = this.request('cubesql', cubesqlParams); + const request = this.request("cubesql", cubesqlParams); return request; }, (response: any) => { // TODO: The response is sending both errors and successful results as `error` if (!response || !response.error) { - throw new Error('Invalid response format'); + throw new Error("Invalid response format"); } // Check if this is a timeout or abort error from transport - if (response.error === 'timeout') { + if (response.error === "timeout") { const timeoutMs = options?.timeout || 5 * 60 * 1000; throw new Error(`CubeSQL query timed out after ${timeoutMs}ms`); } - if (response.error === 'aborted') { - throw new Error('CubeSQL query was aborted'); + if (response.error === "aborted") { + throw new Error("CubeSQL query was aborted"); } - const [schema, ...data] = response.error.split('\n'); + const [schema, ...data] = response.error.split("\n"); try { const parsedSchema = JSON.parse(schema); @@ -782,7 +903,9 @@ class CubeApi { .filter((d: string) => d.trim().length) .map((d: string) => JSON.parse(d).data) .reduce((a: any, b: any) => a.concat(b), []), - ...(parsedSchema.lastRefreshTime ? { lastRefreshTime: parsedSchema.lastRefreshTime } : {}), + ...(parsedSchema.lastRefreshTime + ? { lastRefreshTime: parsedSchema.lastRefreshTime } + : {}), }; } catch (err) { throw new Error(response.error); @@ -797,24 +920,27 @@ class CubeApi { * Execute a Cube SQL query against Cube SQL interface and return streaming results as an async generator. * The server returns JSONL (JSON Lines) format with schema first, then data rows. */ - public async* cubeSqlStream(sqlQuery: string, options?: CubeSqlOptions): AsyncGenerator { + public async *cubeSqlStream( + sqlQuery: string, + options?: CubeSqlOptions + ): AsyncGenerator { if (!this.transport.requestStream) { - throw new Error('Transport does not support streaming'); + throw new Error("Transport does not support streaming"); } - const streamResponse = this.transport.requestStream('cubesql', { - method: 'POST', + const streamResponse = this.transport.requestStream("cubesql", { + method: "POST", signal: options?.signal, fetchTimeout: options?.timeout, baseRequestId: uuidv4(), params: { query: sqlQuery, - cache: options?.cache - } + cache: options?.cache, + }, }); const decoder = new TextDecoder(); - let buffer = ''; + let buffer = ""; try { const stream = await streamResponse.stream(); @@ -822,8 +948,8 @@ class CubeApi { for await (const chunk of stream) { buffer += decoder.decode(chunk, { stream: true }); - const lines = buffer.split('\n'); - buffer = lines.pop() || ''; + const lines = buffer.split("\n"); + buffer = lines.pop() || ""; for (const line of lines) { if (line.trim()) { @@ -832,25 +958,27 @@ class CubeApi { if (parsed.schema) { yield { - type: 'schema' as const, + type: "schema" as const, schema: parsed.schema, - ...(parsed.lastRefreshTime ? { lastRefreshTime: parsed.lastRefreshTime } : {}), + ...(parsed.lastRefreshTime + ? { lastRefreshTime: parsed.lastRefreshTime } + : {}), }; } else if (parsed.data) { yield { - type: 'data' as const, - data: parsed.data + type: "data" as const, + data: parsed.data, }; } else if (parsed.error) { yield { - type: 'error' as const, - error: parsed.error + type: "error" as const, + error: parsed.error, }; } } catch (parseError) { yield { - type: 'error' as const, - error: `Failed to parse JSON line: ${line}` + type: "error" as const, + error: `Failed to parse JSON line: ${line}`, }; } } @@ -863,31 +991,33 @@ class CubeApi { if (parsed.schema) { yield { - type: 'schema' as const, + type: "schema" as const, schema: parsed.schema, - ...(parsed.lastRefreshTime ? { lastRefreshTime: parsed.lastRefreshTime } : {}), + ...(parsed.lastRefreshTime + ? { lastRefreshTime: parsed.lastRefreshTime } + : {}), }; } else if (parsed.data) { yield { - type: 'data' as const, - data: parsed.data + type: "data" as const, + data: parsed.data, }; } else if (parsed.error) { yield { - type: 'error' as const, - error: parsed.error + type: "error" as const, + error: parsed.error, }; } } catch (parseError) { yield { - type: 'error' as const, - error: `Failed to parse remaining JSON: ${buffer}` + type: "error" as const, + error: `Failed to parse remaining JSON: ${buffer}`, }; } } } catch (error: any) { - if (error.name === 'AbortError') { - throw new Error('aborted'); + if (error.name === "AbortError") { + throw new Error("aborted"); } throw error; } finally { @@ -898,15 +1028,18 @@ class CubeApi { } } -export default (apiToken: string | (() => Promise), options: CubeApiOptions) => new CubeApi(apiToken, options); +export default ( + apiToken: string | (() => Promise), + options: CubeApiOptions +) => new CubeApi(apiToken, options); export { CubeApi }; -export { default as Meta } from './Meta'; -export { default as SqlQuery } from './SqlQuery'; -export { default as RequestError } from './RequestError'; -export { default as ProgressResult } from './ProgressResult'; -export { default as ResultSet } from './ResultSet'; -export * from './HttpTransport'; -export * from './utils'; -export * from './time'; -export * from './types'; +export { default as Meta } from "./Meta"; +export { default as SqlQuery } from "./SqlQuery"; +export { default as RequestError } from "./RequestError"; +export { default as ProgressResult } from "./ProgressResult"; +export { default as ResultSet } from "./ResultSet"; +export * from "./HttpTransport"; +export * from "./utils"; +export * from "./time"; +export * from "./types"; From eeb8c55c854f8ede1f4ace16c83e03e29604766f Mon Sep 17 00:00:00 2001 From: Jake Date: Sat, 4 Apr 2026 12:09:03 -0400 Subject: [PATCH 2/7] fix: lint --- packages/cubejs-client-core/src/index.ts | 225 +++++++++++------------ 1 file changed, 106 insertions(+), 119 deletions(-) diff --git a/packages/cubejs-client-core/src/index.ts b/packages/cubejs-client-core/src/index.ts index 719f9ae8557d3..f8150e9292048 100644 --- a/packages/cubejs-client-core/src/index.ts +++ b/packages/cubejs-client-core/src/index.ts @@ -1,14 +1,14 @@ -import { v4 as uuidv4 } from "uuid"; -import ResultSet from "./ResultSet"; -import SqlQuery from "./SqlQuery"; -import Meta from "./Meta"; -import ProgressResult from "./ProgressResult"; +import { v4 as uuidv4 } from 'uuid'; +import ResultSet from './ResultSet'; +import SqlQuery from './SqlQuery'; +import Meta from './Meta'; +import ProgressResult from './ProgressResult'; import HttpTransport, { ErrorResponse, ITransport, TransportOptions, -} from "./HttpTransport"; -import RequestError from "./RequestError"; +} from './HttpTransport'; +import RequestError from './RequestError'; import { CacheMode, DimensionFormat, @@ -22,7 +22,7 @@ import { QueryOrder, QueryType, TransformedQuery, -} from "./types"; +} from './types'; export type LoadMethodCallback = (error: Error | null, resultSet: T) => void; @@ -155,16 +155,16 @@ export type CubeSqlResult = { export type CubeSqlStreamChunk = | { - type: "schema"; + type: 'schema'; schema: CubeSqlSchemaColumn[]; lastRefreshTime?: string; } | { - type: "data"; + type: 'data'; data: (string | number | boolean | null)[]; } | { - type: "error"; + type: 'error'; error: string; }; @@ -175,7 +175,7 @@ interface BodyResponse { let mutexCounter = 0; -const MUTEX_ERROR = "Mutex has been changed"; +const MUTEX_ERROR = 'Mutex has been changed'; function mutexPromise(promise: Promise): Promise { return promise @@ -189,7 +189,7 @@ function mutexPromise(promise: Promise): Promise { }); } -export type ResponseFormat = "compact" | "default" | undefined; +export type ResponseFormat = 'compact' | 'default' | undefined; export type CubeApiOptions = { /** @@ -200,12 +200,12 @@ export type CubeApiOptions = { * Transport implementation to use. [HttpTransport](#http-transport) will be used by default. */ transport?: ITransport; - method?: TransportOptions["method"]; - headers?: TransportOptions["headers"]; + method?: TransportOptions['method']; + headers?: TransportOptions['headers']; pollInterval?: number; - credentials?: TransportOptions["credentials"]; + credentials?: TransportOptions['credentials']; parseDateMeasures?: boolean; - resType?: "default" | "compact"; + resType?: 'default' | 'compact'; castNumerics?: boolean; /** * How many network errors would be retried before returning to users. Default to 0. @@ -233,11 +233,11 @@ class CubeApi { private readonly apiUrl: string; - private readonly method: TransportOptions["method"]; + private readonly method: TransportOptions['method']; - private readonly headers: TransportOptions["headers"]; + private readonly headers: TransportOptions['headers']; - private readonly credentials: TransportOptions["credentials"]; + private readonly credentials: TransportOptions['credentials']; protected readonly transport: ITransport; @@ -283,13 +283,13 @@ class CubeApi { apiToken: string | (() => Promise) | undefined | CubeApiOptions, options?: CubeApiOptions ) { - if (apiToken && !Array.isArray(apiToken) && typeof apiToken === "object") { + if (apiToken && !Array.isArray(apiToken) && typeof apiToken === 'object') { options = apiToken; apiToken = undefined; } if (!options || (!options.transport && !options.apiUrl)) { - throw new Error("The `apiUrl` option is required"); + throw new Error('The `apiUrl` option is required'); } this.apiToken = apiToken; @@ -301,7 +301,7 @@ class CubeApi { this.transport = options.transport || new HttpTransport({ - authorization: typeof apiToken === "string" ? apiToken : undefined, + authorization: typeof apiToken === 'string' ? apiToken : undefined, apiUrl: this.apiUrl, method: this.method, headers: this.headers, @@ -313,7 +313,7 @@ class CubeApi { this.pollInterval = options.pollInterval || 5; this.parseDateMeasures = options.parseDateMeasures; this.castNumerics = - typeof options.castNumerics === "boolean" ? options.castNumerics : false; + typeof options.castNumerics === 'boolean' ? options.castNumerics : false; this.networkErrorRetries = options.networkErrorRetries || 0; this.updateAuthorizationPromise = null; @@ -333,21 +333,19 @@ class CubeApi { callback?: CallableFunction ) { const mutexValue = ++mutexCounter; - if (typeof options === "function" && !callback) { + if (typeof options === 'function' && !callback) { callback = options; options = undefined; } options = options || {}; - const mutexKey = options.mutexKey || "default"; + const mutexKey = options.mutexKey || 'default'; if (options.mutexObj) { options.mutexObj[mutexKey] = mutexValue; } - const requestPromise = this.updateTransportAuthorization().then(() => - request() - ); + const requestPromise = this.updateTransportAuthorization().then(() => request()); let skipAuthorizationUpdate = true; let unsubscribed = false; @@ -381,9 +379,7 @@ class CubeApi { if (requestInstance.unsubscribe) { return next(); } else { - await new Promise((resolve) => - setTimeout(() => resolve(), this.pollInterval * 1000) - ); + await new Promise((resolve) => setTimeout(() => resolve(), this.pollInterval * 1000)); return next(); } } @@ -393,9 +389,7 @@ class CubeApi { const continueWait = async (wait: boolean = false) => { if (!unsubscribed) { if (wait) { - await new Promise((resolve) => - setTimeout(() => resolve(), this.pollInterval * 1000) - ); + await new Promise((resolve) => setTimeout(() => resolve(), this.pollInterval * 1000)); } return next(); } @@ -409,9 +403,9 @@ class CubeApi { skipAuthorizationUpdate = false; if ( - ("status" in response && response.status === 502) || - ("error" in response && - response.error?.toLowerCase() === "network error" && + ('status' in response && response.status === 502) || + ('error' in response && + response.error?.toLowerCase() === 'network error' && --networkRetries >= 0) ) { await checkMutex(); @@ -421,7 +415,7 @@ class CubeApi { // From here we're sure that response is only fetch Response response = response as Response; let body: BodyResponse = {}; - let text = ""; + let text = ''; try { text = await response.text(); body = JSON.parse(text); @@ -429,7 +423,7 @@ class CubeApi { body.error = text; } - if (body.error?.includes("Continue wait")) { + if (body.error?.includes('Continue wait')) { await checkMutex(); if (options?.progressCallback) { options.progressCallback( @@ -446,7 +440,7 @@ class CubeApi { } const error = new RequestError( - body.error || (response as any).error || "", + body.error || (response as any).error || '', body, response.status ); @@ -472,9 +466,7 @@ class CubeApi { return subscribeNext(); }; - const promise: Promise = requestPromise.then((requestInstance) => - mutexPromise(requestInstance.subscribe(loadImpl)) - ); + const promise: Promise = requestPromise.then((requestInstance) => mutexPromise(requestInstance.subscribe(loadImpl))); if (callback) { return { @@ -501,7 +493,7 @@ class CubeApi { const tokenFetcher = this.apiToken; - if (typeof tokenFetcher === "function") { + if (typeof tokenFetcher === 'function') { const promise = (async () => { try { const token = await tokenFetcher(); @@ -526,10 +518,10 @@ class CubeApi { query: DeeplyReadonly, responseFormat: ResponseFormat ): DeeplyReadonly { - if (responseFormat === "compact" && query.responseFormat !== "compact") { + if (responseFormat === 'compact' && query.responseFormat !== 'compact') { return { ...query, - responseFormat: "compact", + responseFormat: 'compact', }; } else { return query; @@ -551,7 +543,7 @@ class CubeApi { ...result.annotation.measures, ...result.annotation.dimensions, }).reduce((acc, [k, v]) => { - if (v.type === "number") { + if (v.type === 'number') { acc.push(k); } return acc; @@ -571,7 +563,7 @@ class CubeApi { if ( response.results[0].query.responseFormat && - response.results[0].query.responseFormat === "compact" + response.results[0].query.responseFormat === 'compact' ) { response.results.forEach((result, j) => { const data: Record[] = []; @@ -645,13 +637,13 @@ class CubeApi { query: QueryType, options?: LoadMethodOptions, callback?: CallableFunction, - responseFormat: ResponseFormat = "default" + responseFormat: ResponseFormat = 'default' ) { [query, options] = this.prepareQueryOptions(query, options, responseFormat); const params: Record = { query, - queryType: "multi", + queryType: 'multi', signal: options?.signal, baseRequestId: options?.baseRequestId, }; @@ -661,7 +653,7 @@ class CubeApi { } return this.loadMethod( - () => this.request("load", params), + () => this.request('load', params), (response: any) => this.loadResponseInternal(response, options), options, callback @@ -673,21 +665,21 @@ class CubeApi { >( query: QueryType, options?: LoadMethodOptions | null, - responseFormat: ResponseFormat = "default" + responseFormat: ResponseFormat = 'default' ): [query: QueryType, options: LoadMethodOptions] { options = { castNumerics: this.castNumerics, ...options, }; - if (responseFormat === "compact") { + if (responseFormat === 'compact') { if (Array.isArray(query)) { - const patched = query.map((q) => this.patchQueryInternal(q, "compact")); + const patched = query.map((q) => this.patchQueryInternal(q, 'compact')); return [patched as unknown as QueryType, options]; } else { const patched = this.patchQueryInternal( query as DeeplyReadonly, - "compact" + 'compact' ); return [patched as QueryType, options]; } @@ -728,17 +720,16 @@ class CubeApi { query: QueryType, options: LoadMethodOptions | null, callback: LoadMethodCallback>>, - responseFormat: ResponseFormat = "default" + responseFormat: ResponseFormat = 'default' ): UnsubscribeObj { [query, options] = this.prepareQueryOptions(query, options, responseFormat); return this.loadMethod( - () => - this.request("subscribe", { - query, - queryType: "multi", - signal: options?.signal, - baseRequestId: options?.baseRequestId, - }), + () => this.request('subscribe', { + query, + queryType: 'multi', + signal: options?.signal, + baseRequestId: options?.baseRequestId, + }), (response: any) => this.loadResponseInternal(response, options), { ...options, subscribe: true }, callback @@ -765,16 +756,14 @@ class CubeApi { callback?: LoadMethodCallback ): Promise | UnsubscribeObj { return this.loadMethod( - () => - this.request("sql", { - query, - signal: options?.signal, - baseRequestId: options?.baseRequestId, - }), - (response: any) => - Array.isArray(response) - ? response.map((body) => new SqlQuery(body)) - : new SqlQuery(response), + () => this.request('sql', { + query, + signal: options?.signal, + baseRequestId: options?.baseRequestId, + }), + (response: any) => (Array.isArray(response) + ? response.map((body) => new SqlQuery(body)) + : new SqlQuery(response)), options, callback ); @@ -795,12 +784,11 @@ class CubeApi { callback?: LoadMethodCallback ): Promise | UnsubscribeObj { return this.loadMethod( - () => - this.request("meta", { - signal: options?.signal, - baseRequestId: options?.baseRequestId, - ...(options?.extended === true ? { extended: true } : {}), - }), + () => this.request('meta', { + signal: options?.signal, + baseRequestId: options?.baseRequestId, + ...(options?.extended === true ? { extended: true } : {}), + }), (body: MetaResponse) => new Meta(body), options, callback @@ -827,12 +815,11 @@ class CubeApi { callback?: LoadMethodCallback ): Promise | UnsubscribeObj { return this.loadMethod( - () => - this.request("dry-run", { - query, - signal: options?.signal, - baseRequestId: options?.baseRequestId, - }), + () => this.request('dry-run', { + query, + signal: options?.signal, + baseRequestId: options?.baseRequestId, + }), (response: DryRunResponse) => response, options, callback @@ -862,7 +849,7 @@ class CubeApi { () => { const cubesqlParams: Record = { query: sqlQuery, - method: "POST", + method: 'POST', signal: options?.signal, fetchTimeout: options?.timeout, baseRequestId: options?.baseRequestId, @@ -873,27 +860,27 @@ class CubeApi { cubesqlParams.cache = options.cache; } - const request = this.request("cubesql", cubesqlParams); + const request = this.request('cubesql', cubesqlParams); return request; }, (response: any) => { // TODO: The response is sending both errors and successful results as `error` if (!response || !response.error) { - throw new Error("Invalid response format"); + throw new Error('Invalid response format'); } // Check if this is a timeout or abort error from transport - if (response.error === "timeout") { + if (response.error === 'timeout') { const timeoutMs = options?.timeout || 5 * 60 * 1000; throw new Error(`CubeSQL query timed out after ${timeoutMs}ms`); } - if (response.error === "aborted") { - throw new Error("CubeSQL query was aborted"); + if (response.error === 'aborted') { + throw new Error('CubeSQL query was aborted'); } - const [schema, ...data] = response.error.split("\n"); + const [schema, ...data] = response.error.split('\n'); try { const parsedSchema = JSON.parse(schema); @@ -920,16 +907,16 @@ class CubeApi { * Execute a Cube SQL query against Cube SQL interface and return streaming results as an async generator. * The server returns JSONL (JSON Lines) format with schema first, then data rows. */ - public async *cubeSqlStream( + public async* cubeSqlStream( sqlQuery: string, options?: CubeSqlOptions ): AsyncGenerator { if (!this.transport.requestStream) { - throw new Error("Transport does not support streaming"); + throw new Error('Transport does not support streaming'); } - const streamResponse = this.transport.requestStream("cubesql", { - method: "POST", + const streamResponse = this.transport.requestStream('cubesql', { + method: 'POST', signal: options?.signal, fetchTimeout: options?.timeout, baseRequestId: uuidv4(), @@ -940,7 +927,7 @@ class CubeApi { }); const decoder = new TextDecoder(); - let buffer = ""; + let buffer = ''; try { const stream = await streamResponse.stream(); @@ -948,8 +935,8 @@ class CubeApi { for await (const chunk of stream) { buffer += decoder.decode(chunk, { stream: true }); - const lines = buffer.split("\n"); - buffer = lines.pop() || ""; + const lines = buffer.split('\n'); + buffer = lines.pop() || ''; for (const line of lines) { if (line.trim()) { @@ -958,7 +945,7 @@ class CubeApi { if (parsed.schema) { yield { - type: "schema" as const, + type: 'schema' as const, schema: parsed.schema, ...(parsed.lastRefreshTime ? { lastRefreshTime: parsed.lastRefreshTime } @@ -966,18 +953,18 @@ class CubeApi { }; } else if (parsed.data) { yield { - type: "data" as const, + type: 'data' as const, data: parsed.data, }; } else if (parsed.error) { yield { - type: "error" as const, + type: 'error' as const, error: parsed.error, }; } } catch (parseError) { yield { - type: "error" as const, + type: 'error' as const, error: `Failed to parse JSON line: ${line}`, }; } @@ -991,7 +978,7 @@ class CubeApi { if (parsed.schema) { yield { - type: "schema" as const, + type: 'schema' as const, schema: parsed.schema, ...(parsed.lastRefreshTime ? { lastRefreshTime: parsed.lastRefreshTime } @@ -999,25 +986,25 @@ class CubeApi { }; } else if (parsed.data) { yield { - type: "data" as const, + type: 'data' as const, data: parsed.data, }; } else if (parsed.error) { yield { - type: "error" as const, + type: 'error' as const, error: parsed.error, }; } } catch (parseError) { yield { - type: "error" as const, + type: 'error' as const, error: `Failed to parse remaining JSON: ${buffer}`, }; } } } catch (error: any) { - if (error.name === "AbortError") { - throw new Error("aborted"); + if (error.name === 'AbortError') { + throw new Error('aborted'); } throw error; } finally { @@ -1034,12 +1021,12 @@ export default ( ) => new CubeApi(apiToken, options); export { CubeApi }; -export { default as Meta } from "./Meta"; -export { default as SqlQuery } from "./SqlQuery"; -export { default as RequestError } from "./RequestError"; -export { default as ProgressResult } from "./ProgressResult"; -export { default as ResultSet } from "./ResultSet"; -export * from "./HttpTransport"; -export * from "./utils"; -export * from "./time"; -export * from "./types"; +export { default as Meta } from './Meta'; +export { default as SqlQuery } from './SqlQuery'; +export { default as RequestError } from './RequestError'; +export { default as ProgressResult } from './ProgressResult'; +export { default as ResultSet } from './ResultSet'; +export * from './HttpTransport'; +export * from './utils'; +export * from './time'; +export * from './types'; From 9895618ff0a5359fcd1c6c701b9e426e0af263a1 Mon Sep 17 00:00:00 2001 From: Jake Date: Sat, 4 Apr 2026 12:59:15 -0400 Subject: [PATCH 3/7] feat(cubejs-client-core): add extended meta types for joins and cube members --- packages/cubejs-client-core/src/types.ts | 44 +++++++++++++++++++++++- 1 file changed, 43 insertions(+), 1 deletion(-) diff --git a/packages/cubejs-client-core/src/types.ts b/packages/cubejs-client-core/src/types.ts index f4e8c61f9c526..9a190e300dc68 100644 --- a/packages/cubejs-client-core/src/types.ts +++ b/packages/cubejs-client-core/src/types.ts @@ -386,6 +386,10 @@ export type BaseCubeMember = { aliasMember?: string; }; +export type TCubeMeasureFilterMeta = { + sql?: string; +}; + export type TCubeMeasure = BaseCubeMember & { aggType: 'count' | 'number'; cumulative: boolean; @@ -398,6 +402,8 @@ export type TCubeMeasure = BaseCubeMember & { format?: 'currency' | 'percent'; /** ISO 4217 currency code in uppercase (e.g. USD, EUR) */ currency?: string; + sql?: string; + filters?: TCubeMeasureFilterMeta[]; }; export type CubeTimeDimensionGranularity = { @@ -405,6 +411,19 @@ export type CubeTimeDimensionGranularity = { title: string; }; +export type TCubeDimensionCaseWhen = { + sql?: string; + label?: string | { sql?: string } | unknown; +}; + +export type TCubeDimensionCase = { + when?: TCubeDimensionCaseWhen[]; + else?: { + label?: string; + [key: string]: unknown; + }; +}; + export type BaseCubeDimension = BaseCubeMember & { primaryKey?: boolean; suggestFilterValues: boolean; @@ -412,6 +431,8 @@ export type BaseCubeDimension = BaseCubeMember & { /** ISO 4217 currency code in uppercase (e.g. USD, EUR) */ currency?: string; key?: string; + sql?: string; + case?: TCubeDimensionCase; }; export type CubeTimeDimension = BaseCubeDimension & @@ -421,7 +442,9 @@ export type TCubeDimension = (BaseCubeDimension & { type: Exclude }) | CubeTimeDimension; -export type TCubeSegment = Omit; +export type TCubeSegment = Omit & { + sql?: string; +}; export type NotFoundMember = { title: string; @@ -455,6 +478,19 @@ export type TCubeHierarchy = { public?: boolean; }; +export type TCubeJoin = { + name: string; + sql?: string; + relationship?: string; +} & Record; + +export type TCubePreAggregationMeta = { + name: string; + timeDimensionReference?: string; + dimensionReferences?: string; + measureReferences?: string; +} & Record; + /** * @deprecated use DryRunResponse */ @@ -492,6 +528,12 @@ export type Cube = { isVisible?: boolean; public?: boolean; meta?: any; + joins?: TCubeJoin[]; + sql?: string; + extends?: string; + fileName?: string; + refreshKey?: unknown; + preAggregations?: TCubePreAggregationMeta[]; }; export type CubeMap = { From 45aab6fe6428d1dba248092e0e550ba9a50cfe75 Mon Sep 17 00:00:00 2001 From: Jake Date: Sun, 5 Apr 2026 09:41:01 -0400 Subject: [PATCH 4/7] refactor(cubejs-client-core): fix formatting --- packages/cubejs-client-core/src/index.ts | 303 ++++++++--------------- 1 file changed, 97 insertions(+), 206 deletions(-) diff --git a/packages/cubejs-client-core/src/index.ts b/packages/cubejs-client-core/src/index.ts index f8150e9292048..2b5836cb01bd9 100644 --- a/packages/cubejs-client-core/src/index.ts +++ b/packages/cubejs-client-core/src/index.ts @@ -3,11 +3,7 @@ import ResultSet from './ResultSet'; import SqlQuery from './SqlQuery'; import Meta from './Meta'; import ProgressResult from './ProgressResult'; -import HttpTransport, { - ErrorResponse, - ITransport, - TransportOptions, -} from './HttpTransport'; +import HttpTransport, { ErrorResponse, ITransport, TransportOptions } from './HttpTransport'; import RequestError from './RequestError'; import { CacheMode, @@ -21,7 +17,7 @@ import { Query, QueryOrder, QueryType, - TransformedQuery, + TransformedQuery } from './types'; export type LoadMethodCallback = (error: Error | null, resultSet: T) => void; @@ -82,29 +78,24 @@ export type DeeplyReadonly = { }; export type ExtractMembers> = - | (T extends { dimensions: readonly (infer Names)[] } ? Names : never) - | (T extends { measures: readonly (infer Names)[] } ? Names : never) - | (T extends { timeDimensions: infer U } ? ExtractTimeMembers : never); + | (T extends { dimensions: readonly (infer Names)[]; } ? Names : never) + | (T extends { measures: readonly (infer Names)[]; } ? Names : never) + | (T extends { timeDimensions: (infer U); } ? ExtractTimeMembers : never); // If we can't infer any members at all, then return any. -export type SingleQueryRecordType> = - ExtractMembers extends never - ? any - : { [K in string & ExtractMembers]: string | number | boolean | null }; +export type SingleQueryRecordType> = ExtractMembers extends never + ? any + : { [K in string & ExtractMembers]: string | number | boolean | null }; export type QueryArrayRecordType> = T extends readonly [infer First, ...infer Rest] - ? - | SingleQueryRecordType> - | QueryArrayRecordType> + ? SingleQueryRecordType> | QueryArrayRecordType> : never; export type QueryRecordType> = - T extends DeeplyReadonly - ? QueryArrayRecordType - : T extends DeeplyReadonly - ? SingleQueryRecordType - : never; + T extends DeeplyReadonly ? QueryArrayRecordType : + T extends DeeplyReadonly ? SingleQueryRecordType : + never; export interface UnsubscribeObj { /** @@ -153,20 +144,17 @@ export type CubeSqlResult = { lastRefreshTime?: string; }; -export type CubeSqlStreamChunk = - | { - type: 'schema'; - schema: CubeSqlSchemaColumn[]; - lastRefreshTime?: string; - } - | { - type: 'data'; - data: (string | number | boolean | null)[]; - } - | { - type: 'error'; - error: string; - }; +export type CubeSqlStreamChunk = { + type: 'schema'; + schema: CubeSqlSchemaColumn[]; + lastRefreshTime?: string; +} | { + type: 'data'; + data: (string | number | boolean | null)[]; +} | { + type: 'error'; + error: string; +}; interface BodyResponse { error?: string; @@ -225,11 +213,7 @@ export type CubeApiOptions = { * Main class for accessing Cube API */ class CubeApi { - private readonly apiToken: - | string - | (() => Promise) - | (CubeApiOptions & any[]) - | undefined; + private readonly apiToken: string | (() => Promise) | (CubeApiOptions & any[]) | undefined; private readonly apiUrl: string; @@ -251,10 +235,7 @@ class CubeApi { private updateAuthorizationPromise: Promise | null; - public constructor( - apiToken: string | (() => Promise) | undefined, - options: CubeApiOptions - ); + public constructor(apiToken: string | (() => Promise) | undefined, options: CubeApiOptions); public constructor(options: CubeApiOptions); @@ -298,22 +279,19 @@ class CubeApi { this.headers = options.headers || {}; this.credentials = options.credentials; - this.transport = - options.transport || - new HttpTransport({ - authorization: typeof apiToken === 'string' ? apiToken : undefined, - apiUrl: this.apiUrl, - method: this.method, - headers: this.headers, - credentials: this.credentials, - fetchTimeout: options.fetchTimeout, - signal: options.signal, - }); + this.transport = options.transport || new HttpTransport({ + authorization: typeof apiToken === 'string' ? apiToken : undefined, + apiUrl: this.apiUrl, + method: this.method, + headers: this.headers, + credentials: this.credentials, + fetchTimeout: options.fetchTimeout, + signal: options.signal + }); this.pollInterval = options.pollInterval || 5; this.parseDateMeasures = options.parseDateMeasures; - this.castNumerics = - typeof options.castNumerics === 'boolean' ? options.castNumerics : false; + this.castNumerics = typeof options.castNumerics === 'boolean' ? options.castNumerics : false; this.networkErrorRetries = options.networkErrorRetries || 0; this.updateAuthorizationPromise = null; @@ -326,12 +304,7 @@ class CubeApi { }); } - private loadMethod( - request: CallableFunction, - toResult: CallableFunction, - options?: LoadMethodOptions, - callback?: CallableFunction - ) { + private loadMethod(request: CallableFunction, toResult: CallableFunction, options?: LoadMethodOptions, callback?: CallableFunction) { const mutexValue = ++mutexCounter; if (typeof options === 'function' && !callback) { callback = options; @@ -345,7 +318,9 @@ class CubeApi { options.mutexObj[mutexKey] = mutexValue; } - const requestPromise = this.updateTransportAuthorization().then(() => request()); + const requestPromise = this + .updateTransportAuthorization() + .then(() => request()); let skipAuthorizationUpdate = true; let unsubscribed = false; @@ -353,10 +328,9 @@ class CubeApi { const checkMutex = async () => { const requestInstance = await requestPromise; - if ( - options && + if (options && options.mutexObj && - options.mutexObj[mutexKey] !== mutexValue + options.mutexObj[mutexKey] !== mutexValue ) { unsubscribed = true; if (requestInstance.unsubscribe) { @@ -368,10 +342,7 @@ class CubeApi { let networkRetries = this.networkErrorRetries; - const loadImpl = async ( - response: Response | ErrorResponse, - next: CallableFunction - ) => { + const loadImpl = async (response: Response | ErrorResponse, next: CallableFunction) => { const requestInstance = await requestPromise; const subscribeNext = async () => { @@ -379,7 +350,7 @@ class CubeApi { if (requestInstance.unsubscribe) { return next(); } else { - await new Promise((resolve) => setTimeout(() => resolve(), this.pollInterval * 1000)); + await new Promise(resolve => setTimeout(() => resolve(), this.pollInterval * 1000)); return next(); } } @@ -389,7 +360,7 @@ class CubeApi { const continueWait = async (wait: boolean = false) => { if (!unsubscribed) { if (wait) { - await new Promise((resolve) => setTimeout(() => resolve(), this.pollInterval * 1000)); + await new Promise(resolve => setTimeout(() => resolve(), this.pollInterval * 1000)); } return next(); } @@ -402,18 +373,16 @@ class CubeApi { skipAuthorizationUpdate = false; - if ( - ('status' in response && response.status === 502) || - ('error' in response && - response.error?.toLowerCase() === 'network error' && - --networkRetries >= 0) + if (('status' in response && response.status === 502) || + ('error' in response && response.error?.toLowerCase() === 'network error') && + --networkRetries >= 0 ) { await checkMutex(); return continueWait(true); } // From here we're sure that response is only fetch Response - response = response as Response; + response = (response as Response); let body: BodyResponse = {}; let text = ''; try { @@ -426,9 +395,7 @@ class CubeApi { if (body.error?.includes('Continue wait')) { await checkMutex(); if (options?.progressCallback) { - options.progressCallback( - new ProgressResult(body as ProgressResponse) - ); + options.progressCallback(new ProgressResult(body as ProgressResponse)); } return continueWait(); } @@ -439,11 +406,7 @@ class CubeApi { await requestInstance.unsubscribe(); } - const error = new RequestError( - body.error || (response as any).error || '', - body, - response.status - ); + const error = new RequestError(body.error || (response as any).error || '', body, response.status); if (callback) { callback(error); } else { @@ -466,7 +429,8 @@ class CubeApi { return subscribeNext(); }; - const promise: Promise = requestPromise.then((requestInstance) => mutexPromise(requestInstance.subscribe(loadImpl))); + const promise: Promise = requestPromise + .then(requestInstance => mutexPromise(requestInstance.subscribe(loadImpl))); if (callback) { return { @@ -478,7 +442,7 @@ class CubeApi { return requestInstance.unsubscribe(); } return null; - }, + } }; } else { return promise; @@ -514,11 +478,11 @@ class CubeApi { /** * Add system properties to a query object. */ - private patchQueryInternal( - query: DeeplyReadonly, - responseFormat: ResponseFormat - ): DeeplyReadonly { - if (responseFormat === 'compact' && query.responseFormat !== 'compact') { + private patchQueryInternal(query: DeeplyReadonly, responseFormat: ResponseFormat): DeeplyReadonly { + if ( + responseFormat === 'compact' && + query.responseFormat !== 'compact' + ) { return { ...query, responseFormat: 'compact', @@ -532,11 +496,10 @@ class CubeApi { * Process result fetched from the gateway#load method according * to the network protocol. */ - protected loadResponseInternal( - response: LoadResponse, - options: LoadMethodOptions | null = {} - ): ResultSet { - if (response.results.length) { + protected loadResponseInternal(response: LoadResponse, options: LoadMethodOptions | null = {}): ResultSet { + if ( + response.results.length + ) { if (options?.castNumerics) { response.results.forEach((result) => { const numericMembers = Object.entries({ @@ -561,16 +524,11 @@ class CubeApi { }); } - if ( - response.results[0].query.responseFormat && - response.results[0].query.responseFormat === 'compact' - ) { + if (response.results[0].query.responseFormat && + response.results[0].query.responseFormat === 'compact') { response.results.forEach((result, j) => { const data: Record[] = []; - const { dataset, members } = result.data as unknown as { - dataset: any[]; - members: string[]; - }; + const { dataset, members } = result.data as unknown as { dataset: any[]; members: string[] }; dataset.forEach((r) => { const row: Record = {}; members.forEach((m, i) => { @@ -584,19 +542,19 @@ class CubeApi { } return new ResultSet(response, { - parseDateMeasures: this.parseDateMeasures, + parseDateMeasures: this.parseDateMeasures }); } public load>( query: QueryType, - options?: LoadMethodOptions + options?: LoadMethodOptions, ): Promise>>; public load>( query: QueryType, options?: LoadMethodOptions, - callback?: LoadMethodCallback>> + callback?: LoadMethodCallback>>, ): UnsubscribeObj; public load>( @@ -633,12 +591,7 @@ class CubeApi { * @param callback * @param responseFormat */ - public load>( - query: QueryType, - options?: LoadMethodOptions, - callback?: CallableFunction, - responseFormat: ResponseFormat = 'default' - ) { + public load>(query: QueryType, options?: LoadMethodOptions, callback?: CallableFunction, responseFormat: ResponseFormat = 'default') { [query, options] = this.prepareQueryOptions(query, options, responseFormat); const params: Record = { @@ -660,16 +613,10 @@ class CubeApi { ); } - private prepareQueryOptions< - QueryType extends DeeplyReadonly - >( - query: QueryType, - options?: LoadMethodOptions | null, - responseFormat: ResponseFormat = 'default' - ): [query: QueryType, options: LoadMethodOptions] { + private prepareQueryOptions>(query: QueryType, options?: LoadMethodOptions | null, responseFormat: ResponseFormat = 'default'): [query: QueryType, options: LoadMethodOptions] { options = { castNumerics: this.castNumerics, - ...options, + ...options }; if (responseFormat === 'compact') { @@ -677,10 +624,7 @@ class CubeApi { const patched = query.map((q) => this.patchQueryInternal(q, 'compact')); return [patched as unknown as QueryType, options]; } else { - const patched = this.patchQueryInternal( - query as DeeplyReadonly, - 'compact' - ); + const patched = this.patchQueryInternal(query as DeeplyReadonly, 'compact'); return [patched as QueryType, options]; } } @@ -736,34 +680,21 @@ class CubeApi { ) as UnsubscribeObj; } - public sql( - query: DeeplyReadonly, - options?: LoadMethodOptions - ): Promise; + public sql(query: DeeplyReadonly, options?: LoadMethodOptions): Promise; - public sql( - query: DeeplyReadonly, - options?: LoadMethodOptions, - callback?: LoadMethodCallback - ): UnsubscribeObj; + public sql(query: DeeplyReadonly, options?: LoadMethodOptions, callback?: LoadMethodCallback): UnsubscribeObj; /** * Get generated SQL string for the given `query`. */ - public sql( - query: DeeplyReadonly, - options?: LoadMethodOptions, - callback?: LoadMethodCallback - ): Promise | UnsubscribeObj { + public sql(query: DeeplyReadonly, options?: LoadMethodOptions, callback?: LoadMethodCallback): Promise | UnsubscribeObj { return this.loadMethod( () => this.request('sql', { query, signal: options?.signal, baseRequestId: options?.baseRequestId, }), - (response: any) => (Array.isArray(response) - ? response.map((body) => new SqlQuery(body)) - : new SqlQuery(response)), + (response: any) => (Array.isArray(response) ? response.map((body) => new SqlQuery(body)) : new SqlQuery(response)), options, callback ); @@ -771,18 +702,12 @@ class CubeApi { public meta(options?: MetaMethodOptions): Promise; - public meta( - options?: MetaMethodOptions, - callback?: LoadMethodCallback - ): UnsubscribeObj; + public meta(options?: MetaMethodOptions, callback?: LoadMethodCallback): UnsubscribeObj; /** * Get meta description of cubes available for querying. */ - public meta( - options?: MetaMethodOptions, - callback?: LoadMethodCallback - ): Promise | UnsubscribeObj { + public meta(options?: MetaMethodOptions, callback?: LoadMethodCallback): Promise | UnsubscribeObj { return this.loadMethod( () => this.request('meta', { signal: options?.signal, @@ -795,25 +720,14 @@ class CubeApi { ); } - public dryRun( - query: DeeplyReadonly, - options?: LoadMethodOptions - ): Promise; + public dryRun(query: DeeplyReadonly, options?: LoadMethodOptions): Promise; - public dryRun( - query: DeeplyReadonly, - options: LoadMethodOptions, - callback?: LoadMethodCallback - ): UnsubscribeObj; + public dryRun(query: DeeplyReadonly, options: LoadMethodOptions, callback?: LoadMethodCallback): UnsubscribeObj; /** * Get query related meta without query execution */ - public dryRun( - query: DeeplyReadonly, - options?: LoadMethodOptions, - callback?: LoadMethodCallback - ): Promise | UnsubscribeObj { + public dryRun(query: DeeplyReadonly, options?: LoadMethodOptions, callback?: LoadMethodCallback): Promise | UnsubscribeObj { return this.loadMethod( () => this.request('dry-run', { query, @@ -826,25 +740,14 @@ class CubeApi { ); } - public cubeSql( - sqlQuery: string, - options?: CubeSqlOptions - ): Promise; + public cubeSql(sqlQuery: string, options?: CubeSqlOptions): Promise; - public cubeSql( - sqlQuery: string, - options?: CubeSqlOptions, - callback?: LoadMethodCallback - ): UnsubscribeObj; + public cubeSql(sqlQuery: string, options?: CubeSqlOptions, callback?: LoadMethodCallback): UnsubscribeObj; /** * Execute a Cube SQL query against Cube SQL interface and return the results. */ - public cubeSql( - sqlQuery: string, - options?: CubeSqlOptions, - callback?: LoadMethodCallback - ): Promise | UnsubscribeObj { + public cubeSql(sqlQuery: string, options?: CubeSqlOptions, callback?: LoadMethodCallback): Promise | UnsubscribeObj { return this.loadMethod( () => { const cubesqlParams: Record = { @@ -890,9 +793,7 @@ class CubeApi { .filter((d: string) => d.trim().length) .map((d: string) => JSON.parse(d).data) .reduce((a: any, b: any) => a.concat(b), []), - ...(parsedSchema.lastRefreshTime - ? { lastRefreshTime: parsedSchema.lastRefreshTime } - : {}), + ...(parsedSchema.lastRefreshTime ? { lastRefreshTime: parsedSchema.lastRefreshTime } : {}), }; } catch (err) { throw new Error(response.error); @@ -907,10 +808,7 @@ class CubeApi { * Execute a Cube SQL query against Cube SQL interface and return streaming results as an async generator. * The server returns JSONL (JSON Lines) format with schema first, then data rows. */ - public async* cubeSqlStream( - sqlQuery: string, - options?: CubeSqlOptions - ): AsyncGenerator { + public async* cubeSqlStream(sqlQuery: string, options?: CubeSqlOptions): AsyncGenerator { if (!this.transport.requestStream) { throw new Error('Transport does not support streaming'); } @@ -922,8 +820,8 @@ class CubeApi { baseRequestId: uuidv4(), params: { query: sqlQuery, - cache: options?.cache, - }, + cache: options?.cache + } }); const decoder = new TextDecoder(); @@ -947,25 +845,23 @@ class CubeApi { yield { type: 'schema' as const, schema: parsed.schema, - ...(parsed.lastRefreshTime - ? { lastRefreshTime: parsed.lastRefreshTime } - : {}), + ...(parsed.lastRefreshTime ? { lastRefreshTime: parsed.lastRefreshTime } : {}), }; } else if (parsed.data) { yield { type: 'data' as const, - data: parsed.data, + data: parsed.data }; } else if (parsed.error) { yield { type: 'error' as const, - error: parsed.error, + error: parsed.error }; } } catch (parseError) { yield { type: 'error' as const, - error: `Failed to parse JSON line: ${line}`, + error: `Failed to parse JSON line: ${line}` }; } } @@ -980,25 +876,23 @@ class CubeApi { yield { type: 'schema' as const, schema: parsed.schema, - ...(parsed.lastRefreshTime - ? { lastRefreshTime: parsed.lastRefreshTime } - : {}), + ...(parsed.lastRefreshTime ? { lastRefreshTime: parsed.lastRefreshTime } : {}), }; } else if (parsed.data) { yield { type: 'data' as const, - data: parsed.data, + data: parsed.data }; } else if (parsed.error) { yield { type: 'error' as const, - error: parsed.error, + error: parsed.error }; } } catch (parseError) { yield { type: 'error' as const, - error: `Failed to parse remaining JSON: ${buffer}`, + error: `Failed to parse remaining JSON: ${buffer}` }; } } @@ -1015,10 +909,7 @@ class CubeApi { } } -export default ( - apiToken: string | (() => Promise), - options: CubeApiOptions -) => new CubeApi(apiToken, options); +export default (apiToken: string | (() => Promise), options: CubeApiOptions) => new CubeApi(apiToken, options); export { CubeApi }; export { default as Meta } from './Meta'; From 004c7768bd762dd5057efa47169fd3a450fa0c46 Mon Sep 17 00:00:00 2001 From: Jake Date: Sun, 5 Apr 2026 10:13:25 -0400 Subject: [PATCH 5/7] feat(cubejs-client-core): enhance meta API with extended response types --- packages/cubejs-client-core/src/Meta.ts | 53 +++++++++------- packages/cubejs-client-core/src/index.ts | 20 ++++-- packages/cubejs-client-core/src/types.ts | 81 +++++++++++++++++++++--- 3 files changed, 119 insertions(+), 35 deletions(-) diff --git a/packages/cubejs-client-core/src/Meta.ts b/packages/cubejs-client-core/src/Meta.ts index f440e4afc4aae..ec9399a1ea7e8 100644 --- a/packages/cubejs-client-core/src/Meta.ts +++ b/packages/cubejs-client-core/src/Meta.ts @@ -1,13 +1,15 @@ import { unnest, fromPairs } from 'ramda'; import { - Cube, - CubesMap, + CubeExtended, + CubePlain, + CubesMapFromMeta, MemberType, + MetaCubeOf, MetaResponse, + MetaResponseExtended, TCubeMeasure, TCubeDimension, - TCubeMember, - TCubeMemberByType, + TCubeMemberByTypeForMeta, Query, FilterOperator, TCubeSegment, @@ -23,13 +25,16 @@ export interface CubeMemberWrapper { members: T[]; } -export type AggregatedMembers = { - measures: CubeMemberWrapper[]; - dimensions: CubeMemberWrapper[]; - segments: CubeMemberWrapper[]; - timeDimensions: CubeMemberWrapper[]; +export type AggregatedMembersFor = { + measures: CubeMemberWrapper[]; + dimensions: CubeMemberWrapper[]; + segments: CubeMemberWrapper[]; + timeDimensions: CubeMemberWrapper[]; }; +/** Plain-meta variant of {@link AggregatedMembersFor} (alias for `AggregatedMembersFor`). */ +export type AggregatedMembers = AggregatedMembersFor; + const memberMap = (memberArray: any[]) => fromPairs( memberArray.map((m) => [m.name, m]) ); @@ -72,23 +77,23 @@ const operators = { /** * Contains information about available cubes and it's members. */ -export default class Meta { +export default class Meta { /** * Raw meta response */ - public readonly meta: MetaResponse; + public readonly meta: T; /** * An array of all available cubes with their members */ - public readonly cubes: Cube[]; + public readonly cubes: T['cubes']; /** * A map of all cubes where the key is a cube name */ - public readonly cubesMap: CubesMap; + public readonly cubesMap: CubesMapFromMeta; - public constructor(metaResponse: MetaResponse) { + public constructor(metaResponse: T) { this.meta = metaResponse; const { cubes } = this.meta; this.cubes = cubes; @@ -110,15 +115,19 @@ export default class Meta { * @param _query - context query to provide filtering of members available to add to this query * @param memberType */ - public membersForQuery(_query: DeeplyReadonly | null, memberType: MemberType): (TCubeMeasure | TCubeDimension | TCubeMember | TCubeSegment)[] { + public membersForQuery(_query: DeeplyReadonly | null, memberType: MemberType): ( + TCubeMemberByTypeForMeta, 'measures'> + | TCubeMemberByTypeForMeta, 'dimensions'> + | TCubeMemberByTypeForMeta, 'segments'> + )[] { return unnest(this.cubes.map((c) => c[memberType])) .sort((a, b) => (a.title > b.title ? 1 : -1)); } - public membersGroupedByCube() { + public membersGroupedByCube(): AggregatedMembersFor> { const memberKeys = ['measures', 'dimensions', 'segments', 'timeDimensions']; - return this.cubes.reduce( + return this.cubes.reduce>>( (memo, cube) => { memberKeys.forEach((key) => { let members: TCubeMeasure[] | TCubeDimension[] | TCubeSegment[] = []; @@ -157,7 +166,7 @@ export default class Meta { dimensions: [], segments: [], timeDimensions: [], - } as AggregatedMembers + } as AggregatedMembersFor> ); } @@ -178,10 +187,10 @@ export default class Meta { * @param memberType * @return An object containing meta information about member */ - public resolveMember( + public resolveMember( memberName: string, - memberType: T | T[] - ): NotFoundMember | TCubeMemberByType { + memberType: M | M[] + ): NotFoundMember | TCubeMemberByTypeForMeta, M> { const [cube] = memberName.split('.'); if (!this.cubesMap[cube]) { @@ -200,7 +209,7 @@ export default class Meta { }; } - return member as TCubeMemberByType; + return member as TCubeMemberByTypeForMeta, M>; } public defaultTimeDimensionNameFor(memberName: string): string | null | undefined { diff --git a/packages/cubejs-client-core/src/index.ts b/packages/cubejs-client-core/src/index.ts index 2b5836cb01bd9..86818915db7b6 100644 --- a/packages/cubejs-client-core/src/index.ts +++ b/packages/cubejs-client-core/src/index.ts @@ -12,6 +12,7 @@ import { LoadResponse, MeasureFormat, MetaResponse, + MetaResponseExtended, PivotQuery, ProgressResponse, Query, @@ -700,21 +701,32 @@ class CubeApi { ); } - public meta(options?: MetaMethodOptions): Promise; + public meta(options?: MetaMethodOptions & { extended?: false }): Promise>; - public meta(options?: MetaMethodOptions, callback?: LoadMethodCallback): UnsubscribeObj; + public meta(options: MetaMethodOptions & { extended: true }): Promise>; + + public meta(options?: MetaMethodOptions & { extended?: false }, callback?: LoadMethodCallback>): UnsubscribeObj; + + public meta(options: MetaMethodOptions & { extended: true }, callback?: LoadMethodCallback>): UnsubscribeObj; /** * Get meta description of cubes available for querying. */ - public meta(options?: MetaMethodOptions, callback?: LoadMethodCallback): Promise | UnsubscribeObj { + public meta( + options?: MetaMethodOptions, + callback?: LoadMethodCallback> | LoadMethodCallback> + ): Promise> | Promise> | UnsubscribeObj { return this.loadMethod( () => this.request('meta', { signal: options?.signal, baseRequestId: options?.baseRequestId, ...(options?.extended === true ? { extended: true } : {}), }), - (body: MetaResponse) => new Meta(body), + (body: MetaResponse | MetaResponseExtended) => ( + options?.extended === true + ? new Meta(body as MetaResponseExtended) + : new Meta(body as MetaResponse) + ), options, callback ); diff --git a/packages/cubejs-client-core/src/types.ts b/packages/cubejs-client-core/src/types.ts index 9a190e300dc68..9ed74b9782a68 100644 --- a/packages/cubejs-client-core/src/types.ts +++ b/packages/cubejs-client-core/src/types.ts @@ -406,6 +406,8 @@ export type TCubeMeasure = BaseCubeMember & { filters?: TCubeMeasureFilterMeta[]; }; +export type TCubeMeasurePlain = Omit; + export type CubeTimeDimensionGranularity = { name: string; title: string; @@ -442,10 +444,16 @@ export type TCubeDimension = (BaseCubeDimension & { type: Exclude }) | CubeTimeDimension; +export type TCubeDimensionPlain = + | (Omit & { type: Exclude }) + | Omit; + export type TCubeSegment = Omit & { sql?: string; }; +export type TCubeSegmentPlain = Omit; + export type NotFoundMember = { title: string; error: string; @@ -510,13 +518,10 @@ export type DryRunResponse = { transformedQueries: TransformedQuery[]; }; -export type Cube = { +type CubeBaseFields = { name: string; title: string; description?: string; - measures: TCubeMeasure[]; - dimensions: TCubeDimension[]; - segments: TCubeSegment[]; folders: TCubeFolder[]; nestedFolders: TCubeNestedFolder[]; hierarchies: TCubeHierarchy[]; @@ -528,6 +533,20 @@ export type Cube = { isVisible?: boolean; public?: boolean; meta?: any; +}; + +/** + * Cube description from plain `/v1/meta` (no `extended` query param). + * Extended schema fields (joins, SQL snippets, pre-aggregations, etc.) are not present on this type. + */ +export type CubePlain = CubeBaseFields & { + measures: TCubeMeasurePlain[]; + dimensions: TCubeDimensionPlain[]; + segments: TCubeSegmentPlain[]; +}; + +/** Cube-level fields only returned when `extended` is requested on `/v1/meta`. */ +export type CubeExtendedFields = { joins?: TCubeJoin[]; sql?: string; extends?: string; @@ -536,10 +555,23 @@ export type Cube = { preAggregations?: TCubePreAggregationMeta[]; }; +/** + * Cube description from `/v1/meta?extended=true` (extended meta). + * Includes optional cube-level and member-level fields from the extended meta transform. + */ +export type CubeExtended = CubeBaseFields & CubeExtendedFields & { + measures: TCubeMeasure[]; + dimensions: TCubeDimension[]; + segments: TCubeSegment[]; +}; + +/** Alias for {@link CubePlain} (default `/v1/meta` response cube shape). */ +export type Cube = CubePlain; + export type CubeMap = { - measures: Record; - dimensions: Record; - segments: Record; + measures: Record; + dimensions: Record; + segments: Record; }; export type CubesMap = Record< @@ -547,10 +579,41 @@ export type CubesMap = Record< CubeMap >; +export type CubeMapExtended = { + measures: Record; + dimensions: Record; + segments: Record; +}; + +export type CubesMapExtended = Record; + +/** Response body for `GET /v1/meta` without extended meta. */ export type MetaResponse = { - cubes: Cube[]; + cubes: CubePlain[]; }; +/** Response body for `GET /v1/meta?extended=true`. */ +export type MetaResponseExtended = { + cubes: CubeExtended[]; +}; + +export type MetaCubeOf = T['cubes'][number]; + +export type CubesMapFromMeta = Record< + string, + { + measures: Record['measures'][number]>; + dimensions: Record['dimensions'][number]>; + segments: Record['segments'][number]>; + } +>; + +export type TCubeMemberByTypeForMeta = + T extends 'measures' ? C['measures'][number] + : T extends 'dimensions' ? C['dimensions'][number] + : T extends 'segments' ? C['segments'][number] + : never; + export type FilterOperator = { name: string; title: string; @@ -561,7 +624,7 @@ export type TSourceAxis = 'x' | 'y'; export type ChartType = 'line' | 'bar' | 'table' | 'area' | 'number' | 'pie'; export type TDefaultHeuristicsOptions = { - meta: Meta; + meta: Meta; sessionGranularity?: TimeDimensionGranularity; }; From a36b05d883c0b33a1d5a486759dca6b834f23fa2 Mon Sep 17 00:00:00 2001 From: Jake Date: Sun, 5 Apr 2026 10:22:03 -0400 Subject: [PATCH 6/7] test(cubejs-client-core): add unit tests for CubeApi meta method with extended options --- .../cubejs-client-core/test/CubeApi.test.ts | 116 ++++++++++++++++++ 1 file changed, 116 insertions(+) diff --git a/packages/cubejs-client-core/test/CubeApi.test.ts b/packages/cubejs-client-core/test/CubeApi.test.ts index b11c0ce9125b5..4eb9553b54778 100644 --- a/packages/cubejs-client-core/test/CubeApi.test.ts +++ b/packages/cubejs-client-core/test/CubeApi.test.ts @@ -774,3 +774,119 @@ describe('CubeApi Mutex Cancellation', () => { ).rejects.toThrow(RequestError); }); }); + +describe('CubeApi meta', () => { + afterEach(() => { + jest.clearAllMocks(); + jest.restoreAllMocks(); + }); + + const minimalMetaResponseText = JSON.stringify({ + cubes: [{ + name: 'Orders', + title: 'Orders', + measures: [{ + name: 'count', + title: 'Count', + shortTitle: 'Count', + type: 'number' + }], + dimensions: [{ + name: 'status', + title: 'Status', + type: 'string' + }], + segments: [] + }] + }); + + function mockMetaRequest(): jest.SpyInstance { + return jest.spyOn(HttpTransport.prototype, 'request').mockImplementation(() => ({ + subscribe: (cb) => Promise.resolve(cb({ + status: 200, + text: () => Promise.resolve(minimalMetaResponseText), + json: () => Promise.resolve(JSON.parse(minimalMetaResponseText)) + } as any, + async () => undefined as any)) + })); + } + + describe('when no extended option is provided', () => { + test('omits `extended` from the meta request params', async () => { + const requestSpy = mockMetaRequest(); + + const cubeApi = new CubeApi('token', { + apiUrl: 'http://localhost:4000/cubejs-api/v1' + }); + + await cubeApi.meta(); + + expect(requestSpy).toHaveBeenCalledWith('meta', expect.any(Object)); + expect(requestSpy.mock.calls[0]?.[1]?.extended).toBeUndefined(); + }); + + test('omits `extended` when options is an empty object', async () => { + const requestSpy = mockMetaRequest(); + + const cubeApi = new CubeApi('token', { + apiUrl: 'http://localhost:4000/cubejs-api/v1' + }); + + await cubeApi.meta({}); + + expect(requestSpy.mock.calls[0]?.[1]?.extended).toBeUndefined(); + }); + + test('omits `extended` when `extended` is false (presence on the wire would still enable extended meta)', async () => { + const requestSpy = mockMetaRequest(); + + const cubeApi = new CubeApi('token', { + apiUrl: 'http://localhost:4000/cubejs-api/v1' + }); + + await cubeApi.meta({ extended: false }); + + expect(requestSpy.mock.calls[0]?.[1]?.extended).toBeUndefined(); + }); + }); + + describe('when extended option is provided', () => { + test('passes `extended: true` in the meta request params', async () => { + const requestSpy = mockMetaRequest(); + + const cubeApi = new CubeApi('token', { + apiUrl: 'http://localhost:4000/cubejs-api/v1' + }); + + await cubeApi.meta({ extended: true }); + + expect(requestSpy).toHaveBeenCalledWith( + 'meta', + expect.objectContaining({ extended: true }) + ); + expect(requestSpy.mock.calls[0]?.[1]?.extended).toBe(true); + }); + + test('still passes signal and baseRequestId together with `extended: true`', async () => { + const controller = new AbortController(); + const { signal } = controller; + const baseRequestId = 'meta-extended-req'; + + const requestSpy = mockMetaRequest(); + + const cubeApi = new CubeApi('token', { + apiUrl: 'http://localhost:4000/cubejs-api/v1' + }); + + await cubeApi.meta({ extended: true, signal, baseRequestId }); + + expect(requestSpy.mock.calls[0]?.[1]).toEqual( + expect.objectContaining({ + extended: true, + signal, + baseRequestId + }) + ); + }); + }); +}); From 6a50aed8d86536b473ca69d093a78286e16b904e Mon Sep 17 00:00:00 2001 From: Jake Date: Sun, 5 Apr 2026 10:28:50 -0400 Subject: [PATCH 7/7] docs(cubejs-client-core): update meta method options and add extended meta options description --- .../reference/cubejs-client-core.mdx | 14 ++++++++++++-- 1 file changed, 12 insertions(+), 2 deletions(-) diff --git a/docs/content/product/apis-integrations/javascript-sdk/reference/cubejs-client-core.mdx b/docs/content/product/apis-integrations/javascript-sdk/reference/cubejs-client-core.mdx index 6e17ad00c51ce..181af13319896 100644 --- a/docs/content/product/apis-integrations/javascript-sdk/reference/cubejs-client-core.mdx +++ b/docs/content/product/apis-integrations/javascript-sdk/reference/cubejs-client-core.mdx @@ -153,12 +153,14 @@ callback? | [LoadMethodCallback](#types-load-method-callback)‹[ResultSet](#res ### `meta` -> **meta**(**options?**: [LoadMethodOptions](#types-load-method-options)): *Promise‹[Meta](#meta)›* +> **meta**(**options?**: [MetaMethodOptions](#types-meta-method-options)): *Promise‹[Meta](#meta)›* -> **meta**(**options?**: [LoadMethodOptions](#types-load-method-options), **callback?**: [LoadMethodCallback](#types-load-method-callback)‹[Meta](#meta)›): *void* +> **meta**(**options?**: [MetaMethodOptions](#types-meta-method-options), **callback?**: [LoadMethodCallback](#types-load-method-callback)‹[Meta](#meta)›): *void* Get meta description of cubes available for querying. +Pass **`extended: true`** to request [extended `/v1/meta`](/product/apis-integrations/rest-api/reference#base_pathv1meta) (joins, SQL snippets, pre-aggregations, and related fields). The client only adds the `extended` query parameter when this option is `true`; omit it otherwise. Do not rely on `extended: false` on the wire—the API treats the presence of any `extended` parameter as extended mode. + ### `sql` > **sql**(**query**: [Query](#types-query) | [Query](#types-query)[], **options?**: [LoadMethodOptions](#types-load-method-options)): *Promise‹[SqlQuery](#sql-query)›* @@ -817,6 +819,14 @@ Name | Type | Optional? | Description | `signal` | `AbortSignal` | ✅ Yes | AbortSignal to cancel the request. This allows you to manually abort requests using AbortController. | `baseRequestId` | `string` | ✅ Yes | Base request ID ([`spanId`](https://cube.dev/docs/product/apis-integrations/core-data-apis/rest-api#request-span-annotation)) to be used for the request. If not provided a random request ID will be generated. | +### `MetaMethodOptions` + +Extends [LoadMethodOptions](#types-load-method-options). + +Name | Type | Optional? | Description | +------ | ------ | ------ | --- | +`extended` | `boolean` | ✅ Yes | Pass **`true`** to load extended meta. Omit when not needed (the client does not send `extended=false`). | + ### `LoadResponse` Name | Type |