Filter an array of references

Using my current schema:

type User {
active: Boolean!
username: String!
description: String
email: String
mobile: String
ownerOf: [Ranking!]! @relation(name:"ownerranking")
memberOf: [Ranking!]!
}

type Player {
active: Boolean!
rank: Int!
ranking: Ranking!
playerInfo: User!
challenger: Player @relation
}

type Ranking {
active: Boolean!
rankingname: String!
rankingdesc: String
owner: User! @relation(name:"ownerranking")
}

type Query {
allRankings: [Ranking] @resolver(name: "all_rankings")
}

I want to retrieve a list of the ranking data a user is a member of by providing the array of ranking references.

I tried filter with 2 existing documents:

Filter(
["290125481092579841", "290125505974239745"],
Lambda(
'i',
Get(Ref(Collection('Ranking'), Var('i'))),
),
)

This gives me:

Error: [
{
"position": [
0
],
"code": "invalid argument",
"description": "Boolean expected, Object provided."
},
{
"position": [
1
],
"code": "invalid argument",
"description": "Boolean expected, Object provided."
}

I attempted to compare this issue with mine, but it appears to be more complex and hence difficult to apply.

  1. Is Filter the correct function to use here?
  2. If not which function should I be using?
  3. How should I be defining the function to retrieve the ranking data?

thanks …

You want Map instead of Filter, I think. Map will allow you to map the IDs to the complete documents whereas Filter would remove entries from a set/array.

If you want all rankings for a user, you could build an index (one probably already exists based on your schema) and then map the result of that index to the documents. The Match would return a set of refs and you map them to Get.

Map(
  Match(Index('ranking_owner'), [userGoesHere]),
  Lambda('ranking', Get(Var('ranking')))
)

FYI, I updated the schema to:

type User {
active: Boolean!
username: String!
description: String
email: String
mobile: String
ownerOf: [Ranking!]! @relation(name:"ownerranking")
memberOf: [Ranking!]! @relation(name: "memberranking")
}

type Player {
active: Boolean!
rank: Int!
ranking: Ranking! 
playerInfo: User!
challenger: Player @relation
}

type Ranking {
active: Boolean!
rankingname: String!
rankingdesc: String
owner: User! @relation(name:"ownerranking")
member: User! @relation(name: "memberranking")
}

type Query {
  allRankings: [Ranking] @resolver(name: "all_rankings")
}

to handle a similar query for member rankings.

However, I appear to be having trouble specifying the User ref correctly. I’ve tried combinations with [ ], () etc. similar to:

Map(
  Match(Index('ownerranking_by_user'), Ref(Collection("User"), "290125575057")),
  Lambda('ranking', Get(Var('ranking')))
)

but I always get:

Error: [
  {
    "position": [
      "collection"
    ],
    "code": "invalid argument",
    "description": "Array or Page expected, Set provided."
  }
]

How should I specify this user ref?
Thanks for your help …

You need to Paginate the Match before Map-ing. Match returns a Set reference and not a list of references. Paginate will give you those references.

Map(
  Paginate(Match(Index('ownerranking_by_user'), Ref(Collection("User"), "290125575057"))),
  Lambda('ranking', Get(Var('ranking')))
)

I have progressed with that change as I am able to retrieve a list of owner rankings with assoc data and a list of member rankings but with refs only.
My schema is now:

type User {
active: Boolean!
username: String!
description: String
email: String
mobile: String
ownerOf: [Ranking!]! @relation(name:"ownerranking")
memberOf: [Player!]! @relation(name: "playerinranking")
}

type Player {
active: Boolean!
rank: Int!
ranking: Ranking! @relation(name: "playerinranking")
playerInfo: User! 
challenger: Player @relation (name: "playerchallenger")
}

type Ranking {
active: Boolean!
rankingname: String!
rankingdesc: String
owner: User! @relation(name:"ownerranking")
}

I will need to display the ranking names for member rankings at least, possibly descriptions, but just names for now.

Following your suggestion I get a list of players with member ranking refs:

Map(
Paginate(
Match(
Index("playerinranking_by_user"),
Ref(Collection("User"), "290125575057572353")
)
),
Lambda("player", Get(Var("player")))
)

However, I need to retrieve the ‘deeply nested’ rankingname. Following another suggestion, I used this as a referernce. User to member rankings is a one to many relationship so the closest example is:

Map(
Paginate(Match(Index('thing2sByThing1'), Var('instance'))),
(ref) =>
Let(
{
instance: Get(Var('ref'))
},
{
/* repeat above recursively */
/* `ref` and `instance` variables are scoped! */
}
)
),

Attempting to merge what I currently know about both queries I get:

Map(
Paginate(
Match(
Index("memberranking_by_user"), Var("instance"))), (ref)
Let(
{
instance: Get(Var('ref'))
}
)
)

which is obviously not correct because, at least, I haven’t supplied the User reference. However, ‘ref’ is used as a variable, so it cannot just be replaced with the reference for User I already have.

How do I need to change this nested query to retrieve rankingnames for all the rankings the user is a member of?

thanks again …

I think you’re on the right track for it. Once you have the instance stored, you can use that reference to fetch other entries. For example, something like:

Let(
  {instance: Get(Var('ref'))},
  Merge(
    Var('instance'),
    {
      ranking: Paginate(
        Match(Index('playerinranking_by_ranking'), Select(['data', 'ranking'], Var('instance')))
      )
    }
  )
)

The above code is probably not :100: but the idea is that you could merge the instance object with another object that includes the results of other operations (like fetching more records). Using Select lets you pluck a piece of data from that instance to use as input into those operations.

I’m having trouble inserting the Let correctly. This, I believe, is the closest attempt:

Map(
Paginate(
Match(
Index("playerinranking_by_user"),
Ref(Collection("User"), "290125575057572353")
)
)
,
Let(
  {instance: Get(Var('ref'))},
  Merge(
    Var('instance'),
    {
      ranking: Paginate(
        Match(Index('playerinranking_by_ranking'), Select(['data', 'ranking'], Var('instance')))
      )
    }
  )
)
Lambda("instance", Get(Var("instance")))
)

Wherever I insert the ‘Let’ statement I get:

Error: missing ) after argument list

I’m using the shell highlighter to check the bracket matching, but this appears to simply confirm that the argument lists are correctly bracketed.

  1. Where should I insert the Let statement?
  2. How can I fix this syntax error?

Thanks …

Map always takes a lambda as a second argument. Currently you seem to be giving it a Let. There is also a ‘,’ missing.

What makes more sense (although I don’t know that that is what you want) is this:

Map(
    Paginate(
        Match(
            Index("playerinranking_by_user"),
            Ref(Collection("User"), "290125575057572353")
        )
    ),
    Lambda("ref", 
      Let({
              instance: Get(Var('ref'))
          },
          Merge(
              Var('instance'), {
                  ranking: Paginate(
                      Match(Index('playerinranking_by_ranking'), Select(['data', 'ranking'], Var('instance')))
                  )
              }
          )
      )
    )
)

Please do use a code formatter on your code, it will help you find the mistakes and it will help us read your query :slight_smile:

Are you still working on the allRankings custom resolver UDF?

That resolver returns the Ranking type. You should not have to Merge anything to it – Fauna’s GraphQL API should let the Rankings expand. If you merge the data of a Ranking type with a field called ranking, I expect the the API will end up ignoring it and it won’t be in the returned results.

The Ranking type already has the owner field. Are you trying to expand that? If you want a different relationship, then it should be defined in the Schema so that the API will just return it when you expand your GraphQL query.

Are you trying to return an array of Player types? That does have a ranking field. If you return a Page of Player types with the ranking relationship set up, then the GraphQL query should work.

Also, the playerinranking relationship is not set up right. User points to a Ranking, but the other side of the relationship is attached to the Player type.

# This relationship should not be working, or if it does, I don't expect it works right.
type User {
  # points to Player.
  memberOf: [Player!]! @relation(name: "playerinranking") 
}

type Player {
  # points to Ranking.  This is not consistent with the matching relationship.
  ranking: Ranking! @relation(name: "playerinranking") 
}

@databrecht @ptpaterson

Thank you, I now get rankingname/desc for the user’s member rankings.
I changed the schema and the field ‘ranking’ to ‘ladder’ to avoid any type/field ambiguity.:

type User {
active: Boolean!
username: String!
description: String
email: String
mobile: String
ownerOf: [Ranking!]! @relation(name:"ownerranking")
memberOf: [Player!]! @relation(name: "playerinranking")
}

type Player {
active: Boolean!
rank: Int!
ladder: Ranking! 
playerInfo: User! @relation(name: "playerinranking")
challenger: Player @relation (name: "playerchallenger")
}

type Ranking {
active: Boolean!
rankingname: String!
rankingdesc: String
owner: User! @relation(name:"ownerranking")
}

I created a new index:

{
  name: "ladder_in_player_by_ranking",
  unique: false,
  serialized: true,
  source: "Ranking",
  terms: [
    {
      field: ["ref"]
    }
  ],
  values: [
    {
      field: ["data", "rankingname"]
    },
    {
      field: ["data", "rankingdesc"]
    }
  ]
}

which I then added to the query:

Map(
  Paginate(
    Match(
      Index("playerinranking_by_user"),
      Ref(Collection("User"), "290125575057572353")
    )
  ),
  Lambda(
    "ref",
    Let(
      {
        instance: Get(Var("ref"))
      },
      Merge(Var("instance"), {
        ladder: Paginate(
          Match(
            Index("ladder_in_player_by_ranking"),
            Select(["data", "ladder"], Var("instance"))
          )
        )
      })
    )
  )
)

{
  data: [
    {
      ref: Ref(Collection("Player"), "290121646658814469"),
      ts: 1613618034246000,
      data: {
        active: true,
        rank: 1,
        ladder: Ref(Collection("Ranking"), "290125481092579841"),
        playerInfo: Ref(Collection("User"), "290125575057572353"),
        challenger: Ref(Collection("Player"), "290125849145901569")
      },
      ladder: {
        data: [["Test1Rnk", "t1"]]
      }
    },
    {
      ref: Ref(Collection("Player"), "290125849145901569"),
      ts: 1613618050484000,
      data: {
        active: true,
        rank: 2,
        ladder: Ref(Collection("Ranking"), "290125505974239745"),
        playerInfo: Ref(Collection("User"), "290125575057572353"),
        challenger: Ref(Collection("Player"), "290121646658814469")
      },
      ladder: {
        data: [["Test2Rnk", "t2"]]
      }
    }
  ]
}

To use this query I have created a function ‘member_rankings’:

Query(
  Lambda(
    ["userRef"],
    Map(
      Paginate(
        Match(
          Index("playerinranking_by_user"),
          Get(Ref(Collection("User"), Var("userRef")))
        )
      ),
      Lambda(
        "ref",
        Let(
          { instance: Get(Var("ref")) },
          Merge(Var("instance"), {
            ladder: Paginate(
              Match(
                Index("ladder_in_player_by_ranking"),
                Select(["data", "ladder"], Var("instance"))
              )
            )
          })
        )
      )
    )
  )
)

I think the data returned is an array of arrays (?) e.g.:

[["Test1Rnk", "t1"], ["Test2Rnk", "t2"]]

How should I define a new @resolver (memberRankings) in the schema to handle the data I obtain from the query function?
Thanks for your patience, and getting me to this point …

My advice is to try to get to a query that looks like the following. If this is way off, then I am super sorry that I am missing what you are going for. Sharing how you want to write the client query would be helpful then.

{
  findUserByID(id: "1234") {
    memberOf {
      data {
        ladder {
          rankingName
          rankingDesc
        }
      }
    }
  }
}

This should be possible without any UDFs or extra custom resolvers.

Update schema to

type User {
  active: Boolean!
  username: String!
  description: String
  email: String
  mobile: String
  ownerOf: [Ranking!]! @relation(name:"ownerranking")
  memberOf: [Player!]! @relation(name: "playerinranking")
}

type Player {
  active: Boolean!
  rank: Int!
  ladder: Ranking!  @relation # add the relation Directive
  playerInfo: User! @relation(name: "playerinranking")
  challenger: Player @relation (name: "playerchallenger")
}

type Ranking {
  active: Boolean!
  rankingname: String!
  rankingdesc: String
  player: [Player] @relation # add the relation
  owner: User! @relation(name:"ownerranking")
}

I did nothing but upload this schema and I could create this query in the playground:

You said (before edit) that you want to get the owned ranking as well as the members’ ranking. That should all be possible in a single query.

Otherwise, I suspect you are making multiple queries or at least invoking multiple top level queries with the same User ID provided.

Maybe calling at different times makes sense. If you are working with a cache, it can be super helpful to fetch data from a common endpoint, e.g. findUserByID and provide different fragments for when you want more involved data.

1 Like

Thank you very much. It works!
I will do a separate GQLHttp.send from the front end for allRankings.
Far less urgent than the problem just solved will be, perhaps, returning allRankings with the Owner and Member rankings excluded from the list. If this is straightforward perhaps you could give me a pointer or documentation reference(?). However, this is really an afterthought at this stage and resolving the above will now give me plenty to work on integrating with the front end … thanks again all your help …

If you want to exclude data then that would definitely be a UDF, involving the Difference of two indexes (everything minus a set).

You can fetch multiple things at the same time, though. And Fauna caches reads well. If you request all of the data, Fauna will retrieve all the docs efficiently, though duplicate data will come over the wire to you and that’s yucky.

One thing you could do (maybe), is to expand all the data in the allRankings query, but only get the _id field for the member and owned rankings. If you are normalizing the data in the client, this should work out.

I think that means I would then have to query the db separately for the owner/member rankingname/desc (using those ids)(?). In this app those rankings and their name/desc (rather than allRankings) will be the priority for the user. Excluding them from the list of allRankings is just a ‘nice to have’ at this stage …

1 Like

There are a few ways to hydrate the owner/member Rankings if you owner fetch the id from the database.

If your client normalizes the data, it should be mostly automatic. The immediate response from Fauna will be lacking the full info, but the cache will get updated. A separate “query” to your local cache could fill in the info without additional hits on the network. This should be workable with Apollo Client.

You can also just map the data yourself once it comes to the client.

1 Like

I am now reviewing Apollo Client as I expect it may help with many similar issues …

Where you say:

I discovered this has been an old time (2018) debate:

I’m not sure what the current consensus on this is in the Elm community(?). I will try to find out, although, I suspect the answer will be your last comment:

Incidentally, not sure if you already know about it but, Apollo Client/Server has a nice ‘Odyssey’ tutorial which is a good intro to GQL I would like to have found earlier …

@ptpaterson
Just a statement, not a question, to perhaps clarify the above.
A bit of confusion set in on my side. Where you mentioned:

You went on to mention caching, which is an important topic generally. However, I hadn’t realized that I only needed to tweak the query to:

# Write your query or mutation here
{
  findUserByID(id: "290125575057572353") {
    _id
    username
    description
    ownerOf {
      data {
        _id
        rankingname
        rankingdesc
      }
    }
    memberOf {
      data {
        _id
        active
        playerInfo {
          username
        }
        ladder {
          _id
          rankingname
          rankingdesc
        }
      }
    }
  }
}

to get all the info I need for the User on initial login and, yes, I can save data (like ranking _ids) to a local cache for use after that. I think Apollo Client isn’t a good fit with Elm currently.