yax-fauna 3.0.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +7 -0
- data/CHANGELOG +51 -0
- data/Gemfile +6 -0
- data/LICENSE +12 -0
- data/README.md +148 -0
- data/Rakefile +13 -0
- data/fauna.gemspec +26 -0
- data/lib/fauna.rb +25 -0
- data/lib/fauna/client.rb +253 -0
- data/lib/fauna/client_logger.rb +52 -0
- data/lib/fauna/context.rb +81 -0
- data/lib/fauna/deprecate.rb +29 -0
- data/lib/fauna/errors.rb +235 -0
- data/lib/fauna/json.rb +99 -0
- data/lib/fauna/objects.rb +147 -0
- data/lib/fauna/page.rb +374 -0
- data/lib/fauna/query.rb +899 -0
- data/lib/fauna/request_result.rb +58 -0
- data/lib/fauna/util.rb +50 -0
- data/lib/fauna/version.rb +4 -0
- data/spec/bytes_spec.rb +36 -0
- data/spec/client_logger_spec.rb +73 -0
- data/spec/client_spec.rb +127 -0
- data/spec/context_spec.rb +84 -0
- data/spec/errors_spec.rb +185 -0
- data/spec/fauna_helper.rb +102 -0
- data/spec/json_spec.rb +161 -0
- data/spec/page_spec.rb +357 -0
- data/spec/query_spec.rb +1104 -0
- data/spec/queryv_spec.rb +25 -0
- data/spec/ref_spec.rb +99 -0
- data/spec/setref_spec.rb +23 -0
- data/spec/spec_helper.rb +27 -0
- data/spec/util_spec.rb +19 -0
- metadata +181 -0
@@ -0,0 +1,147 @@
|
|
1
|
+
module Fauna
|
2
|
+
##
|
3
|
+
# A Ref.
|
4
|
+
#
|
5
|
+
# Reference: {FaunaDB Special Types}[https://fauna.com/documentation/queries#values-special_types]
|
6
|
+
class Ref
|
7
|
+
# The raw attributes of ref.
|
8
|
+
attr_accessor :id, :class_, :database
|
9
|
+
|
10
|
+
##
|
11
|
+
# Creates a Ref object.
|
12
|
+
#
|
13
|
+
# :call-seq:
|
14
|
+
# Ref.new('prydain', Native.databases)
|
15
|
+
#
|
16
|
+
# +id+: A string.
|
17
|
+
# +class_+: A Ref.
|
18
|
+
# +database+: A Ref.
|
19
|
+
def initialize(id, class_ = nil, database = nil)
|
20
|
+
fail ArgumentError.new 'id cannot be nil' if id.nil?
|
21
|
+
|
22
|
+
@id = id
|
23
|
+
@class_ = class_ unless class_.nil?
|
24
|
+
@database = database unless database.nil?
|
25
|
+
end
|
26
|
+
|
27
|
+
# Converts the Ref to a string
|
28
|
+
def to_s
|
29
|
+
cls = class_.nil? ? '' : ",class=#{class_.to_s}"
|
30
|
+
db = database.nil? ? '' : ",database=#{database.to_s}"
|
31
|
+
"Ref(id=#{id}#{cls}#{db})"
|
32
|
+
end
|
33
|
+
|
34
|
+
# Returns +true+ if +other+ is a Ref and contains the same value.
|
35
|
+
def ==(other)
|
36
|
+
return false unless other.is_a? Ref
|
37
|
+
id == other.id && class_ == other.class_ && database == other.database
|
38
|
+
end
|
39
|
+
|
40
|
+
alias_method :eql?, :==
|
41
|
+
end
|
42
|
+
|
43
|
+
class Native
|
44
|
+
@@natives = %w(classes indexes databases functions keys tokens credentials).freeze
|
45
|
+
|
46
|
+
@@natives.each do |id|
|
47
|
+
instance_variable_set "@#{id}", Ref.new(id).freeze
|
48
|
+
self.class.send :attr_reader, id.to_sym
|
49
|
+
end
|
50
|
+
|
51
|
+
def self.from_name(id)
|
52
|
+
return Ref.new(id) unless @@natives.include? id
|
53
|
+
send id.to_sym
|
54
|
+
end
|
55
|
+
end
|
56
|
+
|
57
|
+
##
|
58
|
+
# A SetRef.
|
59
|
+
#
|
60
|
+
# Reference: {FaunaDB Special Types}[https://fauna.com/documentation/queries#values-special_types]
|
61
|
+
class SetRef
|
62
|
+
# The raw set hash.
|
63
|
+
attr_accessor :value
|
64
|
+
|
65
|
+
##
|
66
|
+
# Creates a new SetRef with the given parameters.
|
67
|
+
#
|
68
|
+
# +params+:: Hash of parameters to build the SetRef with.
|
69
|
+
#
|
70
|
+
# Reference: {FaunaDB Special Types}[https://fauna.com/documentation/queries#values-special_types]
|
71
|
+
def initialize(params = {})
|
72
|
+
self.value = params
|
73
|
+
end
|
74
|
+
|
75
|
+
# Returns +true+ if +other+ is a SetRef and contains the same value.
|
76
|
+
def ==(other)
|
77
|
+
return false unless other.is_a? SetRef
|
78
|
+
value == other.value
|
79
|
+
end
|
80
|
+
|
81
|
+
alias_method :eql?, :==
|
82
|
+
end
|
83
|
+
|
84
|
+
##
|
85
|
+
# A Bytes wrapper.
|
86
|
+
#
|
87
|
+
# Reference: {FaunaDB Special Types}[https://fauna.com/documentation/queries#values-special_types]
|
88
|
+
class Bytes
|
89
|
+
# The raw bytes.
|
90
|
+
attr_accessor :bytes
|
91
|
+
|
92
|
+
##
|
93
|
+
# Creates a new Bytes wrapper with the given parameters.
|
94
|
+
#
|
95
|
+
# +bytes+:: The bytes to be wrapped by the Bytes object.
|
96
|
+
#
|
97
|
+
# Reference: {FaunaDB Special Types}[https://fauna.com/documentation/queries#values-special_types]
|
98
|
+
def initialize(bytes)
|
99
|
+
self.bytes = bytes
|
100
|
+
end
|
101
|
+
|
102
|
+
# Converts the Bytes to base64-encoded form.
|
103
|
+
def to_base64
|
104
|
+
Base64.urlsafe_encode64(bytes)
|
105
|
+
end
|
106
|
+
|
107
|
+
# Returns +true+ if +other+ is a Bytes and contains the same bytes.
|
108
|
+
def ==(other)
|
109
|
+
return false unless other.is_a? Bytes
|
110
|
+
bytes == other.bytes
|
111
|
+
end
|
112
|
+
|
113
|
+
alias_method :eql?, :==
|
114
|
+
|
115
|
+
# Create new Bytes object from Base64 encoded bytes.
|
116
|
+
def self.from_base64(enc)
|
117
|
+
new(Base64.urlsafe_decode64(enc))
|
118
|
+
end
|
119
|
+
end
|
120
|
+
|
121
|
+
##
|
122
|
+
# A QueryV.
|
123
|
+
#
|
124
|
+
# Reference: {FaunaDB Special Types}[https://fauna.com/documentation/queries-values-special_types]
|
125
|
+
class QueryV
|
126
|
+
# The raw query hash.
|
127
|
+
attr_accessor :value
|
128
|
+
|
129
|
+
##
|
130
|
+
# Creates a new QueryV with the given parameters.
|
131
|
+
#
|
132
|
+
# +params+:: Hash of parameters to build the QueryV with.
|
133
|
+
#
|
134
|
+
# Reference: {FaunaDB Special Types}[https://fauna.com/documentation/queries-values-special_types]
|
135
|
+
def initialize(params = {})
|
136
|
+
self.value = params
|
137
|
+
end
|
138
|
+
|
139
|
+
# Returns +true+ if +other+ is a QueryV and contains the same value.
|
140
|
+
def ==(other)
|
141
|
+
return false unless other.is_a? QueryV
|
142
|
+
value == other.value
|
143
|
+
end
|
144
|
+
|
145
|
+
alias_method :eql?, :==
|
146
|
+
end
|
147
|
+
end
|
data/lib/fauna/page.rb
ADDED
@@ -0,0 +1,374 @@
|
|
1
|
+
module Fauna
|
2
|
+
##
|
3
|
+
# Helper for handling pagination over sets.
|
4
|
+
#
|
5
|
+
# Given a client and a set, allows you to iterate as well as individually move page by page over a set.
|
6
|
+
#
|
7
|
+
# Pages lazily load the contents of the page. Loading will occur when +data+, +before+, or +after+ are first accessed
|
8
|
+
# for a new page. Additionally this will occur when calling +page_before+ or +page_after+ without calling one of the
|
9
|
+
# data methods first (as the first page must be checked to find the next page). Pages created by builders will unload
|
10
|
+
# any data from the current page. Pages will always proceed in the requested direction.
|
11
|
+
#
|
12
|
+
# Explicit paging is done via the +page_after+ and +page_before+ methods. Iteration can be done via the +each+ and
|
13
|
+
# +reverse_each+ enumerators. A single page can be retrieved by passing a cursor and then accessing it's data.
|
14
|
+
#
|
15
|
+
# Examples:
|
16
|
+
#
|
17
|
+
# Paging over a class index
|
18
|
+
#
|
19
|
+
# page = Page.new(client, Query.match(Query.index('items')))
|
20
|
+
#
|
21
|
+
# Paging over a class index 5 at a time, mapping the refs to the +data.value+ for each instance
|
22
|
+
#
|
23
|
+
# page = Page.new(client, Query.match(Query.index('items')), size: 5) do |ref|
|
24
|
+
# select ['data', 'value'], get(ref)
|
25
|
+
# end
|
26
|
+
#
|
27
|
+
# # Same thing, but using builders instead
|
28
|
+
#
|
29
|
+
# page = Page.new(client, Query.match(Query.index('items'))).with_params(size: 5).map do |ref|
|
30
|
+
# select ['data', 'value'], get(ref)
|
31
|
+
# end
|
32
|
+
#
|
33
|
+
# Paging over a class index, mapping refs to the +data.value+ for each instance, filtering out odd numbers, and
|
34
|
+
# multiplying the value:
|
35
|
+
#
|
36
|
+
# page = Page.new(client, Query.match(Query.index('items'))).map do |ref|
|
37
|
+
# select ['data', 'value'], get(ref)
|
38
|
+
# end.filter do |value|
|
39
|
+
# equals modulo(value, 2), 0
|
40
|
+
# end.map do |value|
|
41
|
+
# multiply value, 2
|
42
|
+
# end
|
43
|
+
class Page
|
44
|
+
##
|
45
|
+
# Creates a pagination helper for paging/iterating over a set.
|
46
|
+
#
|
47
|
+
# +client+:: Client to execute queries with.
|
48
|
+
# +set+:: A set query to paginate over.
|
49
|
+
# +params+:: A list of parameters to pass to {paginate}[https://fauna.com/documentation/queries#read_functions-paginate_set].
|
50
|
+
# +lambda+:: Optional lambda to map the generated paginate query with. The block will be run in a query context.
|
51
|
+
# An element from the current page will be passed into the block as an argument. See #map for more info.
|
52
|
+
def initialize(client, set, params = {}, &lambda)
|
53
|
+
@client = client
|
54
|
+
@set = set
|
55
|
+
@params = params.dup
|
56
|
+
@fauna_funcs = []
|
57
|
+
@postprocessing_map = nil
|
58
|
+
|
59
|
+
@fauna_funcs << proc { |query| map(query, &lambda) } unless lambda.nil?
|
60
|
+
|
61
|
+
unload_page
|
62
|
+
@params.freeze
|
63
|
+
end
|
64
|
+
|
65
|
+
# Returns +true+ if +other+ is a Page and contains the same configuration and data.
|
66
|
+
def ==(other)
|
67
|
+
return false unless other.is_a? Page
|
68
|
+
@populated == other.instance_variable_get(:@populated) &&
|
69
|
+
@data == other.instance_variable_get(:@data) &&
|
70
|
+
@before == other.instance_variable_get(:@before) &&
|
71
|
+
@after == other.instance_variable_get(:@after) &&
|
72
|
+
@client == other.instance_variable_get(:@client) &&
|
73
|
+
@set == other.instance_variable_get(:@set) &&
|
74
|
+
@params == other.instance_variable_get(:@params) &&
|
75
|
+
@fauna_funcs == other.instance_variable_get(:@fauna_funcs) &&
|
76
|
+
@postprocessing_map == other.instance_variable_get(:@postprocessing_map)
|
77
|
+
end
|
78
|
+
|
79
|
+
alias_method :eql?, :==
|
80
|
+
|
81
|
+
# The configured params used for the current pagination.
|
82
|
+
attr_reader :params
|
83
|
+
|
84
|
+
##
|
85
|
+
# Explicitly loads data for the current page if it has not already been loaded.
|
86
|
+
#
|
87
|
+
# Returns +true+ if the data was just loaded and +false+ if it was already loaded.
|
88
|
+
def load!
|
89
|
+
if @populated
|
90
|
+
false
|
91
|
+
else
|
92
|
+
load_page(get_page(@params))
|
93
|
+
true
|
94
|
+
end
|
95
|
+
end
|
96
|
+
|
97
|
+
# :section: Data
|
98
|
+
|
99
|
+
##
|
100
|
+
# Data contained within the current page.
|
101
|
+
#
|
102
|
+
# Lazily loads the page data if it has not already been loaded.
|
103
|
+
def data
|
104
|
+
load!
|
105
|
+
@data
|
106
|
+
end
|
107
|
+
|
108
|
+
##
|
109
|
+
# Before cursor for the current page.
|
110
|
+
#
|
111
|
+
# Lazily loads the page data if it has not already been loaded.
|
112
|
+
def before
|
113
|
+
load!
|
114
|
+
@before
|
115
|
+
end
|
116
|
+
|
117
|
+
##
|
118
|
+
# After cursor for the current page.
|
119
|
+
#
|
120
|
+
# Lazily loads the page data if it has not already been loaded.
|
121
|
+
def after
|
122
|
+
load!
|
123
|
+
@after
|
124
|
+
end
|
125
|
+
|
126
|
+
# :section: Builders
|
127
|
+
|
128
|
+
##
|
129
|
+
# Returns a copy of the page with the given +params+ set.
|
130
|
+
#
|
131
|
+
# See {paginate}[https://fauna.com/documentation/queries#read_functions-paginate_set] for more details.
|
132
|
+
def with_params(params = {})
|
133
|
+
with_dup do |page|
|
134
|
+
page_params = page.instance_variable_get(:@params)
|
135
|
+
|
136
|
+
if CURSOR_KEYS.any? { |key| params.include? key }
|
137
|
+
# Remove previous cursor
|
138
|
+
CURSOR_KEYS.each { |key| page_params.delete key }
|
139
|
+
end
|
140
|
+
|
141
|
+
# Update params
|
142
|
+
page_params.merge!(params)
|
143
|
+
end
|
144
|
+
end
|
145
|
+
|
146
|
+
##
|
147
|
+
# Returns a copy of the page with a fauna +map+ using the given lambda chained onto the paginate query.
|
148
|
+
#
|
149
|
+
# The lambda will be passed into a +map+ function that wraps the generated paginate query. Additional collection
|
150
|
+
# functions may be combined by chaining them together.
|
151
|
+
#
|
152
|
+
# The lambda will be run in a Query.expr context, and passed an element from the current page as an argument.
|
153
|
+
#
|
154
|
+
# Example of mapping a set of refs to their instances:
|
155
|
+
#
|
156
|
+
# page.map { |ref| get ref }
|
157
|
+
def map(&lambda)
|
158
|
+
with_dup do |page|
|
159
|
+
page.instance_variable_get(:@fauna_funcs) << proc { |query| map(query, &lambda) }
|
160
|
+
end
|
161
|
+
end
|
162
|
+
|
163
|
+
##
|
164
|
+
# Returns a copy of the page with a fauna +filter+ using the given lambda chained onto the paginate query.
|
165
|
+
#
|
166
|
+
# The lambda will be passed into a +filter+ function that wraps the generated paginate query. Additional collection
|
167
|
+
# functions may be combined by chaining them together.
|
168
|
+
#
|
169
|
+
# The lambda will be run in a Query.expr context, and passed an element from the current page as an argument.
|
170
|
+
#
|
171
|
+
# Example of filtering out odd numbers from a set of numbers:
|
172
|
+
#
|
173
|
+
# page.filter { |value| equals(modulo(value, 2), 0) }
|
174
|
+
def filter(&lambda)
|
175
|
+
with_dup do |page|
|
176
|
+
page.instance_variable_get(:@fauna_funcs) << proc { |query| filter(query, &lambda) }
|
177
|
+
end
|
178
|
+
end
|
179
|
+
|
180
|
+
##
|
181
|
+
# Returns a copy of the page with the given ruby block set.
|
182
|
+
#
|
183
|
+
# The block will be used to map the returned data elements from the executed fauna query. Only one postprocessing
|
184
|
+
# map can be configured at a time.
|
185
|
+
#
|
186
|
+
# Intended for use when the elements in a page need to be converted within ruby (ie loading into a model). Wherever
|
187
|
+
# the operation can be performed from within FaunaDB, +map+ should be used instead.
|
188
|
+
#
|
189
|
+
# The block will be passed the each element as a parameter from the data of the page currently being loaded.
|
190
|
+
#
|
191
|
+
# Example of loading instances into your own model:
|
192
|
+
#
|
193
|
+
# page.postprocessing_map { |instance| YourModel.load(instance) }
|
194
|
+
def postprocessing_map(&block)
|
195
|
+
with_dup do |page|
|
196
|
+
page.instance_variable_set(:@postprocessing_map, block)
|
197
|
+
end
|
198
|
+
end
|
199
|
+
|
200
|
+
# :section: Pagination
|
201
|
+
|
202
|
+
##
|
203
|
+
# The page after the current one in the set.
|
204
|
+
#
|
205
|
+
# Returns +nil+ when there are no more pages after the current page. Lazily loads the current page if it has not
|
206
|
+
# already been loaded in order to determine the page after.
|
207
|
+
def page_after
|
208
|
+
new_page(:after)
|
209
|
+
end
|
210
|
+
|
211
|
+
##
|
212
|
+
# The page before the current one in the set.
|
213
|
+
#
|
214
|
+
# Returns +nil+ when there are no more pages before the current page. Lazily loads the current page if it has not
|
215
|
+
# already been loaded in order to determine the page before.
|
216
|
+
def page_before
|
217
|
+
new_page(:before)
|
218
|
+
end
|
219
|
+
|
220
|
+
##
|
221
|
+
# Returns an enumerator that iterates in the +after+ direction.
|
222
|
+
#
|
223
|
+
# When a block is provided, the return of the block will always be +nil+ (to avoid loading large sets into memory).
|
224
|
+
def each
|
225
|
+
return enum_for(:each) unless block_given?
|
226
|
+
|
227
|
+
# Return current page
|
228
|
+
yield data
|
229
|
+
|
230
|
+
# Begin returning pages after
|
231
|
+
page = self.page_after
|
232
|
+
until page.nil?
|
233
|
+
yield page.data
|
234
|
+
page = page.page_after
|
235
|
+
end
|
236
|
+
end
|
237
|
+
|
238
|
+
##
|
239
|
+
# Returns an enumerator that iterates in the +before+ direction.
|
240
|
+
#
|
241
|
+
# When a block is provided, the return of the block will always be +nil+ (to avoid loading large sets into memory).
|
242
|
+
#
|
243
|
+
# While the paging will occur in the reverse direction, the data returned will still be in the normal direction.
|
244
|
+
def reverse_each
|
245
|
+
return enum_for(:reverse_each) unless block_given?
|
246
|
+
|
247
|
+
# Return current page
|
248
|
+
yield data
|
249
|
+
|
250
|
+
# Begin returning pages before
|
251
|
+
page = self.page_before
|
252
|
+
until page.nil?
|
253
|
+
yield page.data
|
254
|
+
page = page.page_before
|
255
|
+
end
|
256
|
+
end
|
257
|
+
|
258
|
+
##
|
259
|
+
# Returns the flattened contents of the set as an array.
|
260
|
+
#
|
261
|
+
# Ideal for when you need the full contents of a set with a known small size. If you need to iterate over a set
|
262
|
+
# of large or unknown size, it is recommended to use +each+ instead.
|
263
|
+
#
|
264
|
+
# The set is paged in the +after+ direction.
|
265
|
+
def all
|
266
|
+
each.flat_map { |x| x }
|
267
|
+
end
|
268
|
+
|
269
|
+
##
|
270
|
+
# Iterates over the entire set, applying the configured lambda in a foreach block, and discarding the result.
|
271
|
+
#
|
272
|
+
# Ideal for performing a +foreach+ over an entire set (like deleting all instances in a set). The set is iterated in
|
273
|
+
# the +after+ direction. The +foreach+ will be chained on top of any previously configured collection functions.
|
274
|
+
#
|
275
|
+
# Example of deleting every instance in a set:
|
276
|
+
#
|
277
|
+
# page.foreach! { |ref| delete ref }
|
278
|
+
def foreach!(&lambda)
|
279
|
+
# Create new page with foreach block
|
280
|
+
iter_page = with_dup do |page|
|
281
|
+
page.instance_variable_get(:@fauna_funcs) << proc { |query| foreach(query, &lambda) }
|
282
|
+
end
|
283
|
+
|
284
|
+
# Apply to all pages in the set
|
285
|
+
until iter_page.nil?
|
286
|
+
iter_page.load!
|
287
|
+
iter_page = iter_page.page_after
|
288
|
+
end
|
289
|
+
nil
|
290
|
+
end
|
291
|
+
|
292
|
+
def dup # :nodoc:
|
293
|
+
page = super
|
294
|
+
page.instance_variable_set(:@params, @params.dup)
|
295
|
+
page.instance_variable_set(:@fauna_funcs, @fauna_funcs.dup)
|
296
|
+
page
|
297
|
+
end
|
298
|
+
|
299
|
+
private
|
300
|
+
|
301
|
+
CURSOR_KEYS = [:before, :after].freeze # :nodoc:
|
302
|
+
|
303
|
+
def with_dup
|
304
|
+
# Create a copy and drop loaded data
|
305
|
+
page = self.dup
|
306
|
+
page.send(:unload_page)
|
307
|
+
|
308
|
+
# Yield page for manipulation
|
309
|
+
yield page
|
310
|
+
|
311
|
+
# Freeze params and return page
|
312
|
+
page.params.freeze
|
313
|
+
page
|
314
|
+
end
|
315
|
+
|
316
|
+
def get_page(params)
|
317
|
+
# Create query
|
318
|
+
query = Query.paginate @set, params
|
319
|
+
|
320
|
+
unless @fauna_funcs.empty?
|
321
|
+
# Wrap paginate query with the fauna maps
|
322
|
+
dsl = Query::QueryDSLContext.new
|
323
|
+
@fauna_funcs.each do |proc|
|
324
|
+
query = DSLContext.eval_dsl(dsl, query, &proc)
|
325
|
+
end
|
326
|
+
query = Query::Expr.wrap query
|
327
|
+
end
|
328
|
+
|
329
|
+
# Execute query
|
330
|
+
result = @client.query query
|
331
|
+
|
332
|
+
unless @postprocessing_map.nil?
|
333
|
+
# Map the resulting data with the ruby block
|
334
|
+
result[:data].map! { |element| @postprocessing_map.call(element) }
|
335
|
+
end
|
336
|
+
|
337
|
+
# Return result
|
338
|
+
result
|
339
|
+
end
|
340
|
+
|
341
|
+
def load_page(page)
|
342
|
+
# Not initial after the first page
|
343
|
+
@populated = true
|
344
|
+
|
345
|
+
# Update the page fields
|
346
|
+
@data = page[:data]
|
347
|
+
@before = page[:before]
|
348
|
+
@after = page[:after]
|
349
|
+
end
|
350
|
+
|
351
|
+
def unload_page
|
352
|
+
# Reset paging
|
353
|
+
@populated = false
|
354
|
+
|
355
|
+
# Reset data
|
356
|
+
@data = nil
|
357
|
+
@before = nil
|
358
|
+
@after = nil
|
359
|
+
end
|
360
|
+
|
361
|
+
def new_page(direction)
|
362
|
+
fail "Invalid direction; must be one of #{CURSOR_KEYS}" unless CURSOR_KEYS.include?(direction)
|
363
|
+
|
364
|
+
cursor = self.send(direction)
|
365
|
+
|
366
|
+
# If there is no next cursor, we have reached the end of the set.
|
367
|
+
# Return +nil+.
|
368
|
+
return nil if cursor.nil?
|
369
|
+
|
370
|
+
# Use the configured cursor to fetch the first page.
|
371
|
+
with_params(direction => cursor)
|
372
|
+
end
|
373
|
+
end
|
374
|
+
end
|