mswjs / data

Data modeling and relation library for testing JavaScript applications.

Home Page:https://npm.im/@mswjs/data

Geek Repo:Geek Repo

Github PK Tool:Github PK Tool

Entity with `manyOf` Relation not Being Updated When Updating Relationship with MSW

JoseRFelix opened this issue · comments

Problem

Whenever I update an entity that is related to another through MSW, the other entity is not updated. This only happens when done through MSW. If I were to create an entity and update it imperatively (like inside a function) it would work as expected.

Reproduction steps:

  1. Create an entity outside of MSW with db.create and link with another entity in a manyOf relationship.
  2. In an MSW route, update the relationship.
  3. Query the first entity with db.findFirst.

Reproduction Sandbox:

https://codesandbox.io/s/manyof-repro-soic5?file=/src/msw.ts

Notes:

  • After debugging, the getter in defineRelationalProperties is not being called. Perhaps, the entity has detached.
  • Maybe a Proxy Object could be better than Object.defineProperties

Hey, @JoseRFelix. Thanks for raising this.

I've added some missing tests for this, including a test where you create an entity and a relationship directly in db, then update that entity via a generated handler, and then query that entity via db. All tests pass.

I've spotted a few things you may improve in the sandbox.

  • Request you make sends a plain text instead of JSON. Specify the correct Content-Type header for the request:
const response = await fetch("/notes", {
  body: JSON.stringify(data),
+ headers: {
+   'Content-Type': 'application/json'
+ },
  method: "PATCH"
});

By providing a correct Content-Type, MSW will parse your req.body appropriately, so you don't have to parse it manually here:

-const { customerId, noteId, description } = JSON.parse(req.body as any);
+const { customerId, noteId, description } = req.body

This, however, shouldn't be related to the issue you're experiencing.

Thanks for preparing the sandbox, but could you please elaborate more on where exactly is the problem? It'd help to include some expected/actual data snippets.

@kettanaito Thanks for the feedback!

Thanks for preparing the sandbox, but could you please elaborate more on where exactly is the problem? It'd help to include some expected/actual data snippets.

What I'm trying to do is an update to an existing related entity. For example, a post related to a user. I see in the integration tests you are updating the user itself instead of the post. But similar to a database I want to update the post and the changes to be reflected on the user.

On the PATCH /notes inside the sandbox when I update the note with a new description, it should reflect the changes on the customer. However, on request, the note itself is updated and db.note.getall does return the correct notes, but the db.customer.findFirst still has the outdated note.

I've added some console logs for this:

image

You may notice that the customer notes still are outdated even though the note was updated.

Got it. I've added a test that updates a referenced entity instead of the parent entity:

https://github.com/mswjs/data/pull/93/files#diff-4d24d34f243d2f8e5d460606e73f0a491f47c07f8974ff2a2e482af2ab7bd93aR267-R274

Will analyze your example closer to see what may be off. Thanks for the clarifications.

I've rewritten the regression test now that I better understand the use case. Please see this commit. The test still passes, so I suspect something else is causing your issue.

I highly recommend stripping your example from any unrelated logic:

  • Generation of a unique ID.
  • Creation of entities in the GET /user handler (consider creating outside of the handler's scope).
  • Use plain window.fetch() to eliminate any cache issues.

Could you please try those and let me know? I'd also appreciate any feedback on the test, I wish it to capture the problem precisely.

For some reason Codesanbox doesn't work for me at all in both Brave and Chrome. If you have a minute, it'd be great to push your use case to GitHub as a standalone repository.

Here is the repro repo: https://github.com/JoseRFelix/manyof-repro

I tried all the recommended steps:

  • Generation of a unique ID. Tried with UUID for id and still no luck.
  • Creation of entities in the GET /user handler (consider creating outside of the handler's scope). Tried creating inside and outside the handler and still no luck.
  • Use plain window.fetch() to eliminate any cache issues. Likewise, no luck.

Could you please try those and let me know? I'd also appreciate any feedback on the test, I wish it to capture the problem precisely.

Interestingly, I had another sandbox environment, and when done sequentially, it works as expected. However, when done with imports it breaks.

Ok, after debugging I noticed that Object.getOwnPropertyDescriptors inside createModel logs get correctly.

image

However, when created inside the handler or outside, it's not defined:

image

According to MDN docs about Object.defineProperties,

Bear in mind that these attributes are not necessarily the descriptor's own properties. Inherited properties will be considered as well. In order to ensure these defaults are preserved, you might freeze the Object upfront, specify all options explicitly, or point to null with Object.create(null).

Maybe there is an update in the object itself?

I think you're right, I notice this too: the relational property stops being a getter and becomes a regular value once it's updated. What I'm still unsure about is why the tests still work properly, even when the relational property is no longer a getter.

What's interesting, if you see that relational property becoming a plain value, this means the library updates the parent entity somewhere along the way. As you've expressed above, that's not an intention. The intention is to update a note and see that the customer has the updated list of notes.

Such shift from the getter to a plain value is, actually, clear because of this call stack:

  1. [model].update()

db.update(modelName, prevRecord, nextRecord)

  1. Database.update():

this.create(modelName, nextEntity, nextPrimaryKey as string)

Since the next entity you pass doesn't have relational properties (you provide exact data as the next entity), the updated entity will be set as-is, ignoring any relational getters it might have had. Perhaps we should treat updates to relational properties somehow differently... Wonder if we can just drop them, because relational values are resolved by reference, so there's is zero need to touch the parent entity at all.

The issue

I think the way we update relational properties is generally not correct. We need to account for two scenarios:

  1. A relational entity is updated without updating the parent entity.
const db = factory({
  user: { note: manyOf('note') },
  note: { title: String }
})

db.user.create({
  notes: [db.note.create({ title: 'a' }), db.note.create({ title: 'b' })]
})

db.note.update({
  where: { title: { equals: 'b' } },
  data: { title: 'updated' }
})

I've already added a test for this, and confirm it functions as expected.

  1. A parent entity is updated, including next relational values, but there are no updates to the relational entities directly.
const db = factory({
  user: { id: String, note: manyOf('note') },
  note: { title: String }
})

db.user.create({
  id: 'user-1',
  notes: [db.note.create({ title: 'a' }), db.note.create({ title: 'b' })]
})

db.user.update({
  where: { id: { equals: 'user-1 } },
  data: { notes: [{ title: 'c' }] }
})

What is the expected result of such user update?

The issue here is that .update() is not meant to implicitly create entities. So while you can provide an exact compatible object as the value of a relational property, it won't be present as a queriable resource in the database (you never created such an entity).

It may come to better documentation in explaining that when you wish to update a relational property of an entity via .update() method, you need to provide actual entities:

db.user.update({
  where: { id: { equals: 'user-1' } },
  data: { notes: [db.note.create({...})] }
})

This, however, doesn't solve the issue that even with this update above the user.notes will stop being a relational property, and will turn from a getter (that resolves value by reference) to an exact plain value. I think in this case the library should ignore re-assigning the value of user.notes, and instead update its relation node that keeps pointers to the referenced entities in a getter.

This, however, doesn't solve the issue that even with this update above the user.notes will stop being a relational property, and will turn from a getter (that resolves value by reference) to an exact plain value. I think in this case the library should ignore re-assigning the value of user.notes, and instead update its relation node that keeps pointers to the referenced entities in a getter.

I concur with this. We could use Object.getOwnPropertyDescriptor and update the ref. Maybe creating a common method could help here, that way model creation and update could share assignment logic for relations.

Maybe creating a common method could help here, that way model creation and update could share assignment logic for relations.

Yes, that'd make sense. I'll try to design the logic behind this so it's straightforward and can be reused internally. This has been a great discussion so far, thank you for raising this!

commented

Wow, the fix fo now is to downgrade all the way to version 0.1.1 - the first release. I hope that can be fixed soon.

@maciejmyslinski downgrading does not equal fixing, you're just reverting to the library version where there was no support for relational properties, I presume.

Anybody is welcome to tackle this, I'll help with the code review. Otherwise, it will be fixed when the maintainers got time.

commented

I know 😄

I'm looking for a workaround until the fix is available.