Skip to content

Conversation

@jgellin-sf
Copy link
Contributor

@jgellin-sf jgellin-sf commented Dec 22, 2020

What does this PR do?

This PR:

  • creates a utility to get a field type from SObject metadata
  • creates a validation framework for where input using a factory pattern
  • implements validation for boolean, currency, date, integer, floating point, and pickist types
  • implements normalization (quoting and escaping of string values
  • implements special case normalization for null input
  • shows an error message when validation of where input fails

This PR does not:

  • show an error message in a beautiful elegant manner
  • change the HTML input based on type

What issues does this PR fix or reference?

@W-8555377@

@codecov
Copy link

codecov bot commented Dec 22, 2020

Codecov Report

Merging #109 (1723daf) into feature/where (2ebc1df) will decrease coverage by 0.39%.
The diff coverage is 92.22%.

Impacted file tree graph

@@                Coverage Diff                @@
##           feature/where     #109      +/-   ##
=================================================
- Coverage          94.85%   94.46%   -0.40%     
=================================================
  Files                 49       58       +9     
  Lines               1595     1878     +283     
  Branches             344      406      +62     
=================================================
+ Hits                1513     1774     +261     
- Misses                79      101      +22     
  Partials               3        3              
Flag Coverage Δ
language-server 97.46% <ø> (ø)
soql-builder-ui 92.31% <83.06%> (-1.86%) ⬇️
soql-model 95.39% <98.83%> (+0.80%) ⬆️

Flags with carried forward coverage won't be shown. Click here to find out more.

Impacted Files Coverage Δ
...builder-ui/src/modules/querybuilder/where/where.ts 100.00% <ø> (ø)
...rybuilder/whereModifierGroup/whereModifierGroup.ts 83.96% <75.29%> (-14.34%) ⬇️
packages/soql-model/src/validators/validator.ts 93.33% <93.33%> (ø)
...oql-builder-ui/src/modules/querybuilder/app/app.ts 87.85% <100.00%> (+0.11%) ⬆️
.../src/modules/querybuilder/services/sobjectUtils.ts 100.00% <100.00%> (ø)
...-ui/src/modules/querybuilder/services/soqlUtils.ts 95.95% <100.00%> (+0.72%) ⬆️
packages/soql-model/src/messages/messages.ts 100.00% <100.00%> (ø)
packages/soql-model/src/model/model.ts 100.00% <100.00%> (ø)
...ages/soql-model/src/validators/booleanValidator.ts 100.00% <100.00%> (ø)
...ges/soql-model/src/validators/currencyValidator.ts 100.00% <100.00%> (ø)
... and 14 more

Continue to review full report at Codecov.

Legend - Click here to learn more
Δ = absolute <relative> (impact), ø = not affected, ? = missing data
Powered by Codecov. Last update 2ebc1df...1723daf. Read the comment docs.

});
}

get buttonData() {
Copy link
Contributor

@dehru dehru Dec 22, 2020

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This would be best to do in the @api. so add get/set to the buttonLabels and do it in the setter. Then just render buttonData. It's best to keep as much looping as possible outside of the render loop of LWC, since we don't know how often it attempts to re-render itself.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

You could also apply the class names in a function called from the template.
class={getClassName(idx)}
This would allow the button to dynamically restyle whenever the selectedIndex is changed. Altering the selected index will trigger a re-render and calculate the new styles...the most important being which is selected.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ok I put the className in the buttonData so everything needed to render the template is in that object. Then on selection I update the style. Since the buttonData is tracked LWC knows to re-render.

@api selectedIndex = '-1';

renderedCallback() {
const buttonElements = this.template.querySelectorAll('button');
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Opposite of my last comment, this could be best to do in the render loop from html. You can call a function from the template with class={applyStyle(index)} in the template.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Would love to do this, but you can't have a call with arguments in the template.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Sorta kinda do this now with keeping the classlist in the buttondata.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yeah. the limitations of LWC often catch me as a surprise coming from Angular / React's much richer template syntax.

align-self: flex-start;
}
.btn {
height: 1.5em;
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I generally like to use rem vs em personally. Because em is dynamic as compared to it's parent. So that can be funky as text up the dom possibly gets set to "larger" or "smaller", it affects what an em is on the button. Where as rem is based on the root font size, which is just easier to think about logically for me.

Normally, it's not a big issue, but i wanted to mention it from my experience.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I like that better. I made the change.

buttonGroup.addEventListener('selection__changed', handler);
document.body.appendChild(buttonGroup);

const buttonElements = buttonGroup.shadowRoot.querySelectorAll('button');
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think you can just do
const button0 = buttonGroup.shadowRoot.querySelectorAll('button')[0];

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks. Done.

}

@api get selection() {
return parseInt(this.selectedIndex);
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

How do you see this being used vs the event? I noticed you are using it in testing, however, outside of testing, i think the event will likely drive the app.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I agree. It's gone now.

@jgellin-sf jgellin-sf marked this pull request as ready for review January 13, 2021 14:52
@jgellin-sf jgellin-sf requested a review from dehru January 13, 2021 14:52
export const error_incompleteFrom = 'Incomplete FROM clause. The FROM clause requires an object.';
export const error_incompleteLimit = 'Incomplete LIMIT clause. The LIMIT keyword must be followed by a number.';

export const error_fieldInput_boolean = 'Value must be TRUE or FALSE';
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Does case matter here? Must be all caps?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Case does not actually matter, but all-caps is consistent with the way the documentation (https://developer.salesforce.com/docs/atlas.en-us.soql_sosl.meta/soql_sosl/sforce_api_calls_soql.htm) writes elements of SOQL syntax.

@jgellin-sf jgellin-sf requested a review from ekapner January 13, 2021 16:21
export const error_incompleteLimit = 'Incomplete LIMIT clause. The LIMIT keyword must be followed by a number.';

export const error_fieldInput_boolean = 'Value must be TRUE or FALSE';
export const error_fieldInput_currency = 'Currency value is not valid';
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Any guidelines we can add here?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

There's a 3 letter currency code followed by the numeric value according to the ISO standard. So $100 US is USD100. I think the currency value can also be just a floating point number. So the most concise we can get is maybe...

"Value must be a number or a three-letter ISO currency code followed by a number"

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I feel like including the three-letter ISO currency code makes this feel more complicated than necessary.

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

They are all a bit complicated - so let's just leave them as @jgellin-sf wrote them. Thanks for answering my questions in such detail.


export const error_fieldInput_boolean = 'Value must be TRUE or FALSE';
export const error_fieldInput_currency = 'Currency value is not valid';
export const error_fieldInput_date = 'Date value is not valid';
Copy link

@ekapner ekapner Jan 13, 2021

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can we provide valid date format or some other guidance? like: You must enter the date as YYYY-MM-DD (or something like that)

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Date is tricky. Because you can write date as:

export const error_fieldInput_boolean = 'Value must be TRUE or FALSE';
export const error_fieldInput_currency = 'Currency value is not valid';
export const error_fieldInput_date = 'Date value is not valid';
export const error_fieldInput_float = 'Value must be numeric';
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Does this mean "numbers only"? decimals?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Right. So numeric values, containing decimal or not, are fine.

Copy link

@ekapner ekapner left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

A couple questions before I can finalize the validation messages

modelService: ToolingModelService;
messageService: IMessageService;
theme = 'light';
_sobjectTypeUtils: SObjectTypeUtils;
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is there a reason to use a getter/setter here?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I don't think it's really necessary.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is irrelevant following the pass sobject metadata rather than utils object commit

document.body.appendChild(modifierGroup);

const { criteriaInputEl } = getModifierElements();
criteriaInputEl.value = 'FALSE';
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

would this test pass if criteriaInputEl.value was lower case 'false'?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

yes


export class IntegerValidator extends Validator {
public validate(input: string): ValidateResult {
const isValid = /^[+-]?[0-9]+$/.test(input.trim());
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

There's also Number.isInteger() or parseInt(). But i don't know if they are more inclusive than you want.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Those will return a integer for any string starting with an integer, so parseInt() will return 134 for " 134hello".

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

yeah, i guess that's a false positive.

type,
picklistValues
};

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I notice we are checking 2 validations here, but only displaying 1 error message. In your experience with this, is it best to check the input validation for the field first, and if that passes, show check operator validation?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I don't have any special insight to the order of the validations. It felt right. 😂

@api selectedOperator: string;
@api isLoading = false;
@api index;
@api sobjectTypeUtils: SObjectTypeUtils;
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm thinking we should pass the metadata and then process it here instead of with app.ts. using a setter to process the data with sobjectutils

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Made this change in pass sobject metadata rather than utils object commit

import { Soql } from '@salesforce/soql-model';

export class SObjectTypeUtils {
constructor(protected sobjectMetadata: any) {
Copy link
Contributor

@dehru dehru Jan 14, 2021

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

To limit the looping/filtering in this utility, we could use a map so that the key is the field name ( possibly lowercased ). Something like this:

{
    'id' : { name: 'Id', type: 'id', picklistValues: [] },
    'name' : { name: 'Name', type: 'string', picklistValues: [] },
    'accountsource': { name: 'AccountSource', type: 'picklist', picklistValues: [{ value: 'apple' }, { value: 'banana' }, { value: 'cherry' }] },
    'annualrevenue': { name: 'AnnualRevenue', type: 'currency', picklistValues: [] },
    'billingaddress' : { name: 'BillingAddress', type: 'address', picklistValues: [] },
       ...
}

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I updated this code in the Refactor sobject utils for easier map lookups commit.

@dehru
Copy link
Contributor

dehru commented Jan 14, 2021

This is amazingly cool work JG!

@dehru dehru self-requested a review January 14, 2021 00:16
Copy link
Contributor

@dehru dehru left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

None of my comments are change requests. Just possible enhancements and/or thinking out loud. Address any you see fit.

We could even merge this so that I can attempt to beautify the message while you address any of the comments.

@dehru dehru merged commit 8602b7d into feature/where Jan 15, 2021
@dehru dehru deleted the jg/typeAwareWhere branch January 15, 2021 23:02
dehru added a commit that referenced this pull request Feb 2, 2021
* Jhork: Support for Simple WHERE Expressions (#101)
* move rule to css common (#108)
* Preventing webview re-rendering when soql statement has not changed (#105)
* Revert "Disable WHERE in model (#113)"
* Type-aware where (#109)
* Recognize recoverable where errors (#116)
* Operator and Criteria Error Tooltips (#122)
* Support IN, NOT IN, INCLUDES, EXCLUDE operators (#123)
jgellin-sf added a commit that referenced this pull request Feb 3, 2021
* Utility to return field type from SObject metadata
* type-aware input validation
* boolean input initialization
* integer and float input validation
* Validation for picklists and dates
* validation of currency literals
dehru added a commit that referenced this pull request Feb 5, 2021
* Jhork: Support for Simple WHERE Expressions (#101)
* move rule to css common (#108)
* Preventing webview re-rendering when soql statement has not changed (#105)
* Revert "Disable WHERE in model (#113)"
* Type-aware where (#109)
* Fix preview not updating when autosave is off - issue 120 (#125)
* Recognize recoverable where errors (#116)
* Operator and Criteria Error Tooltips (#122)
* Support IN, NOT IN, INCLUDES, EXCLUDE operators (#123)
* Add smarter completin on GROUP BY clause and WHERE literals (#126)
* Feature/custom select (#148)
* Jhork/custom select fields (#111)
* Jhork/custom select from (#130)
* Jhork/custom select order by (#134)
* set the error overlay to be on top of all other form elements (#150)
* prepare to publish 0 .0.30 (#151)
* Feature/where (#154)
* Jhork: Support for Simple WHERE Expressions (#101)
* move rule to css common (#108)
* Preventing webview re-rendering when soql statement has not changed (#105)
* Revert "Disable WHERE in model (#113)"
* Type-aware where (#109)
* Recognize recoverable where errors (#116)
* Operator and Criteria Error Tooltips (#122)
* Support IN, NOT IN, INCLUDES, EXCLUDE operators (#123)
* Revert "Feature/where (#154)" (#157)
* Add support for line comments ( `//` ) at the top of SOQL files (#132)
* Jhork/where custom select (#161)
* Fix: 'undefined' in SOQL string (#162)
* Clear tooltip when object changes, fix empty soql statement error blocker (#165)
* Track to force render on condition change - fix rebase conflict (#166)
* do not send event when user still adding input' (#169)
* Jhork/clear modifier field bug fix (#168)
* handle a option selection event (#178)
* cler tooltip error when X is clicked (#180)
* Jhork/workflow updates (#163)
* Bump versions for release (#181)

Co-authored-by: Jonny Hork <jhork@salesforce.com>
Co-authored-by: dehru <599418+dehru@users.noreply.github.com>
Co-authored-by: Dehru Cromer <dehru.cromer@salesforce.com>
Co-authored-by: Fernando Dobladez <fernandodobladez@salesforce.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants