Skip to content
Merged
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
50 changes: 45 additions & 5 deletions docs-mintlify/docs/data-modeling/view-groups.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -41,7 +41,7 @@ view_group(`sales`, {
## Assigning views to a group

To assign a view to a group, list its name on the group via the
[`views`][ref-view-group-views] parameter. This keeps the full
[`includes`][ref-view-group-includes] parameter. This keeps the full
membership in one place, which makes it easy to review a group at a glance.

<CodeGroup>
Expand All @@ -50,23 +50,62 @@ membership in one place, which makes it easy to review a group at a glance.
view_groups:
- name: sales
title: Sales
views:
includes:
- orders_overview
- revenue
```

```javascript title="JavaScript"
view_group(`sales`, {
title: `Sales`,
views: [`orders_overview`, `revenue`]
includes: [`orders_overview`, `revenue`]
})
```

</CodeGroup>

A view can belong to more than one group — list it under the `views`
A view can belong to more than one group — list it under the `includes`
parameter of every group it should appear in.

## Nesting

View groups can be nested, similar to [nested folders][ref-view-nesting]. Add a
nested view group — with its own `name`, `title`, `description`, and
`includes` — directly inside a parent group's `includes`.

<CodeGroup>

```yaml title="YAML"
view_groups:
- name: sales
title: Sales
includes:
- orders_overview
- revenue

- name: enterprise_sales
title: Enterprise Sales
includes:
- enterprise_deals
```

```javascript title="JavaScript"
view_group(`sales`, {
title: `Sales`,
includes: [
`orders_overview`,
`revenue`,
{
name: `enterprise_sales`,
title: `Enterprise Sales`,
includes: [`enterprise_deals`]
}
]
})
```

</CodeGroup>

## Where view groups live in the model

By [convention][ref-syntax], view groups are typically defined alongside
Expand All @@ -83,8 +122,9 @@ entity and can be split across multiple files as your model grows.
- Explore [AI context][ref-ai-context] to improve AI query accuracy

[ref-views]: /docs/data-modeling/views
[ref-view-nesting]: /reference/data-modeling/view#nesting
[ref-syntax]: /docs/data-modeling/concepts/syntax
[ref-ai-context]: /docs/data-modeling/ai-context
[ref-view-group-ref]: /reference/data-modeling/view-group
[ref-view-group-views]: /reference/data-modeling/view-group#views
[ref-view-group-includes]: /reference/data-modeling/view-group#includes
[ref-meta-endpoint]: /reference/core-data-apis/rest-api/reference
70 changes: 58 additions & 12 deletions docs-mintlify/reference/data-modeling/view-group.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -79,25 +79,67 @@ view_group(`sales`, {

</CodeGroup>

### `views`
### `includes`

A list of view names that belong to this group.
A list of [views][ref-views] that belong to this group. It can also contain
nested view groups (see [Nesting](#nesting) below).

<CodeGroup>

```yaml title="YAML"
view_groups:
- name: sales
title: Sales
views:
includes:
- orders_overview
- revenue
```

```javascript title="JavaScript"
view_group(`sales`, {
title: `Sales`,
views: [`orders_overview`, `revenue`]
includes: [`orders_overview`, `revenue`]
})
```

</CodeGroup>

#### Nesting

View groups can be nested, similar to [nested folders][ref-view-nesting]. The
`includes` parameter can contain not only references to views but also other
view groups, each with its own `title`, `description`, and `includes`.

<CodeGroup>

```yaml title="YAML"
view_groups:
- name: sales
title: Sales
includes:
- orders_overview
- revenue

- name: enterprise_sales
title: Enterprise Sales
description: Views for the enterprise sales team
includes:
- enterprise_deals
```

```javascript title="JavaScript"
view_group(`sales`, {
title: `Sales`,
includes: [
`orders_overview`,
`revenue`,
{
name: `enterprise_sales`,
title: `Enterprise Sales`,
description: `Views for the enterprise sales team`,
includes: [`enterprise_deals`]
}
]
})
```

Expand All @@ -106,12 +148,12 @@ view_group(`sales`, {
## Assigning views to groups

To associate a view with a view group, list its name in the
[`views`](#views) parameter on the view group.
[`includes`](#includes) parameter on the view group.

### Example

The following model defines two view groups. The `sales` group lists
`orders_overview` and `revenue` in its `views` parameter, while the
`orders_overview` and `revenue` in its `includes` parameter, while the
`people` group lists `customers_view`.

<CodeGroup>
Expand Down Expand Up @@ -186,14 +228,14 @@ view_groups:
- name: sales
title: Sales
description: Revenue and order views for the sales team
views:
includes:
- orders_overview
- revenue

- name: people
title: People
description: Customer and user views
views:
includes:
- customers_view
```

Expand Down Expand Up @@ -292,13 +334,13 @@ view(`customers_view`, {
view_group(`sales`, {
title: `Sales`,
description: `Revenue and order views for the sales team`,
views: [`orders_overview`, `revenue`]
includes: [`orders_overview`, `revenue`]
})

view_group(`people`, {
title: `People`,
description: `Customer and user views`,
views: [`customers_view`]
includes: [`customers_view`]
})
```

Expand All @@ -313,18 +355,22 @@ With this model, the `/v1/meta` response includes a `viewGroups` array:
"name": "sales",
"title": "Sales",
"description": "Revenue and order views for the sales team",
"views": ["orders_overview", "revenue"]
"includes": ["orders_overview", "revenue"]
},
{
"name": "people",
"title": "People",
"description": "Customer and user views",
"views": ["customers_view"]
"includes": ["customers_view"]
}
]
}
```

Each view group carries an `includes` array that preserves the authoring order
and contains the group's views as well as any nested view groups.

[ref-views]: /docs/data-modeling/views
[ref-view-nesting]: /reference/data-modeling/view#nesting
[ref-naming]: /docs/data-modeling/concepts/syntax#naming
[ref-meta-endpoint]: /reference/core-data-apis/rest-api/reference
40 changes: 35 additions & 5 deletions packages/cubejs-api-gateway/src/gateway.ts
Original file line number Diff line number Diff line change
Expand Up @@ -653,6 +653,39 @@ class ApiGateway {
})).filter(cube => cube.config.measures?.length || cube.config.dimensions?.length || cube.config.segments?.length);
}

/**
* Recursively filters a (possibly nested) view group so that only visible
* views are exposed. A view group is dropped (returns null) when neither it
* nor any of its nested groups contains a visible view, preventing leaks of
* restricted view names.
*/
private filterVisibleViewGroup(group: any, visibleCubeNames: Set<string>): any | null {
const views = (group.views || []).filter((v: string) => visibleCubeNames.has(v));
const includes = (group.includes || [])
.map((include: any) => {
if (typeof include === 'string') {
return visibleCubeNames.has(include) ? include : null;
}
return this.filterVisibleViewGroup(include, visibleCubeNames);
})
.filter((include: any) => include !== null);

if (views.length === 0 && includes.length === 0) {
return null;
}

// Explicit projection (rather than spreading `group`) so internal fields
// added to the compiled view group in the future don't leak into the meta
// response by accident.
return {
name: group.name,
title: group.title,
description: group.description,
views,
includes,
};
}

public async meta({ context, res, includeCompilerId, onlyCompilerId, onlyViews }: {
context: RequestContext,
res: MetaResponseResultFn,
Expand Down Expand Up @@ -684,11 +717,8 @@ class ApiGateway {
const cubes = this.filterVisibleItemsInMeta(context, cubesConfig).map(cube => cube.config);
const visibleCubeNames = new Set(cubes.map(c => c.name));
const viewGroups = (metaConfig.viewGroups || [])
.map(group => ({
...group,
views: group.views.filter((v: string) => visibleCubeNames.has(v)),
}))
.filter(group => group.views.length > 0);
.map(group => this.filterVisibleViewGroup(group, visibleCubeNames))
.filter(group => group !== null);
const response: { cubes: any[], viewGroups?: any[], compilerId?: string } = { cubes };
if (viewGroups.length > 0) {
response.viewGroups = viewGroups;
Expand Down
4 changes: 4 additions & 0 deletions packages/cubejs-api-gateway/test/index.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -698,11 +698,14 @@ describe('API Gateway', () => {
expect(res.body).toHaveProperty('viewGroups');

expect(res.body.viewGroups).toHaveLength(1);
// The nested `restricted` group references only a hidden view, so it is
// pruned from the response.
expect(res.body.viewGroups[0]).toEqual({
name: 'analytics',
title: 'Analytics',
description: 'Analytics related views',
views: ['FooView'],
includes: ['FooView'],
});

const fooView = res.body.cubes.find(c => c.name === 'FooView');
Expand Down Expand Up @@ -750,6 +753,7 @@ describe('API Gateway', () => {
title: 'Analytics',
description: 'Analytics related views',
views: ['FooView'],
includes: ['FooView'],
},
]);
});
Expand Down
13 changes: 13 additions & 0 deletions packages/cubejs-api-gateway/test/mocks.ts
Original file line number Diff line number Diff line change
Expand Up @@ -161,9 +161,22 @@ export const compilerApi = jest.fn().mockImplementation(async () => ({
title: 'Analytics',
description: 'Analytics related views',
views: ['FooView'],
includes: [
'FooView',
{
name: 'restricted',
title: 'Restricted',
// Only references a hidden view, so it must be pruned from meta.
views: ['HiddenView'],
includes: ['HiddenView'],
},
],
},
];
}
// NOTE: `views`/`includes` here represent the already-compiled meta shape
// returned by the compiler (post-resolution), where both fields are
// always present — not the authored view group definition.
return result;
}

Expand Down
9 changes: 9 additions & 0 deletions packages/cubejs-client-core/src/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -545,7 +545,16 @@ export type ViewGroup = {
name: string;
title?: string;
description?: string;
/**
* The group's own direct view references at this level.
*/
views: string[];
/**
* Recursive representation: view names interleaved with nested view groups,
* preserving authoring order. Present when the group is defined via
* `includes` (including nested view groups).
*/
includes?: (string | ViewGroup)[];
};

export type MetaResponse = {
Expand Down
Loading
Loading