-
Notifications
You must be signed in to change notification settings - Fork 208
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
user order history component #12
Conversation
@param.path.string('userId') userId: string, | ||
@requestBody() order: Order, | ||
): Promise<Order> { | ||
if (userId !== order.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.
We should check existence of order.UserId
first. Otherwise, we throw if userId
is not in the order
.
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 don't need to as requestBody
validation does that for us. If you make a request with an order
not containing a userId
, the following error is thrown:
{
"error": {
"statusCode": 422,
"name": "UnprocessableEntityError",
"message": "The request body is invalid. See error object `details` property for more info.",
"details": [
{
"path": "",
"code": "required",
"message": "should have required property 'userId'",
"info": {
"missingProperty": "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.
Ideally, we should allow the order
to not have userId
as the purpose of relational method is to enforce the referential integrity. But that can be out of scope for this PR.
}, | ||
}, | ||
}) | ||
async create( |
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.
For a controller, is createOrder
a better name?
}, | ||
}, | ||
}) | ||
async patch( |
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 would like to see a name meaningful for business logic, such as updateOrders
.
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.
update
does not make it clear whether it's a full replace or a partial patch. Let's use patchOrders
please.
}) | ||
async find( | ||
@param.path.string('userId') userId: string, | ||
@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.
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 update once it lands.
src/models/shopping-cart.model.ts
Outdated
@@ -25,6 +25,10 @@ export class ShoppingCartItem { | |||
*/ | |||
@property() | |||
price?: number; | |||
|
|||
constructor(item: Partial<ShoppingCartItem>) { | |||
Object.assign(this, item); |
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 super(item);
better?
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 is not a derived class, there is no super.
Travis is failing with the following error:
I think we need to fix juggler to support class as a model function. |
|
The juggler issue is fixed. Please pick up [email protected] |
Please sign off the commits. |
src/datasources/order.datasource.ts
Outdated
import {juggler, AnyObject} from '@loopback/repository'; | ||
const config = require('./mongo.datasource.json'); | ||
|
||
export class OrderDataSource extends juggler.DataSource { |
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 there any particular reason for having different datasource classes for Customer and Order models? Typically, a single datasource should be shared by all models persisted by the same database.
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, the reason was to use the same connection information while setting a different database name in the Class for each model.
That said, we default the database name based on the model so I've updated the PR to just have a single DataSource shared by all models.
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, the reason was to use the same connection information while setting a different database name in the Class for each model.
I think you may be confusing database and table name?
In SQL world, a single database server can provide multiple different databases as a kind of a multi-tenancy. Each database can have multiple tables. In LoopBack, we map models to tables.
From my experience, a single application typically stores data in a single database. This make it easy to leverage SQL features like the JOIN statement. I am not sure is there are any SQL databases that would support JOIN statements across different databases.
92a0cf5
to
6a6f99b
Compare
@@ -58,7 +58,7 @@ describe('UserOrderController acceptance tests', () => { | |||
|
|||
expect(res.body.orderId).to.be.a.String(); | |||
delete res.body.orderId; | |||
expect(res.body).to.deepEqual(order); | |||
expect(res.body).to.deepEqual(JSON.parse(JSON.stringify(order))); |
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.
order.toJSON()
should do the trick, isn't 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.
IIRC, order.toJSON()
returns an object that includes properties with undefined values.
// our expectation
{
title: 'My Todo',
isCompleted: undefined
}
On the other hand, the actual JSON string send by the server and parsed by the client in the test leaves out properties set to undefined
.
// the actual value
{
title: 'My Todo'
}
AFAIK, the deep equality check distinguishes between a property set to undefined
and a property that does not exist on the object at all.
As a result, the expected object obtained via .toJSON()
is rejected as being different from the actual value parsed from a JSON string.
I have encountered this issue myself in loopbackio/loopback-next#1679, I am going to add a new testlab
helper to deal with that. See https://github.com/strongloop/loopback-next/blob/b4bba3d09d663b2919bd6ca3c5a5430d70e1d8c7/packages/testlab/src/misc.ts
Based on this discussion, I am going to open a new pull request to add this helper only, so that we can start using it sooner.
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.
Based on this discussion, I am going to open a new pull request to add this helper only, so that we can start using it sooner.
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.
See also loopbackio/loopback-next#1734
@@ -183,11 +183,11 @@ describe('UserOrderController acceptance tests', () => { | |||
userId: '', | |||
total: 99.99, | |||
products: [ | |||
{ | |||
new ShoppingCartItem({ |
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 do we have to use ShoppingCartItem
class?
it('creates an order for a user with a given orderId', async () => { | ||
const user = await givenAUser(); | ||
const userId = user.id.toString(); | ||
const order = { |
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.
Have you considered using a test-data builder here and specify only few property values that are relevant for this test?
Acceptance tests are verifying that all application parts work together, they should not be verifying behavior of individual components (we have unit and integration tests for that).
In this particular case, we should rely on integration tests to verify that all properties provided to orderRepo.create
are correctly stored in the database and retrieved back. IMO, we should use const order = givenAnOrder()
here, because the actual order details are not important for this test.
const user = await givenAUser(); | ||
const userId = user.id.toString(); | ||
const order = givenAOrder(); | ||
order.userId = 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.
A test-data builder typically accepts a Partial object allowing callers to customize/overwrite default values provided by the builder.
const order = givenAnOrder({userId});
.expect(400); | ||
}); | ||
|
||
it.skip('throws an error when creating an order for a non-existent user', async () => {}); |
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.
Please create a follow-up issue (if you haven't do so already) and add a code comment pointing to that issue. (Explain that this test is skipped because the linked issue is not implemented yet.)
Also note that the test-function is optional for both it
and it.skip
. This line can be simplified as follows:
it.skip('throws an error when creating an order for a non-existent user');
expect(getOrder2).to.deepEqual([savedOrder2]); | ||
}); | ||
|
||
it.skip('patches orders matching filter for a given user', async () => {}); |
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.
Please create a follow-up issue (if you haven't do so already) and add a code comment pointing to that issue. (Explain that this test is skipped because the linked issue is not implemented yet.)
expect(getOrder2).to.deepEqual([]); | ||
}); | ||
|
||
it.skip('deletes orders matching filter for a given user', () => {}); |
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 looks like a duplicated test case?
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 first test is to test that all orders are deleted while this one would test deleting only 1 out of the 2 orders by passing an appropriate filter.
|
||
async function givenAUser() { | ||
const user = { | ||
email: '[email protected]', |
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.
Please use @example.com
address. See https://loopback.io/doc/en/contrib/style-guide.html#email-examples
// MongoDB returns an id object we need to convert to string | ||
// since the REST API returns a string for the id property. | ||
newUser.id = newUser.id.toString(); | ||
|
||
await client.get(`/users/${newUser.id}`).expect(200, newUser.toJSON()); | ||
}); | ||
|
||
function givenAnApplication() { | ||
async function setupApplication() { |
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.
setupApplication
is duplicated in acceptance tests. Can we extract a shared helper please?
I understand setupApplication
is updating global variables. Can we leverage object destructuring as an easy-to-use solution?
// the helper
async function setupApplication() {
// ...
return {app, server, client};
}
// usage in tests
let app: Application;
let server: RestServer;
let client: supertest.SuperTest<supertest.Test>;
before(async () => {
// notice no "const", "let" or "var"
{app, server, client} = await setupApplication();
});
Also since the function is not setting only the app, but also the server and the client, I think we should use a different name, e.g. setupAcceptanceWorkbench
.
}, | ||
}, | ||
}) | ||
async createOrders( |
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 should be createOrder
as the method only accepts one order.
We have discussed this in the past, when implementing "hasMany" relation. Updating a single model via relation endpoint does not make much sense. Once you know the id of the target model ( The endpoint
If the issue has been created, could you please cross-link that issue with this pull request |
|
||
// TODO(virkt25): Implement after issue below is fixed. | ||
// https://github.com/strongloop/loopback-next/issues/100 | ||
it.skip('patches orders matching filter for a given user'); |
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.
FWIW, this can be done now - see loopbackio/loopback-next#1679.
I think it's best to land this PR as-is and leave the fix for a follow-up pull request.
test/acceptance/helper.ts
Outdated
export async function setupApplication(): Promise<setupApp> { | ||
const app = new ShoppingApplication({ | ||
rest: { | ||
port: 0, |
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.
Please use givenHttpServerConfig
, see loopbackio/loopback-next#1738.
const app = new ShoppingApplication {
rest: givenHttpServerConfig(),
});
.travis.yml
Outdated
# Add an IPv6 config - see the corresponding Travis issue | ||
# https://github.com/travis-ci/travis-ci/issues/8361 | ||
- if [ "${TRAVIS_OS_NAME}" == "linux" ]; then | ||
sudo sh -c 'echo 0 > /proc/sys/net/ipv6/conf/all/disable_ipv6'; |
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 is not needed if you use givenHttpServerConfig
, see my other comment.
test/acceptance/helper.ts
Outdated
|
||
const client = createRestAppClient(app); | ||
|
||
return {app: app, client: client}; |
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: This can be simplified as follows:
return {app, client};
a79d375
to
3264a7d
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.
Two more tiny comments to consider. No further review is necessary as far as I am concerned.
|
||
describe('ShoppingCartController', () => { | ||
let app: ShoppingApplication; | ||
let server: RestServer; | ||
let client: supertest.SuperTest<supertest.Test>; |
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.
Please use Client
instead of supertest.SuperTest<supertest.Test>
.
|
||
describe('UserOrderController acceptance tests', () => { | ||
let app: ShoppingApplication; | ||
let client: supertest.SuperTest<supertest.Test>; |
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, use Client
instead of supertest.SuperTest<supertest.Test>
.
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 fixed that with #16
Signed-off-by: virkt25 <[email protected]>
Signed-off-by: virkt25 <[email protected]>
Signed-off-by: virkt25 <[email protected]>
Signed-off-by: virkt25 <[email protected]>
119b9e9
to
f11cddb
Compare
Signed-off-by: Raymond Feng <[email protected]>
So I ran into an issue with MongoDB coercing the userId into an Object for the
orders
so I made changes locally to the connector to take in an option to disable it for each method that I've used. Will make a PR against the connector to fix that.Also not sure how to pass in a filter right now for testing.
Also ... the API expects
userId
to be set on an order but the repository throws an error saying it can't be changes even though it's the same. I think the constraint check should be updated to not throw in this case.fixes loopbackio/loopback-next#1483
blocked by loopbackio/loopback-connector-mongodb#467