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.
@@ -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
@@ -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