tywalch/electrodb

How to get cursor for each item returned by a query?

Closed this issue · 3 comments

Describe the bug
My project is using GraphQL and cursor-based pagination (connections). As part of implementing this kind of pagination, I want to return each item from DynamoDB with a cursor specific to that item. This is so that users can paginate starting at a specific cursor.

A GraphQL query using cursor-based pagination would look something like this:

query {
  friends(first: 2, after: "<cursor of friend 3>") { # Return a list of 2 friends, starting after friend 3
    edges {
      node {
        name
      }
      cursor # Cursor specific to this friend
    }
    pageInfo {
      endCursor # Cursor specific to the last friend in this connection
      hasNextPage
    }
  }
}

ElectroDB's query operation will return a single cursor (I believe this is the LastEvaluatedKey from DynamoDB that is then copied, stringified, and base64 encoded) for the entire item collection from DynamoDB. However, I'm trying to create a cursor for each item in the collection.

How can I do this? Looking at this util, it looks like I need the primary key and values of each entity, which I can't easily get.

I think I could use .go({ data: "raw" }) but I'd like to stay away from that if possible, since I'd need to convert back to the "nice" format of my data that ElectroDB helps with with. I could also use .go({ data: "includeKeys" }), but I don't get any type safety on the keys returned (for example, pk doesn't exist on the entity, and I'm having to use a // @ts-expect-error when referring to that, which I'd also like to stay away from).

ElectroDB Version

2.13.0

Expected behavior
Just brainstorming on potential API options here, while trying to keep in mind backwards compatibility...

It'd be great to have some sort of utility function on the entity like:

const { cursor, data } = await MyEntity.query.all({ id: "blah" }).go()

const edges = data.forEach(entity => ({
  node: entity,
  cursor: MyEntity.createCursor(entity), // Returns a correctly formatted cursor for this entity
}))

return {
  edges,
  pageInfo: {
    endCursor: edges[edges.length - 1].cursor,
    hasNextPage: Boolean(cursor),
  }
}

Another option could be an addCursorForEachItem execution option:

const { cursor, data } = await MyEntity.query.all({ id: "blah" }).go({
  addCursorForEachItem: true
})

// data: [
//   {  item: { ...item... },  cursor: "..." },
//   {  item: { ...item... },  cursor: "..." },
//   {  item: { ...item... },  cursor: "..." },
// ]

// compared to

const { cursor, data } = await MyEntity.query.all({ id: "blah" }).go({
  addCursorForEachItem: false
})

// data: [
//   { ...item... },
//   { ...item... },
//   { ...item... },
// ]

(writing this code out from hand, hopefully it makes sense)

I had the same problem and ended up doing it manually in the past. I haven't tried this yet, but there is a utility called conversions that looks to convert an item to a cursor.

This is perfect @CaveSeal (and @tywalch for having this in the first place 😄) thank you!

I'm able to do the following for my example above:

const { cursor, data } = await MyEntity.query.all({ id: "blah" }).go()

const edges = data.forEach(entity => ({
  node: entity,
  cursor: MyEntity.conversions.byAccessPattern.all.fromComposite.toCursor(entity, { strict: "all" })
}))

...

Thank you @CaveSeal!! This is exactly what I would have pointed toward