Skip to content

feat: add view_group support to data model#10768

Draft
paveltiunov wants to merge 2 commits intomasterfrom
cursor/view-group-support-aafc
Draft

feat: add view_group support to data model#10768
paveltiunov wants to merge 2 commits intomasterfrom
cursor/view-group-support-aafc

Conversation

@paveltiunov
Copy link
Copy Markdown
Member

Check List

  • Tests have been run in packages where changes have been made if available
  • Linter has been run for changed code
  • Tests for the changes have been added if not covered yet
  • Docs have been added / updated if required

Description of Changes Made

Adds view_group as a new first-class object in the Cube data model. View groups allow organizing views into logical groups for better discoverability and navigation in the meta API response.

Definition Methods

View groups can be defined in two ways:

1. Standalone view_group definition (JS):

view_group('sales', {
  title: 'Sales',
  description: 'Sales related views',
  views: ['revenue', 'customers']
});

2. Via viewGroup property on individual views (JS):

view('revenue', {
  viewGroup: 'sales',
  cubes: [{ joinPath: Orders, includes: '*' }]
});

3. YAML standalone definition:

view_groups:
  - name: sales
    title: Sales
    description: Sales related views
    views:
      - revenue
      - customers

4. YAML view-level definition:

views:
  - name: revenue
    view_group: sales
    cubes:
      - join_path: Orders
        includes: '*'

Both definition methods can be combined — a standalone view_group defines the group metadata (title, description) and lists views, while individual views can also declare their group membership via view_group. The system merges both sources.

Meta API Response

View groups are served as part of the /v1/meta response:

{
  "cubes": [...],
  "viewGroups": [
    {
      "name": "sales",
      "title": "Sales",
      "description": "Sales related views",
      "views": ["revenue", "customers"]
    }
  ]
}

Each view cube config also includes a viewGroup field indicating which group it belongs to.

Implementation Details

Modified packages:

  • @cubejs-backend/schema-compiler — New ViewGroupEvaluator compiler, schema validation, YAML transpilation, meta transformer integration
  • @cubejs-backend/api-gateway — Include viewGroups in meta response
  • @cubejs-backend/server-core — Include viewGroups in CompilerApi.metaConfig() response

Key changes:

  • New ViewGroupEvaluator class (similar to ContextEvaluator) that compiles view_group() definitions
  • viewGroup property added to view schema in CubeValidator
  • view_groups top-level key support in YAML compiler
  • view_group() global function registered in DataSchemaCompiler VM context
  • CubeToMetaTransformer resolves and merges view groups from both sources
  • CompilerApi.metaConfig() now returns { cubes, viewGroups } object (previously returned cubes array directly when includeCompilerId was false)
  • All gateway consumers updated to handle the new response format
Open in Web Open in Cursor 

cursoragent and others added 2 commits April 29, 2026 03:40
Add view_group as a new first-class object in the Cube data model.
View groups can be defined either as standalone view_group() objects
or via the viewGroup property on individual views.

- Add ViewGroupEvaluator compiler
- Add view_group() global function in DataSchemaCompiler
- Add viewGroup property to view schema in CubeValidator
- Handle view_groups in YamlCompiler for YAML data models
- Resolve and merge view groups in CubeToMetaTransformer
- Include viewGroups in meta API response
- Add comprehensive tests for both JS and YAML definitions

Co-authored-by: Pavel Tiunov <pavel.tiunov@gmail.com>
- Fix no-continue lint errors in CubeToMetaTransformer
- Remove unused CompiledViewGroup import
- Fix prefer-const lint errors in gateway
- Update all metaConfig consumers to handle new object format
- Fix metaTransformer.compileViewGroups() timing via DataSchemaCompiler

Co-authored-by: Pavel Tiunov <pavel.tiunov@gmail.com>
@github-actions github-actions Bot added the javascript Pull requests that update Javascript code label Apr 29, 2026
@claude
Copy link
Copy Markdown
Contributor

claude Bot commented Apr 29, 2026

Claude finished @paveltiunov's task in 4m 5s —— View job


Code Review: feat: add view_group support to data model

  • Gather context and understand the changes
  • Review schema compiler changes
  • Review API gateway and server-core changes
  • Review test coverage
  • Review YAML compiler changes
  • Compile final review feedback

Overall Assessment

Well-structured feature addition that follows existing patterns (mirrors ContextEvaluator for ViewGroupEvaluator, extends the compilation pipeline cleanly). The dual definition approach (standalone view_group() + viewGroup on views) with merge semantics is a nice design. I left 7 inline comments on specific issues — here's the summary:

Issues Found

1. Breaking change in metaConfig() return type (medium severity)

CompilerApi.metaConfig() previously returned a bare cubes array when includeCompilerId was false. It now always returns { cubes, viewGroups }. The gateway consumers use metaConfigResult.cubes || metaConfigResult as a fallback, but any external consumers of CompilerApi (plugins, custom integrations) that call metaConfig() directly will break. This should be called out as a breaking change, or documented in a migration note.

2. Dead fallback code in gateway (low severity — inline comment on gateway.ts:265)

The || metaConfigResult fallback pattern in 4 places is dead code since metaConfig() now always returns an object. It's misleading because it implies the old array format might still occur. See inline comment →

3. metaConfigResponse2 naming (low severity — inline comment on gateway.ts:2068)

Looks like a leftover from iterating on the variable rename. See inline comment →

4. Unused viewGroupForView() method (low severity — inline comment on ViewGroupEvaluator.ts:62-74)

ViewGroupEvaluator.viewGroupForView() is defined but never called. The resolution logic lives inline in CubeToMetaTransformer.resolveViewGroups(). Either use it or remove it. See inline comment →

5. No validation of view names in view_group() definitions (medium severity — inline comment on CubeToMetaTransformer.ts:217-257)

A view_group can reference view names that don't exist (e.g. views: ['typo_view']), and the invalid name silently propagates to the meta API response. Consider validating and warning. See inline comment →

6. compileViewGroups() called in every compilation phase (low severity — inline comment on DataSchemaCompiler.ts:859-862)

compileCubeFiles runs in phases 0–3, so compileViewGroups() executes 4 times. Only the phase 3 call has correct data. The extra calls are wasted work (not a bug). See inline comment →

7. metaConfigExtended doesn't include viewGroups

CompilerApi.metaConfigExtended() (used by /v1/meta/extended) still returns { metaConfig, cubeDefinitions } without viewGroups. If the extended meta endpoint is consumed by tooling that needs the full picture (e.g. Cube Cloud UI), this could be a gap.

Things Done Well

  • Clean pattern adherence: ViewGroupEvaluator closely mirrors ContextEvaluator, making it immediately familiar to anyone who knows the codebase.
  • Merge semantics: The design where standalone view_group() and view-level viewGroup are merged is well-thought-out and thoroughly tested.
  • YAML support: Properly integrated with the YAML compiler including camelizeCube for view_groupviewGroup conversion.
  • Conditional API response: viewGroups is only included in the meta response when non-empty, avoiding noise for users who don't use the feature.
  • Good test coverage: Tests cover JS standalone, JS view-level, YAML standalone, YAML view-level, and the merge scenario. All key paths are exercised.

Test Coverage Notes

The test suite covers the happy paths well. Consider adding tests for:

  • A view_group that references a non-existent view name
  • Duplicate view_group definitions (to verify the error path)
  • A view assigned to a viewGroup that also has a standalone view_group definition with different metadata (verifying which title/description wins)
  • Empty view_group (no views listed, no view-level assignments)

Security

No security concerns — view_group is metadata-only and doesn't affect query execution or access control. The viewGroup property is a plain string validated by Joi (Joi.string()).



const compilerApi = await this.getCompilerApi(context);
let metaConfigResult = await compilerApi.metaConfig(request.context, {
const metaConfigResponse2 = await compilerApi.metaConfig(request.context, {
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Nit: metaConfigResponse2 looks like a leftover from iterating on the rename. Consider a more descriptive name like metaConfigResponse (same as the pattern used at line 1969).

Suggested change
const metaConfigResponse2 = await compilerApi.metaConfig(request.context, {
const metaConfigResponse = await compilerApi.metaConfig(request.context, {

const metaConfigResult = await compilerApi.metaConfig(req.context, {
requestId: req.context.requestId,
});
const metaConfigCubes = metaConfigResult.cubes || metaConfigResult;
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The || metaConfigResult fallback is dead code — CompilerApi.metaConfig() now always returns { cubes, viewGroups }, never an array. This fallback gives the false impression that the old array format might still occur, which makes the code harder to reason about. Same applies to lines 304, 1974, and 2072.

Consider removing the fallback:

Suggested change
const metaConfigCubes = metaConfigResult.cubes || metaConfigResult;
const metaConfigCubes = metaConfigResult.cubes;

title?: string;
description?: string;
views?: any;
fileName?: string;
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Nit: views is typed as any but in practice it's either string[], a function returning string[], or undefined (handled in compileViewGroup). A more specific type would improve readability:

Suggested change
fileName?: string;
views?: string[] | (() => string[]);

Comment on lines +62 to +74

public get compiledViewGroups(): CompiledViewGroup[] {
return Array.from(this.viewGroupDefinitions.values());
}

public viewGroupForView(viewName: string): string | undefined {
for (const [, viewGroup] of this.viewGroupDefinitions) {
if (viewGroup.views.includes(viewName)) {
return viewGroup.name;
}
}
return undefined;
}
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

viewGroupForView is defined but never called — the view-to-group resolution is done inline in CubeToMetaTransformer.resolveViewGroups() instead. Consider removing this dead method, or refactoring resolveViewGroups to use it for consistency.

Comment on lines +231 to +232
const extendedCube = this.cubeSymbols.cubeList.find(c => c.name === cube.config.name) as any;
const viewGroupName = extendedCube?.viewGroup;
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This cubeList.find() scan runs inside a loop over all cubes, making it O(n²). Consider building a lookup Map<string, CubeDefinition> before the loop. Similarly, this.cubes.find() at line 249 has the same issue.

For small numbers of views/groups this is fine, but if someone has hundreds of views it could add up.

Comment on lines 859 to +862
return this.compileObjects(compilers.cubeCompilers || [], cubes, errorsReport)
.then(() => this.compileObjects(compilers.contextCompilers || [], contexts, errorsReport));
.then(() => this.compileObjects(compilers.contextCompilers || [], contexts, errorsReport))
.then(() => this.compileObjects(compilers.viewGroupCompilers || [], viewGroups, errorsReport))
.then(() => this.metaTransformer?.compileViewGroups());
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

compileCubeFiles is called in every phase (0–3), but viewGroupCompilers is only provided in phase 3. In earlier phases this runs compileObjects([], viewGroups, errorsReport) (no-op) followed by this.metaTransformer?.compileViewGroups() which executes against incomplete data and gets overwritten in phase 3.

Not a bug (phase 3 produces the correct final result), but it means resolveViewGroups() runs 3 extra times with stale data. Consider guarding:

Suggested change
return this.compileObjects(compilers.cubeCompilers || [], cubes, errorsReport)
.then(() => this.compileObjects(compilers.contextCompilers || [], contexts, errorsReport));
.then(() => this.compileObjects(compilers.contextCompilers || [], contexts, errorsReport))
.then(() => this.compileObjects(compilers.viewGroupCompilers || [], viewGroups, errorsReport))
.then(() => this.metaTransformer?.compileViewGroups());
return this.compileObjects(compilers.cubeCompilers || [], cubes, errorsReport)
.then(() => this.compileObjects(compilers.contextCompilers || [], contexts, errorsReport))
.then(() => this.compileObjects(compilers.viewGroupCompilers || [], viewGroups, errorsReport))
.then(() => (compilers.viewGroupCompilers?.length ? this.metaTransformer?.compileViewGroups() : undefined));

Comment on lines +217 to +257
private resolveViewGroups(): ViewGroupConfig[] {
const viewGroupMap = new Map<string, ViewGroupConfig>();

for (const compiled of this.viewGroupEvaluator.compiledViewGroups) {
viewGroupMap.set(compiled.name, {
name: compiled.name,
title: compiled.title,
description: compiled.description,
views: [...compiled.views],
});
}

for (const cube of this.cubes) {
if (cube.config.type === 'view') {
const extendedCube = this.cubeSymbols.cubeList.find(c => c.name === cube.config.name) as any;
const viewGroupName = extendedCube?.viewGroup;
if (viewGroupName) {
let group = viewGroupMap.get(viewGroupName);
if (!group) {
group = { name: viewGroupName, views: [] };
viewGroupMap.set(viewGroupName, group);
}
if (!group.views.includes(cube.config.name)) {
group.views.push(cube.config.name);
}
cube.config.viewGroup = viewGroupName;
}
}
}

for (const group of viewGroupMap.values()) {
for (const viewName of group.views) {
const cube = this.cubes.find(c => c.config.name === viewName);
if (cube && !cube.config.viewGroup) {
cube.config.viewGroup = group.name;
}
}
}

return Array.from(viewGroupMap.values());
}
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

No validation that views referenced in a view_group() definition actually exist. If someone writes views: ['nonexistent_view'], it silently appears in the meta API output. Consider logging a warning (via an ErrorReporter, similar to how CubeValidator reports invalid cube names) for view names that don't match any compiled view.

@codecov
Copy link
Copy Markdown

codecov Bot commented Apr 29, 2026

Codecov Report

❌ Patch coverage is 57.14286% with 42 lines in your changes missing coverage. Please review.
✅ Project coverage is 58.02%. Comparing base (d9fa353) to head (d52504d).

Files with missing lines Patch % Lines
...ubejs-schema-compiler/src/compiler/YamlCompiler.ts 0.00% 16 Missing ⚠️
packages/cubejs-api-gateway/src/gateway.ts 18.75% 11 Missing and 2 partials ⚠️
...schema-compiler/src/compiler/ViewGroupEvaluator.ts 63.63% 6 Missing and 2 partials ⚠️
...schema-compiler/src/compiler/DataSchemaCompiler.ts 76.92% 2 Missing and 1 partial ⚠️
...ackages/cubejs-server-core/src/core/CompilerApi.ts 50.00% 2 Missing ⚠️
Additional details and impacted files
@@            Coverage Diff             @@
##           master   #10768      +/-   ##
==========================================
+ Coverage   58.01%   58.02%   +0.01%     
==========================================
  Files         215      216       +1     
  Lines       16764    16841      +77     
  Branches     3383     3405      +22     
==========================================
+ Hits         9725     9772      +47     
- Misses       6547     6573      +26     
- Partials      492      496       +4     
Flag Coverage Δ
cube-backend 58.02% <57.14%> (+0.01%) ⬆️

Flags with carried forward coverage won't be shown. Click here to find out more.

☔ View full report in Codecov by Sentry.
📢 Have feedback on the report? Share it here.

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.
  • 📦 JS Bundle Analysis: Save yourself from yourself by tracking and limiting bundle sizes in JS merges.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

javascript Pull requests that update Javascript code

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants