From d1d4d1df0ff0c3c20acbb6af8d6ee6ca6d863ccf Mon Sep 17 00:00:00 2001 From: Artyom Keydunov Date: Mon, 4 May 2026 20:18:16 -0700 Subject: [PATCH 1/3] feat(api-gateway): expose query limit settings on /v1/meta Add a `settings` object to the `/v1/meta` response containing `defaultLimit` (CUBEJS_DB_QUERY_DEFAULT_LIMIT) and `maxLimit` (CUBEJS_DB_QUERY_LIMIT) so clients can render correct UI defaults and validate queries client-side without knowing about Cube env vars. `defaultLimit` is clamped to `maxLimit` to mirror the runtime behaviour in query.js#normalizeQuery. Co-authored-by: Cursor --- packages/cubejs-api-gateway/openspec.yml | 15 +++++++++++ packages/cubejs-api-gateway/src/gateway.ts | 27 +++++++++++++++++-- .../cubejs-api-gateway/src/types/request.ts | 8 +++++- .../cubejs-api-gateway/test/index.test.ts | 27 +++++++++++++++++++ packages/cubejs-client-core/src/types.ts | 6 +++++ 5 files changed, 80 insertions(+), 3 deletions(-) diff --git a/packages/cubejs-api-gateway/openspec.yml b/packages/cubejs-api-gateway/openspec.yml index 0a5c71ddddc04..7e460ce7704c5 100644 --- a/packages/cubejs-api-gateway/openspec.yml +++ b/packages/cubejs-api-gateway/openspec.yml @@ -381,6 +381,21 @@ components: $ref: "#/components/schemas/V1CubeMeta" compilerId: type: "string" + settings: + $ref: "#/components/schemas/V1MetaSettings" + V1MetaSettings: + type: "object" + description: "Server-wide settings exposed alongside meta. Useful for clients that need to render UI defaults or validate queries before sending them." + required: + - defaultLimit + - maxLimit + properties: + defaultLimit: + type: "integer" + description: "Default row limit applied to incoming queries when `limit` is not specified. Sourced from `CUBEJS_DB_QUERY_DEFAULT_LIMIT`, clamped to `maxLimit`." + maxLimit: + type: "integer" + description: "Maximum row limit allowed for an incoming query. Sourced from `CUBEJS_DB_QUERY_LIMIT`. Requests with `limit` above this value are rejected." V1LoadResultAnnotation: type: "object" required: diff --git a/packages/cubejs-api-gateway/src/gateway.ts b/packages/cubejs-api-gateway/src/gateway.ts index 08c462c00d178..567b4d551ba9e 100644 --- a/packages/cubejs-api-gateway/src/gateway.ts +++ b/packages/cubejs-api-gateway/src/gateway.ts @@ -673,6 +673,21 @@ class ApiGateway { })).filter(cube => cube.config.measures?.length || cube.config.dimensions?.length || cube.config.segments?.length); } + /** + * Build the `settings` object returned by `/v1/meta`. Exposes server-wide + * query limits so clients can render correct defaults and validation + * without having to know about Cube env vars. + * + * `defaultLimit` is clamped to `maxLimit` to mirror the behaviour in + * `query.js#normalizeQuery`, where `dbQueryDefaultLimit` is reduced to + * `dbQueryLimit` if it ever exceeds it. + */ + protected getMetaSettings(): { defaultLimit: number, maxLimit: number } { + const maxLimit = getEnv('dbQueryLimit'); + const defaultLimit = Math.min(getEnv('dbQueryDefaultLimit'), maxLimit); + return { defaultLimit, maxLimit }; + } + public async meta({ context, res, includeCompilerId, onlyCompilerId, onlyViews }: { context: RequestContext, res: MetaResponseResultFn, @@ -709,7 +724,15 @@ class ApiGateway { views: group.views.filter((v: string) => visibleCubeNames.has(v)), })) .filter(group => group.views.length > 0); - const response: { cubes: any[], viewGroups?: any[], compilerId?: string } = { cubes }; + const response: { + cubes: any[], + viewGroups?: any[], + compilerId?: string, + settings: { defaultLimit: number, maxLimit: number }, + } = { + cubes, + settings: this.getMetaSettings(), + }; if (viewGroups.length > 0) { response.viewGroups = viewGroups; } @@ -763,7 +786,7 @@ class ApiGateway { preAggregations: transformPreAggregations(cubeDefinitions[cube.name]?.preAggregations), })); - await res({ cubes }); + await res({ cubes, settings: this.getMetaSettings() }); } catch (e: any) { this.handleError({ e, diff --git a/packages/cubejs-api-gateway/src/types/request.ts b/packages/cubejs-api-gateway/src/types/request.ts index 8478d008354a2..dd4b2db985e54 100644 --- a/packages/cubejs-api-gateway/src/types/request.ts +++ b/packages/cubejs-api-gateway/src/types/request.ts @@ -95,7 +95,13 @@ type ErrorResponse = { error: string, }; -type MetaResponse = { cubes: any[], viewGroups?: any[], compilerId?: string }; +type MetaResponseSettings = { defaultLimit: number, maxLimit: number }; +type MetaResponse = { + cubes: any[], + viewGroups?: any[], + compilerId?: string, + settings?: MetaResponseSettings, +}; type MetaResponseResultFn = (message: MetaResponse | ErrorResponse) => void; /** diff --git a/packages/cubejs-api-gateway/test/index.test.ts b/packages/cubejs-api-gateway/test/index.test.ts index 43e6efed80a7c..9bd86dd31f350 100644 --- a/packages/cubejs-api-gateway/test/index.test.ts +++ b/packages/cubejs-api-gateway/test/index.test.ts @@ -686,6 +686,33 @@ describe('API Gateway', () => { expect(res.body.cubes[0]?.segments.find(segment => segment.name === 'Foo.quux').description).toBe('segment from compilerApi mock'); }); + test('meta endpoint exposes query limit settings', async () => { + const { app } = await createApiGateway(); + + const res = await request(app) + .get('/cubejs-api/v1/meta') + .set('Authorization', 'eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.e30.t-IDcSemACt8x4iTMCda8Yhe3iZaWbvV5XKSTbuAn0M') + .expect(200); + + expect(res.body).toHaveProperty('settings'); + expect(typeof res.body.settings.defaultLimit).toBe('number'); + expect(typeof res.body.settings.maxLimit).toBe('number'); + expect(res.body.settings.defaultLimit).toBeLessThanOrEqual(res.body.settings.maxLimit); + }); + + test('meta endpoint extended exposes query limit settings', async () => { + const { app } = await createApiGateway(); + + const res = await request(app) + .get('/cubejs-api/v1/meta?extended') + .set('Authorization', 'eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.e30.t-IDcSemACt8x4iTMCda8Yhe3iZaWbvV5XKSTbuAn0M') + .expect(200); + + expect(res.body).toHaveProperty('settings'); + expect(typeof res.body.settings.defaultLimit).toBe('number'); + expect(typeof res.body.settings.maxLimit).toBe('number'); + }); + test('meta endpoint returns view groups', async () => { const { app } = await createApiGateway(); diff --git a/packages/cubejs-client-core/src/types.ts b/packages/cubejs-client-core/src/types.ts index 876533d97d471..e06f7182d4e9b 100644 --- a/packages/cubejs-client-core/src/types.ts +++ b/packages/cubejs-client-core/src/types.ts @@ -548,9 +548,15 @@ export type ViewGroup = { views: string[]; }; +export type MetaResponseSettings = { + defaultLimit: number; + maxLimit: number; +}; + export type MetaResponse = { cubes: Cube[]; viewGroups?: ViewGroup[]; + settings?: MetaResponseSettings; }; export type FilterOperator = { From e32e22e93030e3c96f841d1ecd81b9828a40d203 Mon Sep 17 00:00:00 2001 From: Artyom Keydunov Date: Tue, 5 May 2026 15:14:43 -0700 Subject: [PATCH 2/3] refactor(api-gateway): gate meta `settings` behind includeSettings flag Mirror the `extended` query parameter pattern: the new `settings` object on `/v1/meta` is now opt-in and only returned when the `includeSettings` query parameter is present. Keeps the default response minimal and avoids leaking server config to clients that don't ask for it. Co-authored-by: Cursor --- packages/cubejs-api-gateway/openspec.yml | 9 ++++++- packages/cubejs-api-gateway/src/gateway.ts | 24 +++++++++++------ .../cubejs-api-gateway/test/index.test.ts | 26 +++++++++++++++++-- 3 files changed, 48 insertions(+), 11 deletions(-) diff --git a/packages/cubejs-api-gateway/openspec.yml b/packages/cubejs-api-gateway/openspec.yml index 7e460ce7704c5..f241bc052024d 100644 --- a/packages/cubejs-api-gateway/openspec.yml +++ b/packages/cubejs-api-gateway/openspec.yml @@ -22,6 +22,12 @@ paths: # type: boolean # default: false # description: When set to "true", only views (entries with type="view") and their viewGroups are returned, excluding cubes + # - in: query + # name: includeSettings + # required: false + # schema: + # type: boolean + # description: When this parameter is present (presence-only, value is ignored), the response includes a `settings` object with server-wide query limits. Omitted by default to keep the response minimal. description: "" operationId: "metaV1" responses: @@ -383,9 +389,10 @@ components: type: "string" settings: $ref: "#/components/schemas/V1MetaSettings" + description: "Only present when the request includes the `includeSettings` query parameter." V1MetaSettings: type: "object" - description: "Server-wide settings exposed alongside meta. Useful for clients that need to render UI defaults or validate queries before sending them." + description: "Server-wide settings returned alongside meta when `includeSettings` is set. Useful for clients that need to render UI defaults or validate queries before sending them." required: - defaultLimit - maxLimit diff --git a/packages/cubejs-api-gateway/src/gateway.ts b/packages/cubejs-api-gateway/src/gateway.ts index 567b4d551ba9e..88ecc28f21088 100644 --- a/packages/cubejs-api-gateway/src/gateway.ts +++ b/packages/cubejs-api-gateway/src/gateway.ts @@ -470,17 +470,20 @@ class ApiGateway { userMiddlewares, userAsyncHandler(async (req, res) => { const onlyViews = req.query.onlyViews === 'true'; + const includeSettings = 'includeSettings' in req.query; if ('extended' in req.query) { await this.metaExtended({ context: req.context, res: this.resToResultFn(res), onlyViews, + includeSettings, }); } else { await this.meta({ context: req.context, res: this.resToResultFn(res), onlyViews, + includeSettings, }); } }) @@ -688,12 +691,13 @@ class ApiGateway { return { defaultLimit, maxLimit }; } - public async meta({ context, res, includeCompilerId, onlyCompilerId, onlyViews }: { + public async meta({ context, res, includeCompilerId, onlyCompilerId, onlyViews, includeSettings }: { context: RequestContext, res: MetaResponseResultFn, includeCompilerId?: boolean, onlyCompilerId?: boolean, onlyViews?: boolean, + includeSettings?: boolean, }) { const requestStarted = new Date(); @@ -728,17 +732,17 @@ class ApiGateway { cubes: any[], viewGroups?: any[], compilerId?: string, - settings: { defaultLimit: number, maxLimit: number }, - } = { - cubes, - settings: this.getMetaSettings(), - }; + settings?: { defaultLimit: number, maxLimit: number }, + } = { cubes }; if (viewGroups.length > 0) { response.viewGroups = viewGroups; } if (includeCompilerId) { response.compilerId = metaConfig.compilerId; } + if (includeSettings) { + response.settings = this.getMetaSettings(); + } res(response); } catch (e: any) { this.handleError({ @@ -751,10 +755,11 @@ class ApiGateway { } } - public async metaExtended({ context, res, onlyViews }: { + public async metaExtended({ context, res, onlyViews, includeSettings }: { context: ExtendedRequestContext, res: ResponseResultFn, onlyViews?: boolean, + includeSettings?: boolean, }) { const requestStarted = new Date(); @@ -786,7 +791,10 @@ class ApiGateway { preAggregations: transformPreAggregations(cubeDefinitions[cube.name]?.preAggregations), })); - await res({ cubes, settings: this.getMetaSettings() }); + await res({ + cubes, + ...(includeSettings ? { settings: this.getMetaSettings() } : {}), + }); } catch (e: any) { this.handleError({ e, diff --git a/packages/cubejs-api-gateway/test/index.test.ts b/packages/cubejs-api-gateway/test/index.test.ts index 9bd86dd31f350..1f8901d84352f 100644 --- a/packages/cubejs-api-gateway/test/index.test.ts +++ b/packages/cubejs-api-gateway/test/index.test.ts @@ -686,7 +686,7 @@ describe('API Gateway', () => { expect(res.body.cubes[0]?.segments.find(segment => segment.name === 'Foo.quux').description).toBe('segment from compilerApi mock'); }); - test('meta endpoint exposes query limit settings', async () => { + test('meta endpoint omits settings by default', async () => { const { app } = await createApiGateway(); const res = await request(app) @@ -694,13 +694,24 @@ describe('API Gateway', () => { .set('Authorization', 'eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.e30.t-IDcSemACt8x4iTMCda8Yhe3iZaWbvV5XKSTbuAn0M') .expect(200); + expect(res.body).not.toHaveProperty('settings'); + }); + + test('meta endpoint exposes query limit settings when includeSettings is set', async () => { + const { app } = await createApiGateway(); + + const res = await request(app) + .get('/cubejs-api/v1/meta?includeSettings') + .set('Authorization', 'eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.e30.t-IDcSemACt8x4iTMCda8Yhe3iZaWbvV5XKSTbuAn0M') + .expect(200); + expect(res.body).toHaveProperty('settings'); expect(typeof res.body.settings.defaultLimit).toBe('number'); expect(typeof res.body.settings.maxLimit).toBe('number'); expect(res.body.settings.defaultLimit).toBeLessThanOrEqual(res.body.settings.maxLimit); }); - test('meta endpoint extended exposes query limit settings', async () => { + test('meta endpoint extended omits settings by default', async () => { const { app } = await createApiGateway(); const res = await request(app) @@ -708,6 +719,17 @@ describe('API Gateway', () => { .set('Authorization', 'eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.e30.t-IDcSemACt8x4iTMCda8Yhe3iZaWbvV5XKSTbuAn0M') .expect(200); + expect(res.body).not.toHaveProperty('settings'); + }); + + test('meta endpoint extended exposes query limit settings when includeSettings is set', async () => { + const { app } = await createApiGateway(); + + const res = await request(app) + .get('/cubejs-api/v1/meta?extended&includeSettings') + .set('Authorization', 'eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.e30.t-IDcSemACt8x4iTMCda8Yhe3iZaWbvV5XKSTbuAn0M') + .expect(200); + expect(res.body).toHaveProperty('settings'); expect(typeof res.body.settings.defaultLimit).toBe('number'); expect(typeof res.body.settings.maxLimit).toBe('number'); From 15021e0ba3130c8681e3746530108222a0d6891b Mon Sep 17 00:00:00 2001 From: Artyom Keydunov Date: Tue, 5 May 2026 15:15:10 -0700 Subject: [PATCH 3/3] docs(api-gateway): note that meta settings are non-sensitive Clarify in the getMetaSettings JSDoc that this object only carries non-sensitive deployment settings (operational knobs like query limits) and is safe to expose to API clients. Co-authored-by: Cursor --- packages/cubejs-api-gateway/src/gateway.ts | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/packages/cubejs-api-gateway/src/gateway.ts b/packages/cubejs-api-gateway/src/gateway.ts index 88ecc28f21088..51b18b8b7dcf1 100644 --- a/packages/cubejs-api-gateway/src/gateway.ts +++ b/packages/cubejs-api-gateway/src/gateway.ts @@ -681,6 +681,10 @@ class ApiGateway { * query limits so clients can render correct defaults and validation * without having to know about Cube env vars. * + * These are non-sensitive deployment settings — only operational knobs + * that affect query shape (limits, defaults), never credentials, secrets, + * or anything that would let a caller infer infrastructure details. + * * `defaultLimit` is clamped to `maxLimit` to mirror the behaviour in * `query.js#normalizeQuery`, where `dbQueryDefaultLimit` is reduced to * `dbQueryLimit` if it ever exceeds it.