1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
|
# Pleroma: A lightweight social networking server
# Copyright © 2017-2020 Pleroma Authors <https://pleroma.social/>
# SPDX-License-Identifier: AGPL-3.0-only
defmodule Pleroma.Pagination do
@moduledoc """
Implements Mastodon-compatible pagination.
"""
import Ecto.Query
import Ecto.Changeset
alias Pleroma.Repo
@type type :: :keyset | :offset
@default_limit 20
@max_limit 40
@page_keys ["max_id", "min_id", "limit", "since_id", "order"]
def page_keys, do: @page_keys
@spec fetch_paginated(Ecto.Query.t(), map(), type(), atom() | nil) :: [Ecto.Schema.t()]
def fetch_paginated(query, params, type \\ :keyset, table_binding \\ nil)
def fetch_paginated(query, %{total: true} = params, :keyset, table_binding) do
total = Repo.aggregate(query, :count, :id)
%{
total: total,
items: fetch_paginated(query, Map.drop(params, [:total]), :keyset, table_binding)
}
end
def fetch_paginated(query, params, :keyset, table_binding) do
options = cast_params(params)
query
|> paginate(options, :keyset, table_binding)
|> Repo.all()
|> enforce_order(options)
end
def fetch_paginated(query, %{total: true} = params, :offset, table_binding) do
total =
query
|> Ecto.Query.exclude(:left_join)
|> Repo.aggregate(:count, :id)
%{
total: total,
items: fetch_paginated(query, Map.drop(params, [:total]), :offset, table_binding)
}
end
def fetch_paginated(query, params, :offset, table_binding) do
options = cast_params(params)
query
|> paginate(options, :offset, table_binding)
|> Repo.all()
end
@spec paginate(Ecto.Query.t(), map(), type(), atom() | nil) :: [Ecto.Schema.t()]
def paginate(query, options, method \\ :keyset, table_binding \\ nil)
def paginate(query, options, :keyset, table_binding) do
query
|> restrict(:min_id, options, table_binding)
|> restrict(:since_id, options, table_binding)
|> restrict(:max_id, options, table_binding)
|> restrict(:order, options, table_binding)
|> restrict(:limit, options, table_binding)
end
def paginate(query, options, :offset, table_binding) do
query
|> restrict(:order, options, table_binding)
|> restrict(:offset, options, table_binding)
|> restrict(:limit, options, table_binding)
end
defp cast_params(params) do
param_types = %{
min_id: :string,
since_id: :string,
max_id: :string,
offset: :integer,
limit: :integer,
skip_order: :boolean
}
changeset = cast({%{}, param_types}, params, Map.keys(param_types))
changeset.changes
end
defp restrict(query, :min_id, %{min_id: min_id}, table_binding) do
where(query, [{q, table_position(query, table_binding)}], q.id > ^min_id)
end
defp restrict(query, :since_id, %{since_id: since_id}, table_binding) do
where(query, [{q, table_position(query, table_binding)}], q.id > ^since_id)
end
defp restrict(query, :max_id, %{max_id: max_id}, table_binding) do
where(query, [{q, table_position(query, table_binding)}], q.id < ^max_id)
end
defp restrict(query, :order, %{skip_order: true}, _), do: query
defp restrict(query, :order, %{min_id: _}, table_binding) do
order_by(
query,
[{u, table_position(query, table_binding)}],
fragment("? asc nulls last", u.id)
)
end
defp restrict(query, :order, _options, table_binding) do
order_by(
query,
[{u, table_position(query, table_binding)}],
fragment("? desc nulls last", u.id)
)
end
defp restrict(query, :offset, %{offset: offset}, _table_binding) do
offset(query, ^offset)
end
defp restrict(query, :limit, options, _table_binding) do
limit =
case Map.get(options, :limit, @default_limit) do
limit when limit < @max_limit -> limit
_ -> @max_limit
end
query
|> limit(^limit)
end
defp restrict(query, _, _, _), do: query
defp enforce_order(result, %{min_id: _}) do
result
|> Enum.reverse()
end
defp enforce_order(result, _), do: result
defp table_position(%Ecto.Query{} = query, binding_name) do
Map.get(query.aliases, binding_name, 0)
end
defp table_position(_, _), do: 0
end
|