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
17 changes: 14 additions & 3 deletions packages/cubejs-client-core/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -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"
}
Comment on lines +21 to +24
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.

Nit (non-blocking): The ./format sub-path only exposes types + import — there's no require entry. This means require('@cubejs-client/core/format') will fail for CJS consumers.

I see from src/index.ts:914 that this is intentional to avoid bloating the CJS/UMD bundle — which is a reasonable trade-off. Just flagging that this could surprise users who mix CJS and ESM. Consider adding a brief note in the package README or the JSDoc for formatValue mentioning ESM-only availability via the sub-path.

},
"typesVersions": {
"*": {
"format": ["./dist/src/format.d.ts"]
}
},
"dependencies": {
"core-js": "^3.6.5",
Expand Down
6 changes: 4 additions & 2 deletions packages/cubejs-client-core/src/ResultSet.ts
Original file line number Diff line number Diff line change
Expand Up @@ -917,15 +917,17 @@ export default class ResultSet<T extends Record<string, any> = any> {
const schema: Record<string, any> = {};

const extractFields = (key: string) => {
const { title, shortTitle, type, format, meta } = flatMeta[key] || {};
const { title, shortTitle, type, format, meta, currency, granularity } = flatMeta[key] || {};

return {
key,
title,
shortTitle,
type,
format,
meta
meta,
currency,
granularity: granularity?.name,
};
};

Expand Down
27 changes: 26 additions & 1 deletion packages/cubejs-client-core/src/format.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -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);
}
Comment thread
ovr marked this conversation as resolved.

if (format && typeof format === 'object') {
if (format.type === 'custom-numeric') {
return d3Format(format.value)(parseNumber(value));
Expand All @@ -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);
Expand Down
4 changes: 4 additions & 0 deletions packages/cubejs-client-core/src/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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[];
};

Expand Down
6 changes: 6 additions & 0 deletions packages/cubejs-client-core/test/ResultSet.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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',
Expand All @@ -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',
Expand All @@ -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',
Expand Down
18 changes: 17 additions & 1 deletion packages/cubejs-client-core/test/format.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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');
});
Expand Down Expand Up @@ -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');
});
Comment thread
ovr marked this conversation as resolved.
});
Loading