Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
22 changes: 22 additions & 0 deletions packages/cubejs-api-gateway/openspec.yml
Original file line number Diff line number Diff line change
Expand Up @@ -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:
Expand Down Expand Up @@ -381,6 +387,22 @@ components:
$ref: "#/components/schemas/V1CubeMeta"
compilerId:
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 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
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:
Expand Down
43 changes: 39 additions & 4 deletions packages/cubejs-api-gateway/src/gateway.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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,
});
}
})
Expand Down Expand Up @@ -673,12 +676,32 @@ class ApiGateway {
})).filter(cube => cube.config.measures?.length || cube.config.dimensions?.length || cube.config.segments?.length);
}

public async meta({ context, res, includeCompilerId, onlyCompilerId, onlyViews }: {
/**
* 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.
*
* 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.
*/
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, includeSettings }: {
context: RequestContext,
res: MetaResponseResultFn,
includeCompilerId?: boolean,
onlyCompilerId?: boolean,
onlyViews?: boolean,
includeSettings?: boolean,
}) {
const requestStarted = new Date();

Expand Down Expand Up @@ -709,13 +732,21 @@ 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 };
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({
Expand All @@ -728,10 +759,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();

Expand Down Expand Up @@ -763,7 +795,10 @@ class ApiGateway {
preAggregations: transformPreAggregations(cubeDefinitions[cube.name]?.preAggregations),
}));

await res({ cubes });
await res({
cubes,
...(includeSettings ? { settings: this.getMetaSettings() } : {}),
});
} catch (e: any) {
this.handleError({
e,
Expand Down
8 changes: 7 additions & 1 deletion packages/cubejs-api-gateway/src/types/request.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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;

/**
Expand Down
49 changes: 49 additions & 0 deletions packages/cubejs-api-gateway/test/index.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -686,6 +686,55 @@ describe('API Gateway', () => {
expect(res.body.cubes[0]?.segments.find(segment => segment.name === 'Foo.quux').description).toBe('segment from compilerApi mock');
});

test('meta endpoint omits settings by default', 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).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 omits settings by default', 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).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');
});

test('meta endpoint returns view groups', async () => {
const { app } = await createApiGateway();

Expand Down
6 changes: 6 additions & 0 deletions packages/cubejs-client-core/src/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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 = {
Expand Down
Loading