From b91a833f79c8aa7ef85958a288db0480f12e74ba Mon Sep 17 00:00:00 2001 From: akshaykumar2505 Date: Mon, 14 Sep 2026 16:28:46 +0530 Subject: [PATCH 1/2] feat(models): align Cube editor spec with backend 1.7.30 The Models IDE still advertised Cube 1.6.19 after dbx moved to schema-compiler 1.7.30, which showed the spec mismatch banner. Co-authored-by: Cursor --- .../cubejs-language/__tests__/spec.test.ts | 35 ++++- src/utils/cubejs-language/spec.ts | 145 ++++++++++++++++-- 2 files changed, 163 insertions(+), 17 deletions(-) diff --git a/src/utils/cubejs-language/__tests__/spec.test.ts b/src/utils/cubejs-language/__tests__/spec.test.ts index 5ca6473a..fd2e4e61 100644 --- a/src/utils/cubejs-language/__tests__/spec.test.ts +++ b/src/utils/cubejs-language/__tests__/spec.test.ts @@ -2,7 +2,7 @@ * T004: TDD tests for the Cube.js schema spec. * * Validates the static spec exported from ../spec.ts against known - * Cube.js v1.6.19 schema requirements. + * Cube.js v1.7.30 schema requirements. */ import { describe, it, expect } from "vitest"; @@ -24,8 +24,8 @@ function memberType(name: string) { // --------------------------------------------------------------------------- describe("CUBEJS_SPEC_VERSION", () => { - it('equals "1.6.19"', () => { - expect(CUBEJS_SPEC_VERSION).toBe("1.6.19"); + it('equals "1.7.30"', () => { + expect(CUBEJS_SPEC_VERSION).toBe("1.7.30"); }); it("matches the spec version", () => { @@ -67,6 +67,7 @@ describe("cube construct", () => { "preAggregations", "accessPolicy", "hierarchies", + "calendar", ]; for (const k of expected) { expect(keys).toContain(k); @@ -102,6 +103,9 @@ describe("view construct", () => { expect(view.properties.cubes).toBeDefined(); expect(view.properties.folders).toBeDefined(); expect(view.properties.isView).toBeDefined(); + expect(view.properties.viewGroup).toBeDefined(); + expect(view.properties.viewGroups).toBeDefined(); + expect(view.properties.defaultFilters).toBeDefined(); }); it("cubes property has children with view cube item keys", () => { @@ -128,11 +132,18 @@ describe("view construct", () => { describe("dimension types", () => { const dimSpec = memberType("dimensions"); - it("has all 5 dimension types", () => { + it("has all 6 dimension types", () => { expect(dimSpec.typeValues).toEqual( - expect.arrayContaining(["string", "number", "boolean", "time", "geo"]) + expect.arrayContaining([ + "string", + "number", + "boolean", + "time", + "geo", + "switch", + ]) ); - expect(dimSpec.typeValues).toHaveLength(5); + expect(dimSpec.typeValues).toHaveLength(6); }); }); @@ -420,10 +431,18 @@ describe("required properties", () => { describe("access policy", () => { const apChildren = cube.properties.accessPolicy.children!; - it("has role, memberLevel, rowLevel, conditions", () => { + it("has role, group, groups, memberLevel, memberMasking, rowLevel, conditions", () => { const keys = Object.keys(apChildren); expect(keys).toEqual( - expect.arrayContaining(["role", "memberLevel", "rowLevel", "conditions"]) + expect.arrayContaining([ + "role", + "group", + "groups", + "memberLevel", + "memberMasking", + "rowLevel", + "conditions", + ]) ); }); diff --git a/src/utils/cubejs-language/spec.ts b/src/utils/cubejs-language/spec.ts index 5d59d9b6..ba415116 100644 --- a/src/utils/cubejs-language/spec.ts +++ b/src/utils/cubejs-language/spec.ts @@ -1,7 +1,7 @@ /** - * Static Cube.js v1.6.19 schema specification. + * Static Cube.js v1.7.30 schema specification. * - * Extracted from @cubejs-backend/schema-compiler CubeValidator.js. + * Extracted from @cubejs-backend/schema-compiler CubeValidator.ts. * This is the single source of truth for all valid model properties, * used by the language service for completions, hover, and validation. */ @@ -18,7 +18,7 @@ import type { // Version // --------------------------------------------------------------------------- -export const CUBEJS_SPEC_VERSION = "1.6.19"; +export const CUBEJS_SPEC_VERSION = "1.7.30"; // --------------------------------------------------------------------------- // Helper: property factory @@ -251,9 +251,21 @@ const conditionProperties: Record = { }; const accessPolicyProperties: Record = { - role: prop("role", "string", "Role name this policy applies to", { + role: prop("role", "string", "Role name this policy applies to (use group)", { required: true, + deprecated: true, + deprecatedBy: "group", }), + group: prop( + "group", + "string", + "Group name this policy applies to (mutually exclusive with groups)" + ), + groups: prop( + "groups", + "array", + "Group names this policy applies to (mutually exclusive with group)" + ), memberLevel: withYamlKey( prop( "memberLevel", @@ -265,6 +277,17 @@ const accessPolicyProperties: Record = { ), "member_level" ), + memberMasking: withYamlKey( + prop( + "memberMasking", + "object", + "Member masking policy (requires memberLevel)", + { + children: memberLevelProperties, + } + ), + "member_masking" + ), rowLevel: withYamlKey( prop("rowLevel", "object", "Row-level security filters", { children: rowLevelProperties, @@ -319,7 +342,32 @@ const viewFolderItemProperties: Record = { includes: prop( "includes", "array", - 'Members to include ("*" for all, or array of member names)' + 'Members to include ("*" for all, nested folders, or joinPath objects)' + ), + joinPath: withYamlKey( + prop("joinPath", "function", "Join path for a nested folder include"), + "join_path" + ), +}; + +const viewDefaultFilterProperties: Record = { + member: prop( + "member", + "function", + "Member reference for the default filter", + { + required: true, + } + ), + operator: prop("operator", "enum", "Filter operator", { + required: true, + values: filterOperators.filter((op) => op !== "measureFilter"), + }), + values: prop("values", "array", "Filter values"), + unless: prop( + "unless", + "function", + "Skip this default filter when the condition is true" ), }; @@ -341,6 +389,7 @@ const granularityProperties: Record = { "Origin point for the granularity alignment" ), offset: prop("offset", "string", "Offset from the origin for alignment"), + sql: prop("sql", "sql", "SQL expression for a custom granularity"), }; // --------------------------------------------------------------------------- @@ -389,7 +438,7 @@ const geoSubProperties: Record = { const formatObjectProperties: Record = { type: prop("type", "enum", "Format type", { required: true, - values: ["link", "currency", "percent", "number", "id"], + values: ["imageUrl", "link", "currency", "percent", "number", "id"], }), label: prop("label", "string", "Display label for link format"), }; @@ -425,7 +474,7 @@ const dimensionProperties: Record = { sql: prop("sql", "sql", "SQL expression for this dimension"), type: prop("type", "enum", "Data type of this dimension", { required: true, - values: ["string", "number", "boolean", "time", "geo"], + values: ["string", "number", "boolean", "time", "geo", "switch"], }), aliases: prop("aliases", "array", "Alternative names for this dimension"), fieldType: withYamlKey( @@ -476,7 +525,7 @@ const dimensionProperties: Record = { "enum", "Display format for the dimension value (string or object form)", { - values: ["link", "currency", "percent", "number", "id"], + values: ["imageUrl", "link", "currency", "percent", "number", "id"], children: formatObjectProperties, } ), @@ -544,6 +593,44 @@ const dimensionProperties: Record = { ), "add_group_by" ), + values: prop("values", "array", "Allowed values for a switch dimension"), + timeShift: withYamlKey( + prop( + "timeShift", + "array", + "Named time shifts for calendar cube time dimensions", + { + children: { + name: prop("name", "string", "Named time-shift identifier"), + interval: prop( + "interval", + "string", + 'Shift interval (e.g., "1 year")' + ), + type: prop("type", "enum", "Shift type", { + values: ["prior", "next"], + }), + sql: prop("sql", "sql", "Custom SQL for this time shift"), + }, + } + ), + "time_shift" + ), + filter: prop( + "filter", + "object", + "Multi-stage include/exclude filter for this dimension" + ), + links: prop("links", "array", "Related links shown for this dimension"), + mask: prop("mask", "object", "Masking expression for this dimension"), + currency: prop( + "currency", + "string", + "ISO 4217 currency code (number dimensions only)" + ), + order: prop("order", "enum", "Default sort order", { + values: ["asc", "desc"], + }), }; // --------------------------------------------------------------------------- @@ -730,6 +817,22 @@ const measureProperties: Record = { }), "order_by" ), + filter: prop( + "filter", + "object", + "Multi-stage include/exclude filter for this measure" + ), + grain: prop( + "grain", + "object", + "Multi-stage grain include/exclude for this measure" + ), + currency: prop( + "currency", + "string", + "ISO 4217 currency code (numeric measures only)" + ), + mask: prop("mask", "object", "Masking expression for this measure"), }; // --------------------------------------------------------------------------- @@ -1150,6 +1253,11 @@ const cubeProperties: Record = { ), "sql_table" ), + calendar: prop( + "calendar", + "boolean", + "Mark this cube as a calendar cube for custom time dimensions" + ), title: prop("title", "string", "Display title for this cube"), sqlAlias: withYamlKey( prop( @@ -1333,6 +1441,25 @@ const viewProperties: Record = { children: viewFolderItemProperties, } ), + viewGroup: withYamlKey( + prop("viewGroup", "string", "Single view group this view belongs to"), + "view_group" + ), + viewGroups: withYamlKey( + prop("viewGroups", "array", "View groups this view belongs to"), + "view_groups" + ), + defaultFilters: withYamlKey( + prop( + "defaultFilters", + "array", + "Filters applied by default when querying this view", + { + children: viewDefaultFilterProperties, + } + ), + "default_filters" + ), // Views can also define joins, measures, dimensions, segments, preAggregations joins: prop( "joins", @@ -1427,7 +1554,7 @@ const templateVariables: TemplateVariableSpec[] = [ const dimensionTypeSpec: MemberTypeSpec = { name: "dimensions", properties: dimensionProperties, - typeValues: ["string", "number", "boolean", "time", "geo"], + typeValues: ["string", "number", "boolean", "time", "geo", "switch"], }; const measureTypeSpec: MemberTypeSpec = { From 9837c4c0dc0d738ae1931357544adde2c172ca06 Mon Sep 17 00:00:00 2001 From: akshaykumar2505 Date: Mon, 14 Sep 2026 16:44:48 +0530 Subject: [PATCH 2/2] feat(models): complete Cube 1.7 editor keys that still lied Drop required role and runningTotal, and add link/filter/view-include children so autocomplete matches CubeValidator 1.7.30. Co-authored-by: Cursor --- .../__tests__/completionProvider.test.ts | 2 +- .../__tests__/hoverProvider.test.ts | 2 +- .../cubejs-language/__tests__/spec.test.ts | 36 +++++- src/utils/cubejs-language/spec.ts | 104 ++++++++++++++++-- 4 files changed, 132 insertions(+), 12 deletions(-) diff --git a/src/utils/cubejs-language/__tests__/completionProvider.test.ts b/src/utils/cubejs-language/__tests__/completionProvider.test.ts index 2e99dd13..65399103 100644 --- a/src/utils/cubejs-language/__tests__/completionProvider.test.ts +++ b/src/utils/cubejs-language/__tests__/completionProvider.test.ts @@ -272,7 +272,7 @@ describe("property_value completions (measure type)", () => { expect(labelList).toContain("max"); expect(labelList).toContain("countDistinct"); expect(labelList).toContain("countDistinctApprox"); - expect(labelList).toContain("runningTotal"); + expect(labelList).not.toContain("runningTotal"); expect(labelList).toContain("number"); expect(labelList).toContain("numberAgg"); expect(labelList).toContain("rank"); diff --git a/src/utils/cubejs-language/__tests__/hoverProvider.test.ts b/src/utils/cubejs-language/__tests__/hoverProvider.test.ts index 2b787d72..e232f17f 100644 --- a/src/utils/cubejs-language/__tests__/hoverProvider.test.ts +++ b/src/utils/cubejs-language/__tests__/hoverProvider.test.ts @@ -306,7 +306,7 @@ describe("property hover: type in measure", () => { expect(info!.content).toContain("`min`"); expect(info!.content).toContain("`max`"); expect(info!.content).toContain("`countDistinct`"); - expect(info!.content).toContain("`runningTotal`"); + expect(info!.content).not.toContain("`runningTotal`"); // Should NOT contain dimension-only types like 'geo' expect(info!.content).not.toContain("`geo`"); }); diff --git a/src/utils/cubejs-language/__tests__/spec.test.ts b/src/utils/cubejs-language/__tests__/spec.test.ts index fd2e4e61..99f1f359 100644 --- a/src/utils/cubejs-language/__tests__/spec.test.ts +++ b/src/utils/cubejs-language/__tests__/spec.test.ts @@ -164,7 +164,6 @@ describe("measure types", () => { "number", "countDistinct", "countDistinctApprox", - "runningTotal", "string", "boolean", "time", @@ -446,8 +445,10 @@ describe("access policy", () => { ); }); - it("role is required", () => { - expect(apChildren.role.required).toBe(true); + it("role is deprecated in favor of group", () => { + expect(apChildren.role.required).toBe(false); + expect(apChildren.role.deprecated).toBe(true); + expect(apChildren.role.deprecatedBy).toBe("group"); }); it("memberLevel has includes/excludes children", () => { @@ -486,3 +487,32 @@ describe("hierarchies", () => { expect(hierSpec.properties.levels.required).toBe(true); }); }); + +describe("cube 1.7 leftovers", () => { + it("does not offer runningTotal as a measure type", () => { + expect(memberType("measures").typeValues).not.toContain("runningTotal"); + }); + + it("dimensions include key, keyReference, and link children", () => { + const dimProps = memberType("dimensions").properties; + expect(dimProps.key).toBeDefined(); + expect(dimProps.keyReference).toBeDefined(); + expect(Object.keys(dimProps.links.children!)).toEqual( + expect.arrayContaining(["name", "label", "url", "dashboard"]) + ); + }); + + it("measure filter has include/exclude/keepOnly children", () => { + const filter = memberType("measures").properties.filter; + expect(Object.keys(filter.children!)).toEqual( + expect.arrayContaining(["mode", "include", "exclude", "keepOnly"]) + ); + }); + + it("view cube includes support name/alias/title objects", () => { + const includes = view.properties.cubes.children!.includes; + expect(Object.keys(includes.children!)).toEqual( + expect.arrayContaining(["name", "alias", "title", "description"]) + ); + }); +}); diff --git a/src/utils/cubejs-language/spec.ts b/src/utils/cubejs-language/spec.ts index ba415116..bf71d63a 100644 --- a/src/utils/cubejs-language/spec.ts +++ b/src/utils/cubejs-language/spec.ts @@ -221,6 +221,14 @@ const memberLevelProperties: Record = { const rowLevelFilterProperties: Record = { member: prop("member", "string", "Member reference for the filter"), + memberReference: withYamlKey( + prop( + "memberReference", + "string", + "Explicit member name when member is a function" + ), + "member_reference" + ), operator: prop("operator", "enum", "Filter operator", { values: filterOperators, }), @@ -252,7 +260,6 @@ const conditionProperties: Record = { const accessPolicyProperties: Record = { role: prop("role", "string", "Role name this policy applies to (use group)", { - required: true, deprecated: true, deprecatedBy: "group", }), @@ -303,6 +310,21 @@ const accessPolicyProperties: Record = { // View cubes item properties // --------------------------------------------------------------------------- +const viewCubeIncludeItemProperties: Record = { + name: prop("name", "string", "Member name to include", { required: true }), + alias: prop("alias", "string", "Alias for the included member"), + title: prop("title", "string", "Display title for the included member"), + description: prop( + "description", + "string", + "Description of the included member" + ), + format: prop("format", "enum", "Display format override", { + values: ["imageUrl", "link", "currency", "percent", "number", "id"], + }), + meta: prop("meta", "object", "Arbitrary metadata for the included member"), +}; + const viewCubeItemProperties: Record = { joinPath: withYamlKey( prop("joinPath", "function", "Join path reference to the cube", { @@ -324,7 +346,10 @@ const viewCubeItemProperties: Record = { includes: prop( "includes", "array", - 'Members to include ("*" for all, or array of member names)' + 'Members to include ("*" for all, names, or objects with name/alias/title)', + { + children: viewCubeIncludeItemProperties, + } ), excludes: prop( "excludes", @@ -425,6 +450,56 @@ const caseProperties: Record = { // Geo sub-properties // --------------------------------------------------------------------------- +const linkItemProperties: Record = { + name: prop("name", "string", "Link identifier", { required: true }), + label: prop("label", "string", "Display label", { required: true }), + url: prop( + "url", + "function", + "URL expression (mutually exclusive with dashboard)" + ), + dashboard: prop( + "dashboard", + "string", + "Dashboard path (mutually exclusive with url)" + ), + icon: prop("icon", "string", "Icon name"), + target: prop("target", "enum", "Link target", { + values: ["blank", "self"], + }), + primary: prop("primary", "boolean", "Mark this as the primary link"), + params: prop("params", "array", "Query parameters for the link"), +}; + +const multiStageFilterProperties: Record = { + mode: prop("mode", "enum", "How include filters are applied", { + values: ["relative", "fixed"], + }), + include: prop( + "include", + "array", + "Predicates to keep in this multi-stage step" + ), + exclude: prop("exclude", "function", "Members to exclude from this step"), + keepOnly: withYamlKey( + prop("keepOnly", "function", "Members to keep in this step"), + "keep_only" + ), +}; + +const multiStageGrainProperties: Record = { + include: prop("include", "function", "Grain members to include"), + exclude: prop("exclude", "function", "Grain members to exclude"), + keepOnly: withYamlKey( + prop("keepOnly", "function", "Grain members to keep"), + "keep_only" + ), +}; + +// --------------------------------------------------------------------------- +// Geo sub-properties +// --------------------------------------------------------------------------- + const geoSubProperties: Record = { sql: prop("sql", "sql", "SQL expression for the coordinate value", { required: true, @@ -619,9 +694,14 @@ const dimensionProperties: Record = { filter: prop( "filter", "object", - "Multi-stage include/exclude filter for this dimension" + "Multi-stage include/exclude filter for this dimension", + { + children: multiStageFilterProperties, + } ), - links: prop("links", "array", "Related links shown for this dimension"), + links: prop("links", "array", "Related links shown for this dimension", { + children: linkItemProperties, + }), mask: prop("mask", "object", "Masking expression for this dimension"), currency: prop( "currency", @@ -631,6 +711,11 @@ const dimensionProperties: Record = { order: prop("order", "enum", "Default sort order", { values: ["asc", "desc"], }), + key: prop("key", "function", "Unique key expression for this dimension"), + keyReference: withYamlKey( + prop("keyReference", "string", "Member name used as the unique key"), + "key_reference" + ), }; // --------------------------------------------------------------------------- @@ -649,7 +734,6 @@ const measureTypes = [ "max", "countDistinct", "countDistinctApprox", - "runningTotal", ]; const measureTypesMultiStage = [...measureTypes, "numberAgg", "rank"]; @@ -820,12 +904,18 @@ const measureProperties: Record = { filter: prop( "filter", "object", - "Multi-stage include/exclude filter for this measure" + "Multi-stage include/exclude filter for this measure", + { + children: multiStageFilterProperties, + } ), grain: prop( "grain", "object", - "Multi-stage grain include/exclude for this measure" + "Multi-stage grain include/exclude for this measure", + { + children: multiStageGrainProperties, + } ), currency: prop( "currency",