From 48ae02c7ab4a58b7c9ce4b26cc770e94d39f0b6c Mon Sep 17 00:00:00 2001 From: Dmitry Patsura Date: Wed, 15 Apr 2026 15:08:44 +0200 Subject: [PATCH 1/4] feat(client-core): Improve formatting API --- packages/cubejs-client-core/package.json | 17 ++++++++++++++--- packages/cubejs-client-core/src/ResultSet.ts | 6 ++++-- packages/cubejs-client-core/src/format.ts | 12 ++++++++++++ packages/cubejs-client-core/src/types.ts | 4 ++++ .../cubejs-client-core/test/ResultSet.test.ts | 6 ++++++ packages/cubejs-client-core/test/format.test.ts | 9 +++++++++ 6 files changed, 49 insertions(+), 5 deletions(-) diff --git a/packages/cubejs-client-core/package.json b/packages/cubejs-client-core/package.json index 484562a48d810..9f461acea2c72 100644 --- a/packages/cubejs-client-core/package.json +++ b/packages/cubejs-client-core/package.json @@ -13,9 +13,20 @@ "typings": "dist/src/index.d.ts", "author": "Cube Dev, Inc.", "exports": { - "types": "./dist/src/index.d.ts", - "import": "./dist/src/index.js", - "require": "./dist/cubejs-client-core.cjs.js" + ".": { + "types": "./dist/src/index.d.ts", + "import": "./dist/src/index.js", + "require": "./dist/cubejs-client-core.cjs.js" + }, + "./format": { + "types": "./dist/src/format.d.ts", + "import": "./dist/src/format.js" + } + }, + "typesVersions": { + "*": { + "format": ["./dist/src/format.d.ts"] + } }, "dependencies": { "core-js": "^3.6.5", diff --git a/packages/cubejs-client-core/src/ResultSet.ts b/packages/cubejs-client-core/src/ResultSet.ts index 06a5ad0f42af2..fe6d97ba15137 100644 --- a/packages/cubejs-client-core/src/ResultSet.ts +++ b/packages/cubejs-client-core/src/ResultSet.ts @@ -917,7 +917,7 @@ export default class ResultSet = any> { const schema: Record = {}; const extractFields = (key: string) => { - const { title, shortTitle, type, format, meta } = flatMeta[key] || {}; + const { title, shortTitle, type, format, meta, currency, granularity } = flatMeta[key] || {}; return { key, @@ -925,7 +925,9 @@ export default class ResultSet = any> { shortTitle, type, format, - meta + meta, + currency, + granularity: granularity?.name, }; }; diff --git a/packages/cubejs-client-core/src/format.ts b/packages/cubejs-client-core/src/format.ts index 34427bdae7d18..408dc65e54f65 100644 --- a/packages/cubejs-client-core/src/format.ts +++ b/packages/cubejs-client-core/src/format.ts @@ -79,6 +79,18 @@ export function formatValue( return emptyPlaceholder; } + if (type === 'boolean') { + if (typeof value === 'boolean') { + return value.toString(); + } + + if (typeof value === 'number') { + return Boolean(value).toString(); + } + + return String(value); + } + if (format && typeof format === 'object') { if (format.type === 'custom-numeric') { return d3Format(format.value)(parseNumber(value)); diff --git a/packages/cubejs-client-core/src/types.ts b/packages/cubejs-client-core/src/types.ts index d47382272a3d8..76d505f163eff 100644 --- a/packages/cubejs-client-core/src/types.ts +++ b/packages/cubejs-client-core/src/types.ts @@ -343,6 +343,10 @@ export type TableColumn = { title: string; shortTitle: string; format?: any; + /** ISO 4217 currency code in uppercase (e.g. USD, EUR). Carried over from the annotation for currency-formatted members. */ + currency?: string; + /** Granularity name (e.g. 'day', 'month', 'year'). Carried over from the annotation for time dimensions. */ + granularity?: string; children?: TableColumn[]; }; diff --git a/packages/cubejs-client-core/test/ResultSet.test.ts b/packages/cubejs-client-core/test/ResultSet.test.ts index efa3bdb4dbb37..6045aea526f6d 100644 --- a/packages/cubejs-client-core/test/ResultSet.test.ts +++ b/packages/cubejs-client-core/test/ResultSet.test.ts @@ -658,6 +658,8 @@ describe('ResultSet', () => { format: undefined, key: 'base_orders.created_at.month', meta: undefined, + currency: undefined, + granularity: 'month', shortTitle: 'Created at', title: 'Base Orders Created at', type: 'time', @@ -670,6 +672,8 @@ describe('ResultSet', () => { addDesc: 'The status of order', moreNum: 42, }, + currency: undefined, + granularity: undefined, shortTitle: 'Status', title: 'Base Orders Status', type: 'string', @@ -679,6 +683,8 @@ describe('ResultSet', () => { format: undefined, key: 'base_orders.count', meta: undefined, + currency: undefined, + granularity: undefined, shortTitle: 'Count', title: 'Base Orders Count', type: 'number', diff --git a/packages/cubejs-client-core/test/format.test.ts b/packages/cubejs-client-core/test/format.test.ts index 9af1179e47f85..4356ac60edc9e 100644 --- a/packages/cubejs-client-core/test/format.test.ts +++ b/packages/cubejs-client-core/test/format.test.ts @@ -99,4 +99,13 @@ describe('formatValue', () => { expect(formatValue(true, { type: 'boolean' })).toBe('true'); expect(formatValue('', { type: 'string' })).toBe(''); }); + + it('boolean: coerces numeric 0/1 from SQL drivers', () => { + expect(formatValue(true, { type: 'boolean' })).toBe('true'); + expect(formatValue(false, { type: 'boolean' })).toBe('false'); + expect(formatValue(1, { type: 'boolean' })).toBe('true'); + expect(formatValue(0, { type: 'boolean' })).toBe('false'); + expect(formatValue('true', { type: 'boolean' })).toBe('true'); + expect(formatValue('false', { type: 'boolean' })).toBe('false'); + }); }); From 6278c7f55aad133c8873bfa8f8cd40d254f5db02 Mon Sep 17 00:00:00 2001 From: Dmitry Patsura Date: Wed, 15 Apr 2026 15:16:48 +0200 Subject: [PATCH 2/4] chore: put legacy hack --- packages/cubejs-client-core/src/format.ts | 10 ++++++++++ packages/cubejs-client-core/test/format.test.ts | 3 ++- 2 files changed, 12 insertions(+), 1 deletion(-) diff --git a/packages/cubejs-client-core/src/format.ts b/packages/cubejs-client-core/src/format.ts index 408dc65e54f65..a18a9c05f0891 100644 --- a/packages/cubejs-client-core/src/format.ts +++ b/packages/cubejs-client-core/src/format.ts @@ -88,6 +88,16 @@ export function formatValue( return Boolean(value).toString(); } + // Some SQL drivers return booleans as '0'/'1' or 'true'/'false' strings, It's incorrect behaivour in Cube, + // but let's format it as boolean for backward compatibility. + if (value === '0' || value === 'false') { + return 'false'; + } + + if (value === '1' || value === 'true') { + return 'true'; + } + return String(value); } diff --git a/packages/cubejs-client-core/test/format.test.ts b/packages/cubejs-client-core/test/format.test.ts index 4356ac60edc9e..6673d2396fd8b 100644 --- a/packages/cubejs-client-core/test/format.test.ts +++ b/packages/cubejs-client-core/test/format.test.ts @@ -101,11 +101,12 @@ describe('formatValue', () => { }); it('boolean: coerces numeric 0/1 from SQL drivers', () => { - expect(formatValue(true, { type: 'boolean' })).toBe('true'); expect(formatValue(false, { type: 'boolean' })).toBe('false'); expect(formatValue(1, { type: 'boolean' })).toBe('true'); expect(formatValue(0, { type: 'boolean' })).toBe('false'); expect(formatValue('true', { type: 'boolean' })).toBe('true'); expect(formatValue('false', { type: 'boolean' })).toBe('false'); + expect(formatValue('1', { type: 'boolean' })).toBe('true'); + expect(formatValue('0', { type: 'boolean' })).toBe('false'); }); }); From 1318e95bd81c47ca0418d7bb844e2ea683c7e3ed Mon Sep 17 00:00:00 2001 From: Dmitry Patsura Date: Wed, 15 Apr 2026 15:27:23 +0200 Subject: [PATCH 3/4] chore: format id --- packages/cubejs-client-core/src/format.ts | 3 ++- packages/cubejs-client-core/test/format.test.ts | 8 +++++++- 2 files changed, 9 insertions(+), 2 deletions(-) diff --git a/packages/cubejs-client-core/src/format.ts b/packages/cubejs-client-core/src/format.ts index a18a9c05f0891..512a485638c0b 100644 --- a/packages/cubejs-client-core/src/format.ts +++ b/packages/cubejs-client-core/src/format.ts @@ -123,8 +123,9 @@ export function formatValue( return getD3NumericLocale(locale).format(DEFAULT_PERCENT_FORMAT)(parseNumber(value)); case 'number': return getD3NumericLocale(locale).format(DEFAULT_NUMBER_FORMAT)(parseNumber(value)); - case 'imageUrl': case 'id': + return d3Format('.0f')(parseNumber(value)); + case 'imageUrl': case 'link': default: return String(value); diff --git a/packages/cubejs-client-core/test/format.test.ts b/packages/cubejs-client-core/test/format.test.ts index 6673d2396fd8b..df1cc6ead53f9 100644 --- a/packages/cubejs-client-core/test/format.test.ts +++ b/packages/cubejs-client-core/test/format.test.ts @@ -47,11 +47,17 @@ describe('formatValue', () => { it('passthrough formats', () => { expect(formatValue('https://img.example.com/photo.png', { type: 'string', format: 'imageUrl' })).toBe('https://img.example.com/photo.png'); - expect(formatValue(12345, { type: 'number', format: 'id' })).toBe('12345'); expect(formatValue('https://example.com', { type: 'string', format: 'link' })).toBe('https://example.com'); expect(formatValue('https://example.com', { type: 'string', format: { type: 'link', label: 'Example' } })).toBe('https://example.com'); }); + it('format: id (integer, no thousands separator)', () => { + expect(formatValue(12345, { type: 'number', format: 'id' })).toBe('12345'); + expect(formatValue('12345', { type: 'number', format: 'id' })).toBe('12345'); + expect(formatValue(12345.78, { type: 'number', format: 'id' })).toBe('12346'); + expect(formatValue(0, { type: 'number', format: 'id' })).toBe('0'); + }); + it('type-based fallback: number', () => { expect(formatValue(1234.56, { type: 'number' })).toBe('1,234.56'); }); From 24a186eb952a1905f40ce2ef9bb0bf856d505a2d Mon Sep 17 00:00:00 2001 From: Dmitry Patsura Date: Wed, 15 Apr 2026 15:33:31 +0200 Subject: [PATCH 4/4] chore: const --- packages/cubejs-client-core/src/format.ts | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/packages/cubejs-client-core/src/format.ts b/packages/cubejs-client-core/src/format.ts index 512a485638c0b..975e266543316 100644 --- a/packages/cubejs-client-core/src/format.ts +++ b/packages/cubejs-client-core/src/format.ts @@ -10,6 +10,8 @@ const DEFAULT_NUMBER_FORMAT = ',.2f'; const DEFAULT_CURRENCY_FORMAT = '$,.2f'; const DEFAULT_PERCENT_FORMAT = '.2%'; +const DEFAULT_ID_FORMAT = '.0f'; + function detectLocale() { try { return new Intl.NumberFormat().resolvedOptions().locale; @@ -124,7 +126,7 @@ export function formatValue( case 'number': return getD3NumericLocale(locale).format(DEFAULT_NUMBER_FORMAT)(parseNumber(value)); case 'id': - return d3Format('.0f')(parseNumber(value)); + return d3Format(DEFAULT_ID_FORMAT)(parseNumber(value)); case 'imageUrl': case 'link': default: