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

Updating a `oneOf` resource without an ID throws exception

ericchernuka opened this issue · comments

As part of a conversation around updating related models in #167 it was discovered that
performing an update such as the example below causes an invariant violation with the error message of Invariant Violation: Failed to define a relational property "draftSegmentRevision" on "segment": referenced entity "undefined" ("id") does not exist.

db.segment.update({
  // No need to query the segment, you can provide the predicate
  // (the segment's "id") to the "update" call directly.
  where: { id: { equals: id } },
  data: {
    updatedAt: now,
    // You can update the relational property from the parent.
    draftSegmentRevision: {
      updatedAt: now,
    }
  }
})

I will note that TypeScript did complain, but upon running the code it resulted the exception.

Thanks for opening this one, @ericchernuka.

Based on our one-to-one integration test suite I can say that we probably never implemented the support to update relational properties from the parent. That's a pity, really, as it does sound like something our API should support.

I will try looking into this whenever I've got time. If you have the desire to help me, I'd be thankful for your involvement.

Here's the part of the update code that handles updating relationships:

if (propertyDefinition instanceof Relation) {

You can update the entire relationship to the next value, but not partially (update a subset of properties in the existing relationship).

At the moment, updating a relational property means re-defining it. That's why when the library attempts to re-define the segment revision relationship on the segment it throws the error you've mentioned: the subset of properties you provide in the data when updating is not sufficient to create a whole segment revision entity.

How I'd approach it:

  1. Add integration tests to one-to-one.test.ts.
  2. Add the check when handling the relationships whether the next data value contains the entity[ENTITY_TYPE] property. If it does, that means you're setting the next value of the relationship to the entire entity (which is how it works right now). If it doesn't, however, we'd have to update the existing entity referenced in the relationship with the next subset of properties.

Let me see if I can get some time to help out with this.

What I've come to realize is that you can collocate the two updates using the value getter on the updated properties.

db.segment.update({
  where: { id: { equals: id } },
  data: {
    updatedAt: now,
    // You can update properties using a getter function.
    // The getter function is provided with the current value of the field
    // and the current value of the parent entity (segment).
    draftSegmentRevision(prevDraftSegmentRevision, segment) {
      // Perform the full update operation on the segment revision.
      // Return the result of the update as the next value
      // of the "draftSegmentRevision" property.
      return db.segmentRevision.update({
        where: { id: { equals: segment.draftSegmentRevision.id } },
        data: {
          updatedAt: now
        }
      })
    }
  }
})

I think I like this explicit update on the draftSegmentRevision more than specifying a subset of the next properties and relying on the library figuring it out internally. Such internal handling would certainly result in a more concise call, but it'd take away what's actually happening from the end-developer. Also, it won't work for manyOf relationships, as then a single parent entity may reference a list of many entities through which you cannot update. You'd have to use the value getter pattern above, and, perhaps, it makes more sense to ask developers to always use it. That way their expectations towards collocated updates stay the same (the fewer ways there's to do a single thing, the more natural that thing is).

Another benefit of using the full nested update operation is that it can be abstracted, need be. Then you'd be able to update "segmentRevision" both directly and from within a parent update using the same abstraction.

function syncRevision(...options) {
  return db.segmentRevision.update(...options)
}

// Direct update
syncRevision(toNextState)

// Nested update
db.segment.update({
  where: { ... },
  data: {
    draftSegmentRevision: syncRevision(toAnotherState)
  }
})

This kind of predictability is, usually, a sign of a good API. This reads as: the same operation (updating a revision) should be performed the same way, regardless of its context (direct/nested).

@ericchernuka, what do you think about this? How would this work with your use case?

I like it. Makes sense and is transparent. I saw the docs were updated with this the value getter method. I attempted it this morning, but ran into a few issues:

  • Upon executing it it throws a Invariant Violation: Failed to update relational property "draftSegmentRevision" on "segment": the next value must be an entity, a list of entities, or null if relation is nullable
  • TS doesn't seem to like the return type.
Type '(prevRevision: any, segment: any) => Entity<{ dataTargetDefinition: { id: PrimaryKey<string>; name: () => string; dataType: () => DataTargetDefinitionType; ... 4 more ...; category: () => string; }; ingressWorkload: { ...; }; organization: { ...; }; segment: { ...; }; segmentRevision: { ...; }; }, "segmentRevision">...' is not assignable to type 'Value<{ id: PrimaryKey<string>; rules: { type: () => SegmentRuleTypes; condition: () => SegmentConditions; rules: ArrayConstructor; }; updatedAt: () => Date; }, { ...; }> | Partial<...> | undefined'.
  Type '(prevRevision: any, segment: any) => Entity<{ dataTargetDefinition: { id: PrimaryKey<string>; name: () => string; dataType: () => DataTargetDefinitionType; ... 4 more ...; category: () => string; }; ingressWorkload: { ...; }; organization: { ...; }; segment: { ...; }; segmentRevision: { ...; }; }, "segmentRevision">...' has no properties in common with type 'Partial<{ kind?: ((prevValue: any, entity: Value<OneOf<"segmentRevision", false>, { id: PrimaryKey<string>; organization: OneOf<"organization", false>; name: () => string; updatedAt: () => Date; draftSegmentRevision: OneOf<...>; publishedSegmentRevision: OneOf<...>; }>) => any) | undefined; ... 4 more ...; resolveWi...'.

Definitely getting close. This API looks great. I'll see if I can get some time today to investigate.

I will find some time to improve the tests for relationships. I think we miss much by not clearly defining certain things we've discovered along the way. I'd like for the tests to be more granular and represent all the use cases I think the library should cover (but doesn't, as we gradually find out).

I already have the proposed collocated update API working locally but I need to make sure the tests are in order before merging it. Thanks for the patience.