From 445d6949e7c293420f1160816e9659ca766d8b51 Mon Sep 17 00:00:00 2001 From: Donya MashaallahPoor Date: Tue, 6 May 2025 13:07:19 +0200 Subject: [PATCH] chore: add hypercube generic functions - part02 (#1715) * chore: add hypercube generic functions - part02 --- .../__test__/data-property-handler.test.js | 61 ---- .../__test__/hypercube-handler.test.js | 115 ------- .../__tests__/data-property-handler.test.js | 180 +++++++++++ .../__tests__/hypercube-handler.test.js | 291 ++++++++++++++++++ .../src/handler/data-property-handler.js | 218 ++++++++++++- .../src/handler/hypercube-handler.js | 132 ++++++-- apis/supernova/src/handler/utils/constants.js | 14 + .../expand-field-derived-data.test.js | 70 +++++ .../find-field-in-expandedList.test.js | 39 +++ .../__tests__/get-data-geo-field.test.js | 24 ++ .../__tests__/get-derived-fields.test.js | 131 ++++++++ .../__tests__/get-sorted-field.test.js | 80 +++++ .../get-sorted-library-field.test.js | 77 +++++ .../field-helper/__tests__/utils.test.js | 65 ++++ .../field-helper/expand-field-derived-data.js | 16 + .../handler/utils/field-helper/field-utils.js | 85 +++++ .../find-field-in-expandedList.js | 10 + .../utils/field-helper/get-data-geo-field.js | 10 + .../utils/field-helper/get-derived-fields.js | 28 ++ .../utils/field-helper/get-sorted-field.js | 14 + .../field-helper/get-sorted-library-field.js | 13 + .../src/handler/utils/handler-helper.js | 32 -- .../hypercube-helper/add-main-dimension.js | 12 + .../hypercube-helper/add-main-measure.js | 12 + .../utils/hypercube-helper/hypercube-utils.js | 182 +++++++++++ 25 files changed, 1662 insertions(+), 249 deletions(-) delete mode 100644 apis/supernova/src/handler/__test__/data-property-handler.test.js delete mode 100644 apis/supernova/src/handler/__test__/hypercube-handler.test.js create mode 100644 apis/supernova/src/handler/__tests__/data-property-handler.test.js create mode 100644 apis/supernova/src/handler/__tests__/hypercube-handler.test.js create mode 100644 apis/supernova/src/handler/utils/constants.js create mode 100644 apis/supernova/src/handler/utils/field-helper/__tests__/expand-field-derived-data.test.js create mode 100644 apis/supernova/src/handler/utils/field-helper/__tests__/find-field-in-expandedList.test.js create mode 100644 apis/supernova/src/handler/utils/field-helper/__tests__/get-data-geo-field.test.js create mode 100644 apis/supernova/src/handler/utils/field-helper/__tests__/get-derived-fields.test.js create mode 100644 apis/supernova/src/handler/utils/field-helper/__tests__/get-sorted-field.test.js create mode 100644 apis/supernova/src/handler/utils/field-helper/__tests__/get-sorted-library-field.test.js create mode 100644 apis/supernova/src/handler/utils/field-helper/__tests__/utils.test.js create mode 100644 apis/supernova/src/handler/utils/field-helper/expand-field-derived-data.js create mode 100644 apis/supernova/src/handler/utils/field-helper/field-utils.js create mode 100644 apis/supernova/src/handler/utils/field-helper/find-field-in-expandedList.js create mode 100644 apis/supernova/src/handler/utils/field-helper/get-data-geo-field.js create mode 100644 apis/supernova/src/handler/utils/field-helper/get-derived-fields.js create mode 100644 apis/supernova/src/handler/utils/field-helper/get-sorted-field.js create mode 100644 apis/supernova/src/handler/utils/field-helper/get-sorted-library-field.js delete mode 100644 apis/supernova/src/handler/utils/handler-helper.js create mode 100644 apis/supernova/src/handler/utils/hypercube-helper/add-main-dimension.js create mode 100644 apis/supernova/src/handler/utils/hypercube-helper/add-main-measure.js create mode 100644 apis/supernova/src/handler/utils/hypercube-helper/hypercube-utils.js diff --git a/apis/supernova/src/handler/__test__/data-property-handler.test.js b/apis/supernova/src/handler/__test__/data-property-handler.test.js deleted file mode 100644 index 6e892b511..000000000 --- a/apis/supernova/src/handler/__test__/data-property-handler.test.js +++ /dev/null @@ -1,61 +0,0 @@ -import HyperCubeHandler from '../hypercube-handler'; - -describe('DataPropertyHandler - getDimensions and getMeasure', () => { - let handler; - let properties; - - beforeEach(() => { - properties = { - qHyperCubeDef: { - qDimensions: [{ qDef: { cId: 'dim1' } }], - qLayoutExclude: { - qHyperCubeDef: { - qDimensions: [{ qDef: { cId: 'altDim1' } }], - qMeasures: [{ qDef: { cId: 'altMeasure1' } }], - }, - }, - }, - }; - handler = new HyperCubeHandler(properties); - }); - - afterEach(() => { - jest.clearAllMocks(); - }); - - describe('getDimensions()', () => { - test('should return null when dimension is undefined', () => { - jest.spyOn(handler, 'getDimensions').mockReturnValue([]); - const dimension = handler.getDimension(undefined); - expect(dimension).toBeFalsy(); - }); - - test('should return dimension when it exists in getDimensions()', () => { - jest.spyOn(handler, 'getDimensions').mockReturnValue([{ qDef: { cId: 'dim1' } }]); - jest.spyOn(handler, 'getAlternativeDimensions').mockReturnValue([{ qDef: { cId: 'altDim1' } }]); - - const dimension = handler.getDimension('dim1'); - expect(dimension).toEqual({ qDef: { cId: 'dim1' } }); - const alternativeDimension = handler.getDimension('altDim1'); - expect(alternativeDimension).toEqual({ qDef: { cId: 'altDim1' } }); - }); - }); - - describe('getMeasure()', () => { - test('should return null when both measures and alternative measures are empty', () => { - jest.spyOn(handler, 'getMeasures').mockReturnValue([]); - const measure = handler.getMeasure(undefined); - expect(measure).toBeFalsy(); - }); - - test('should return measure when it exists in getMeasures()', () => { - jest.spyOn(handler, 'getMeasures').mockReturnValue([{ qDef: { cId: 'measure1' } }]); - jest.spyOn(handler, 'getAlternativeMeasures').mockReturnValue([{ qDef: { cId: 'altMeasure1' } }]); - - const measure = handler.getMeasure('measure1'); - const alternativeMeasure = handler.getMeasure('altMeasure1'); - expect(measure).toEqual({ qDef: { cId: 'measure1' } }); - expect(alternativeMeasure).toEqual({ qDef: { cId: 'altMeasure1' } }); - }); - }); -}); diff --git a/apis/supernova/src/handler/__test__/hypercube-handler.test.js b/apis/supernova/src/handler/__test__/hypercube-handler.test.js deleted file mode 100644 index 37199979a..000000000 --- a/apis/supernova/src/handler/__test__/hypercube-handler.test.js +++ /dev/null @@ -1,115 +0,0 @@ -import HyperCubeHandler from '../hypercube-handler'; - -describe('HyperCube Handlers', () => { - let handler; - let properties; - - beforeEach(() => { - properties = { - qHyperCubeDef: { - qDimensions: [{ qDef: { cId: 'dim1' } }], - qInterColumnSortOrder: [0, 1], - qLayoutExclude: { - qHyperCubeDef: { - qDimensions: [{ qDef: { cId: 'altDim1' } }], - qMeasures: [{ qDef: { cId: 'altMeasure1' } }], - }, - }, - }, - }; - handler = new HyperCubeHandler(properties); - }); - - afterEach(() => { - jest.clearAllMocks(); - }); - - describe('setProperties', () => { - test('should return undefined when properties is null or undefined', () => { - handler.setProperties(null); - expect(handler.hcProperties).toBeUndefined(); - handler.setProperties({}); - expect(handler.hcProperties).toBeUndefined(); - handler.setProperties(undefined); - expect(handler.hcProperties).toBeUndefined(); - }); - - test('should set properties when qHyperCubeDef provides defined/undefined values', () => { - properties.qHyperCubeDef.qLayoutExclude.qHyperCubeDef.qDimensions = undefined; - handler.setProperties(properties); - - expect(handler.hcProperties.qDimensions[0]).toEqual({ qDef: { cId: 'dim1' } }); - expect(handler.hcProperties.qMeasures).toEqual([]); - expect(handler.hcProperties.qInterColumnSortOrder).toEqual([0, 1]); - expect(handler.hcProperties.qLayoutExclude).toEqual({ - qHyperCubeDef: { - qDimensions: [], - qMeasures: [{ qDef: { cId: 'altMeasure1' } }], - }, - }); - }); - - test('should set properties when qLayoutExclude.qHyperCubeDef is undefined', () => { - properties.qHyperCubeDef.qLayoutExclude.qHyperCubeDef = undefined; - handler.setProperties(properties); - - expect(handler.hcProperties.qLayoutExclude).toEqual({ - qHyperCubeDef: { - qDimensions: [], - qMeasures: [], - }, - }); - }); - }); - - describe('getDimensions and getAlternativeDimensions', () => { - test('should return empty arrays when hcProperties is null', () => { - handler.hcProperties = null; - - expect(handler.getDimensions()).toEqual([]); - expect(handler.getAlternativeDimensions()).toEqual([]); - }); - - test('should return empty arrays when qDimensions and qLayoutExclude.qHyperCubeDef.qDimensions are empty', () => { - handler.hcProperties = { - qDimensions: [], - qLayoutExclude: { - qHyperCubeDef: { - qDimensions: [], - }, - }, - }; - - expect(handler.getDimensions()).toEqual([]); - expect(handler.getAlternativeDimensions()).toEqual([]); - }); - - test('should return qDimensions when qDimensions contains dimensions', () => { - handler.hcProperties = { - qDimensions: [{ qDef: { cId: 'dim1' } }, { qDef: { cId: 'dim2' } }], - qLayoutExclude: { - qHyperCubeDef: { - qDimensions: [], - }, - }, - }; - - expect(handler.getDimensions()).toEqual([{ qDef: { cId: 'dim1' } }, { qDef: { cId: 'dim2' } }]); - expect(handler.getAlternativeDimensions()).toEqual([]); - }); - - test('should return qLayoutExclude.qHyperCubeDef.qDimensions when it contains alternative dimensions', () => { - handler.hcProperties = { - qDimensions: [], - qLayoutExclude: { - qHyperCubeDef: { - qDimensions: [{ qDef: { cId: 'altDim1' } }, { qDef: { cId: 'altDim2' } }], - }, - }, - }; - - expect(handler.getDimensions()).toEqual([]); - expect(handler.getAlternativeDimensions()).toEqual([{ qDef: { cId: 'altDim1' } }, { qDef: { cId: 'altDim2' } }]); - }); - }); -}); diff --git a/apis/supernova/src/handler/__tests__/data-property-handler.test.js b/apis/supernova/src/handler/__tests__/data-property-handler.test.js new file mode 100644 index 000000000..20a37d62e --- /dev/null +++ b/apis/supernova/src/handler/__tests__/data-property-handler.test.js @@ -0,0 +1,180 @@ +import DataPropertyHandler from '../data-property-handler'; +import HyperCubeHandler from '../hypercube-handler'; + +describe('DataPropertyHandler', () => { + let handler; + let properties; + const sortingProperties = [ + { + qSortByLoadOrder: 1, + qSortByNumeric: 1, + qSortByAscii: 1, + }, + ]; + + beforeEach(() => { + properties = { + qHyperCubeDef: { + qDimensions: [{ qDef: { cId: 'dim1' } }], + qLayoutExclude: { + qHyperCubeDef: { + qDimensions: [{ qDef: { cId: 'altDim1' } }], + qMeasures: [{ qDef: { cId: 'altMeasure1' } }], + }, + }, + }, + }; + handler = new HyperCubeHandler(properties); + }); + + afterEach(() => { + jest.clearAllMocks(); + }); + + describe('getDimensions()', () => { + test('should return null when dimension is undefined', () => { + jest.spyOn(handler, 'getDimensions').mockReturnValue([]); + const dimension = handler.getDimension(undefined); + expect(dimension).toBeFalsy(); + }); + + test('should return dimension when it exists in getDimensions()', () => { + jest.spyOn(handler, 'getDimensions').mockReturnValue([{ qDef: { cId: 'dim1' } }]); + jest.spyOn(handler, 'getAlternativeDimensions').mockReturnValue([{ qDef: { cId: 'altDim1' } }]); + + const dimension = handler.getDimension('dim1'); + expect(dimension).toEqual({ qDef: { cId: 'dim1' } }); + const alternativeDimension = handler.getDimension('altDim1'); + expect(alternativeDimension).toEqual({ qDef: { cId: 'altDim1' } }); + }); + }); + + describe('getMeasure()', () => { + test('should return null when both measures and alternative measures are empty', () => { + jest.spyOn(handler, 'getMeasures').mockReturnValue([]); + const measure = handler.getMeasure(undefined); + expect(measure).toBeFalsy(); + }); + + test('should return measure when it exists in getMeasures()', () => { + jest.spyOn(handler, 'getMeasures').mockReturnValue([{ qDef: { cId: 'measure1' } }]); + jest.spyOn(handler, 'getAlternativeMeasures').mockReturnValue([{ qDef: { cId: 'altMeasure1' } }]); + + const measure = handler.getMeasure('measure1'); + const alternativeMeasure = handler.getMeasure('altMeasure1'); + expect(measure).toEqual({ qDef: { cId: 'measure1' } }); + expect(alternativeMeasure).toEqual({ qDef: { cId: 'altMeasure1' } }); + }); + }); + + describe('createFieldDimension', () => { + beforeEach(() => { + handler = new DataPropertyHandler({ + dimensionProperties: { someProperty: 'defaultValue' }, + }); + }); + + test('should create a dimension with default properties when no field is provided', () => { + const result = handler.createFieldDimension(null, null, { customDefault: 'value' }); + + expect(result.qDef.qFieldDefs).toEqual([null]); + expect(result.qDef.qFieldLabels).toEqual(['']); + expect(result.qDef.qSortCriterias).toEqual(sortingProperties); + expect(result.qDef.autoSort).toBe(true); + expect(result.someProperty).toBe('defaultValue'); + expect(result.customDefault).toBe('value'); + }); + + describe('createLibraryDimension', () => { + test('should create a library dimension with default properties', () => { + const result = handler.createLibraryDimension('libraryId', { customDefault: 'value' }); + + expect(result.qLibraryId).toBe('libraryId'); + expect(result.qDef.qSortCriterias).toEqual(sortingProperties); + expect(result.qDef.autoSort).toBe(true); + expect(result.someProperty).toBe('defaultValue'); + expect(result.customDefault).toBe('value'); + }); + + test('should delete qFieldDefs and qFieldLabels from the dimension', () => { + const result = handler.createLibraryDimension('libraryId', {}); + + expect(result.qDef.qFieldDefs).toBeUndefined(); + expect(result.qDef.qFieldLabels).toBeUndefined(); + }); + }); + + test('should create a dimension with provided field and label', () => { + const result = handler.createFieldDimension('fieldName', 'fieldLabel', { customDefault: 'value' }); + + expect(result.qDef.qFieldDefs).toEqual(['fieldName']); + expect(result.qDef.qFieldLabels).toEqual(['fieldLabel']); + }); + }); + + describe('createExpressionMeasure', () => { + beforeEach(() => { + handler = new DataPropertyHandler({ + measureProperties: { someProperty: 'defaultValue' }, + }); + }); + + test('should create a measure with provided expression and label', () => { + const result = handler.createExpressionMeasure('SUM(Sales)', 'Total Sales', { customDefault: 'value' }); + + expect(result.qDef.qDef).toBe('SUM(Sales)'); + expect(result.qDef.qLabel).toBe('Total Sales'); + expect(result.qDef.autoSort).toBe(true); + expect(result.someProperty).toBe('defaultValue'); + expect(result.customDefault).toBe('value'); + }); + + test('should initialize qDef and qNumFormat if not provided', () => { + const result = handler.createExpressionMeasure('SUM(Sales)', 'Total Sales', {}); + + expect(result.qDef).toBeDefined(); + expect(result.qDef.qNumFormat).toBeDefined(); + }); + + test('should handle empty defaults gracefully', () => { + const result = handler.createExpressionMeasure('SUM(Sales)', 'Total Sales', null); + + expect(result.qDef.qDef).toBe('SUM(Sales)'); + expect(result.qDef.qLabel).toBe('Total Sales'); + expect(result.qDef.autoSort).toBe(true); + expect(result.someProperty).toBe('defaultValue'); + }); + }); + + describe('createLibraryMeasure', () => { + beforeEach(() => { + handler = new DataPropertyHandler({ + measureProperties: { someProperty: 'defaultValue' }, + }); + }); + + test('should create a library measure with provided id and defaults', () => { + const result = handler.createLibraryMeasure('libraryId', { customDefault: 'value' }); + + expect(result.qLibraryId).toBe('libraryId'); + expect(result.qDef.qNumFormat).toBeDefined(); + expect(result.qDef.autoSort).toBe(true); + expect(result.someProperty).toBe('defaultValue'); + expect(result.customDefault).toBe('value'); + }); + + test('should initialize qDef and qNumFormat if not provided', () => { + const result = handler.createLibraryMeasure('libraryId', {}); + + expect(result.qDef).toBeDefined(); + expect(result.qDef.qNumFormat).toBeDefined(); + }); + + test('should delete qDef.qDef and qDef.qLabel from the measure', () => { + const result = handler.createLibraryMeasure('libraryId', {}); + + expect(result.qDef.qDef).toBeUndefined(); + expect(result.qDef.qLabel).toBeUndefined(); + }); + }); +}); diff --git a/apis/supernova/src/handler/__tests__/hypercube-handler.test.js b/apis/supernova/src/handler/__tests__/hypercube-handler.test.js new file mode 100644 index 000000000..5f6754f32 --- /dev/null +++ b/apis/supernova/src/handler/__tests__/hypercube-handler.test.js @@ -0,0 +1,291 @@ +import * as hcHelper from '../utils/hypercube-helper/hypercube-utils'; +import HyperCubeHandler from '../hypercube-handler'; + +describe('HyperCube Handlers', () => { + let handler; + let properties; + + beforeEach(() => { + properties = { + qHyperCubeDef: { + qDimensions: [{ qDef: { cId: 'dim1' } }], + qMeasures: [], + qInterColumnSortOrder: [0, 1], + qLayoutExclude: { + qHyperCubeDef: { + qDimensions: [{ qDef: { cId: 'altDim1' } }], + qMeasures: [{ qDef: { cId: 'altMeas1' } }], + }, + }, + }, + }; + handler = new HyperCubeHandler(properties); + }); + + afterEach(() => { + handler.hcProperties = undefined; + jest.clearAllMocks(); + }); + + describe('setProperties', () => { + test('should return undefined when properties is null or undefined', () => { + handler.setProperties(null); + expect(handler.hcProperties).toBeUndefined(); + handler.setProperties({}); + expect(handler.hcProperties).toBeUndefined(); + handler.setProperties(undefined); + expect(handler.hcProperties).toBeUndefined(); + }); + + test('should set properties when qHyperCubeDef provides defined/undefined values', () => { + properties.qHyperCubeDef.qLayoutExclude.qHyperCubeDef.qDimensions = undefined; + handler.setProperties(properties); + + expect(handler.hcProperties.qDimensions[0]).toEqual({ qDef: { cId: 'dim1' } }); + expect(handler.hcProperties.qMeasures).toEqual([]); + expect(handler.hcProperties.qInterColumnSortOrder).toEqual([0, 1]); + expect(handler.hcProperties.qLayoutExclude).toEqual({ + qHyperCubeDef: { + qDimensions: [], + qMeasures: [{ qDef: { cId: 'altMeas1' } }], + }, + }); + }); + + test('should set properties when qLayoutExclude.qHyperCubeDef is undefined', () => { + properties.qHyperCubeDef.qLayoutExclude.qHyperCubeDef = undefined; + handler.setProperties(properties); + + expect(handler.hcProperties.qLayoutExclude).toEqual({ + qHyperCubeDef: { + qDimensions: [], + qMeasures: [], + }, + }); + }); + }); + + describe('getDimensions and getAlternativeDimensions', () => { + test('should return empty arrays when hcProperties is null', () => { + handler.setProperties(properties); + handler.hcProperties = null; + + expect(handler.getDimensions()).toEqual([]); + expect(handler.getAlternativeDimensions()).toEqual([]); + }); + + test('should return empty arrays when qDimensions and alternative dimension are empty', () => { + handler.setProperties(properties); + handler.hcProperties = { + qDimensions: [], + qLayoutExclude: { + qHyperCubeDef: { + qDimensions: [], + }, + }, + }; + + expect(handler.getDimensions()).toEqual([]); + expect(handler.getAlternativeDimensions()).toEqual([]); + }); + + test('should return alternative dimensions when it has value', () => { + handler.setProperties(properties); + handler.hcProperties = { + qDimensions: [{ qDef: { cId: 'dim1' } }, { qDef: { cId: 'dim2' } }], + qLayoutExclude: { + qHyperCubeDef: { + qDimensions: [{ qDef: { cId: 'altDim1' } }, { qDef: { cId: 'altDim2' } }], + }, + }, + }; + + expect(handler.getDimensions()).toEqual([{ qDef: { cId: 'dim1' } }, { qDef: { cId: 'dim2' } }]); + expect(handler.getAlternativeDimensions()).toEqual([{ qDef: { cId: 'altDim1' } }, { qDef: { cId: 'altDim2' } }]); + }); + }); + + describe('addDimensions', () => { + beforeEach(() => { + handler.setProperties(properties); + jest.spyOn(hcHelper, 'isTotalDimensionsExceeded').mockReturnValue(false); + }); + + test('should return an empty array when newDimensions is empty', async () => { + const result = await handler.addDimensions([]); + expect(result).toEqual([]); + }); + + test('should add dimensions to alternative dimensions when alternative is true', async () => { + const newDimensions = [{ qDef: { cId: 'altDim2' } }, { qDef: { cId: 'altDim3' } }]; + const dimensions = await handler.addDimensions(newDimensions, true); + + expect(dimensions).toEqual([ + { qDef: { cId: 'altDim2' }, qOtherTotalSpec: {} }, + { qDef: { cId: 'altDim3' }, qOtherTotalSpec: {} }, + ]); + expect(handler.hcProperties.qLayoutExclude.qHyperCubeDef.qDimensions).toEqual([ + { qDef: { cId: 'altDim1' } }, + { qDef: { cId: 'altDim2' }, qOtherTotalSpec: {} }, + { qDef: { cId: 'altDim3' }, qOtherTotalSpec: {} }, + ]); + }); + + test('should add dimensions to main dimensions when alternative is false', async () => { + const newDimensions = [{ qDef: { cId: 'dim2' } }]; + handler.maxDimensions = jest.fn().mockReturnValue(2); + handler.autoSortDimension = jest.fn(); + + const dimensions = await handler.addDimensions(newDimensions, false); + expect(handler.autoSortDimension).toHaveBeenCalledTimes(1); + expect(handler.autoSortDimension).toHaveBeenCalledWith({ qDef: { cId: 'dim2' }, qOtherTotalSpec: {} }); + + expect(dimensions).toEqual([{ qDef: { cId: 'dim2' }, qOtherTotalSpec: {} }]); + expect(handler.hcProperties.qDimensions).toEqual([ + { qDef: { cId: 'dim1' } }, + { qDef: { cId: 'dim2' }, qOtherTotalSpec: {} }, + ]); + }); + + test('should not add dimensions when isTotalDimensionsExceeded returns true', async () => { + jest.spyOn(hcHelper, 'isTotalDimensionsExceeded').mockReturnValue(true); + const newDimensions = [{ qDef: { cId: 'dim2' } }]; + + const dimensions = await handler.addDimensions(newDimensions); + + expect(dimensions).toEqual([]); + }); + + test('should add dimensions to alternative dimensions when maxDim is exceeded but less than total maxDim', async () => { + handler.maxDimensions = jest.fn().mockReturnValue(1); + const newDimensions = [{ qDef: { cId: 'dim2' } }]; + + const dimension = await handler.addDimensions(newDimensions, false); + + expect(dimension).toEqual([{ qDef: { cId: 'dim2' }, qOtherTotalSpec: {} }]); + expect(handler.hcProperties.qDimensions).toEqual([{ qDef: { cId: 'dim1' } }]); + expect(handler.hcProperties.qLayoutExclude.qHyperCubeDef.qDimensions).toEqual([ + { qDef: { cId: 'altDim1' } }, + { qDef: { cId: 'dim2' }, qOtherTotalSpec: {} }, + ]); + }); + }); + + describe('getMeasures and getAlternativeMeasures', () => { + test('should return empty arrays when hcProperties is null', () => { + handler.setProperties(properties); + handler.hcProperties = null; + + expect(handler.getMeasures()).toEqual([]); + expect(handler.getAlternativeMeasures()).toEqual([]); + }); + + test('should return empty arrays when qMeasures and alternative measures are empty', () => { + handler.setProperties(properties); + handler.hcProperties = { + qMeasures: [], + qLayoutExclude: { + qHyperCubeDef: { + qDimensions: [], + qMeasures: [], + }, + }, + }; + + expect(handler.getMeasures()).toEqual([]); + expect(handler.getAlternativeMeasures()).toEqual([]); + }); + + test('should return qMeasures when qMeasures has value', () => { + handler.setProperties(properties); + handler.hcProperties = { + qMeasures: [{ qDef: { cId: 'meas1' } }, { qDef: { cId: 'meas2' } }], + qLayoutExclude: { + qHyperCubeDef: { + qMeasures: [{ qDef: { cId: 'altMeas1' } }, { qDef: { cId: 'altMeas2' } }], + }, + }, + }; + + expect(handler.getMeasures()).toEqual([{ qDef: { cId: 'meas1' } }, { qDef: { cId: 'meas2' } }]); + expect(handler.getAlternativeMeasures()).toEqual([{ qDef: { cId: 'altMeas1' } }, { qDef: { cId: 'altMeas2' } }]); + }); + }); + + describe('addMeasures', () => { + beforeEach(() => { + properties.qHyperCubeDef.qMeasures = [{ qDef: { cId: 'meas1' } }]; + handler.setProperties(properties); + jest.spyOn(hcHelper, 'isTotalMeasureExceeded').mockReturnValue(false); + }); + + test('should return an empty array when new measure is empty', () => { + const result = handler.addMeasures([]); + expect(result).toEqual([]); + }); + + test('should add measures to alternative measures when alternative is true', () => { + const newMeasures = [{ qDef: { cId: 'altMeas2' } }, { qDef: { cId: 'altMeas3' } }]; + const measures = handler.addMeasures(newMeasures, true); + + expect(measures).toEqual([{ qDef: { cId: 'altMeas2' } }, { qDef: { cId: 'altMeas3' } }]); + expect(handler.hcProperties.qMeasures).toEqual([{ qDef: { cId: 'meas1' } }]); + expect(handler.hcProperties.qLayoutExclude.qHyperCubeDef.qMeasures).toEqual([ + { qDef: { cId: 'altMeas1' } }, + { qDef: { cId: 'altMeas2' } }, + { qDef: { cId: 'altMeas3' } }, + ]); + }); + + test('should add measures to main measures when alternative is false', () => { + const newMeasures = [{ qDef: { cId: 'meas2' } }]; + handler.maxMeasures = jest.fn().mockReturnValue(2); + handler.autoSortDimension = jest.fn(); + + const measures = handler.addMeasures(newMeasures, false); + expect(measures).toEqual([ + { + qDef: { cId: 'meas2' }, + qSortBy: { + qSortByLoadOrder: 1, + qSortByNumeric: -1, + }, + }, + ]); + expect(handler.hcProperties.qMeasures).toEqual([ + { qDef: { cId: 'meas1' } }, + { + qDef: { cId: 'meas2' }, + qSortBy: { + qSortByLoadOrder: 1, + qSortByNumeric: -1, + }, + }, + ]); + }); + + test('should not add measures when isTotalMeasureExceeded returns true', () => { + jest.spyOn(hcHelper, 'isTotalMeasureExceeded').mockReturnValue(true); + const newMeasure = [{ qDef: { cId: 'meas2' } }]; + + const measure = handler.addMeasures(newMeasure); + expect(measure).toEqual([]); + }); + + test('should add measure to alternative measures when maxMeasure is exceeded but less than total', () => { + handler.maxMeasures = jest.fn().mockReturnValue(1); + const newMeasure = [{ qDef: { cId: 'meas2' } }]; + + const measure = handler.addMeasures(newMeasure, false); + + expect(measure).toEqual([{ qDef: { cId: 'meas2' } }]); + expect(handler.hcProperties.qMeasures).toEqual([{ qDef: { cId: 'meas1' } }]); + expect(handler.hcProperties.qLayoutExclude.qHyperCubeDef.qMeasures).toEqual([ + { qDef: { cId: 'altMeas1' } }, + { + qDef: { cId: 'meas2' }, + }, + ]); + }); + }); +}); diff --git a/apis/supernova/src/handler/data-property-handler.js b/apis/supernova/src/handler/data-property-handler.js index 54045ba04..e1761491f 100644 --- a/apis/supernova/src/handler/data-property-handler.js +++ b/apis/supernova/src/handler/data-property-handler.js @@ -1,4 +1,10 @@ -import { getFieldById } from './utils/handler-helper'; +// eslint-disable-next-line import/no-extraneous-dependencies +import { merge } from 'lodash'; +// eslint-disable-next-line import/no-relative-packages +import isEnabled from '../../../nucleus/src/flags/flags'; +import { findFieldById, initializeField, useMasterNumberFormat } from './utils/field-helper/field-utils'; +import { INITIAL_SORT_CRITERIAS } from './utils/constants'; +import { notSupportedError } from './utils/hypercube-helper/hypercube-utils'; class DataPropertyHandler { constructor(opts) { @@ -30,7 +36,9 @@ class DataPropertyHandler { throw new Error('Must override this method'); } - // ----------------DIMENSION---------------- + // --------------------------------------- + // ---------------DIMENSION--------------- + // --------------------------------------- static getDimensions() { return []; @@ -40,26 +48,121 @@ class DataPropertyHandler { const dimensions = this.getDimensions(); const alternativeDimensions = this.getAlternativeDimensions(); - const dim = getFieldById(dimensions, id); - const altDim = getFieldById(alternativeDimensions, id); - - return dim ?? altDim; + return findFieldById(dimensions, id) ?? findFieldById(alternativeDimensions, id); } static getAlternativeDimensions() { throw new Error('Method not implemented.'); } + static addDimension() { + throw notSupportedError; + } + + static addDimensions() { + throw notSupportedError; + } + + static autoSortDimension() { + throw notSupportedError; + } + + createLibraryDimension(id, defaults) { + let dimension = merge({}, this.dimensionProperties || {}, defaults || {}); + + dimension = initializeField(dimension); + + dimension.qLibraryId = id; + dimension.qDef.autoSort = true; + dimension.qDef.qSortCriterias = INITIAL_SORT_CRITERIAS; + + delete dimension.qDef.qFieldDefs; + delete dimension.qDef.qFieldLabels; + + return dimension; + } + + createFieldDimension(field, label, defaults) { + let dimension = merge({}, this.dimensionProperties || {}, defaults || {}); + + dimension = initializeField(dimension); + + if (!field) { + dimension.qDef.qFieldDefs = []; + dimension.qDef.qFieldLabels = []; + dimension.qDef.qSortCriterias = []; + } + + dimension.qDef.qFieldDefs = [field]; + dimension.qDef.qFieldLabels = label ? [label] : ['']; + dimension.qDef.qSortCriterias = INITIAL_SORT_CRITERIAS; + + dimension.qDef.autoSort = true; + + return dimension; + } + + addFieldDimension(field, label, defaults) { + const dimension = this.createFieldDimension(field, label, defaults); + return this.addDimension(dimension); + } + + addFieldDimensions(args) { + const dimensions = args.map(({ field, label, defaults }) => this.createFieldDimension(field, label, defaults)); + return this.addDimensions(dimensions); + } + + addLibraryDimension(id, defaults) { + const dimension = this.createLibraryDimension(id, defaults); + return this.addDimension(dimension); + } + + addLibraryDimensions(args) { + const dimensions = args.map(({ id, defaults }) => this.createLibraryDimension(id, defaults)); + const result = this.addDimensions(dimensions); + return result; + } + + async addAltLibraryDimensions(args) { + const dimensions = args.map(({ id }) => this.createLibraryDimension(id)); + return this.addDimensions(dimensions, true); + } + + async addAltFieldDimensions(args) { + const dimensions = args.map(({ field }) => this.createFieldDimension(field)); + return this.addDimensions(dimensions, true); + } + + addAlternativeFieldDimension(field, label, defaults) { + const dimension = this.createFieldDimension(field, label, defaults); + return this.addDimension(dimension, true); + } + + addAlternativeLibraryDimension(id, defaults) { + const dimension = this.createLibraryDimension(id, defaults); + return this.addDimension(dimension, true); + } + + maxDimensions(decrement = 0) { + const measureLength = this.getMeasures().length - decrement; + + if (typeof this.dimensionDefinition.max === 'function') { + const dimParams = isEnabled('PS_21371_ANALYSIS_TYPES') ? [measureLength, this.properties] : [measureLength]; + return this.dimensionDefinition.max?.apply(null, dimParams); + } + + return Number.isNaN(+this.dimensionDefinition.max) ? 10000 : this.dimensionDefinition.max; + } + + // --------------------------------------- // ----------------MEASURE---------------- + // --------------------------------------- getMeasure(id) { const measures = this.getMeasures(); const alternativeMeasures = this.getAlternativeMeasures(); - const meas = getFieldById(measures, id); - const altMeas = getFieldById(alternativeMeasures, id); - - return meas ?? altMeas; + return findFieldById(measures, id) ?? findFieldById(alternativeMeasures, id); } static getMeasures() { @@ -69,6 +172,101 @@ class DataPropertyHandler { static getAlternativeMeasures() { throw new Error('Method not implemented.'); } + + static addMeasure() { + throw notSupportedError; + } + + static addMeasures() { + throw notSupportedError; + } + + static autoSortMeasure() { + throw notSupportedError; + } + + createExpressionMeasure(expression, label, defaults) { + const measure = merge({}, this.measureProperties || {}, defaults || {}); + + measure.qDef = measure.qDef ?? {}; + measure.qDef.qNumFormat = measure.qDef.qNumFormat ?? {}; + + measure.qDef.qDef = expression; + measure.qDef.qLabel = label; + measure.qDef.autoSort = true; + + return measure; + } + + addExpressionMeasure(expression, label, defaults) { + const measure = this.createExpressionMeasure(expression, label, defaults); + return this.addMeasure(measure); + } + + addExpressionMeasures(args) { + const measures = args.map(({ expression, label, defaults }) => + this.createExpressionMeasure(expression, label, defaults) + ); + return this.addMeasures(measures); + } + + createLibraryMeasure(id, defaults) { + const measure = merge({}, this.measureProperties || {}, defaults || {}); + measure.qDef = measure.qDef ?? {}; + measure.qDef.qNumFormat = measure.qDef.qNumFormat ?? {}; + + if (isEnabled('MASTER_MEASURE_FORMAT')) { + useMasterNumberFormat(measure.qDef); + } + + measure.qLibraryId = id; + measure.qDef.autoSort = true; + + delete measure.qDef.qDef; + delete measure.qDef.qLabel; + + return measure; + } + + addLibraryMeasure(id, defaults) { + const measure = this.createLibraryMeasure(id, defaults); + return this.addMeasure(measure); + } + + addLibraryMeasures(args) { + const measures = args.map(({ id, defaults }) => this.createLibraryMeasure(id, defaults)); + return this.addMeasures(measures); + } + + addAltLibraryMeasures(args) { + const measures = args.map(({ id }) => this.createLibraryMeasure(id)); + return this.addMeasures(measures, true); + } + + addAltExpressionMeasures(args) { + const measures = args.map(({ expression }) => this.createExpressionMeasure(expression)); + return this.addMeasures(measures, true); + } + + addAlternativeExpressionMeasure(expression, label, defaults) { + const measure = this.createExpressionMeasure(expression, label, defaults); + return this.addMeasure(measure, true); + } + + addAlternativeLibraryMeasure(id, defaults) { + const measure = this.createLibraryMeasure(id, defaults); + return this.addMeasure(measure, true); + } + + maxMeasures(decrement) { + const decr = decrement || 0; + if (typeof this.measureDefinition.max === 'function') { + const dimLength = this.getDimensions().length - decr; + const measureParams = isEnabled('PS_21371_ANALYSIS_TYPES') ? [dimLength, this.properties] : [dimLength]; + return this.measureDefinition.max.apply(null, measureParams); + } + return Number.isNaN(+this.measureDefinition.max) ? 10000 : this.measureDefinition.max; + } } export default DataPropertyHandler; diff --git a/apis/supernova/src/handler/hypercube-handler.js b/apis/supernova/src/handler/hypercube-handler.js index 189bb1a08..22098a3ab 100644 --- a/apis/supernova/src/handler/hypercube-handler.js +++ b/apis/supernova/src/handler/hypercube-handler.js @@ -1,7 +1,12 @@ // eslint-disable-next-line import/no-relative-packages import utils from '../../../conversion/src/utils'; import DataPropertyHandler from './data-property-handler'; -import { getHyperCube, setFieldProperties } from './utils/handler-helper'; +import * as hcHelper from './utils/hypercube-helper/hypercube-utils'; +import getAutoSortLibraryDimension from './utils/field-helper/get-sorted-library-field'; +import getAutoSortFieldDimension from './utils/field-helper/get-sorted-field'; +import { initializeField, initializeId } from './utils/field-helper/field-utils'; +import addMainDimension from './utils/hypercube-helper/add-main-dimension'; +import addMainMeasure from './utils/hypercube-helper/add-main-measure'; class HyperCubeHandler extends DataPropertyHandler { constructor(opts) { @@ -11,7 +16,7 @@ class HyperCubeHandler extends DataPropertyHandler { setProperties(properties) { if (!properties) { - return; + return {}; } super.setProperties(properties); @@ -19,38 +24,17 @@ class HyperCubeHandler extends DataPropertyHandler { this.hcProperties = this.path ? utils.getValue(properties, `${this.path}.qHyperCubeDef`) : properties.qHyperCubeDef; if (!this.hcProperties) { - return; + return {}; } - // Set defaults - this.hcProperties.qDimensions = this.hcProperties.qDimensions ?? []; - this.hcProperties.qMeasures = this.hcProperties.qMeasures ?? []; - this.hcProperties.qInterColumnSortOrder = this.hcProperties.qInterColumnSortOrder ?? []; - this.hcProperties.qLayoutExclude = this.hcProperties.qLayoutExclude ?? { - qHyperCubeDef: { qDimensions: [], qMeasures: [] }, - }; - this.hcProperties.qLayoutExclude.qHyperCubeDef = this.hcProperties.qLayoutExclude.qHyperCubeDef ?? { - qDimensions: [], - qMeasures: [], - }; - this.hcProperties.qLayoutExclude.qHyperCubeDef.qDimensions = - this.hcProperties.qLayoutExclude.qHyperCubeDef.qDimensions ?? []; - this.hcProperties.qLayoutExclude.qHyperCubeDef.qMeasures = - this.hcProperties.qLayoutExclude.qHyperCubeDef.qMeasures ?? []; - - if ( - this.hcProperties.isHCEnabled && - this.hcProperties.qDynamicScript.length === 0 && - this.hcProperties.qMode === 'S' - ) { - // this is only for line chart with forecast - this.hcProperties.qDynamicScript = []; - } + hcHelper.setDefaultProperties(this); + hcHelper.setPropForLineChartWithForecast(this); // Set auto-sort property (compatibility 0.85 -> 0.9), // can probably be removed in 1.0 - this.hcProperties.qDimensions = setFieldProperties(this.hcProperties.qDimensions); - this.hcProperties.qMeasures = setFieldProperties(this.hcProperties.qMeasures); + this.hcProperties.qDimensions = hcHelper.setFieldProperties(this.hcProperties.qDimensions); + this.hcProperties.qMeasures = hcHelper.setFieldProperties(this.hcProperties.qMeasures); + return {}; } // ---------------------------------- @@ -70,10 +54,52 @@ class HyperCubeHandler extends DataPropertyHandler { } getDimensionLayouts() { - const hc = getHyperCube(this.layout, this.path); + const hc = hcHelper.getHyperCube(this.layout, this.path); return hc ? hc.qDimensionInfo : []; } + addDimension(dimension, alternative, idx) { + const dim = initializeField(dimension); + + if (hcHelper.isDimensionAlternative(this, dim, alternative)) { + return hcHelper.addAlternativeDimension(this, dim, idx); + } + + return addMainDimension(this, dim, idx); + } + + async addDimensions(dimensions, alternative = false) { + const existingDimensions = this.getDimensions(); + const addedDimensions = []; + let addedActive = 0; + + // eslint-disable-next-line no-restricted-syntax + for await (const dimension of dimensions) { + if (hcHelper.isTotalDimensionsExceeded(this, existingDimensions)) { + return addedDimensions; + } + + const dim = initializeField(dimension); + + if (hcHelper.isDimensionAlternative(this, alternative)) { + const altDim = await hcHelper.addAlternativeDimension(this, dim); + addedDimensions.push(altDim); + } else if (existingDimensions.length < this.maxDimensions()) { + await hcHelper.addActiveDimension(this, dim, existingDimensions, addedDimensions, addedActive); + addedActive++; + } + } + + return addedDimensions; + } + + autoSortDimension(dimension) { + if (dimension.qLibraryId) { + return getAutoSortLibraryDimension(this, dimension); + } + return getAutoSortFieldDimension(this, dimension); + } + // ---------------------------------- // ------------ MEASURES ------------ // ---------------------------------- @@ -87,13 +113,57 @@ class HyperCubeHandler extends DataPropertyHandler { } getMeasureLayouts() { - const hc = getHyperCube(this.layout, this.path); + const hc = hcHelper.getHyperCube(this.layout, this.path); return hc ? hc.qMeasureInfo : []; } getMeasureLayout(cId) { return this.getMeasureLayouts().filter((item) => cId === item.cId)[0]; } + + addMeasure(measure, alternative, idx) { + const meas = initializeField(measure); + + if (hcHelper.isMeasureAlternative(this, meas, alternative)) { + const hcMeasures = this.hcProperties.qLayoutExclude.qHyperCubeDef.qMeasures; + return hcHelper.addAlternativeMeasure(meas, hcMeasures, idx); + } + + return addMainMeasure(this, meas, idx); + } + + // eslint-disable-next-line class-methods-use-this + autoSortMeasure(measure) { + const meas = { ...measure }; + meas.qSortBy = { + qSortByLoadOrder: 1, + qSortByNumeric: -1, + }; + return Promise.resolve(meas); + } + + addMeasures(measures, alternative = false) { + const existingMeasures = this.getMeasures(); + const addedMeasures = []; + let addedActive = 0; + measures.forEach(async (measure) => { + if (hcHelper.isTotalMeasureExceeded(this, existingMeasures)) { + return false; + } + + const meas = initializeId(measure); + + if (hcHelper.isMeasureAlternative(this, existingMeasures, alternative)) { + hcHelper.addAlternativeMeasure(this, meas); + addedMeasures.push(meas); + } else if (existingMeasures.length < this.maxMeasures()) { + await hcHelper.addActiveMeasure(this, meas, existingMeasures, addedMeasures, addedActive); + addedActive++; + } + return true; + }); + return addedMeasures; + } } export default HyperCubeHandler; diff --git a/apis/supernova/src/handler/utils/constants.js b/apis/supernova/src/handler/utils/constants.js new file mode 100644 index 000000000..463a8324c --- /dev/null +++ b/apis/supernova/src/handler/utils/constants.js @@ -0,0 +1,14 @@ +export const TOTAL_MAX = { + DIMENSIONS: 1000, // Maximum number of active dimensions + disabled dimensions + MEASURES: 1000, // Maximum number of active measures + disabled measures +}; + +export const AUTOCALENDAR_NAME = '.autoCalendar'; + +export const INITIAL_SORT_CRITERIAS = [ + { + qSortByLoadOrder: 1, + qSortByNumeric: 1, + qSortByAscii: 1, + }, +]; diff --git a/apis/supernova/src/handler/utils/field-helper/__tests__/expand-field-derived-data.test.js b/apis/supernova/src/handler/utils/field-helper/__tests__/expand-field-derived-data.test.js new file mode 100644 index 000000000..180939d44 --- /dev/null +++ b/apis/supernova/src/handler/utils/field-helper/__tests__/expand-field-derived-data.test.js @@ -0,0 +1,70 @@ +import expandFieldsWithDerivedData from '../expand-field-derived-data'; +import * as getDataGeoField from '../get-data-geo-field'; +import * as getDerivedFields from '../get-derived-fields'; + +jest.mock('../get-data-geo-field', () => jest.fn()); +jest.mock('../get-derived-fields', () => jest.fn()); + +describe('expandFieldsWithDerivedData', () => { + let inputList; + let geoField; + let derivedFields; + + beforeEach(() => { + inputList = [{ name: 'field1' }]; + geoField = { name: 'geoField' }; + derivedFields = [{ name: 'derivedFields' }]; + }); + + test('should expand fields with geo and derived fields', () => { + getDataGeoField.mockReturnValue(geoField); + getDerivedFields.mockReturnValue(derivedFields); + + const result = expandFieldsWithDerivedData(inputList); + + expect(result).toEqual([geoField, { name: 'derivedFields' }]); + }); + + test('should handle an empty input list', () => { + inputList = []; + getDataGeoField.mockReturnValue(geoField); + getDerivedFields.mockReturnValue(derivedFields); + + const result = expandFieldsWithDerivedData(inputList); + + expect(getDataGeoField.default).not.toHaveBeenCalled(); + expect(getDerivedFields.default).not.toHaveBeenCalled(); + expect(result).toEqual([]); + }); + + test('should handle fields with no derived fields', () => { + derivedFields = []; + getDataGeoField.mockReturnValue(geoField); + getDerivedFields.mockReturnValue(derivedFields); + + const result = expandFieldsWithDerivedData(inputList); + + expect(getDataGeoField).toHaveBeenCalledTimes(1); + expect(getDataGeoField).toHaveBeenCalledWith({ name: 'field1' }); + + expect(getDerivedFields).toHaveBeenCalledTimes(1); + expect(getDerivedFields).toHaveBeenCalledWith({ name: 'field1' }); + + expect(result).toEqual([geoField]); + }); + + test('should handle fields with no geo field', () => { + geoField = []; + getDataGeoField.mockReturnValue([]); + getDerivedFields.mockReturnValue(derivedFields); + + const result = expandFieldsWithDerivedData(inputList); + + expect(getDataGeoField).toHaveBeenCalledTimes(1); + expect(getDataGeoField).toHaveBeenCalledWith({ name: 'field1' }); + + expect(getDerivedFields).toHaveBeenCalledTimes(1); + expect(getDerivedFields).toHaveBeenCalledWith({ name: 'field1' }); + expect(result).toEqual([[], ...derivedFields]); + }); +}); diff --git a/apis/supernova/src/handler/utils/field-helper/__tests__/find-field-in-expandedList.test.js b/apis/supernova/src/handler/utils/field-helper/__tests__/find-field-in-expandedList.test.js new file mode 100644 index 000000000..bf03a4bfe --- /dev/null +++ b/apis/supernova/src/handler/utils/field-helper/__tests__/find-field-in-expandedList.test.js @@ -0,0 +1,39 @@ +import findFieldInExpandedList from '../find-field-in-expandedList'; +import * as expandFieldsWithDerivedData from '../expand-field-derived-data'; + +describe('findFieldInExpandedList', () => { + let fieldList; + let expandedList; + + beforeEach(() => { + fieldList = [{ qName: 'field1' }, { qName: 'field2' }]; + expandedList = [{ qName: 'field1' }, { qName: 'field2' }, { qName: 'derivedField' }]; + }); + afterEach(() => { + jest.clearAllMocks(); + }); + + test('should return the field if it exists in the expanded list', () => { + jest.spyOn(expandFieldsWithDerivedData, 'default').mockImplementation(() => expandedList); + + const result = findFieldInExpandedList('field1', fieldList); + expect(result).toEqual({ qName: 'field1' }); + }); + + test('should return null if the field does not exist in the expanded list', () => { + jest.spyOn(expandFieldsWithDerivedData, 'default').mockImplementation(() => expandedList); + + const result = findFieldInExpandedList('nonExistentField', fieldList); + + expect(result).toBeNull(); + }); + + test('should return null if the expanded list is empty', () => { + fieldList = []; + expandedList = null; + jest.spyOn(expandFieldsWithDerivedData, 'default').mockImplementation(() => expandedList); + + const result = findFieldInExpandedList('field1', fieldList); + expect(result).toBeNull(); + }); +}); diff --git a/apis/supernova/src/handler/utils/field-helper/__tests__/get-data-geo-field.test.js b/apis/supernova/src/handler/utils/field-helper/__tests__/get-data-geo-field.test.js new file mode 100644 index 000000000..8f97d0fbc --- /dev/null +++ b/apis/supernova/src/handler/utils/field-helper/__tests__/get-data-geo-field.test.js @@ -0,0 +1,24 @@ +import getDataGeoField from '../get-data-geo-field'; +import { isDateField, isGeoField } from '../field-utils'; + +jest.mock('../field-utils', () => ({ + isDateField: jest.fn(), + isGeoField: jest.fn(), +})); + +describe('getDataGeoField', () => { + afterEach(() => { + jest.clearAllMocks(); + }); + + test('should return field with the correct property values', () => { + const field = { name: 'dateField' }; + isDateField.mockReturnValue(true); + isGeoField.mockReturnValue(false); + + const result = getDataGeoField(field); + + expect(result.isDateField).toBe(true); + expect(result.isGeoField).toBe(false); + }); +}); diff --git a/apis/supernova/src/handler/utils/field-helper/__tests__/get-derived-fields.test.js b/apis/supernova/src/handler/utils/field-helper/__tests__/get-derived-fields.test.js new file mode 100644 index 000000000..703bb0561 --- /dev/null +++ b/apis/supernova/src/handler/utils/field-helper/__tests__/get-derived-fields.test.js @@ -0,0 +1,131 @@ +import getDerivedFields from '../get-derived-fields'; +import { trimAutoCalendarName } from '../field-utils'; + +jest.mock('../field-utils', () => ({ + trimAutoCalendarName: jest.fn((name) => `trimmed_${name}`), +})); + +describe('getDerivedFields', () => { + let field; + + beforeEach(() => { + field = { + qName: 'field1', + qSrcTables: ['table1'], + isDateField: true, + qDerivedFieldData: { + qDerivedFieldLists: [ + { + qDerivedDefinitionName: 'DerivedDef1', + qFieldDefs: [ + { + qName: 'derivedField1', + qTags: ['tag1'], + }, + ], + }, + ], + }, + }; + }); + afterEach(() => { + jest.clearAllMocks(); + }); + + test('should return an empty array if derived field data is undefined', () => { + field = { qName: 'field1' }; + + const result = getDerivedFields(field); + + expect(result).toEqual([]); + }); + + test('should return an empty array if derived field data list is empty', () => { + field = { + qName: 'field1', + qDerivedFieldData: { + qDerivedFieldLists: [], + }, + }; + + const result = getDerivedFields(field); + + expect(result).toEqual([]); + }); + + test('should return derived fields with correct properties', () => { + const result = getDerivedFields(field); + + expect(trimAutoCalendarName).toHaveBeenCalledWith('derivedField1'); + expect(result).toEqual([ + { + qName: 'derivedField1', + displayName: 'trimmed_derivedField1', + qSrcTables: ['table1'], + qTags: ['tag1'], + isDerived: true, + isDerivedFromDate: true, + sourceField: 'field1', + derivedDefinitionName: 'DerivedDef1', + }, + ]); + }); + + test('should set isDerivedFromDate to false if field.isDateField is false', () => { + field.isDateField = false; + + const result = getDerivedFields(field); + + expect(result[0].isDerivedFromDate).toBe(false); + }); + + test('should handle multiple derived fields', () => { + field = { + qName: 'field1', + qSrcTables: ['table1'], + isDateField: true, + qDerivedFieldData: { + qDerivedFieldLists: [ + { + qDerivedDefinitionName: 'DerivedDef1', + qFieldDefs: [ + { + qName: 'derivedField1', + qTags: ['tag1'], + }, + { + qName: 'derivedField2', + qTags: ['tag2'], + }, + ], + }, + ], + }, + }; + + const result = getDerivedFields(field); + + expect(result).toEqual([ + { + qName: 'derivedField1', + displayName: 'trimmed_derivedField1', + qSrcTables: ['table1'], + qTags: ['tag1'], + isDerived: true, + isDerivedFromDate: true, + sourceField: 'field1', + derivedDefinitionName: 'DerivedDef1', + }, + { + qName: 'derivedField2', + displayName: 'trimmed_derivedField2', + qSrcTables: ['table1'], + qTags: ['tag2'], + isDerived: true, + isDerivedFromDate: true, + sourceField: 'field1', + derivedDefinitionName: 'DerivedDef1', + }, + ]); + }); +}); diff --git a/apis/supernova/src/handler/utils/field-helper/__tests__/get-sorted-field.test.js b/apis/supernova/src/handler/utils/field-helper/__tests__/get-sorted-field.test.js new file mode 100644 index 000000000..ba4ff1e10 --- /dev/null +++ b/apis/supernova/src/handler/utils/field-helper/__tests__/get-sorted-field.test.js @@ -0,0 +1,80 @@ +import getAutoSortFieldDimension from '../get-sorted-field'; +import findFieldInExpandedList from '../find-field-in-expandedList'; +import { setAutoSort } from '../field-utils'; + +jest.mock('../find-field-in-expandedList', () => jest.fn()); +jest.mock('../field-utils', () => ({ + setAutoSort: jest.fn(), +})); + +describe('getAutoSortFieldDimension', () => { + let self; + let dimension; + + beforeEach(() => { + self = { + app: { + getFieldList: jest.fn(), + }, + }; + dimension = { + qDef: { + qFieldDefs: ['field1'], + }, + }; + }); + + afterEach(() => { + jest.clearAllMocks(); + }); + + test('should call findFieldInExpandedList with correct arguments', async () => { + const fieldList = [{ qName: 'field1' }]; + self.app.getFieldList.mockResolvedValue(fieldList); + + const result = await getAutoSortFieldDimension(self, dimension); + + expect(self.app.getFieldList).toHaveBeenCalled(); + expect(findFieldInExpandedList).toHaveBeenCalledWith('field1', fieldList); + expect(result).toBe(dimension); + }); + + test('should call setAutoSort if a field is found', async () => { + const fieldList = [{ qName: 'field1' }]; + const field = { qName: 'field1' }; + self.app.getFieldList.mockResolvedValue(fieldList); + findFieldInExpandedList.mockReturnValue(field); + + await getAutoSortFieldDimension(self, dimension); + + expect(setAutoSort).toHaveBeenCalledWith([field], dimension, self); + }); + + test('should not call setAutoSort if no field is found', async () => { + self.app.getFieldList.mockResolvedValue([]); + findFieldInExpandedList.mockReturnValue(null); + + await getAutoSortFieldDimension(self, dimension); + + expect(setAutoSort).not.toHaveBeenCalled(); + }); + + test('should handle empty field list', async () => { + self.app.getFieldList.mockResolvedValue([]); + + const result = await getAutoSortFieldDimension(self, dimension); + + expect(result).toBe(dimension); + expect(setAutoSort).not.toHaveBeenCalled(); + }); + + test('should handle missing qFieldDefs', async () => { + dimension.qDef.qFieldDefs = undefined; + self.app.getFieldList.mockResolvedValue([]); + + const result = await getAutoSortFieldDimension(self, dimension); + + expect(result).toBe(dimension); + expect(setAutoSort).not.toHaveBeenCalled(); + }); +}); diff --git a/apis/supernova/src/handler/utils/field-helper/__tests__/get-sorted-library-field.test.js b/apis/supernova/src/handler/utils/field-helper/__tests__/get-sorted-library-field.test.js new file mode 100644 index 000000000..a89c7af0b --- /dev/null +++ b/apis/supernova/src/handler/utils/field-helper/__tests__/get-sorted-library-field.test.js @@ -0,0 +1,77 @@ +import getAutoSortLibraryDimension from '../get-sorted-library-field'; +import { findLibraryItem, setAutoSort } from '../field-utils'; + +jest.mock('../field-utils', () => ({ + findLibraryItem: jest.fn(), + setAutoSort: jest.fn(), +})); + +describe('getAutoSortLibraryDimension', () => { + let self; + let dimension; + + beforeEach(() => { + self = { + app: { + getDimensionList: jest.fn(), + }, + }; + dimension = { + qLibraryId: 'libDim1', + }; + }); + + afterEach(() => { + jest.clearAllMocks(); + }); + + test('should call findLibraryItem with correct arguments', async () => { + const dimensionList = [{ qInfo: { qId: 'libDim1' }, qData: { info: ['field1'] } }]; + const libDim = dimensionList[0]; + self.app.getDimensionList.mockResolvedValue(dimensionList); + findLibraryItem.mockReturnValue(libDim); + + const result = await getAutoSortLibraryDimension(self, dimension); + + expect(findLibraryItem).toHaveBeenCalledWith('libDim1', dimensionList); + expect(result).toBe(dimension); + }); + + test('should call setAutoSort if a library dimension is found', async () => { + const dimensionList = [{ qInfo: { qId: 'libDim1' }, qData: { info: ['field1'] } }]; + const libDim = dimensionList[0]; + self.app.getDimensionList.mockResolvedValue(dimensionList); + + await getAutoSortLibraryDimension(self, dimension); + + expect(setAutoSort).toHaveBeenCalledWith(libDim.qData.info, dimension, self); + }); + + test('should not call setAutoSort if no library dimension is found', async () => { + self.app.getDimensionList.mockResolvedValue([]); + findLibraryItem.mockReturnValue(null); + + await getAutoSortLibraryDimension(self, dimension); + + expect(setAutoSort).not.toHaveBeenCalled(); + }); + + test('should handle empty dimension list', async () => { + self.app.getDimensionList.mockResolvedValue([]); + + const result = await getAutoSortLibraryDimension(self, dimension); + + expect(result).toBe(dimension); + expect(setAutoSort).not.toHaveBeenCalled(); + }); + + test('should handle missing dimension list', async () => { + dimension = undefined; + self.app.getDimensionList.mockResolvedValue([]); + + const result = await getAutoSortLibraryDimension(self, dimension); + + expect(result).toBe(dimension); + expect(setAutoSort).not.toHaveBeenCalled(); + }); +}); diff --git a/apis/supernova/src/handler/utils/field-helper/__tests__/utils.test.js b/apis/supernova/src/handler/utils/field-helper/__tests__/utils.test.js new file mode 100644 index 000000000..5ac3ed438 --- /dev/null +++ b/apis/supernova/src/handler/utils/field-helper/__tests__/utils.test.js @@ -0,0 +1,65 @@ +import { isDateField } from '../field-utils'; + +describe('isDateField', () => { + test('should return true if field has $date tag', () => { + const field01 = { + qDerivedFieldData: {}, + qTags: ['$date', 'otherTag'], + }; + + const field02 = { + qDerivedFieldData: {}, + qTags: ['otherTag', '$timestamp'], + }; + + const result01 = isDateField(field01); + expect(result01).toBe(true); + + const result02 = isDateField(field02); + expect(result02).toBe(true); + }); + + test('should return false if field does not have $date or $timestamp tag', () => { + const field = { + qDerivedFieldData: {}, + qTags: ['otherTag'], + }; + + const result = isDateField(field); + expect(result).toBeFalsy(); + }); + + test('should return false if qDerivedFieldData is missing', () => { + const field = { + qTags: ['$date'], + }; + + const result = isDateField(field); + expect(result).toBeFalsy(); + }); + + test('should return false if qTags is empty', () => { + const field = { + qDerivedFieldData: {}, + qTags: [], + }; + + const result = isDateField(field); + expect(result).toBeFalsy(); + }); + + test('should return false if field is null or undefined', () => { + expect(isDateField([])).toBeFalsy(); + expect(isDateField(undefined)).toBeFalsy(); + }); + + test('should return false if qTags is not an array', () => { + const field = { + qDerivedFieldData: {}, + qTags: null, + }; + + const result = isDateField(field); + expect(result).toBeFalsy(); + }); +}); diff --git a/apis/supernova/src/handler/utils/field-helper/expand-field-derived-data.js b/apis/supernova/src/handler/utils/field-helper/expand-field-derived-data.js new file mode 100644 index 000000000..8d172914c --- /dev/null +++ b/apis/supernova/src/handler/utils/field-helper/expand-field-derived-data.js @@ -0,0 +1,16 @@ +import getDataGeoField from './get-data-geo-field'; +import getDerivedFields from './get-derived-fields'; + +const expandFieldsWithDerivedData = (list) => { + const fieldList = []; + list.forEach((field) => { + fieldList.push(getDataGeoField(field)); + + const derivedFields = getDerivedFields(field); + fieldList.push(...derivedFields); + }); + + return fieldList; +}; + +export default expandFieldsWithDerivedData; diff --git a/apis/supernova/src/handler/utils/field-helper/field-utils.js b/apis/supernova/src/handler/utils/field-helper/field-utils.js new file mode 100644 index 000000000..270ae14f1 --- /dev/null +++ b/apis/supernova/src/handler/utils/field-helper/field-utils.js @@ -0,0 +1,85 @@ +// eslint-disable-next-line import/no-relative-packages +import uid from '../../../../../nucleus/src/object/uid'; +import { AUTOCALENDAR_NAME } from '../constants'; + +/** + * Get the field name from the expression. + * @param {string} expression + * @returns the field + */ +export const getField = (expression) => { + let exp = expression; + exp = exp.trim(); + if (exp.charAt(0) === '=') { + exp = exp.substring(1); + exp = exp.trim(); + } + const lastIndex = exp.length - 1; + if (exp.charAt(0) === '[' && exp.charAt(lastIndex) === ']') { + exp = exp.substring(1, lastIndex); + exp = exp.trim(); + } + return exp; +}; + +export const findFieldById = (fields, id) => (fields && fields.find((field) => field.qDef?.cId === id)) || null; + +export const findLibraryItem = (id, masterItemList) => + (masterItemList && masterItemList.find((item) => item.qInfo.qId === id)) || null; + +export const findFieldByName = (name, fieldList) => + (fieldList && fieldList.find((field) => field.qName === name)) || null; + +export const initializeId = (field) => ({ + ...field, + qDef: { + ...field.qDef, + cId: field.qDef?.cId ?? uid(), + }, +}); + +export const initializeField = (field) => ({ + ...initializeId(field), + qOtherTotalSpec: field.qOtherTotalSpec ?? {}, +}); + +export const setAutoSort = (fields, dimension, self) => { + const dim = dimension; + fields.forEach((field, index) => { + const tags = field.qTags; + const sortCriterias = { + qSortByLoadOrder: 1, + }; + + if (typeof self.dimensionDefinition.autoSort === 'function') { + self.dimensionDefinition.autoSort(dim, self.properties, tags, sortCriterias, self); + } else { + // Default auto sorting + sortCriterias.qSortByNumeric = 1; + sortCriterias.qSortByAscii = 1; + } + + if (!dim.qDef.qSortCriterias) { + dim.qDef.qSortCriterias = [sortCriterias]; + } else { + dim.qDef.qSortCriterias[index] = sortCriterias; + } + }); +}; + +export const useMasterNumberFormat = (formatting) => { + const format = formatting; + format.quarantine = { + qNumFormat: format.qNumFormat || {}, + isCustomFormatted: format.isCustomFormatted || false, + }; + format.qNumFormat = null; + format.isCustomFormatted = undefined; +}; + +export const isDateField = (field) => + field?.qDerivedFieldData && (field?.qTags?.indexOf('$date') > -1 || field?.qTags?.indexOf('$timestamp') > -1); + +export const isGeoField = (field) => field.qTags.indexOf('$geoname') > -1; + +export const trimAutoCalendarName = (fieldName) => (fieldName ? fieldName.split(AUTOCALENDAR_NAME).join('') : ''); diff --git a/apis/supernova/src/handler/utils/field-helper/find-field-in-expandedList.js b/apis/supernova/src/handler/utils/field-helper/find-field-in-expandedList.js new file mode 100644 index 000000000..b10658cdf --- /dev/null +++ b/apis/supernova/src/handler/utils/field-helper/find-field-in-expandedList.js @@ -0,0 +1,10 @@ +import expandFieldsWithDerivedData from './expand-field-derived-data'; +import { findFieldByName, getField } from './field-utils'; + +const findFieldInExpandedList = (name, fieldList) => { + const expandedList = expandFieldsWithDerivedData(fieldList.slice(0)); + const fieldName = getField(name); + return (expandedList && findFieldByName(fieldName, expandedList)) || null; +}; + +export default findFieldInExpandedList; diff --git a/apis/supernova/src/handler/utils/field-helper/get-data-geo-field.js b/apis/supernova/src/handler/utils/field-helper/get-data-geo-field.js new file mode 100644 index 000000000..30099e221 --- /dev/null +++ b/apis/supernova/src/handler/utils/field-helper/get-data-geo-field.js @@ -0,0 +1,10 @@ +import { isDateField, isGeoField } from './field-utils'; + +const getDataGeoField = (field) => { + const item = field; + item.isDateField = isDateField(item); + item.isGeoField = isGeoField(item); + return item; +}; + +export default getDataGeoField; diff --git a/apis/supernova/src/handler/utils/field-helper/get-derived-fields.js b/apis/supernova/src/handler/utils/field-helper/get-derived-fields.js new file mode 100644 index 000000000..566fe34e6 --- /dev/null +++ b/apis/supernova/src/handler/utils/field-helper/get-derived-fields.js @@ -0,0 +1,28 @@ +import { trimAutoCalendarName } from './field-utils'; + +const getDerivedFields = (field) => { + const derivedFields = []; + + if (!field.qDerivedFieldData) { + return derivedFields; + } + + field.qDerivedFieldData.qDerivedFieldLists.forEach((derived) => { + derived.qFieldDefs.forEach((derivedField) => { + derivedFields.push({ + qName: derivedField.qName, + displayName: trimAutoCalendarName(derivedField.qName), + qSrcTables: field.qSrcTables, + qTags: derivedField.qTags, + isDerived: true, + isDerivedFromDate: field.isDateField, + sourceField: field.qName, + derivedDefinitionName: derived.qDerivedDefinitionName, + }); + }); + }); + + return derivedFields; +}; + +export default getDerivedFields; diff --git a/apis/supernova/src/handler/utils/field-helper/get-sorted-field.js b/apis/supernova/src/handler/utils/field-helper/get-sorted-field.js new file mode 100644 index 000000000..70b43c2a4 --- /dev/null +++ b/apis/supernova/src/handler/utils/field-helper/get-sorted-field.js @@ -0,0 +1,14 @@ +import findFieldInExpandedList from './find-field-in-expandedList'; +import { setAutoSort } from './field-utils'; + +function getAutoSortFieldDimension(self, dimension) { + return self.app.getFieldList().then((fieldList) => { + const field = dimension?.qDef?.qFieldDefs && findFieldInExpandedList(dimension.qDef.qFieldDefs[0], fieldList); + if (field) { + setAutoSort([field], dimension, self); + } + return dimension; + }); +} + +export default getAutoSortFieldDimension; diff --git a/apis/supernova/src/handler/utils/field-helper/get-sorted-library-field.js b/apis/supernova/src/handler/utils/field-helper/get-sorted-library-field.js new file mode 100644 index 000000000..441419b80 --- /dev/null +++ b/apis/supernova/src/handler/utils/field-helper/get-sorted-library-field.js @@ -0,0 +1,13 @@ +import { findLibraryItem, setAutoSort } from './field-utils'; + +function getAutoSortLibraryDimension(self, dimension) { + return self.app.getDimensionList().then((dimensionList) => { + const libDim = dimension?.qLibraryId && findLibraryItem(dimension.qLibraryId, dimensionList); + if (libDim) { + setAutoSort(libDim.qData.info, dimension, self); + } + return dimension; + }); +} + +export default getAutoSortLibraryDimension; diff --git a/apis/supernova/src/handler/utils/handler-helper.js b/apis/supernova/src/handler/utils/handler-helper.js deleted file mode 100644 index b5943002a..000000000 --- a/apis/supernova/src/handler/utils/handler-helper.js +++ /dev/null @@ -1,32 +0,0 @@ -// eslint-disable-next-line import/no-relative-packages -import getValue from '../../../../conversion/src/utils'; - -export const getFieldById = (fields, id) => fields.find((field) => field.qDef?.cId === id) || null; - -export const setFieldProperties = (hcFieldProperties) => { - if (!hcFieldProperties) { - return []; - } - const updatedProperties = [...hcFieldProperties]; - - return updatedProperties.map((field) => { - if (field.qDef?.autoSort && field.autoSort !== undefined) { - return { - ...field, - qDef: { - ...field.qDef, - autoSort: field.autoSort, - }, - autoSort: undefined, - }; - } - return field; - }); -}; - -export const getHyperCube = (layout, path) => { - if (!layout) { - return undefined; - } - return path && getValue(layout, path) ? getValue(layout, path).qHyperCube : layout.qHyperCube; -}; diff --git a/apis/supernova/src/handler/utils/hypercube-helper/add-main-dimension.js b/apis/supernova/src/handler/utils/hypercube-helper/add-main-dimension.js new file mode 100644 index 000000000..1a4ce0afb --- /dev/null +++ b/apis/supernova/src/handler/utils/hypercube-helper/add-main-dimension.js @@ -0,0 +1,12 @@ +import { insertMainDimension } from './hypercube-utils'; + +export default function addMainDimension(self, dimension, index) { + const dimensions = self.getDimensions(); + const idx = index ?? dimensions.length; + + if (dimensions.length < self.maxDimensions()) { + return insertMainDimension(self, dimension, dimensions, idx); + } + + return Promise.resolve(); +} diff --git a/apis/supernova/src/handler/utils/hypercube-helper/add-main-measure.js b/apis/supernova/src/handler/utils/hypercube-helper/add-main-measure.js new file mode 100644 index 000000000..a103548aa --- /dev/null +++ b/apis/supernova/src/handler/utils/hypercube-helper/add-main-measure.js @@ -0,0 +1,12 @@ +import { insertMainMeasure } from './hypercube-utils'; + +export default function addMainMeasure(self, measure, index) { + const measures = self.getMeasures(); + const idx = index ?? measures.length; + + if (measures.length < self.maxMeasures()) { + insertMainMeasure(measure, measures, idx); + } + + return measure; +} diff --git a/apis/supernova/src/handler/utils/hypercube-helper/hypercube-utils.js b/apis/supernova/src/handler/utils/hypercube-helper/hypercube-utils.js new file mode 100644 index 000000000..ba8d5059c --- /dev/null +++ b/apis/supernova/src/handler/utils/hypercube-helper/hypercube-utils.js @@ -0,0 +1,182 @@ +// eslint-disable-next-line import/no-relative-packages +import getValue from '../../../../../conversion/src/utils'; +// eslint-disable-next-line import/no-relative-packages +import arrayUtil from '../../../../../conversion/src/array-util'; +import { TOTAL_MAX } from '../constants'; + +export const notSupportedError = new Error('Not supported in this object, need to implement in subclass.'); + +export const setFieldProperties = (hcFieldProperties) => { + if (!hcFieldProperties) { + return []; + } + const updatedProperties = [...hcFieldProperties]; + + return updatedProperties.map((field) => { + if (field.qDef?.autoSort && field.autoSort !== undefined) { + return { + ...field, + qDef: { + ...field.qDef, + autoSort: field.autoSort, + }, + autoSort: undefined, + }; + } + return field; + }); +}; + +export const getHyperCube = (layout, path) => { + if (!layout) { + return undefined; + } + return path && getValue(layout, path) ? getValue(layout, path).qHyperCube : layout.qHyperCube; +}; + +export function setDefaultProperties(self) { + const current = self; + current.hcProperties.qDimensions = current.hcProperties.qDimensions ?? []; + current.hcProperties.qMeasures = current.hcProperties.qMeasures ?? []; + current.hcProperties.qInterColumnSortOrder = current.hcProperties.qInterColumnSortOrder ?? []; + current.hcProperties.qLayoutExclude = current.hcProperties.qLayoutExclude ?? { + qHyperCubeDef: { qDimensions: [], qMeasures: [] }, + }; + current.hcProperties.qLayoutExclude.qHyperCubeDef = current.hcProperties.qLayoutExclude.qHyperCubeDef ?? { + qDimensions: [], + qMeasures: [], + }; + current.hcProperties.qLayoutExclude.qHyperCubeDef.qDimensions = + current.hcProperties.qLayoutExclude.qHyperCubeDef.qDimensions ?? []; + current.hcProperties.qLayoutExclude.qHyperCubeDef.qMeasures = + current.hcProperties.qLayoutExclude.qHyperCubeDef.qMeasures ?? []; +} + +export function setPropForLineChartWithForecast(self) { + const current = self; + if ( + current.hcProperties.isHCEnabled && + current.hcProperties.qDynamicScript.length === 0 && + current.hcProperties.qMode === 'S' + ) { + current.hcProperties.qDynamicScript = []; + } +} + +// ---------------------------------- +// ----------- DIMENSIONS ----------- +// ---------------------------------- + +export function addAlternativeDimension(self, dimension, index = undefined) { + const dimensions = self.hcProperties.qLayoutExclude.qHyperCubeDef.qDimensions; + const idx = index ?? dimensions.length; + dimensions.splice(idx, 0, dimension); + return Promise.resolve(dimension); +} + +export function insertMainDimension(self, dimension, dimensions, idx) { + dimensions.splice(idx, 0, dimension); + + return self.autoSortDimension(dimension).then(() => { + arrayUtil.indexAdded(self.hcProperties.qInterColumnSortOrder, self.getDimensions().length + dimension.length - 1); + + if (typeof self.dimensionDefinition.add === 'function') { + return Promise.resolve(self.dimensionDefinition.add.call(null, dimension, self.properties, self)); + } + + return dimension; + }); +} + +export function addSortedDimension(self, dimension, dimensions, idx) { + const dimIdx = idx ?? dimensions.length; + dimensions.splice(dimIdx, 0, dimension); + + return self.autoSortDimension(dimension).then(() => { + arrayUtil.indexAdded(self.hcProperties.qInterColumnSortOrder, dimIdx ?? dimensions.length - 1); + + return self.dimensionDefinition.add?.call(self, dimension, self.properties, self) || Promise.resolve(dimension); + }); +} + +export function isTotalDimensionsExceeded(self, dimensions) { + const altDimensions = self.getAlternativeDimensions(); + return altDimensions.length + dimensions.length >= TOTAL_MAX.DIMENSIONS; +} + +export function isDimensionAlternative(self, alternative) { + const dimensions = self.hcProperties.qLayoutExclude.qHyperCubeDef.qDimensions; + return alternative || (self.maxDimensions() <= dimensions.length && dimensions.length < TOTAL_MAX.DIMENSIONS); +} + +export async function addActiveDimension(self, dimension, existingDimensions, addedDimensions, addedActive) { + const initialLength = existingDimensions.length; + await self.autoSortDimension(dimension); + + // Update sorting order + arrayUtil.indexAdded(self.hcProperties.qInterColumnSortOrder, initialLength + addedActive); + + existingDimensions.push(dimension); + addedDimensions.push(dimension); + + if (typeof self.dimensionDefinition.add === 'function') { + self.dimensionDefinition.add.call(self, dimension, self.properties, self); + } +} + +// ---------------------------------- +// ------------ MEASURES ------------ +// ---------------------------------- + +export function addAlternativeMeasure(self, measure, index = undefined) { + const measures = self.hcProperties.qLayoutExclude.qHyperCubeDef.qMeasures; + const idx = index ?? measures.length; + measures.splice(idx, 0, measure); + return Promise.resolve(measure); +} + +export function insertMainMeasure(self, measure, measures, idx) { + measures.splice(idx, 0, measure); + + return self.autoSortMeasure(measure).then(() => { + arrayUtil.indexAdded(self.hcProperties.qInterColumnSortOrder, self.getDimensions().length + measure.length - 1); + + if (typeof self.measureDefinition.add === 'function') { + return Promise.resolve(self.measureDefinition.add.call(null, measure, self.properties, self)); + } + + return measure; + }); +} + +export function isTotalMeasureExceeded(self, measures) { + // Adding more measures than TOTAL_MAX_MEASURES is not allowed and we expect this.maxMeasures() to always be <= TOTAL_MAX_MEASURES + const altMeasures = self.getAlternativeMeasures(); + return altMeasures.length + measures.length >= TOTAL_MAX.MEASURES; +} + +export function isMeasureAlternative(self, measures, alternative) { + return alternative || (self.maxMeasures() <= measures.length && measures.length < TOTAL_MAX.MEASURES); +} + +export function addActiveMeasure(self, measure, existingMeasures, addedMeasures, addedActive) { + const dimensions = self.getDimensions(); + const meas = { ...measure }; + meas.qSortBy = { + qSortByLoadOrder: 1, + qSortByNumeric: -1, + }; + + arrayUtil.indexAdded( + self.hcProperties.qInterColumnSortOrder, + dimensions.length + existingMeasures.length + addedActive + ); + existingMeasures.push(meas); + addedMeasures.push(meas); + + if (typeof self.measureDefinition.add === 'function') { + self.measureDefinition.add.call(null, meas, self.properties, self); + } + + return Promise.resolve(addedMeasures); +}