Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion packages/cubejs-api-gateway/package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "@cubejs-backend/api-gateway",
"description": "Cube.js API Gateway",
"description": "Cube API Gateway",
"author": "Cube Dev, Inc.",
"version": "1.6.33",
"repository": {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -225,12 +225,12 @@ describe('transformMetaExtended helpers', () => {
{
name: 'PlaygroundUsers',
relationship: 'belongsTo',
sql: () => `{CUBE}.id = {PlaygroundUsers.anonymous}`,
sql: () => '{CUBE}.id = {PlaygroundUsers.anonymous}',
},
{
name: 'IpEnrich',
relationship: 'belongsTo',
sql: () => `{CUBE.email} = {IpEnrich.email}`,
sql: () => '{CUBE.email} = {IpEnrich.email}',
},
];

Expand All @@ -239,8 +239,8 @@ describe('transformMetaExtended helpers', () => {
expect(handledJoins?.length).toBe(2);
expect(handledJoins?.[0].name).toBe('PlaygroundUsers');
expect(handledJoins?.[1].name).toBe('IpEnrich');
expect(handledJoins?.[0].sql).toBe('`{CUBE}.id = {PlaygroundUsers.anonymous}`');
expect(handledJoins?.[1].sql).toBe('`{CUBE.email} = {IpEnrich.email}`');
expect(handledJoins?.[0].sql).toBe("'{CUBE}.id = {PlaygroundUsers.anonymous}'");
expect(handledJoins?.[1].sql).toBe("'{CUBE.email} = {IpEnrich.email}'");
});

test('transformPreAggregations', () => {
Expand Down
2 changes: 1 addition & 1 deletion packages/cubejs-api-gateway/test/index.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1276,7 +1276,7 @@ describe('API Gateway', () => {
const execSqlMock = jest.fn(async (query, stream, securityContext, cacheMode, timezone) => {
// Simulate writing error to the stream
stream.write(`${JSON.stringify({
error: "Continue wait"
error: 'Continue wait'
})}\n`);
stream.end();
});
Expand Down
Empty file.
15 changes: 15 additions & 0 deletions packages/cubejs-client-core/src/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -31,12 +31,27 @@ export type DimensionFormat = 'percent' | 'currency' | 'number' | 'imageUrl' | '
| DimensionLinkFormat | DimensionCustomTimeFormat | CustomNumericFormat;
export type MeasureFormat = 'percent' | 'currency' | 'number' | CustomNumericFormat;

type FormatDescriptionBaseName = 'number' | 'percent' | 'currency' | 'abbr' | 'accounting';
type FormatDescriptionPrecision = 0 | 1 | 2 | 3 | 4 | 5 | 6;
type FormatDescriptionName = 'custom' | 'id' | FormatDescriptionBaseName | `${FormatDescriptionBaseName}_${FormatDescriptionPrecision}`;

export type FormatDescription = {
/** Predefined format name (e.g., 'percent_2', 'currency_1') or a base name like 'number' */
name: FormatDescriptionName;
/** d3-format specifier string (e.g., '.2f', ',.0f', '$,.2f'). See https://d3js.org/d3-format */
specifier: string;
/** ISO 4217 currency code in uppercase (e.g. USD, EUR). Present when a currency format is used. */
currency?: string;
};

export type Annotation = {
title: string;
shortTitle: string;
type: string;
meta?: any;
format?: DimensionFormat | MeasureFormat;
/** Resolved format description with the predefined name and d3-format specifier */
formatDescription?: FormatDescription;
/** ISO 4217 currency code in uppercase (e.g. USD, EUR) */
currency?: string;
drillMembers?: any[];
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -18,14 +18,22 @@ import type { ContextEvaluator } from './ContextEvaluator';
import type { JoinGraph } from './JoinGraph';
import type { ErrorReporter } from './ErrorReporter';
import { CompilerInterface } from './PrepareCompiler';
import { resolveNamedNumericFormat } from './named-numeric-formats';
import { resolveNamedNumericFormat, STANDARD_FORMAT_SPECIFIERS, DEFAULT_FORMAT_SPECIFIER } from './named-numeric-formats';

export type CustomNumericFormat = { type: 'custom-numeric'; value: string; alias?: string };
export type DimensionCustomTimeFormat = { type: 'custom-time'; value: string };
export type DimensionLinkFormat = { type: 'link'; label?: string };
export type DimensionFormat = string | DimensionLinkFormat | DimensionCustomTimeFormat | CustomNumericFormat;
export type MeasureFormat = string | CustomNumericFormat;

export type FormatDescription = {
name: string;
specifier: string;
currency?: string;
};

const EXCLUDED_MEASURE_TYPES = new Set(['string', 'boolean', 'time']);

// Extended types for cube symbols with all runtime properties
export interface ExtendedCubeSymbolDefinition extends CubeSymbolDefinition {
description?: string;
Expand Down Expand Up @@ -71,6 +79,7 @@ export type MeasureConfig = {
description?: string;
shortTitle: string;
format?: MeasureFormat;
formatDescription?: FormatDescription;
currency?: string;
cumulativeTotal: boolean;
cumulative: boolean;
Expand All @@ -95,6 +104,7 @@ export type DimensionConfig = {
shortTitle: string;
suggestFilterValues: boolean;
format?: DimensionFormat;
formatDescription?: FormatDescription;
currency?: string;
meta?: any;
isVisible: boolean;
Expand Down Expand Up @@ -255,19 +265,23 @@ export class CubeToMetaTransformer implements CompilerInterface {
? this.isVisible(extendedDimDef, !extendedDimDef.primaryKey)
: false;
const granularitiesObj = extendedDimDef.granularities;
const dimType = this.dimensionDataType(extendedDimDef.type || 'string');
const dimFormat = this.transformDimensionFormat(extendedDimDef);
const dimCurrency = extendedDimDef.currency?.toUpperCase();

return {
name: `${cubeName}.${dimensionName}`,
title: this.title(cubeTitle, nameToDimension, false),
type: this.dimensionDataType(extendedDimDef.type || 'string'),
type: dimType,
description: extendedDimDef.description,
shortTitle: this.title(cubeTitle, nameToDimension, true),
suggestFilterValues:
extendedDimDef.suggestFilterValues == null
? true
: extendedDimDef.suggestFilterValues,
format: this.transformDimensionFormat(extendedDimDef),
currency: extendedDimDef.currency?.toUpperCase(),
format: dimFormat,
formatDescription: this.resolveFormatDescription(dimFormat, dimType, false, dimCurrency),
currency: dimCurrency,
meta: extendedDimDef.meta,
isVisible: dimensionVisibility,
public: dimensionVisibility,
Expand Down Expand Up @@ -380,13 +394,17 @@ export class CubeToMetaTransformer implements CompilerInterface {
}
}

const format = this.transformMeasureFormat(extendedMetricDef.format);
const currency = extendedMetricDef.currency?.toUpperCase();

return {
name,
title: this.title(cubeTitle, nameToMetric, false),
description: extendedMetricDef.description,
shortTitle: this.title(cubeTitle, nameToMetric, true),
format: this.transformMeasureFormat(extendedMetricDef.format),
currency: extendedMetricDef.currency?.toUpperCase(),
format,
formatDescription: this.resolveFormatDescription(format, type, true, currency),
currency,
cumulativeTotal: isCumulative,
cumulative: isCumulative,
type,
Expand Down Expand Up @@ -459,4 +477,43 @@ export class CubeToMetaTransformer implements CompilerInterface {
// Custom numeric format (raw d3-format specifier)
return { type: 'custom-numeric', value: formatOrName };
}

/**
* Resolves a format into a FormatDescription.
* - Measures: returned for all types except string, boolean, and time.
* - Dimensions: returned only for number type.
*/
private resolveFormatDescription(
format: MeasureFormat | DimensionFormat | undefined,
type: string,
isMeasure: boolean,
currency?: string,
): FormatDescription | undefined {
if (isMeasure) {
if (EXCLUDED_MEASURE_TYPES.has(type)) {
return undefined;
}
} else if (type !== 'number') {
return undefined;
}

let desc: FormatDescription;

if (format && typeof format === 'object' && format.type === 'custom-numeric') {
desc = {
name: format.alias || 'custom',
specifier: format.value,
};
} else if (typeof format === 'string' && STANDARD_FORMAT_SPECIFIERS[format]) {
desc = { ...STANDARD_FORMAT_SPECIFIERS[format] };
} else {
desc = { ...DEFAULT_FORMAT_SPECIFIER };
}

if (currency) {
desc.currency = currency;
}

return desc;
}
}
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
// Predefined named numeric formats and their d3-format specifiers.
//
// "number", "percent", "currency", and "id" (without _X suffix) are already handled
// "number", "percent", and "currency" (without _X suffix) are already handled
// as separate format types in the existing API contract. Converting them to named
// formats here would be a breaking change. Only the _X suffixed variants are named.
export const NAMED_NUMERIC_FORMATS: Record<string, string> = {
Expand Down Expand Up @@ -53,6 +53,9 @@ export const NAMED_NUMERIC_FORMATS: Record<string, string> = {
abbr_5: '.5s',
abbr_6: '.6s',

// id: grouped integer (no decimals)
id: '.0f',

// accounting (negative in parens): (,.Xf
// Alias to accounting_2
accounting: '(,.2f',
Expand All @@ -68,3 +71,23 @@ export const NAMED_NUMERIC_FORMATS: Record<string, string> = {
export function resolveNamedNumericFormat(value: string): string | undefined {
return NAMED_NUMERIC_FORMATS[value];
}

/**
* Maps standard/base format names to their default d3-format specifiers.
* Used by resolveFormatDescription to produce FormatDescription for
* formats that pass through as bare strings (percent, currency, number)
* as well as named formats resolved from NAMED_NUMERIC_FORMATS.
*/
export const STANDARD_FORMAT_SPECIFIERS: Record<string, { name: string; specifier: string }> = {
percent: { name: 'percent', specifier: '.2%' },
currency: { name: 'currency', specifier: '$,.2f' },
number: { name: 'number', specifier: ',.2f' },
abbr: { name: 'abbr', specifier: '.2s' },
accounting: { name: 'accounting', specifier: '(,.2f' },
id: { name: 'id', specifier: '.0f' },
};

export const DEFAULT_FORMAT_SPECIFIER: { name: string; specifier: string } = {
name: 'number',
specifier: ',.2f',
};
Loading
Loading