-
Notifications
You must be signed in to change notification settings - Fork 1.1k
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
REST API to find models using a filter #1679
Conversation
Regarding API Explorer/swagger-ui - I am not able to send a request with a |
import {JsonSchema} from '../../src'; | ||
|
||
describe('getFilterJsonSchemaFor', () => { | ||
@model() |
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.
Can we define models at the bottom of the test file?
it('describes "fields" as an object', () => { | ||
const filter = {fields: 'invalid-fields'}; | ||
ajv.validate(customerFilterSchema, filter); | ||
expect(ajv.errors || []).to.containDeep([ |
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'm curious; why are we doing ajv.errors || []
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.
When ajv.validate
returns true
, ajv.errors
is set to null
. As a result, shouldjs
produced error message that seemed to me as more difficult to understand.
I am open to suggestions on how to improve this assertion and/or make the code easier to understand!
What about stringified json? For qs style such as |
I did some more investigation. It seems that we need to upgrade our api-explorer with the latest swagger-ui version to bring in deepObject fix from swagger-api/swagger-js#1235. I opened a pull request for that - see loopbackio/loopback.io#736 We should also set With those changes in place, I am able to send a request like Unfortunately, when I provide a value with nested properties, e.g. Based on the following discussions, I think it may be up to us to contribute serialization of deeply nested objects:
swagger-api/swagger-js#1140 (comment)
Related issues: swagger-api/swagger-ui#4216 and swagger-api/swagger-ui#4064
Unfortunately not. Because the OAS says When I tried to enter a string containing the JSON, e.g. |
I left a comment in a swagger-ui issue to discuss this problem further, see swagger-api/swagger-ui#4064 (comment) @raymondfeng @strongloop/sq-lb-apex what's your opinion on the next steps in this pull request? Should I continue adding Here is my concern with landing these changes: users trying out LoopBack 4 and the API Explorer will quickly discover that they cannot send Thoughts? UPDATE 2018-09-13 I opened a new issue in swagger-js with the hope that it will get more attention. See swagger-api/swagger-js#1385 |
a9779c2
to
bc9e2f6
Compare
It will definitely be an annoyance of not being able to test a |
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! 🚢
Wait I open the PR a few days ago and didn't see the new discussion regarding swagger-ui.
The code change in this PR LGTM but if it causes the explorer ignoring the filter then let's fix the swagger-ui
first.
// TODO(bajtos) restrict values to relations defined by "model" | ||
relation: {type: 'string'}, | ||
// TODO(bajtos) describe the filter for the relation target model | ||
scope: scopeFilter, |
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.
Not involved in this PR:
I think we can use a $ref
here and append the related model's filter schema in spec.components.schemas
when work on describing the filter for the related model.
// result returned by REST API does not. | ||
// Use this function to convert a model instance into a data object | ||
// as returned by REST API | ||
function deserializedFromJson<T extends object>(value: T): T { |
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: not a necessary part of this PR, we can update this test to call deserializedFromJson
as well :)
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! 🚢
Wait I open the PR a few days ago and didn't see the new discussion regarding swagger-ui.
The code change in this PR LGTM but if it causes the explorer ignoring the filter then let's fix the swagger-ui
first.
Maybe we should start with what LB3 allows - when the openapi spec is generated, we mark |
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.
Excellent PR! Thinking more about my previous comment, I think it's probably better to not wait for swagger-ui
to be fixed and this PR should be landed as most users creating apps will not be consuming the APIs using the explorer (its great for testing) but not real life usage. For real life usage I think it's best to land the ability to filter a Model ASAP. :)
|
||
// TODO(bajtos) Rework this condition to check whether "model has any | ||
// relations defined | ||
if (modelCtor !== EmptyModel) { |
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.
When do we expect a modelCtor
to be an EmptyModel
. Not sure I follow this logic.
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.
When a model has relations configured, the filter can provide include
property to describe which related models to include in the result. Think about object-tree traversal in GraphQL.
So for example, when a Customer
has many Order
instances, and Order
instance belongs to a Customer
, it's possible to craft the following Customer query:
{
where: {
email: 'mbajtoss@gmail.com'
},
include: [{
relation: 'orders',
scope: {
// a full Filter for Order model
where: {totalPrice: {gte: 100}},
// we can do recursion too!
include: [{
relation: 'customer',
}],
},
}
Ideally, we would like to describe include[].filter
property as a Filter
schema tailored to the properties and relations available at the target model of the relation. I decided to leave that out of the initial implementation and describe filter
as a Filter object for a model with no pre-defined properties but allowing any extra properties:
const scopeFilter = getFilterJsonSchemaFor(EmptyObject);
However, this creates a recursion - in order to build schema for scopeFilter
, I need to describe include[].filter
property as scopeFilter
too.
The check modelCtor !== EmptyModel
is just a quick way how to detect this situation, avoid infinite recursion and also leave out the property include
from the nested include[].filter
object (for better or worse).
My takeaway is that I should fix this condition to check the actual relations defined on the model, and do that before this pull request is landed.
b4bba3d
to
c4b08d7
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.
The patch is ready for final review and landing.
packages/testlab/src/index.ts
Outdated
@@ -12,3 +12,4 @@ export * from './test-sandbox'; | |||
export * from './skip-travis'; | |||
export * from './request'; | |||
export * from './http-server-config'; | |||
export * from './misc'; |
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.
packages/testlab/src/misc.ts
Outdated
* Use this function to convert a model instance into a data object | ||
* as returned by REST API | ||
*/ | ||
export function deserializedFromJson<T extends object>(value: T): T { |
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 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.
Looks great! Excited to see functional filter
support. Left some comments but overall looks good.
@@ -68,10 +68,10 @@ export class <%= className %>Controller { | |||
}, | |||
}) | |||
async updateAll( | |||
@requestBody() <%= name %>: <%= modelName %>, | |||
@requestBody() data: <%= modelName %>, |
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.
Not a big deal but I personally prefer not using a generic name like data
since it would repeat multiple times and as such would much rather have it be the model name ... todo: Todo
.
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.
Makes sense, will go with todo: Todo
.
Before my change, we were using the controller name as the argument name, e.g. TodoController: Todo
.
properties: { | ||
where: { | ||
type: 'object', | ||
// TODO(bajtos) enumberate "model" properties and operators like "and" |
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.
WIll this be a part of this 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.
docs/site/Controller-generator.md
Outdated
content: {'application/json': {'x-ts-type': Number}}, | ||
}, | ||
}, | ||
}) | ||
async count(@param.query.string('where') where?: Where): Promise<number> { |
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.
Shouldn't this be changed to @param.query.object('where')
?
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!
}, | ||
}, | ||
}) | ||
async create(@requestBody() obj: TodoList): Promise<TodoList> { |
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.
obj
=> data
as per the new CLI template.
async find( | ||
@param.path.number('id') id: number, | ||
@param.query.string('filter') filter?: Filter, |
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 should be @param.query.object
|
||
fields: { | ||
type: 'object', | ||
// TODO(bajtos) enumberate "model" properties |
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.
Will this be a part of this 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.
No, see #1748
docs/site/Controller-generator.md
Outdated
async updateAll( | ||
@requestBody() obj: Todo, | ||
@requestBody() data: Todo, | ||
@param.query.string('where') where?: Where, |
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.
shouldn't where
an object instead of string?
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.
shouldn't
where
an object instead of string?
Good point, will fix.
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.
@bajtos Great effort!
I left a few nitpick for updating the doc.
And one design question regarding the relation metadata (don't think it's this in PR's scope to address it, we can discuss in a new issue if you also think my concern is valid, thanks!)
docs/site/Controller-generator.md
Outdated
content: {'application/json': {'x-ts-type': Number}}, | ||
}, | ||
}, | ||
}) | ||
async count(@param.query.string('where') where?: Where): Promise<number> { |
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.
same here @param.query.object('where')
}, | ||
}, | ||
}) | ||
async count(@param.query.string('where') where?: Where): Promise<number> { |
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.
@param.query.object('where')
const relations = getModelRelations(Customer); | ||
expect(relations).to.deepEqual({ | ||
accessTokens: { | ||
keyTo: 'userId', |
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.
wouldn't it be customerId
?
Otherwise when we have multiple models extend User
, they will share the same fk which is userId
...
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.
No, it's userId
because Customer inherited the relation the User class.
In LB 3.x, we call this a "polymorphic relation" and add another property to accessToken
to distinguish between different targets that can have the same userId
. For example, accessToken
can have two properties: userId
for the id (PK/FK) value, userModel
for a name of the target model class (User, Customer, etc.)
The purpose of the test is just to verify that the new function returns all relations - defined on the model and inherited from parents.
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 call this a "polymorphic relation"
oh ok I got it 👍 it's polymorphic.
5ce9332
to
a4b7198
Compare
@jannyHou @virkt25 thank you for a thorough review, you spotted important places to fix! In order to support Then I used this new helper in all controller-related code and docs, plus addressed your other comments too. Could you please take another look and let me know if the changes LGTY now? |
I have also created two follow-up stories (post 4.0 GA):
|
* | ||
* @param modelCtor The model constructor to build the filter schema for. | ||
*/ | ||
export function getWhereSchemaFor(modelCtor: typeof Model): SchemaObject { |
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.
Since we include getWhereSchemaFor
in this file, would it rename this file to get-partial-schema
? or something along those lines?
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 file contains functions to build schema of Filter
and Where
objects. I feel get-partial-schema
is misleading, because both Filter
and Where
are much more than just an object allowing a subset of model properties. For example, Where
can contain conditions like and
and custom operators like neq
, gte
, etc.
As I see it, both Filter
and Where
are related to querying/filtering model instances. Where
describes condition(s) to apply, Filter
provides additional query instructions like pagination and which fields & relations to fetch.
That's why I think filter-schema
is a good file name.
* | ||
* @param modelCtor The model constructor to build the filter schema for. | ||
*/ | ||
export function getWhereJsonSchemaFor(modelCtor: typeof Model): JsonSchema { |
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: Should we consider renaming the file?
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. A non-blocker comment on file names.
Implement new APIs for building JSON and OpenAPI schemas describing the "filter" and "where" objects used to query or modify model instances.
Modify the CLI template and example controllers to accept a new filter argument, leveraging the recently added `@param.query.object()` and Filter schema. Rename the controller-method argument accepting model data from `${controllerName}` to `data` (e.g. `data` instead of `TodoController`). I think this was an oversight in the implementation of the REST Controller template. Update the code snippets in documentation that are showing controller classes to match the actual code used in the examples and produced by our `lb4 controller` command.
a4b7198
to
6e9d2a9
Compare
It would be of great help if anyone here can answer this question for me. Thanks a lot |
This pull request leverages the recently added
@param.query.object()
decorator (see #1667) to add support for querying a subset of models using a Filter object.For example:
The OpenAPI spec contains a reasonably-minimal description of the Filter object.
Done
filter
andwhere
parameterstodo
exampletodo-list
exampleSee also #100
Checklist
npm test
passes on your machinepackages/cli
were updatedexamples/*
were updated