Skip to content

feat: support nested view groups via includes#10999

Open
igorlukanin wants to merge 1 commit into
masterfrom
igor/core-470-support-creating-full-folder-hierarchy-in-view_groups
Open

feat: support nested view groups via includes#10999
igorlukanin wants to merge 1 commit into
masterfrom
igor/core-470-support-creating-full-folder-hierarchy-in-view_groups

Conversation

@igorlukanin
Copy link
Copy Markdown
Member

Summary

  • Add an includes parameter to view groups as the way to add views to a group, mirroring how folders use includes. It accepts view references and nested view group definitions ({ name, title, description, includes }), supporting an arbitrarily deep structure.
  • includes is the encouraged form; the legacy views parameter is still accepted for backward compatibility but is no longer documented. views and includes are mutually exclusive on a single view group (enforced via Joi oxor).
  • The /v1/meta response exposes a recursive includes array on each view group (alongside the existing views array of direct members). A view's viewGroups tag reports the most-specific group(s) it directly belongs to; a view may belong to multiple groups (e.g. two sibling groups, or a parent and its nested child). Nested groups are pruned from meta when none of their views are visible to the requester.
  • Referencing a view that does not exist now fails compilation (for both views and includes).

Test plan

  • schema-compiler integration tests (JS + YAML): string/bare/mixed refs, single & deep nesting, membership in sibling and nested groups, mutual-exclusion error, unknown-view error, duplicate nested name, malformed nested group
  • schema-compiler unit tests (yaml-schema): nested groups via includes, YAML mutual-exclusion
  • api-gateway meta tests: recursive includes + visibility pruning of nested groups
  • Verified end-to-end against a dev build: /v1/meta returns the nested structure; invalid both-fields model fails to compile
  • CI must pass

@igorlukanin igorlukanin requested review from a team and keydunov as code owners June 2, 2026 20:43
@igorlukanin igorlukanin requested a review from paveltiunov June 2, 2026 20:43
@github-actions github-actions Bot added client:core Issues relating to the JavaScript client SDK javascript Pull requests that update Javascript code labels Jun 2, 2026
@claude
Copy link
Copy Markdown
Contributor

claude Bot commented Jun 2, 2026

Claude finished @igorlukanin's task in 3m 0s —— View job


Code review

Verdict: Looks solid overall — feature works end-to-end, validation/transpile/evaluation/meta paths line up, and the test matrix is unusually thorough (string/bare/mixed refs, deep nesting, sibling + parent/child membership, mutual exclusion, unknown view, dup nested, malformed). Posted 0 high, 2 medium, 3 low inline.

Details (click to expand)

Checklist

  • ViewGroupEvaluator
  • CubeValidator
  • API gateway meta filtering
  • YAML + transpiler
  • Tests + docs
  • Dedup against prior threads (none)
  • Inline comments posted

Findings

Medium

  1. ViewGroupEvaluator.isNestedGroup keys off 'includes' in item only. The recursive Joi schema accepts { name, views: [...] } for nested entries (only oxor), so a nested group authored with legacy views: passes validation, then evaluator silently drops it. Same hole exists for YAML via viewGroupBodyAst.
  2. Nested-name uniqueness is scoped per top-level tree (seenNames resets each top-level call), so two unrelated trees can each define the same nested name, or a nested name can collide with a top-level group — making viewToGroups/viewGroupMap lookups ambiguous.

Low
3. viewGroupNamesForCube only resolves top-level groups, so a cube declaring viewGroup: '<nested-name>' errors with "not defined" despite the group visibly existing. Either allow nested auto-attach or document the restriction.
4. The recursive #viewGroupSchema link lets a nested entry pass validation with neither views nor includes (zero-view group) and even with fileName. Consider a dedicated nested-entry schema with .or('views', 'includes').
5. filterVisibleViewGroup spreads the compiler group into the meta response; explicit projection ({ name, title, description, views, includes }) would be safer against future internal fields.

Nits / praise

  • Nice explicit oxor + clear error message, and the test for it.
  • Good call leaving views undocumented but accepted — clean backward-compat story.
  • viewGroupBodyAst's comment about emitting views and includes independently (instead of else-if) so Joi catches the conflict is a thoughtful touch.
· `igor/core-470-support-creating-full-folder-hierarchy-in-view_groups`

Comment on lines +180 to +182
private isNestedGroup(item: any): item is ViewGroupIncludeNested {
return typeof item === 'object' && item !== null && typeof item.name === 'string' && 'includes' in item;
}
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.

[medium] isNestedGroup keys off 'includes' in item — but the recursive Joi viewGroupSchema (via Joi.link('#viewGroupSchema')) also accepts { name, views: [...] } for a nested entry (since the schema only enforces oxor('views', 'includes'), not xor). A nested group authored with the legacy views: field would pass validation, then isNestedGroup returns false, then resolveViewReference is called on the object and returns [] — the nested group is silently dropped with no error.

Either tighten the schema to require includes on nested entries, or detect "is this an object with name" here and surface an error (or honor views). Right now there's a quiet hole between validator and evaluator.

Same hole exists in the YAML path: viewGroupBodyAst emits both views and includes for nested groups, so a YAML nested group using views: makes it through transpile but gets dropped at evaluation.

Comment on lines 98 to +157
@@ -63,86 +122,207 @@ export class ViewGroupEvaluator implements CompilerInterface {
title: viewGroup.title,
description: viewGroup.description,
views,
includes: views.slice(),
};
}

/**
* Recursively compiles a view group's `includes` into the group's own direct
* view references plus a recursive `includes` representation (strings for
* views, nested CompiledViewGroup objects for nested groups).
*/
private compileIncludes(
rawIncludes: ViewGroupInclude[] | (() => any[]),
seenNames: Set<string>,
errorReporter?: ErrorReporter,
): { views: string[]; includes: (string | CompiledViewGroup)[] } {
let items: any[] = [];
if (typeof rawIncludes === 'function') {
const evaluated = rawIncludes();
items = Array.isArray(evaluated) ? evaluated : [evaluated];
} else if (Array.isArray(rawIncludes)) {
items = rawIncludes;
}

const views: string[] = [];
const includes: (string | CompiledViewGroup)[] = [];

for (const item of items) {
if (this.isNestedGroup(item)) {
if (errorReporter && seenNames.has(item.name)) {
errorReporter.error(`View group "${item.name}" already exists!`);
// eslint-disable-next-line no-continue
continue;
}
seenNames.add(item.name);
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.

[medium] Nested group name uniqueness is scoped per-top-level tree (seenNames is reset each compileViewGroup call), so two unrelated top-level groups can each define a nested ent_sales, and a nested group can even share its name with an unrelated top-level group — no collision error.

Two concrete consequences:

  • viewToGroups (used for the per-view viewGroups tag in meta) maps view → group name. If a top-level foo and a nested foo both exist, the tag becomes ambiguous and downstream lookups (viewGroupMap.get(name)) resolve only the top-level entry.
  • The duplicate-name test on line 1268 of the test file passes only because both dups sit in the same parent tree. Move one to a sibling top-level group and the error disappears.

Consider keeping a global seenNames set populated with all top-level names up-front, and threading it through compileIncludes recursion across all groups — or at minimum, after compiling, fail if any nested name clashes with another nested or top-level name.

Comment on lines +216 to 227
for (const groupName of this.viewGroupNamesForCube(cube)) {
const group = viewGroupMap.get(groupName);
if (!group) {
if (errorReporter) {
errorReporter.error(`View "${cube.name}" references view group "${groupName}" which is not defined. Define it using view_group('${groupName}', { ... }).`);
}
} else if (!group.views.includes(cube.name)) {
group.views.push(cube.name);
group.includes.push(cube.name);
}
}
}
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.

[low] viewGroupNamesForCube only looks up top-level entries in viewGroupMap, so a view declaring viewGroup: 'ent_sales' (a nested-group name) compiles with an error saying the group is "not defined" — even though it visibly exists in the nested hierarchy. Worth either (a) allowing cubes to auto-attach to nested groups by walking the resolved tree, or (b) documenting that the viewGroup/viewGroups cube properties only accept top-level group names. Either is fine; right now the behavior is surprising.

Comment on lines +1230 to +1253
const viewGroupSchema = Joi.object().keys({
name: Joi.string().required(),
title: Joi.string(),
description: Joi.string(),
// Legacy way of including views into a group, kept for backward compatibility.
views: Joi.alternatives([Joi.array().items(Joi.string().required()), Joi.func()]),
// Preferred way of including views (and nested view groups) into a group.
includes: Joi.alternatives([
Joi.func(),
Joi.array().items(
Joi.alternatives([
Joi.string().required(),
Joi.func(),
Joi.link('#viewGroupSchema'), // Can contain nested view groups
]),
),
]),
fileName: Joi.string(),
})
.oxor('views', 'includes')
.messages({
'object.oxor': 'View group must use either "views" or "includes", but not both'
})
.id('viewGroupSchema');
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.

[low] Because nested entries are validated through the same #viewGroupSchema link, a nested entry inherits name: required plus optional views | includes plus fileName — but a nested entry with neither views nor includes (e.g. { name: 'empty' }) passes validation and produces a zero-view compiled group that still gets a viewToGroups entry for no one. Consider adding .or('views', 'includes') so a nested entry must include something, or — better — defining a separate schema for nested entries that omits the legacy views path entirely.

Side note: fileName is also accepted for nested entries from this same link, which is meaningless. A separate nested schema would tidy that up too.

Comment on lines +654 to +671
* 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;
}

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.

[low] { ...group, views, includes } copies every property from the compiler-side group object into the meta response — including anything future code adds to CompiledViewGroup (e.g. internal fields). This is mostly fine today but is an easy place to leak future internal state. Consider an explicit projection: { name, title, description, views, includes }.

@mintlify
Copy link
Copy Markdown
Contributor

mintlify Bot commented Jun 2, 2026

Preview deployment for your docs. Learn more about Mintlify Previews.

Project Status Preview Updated (UTC)
cubed3 🟢 Ready View Preview Jun 2, 2026, 8:56 PM

💡 Tip: Enable Workflows to automatically generate PRs for you.

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

Labels

client:core Issues relating to the JavaScript client SDK javascript Pull requests that update Javascript code

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant