-
Notifications
You must be signed in to change notification settings - Fork 57
chore: add hypercube generic functions - part02 #1715
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Conversation
eaa2870 to
0b1317b
Compare
c6ab81c to
8aafde3
Compare
222061c to
96e0ca5
Compare
caa6d6a to
ddd662f
Compare
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Copilot reviewed 9 out of 12 changed files in this pull request and generated 2 comments.
Files not reviewed (3)
- apis/nucleus/package.json: Language not supported
- apis/supernova/package.json: Language not supported
- package.json: Language not supported
b42c161 to
c8d4a7d
Compare
7ee1ab0 to
430b60c
Compare
fc374d0 to
70cd75b
Compare
|
This is mostly just copy/pasted right? Have you done any major refactoring changes? |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Could we change all these folders with index files into a more flat structure? To better match the rest of Nebula, it has more named files and no /folder/index.js setups.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Well...It is mostly because there are lots of functions that are not in use directly in hypercube-handler but only to define some other functions, so they are helper functions of the other functions. But yes, I can change this arrangement to a flatter structure and still keep them in separated files to be able to test them anyway.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
There are a couple of async functions and usage of Promise that I don't understand the usage of. Is there an reason behind it?
EDIT: Why is there a need to use async/Promise at all in the handler?
|
|
||
| if (!this.hcProperties) { | ||
| return; | ||
| return undefined; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Why return undefined? The value from setProperties is not used anywhere?
| if (typeof self.measureDefinition.add === 'function') { | ||
| self.measureDefinition.add.call(null, meas, self.properties, self); | ||
| } | ||
| return Promise.resolve(addedMeasures); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Why does this return a Promise?
| return insertMainMeasure(measure, measures, idx); | ||
| } | ||
|
|
||
| return Promise.resolve(); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Why return a Promise?
| arrayUtil.indexAdded(self.hcProperties.qInterColumnSortOrder, self.getDimensions().length + measure.length - 1); | ||
|
|
||
| if (typeof self.measureDefinition.add === 'function') { | ||
| return Promise.when(self.measureDefinition.add.call(null, measure, self.properties, self)); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
What is Promise.when? And why use a Promise?
| const measures = self.hcProperties.qLayoutExclude.qHyperCubeDef.qMeasures; | ||
| const idx = index ?? measures.length; | ||
| measures.splice(idx, 0, measure); | ||
| return Promise.resolve(measure); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Why return a Promise?
Yes, I see. In sense-client they all return promises, but using AngularJs promises, so I kept them as they were, to avoid breaking anything. I tried removing a couple of them, but the function failed, so it seems they are needed to ensure the data is fetched properly (e.x: adding field and sorting the orders at the right time). but I'll check those you left comment on, maybe I can remove those without facing any problem. |
3020075 to
87b836f
Compare
87b836f to
8a4d712
Compare
| export 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(); | ||
| } |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Moved it to a separated file add-main-dimension.js because of having a nested function
| export function insertFieldAtIndex(self, field, alternative, currentFields, index = undefined) { | ||
| if (alternative || (self.maxDimensions() <= currentFields.length && currentFields.length < TOTAL_MAX.DIMENSIONS)) { | ||
| const fields = self.hcProperties.qLayoutExclude.qHyperCubeDef.qDimensions; | ||
| const idx = index ?? fields.length; | ||
| fields.splice(idx, 0, field); | ||
| return Promise.resolve(field); | ||
| } | ||
| return undefined; | ||
| } | ||
|
|
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I had created this function as a helper fn, but noticed that is redundant.
| export function insertDimensionAtIndex(self, dimension, alternative, currentDimensions, index = undefined) { | ||
| const dimensions = self.hcProperties.qLayoutExclude.qHyperCubeDef.qDimensions; | ||
| const idx = index ?? dimensions.length; | ||
| dimensions.splice(idx, 0, dimension); | ||
| return Promise.resolve(dimension); | ||
| } | ||
|
|
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The same here, it is redundant, so I removed it.
| export function addMainMeasure(self, measure, index) { | ||
| const measures = self.getMeasures(); | ||
| const idx = index ?? measures.length; | ||
|
|
||
| if (measures.length < self.maxMeasures()) { | ||
| return insertMainMeasure(measure, measures, idx); | ||
| } | ||
|
|
||
| return Promise.resolve(); | ||
| } | ||
|
|
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Moved it to a separated file add-main-measure.js
cbt1
left a comment
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Yes, I see. In sense-client they all return promises, but using AngularJs promises, so I kept them as they were, to avoid breaking anything. I tried removing a couple of them, but the function failed, so it seems they are needed to ensure the data is fetched properly (e.x: adding field and sorting the orders at the right time). but I'll check those you left comment on, maybe I can remove those without facing any problem.
I can understand the concern to reduce risk of breaking something. But if there is no async operation in this code. The only other risk I can think of now is some outside code that consumes the code where it calls a method and expect it to be async, maybe it will call for example like this handler.someMethod().then(() => // Do something after method resolves)
| return this.addDimension(dimension, true); | ||
| } | ||
|
|
||
| maxDimensions(decrement) { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Assign a default value directly on the paramter decrement = 0. That way there is no need for const decr = decrement || 0;. Unless decrement can also be other falsy values.
| measure.qDef = measure.qDef ?? {}; | ||
| measure.qDef.qNumFormat = measure.qDef.qNumFormat ?? {}; | ||
|
|
||
| if (isEnabled('MASTER_measureURE_FORMAT')) { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
| if (isEnabled('MASTER_measureURE_FORMAT')) { | |
| if (isEnabled('MASTER_MEASURE_FORMAT')) { |
| arrayUtil.indexAdded(self.hcProperties.qInterColumnSortOrder, self.getDimensions().length + dimension.length - 1); | ||
|
|
||
| if (typeof self.dimensionDefinition.add === 'function') { | ||
| return Promise.when(self.dimensionDefinition.add.call(null, dimension, self.properties, self)); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Promise.when does not exist.
Is this line not covered by unit tests?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Oh Sorry, I missed to fix it.
I splitted this function in my next PR, into two for different type of orders, sort-order and column-order, there it's correct and added some test cases for that but not here.
The fact is this function is not called when I normally add fields so I didn't get any incorrect value or error even when I tested adding-fields manually.
If you don't mind, in case of adding test cases, I'll go for the refactored version of this and the testcases in the next PR since I have already covered it there.
| dimensions.map(async (dimension) => { | ||
| 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; | ||
| }) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I do expect addDimensions to add thing in same order as dimensions array.
this looks to add them in the order of what is done first.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Good catch. But I do think it does add them in the correct order since the "awaited" call is not actually an async function and gets resolved directly.
Either way, the calls inside addDimensions does have to be async. But the method itself might still need to be, since it's possible called as an async function but some code that consumes it.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
the .map (dimensions.map) will start on next item if the previous one needs awaiting.
making it possible for later items to be done before earlier items
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Turns out addActiveDimension is making something that looks like an async call. So you're right, there is a risk of a race condition here.
I would then think for await (const value of object) {} is the way to go. Instead of using dimensions.map().
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Ok, understand. It was for-await in the original code but I thought using .map() might be a better option. I'll change it to for-await.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
@T-Wizard @cbt1 I ended up using await array.reduce() instead. Apparently for await requires regenerator-runtime, which is too heavyweight for it to allow as Eslint error says. The best replacement which follows the same concept is reduce, it waits for each previous round promise to be resolved and then it goes to the next one.
…romise at each round
| const addedDimensions = []; | ||
| let addedActive = 0; | ||
|
|
||
| await dimensions.reduce(async (prevPromise, dimension) => { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
While using reduce in this way will work. I do think you should use for await loop instead as it's easier to read and built to do what you want.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Eslint doesn't let me use it unless I ignore its error. Do you think reduce is risky to use ?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I seem like a hack to me. If someone in the future where to look at this they might wonder why reduce is being used.
IMO it's fine to just ignore the lint error. The error is not valid in this case.
cbt1
left a comment
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Just had some final comments. This LGTM now. But it is kind of hard to tell what has changed in this move. So for upcoming PR:s maybe change as little as possible and then in a follow-up PR make refactoring changes?
| setProperties(properties) { | ||
| if (!properties) { | ||
| return; | ||
| return {}; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Why return an object from setProperties?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
In the original code in sense-client, there is an early 'return' here, however it doesn't return anything. Moving it to nebula, I faced Eslint error so I returned an empty object instead. Is there any better way to do so? then I can change it in the next PR.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This works fine for me without any lint error
setProperties(properties) {
if (!properties) {
return;
}
super.setProperties(properties);
this.hcProperties = this.path ? utils.getValue(properties, `${this.path}.qHyperCubeDef`) : properties.qHyperCubeDef;
if (!this.hcProperties) {
return;
}
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 = hcHelper.setFieldProperties(this.hcProperties.qDimensions);
this.hcProperties.qMeasures = hcHelper.setFieldProperties(this.hcProperties.qMeasures);
}| if (!dim.qDef.qSortCriterias) { | ||
| const updatedDimension = { ...dim }; | ||
| updatedDimension.qDef.qSortCriterias = [sortCriterias]; | ||
| Object.assign(dim, updatedDimension); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I don't understand the usage of Object.assign here? Why not just assigned directly on dim.qDef.qSortCriterias instead?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Umm... true! couldn't remember why I did this... I'll change it.
Thanks @cbt1 for reviewing my PR I totally understand. These are not used anywhere in this repo to give a reference how they used to work. This one was the largest PR, hopefully the next one will be easier to review, however there also I made a few changes, but it is smaller. |
Motivation
Moving generic hypercube functions from sense-client to nebula.js - PART02
In this PR the functions that are required to add field and alternative fields, are considered.
The source files for these functions are in sense-client (data-property-handler.ts & hypercube-handler.js)
P.S: (Part01 can be found here)
Requirements checklist
yarn specWhen build and tests have passed: