Last active
March 10, 2025 07:47
-
-
Save khanha2/3016a95d3be32a49fd175ffab28ee753 to your computer and use it in GitHub Desktop.
Elixir simple cursornator
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
defmodule Cursornator do | |
import Ecto.Query | |
@doc """ | |
Stream the query with cursor | |
Options | |
- `:loop_query_func` - function to go to the next page | |
- `:limit` - limit of entries to fetch | |
- `:order_expr` - order expression for ordering the query | |
Example | |
|> Cursornator.stream([:field1, :field2, :id]) | |
""" | |
def stream(query, fields, opts \\ []) do | |
loop_query_func = Keyword.get(opts, :loop_query_func) | |
limit = Keyword.get(opts, :limit, 100) | |
order_expr = Keyword.get(opts, :order_expr, Enum.map(fields, &{:asc, &1})) | |
query = | |
query | |
|> order_by(^order_expr) | |
|> limit(^limit) | |
Stream.resource( | |
fn -> %{values: [], has_next?: true} end, | |
fn stream_opts -> | |
case stream_opts do | |
%{has_next?: false} -> | |
{:halt, nil} | |
_ -> | |
query_entries(query, stream_opts[:values], fields, loop_query_func, limit) | |
end | |
end, | |
fn _ -> nil end | |
) | |
end | |
# Query entries with cursor | |
defp query_entries(query, values, fields, loop_query_func, limit) do | |
query = | |
if is_nil(loop_query_func) do | |
loop_query(query, fields, values) | |
else | |
loop_query_func.(query, fields, values) | |
end | |
entries = NetSuiteMiddlewareCore.Repo.all(query) | |
if length(entries) == limit do | |
last_entry = List.last(entries) | |
new_values = Enum.map(fields, &{&1, Map.get(last_entry, &1)}) | |
{entries, %{values: new_values, has_next?: true}} | |
else | |
{entries, %{values: [], has_next?: false}} | |
end | |
end | |
# Build condition for looping query | |
defp loop_query(query, _fields, []), do: query | |
defp loop_query(query, fields, values) do | |
reversed_fields = Enum.reverse(fields) | |
[last_field | reversed_fields] = reversed_fields | |
condition = dynamic([entry], field(entry, ^last_field) > ^values[last_field]) | |
condition = | |
Enum.reduce(reversed_fields, condition, fn field, acc -> | |
value = values[field] | |
dynamic( | |
[entry], | |
field(entry, ^field) > ^value or (field(entry, ^field) == ^value and ^acc) | |
) | |
end) | |
where(query, ^condition) | |
end | |
end |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment