diff --git a/docs/pages/product/apis-integrations.mdx b/docs/pages/product/apis-integrations.mdx
index 9edd6c61261cd..274b9457f4fff 100644
--- a/docs/pages/product/apis-integrations.mdx
+++ b/docs/pages/product/apis-integrations.mdx
@@ -45,10 +45,11 @@ for an unofficial, community-maintained [client library for Python](https://gith
Support for data modeling features differ across APIs, integrations, and [visualization
tools][ref-viz-tools]. Some of the features with partial support are listed below:
-| Feature | ✅ Supported in | ❌ Not supported in |
-| --- | --- | --- |
-| [Hierarchies][ref-hierarchies] | [Microsoft Power BI][ref-powerbi] via the [DAX API][ref-dax-api]
[Cube Cloud for Excel][ref-cube-cloud-for-excel]
[Cube Cloud for Sheets][ref-cube-cloud-for-sheets]
[Tableau][ref-tableau] via [Semantic Layer Sync][ref-sls] | All other tools |
-| [Folders][ref-folders] | [Microsoft Power BI][ref-powerbi] via the [DAX API][ref-dax-api]
[Cube Cloud for Excel][ref-cube-cloud-for-excel]
[Cube Cloud for Sheets][ref-cube-cloud-for-sheets]
[Tableau][ref-tableau] via [Semantic Layer Sync][ref-sls]
[Apache Superset][ref-superset] via [Semantic Layer Sync][ref-sls]
[Preset][ref-preset] via [Semantic Layer Sync][ref-sls] | All other tools |
+| Feature | ✅ Supported in |
+| --- | --- |
+| [Hierarchies][ref-hierarchies] | [Microsoft Power BI][ref-powerbi] via the [DAX API][ref-dax-api]
[Cube Cloud for Excel][ref-cube-cloud-for-excel]
[Cube Cloud for Sheets][ref-cube-cloud-for-sheets]
[Tableau][ref-tableau] via [Semantic Layer Sync][ref-sls]
Also, supported in [Playground][ref-playground] |
+| Flat [folders][ref-folders] | [Microsoft Power BI][ref-powerbi] via the [DAX API][ref-dax-api]
[Cube Cloud for Excel][ref-cube-cloud-for-excel]
[Cube Cloud for Sheets][ref-cube-cloud-for-sheets]
[Tableau][ref-tableau] via [Semantic Layer Sync][ref-sls]
[Apache Superset][ref-superset] via [Semantic Layer Sync][ref-sls]
[Preset][ref-preset] via [Semantic Layer Sync][ref-sls]
Also, supported in [Playground][ref-playground] |
+| Nested [folders][ref-folders] | Currently, not supported in any tool |
### Authentication methods
@@ -96,3 +97,4 @@ API][ref-orchestration-api].
[ref-auth-ntlm]: /product/auth/methods/ntlm
[ref-superset]: /product/configuration/visualization-tools/superset
[ref-preset]: /product/configuration/visualization-tools/superset
+[ref-playground]: /product/workspace/playground
\ No newline at end of file
diff --git a/docs/pages/product/apis-integrations/rest-api/reference.mdx b/docs/pages/product/apis-integrations/rest-api/reference.mdx
index 8715241bdd8bb..ab775c43b7ca9 100644
--- a/docs/pages/product/apis-integrations/rest-api/reference.mdx
+++ b/docs/pages/product/apis-integrations/rest-api/reference.mdx
@@ -254,7 +254,7 @@ Response
- `dimensions` - Array of dimensions in this cube/view
- `hierarchies` - Array of hierarchies in this cube
- `segments` - Array of segments in this cube/view
- - `folders` - Array of folders in this view
+ - `folders` and `nestedFolders` - Arrays of flat and nested [folder][ref-folders] structures in this view, respectively
- `connectedComponent` - An integer representing a join relationship. If the same value is returned for two cubes, then there is
at least one join path between them.
@@ -638,4 +638,5 @@ Keep-Alive: timeout=5
[ref-query-wpp]: /product/apis-integrations/queries#query-with-post-processing
[ref-query-wpd]: /product/apis-integrations/queries#query-with-pushdown
[ref-sql-api]: /product/apis-integrations/sql-api
-[ref-orchestration-api]: /product/apis-integrations/orchestration-api
\ No newline at end of file
+[ref-orchestration-api]: /product/apis-integrations/orchestration-api
+[ref-folders]: /product/data-modeling/reference/view#folders
\ No newline at end of file
diff --git a/docs/pages/product/configuration/reference/environment-variables.mdx b/docs/pages/product/configuration/reference/environment-variables.mdx
index 229faa92433fa..33c134364dba2 100644
--- a/docs/pages/product/configuration/reference/environment-variables.mdx
+++ b/docs/pages/product/configuration/reference/environment-variables.mdx
@@ -1297,6 +1297,21 @@ If `true`, the DAX API will expose time dimensions as calendar hierarchies.
| --------------- | ---------------------- | --------------------- |
| `true`, `false` | `true` | `true` |
+## `CUBEJS_NESTED_FOLDERS_DELIMITER`
+
+Specifies the delimiter used to flatten the names of nested [folder][ref-folders] in
+views when [visualization tools][ref-dataviz-tools] do not support nested folder
+structures. When set, nested folders will be presented at the root level with path-like
+names using the specified delimiter.
+
+| Possible Values | Default in Development | Default in Production |
+| --------------- | ---------------------- | --------------------- |
+| A valid string delimiter (e.g., ` / `) | N/A | N/A |
+
+For example, if set to `/`, a nested folder structure like the `Customer Information`
+folder with the `Personal Details` subfolder will be flattened to `Customer
+Information / Personal Details` at the root level.
+
## `CUBEJS_TELEMETRY`
If `true`, then send telemetry to Cube.
@@ -1781,3 +1796,5 @@ The port for a Cube deployment to listen to API connections on.
[ref-schema-ref-preagg-allownonstrict]: /product/data-modeling/reference/pre-aggregations#allow_non_strict_date_range_match
[link-tesseract]: https://cube.dev/blog/introducing-next-generation-data-modeling-engine
[ref-multi-stage-calculations]: /product/data-modeling/concepts/multi-stage-calculations
+[ref-folders]: /product/data-modeling/reference/view#folders
+[ref-dataviz-tools]: /product/configuration/visualization-tools
\ No newline at end of file
diff --git a/docs/pages/product/data-modeling/concepts.mdx b/docs/pages/product/data-modeling/concepts.mdx
index ae3ea87e8e44d..698f4f2e040a1 100644
--- a/docs/pages/product/data-modeling/concepts.mdx
+++ b/docs/pages/product/data-modeling/concepts.mdx
@@ -131,8 +131,7 @@ paths.
Views do **not** define their own members. Instead, they reference cubes by
-specific join paths and include their members. Optionally, you can also group
-members of a view into [folders][ref-ref-folders].
+specific join paths and include their members.
In the example below, we create the `orders` view which includes select members
from `base_orders`, `products`, and `users` cubes:
@@ -212,6 +211,16 @@ See the reference documentaton for the full list of view [parameters][ref-views]
+### Folders
+
+Optionally, members of a view can be organized into [folders][ref-ref-folders].
+Each folder would contain a subset of members of the view.
+
+Cube supports both flat and nested folder structures, which can be used with various
+[visualization tools][ref-viz-tools]. If a specific tool does not support nested folders,
+they will be exposed to such a tool as an equivalent flat structure. Check [APIs &
+Integrations][ref-apis-support] for details on the nested folders support.
+
## Dimensions
_Dimensions_ represent the properties of a **single** data point in the cube.
@@ -841,4 +850,6 @@ See the reference documentaton for the full list of pre-aggregation
[ref-avg-and-percentile-recipe]: /product/data-modeling/recipes/percentiles
[ref-period-over-period-recipe]: /product/data-modeling/recipes/period-over-period
[ref-custom-calendar-recipe]: /product/data-modeling/recipes/custom-calendar
-[ref-cube-with-dbt]: /product/data-modeling/recipes/dbt
\ No newline at end of file
+[ref-cube-with-dbt]: /product/data-modeling/recipes/dbt
+[ref-apis-support]: /product/apis-integrations#data-modeling
+[ref-viz-tools]: /product/configuration/visualization-tools
\ No newline at end of file
diff --git a/docs/pages/product/data-modeling/reference/view.mdx b/docs/pages/product/data-modeling/reference/view.mdx
index 41d0f566fc017..28ba3eb2b5fe1 100644
--- a/docs/pages/product/data-modeling/reference/view.mdx
+++ b/docs/pages/product/data-modeling/reference/view.mdx
@@ -441,6 +441,120 @@ views:
+Nested folders are also supported. The `includes` parameter can contain not only
+references to view members but also other folders:
+
+
+
+```javascript
+view(`customers`, {
+ cubes: [
+ {
+ join_path: `users`,
+ includes: `*`
+ },
+ {
+ join_path: `users.orders`,
+ prefix: true,
+ includes: [
+ `status`,
+ `price`,
+ `count`
+ ]
+ }
+ ],
+
+ folders: [
+ {
+ name: `Customer Information`,
+ includes: [
+ {
+ name: `Personal Details`,
+ includes: [
+ `name`,
+ `gender`
+ ]
+ },
+ {
+ name: `Location`,
+ includes: [
+ `address`,
+ `postal_code`,
+ `city`
+ ]
+ }
+ ]
+ },
+ {
+ name: `Order Analytics`,
+ includes: [
+ `orders_status`,
+ `orders_price`,
+ {
+ name: `Metrics`,
+ includes: [
+ `orders_count`,
+ `orders_average_value`
+ ]
+ }
+ ]
+ }
+ ]
+})
+```
+
+```yaml
+views:
+ - name: customers
+
+ cubes:
+ - join_path: users
+ includes: "*"
+
+ - join_path: users.orders
+ prefix: true
+ includes:
+ - status
+ - price
+ - count
+
+ folders:
+ - name: Customer Information
+ includes:
+ - name: Personal Details
+ includes:
+ - name
+ - gender
+
+ - name: Location
+ includes:
+ - address
+ - postal_code
+ - city
+
+ - name: Order Analytics
+ includes:
+ - orders_status
+ - orders_price
+
+ - name: Metrics
+ includes:
+ - orders_count
+ - orders_average_value
+```
+
+
+
+You can still define nested folders in the data model even if some of your [visualization
+tools][ref-viz-tools] do not support them. Check [APIs & Integrations][ref-apis-support]
+for details on the nested folders support.
+
+For tools that do not support nested folders, the nested structure will be flattened:
+by default, the members of nested folders are merged into folders at the root level.
+You can also set the `CUBEJS_NESTED_FOLDERS_DELIMITER` environment variable to preserve
+nested folders and give them path-like names, e.g., `Customer Information / Personal
+Details`.
+
### `access_policy`
The `access_policy` parameter is used to configure [data access policies][ref-ref-dap].
diff --git a/packages/cubejs-api-gateway/openspec.yml b/packages/cubejs-api-gateway/openspec.yml
index d97a857d9d0bd..912d650ad0379 100644
--- a/packages/cubejs-api-gateway/openspec.yml
+++ b/packages/cubejs-api-gateway/openspec.yml
@@ -175,6 +175,21 @@ components:
type: array
items:
type: "string"
+ V1CubeMetaNestedFolder:
+ type: "object"
+ required:
+ - name
+ - members
+ properties:
+ name:
+ type: "string"
+ members:
+ type: array
+ items:
+ type: "string"
+ oneOf:
+ - type: string
+ - $ref: "#/components/schemas/V1CubeMetaNestedFolder"
V1CubeMetaHierarchy:
type: "object"
required:
@@ -231,6 +246,10 @@ components:
type: "array"
items:
$ref: "#/components/schemas/V1CubeMetaFolder"
+ nestedFolders:
+ type: "array"
+ items:
+ $ref: "#/components/schemas/V1CubeMetaNestedFolder"
hierarchies:
type: "array"
items:
diff --git a/packages/cubejs-backend-shared/src/env.ts b/packages/cubejs-backend-shared/src/env.ts
index f3f37fd0c7d39..647f9b1eb17db 100644
--- a/packages/cubejs-backend-shared/src/env.ts
+++ b/packages/cubejs-backend-shared/src/env.ts
@@ -240,6 +240,9 @@ const variables: Record any> = {
transpilationNative: () => get('CUBEJS_TRANSPILATION_NATIVE')
.default('false')
.asBoolStrict(),
+ nestedFoldersDelimiter: () => get('CUBEJS_NESTED_FOLDERS_DELIMITER')
+ .default('')
+ .asString(),
/** ****************************************************************
* Common db options *
diff --git a/packages/cubejs-client-core/src/types.ts b/packages/cubejs-client-core/src/types.ts
index 1d408fd4aabb9..3ecc43befa139 100644
--- a/packages/cubejs-client-core/src/types.ts
+++ b/packages/cubejs-client-core/src/types.ts
@@ -417,6 +417,11 @@ export type TCubeFolder = {
members: string[];
};
+export type TCubeNestedFolder = {
+ name: string;
+ members: (string | TCubeNestedFolder)[];
+};
+
export type TCubeHierarchy = {
name: string;
title?: string;
@@ -451,6 +456,7 @@ export type Cube = {
dimensions: TCubeDimension[];
segments: TCubeSegment[];
folders: TCubeFolder[];
+ nestedFolders: TCubeNestedFolder[];
hierarchies: TCubeHierarchy[];
connectedComponent?: number;
type?: 'view' | 'cube';
diff --git a/packages/cubejs-schema-compiler/src/compiler/CubeEvaluator.ts b/packages/cubejs-schema-compiler/src/compiler/CubeEvaluator.ts
index d8ee7b6bd19b0..7a688eb3ccbd0 100644
--- a/packages/cubejs-schema-compiler/src/compiler/CubeEvaluator.ts
+++ b/packages/cubejs-schema-compiler/src/compiler/CubeEvaluator.ts
@@ -259,34 +259,51 @@ export class CubeEvaluator extends CubeSymbols {
private prepareFolders(cube: any, errorReporter: ErrorReporter) {
const folders = cube.rawFolders();
- if (folders.length) {
- cube.folders = folders.map(it => {
- const includedMembers = this.allMembersOrList(cube, it.includes);
- const includes = includedMembers.map(memberName => {
- if (memberName.includes('.')) {
- errorReporter.error(
- `Paths aren't allowed in the 'folders' but '${memberName}' has been provided for ${cube.name}`
- );
- }
+ if (!folders.length) return;
- const member = cube.includedMembers.find(m => m.name === memberName);
- if (!member) {
- errorReporter.error(
- `Member '${memberName}' included in folder '${it.name}' not found`
- );
- return null;
- }
+ const checkMember = (memberName: string, folderName: string) => {
+ if (memberName.includes('.')) {
+ errorReporter.error(
+ `Paths aren't allowed in the 'folders' but '${memberName}' has been provided for ${cube.name}`
+ );
+ }
+
+ const member = cube.includedMembers.find(m => m.name === memberName);
+ if (!member) {
+ errorReporter.error(
+ `Member '${memberName}' included in folder '${folderName}' not found`
+ );
+ return null;
+ }
- return member;
- })
- .filter(Boolean);
+ return member;
+ };
+
+ const processFolder = (folder: any): any => {
+ let includedMembers: string[];
+ let includes: any[] = [];
+
+ if (folder.includes === '*') {
+ includedMembers = this.allMembersOrList(cube, folder.includes);
+ includes = includedMembers.map(m => checkMember(m, folder.name)).filter(Boolean);
+ } else if (Array.isArray(folder.includes)) {
+ includes = folder.includes.map(item => {
+ if (typeof item === 'object' && item !== null) {
+ return processFolder(item);
+ }
- return ({
- ...it,
- includes
+ return checkMember(item, folder.name);
});
- });
- }
+ }
+
+ return {
+ ...folder,
+ type: 'folder',
+ includes: includes.filter(Boolean)
+ };
+ };
+
+ cube.folders = folders.map(processFolder);
}
private prepareHierarchies(cube: any, errorReporter: ErrorReporter): void {
diff --git a/packages/cubejs-schema-compiler/src/compiler/CubeToMetaTransformer.js b/packages/cubejs-schema-compiler/src/compiler/CubeToMetaTransformer.js
index fe79d0e62162c..b6a706feddaa3 100644
--- a/packages/cubejs-schema-compiler/src/compiler/CubeToMetaTransformer.js
+++ b/packages/cubejs-schema-compiler/src/compiler/CubeToMetaTransformer.js
@@ -2,6 +2,7 @@ import inflection from 'inflection';
import R from 'ramda';
import camelCase from 'camelcase';
+import { getEnv } from '@cubejs-backend/shared';
import { CubeSymbols } from './CubeSymbols';
import { UserError } from './UserError';
import { BaseMeasure } from '../adapter';
@@ -43,6 +44,43 @@ export class CubeToMetaTransformer {
const isCubeVisible = this.isVisible(cube, true);
+ const flatFolderSeparator = getEnv('nestedFoldersDelimiter');
+ const flatFolders = [];
+
+ const processFolder = (folder, path = [], mergedMembers = []) => {
+ const flatMembers = [];
+ const nestedMembers = folder.includes.map(member => {
+ if (member.type === 'folder') {
+ return processFolder(member, [...path, folder.name], flatMembers);
+ }
+ const memberName = `${cube.name}.${member.name}`;
+ flatMembers.push(memberName);
+
+ return memberName;
+ });
+
+ if (flatFolderSeparator !== '') {
+ flatFolders.push({
+ name: [...path, folder.name].join(flatFolderSeparator),
+ members: flatMembers,
+ });
+ } else if (path.length > 0) {
+ mergedMembers.push(...flatMembers);
+ } else { // We're at the root level
+ flatFolders.push({
+ name: folder.name,
+ members: [...new Set(flatMembers)],
+ });
+ }
+
+ return {
+ name: folder.name,
+ members: nestedMembers,
+ };
+ };
+
+ const nestedFolders = (cube.folders || []).map(f => processFolder(f));
+
return {
config: {
name: cube.name,
@@ -113,10 +151,8 @@ export class CubeToMetaTransformer {
public: it.public ?? true,
name: `${cube.name}.${it.name}`,
})),
- folders: (cube.folders || []).map((it) => ({
- name: it.name,
- members: it.includes.map(member => `${cube.name}.${member.name}`),
- })),
+ folders: flatFolders,
+ nestedFolders,
},
};
}
diff --git a/packages/cubejs-schema-compiler/src/compiler/CubeValidator.ts b/packages/cubejs-schema-compiler/src/compiler/CubeValidator.ts
index bf604b83cceaa..fe22356346aa8 100644
--- a/packages/cubejs-schema-compiler/src/compiler/CubeValidator.ts
+++ b/packages/cubejs-schema-compiler/src/compiler/CubeValidator.ts
@@ -795,6 +795,19 @@ const cubeSchema = inherit(baseSchema, {
'object.xor': 'You must use either sql or sqlTable within a model, but not both'
});
+const folderSchema = Joi.object().keys({
+ name: Joi.string().required(),
+ includes: Joi.alternatives([
+ Joi.string().valid('*'),
+ Joi.array().items(
+ Joi.alternatives([
+ Joi.string().required(),
+ Joi.link('#folderSchema'), // Can contain nested folders
+ ]),
+ ),
+ ]).required(),
+}).id('folderSchema');
+
const viewSchema = inherit(baseSchema, {
isView: Joi.boolean().strict(),
cubes: Joi.array().items(
@@ -822,13 +835,7 @@ const viewSchema = inherit(baseSchema, {
'object.oxor': 'Using split together with prefix is not supported'
})
),
- folders: Joi.array().items(Joi.object().keys({
- name: Joi.string().required(),
- includes: Joi.alternatives([
- Joi.string().valid('*'),
- Joi.array().items(Joi.string().required())
- ]).required(),
- })),
+ folders: Joi.array().items(folderSchema),
});
function formatErrorMessageFromDetails(explain, d) {
diff --git a/packages/cubejs-schema-compiler/test/unit/__snapshots__/schema.test.ts.snap b/packages/cubejs-schema-compiler/test/unit/__snapshots__/schema.test.ts.snap
index 5857397186fc4..d4867c4c7d141 100644
--- a/packages/cubejs-schema-compiler/test/unit/__snapshots__/schema.test.ts.snap
+++ b/packages/cubejs-schema-compiler/test/unit/__snapshots__/schema.test.ts.snap
@@ -1990,6 +1990,7 @@ Array [
},
],
"name": "folder1",
+ "type": "folder",
},
Object {
"includes": Array [
@@ -2005,6 +2006,7 @@ Array [
},
],
"name": "folder2",
+ "type": "folder",
},
]
`;
diff --git a/packages/cubejs-schema-compiler/test/unit/__snapshots__/views.test.ts.snap b/packages/cubejs-schema-compiler/test/unit/__snapshots__/views.test.ts.snap
index 9811ebd942891..62cb9f9ea8335 100644
--- a/packages/cubejs-schema-compiler/test/unit/__snapshots__/views.test.ts.snap
+++ b/packages/cubejs-schema-compiler/test/unit/__snapshots__/views.test.ts.snap
@@ -89,6 +89,7 @@ Object {
],
"meta": undefined,
"name": "simple_view",
+ "nestedFolders": Array [],
"public": true,
"segments": Array [],
"title": "Simple View",
@@ -168,6 +169,7 @@ Object {
],
"meta": undefined,
"name": "simple_view",
+ "nestedFolders": Array [],
"public": true,
"segments": Array [],
"title": "Simple View",
@@ -281,6 +283,7 @@ Object {
],
"meta": undefined,
"name": "simple_view",
+ "nestedFolders": Array [],
"public": true,
"segments": Array [],
"title": "Simple View",
@@ -360,6 +363,7 @@ Object {
],
"meta": undefined,
"name": "simple_view",
+ "nestedFolders": Array [],
"public": true,
"segments": Array [],
"title": "Simple View",
diff --git a/packages/cubejs-schema-compiler/test/unit/fixtures/folders.yml b/packages/cubejs-schema-compiler/test/unit/fixtures/folders.yml
index ddb8983f280c2..8e2428aa45868 100644
--- a/packages/cubejs-schema-compiler/test/unit/fixtures/folders.yml
+++ b/packages/cubejs-schema-compiler/test/unit/fixtures/folders.yml
@@ -105,6 +105,17 @@ views:
includes:
- users_city
- users_renamed_in_view3_gender
+ - name: test_view4
+ extends: test_view3
+ folders:
+ - name: folder3
+ includes:
+ - users_city
+ - name: inner folder 4
+ includes:
+ - renamed_orders_status
+ - name: inner folder 5
+ includes: "*"
# - name: empty_view
# cubes:
diff --git a/packages/cubejs-schema-compiler/test/unit/fixtures/folders_invalid_path.yml b/packages/cubejs-schema-compiler/test/unit/fixtures/folders_invalid_path.yml
index b9df376e84aa5..5325cda75af95 100644
--- a/packages/cubejs-schema-compiler/test/unit/fixtures/folders_invalid_path.yml
+++ b/packages/cubejs-schema-compiler/test/unit/fixtures/folders_invalid_path.yml
@@ -72,6 +72,9 @@ views:
- age
- renamed_gender
- users.age
+ - name: inner folder
+ includes:
+ - users.renamed_gender
- name: folder2
includes: '*'
- name: test_view2
diff --git a/packages/cubejs-schema-compiler/test/unit/folders.test.ts b/packages/cubejs-schema-compiler/test/unit/folders.test.ts
index adf699f8059fc..22b0bdb89257f 100644
--- a/packages/cubejs-schema-compiler/test/unit/folders.test.ts
+++ b/packages/cubejs-schema-compiler/test/unit/folders.test.ts
@@ -43,6 +43,155 @@ describe('Cube Folders', () => {
);
});
+ it('a nested folders with some * and named members (merged)', async () => {
+ const testView = metaTransformer.cubes.find(
+ (it) => it.config.name === 'test_view4'
+ );
+
+ expect(testView.config.folders.length).toBe(3);
+
+ const folder1 = testView.config.folders.find(
+ (it) => it.name === 'folder1'
+ );
+ expect(folder1.members).toEqual([
+ 'test_view4.users_age',
+ 'test_view4.users_state',
+ 'test_view4.renamed_orders_status',
+ ]);
+
+ const folder2 = testView.config.folders.find(
+ (it) => it.name === 'folder2'
+ );
+ expect(folder2.members).toEqual(
+ expect.arrayContaining(['test_view4.users_city', 'test_view4.users_renamed_in_view3_gender'])
+ );
+
+ const folder3 = testView.config.folders.find(
+ (it) => it.name === 'folder3'
+ );
+ expect(folder3.members.length).toBe(9);
+ expect(folder3.members).toEqual([
+ 'test_view4.users_city',
+ 'test_view4.renamed_orders_status',
+ 'test_view4.renamed_orders_count',
+ 'test_view4.renamed_orders_id',
+ 'test_view4.renamed_orders_number',
+ 'test_view4.users_age',
+ 'test_view4.users_state',
+ 'test_view4.users_gender',
+ 'test_view4.users_renamed_in_view3_gender',
+ ]);
+ });
+
+ it('a nested folders with some * and named members (flattened)', async () => {
+ process.env.CUBEJS_NESTED_FOLDERS_DELIMITER = '/';
+ const modelContent = fs.readFileSync(
+ path.join(process.cwd(), '/test/unit/fixtures/folders.yml'),
+ 'utf8'
+ );
+ const prepared = prepareYamlCompiler(modelContent);
+ const compilerL = prepared.compiler;
+ const metaTransformerL = prepared.metaTransformer;
+
+ await compilerL.compile();
+
+ const testView = metaTransformerL.cubes.find(
+ (it) => it.config.name === 'test_view4'
+ );
+
+ expect(testView.config.folders.length).toBe(5);
+
+ const folder1 = testView.config.folders.find(
+ (it) => it.name === 'folder1'
+ );
+ expect(folder1.members).toEqual([
+ 'test_view4.users_age',
+ 'test_view4.users_state',
+ 'test_view4.renamed_orders_status',
+ ]);
+
+ const folder2 = testView.config.folders.find(
+ (it) => it.name === 'folder2'
+ );
+ expect(folder2.members).toEqual(
+ expect.arrayContaining(['test_view4.users_city', 'test_view4.users_renamed_in_view3_gender'])
+ );
+
+ const folder3 = testView.config.folders.find(
+ (it) => it.name === 'folder3'
+ );
+ expect(folder3.members.length).toBe(1);
+ expect(folder3.members).toEqual([
+ 'test_view4.users_city',
+ ]);
+
+ const folder4 = testView.config.folders.find(
+ (it) => it.name === 'folder3/inner folder 4'
+ );
+ expect(folder4.members.length).toBe(1);
+ expect(folder4.members).toEqual(['test_view4.renamed_orders_status']);
+
+ const folder5 = testView.config.folders.find(
+ (it) => it.name === 'folder3/inner folder 5'
+ );
+ expect(folder5.members.length).toBe(9);
+ expect(folder5.members).toEqual([
+ 'test_view4.renamed_orders_count',
+ 'test_view4.renamed_orders_id',
+ 'test_view4.renamed_orders_number',
+ 'test_view4.renamed_orders_status',
+ 'test_view4.users_age',
+ 'test_view4.users_state',
+ 'test_view4.users_gender',
+ 'test_view4.users_city',
+ 'test_view4.users_renamed_in_view3_gender',
+ ]);
+ });
+
+ it('a nested folders with some * and named members (nested)', async () => {
+ const testView = metaTransformer.cubes.find(
+ (it) => it.config.name === 'test_view4'
+ );
+
+ expect(testView.config.nestedFolders.length).toBe(3);
+
+ const folder1 = testView.config.nestedFolders.find(
+ (it) => it.name === 'folder1'
+ );
+ expect(folder1.members).toEqual([
+ 'test_view4.users_age',
+ 'test_view4.users_state',
+ 'test_view4.renamed_orders_status',
+ ]);
+
+ const folder2 = testView.config.nestedFolders.find(
+ (it) => it.name === 'folder2'
+ );
+ expect(folder2.members).toEqual(
+ expect.arrayContaining(['test_view4.users_city', 'test_view4.users_renamed_in_view3_gender'])
+ );
+
+ const folder3 = testView.config.nestedFolders.find(
+ (it) => it.name === 'folder3'
+ );
+ expect(folder3.members.length).toBe(3);
+ expect(folder3.members[1]).toEqual(
+ { name: 'inner folder 4', members: ['test_view4.renamed_orders_status'] }
+ );
+ expect(folder3.members[2].name).toEqual('inner folder 5');
+ expect(folder3.members[2].members).toEqual([
+ 'test_view4.renamed_orders_count',
+ 'test_view4.renamed_orders_id',
+ 'test_view4.renamed_orders_number',
+ 'test_view4.renamed_orders_status',
+ 'test_view4.users_age',
+ 'test_view4.users_state',
+ 'test_view4.users_gender',
+ 'test_view4.users_city',
+ 'test_view4.users_renamed_in_view3_gender',
+ ]);
+ });
+
it('folders from view extending other view', async () => {
const view2 = metaTransformer.cubes.find(
(it) => it.config.name === 'test_view2'
@@ -93,6 +242,7 @@ describe('Cube Folders', () => {
throw new Error('should throw earlier');
} catch (e: any) {
expect(e.toString()).toMatch(/Paths aren't allowed in the 'folders' but 'users.age' has been provided for test_view/);
+ expect(e.toString()).toMatch(/Paths aren't allowed in the 'folders' but 'users.renamed_gender' has been provided for test_view/);
expect(e.toString()).toMatch(/Member 'users.age' included in folder 'folder1' not found/);
}
});
diff --git a/rust/cubesql/cubeclient/.openapi-generator/FILES b/rust/cubesql/cubeclient/.openapi-generator/FILES
index a499650e840fd..eb8863fbc4d4b 100644
--- a/rust/cubesql/cubeclient/.openapi-generator/FILES
+++ b/rust/cubesql/cubeclient/.openapi-generator/FILES
@@ -8,6 +8,7 @@ src/models/v1_cube_meta_folder.rs
src/models/v1_cube_meta_hierarchy.rs
src/models/v1_cube_meta_join.rs
src/models/v1_cube_meta_measure.rs
+src/models/v1_cube_meta_nested_folder.rs
src/models/v1_cube_meta_segment.rs
src/models/v1_cube_meta_type.rs
src/models/v1_error.rs
diff --git a/rust/cubesql/cubeclient/src/models/mod.rs b/rust/cubesql/cubeclient/src/models/mod.rs
index cb678c35165a0..8b83cf4497450 100644
--- a/rust/cubesql/cubeclient/src/models/mod.rs
+++ b/rust/cubesql/cubeclient/src/models/mod.rs
@@ -12,6 +12,8 @@ pub mod v1_cube_meta_join;
pub use self::v1_cube_meta_join::V1CubeMetaJoin;
pub mod v1_cube_meta_measure;
pub use self::v1_cube_meta_measure::V1CubeMetaMeasure;
+pub mod v1_cube_meta_nested_folder;
+pub use self::v1_cube_meta_nested_folder::V1CubeMetaNestedFolder;
pub mod v1_cube_meta_segment;
pub use self::v1_cube_meta_segment::V1CubeMetaSegment;
pub mod v1_cube_meta_type;
diff --git a/rust/cubesql/cubeclient/src/models/v1_cube_meta.rs b/rust/cubesql/cubeclient/src/models/v1_cube_meta.rs
index ddd19ed03bccd..24557b0eb2613 100644
--- a/rust/cubesql/cubeclient/src/models/v1_cube_meta.rs
+++ b/rust/cubesql/cubeclient/src/models/v1_cube_meta.rs
@@ -33,6 +33,8 @@ pub struct V1CubeMeta {
pub joins: Option>,
#[serde(rename = "folders", skip_serializing_if = "Option::is_none")]
pub folders: Option>,
+ #[serde(rename = "nestedFolders", skip_serializing_if = "Option::is_none")]
+ pub nested_folders: Option>,
#[serde(rename = "hierarchies", skip_serializing_if = "Option::is_none")]
pub hierarchies: Option>,
}
@@ -56,6 +58,7 @@ impl V1CubeMeta {
segments,
joins: None,
folders: None,
+ nested_folders: None,
hierarchies: None,
}
}
diff --git a/rust/cubesql/cubeclient/src/models/v1_cube_meta_nested_folder.rs b/rust/cubesql/cubeclient/src/models/v1_cube_meta_nested_folder.rs
new file mode 100644
index 0000000000000..0ce0fb480b6a7
--- /dev/null
+++ b/rust/cubesql/cubeclient/src/models/v1_cube_meta_nested_folder.rs
@@ -0,0 +1,26 @@
+/*
+ * Cube.js
+ *
+ * Cube.js Swagger Schema
+ *
+ * The version of the OpenAPI document: 1.0.0
+ *
+ * Generated by: https://openapi-generator.tech
+ */
+
+use crate::models;
+use serde::{Deserialize, Serialize};
+
+#[derive(Clone, Default, Debug, PartialEq, Serialize, Deserialize)]
+pub struct V1CubeMetaNestedFolder {
+ #[serde(rename = "name")]
+ pub name: String,
+ #[serde(rename = "members")]
+ pub members: Vec,
+}
+
+impl V1CubeMetaNestedFolder {
+ pub fn new(name: String, members: Vec) -> V1CubeMetaNestedFolder {
+ V1CubeMetaNestedFolder { name, members }
+ }
+}
diff --git a/rust/cubesql/cubesql/benches/large_model.rs b/rust/cubesql/cubesql/benches/large_model.rs
index be7d41f25bc6e..b4e46a2def264 100644
--- a/rust/cubesql/cubesql/benches/large_model.rs
+++ b/rust/cubesql/cubesql/benches/large_model.rs
@@ -93,6 +93,7 @@ pub fn get_large_model_test_meta(dims: usize) -> Vec {
segments: vec![],
joins: None,
folders: None,
+ nested_folders: None,
hierarchies: None,
meta: None,
}]
diff --git a/rust/cubesql/cubesql/src/compile/test/mod.rs b/rust/cubesql/cubesql/src/compile/test/mod.rs
index 9ded03081ede3..f6b3c6d83deb9 100644
--- a/rust/cubesql/cubesql/src/compile/test/mod.rs
+++ b/rust/cubesql/cubesql/src/compile/test/mod.rs
@@ -169,6 +169,7 @@ pub fn get_test_meta() -> Vec {
relationship: "belongsTo".to_string(),
}]),
folders: None,
+ nested_folders: None,
hierarchies: None,
meta: None,
},
@@ -220,6 +221,7 @@ pub fn get_test_meta() -> Vec {
relationship: "belongsTo".to_string(),
}]),
folders: None,
+ nested_folders: None,
hierarchies: None,
meta: None,
},
@@ -241,6 +243,7 @@ pub fn get_test_meta() -> Vec {
segments: vec![],
joins: None,
folders: None,
+ nested_folders: None,
hierarchies: None,
meta: None,
},
@@ -320,6 +323,7 @@ pub fn get_test_meta() -> Vec {
segments: Vec::new(),
joins: Some(Vec::new()),
folders: None,
+ nested_folders: None,
hierarchies: None,
meta: None,
},
@@ -438,6 +442,7 @@ pub fn get_test_meta() -> Vec {
segments: Vec::new(),
joins: Some(Vec::new()),
folders: None,
+ nested_folders: None,
hierarchies: None,
meta: None,
},
@@ -463,6 +468,7 @@ pub fn get_string_cube_meta() -> Vec {
segments: vec![],
joins: None,
folders: None,
+ nested_folders: None,
hierarchies: None,
meta: None,
}]
@@ -507,6 +513,7 @@ pub fn get_sixteen_char_member_cube() -> Vec {
segments: vec![],
joins: None,
folders: None,
+ nested_folders: None,
hierarchies: None,
meta: None,
}]
diff --git a/rust/cubesql/cubesql/src/transport/ctx.rs b/rust/cubesql/cubesql/src/transport/ctx.rs
index 3aa40b97d2d1f..b2645034f204c 100644
--- a/rust/cubesql/cubesql/src/transport/ctx.rs
+++ b/rust/cubesql/cubesql/src/transport/ctx.rs
@@ -273,6 +273,7 @@ mod tests {
segments: vec![],
joins: None,
folders: None,
+ nested_folders: None,
hierarchies: None,
meta: None,
},
@@ -286,6 +287,7 @@ mod tests {
segments: vec![],
joins: None,
folders: None,
+ nested_folders: None,
hierarchies: None,
meta: None,
},