Paginate with dynamic relationship count

Hi there,

Brecht gave a great answer to this thread SQL group by, having counterpart in FaunaDB (which ty Cory for pointing it out to me in my prior tangentially-related forum post), but I’m wondering how one would modify that answer to work in a GraphQL UDF that supports pages in the same way the docs demonstrate with UDF that returns a database page. Since the former relies on mapping through a collection and the latter relies on paginating an index match, I’m struggling to combine them into a single solution.

For context, in a simplified version of my particular use case, I have a Reviews collection and a Movies collection, and want to find “trending” movies based on # of reviews in the last 30 days.

I can do something like the below to calculate a review count within a dynamic date range, though I am not sure how to sort or paginate with that review count since it’s not coming from an index.

CreateIndex({
  name: "reviews_by_movie",
  source: Collection("Reviews"),
  terms: [
    { field: ["data", "movie"] }
  ],
   values: [
    { field: ["data", "created"], reverse: true },
    { field: ["ref"] }
  ]
})
Query(
  Lambda(
    ["input"],
    Let(
      {
        trending: Map(
          Paginate(Documents(Collection("Movies"))),
          Lambda(
            ["movie"],
            Let(
              {
                movieRefs: Range(
                  Match(Index("reviews_by_movie"), Var("movie")),
                  Now(),
                  TimeSubtract(Now(), 30, "days")
                ),
                count: Count(Var("movieRefs"))
              },
              { trendingCount: Var("count"), movie: Var("movie") }
            )
          )
        )
      },
      Select("data", Var("trending"))
    )
  )
)

If what I’m after is not directly possible with FQL, would the best practice approach be some sort of cron job to routinely Map over my Movies collection and update each document with a freshly calculated trendingCount I can use with an index? Any and all feedback greatly appreciated :pray:

Cheers,
Ross

For paginated resolvers (i.e. @resolver(paginated: true, ...)) GraphQL expects to receive a Page with the just the resulting Documents. Of course, in order to sort with indexes you need to add other values to the index. The answer is to use Map with the final resulting page to return only the relevant Documents.

In your example, it appears you are also returning an @embedded type as a payload. Things can get more complicated there. I believe that what you are trying is possible, but I think more details are needed.

Can you please share your GraphQL schema?

Thank you for the reply! You are correct, I was returning an @embedded type as a payload so I could include the dynamically calculated trendingCount with each movie document, complicating my ability to wrap this in a Page.

I actually ended up going the route of adding a trendingCount field to my review document schema however, and setup a GitHub Actions cron job that triggers a UDF via a protected API route in my application a few times a day. The UDF paginates through the Movies collection and recalculates trendingCount for each movie, making it much easier to work with since I can use it with indexes now. I’m thinking this will also be a lot more efficient than needing to paginate the Movies collection to dynamically calculate trendingCount on-demand every time someone hits my homepage, which is what I was originally trying to do :sweat_smile:

If you think there’s still any merit to solving the original approach though (maybe for anyone else who lands on this question looking to solve similar issues, etc), I’ll still share a simplified version of my original schema.

type Query {
    findMoviesByTrending(): [MoviesByTrendingPayload] @resolver(name: "movies_by_trending", paginated: true)
}

type Review @collection(name: "Reviews") {
    created: Time!
    movie: Movie!
}

type Movie @collection(name: "Movies") {
    title: String!
    reviewCount: Int!
}

type MoviesByTrendingPayload @embedded {
    trendingCount: Int!
    movie: Movie!
}

And just for reference, here’s my current approach of manually recalculating trendingCount on a schedule rather than on-demand when fetched.

// schema.gql
type Query {
    findMoviesByTrending(): [Movie] @resolver(name: "movies_by_trending", paginated: true)
}

type Review @collection(name: "Reviews") {
    created: Time!
    movie: Movie!
}

type Movie @collection(name: "Movies") {
    title: String!
    reviewCount: Int!
    trendingCount: Int!
}

// movies_by_trending Index
CreateIndex({
  name: "movies_by_trending",
  source: Collection("Movies"),
  values: [
    { field: ["data", "trendingCount"], reverse: true },
    { field: ["ref"] }
  ]
})

// movies_by_trending UDF
Query(
  Lambda(
    ["_", "size", "after", "before"],
    Let(
      {
        match: Match(Index("movies_by_trending")),
        page: If(
          Equals(Var("before"), null),
          If(
            Equals(Var("after"), null),
            Paginate(Var("match"), { size: Var("size") }),
            Paginate(Var("match"), { after: Var("after"), size: Var("size") })
          ),
          Paginate(Var("match"), { before: Var("before"), size: Var("size") })
        )
      },
      Map(Var("page"), Lambda(["_", "movieRef"], Get(Var("movieRef"))))
    )
  )
)

// recalc_trending UDF
Query(
  Lambda(
    "_",
    Let(
      {
        update: Map(
          Paginate(Documents(Collection("Movies"))),
          Lambda(
            ["movie"],
            Let(
              {
                reviewRefs: Range(
                  Match(Index("reviews_by_movie"), Var("movie")),
                  Now(),
                  TimeSubtract(Now(), 30, "days")
                ),
                count: Count(Var("reviewRefs"))
              },
              Update(Var("movie"), { data: { trendingCount: Var("count") } })
            )
          )
        )
      },
      true
    )
  )
)

This topic was automatically closed 2 days after the last reply. New replies are no longer allowed.