-
Notifications
You must be signed in to change notification settings - Fork 1.1k
Feature/array in fields #6517
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
Feature/array in fields #6517
Conversation
bajtos
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.
Great improvements, thank you @mdbetancourt for the pull request ❤️
You are generally on the right track, I have few comments to discuss and address.
Also, can you please add an end-to-end test making an HTTP call to verify all parts work together as expected? The goal is to specify fields as an array in the request query string, and verify that it was accepted by the validation code and correctly applied by the connector.
IIRC, we don't have a dedicated place for testing REST+Repository interaction, we use the Todo example app instead. I think it's ok to add new test(s) to examples/todo/src/__tests__/acceptance/todo.acceptance.ts
| expect(filter).to.eql({ | ||
| fields: { | ||
| a: true, | ||
| b: true, |
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.
Is it necessary to convert fields from an array to an object? I believe juggler and connectors should support both object and array format, so I don't think we need to implement this conversion in FilterBuilder.
We have test cases to verify that all connectors support both formats, see loopback-datasource-juggler/test/basic-querying.test.js. The base SQL connector seems to support both array and object format, see e.g. here and so does our MongoDB connector (see here).
I believe that internally, juggler will convert fields from an object into an array anyways, see ModelUtils._normalize()
My conclusion is that FilterBuilder should keep not modify filter.fields value and keep it as an object or as an array, depending on what was provided by the user.
@raymondfeng @jannyHou @hacksparrow what do you think?
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.
hi @bajtos thank you for you feedback, i saw some disadvantage about that:
- you need always to check what type is (array or object)
- check if exists in object is faster than array (in performance O(n) vs O(1))
- fewer keystroke to check a value
fields.passwordvsfields.indexOf('password') >= 0vsfields.includes('password')
also you say
My conclusion is that FilterBuilder should keep not modify filter.fields value and keep it as an object or as an array, depending on what was provided by the user.
but FilterBuilder allow do
builder.fields('property').fields(['property2']).fields({ property3: true }).build()what is filter.fields?
another problem is
builder = builder.fields('property') // adding property to fields
// ..some code
builder.fields({property: false}) // i remove itsome edges case like this need to be cover (so fields function become more complex) if we keep the array (we need check if exists and remove it), instead with object that's not a problem because we merge the object
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.
Thank you for a detailed explanation! Based on your arguments, I am fine to keep representing fields as an object internally inside FilterBuilder. (Unless other maintainers disagree.)
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.
+1 appreciate all the considerations. Agree with do the conversion here.
| * included, or an Fields object for the inclusion/exclusion | ||
| */ | ||
| fields(...f: (Fields<MT> | (keyof MT)[] | keyof MT)[]): this { | ||
| fields(...f: (Fields<MT> | Extract<keyof MT, string>)[]): this { |
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.
Do we still need to specify all forms? I think Extract<...> is already covered by Fields<> type now, isn't it? Would the following work?
| fields(...f: (Fields<MT> | Extract<keyof MT, string>)[]): this { | |
| fields(...f: (Fields<MT>)[]): this { |
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.
that cover calling it with
builder.fields('property', 'property2')while Fields<MT> only cover
builder.fields(['property', 'property2'])
// and
builder.fields({property: true, property2: true})so i kept it in order to no introduce a breaking change
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 see, I did not realize Extract<keyof MT, string> is describing a single item (field name) only, not an array.
+1 to preserve backwards compatibility 👍🏻
| this.filter.fields = this.filter.fields.reduce( | ||
| (prev, current) => ({...prev, [current]: true}), | ||
| {}, |
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.
As I mentioned above, I think we should keep fields as an array, no reducing should be needed here.
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 did it to keep some consistency to make more easy the following code (instead of check if fields is an array or an object) also i used object aproach instead of array because object is more versatile. how could be converted the following object into an array?:
{ password: false }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 point 👍🏻 As I wrote in #6517 (comment), you convinced me it's better to store fields as an object internally.
| const objTitle = | ||
| options.setTitle !== false | ||
| ? { | ||
| title: `${modelCtor.modelName}.Fields`, | ||
| } | ||
| : {}; |
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 find this bit rather complex, how about the following?
| const objTitle = | |
| options.setTitle !== false | |
| ? { | |
| title: `${modelCtor.modelName}.Fields`, | |
| } | |
| : {}; | |
| const objTitle = | |
| options.setTitle !== false | |
| ? `${modelCtor.modelName}.Fields` | |
| : undefined; |
And later:
return {
title: objTitle,
oneOf
};It slightly changes the return value - the current version returns object with no title property while the new version will return title set to undefined, I don't know if that is a problem.
Alternatively, may we can keep the current code as is to avoid unnecessary code churn?
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 this could break some test because that approach remove from the object if already existed a title
{title: 'hello', ...getFieldSchema({}, {setTitle: false})}
// returns
{title: undefined}
//instead of
{title: 'hello'}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.
Interesting. Was the original version triggering this problem too? Do we have a test case to ensure that {title: 'hello', ...getFieldSchema({}, {setTitle: false})} works as intended?
Anyhow, if your proposed version works, then I am ok with it, but please do consider to add a test to prevent unintended regression in the future (if we don't have one already).
bajtos
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.
Thank you for the updates and explanations 🙇🏻 The new version looks much better! I found few minor details to improve, see my comments below.
@raymondfeng @jannyHou @hacksparrow Ping, have you had a chance to look at the proposed changes?
| beforeEach(() => { | ||
| ajv = new Ajv(); | ||
| customerFilterSchema = getFilterJsonSchemaFor(Customer); | ||
| dynamiCustomerFilterSchema = getFilterJsonSchemaFor(DynamicCustomer); |
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.
Typo?
| dynamiCustomerFilterSchema = getFilterJsonSchemaFor(DynamicCustomer); | |
| dynamicCustomerFilterSchema = getFilterJsonSchemaFor(DynamicCustomer); |
| ]); | ||
| }); | ||
|
|
||
| it('non-strict model should allow any value in "fields"', () => { |
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.
We write test names to read like an English sentence, e.g. it disallows "include" for the test above.
Can you please improve names of the new test to follow that same style too?
For example:
| it('non-strict model should allow any value in "fields"', () => { | |
| it('allows free-form properties in "fields" for non-strict models', () => { |
| expect(ajv.errors ?? []).to.be.empty(); | ||
| }); | ||
|
|
||
| it('strict model should only allow defined values in "fields"', () => { |
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.
Ditto, e.g.
| it('strict model should only allow defined values in "fields"', () => { | |
| it('allows only defined properties in "fields" for strict models', () => { |
| ]); | ||
| }); | ||
|
|
||
| it('strict "fields" should not have duplicated 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.
| it('strict "fields" should not have duplicated items', () => { | |
| it('rejects "fields" with duplicated items for strict models', () => { |
| ]); | ||
| }); | ||
|
|
||
| it('non-strict "fields" should not have duplicated 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.
| it('non-strict "fields" should not have duplicated items', () => { | |
| it('rejects "fields" with duplicated items for non-strict models', () => { |
| const schema: JsonSchema = {oneOf: []}; | ||
| if (options.setTitle !== false) { | ||
| schema.title = `${modelCtor.modelName}.Fields`; | ||
| } |
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.
Much cleaner & easier to understand 👏🏻
| query will include **only** those you specifically include with filters. | ||
| specify at least one fields filter with a value of `true` or put it in the | ||
| array, then by default the query will include **only** those you specifically | ||
| include with filters. |
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.
Suggestion for re-structuring the initial sections to this:
A _fields_ filter specifies properties (fields) to include or exclude from the
results.
### Node.js API
#### `fields` as an array
<pre>
{ fields: [propertyName1, propertyName2, ...] }
</pre>
Only the properties specified in the list will be returned.
#### `fields` as an object
<pre>
{ fields: {<i>propertyName</i>: <true|false>, <i>propertyName</i>: <true|false>, ... } }
</pre>
Where:
- _propertyName_ is the name of the property (field) to include or exclude.
- `<true|false>` signifies either `true` or `false` Boolean literal. Use `true`
to include the property or `false` to exclude it from results.
By default, queries return all model properties in results. However, if you
specify at least one fields filter with a value of `true`, then by default the
query will include **only** those you specifically include with filters.
@bajtos and others thoughts?
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 suggested structure looks good to me.
Just note that we support array + object format also for REST API, not just in Node.js (TypeScript).
|
@mdbetancourt thank you for the contribution. It's pretty close to landing, please address the inputs from @bajtos. I have left a doc suggestion for everyone to share their thoughts on. |
| expect(filter).to.eql({ | ||
| fields: { | ||
| a: true, | ||
| b: true, |
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.
+1 appreciate all the considerations. Agree with do the conversion here.
| { | ||
| keyword: 'enum', | ||
| dataPath: '.fields[0]', | ||
| message: 'should be equal to one of the allowed values', |
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.
nitpick:
should be equal to one of the allowed values
Does error msg contain more details of the allowed values?
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.
{
keyword: 'enum',
dataPath: '.fields[0]',
schemaPath: '#/properties/fields/oneOf/1/items/enum',
params: { allowedValues: [ 'id', 'name' ] },
message: 'should be equal to one of the allowed values'
}this is the object
| array, then by default the query will include **only** those you specifically | ||
| include with filters. | ||
|
|
||
| ### REST API |
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.
nitpick: @mdbetancourt could you also update the shortcut for the REST API section? thanks.
bajtos
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.
👏🏻
|
@jannyHou do you have any more comments? Is this pull request good to be merged? We will need to fix the git commit history to pass our commit linter check, I hope that rebasing it on top of the latest master should be good enough. |
Signed-off-by: Michel Betancourt <michelbetancourt23@gmail.com>
jannyHou
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.
👏 LGTM 🚢
|
@mdbetancourt, thanks for your contribution. Your PR has landed! 🎉 |
Closes #5857
Checklist
npm testpasses on your machinepackages/cliwere updatedexamples/*were updatedAcceptance criteria