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..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; @@ -79,6 +81,28 @@ export function formatValue( return emptyPlaceholder; } + if (type === 'boolean') { + if (typeof value === 'boolean') { + return value.toString(); + } + + if (typeof value === 'number') { + 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); + } + if (format && typeof format === 'object') { if (format.type === 'custom-numeric') { return d3Format(format.value)(parseNumber(value)); @@ -101,8 +125,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(DEFAULT_ID_FORMAT)(parseNumber(value)); + case 'imageUrl': case 'link': default: return String(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..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'); }); @@ -99,4 +105,14 @@ 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(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'); + }); });