Skip to content
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

Add support for reverse relations to the ARI engine #147

Merged
merged 10 commits into from
Nov 30, 2016

Conversation

bryankane
Copy link
Contributor

While forward relations are very useful in a majority of cases (i.e. courses -> instructors, where the course model has an instructorId on it), there are some cases where a relation is necessary where the first model doesn't have any reference to the second. For instance, while a Membership model has a CourseId available on it, the Course model has no reference to all of the memberships available.

In order to create this course -> membership relationship though, we can dynamically fetch the relationship via a query to the membership resource, which, given information about the course, can return a list of memberships + ids for the relationship. This is referred to as a "reverse relationship", where the relationship data is stored on the related model.

These relationships are defined in the Naptime resource (via the Fields attribute), like so:

override implicit lazy val val Fields = Fields
  .withReverseRelations(
    "memberships" -> ReverseRelation[MembershipId](
      resourceName = ResourceNames.membershipResourceName,
      arguments = Map(
        "q" -> "meByCourse",
        "courseId" -> "$id",
        "showHidden" -> "true")))

The $id string is treated like string interpolation, and will be replaced with the value of the "id" field on the fetched course.

During execution, the engine will make the API call as described on the relationship (appending all interpolated arguments listed), and will make that response available. It will also create a new dynamic field on the original model (in this case, Course), with the ids of the related resources for easier traversal / interop with forward relations.

PTAL @yifan-coursera -- let me know if you want to go over this one in person.

And if you're still interested, cc @saeta

@saeta
Copy link
Contributor

saeta commented Nov 27, 2016

Wahoo!

Bryan Kane added 6 commits November 27, 2016 18:52
The original engine doesn’t support related resources on unions, nested records, nested lists, or anything that isn’t a top-level record. This change uses the Pegasus dataiterator to iterate through all fields of a datamap and find related resources anywhere on the object.
Refactors things a bit to make testing easier
Copy link
Contributor

@yifan-coursera yifan-coursera left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Not quite done but have some preliminary comments / questions

@@ -13,4 +13,6 @@ record Course {
description: string?

extraData: AnyData

courseMetadata: union[CourseMetadata]
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why union?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

just for testing that relations on unions work. i can remove this field if you want

case inlineFragment: InlineFragment =>
inlineFragment.selections.flatMap(selection => parseSelection(selection, document))
inlineFragment.selections.flatMap { selection =>
println(inlineFragment.typeCondition)
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Remove println.

case fragmentSpread: FragmentSpread =>
(for {
fragment <- document.fragments.get(fragmentSpread.name).toList
selection <- fragment.selections
} yield {
parseSelection(selection, document)
println(fragment.typeConditionOpt)
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Remove println.

@@ -107,3 +154,7 @@ object NaptimePaginatedResourceField {
}

}

case class FieldRelation(
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think this should be a union or sealed trait. Is there a case where a field is both a forward relation and a backward relation at the same time?

s"'$name', but that field is already defined on the model")
case None =>
val errorMessageBuilder = new StringBuilder
val newField = new RecordDataSchema.Field(new ArrayDataSchema(new StringDataSchema)) // TODO(bryan): fix type here
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

What happens with this code if the key is a non-string? Do things still work reasonably?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

i believe it still works, because ids are serialized to strings in naptime output. I need to investigate this more though and try cases with integers and stuff. this is why i put the key type on the reverse relation annotation (FinderReverseRelation[KeyType](...)), but i don't know if that's necessary or if i can always assume the id will be a string.

i'm going to keep this todo in here and investigate when there's more time, but i don't think it'll break anything now.

var dataElement: DataElement = null
while ( {
dataElement = it.next(); dataElement
} != null) {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

More idiomatic to write this as

Iterator
  .continually(it.next)
  .takeWhile(_ != null)
  .filter(_.path.toSeq.map(_.toString) == path.dropRight(1))
  .foreach(_.getValue.asInstanceOf[DataMap].put(path.last, data)

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

oh woah. i hadn't used scala iterators before... that's really nice though :)

data.foreach { element =>
val it = Builder.create(element, schema, IterationOrder.PRE_ORDER).dataIterator()
var dataElement: DataElement = null
while ( {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Do an iterator here too, as above.

@bryankane bryankane merged commit 2b7341a into coursera:master Nov 30, 2016
@bryankane bryankane deleted the reverse-relations branch November 30, 2016 20:31
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

3 participants