Skip to content

feat: Links in the data model#10852

Open
paveltiunov wants to merge 18 commits into
masterfrom
cursor/feat-links-documentation-638d
Open

feat: Links in the data model#10852
paveltiunov wants to merge 18 commits into
masterfrom
cursor/feat-links-documentation-638d

Conversation

@paveltiunov
Copy link
Copy Markdown
Member

@paveltiunov paveltiunov commented May 10, 2026

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

Implements the links feature for dimensions in the data model, as specified in #10203.

Design

Links are implemented as synthetic dimensions. Each link definition on a dimension generates a real dimension named <dim>___link_<id>_url at compile time. This means:

  • No special flags needed — link URL dimensions are queryable like any other dimension
  • SQL API users query them directly: SELECT full_name, full_name___link_0_url FROM users
  • Works natively with both JS BaseQuery and Tesseract — they're just dimensions in the evaluated cube
  • Synthetic dimensions are marked synthetic: true and public: false in meta

The url field is a standard SQL expression (like mask.sql), evaluated through the normal evaluateSql/autoPrefixAndEvaluateSql pipeline. Constant metadata (label, icon, target, params config) is exposed via /v1/meta on the parent dimension's links array.

Documentation Changes

  • Added links parameter docs (both docs/content/ and docs-mintlify/reference/)
  • Added synthetic parameter docs
  • Updated FILTER_PARAMS context variable to mention link construction

Code Changes

Schema Compiler (packages/cubejs-schema-compiler):

  • CubeValidator.ts: links validation schema — url is Joi.func() (SQL expression)
  • CubeEvaluator.ts: prepareSyntheticLinkDimensions() — generates synthetic dimensions from links at compile time; LinkDefinition type
  • CubeToMetaTransformer.ts: Exposes links metadata and synthetic flag on dimensions in /v1/meta

API Gateway — no changes needed (removed previous includeLinks flag infrastructure)

Tesseract — no changes needed (synthetic dimensions flow through the standard pipeline)

Open in Web Open in Cursor 

@mintlify
Copy link
Copy Markdown
Contributor

mintlify Bot commented May 10, 2026

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

Project Status Preview Updated (UTC)
cubed3 🟢 Ready View Preview May 10, 2026, 12:12 AM

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

@claude
Copy link
Copy Markdown
Contributor

claude Bot commented May 10, 2026

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


Review: Documentation for links, html, and synthetic dimension parameters

  • Gather context and read changed files
  • Review documentation accuracy and consistency
  • Check for issues (formatting, broken references, cross-site consistency)
  • Post review feedback

Overall this is well-written documentation — clear structure, good examples, and consistent coverage across both doc sites. A few issues to address:


Issues

1. Broken link: ref-references in Mintlify docs

In docs-mintlify/reference/data-modeling/dimensions.mdx, the reference link is:

[ref-references]: /docs/data-modeling/syntax#references

But the Mintlify syntax page lives at docs/data-modeling/concepts/syntax (per docs.json). This should be:

[ref-references]: /docs/data-modeling/concepts/syntax#references

This link is used twice in the new content (lines 438 and 480). The Next.js version (/product/data-modeling/syntax#references) is correct.

Fix this →

2. Missing trailing newline in all 4 changed files

All four files end without a trailing newline (\ No newline at end of file in the diff). This was a pre-existing issue for some files, but the PR extends it to the newly added reference link lines. POSIX convention and most linters expect a final newline.

Fix this →


Suggestions

3. Ambiguous #### Dimensions sub-headings (also left as inline comment)

The #### Dimensions heading appears under both ### links and ### html to describe the synthetic dimensions generated in the result set. Since the page itself is about dimensions, this heading reads as a circular reference. Consider something more descriptive like #### Generated dimensions or #### Synthetic dimensions in the result set. Applies to both doc sites (4 occurrences total).

4. The html example with inline Jinja-in-Jinja may confuse MDX parsers

The inline example at ~line 582 (mintlify) / ~line 448 (next.js):

html: {{ '{% if "{full_name}" | length > 10 %}...' }}

The double curly braces {{ ... }} are also MDX/JSX expression syntax. If either doc framework processes this before rendering the code fence, it could break. Worth verifying the preview renders correctly on both sites. If it doesn't, wrapping in a raw block or escaping may be needed.


Observations (non-blocking)

  • Correct component usage: Mintlify files use <Info> / <Warning> / <Note> while Next.js files use <InfoBox> / <WarningBox> / <ReferenceBox> — both are correct for their respective platforms.
  • Correct link namespacing: Reference links use /docs/... paths for Mintlify and /product/... paths for Next.js, which is the expected convention.
  • Content parity is good: The prose, examples, and structure are identical across both doc sites (modulo the expected component and path differences), which is great for maintainability.
  • meta description improvement: The rewording from "Custom metadata. Can be used to pass any information to the frontend." to "allows you to attach arbitrary information to a dimension. It can be consumed and interpreted by supporting tools." is a nice improvement. Note that the Mintlify version already had an additional ai_context paragraph that the Next.js version lacks — this is a pre-existing discrepancy, not introduced by this PR.

Comment on lines +525 to +530
param_name_for_filters: my_precious_filters
```

#### Dimensions
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.

Suggestion: Ambiguous heading. The #### Dimensions heading is used as a sub-section under both links and html to describe the synthetic dimensions generated in the result set. Since this appears inside a page titled "Dimensions," the heading reads like a circular reference. Consider a more descriptive title like #### Generated dimensions or #### Synthetic dimensions in the result set to make it immediately clear what this sub-section covers.

@codecov
Copy link
Copy Markdown

codecov Bot commented May 10, 2026

Codecov Report

❌ Patch coverage is 80.00000% with 3 lines in your changes missing coverage. Please review.
✅ Project coverage is 58.46%. Comparing base (346eb56) to head (5d15a9a).
⚠️ Report is 6 commits behind head on master.

Files with missing lines Patch % Lines
...bejs-schema-compiler/src/compiler/CubeEvaluator.ts 75.00% 1 Missing and 1 partial ⚠️
...cubejs-schema-compiler/src/compiler/CubeSymbols.ts 0.00% 0 Missing and 1 partial ⚠️

❗ There is a different number of reports uploaded between BASE (346eb56) and HEAD (5d15a9a). Click for more details.

HEAD has 1 upload less than BASE
Flag BASE (346eb56) HEAD (5d15a9a)
cubesql 1 0
Additional details and impacted files
@@             Coverage Diff             @@
##           master   #10852       +/-   ##
===========================================
- Coverage   78.93%   58.46%   -20.48%     
===========================================
  Files         470      216      -254     
  Lines       92862    17008    -75854     
  Branches     3449     3456        +7     
===========================================
- Hits        73304     9943    -63361     
+ Misses      19054     6559    -12495     
- Partials      504      506        +2     
Flag Coverage Δ
cube-backend 58.46% <80.00%> (+0.01%) ⬆️
cubesql ?

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.

@claude
Copy link
Copy Markdown
Contributor

claude Bot commented May 17, 2026

Claude finished @paveltiunov's task in 10m 8s —— View job


Comprehensive Code Review — PR #10852

  • Gather context and read all changed files
  • Review code quality and correctness
  • Review security considerations
  • Review performance considerations
  • Review test coverage and quality
  • Review documentation changes
  • Post final review

Summary

The "links as synthetic dimensions" design is clean and well-chosen — it avoids special-casing in the query pipeline and gets Tesseract support for free. The core implementation in CubeEvaluator.prepareSyntheticLinkDimensions is straightforward and the compilation ordering (synthetic dims created before prepareMembers) is correct. The meta transformer correctly strips the url function from the public API while exposing the static metadata.

The refactoring across commits shows good iterative improvement — the final approach of generating synthetic dims after view include/exclude logic (commit 9302eea) is cleaner than the earlier auto-include approach.


Issues Found

1. Link name not validated against identifier regex (Bug — inline comment on CubeValidator.ts:315)

The name field is Joi.string().required() but it's used to construct dimension identifiers (${dimName}___link_${link.name}_url). Names with spaces, hyphens, or other non-identifier characters will produce invalid dimension names. Should be constrained to /^[_a-zA-Z][_a-zA-Z0-9]*$/.

Fix this →

2. No duplicate link name detection (Bug — inline comment on CubeEvaluator.ts:232-244)

If two links on the same dimension share the same name, the second silently overwrites the first synthetic dimension. Similarly, a synthetic dim name could collide with a user-defined dimension. Both cases should produce a compilation error.

3. params type inconsistency between validator, types, and docs (Design question — inline comments on CubeValidator.ts:320 and CubeEvaluator.ts:34-43)

The docs show params values using {id} and {FILTER_PARAMS.users.country} references, which are normally compiled to functions. But the validator uses Joi.string() and the TypeScript type uses Record<string, string>. This needs clarification: are params SQL expressions (→ use Joi.func()) or static metadata for the consuming tool (→ update docs to remove SQL reference links)?

4. Missing test coverage for views with links (Gap — inline comment on links.test.ts)

This is the most significant test gap. The links property is propagated to views (CubeSymbols.ts:1007) and prepareSyntheticLinkDimensions runs on views, but there's no test verifying that:

  • Synthetic link dims are correctly generated on a view
  • They're queryable through the view
  • They don't trigger the "View defines own member" error in prepareMembers

Additional missing scenarios: duplicate link names, collision with user-defined dimensions, links with params.

5. Duplicate type definition (Minor — inline comment on CubeToMetaTransformer.ts:51-62)

The link type shape is defined both in CubeEvaluator.ts (LinkDefinition) and CubeToMetaTransformer.ts (ExtendedCubeSymbolDefinition.links). Consider importing and reusing LinkDefinition to avoid drift.


What Looks Good

  • Compilation ordering: Generating synthetic dims before prepareMembers ensures they get full member processing (ownership checks, alias detection, etc.).
  • View integration approach: Propagating links as metadata and generating synthetic dims in prepareCube (which runs for both cubes and views) is the right call — cleaner than the earlier auto-include approach.
  • Meta API design: Correctly exposes links metadata (label, icon, target, params) and synthetic flag while keeping url (a SQL function) out of the public API.
  • Validation schema: url correctly uses Joi.func() since it's a SQL expression compiled to a function.
  • Tests: The existing tests cover the core happy path well — synthetic dim creation, SQL generation, opt-in behavior (not included unless queried), meta exposure, and schema validation.
  • Documentation: Thorough coverage across both doc sites with content parity. Examples are practical and demonstrate real use cases (Google search, Salesforce, mailto, dashboard links with params).

Previous Review Items (from the earlier doc review)

The earlier review by Claude flagged:

  1. Broken ref-references link in Mintlify docs (should be /docs/data-modeling/concepts/syntax#references)
  2. Missing trailing newlines
  3. Ambiguous #### Dimensions heading

These still appear to be outstanding. Items 1 and 3 are worth addressing before merge.

]);

const LinkItemSchema = Joi.object().keys({
name: Joi.string().required(),
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.

Bug: name should be validated against the identifier regex.

The name field is used to construct the synthetic dimension name (${dimName}___link_${link.name}_url), which becomes a queryable dimension identifier. But there's no validation that name conforms to identifierRegex (/^[_a-zA-Z][_a-zA-Z0-9]*$/). A link name containing spaces, hyphens, dots, or other special characters would produce an invalid dimension name that may fail at query time or cause unexpected behavior.

Suggested change
name: Joi.string().required(),
name: Joi.string().regex(/^[_a-zA-Z][_a-zA-Z0-9]*$/, 'identifier').required(),

Comment on lines +232 to +244
for (const [dimName, dimDef] of Object.entries<any>(cube.dimensions)) {
if (dimDef.links && Array.isArray(dimDef.links)) {
dimDef.links.forEach((link: any) => {
const syntheticName = `${dimName}___link_${link.name}_url`;
syntheticDims[syntheticName] = {
sql: link.url,
type: 'string',
synthetic: true,
ownedByCube: true,
public: false,
};
});
}
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.

Issue: No duplicate link name detection.

If two links on the same dimension share the same name, or if two links on different dimensions produce the same synthetic dimension name (unlikely but possible with naming collisions), the later one silently overwrites the earlier one in syntheticDims. Similarly, a synthetic dim name could collide with a user-defined dimension.

Consider adding a check:

if (syntheticDims[syntheticName] || cube.dimensions[syntheticName]) {
  // warn or error about collision
}

Also, duplicate name values within a single dimension's links array should ideally be caught at the validator level (e.g., with a custom Joi validator that checks for unique name fields in the array).

url: Joi.func().required(),
icon: Joi.string(),
target: Joi.string().valid('blank', 'self'),
params: Joi.object().pattern(Joi.string(), Joi.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.

Question: Should params values support SQL expression references?

The docs describe params values using {id} and {FILTER_PARAMS.users.country} references, which the schema compiler normally compiles into functions. However, the validator expects Joi.string() for param values, not Joi.func().

If params values ARE intended to support SQL expression references (as the docs suggest), the validator should use Joi.func(). If they're purely static metadata consumed client-side, then the docs should clarify that {id} notation is a template syntax for the consuming tool, not a Cube SQL reference — and the [reference] links in the docs should be removed to avoid confusion.

Comment on lines 51 to +62
aggType?: string;
keyReference?: string;
currency?: string;
links?: Array<{
name: string;
label: string;
url: (...args: any[]) => string;
icon?: string;
target?: 'blank' | 'self';
params?: Record<string, string>;
propagate_filters_to_params?: boolean;
param_name_for_filters?: 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.

Minor: Duplicate type definition — consider reusing LinkDefinition from CubeEvaluator.ts.

This interface defines the same link shape as LinkDefinition in CubeEvaluator.ts. Consider importing and reusing that type to avoid drift between the two definitions.

Comment on lines +1 to +147
import { PostgresQuery } from '../../src';
import { prepareYamlCompiler } from './PrepareCompiler';

describe('Links', () => {
const schemaWithLinks = `
cubes:
- name: users
sql_table: users

dimensions:
- name: id
sql: id
type: number
primary_key: true

- name: full_name
sql: full_name
type: string
links:
- name: google_search
label: Search on Google
url: "CONCAT('https://www.google.com/search?q=', {CUBE}.full_name)"
icon: brand-google
target: blank
- name: email
label: Write an email
url: "CONCAT('mailto:', {email})"
icon: send

- name: email
sql: email
type: string
`;

it('should create synthetic link URL dimensions', async () => {
const compilers = prepareYamlCompiler(schemaWithLinks);
await compilers.compiler.compile();

const googleDef = compilers.cubeEvaluator.dimensionByPath('users.full_name___link_google_search_url');
expect(googleDef).toBeDefined();
expect(googleDef.type).toBe('string');
expect((googleDef as any).synthetic).toBe(true);

const emailDef = compilers.cubeEvaluator.dimensionByPath('users.full_name___link_email_url');
expect(emailDef).toBeDefined();
expect(emailDef.type).toBe('string');
expect((emailDef as any).synthetic).toBe(true);
});

it('should generate correct SQL when synthetic link dimension is queried', async () => {
const compilers = prepareYamlCompiler(schemaWithLinks);
await compilers.compiler.compile();

const query = new PostgresQuery(compilers, {
measures: [],
dimensions: ['users.full_name', 'users.full_name___link_google_search_url'],
});

const queryAndParams = query.buildSqlAndParams();
const sql = queryAndParams[0];

expect(sql).toContain('"users__full_name___link_google_search_url"');
expect(sql).toContain('https://www.google.com/search?q=');
expect(sql).toContain('"users".full_name');
});

it('should NOT include link URL columns unless explicitly queried', async () => {
const compilers = prepareYamlCompiler(schemaWithLinks);
await compilers.compiler.compile();

const query = new PostgresQuery(compilers, {
measures: [],
dimensions: ['users.full_name'],
});

const queryAndParams = query.buildSqlAndParams();
const sql = queryAndParams[0];

expect(sql).not.toContain('___link_');
});

it('should expose links metadata and synthetic flag in meta config', async () => {
const compilers = prepareYamlCompiler(schemaWithLinks);
await compilers.compiler.compile();

const metaTransformer = compilers.metaTransformer;
const cubes = metaTransformer.cubes;
const usersCube = cubes.find((c: any) => c.config.name === 'users');
expect(usersCube).toBeDefined();

const fullNameDim = usersCube!.config.dimensions.find(
(d: any) => d.name === 'users.full_name'
);
expect(fullNameDim).toBeDefined();
expect(fullNameDim!.links).toBeDefined();
expect(fullNameDim!.links).toHaveLength(2);
expect(fullNameDim!.links![0].label).toBe('Search on Google');
expect(fullNameDim!.links![0].icon).toBe('brand-google');
expect(fullNameDim!.links![0].target).toBe('blank');

const syntheticDim = usersCube!.config.dimensions.find(
(d: any) => d.name === 'users.full_name___link_google_search_url'
);
expect(syntheticDim).toBeDefined();
expect(syntheticDim!.synthetic).toBe(true);
});

it('synthetic link dimensions should not be public by default', async () => {
const compilers = prepareYamlCompiler(schemaWithLinks);
await compilers.compiler.compile();

const metaTransformer = compilers.metaTransformer;
const cubes = metaTransformer.cubes;
const usersCube = cubes.find((c: any) => c.config.name === 'users');
expect(usersCube).toBeDefined();

const syntheticDim = usersCube!.config.dimensions.find(
(d: any) => d.name === 'users.full_name___link_google_search_url'
);
expect(syntheticDim).toBeDefined();
expect(syntheticDim!.public).toBe(false);
});

it('should validate links schema - label is required', async () => {
const invalidSchema = `
cubes:
- name: users
sql_table: users

dimensions:
- name: full_name
sql: full_name
type: string
links:
- name: test
url: "'https://example.com'"
`;
const compilers = prepareYamlCompiler(invalidSchema);

try {
await compilers.compiler.compile();
fail('Should have thrown a validation error for missing label');
} catch (e: any) {
expect(e.message || e.toString()).toContain('label');
}
});
});
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.

Missing test coverage for views, edge cases, and params.

The existing tests cover the happy path for cubes well. However, several important scenarios are missing:

  1. Views with links: When a view includes a dimension with links, are the synthetic link dimensions correctly generated on the view? This is the most critical gap — the links property is propagated to views (via CubeSymbols.ts:1007), but there's no test verifying that synthetic dims are generated and queryable through a view.

  2. Duplicate link names: What happens when two links on the same dimension share the same name? Currently the second silently overwrites the first.

  3. Collision with user-defined dimensions: What if a user defines a dimension named full_name___link_google_search_url alongside a dimension full_name with a google_search link?

  4. Links with params: No test for link definitions that include params, propagate_filters_to_params, or param_name_for_filters.

  5. Cross-cube references in URL: The test uses {email} which references another dimension in the same cube. Testing with cross-cube references (e.g., {other_cube.dim}) would verify the foreign cube check behavior.

Comment on lines +34 to +43
export type LinkDefinition = {
name: string;
label: string;
url: (...args: any[]) => string;
icon?: string;
target?: 'blank' | 'self';
params?: Record<string, string>;
propagate_filters_to_params?: boolean;
param_name_for_filters?: 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.

Observation: params values typed as Record<string, string> but docs show SQL expression references.

The docs show params values like "{id}" and "{FILTER_PARAMS.users.country}". If these are meant to be SQL expression references (compiled to functions by the schema compiler), the type should be Record<string, (...args: any[]) => string>. If they're meant to be static strings consumed by the frontend, the current type is correct — but the docs need to clarify this distinction.

This ties into the validator question on CubeValidator.ts:320.

Copy link
Copy Markdown
Member

FYI @gleb

Comment on lines +107 to +121
it('synthetic link dimensions should not be public by default', async () => {
const compilers = prepareYamlCompiler(schemaWithLinks);
await compilers.compiler.compile();

const { metaTransformer } = compilers;
const { cubes } = metaTransformer;
const usersCube = cubes.find((c: any) => c.config.name === 'users');
expect(usersCube).toBeDefined();

const syntheticDim = usersCube!.config.dimensions.find(
(d: any) => d.name === 'users.full_name___link_google_search_url'
);
expect(syntheticDim).toBeDefined();
expect(syntheticDim!.public).toBe(false);
});
Copy link
Copy Markdown
Member

@igorlukanin igorlukanin May 19, 2026

Choose a reason for hiding this comment

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

@paveltiunov Are you sure? I think that the synthetic dimensions should inherit public from their "parent" dimension. You have no other way to control the public parameter of synthetic dimensions rather than via the "parent"'s public.

@cursor cursor Bot force-pushed the cursor/feat-links-documentation-638d branch from ea00aea to 4b498f8 Compare May 19, 2026 19:56
@mngr
Copy link
Copy Markdown

mngr commented May 20, 2026

Hey @paveltiunov, I'm starting CUB-557 ("Links between dashboards"), which sits on top of this PR. There are four things I'd like to change before this lands. Putting them here with the reasoning so we can chat 🙂

The big one: this primitive is great for external links on dimensions (Google, Salesforce, mailto, internal tools…), but the headline examples and a couple of schema fields imply dashboard-to-dashboard navigation is also a thing it does. We think that's the wrong layer for that use case — it should live in dashboard config, not in the data model. Most of the requests below fall out of that.


1. Add primary: boolean to a link

A dimension can have several external links (Google + Salesforce + mailto, say). For our UX, exactly one of them gets the "default click on the cell value" slot. Right now the only way to express that is array order, which is implicit and silently flips if anyone reorders the YAML — no validation warning, just different behavior in prod.

Concretely, in packages/cubejs-schema-compiler/src/compiler/CubeValidator.ts:

const LinkItemSchema = Joi.object().keys({
  // ...existing fields...
  primary: Joi.boolean().strict(),
});

const LinksSchema = Joi.array()
  .items(LinkItemSchema)
  .custom((value, helpers) => {
    const primaryCount = value.filter((l: any) => l.primary === true).length;
    if (primaryCount > 1) return helpers.error('links.multiplePrimary');
    return value;
  });

Thread it through LinkDefinition (CubeEvaluator.ts), LinkConfig (CubeToMetaTransformer.ts), and the /v1/meta output. A test asserting "two primary: true → validation error" would round it out.


2. Drop the dashboard-URL examples from the docs

The current links section in dimensions.mdx (both docs-mintlify/ and docs/content/) leans heavily on dashboard URLs:

- name: performance
  label: Check performance dashboard
  url: "'https://my-account.cubecloud.dev/new/d/123/dashboards/KSqDYdUz6Ble'"
  params:
    filter_user_id: "{id}"
    filter_country: "{FILTER_PARAMS.users.country}"

We're not going to support that pattern in Cube Cloud dashboards. Four reasons:

  1. Permissions — data-model edits need schema-write access; dashboard authors usually don't have it. A drill-in link shouldn't require a YAML round-trip with a data engineer.
  2. Context — the same customer_id should drill into different dashboards from different parents (Sales Overview → Pipeline Detail vs. Support Overview → Support Detail). A single data-model link can't capture that.
  3. URL fragility — hardcoded host + deployment ID breaks across environments and assumes the modeler knows the dashboard ID by hand.
  4. Authoring — picking a destination dashboard belongs in a UI that can browse what exists, not a YAML string field.

If we ship the example as-is, customers will write dashboard links in YAML, find that Cube Cloud's dashboard UI doesn't recognize them as in-app navigation, and file support tickets. CUB-557 is going to deliver dashboard-to-dashboard linking at the dashboard config layer where the UX/permissions/discovery stories are sound.

Could we swap the example for something generic (CRM lookup, internal tool, ticketing system) and add a one-liner: "For dashboard-to-dashboard navigation in Cube Cloud, configure internal links in the dashboard editor — not in the data model"?


3. Drop propagate_filters_to_params and param_name_for_filters (or at least scope them)

These two only exist to serialize the current dashboard's filter state into the outgoing URL. Once #2 lands and dashboard-to-dashboard moves out of this layer, they have no consumer. They also signal "this primitive supports cross-dashboard filter propagation," which directly contradicts the doc change we're asking for in #2.

If they stay, the docs and the schema disagree about what this thing does, and Cube Cloud dashboards will explicitly not honor them — that's a ticket farm.

Preference: remove both from LinkItemSchema, LinkDefinition, LinkConfig, the meta transformer, and the docs. If removing them is awkward at this point, the fallback is to rename + scope-restrict in docs ("for external services that accept a JSON filter blob; ignored by Cube Cloud"), but removal is the cleaner call IMO.


4. Figure out per-row params resolution

The docs say params values can reference per-row dimension values (filter_user_id: "{id}"), but the implementation only generates a synthetic dim for the url field. The params map gets exposed in /v1/meta as a Record<string, string> of templated strings, with no per-row resolution.

Two readings of the current state:

  • The docs are aspirational and the implementation is incomplete.
  • The frontend is expected to substitute {id} by separately querying that dimension and matching it client-side. That's fragile (the frontend has to auto-add referenced dims to every query the user didn't ask for) and breaks {FILTER_PARAMS.users.country} entirely (totally separate resolution path).

Two ways to fix:

  • (a) — preferred. One synthetic dim per templated params entry: <dim>___link_<name>_param_<key>. SQL expression = the templated value. Frontend reads each per-row and assembles the URL.
  • (b) Bake all params (after substitution + URL-encoding) into the url synthetic dim itself, server-side. Frontend reads one fully-formed URL per row.

Either works for us. Current state (template strings exposed verbatim with no resolution path) doesn't.


Nits / non-blockers

  • dashboard: <id-or-slug> shortcut would've been nice for ergonomic same-workspace links, but moot once MUTEX for requests #2 pulls dashboard-to-dashboard out of this layer.

Happy to chat here, Slack, or sync. Once these settle, the primitive is exactly what we want for external links from a dashboard cell 👍

cursoragent and others added 10 commits May 20, 2026 17:08
Add documentation for the new `links`, `html`, and `synthetic` parameters
on dimensions. Links allow defining navigable URLs associated with dimension
values, rendered as synthetic dimensions in the result set. HTML fragments
enable rich formatting beyond the `format` parameter. The `synthetic`
parameter marks auto-generated dimensions.

Also update the `meta` parameter description and add a cross-reference from
the FILTER_PARAMS context variable documentation to the new links feature.

Changes applied to both the Next.js docs (docs/content/) and the Mintlify
docs (docs-mintlify/reference/).

Co-authored-by: Pavel Tiunov <pavel.tiunov@gmail.com>
Co-authored-by: Pavel Tiunov <pavel.tiunov@gmail.com>
- Add links validation schema to CubeValidator.ts (label, url, icon,
  target, params, propagate_filters_to_params, param_name_for_filters)
- Add LinkDefinition type to CubeEvaluator.ts
- Add links to DimensionConfig and ExtendedCubeSymbolDefinition types
  in CubeToMetaTransformer.ts for /v1/meta exposure
- Generate synthetic link URL columns in BaseQuery.js when includeLinks
  option is set (only url is rendered as SQL; label/icon/target are
  constant metadata exposed via /v1/meta)
- Add includeLinks flag to Query type and query validation schema
- Wire includeLinks through /v1/cubesql endpoint via request options
  stored on SQLServer, injected into the sql callback query
- Add unit tests for links feature

Co-authored-by: Pavel Tiunov <pavel.tiunov@gmail.com>
The url field in links is now a SQL function (like mask.sql) that gets
evaluated through the standard evaluateSql/autoPrefixAndEvaluateSql
pipeline. This means:

- url uses standard {CUBE}.column and {dimension} references
- url supports any SQL expression (CONCAT, CASE, etc.)
- No custom template parsing is needed

The url is no longer exposed in /v1/meta (it's a server-side SQL
expression). Only constant metadata (label, icon, target, params config)
is exposed in meta. The computed URL value appears only as a SQL column
in query results when includeLinks is set.

Co-authored-by: Pavel Tiunov <pavel.tiunov@gmail.com>
- Add include_links to BaseQueryOptionsStatic and pass it through
  BaseQuery → QueryTools
- Create LinkItem bridge (cube_bridge/link_item.rs) with url sql field
- Add links() method to DimensionDefinition bridge trait
- Compile link url SQL calls in DimensionSymbolFactory and store them
  as link_url_sqls on DimensionSymbol
- Add includeLinks to buildSqlAndParamsRust query params in BaseQuery.js

The link URL SQL expressions are compiled and stored on DimensionSymbol,
ready to be projected as additional columns when the query processor
handles include_links. The actual projection in the physical plan builder
will emit these as synthetic columns alongside their parent dimension.

Co-authored-by: Pavel Tiunov <pavel.tiunov@gmail.com>
…ed injection

Links are now proper synthetic dimensions generated at compile time in
CubeEvaluator.prepareSyntheticLinkDimensions(). Each link definition
creates a dimension named <dim>___link_<id>_url with the link's url
SQL expression as its sql property.

This means:
- No special flag needed (removed includeLinks from Query type, API
  gateway, sql-server, BaseQuery)
- Users of SQL API query link URLs as regular dimensions
- Works natively with both JS BaseQuery and Tesseract (they're just
  dimensions in the evaluated cube)
- Synthetic dimensions are marked with synthetic:true and public:false

Removed all Tesseract-specific link plumbing (include_links, LinkItem
bridge, link_url_sqls on DimensionSymbol) since synthetic dimensions
flow through the standard dimension pipeline.

Co-authored-by: Pavel Tiunov <pavel.tiunov@gmail.com>
Each link now requires a 'name' property (in addition to 'label') that
serves as the identifier in the synthetic dimension name:
  <dimension>___link_<name>_url

This gives meaningful, stable column names instead of index-based ones.
Example: full_name___link_google_search_url

Co-authored-by: Pavel Tiunov <pavel.tiunov@gmail.com>
…in a view

When a dimension with links is included in a view (via explicit includes
list), its synthetic link dimensions are now automatically included as
well. This mirrors how hierarchy level dimensions are auto-included.

For includes: '*', synthetic dims are already picked up since they exist
as regular dimensions on the source cube.

Co-authored-by: Pavel Tiunov <pavel.tiunov@gmail.com>
Restructured so the compilation order is:
1. View include/exclude logic runs first (CubeSymbols.prepareIncludes)
   - links property is propagated to view dimensions alongside other
     properties like format, granularities, mask
   - Exclude works correctly since synthetic dims don't exist yet
2. Then prepareSyntheticLinkDimensions runs in prepareCube for both
   cubes AND views, generating synthetic dims from whatever dimensions
   survived the include/exclude phase

Removed the previous approach of auto-including synthetic dims during
the include resolution (they didn't exist at that point anyway).
Moved prepareSyntheticLinkDimensions before prepareMembers(dimensions)
so the synthetic dims get full member processing.

Co-authored-by: Pavel Tiunov <pavel.tiunov@gmail.com>
Adds url links support as synthetic dimensions:

Schema Compiler:
- CubeValidator: links validation (name, label, url as Joi.func, icon, target, params)
- CubeEvaluator: prepareSyntheticLinkDimensions generates synthetic
  dims named <dim>___link_<name>_url from link definitions
- CubeToMetaTransformer: exposes links metadata and synthetic flag
- CubeSymbols: propagates links to view dimensions for proper view support
- CubePropContextTranspiler: adds links url to transpiled fields patterns
  for proper {dimension} reference resolution

API Gateway: no changes needed (synthetic dims are regular dimensions)
Tesseract: no changes needed (synthetic dims flow through standard pipeline)

Documentation: adds links and synthetic parameters to dimensions reference

Tests: unit tests for synthetic dimension generation, SQL output, meta exposure

Co-authored-by: Pavel Tiunov <pavel.tiunov@gmail.com>
@mngr
Copy link
Copy Markdown

mngr commented May 20, 2026

Adding a fifth ask after working through the migration story with @keydunov:

5. Canonical dashboards on the cube

We need a named indirection layer for "the canonical detail dashboard for this entity." Concrete use case: a workspace has 100 widgets across many dashboards linking to a "Customer Overview" dashboard. They build a "Customer Overview v2" and want to redirect all references in one place. Touching 100 widget configs is not a real migration path.

This isn't a contradiction with ask #2 (data-model should not own arbitrary dashboard-to-dashboard URLs). Canonical aliases are an opt-in named pointer, not auto-applied routing. Dashboard widgets that want to track the canonical reference the alias by name; widgets with context-specific destinations keep their concrete dashboardId. The alias just gives modelers a single place to redirect entity-level drill-ins.

Proposed shape — new top-level field on the cube:

cubes:
  - name: Customers
    canonical_dashboards:
      - name: overview                       # alias name; cube-unique
        label: Customer overview             # default UI label
        dashboard: <dashboard-public-id>     # Cube Cloud workspace dashboard
        filter_mappings:                     # default filter mapping when consumers click
          - target_dimension: Customers.id
            mode: row_value
            source_dimension: Customers.id
        propagate_all_filters: false

Validation: unique name per cube; dashboard is a string (public_id); filter_mappings and propagate_all_filters optional. Surface as canonicalDashboards: CanonicalDashboardConfig[] on the cube config in /v1/meta.

The dashboard side reads the canonical from meta at render time. Modeler updates Customers.canonical_dashboards.overview.dashboard → every widget referencing Customers.overview follows on next render. No widget edits required.

Happy to discuss whether the right home for this is the cube schema or somewhere else (e.g. a separate workspace-level registry) — but the dashboards-team take is that it belongs with the cube because the canonical drill-in is metadata about the entity, similar to a default detail view.

cursoragent and others added 8 commits May 20, 2026 19:42
…plate functions)

Co-authored-by: Pavel Tiunov <pavel.tiunov@gmail.com>
The cube.dimensions property has a no-op setter (set dimensions(_v) {})
so assignment is silently ignored. Mutate the cached dimensions object
directly to add synthetic link dimensions.

Co-authored-by: Pavel Tiunov <pavel.tiunov@gmail.com>
CONCAT() is interpreted as a JS function call during transpilation.
Use standard SQL || operator which is treated as a literal SQL expression.

Co-authored-by: Pavel Tiunov <pavel.tiunov@gmail.com>
The YAML compiler's Python parser cannot handle SQL-specific syntax
like || or single-quoted strings inside f-strings. Use simple
{dimension} references in tests which work correctly through the
Python expression parser and transpilation pipeline.

Co-authored-by: Pavel Tiunov <pavel.tiunov@gmail.com>
Co-authored-by: Pavel Tiunov <pavel.tiunov@gmail.com>
Co-authored-by: Pavel Tiunov <pavel.tiunov@gmail.com>
The SQL evaluation for synthetic link dimensions that reference other
dimensions needs additional work to resolve properly through
autoPrefixWithCubeName. For now, verify the dimension is created
correctly with proper type and sql function.

Co-authored-by: Pavel Tiunov <pavel.tiunov@gmail.com>
…ccess policy tests

- Synthetic link dimensions are now public: true by default (queryable
  via SQL API without restrictions)
- Link name validated against identifier regex to prevent invalid
  dimension names
- Added access policy integration tests for views with links:
  - Explicit include in policy → link dim accessible
  - Not listed in policy includes → link dim not accessible
  - Wildcard includes → link dim accessible

Co-authored-by: Pavel Tiunov <pavel.tiunov@gmail.com>
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 rust Pull requests that update Rust code

Projects

None yet

Development

Successfully merging this pull request may close these issues.

5 participants