fmrest-spyke 0.24.0 → 0.25.0.rc1
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/CHANGELOG.md +5 -0
- data/README.md +3 -2
- data/lib/fmrest/spyke/model/orm.rb +4 -4
- data/lib/fmrest/spyke/relation.rb +130 -13
- metadata +6 -6
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: fe351af8101b6ca643caf8c389106bae00d5fe72aacf9fa8dba586aef31a5797
|
4
|
+
data.tar.gz: e366111a110101ce574522375ef057ca448e5baf0f5ddc3e952d956534df3ec4
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: f3e875be7b1d271a8ab1a56375d73654f47bf27cee4bbb7f45904c78e5a492874317e7b023590a3c255ad089ab02485298ac1511731335a0df40516767e0a8fa
|
7
|
+
data.tar.gz: 122579f2b002c0c1d10e4f5cb935afaace304f43c12051f100200111fdf9982a1559bdd1e2504c50e509d550c99402d9026086ac1ca48f8172b3fc37562042e8
|
data/CHANGELOG.md
CHANGED
data/README.md
CHANGED
@@ -586,8 +586,9 @@ FM Data API reference: https://help.claris.com/en/data-api-guide/
|
|
586
586
|
|
587
587
|
## Supported Ruby versions
|
588
588
|
|
589
|
-
fmrest-ruby is
|
590
|
-
|
589
|
+
The latest fmrest-ruby is
|
590
|
+
[tested against](https://github.com/beezwax/fmrest-ruby/actions?query=workflow%3ACI)
|
591
|
+
Ruby 2.7 through 3.2.
|
591
592
|
|
592
593
|
## Gem development
|
593
594
|
|
@@ -25,7 +25,7 @@ module FmRest
|
|
25
25
|
delegate :limit, :offset, :sort, :order, :query, :match, :omit,
|
26
26
|
:portal, :portals, :includes, :with_all_portals, :without_portals,
|
27
27
|
:script, :find_one, :first, :any, :find_some, :find_in_batches,
|
28
|
-
:find_each, to: :all
|
28
|
+
:find_each, :and, :or, to: :all
|
29
29
|
|
30
30
|
# Spyke override -- Use FmRest's Relation instead of Spyke's vanilla
|
31
31
|
# one
|
@@ -34,9 +34,9 @@ module FmRest
|
|
34
34
|
current_scope || Relation.new(self, uri: uri)
|
35
35
|
end
|
36
36
|
|
37
|
-
# Spyke override --
|
38
|
-
#
|
39
|
-
#
|
37
|
+
# Spyke override -- properly sets limit, offset and other options, as
|
38
|
+
# well as using the appropriate HTTP method/URL depending on whether
|
39
|
+
# there's a query present in the current scope.
|
40
40
|
#
|
41
41
|
# @option options [Boolean] :raise_on_no_matching_records whether to
|
42
42
|
# raise `APIError::NoMatchingRecordsError` when no records match (FM
|
@@ -5,6 +5,18 @@ module FmRest
|
|
5
5
|
class Relation < ::Spyke::Relation
|
6
6
|
SORT_PARAM_MATCHER = /(.*?)(!|__desc(?:end)?)?\Z/.freeze
|
7
7
|
|
8
|
+
# This needs to use four-digit numbers in order to work with Date fields
|
9
|
+
# also, otherwise FileMaker will complain about date formatting
|
10
|
+
ZERO_RESULTS_QUERY = '1001..1000'
|
11
|
+
|
12
|
+
UNSATISFIABLE_QUERY_VALUE =
|
13
|
+
Object.new.tap do |u|
|
14
|
+
def u.inspect; 'Unsatisfiable'; end
|
15
|
+
def u.to_s; ZERO_RESULTS_QUERY; end
|
16
|
+
end.freeze
|
17
|
+
|
18
|
+
NORMALIZED_OMIT_KEY = 'omit'
|
19
|
+
|
8
20
|
class UnknownQueryKey < ArgumentError; end
|
9
21
|
|
10
22
|
# NOTE: We need to keep limit, offset, sort, query and portal accessors
|
@@ -14,7 +26,8 @@ module FmRest
|
|
14
26
|
|
15
27
|
|
16
28
|
attr_accessor :limit_value, :offset_value, :sort_params, :query_params,
|
17
|
-
:
|
29
|
+
:chain_flag, :included_portals, :portal_params,
|
30
|
+
:script_params
|
18
31
|
|
19
32
|
def initialize(*_args)
|
20
33
|
super
|
@@ -204,9 +217,12 @@ module FmRest
|
|
204
217
|
with_clone do |r|
|
205
218
|
params = params.flatten.map { |p| normalize_query_params(p) }
|
206
219
|
|
207
|
-
if r.
|
220
|
+
if r.chain_flag == :or || r.query_params.empty?
|
208
221
|
r.query_params += params
|
209
|
-
r.
|
222
|
+
r.chain_flag = nil
|
223
|
+
elsif r.chain_flag == :and
|
224
|
+
r.cartesian_product_query_params(params)
|
225
|
+
r.chain_flag = nil
|
210
226
|
elsif params.length > r.query_params.length
|
211
227
|
params[0, r.query_params.length].each_with_index do |p, i|
|
212
228
|
r.query_params[i].merge!(p)
|
@@ -229,22 +245,22 @@ module FmRest
|
|
229
245
|
# @return [FmRest::Spyke::Relation] a new relation with the exact match
|
230
246
|
# conditions applied
|
231
247
|
def match(*params)
|
232
|
-
query(transform_query_values(params) { |v| "==#{FmRest::V1.escape_find_operators(v)}" })
|
248
|
+
query(transform_query_values(params) { |v| "==#{FmRest::V1.escape_find_operators(v.to_s)}" })
|
233
249
|
end
|
234
250
|
|
235
|
-
#
|
251
|
+
# Adds a new set of conditions to omit in a find request.
|
236
252
|
#
|
237
|
-
# This is the same as passing `omit: true` to `.
|
253
|
+
# This is the same as passing `omit: true` to `.or`.
|
238
254
|
#
|
239
255
|
# @return [FmRest::Spyke::Relation] a new relation with the given find
|
240
256
|
# conditions applied negated
|
241
257
|
def omit(params)
|
242
|
-
|
258
|
+
self.or(params.merge(omit: true))
|
243
259
|
end
|
244
260
|
|
245
261
|
# Signals that the next query conditions to be set (through `.query`,
|
246
262
|
# `.match`, etc.) should be added as a logical OR relative to previously
|
247
|
-
# set conditions
|
263
|
+
# set conditions.
|
248
264
|
#
|
249
265
|
# In practice this means the JSON query request will have a new
|
250
266
|
# conditions object appended, e.g.:
|
@@ -256,15 +272,91 @@ module FmRest
|
|
256
272
|
# You can call this method with or without parameters. If parameters are
|
257
273
|
# given they will be passed down to `.query` (and those conditions
|
258
274
|
# immediately set), otherwise it just prepares the next
|
259
|
-
# conditions-setting method to use OR.
|
275
|
+
# conditions-setting method (e.g. `match`) to use OR.
|
260
276
|
#
|
261
277
|
# @example
|
262
|
-
# # Add conditions directly
|
278
|
+
# # Add conditions directly on .or call:
|
263
279
|
# Person.query(name: "=Alice").or(name: "=Bob")
|
264
280
|
# # Add exact match conditions through method chaining
|
265
281
|
# Person.match(email: "alice@example.com").or.match(email: "bob@example.com")
|
266
282
|
def or(*params)
|
267
|
-
clone = with_clone { |r| r.
|
283
|
+
clone = with_clone { |r| r.chain_flag = :or }
|
284
|
+
params.empty? ? clone : clone.query(*params)
|
285
|
+
end
|
286
|
+
|
287
|
+
# Signals that the next query conditions to be set (through `.query`,
|
288
|
+
# `.match`, etc.) should be added as a logical AND relative to previously
|
289
|
+
# set conditions.
|
290
|
+
#
|
291
|
+
# In practice this means the given conditions will be applied through
|
292
|
+
# cartesian product onto the previously defined conditions objects in the
|
293
|
+
# JSON query request.
|
294
|
+
#
|
295
|
+
# For example, if you had these conditions:
|
296
|
+
#
|
297
|
+
# ```
|
298
|
+
# [{name: "Alice"}, {name: "Bob"}]
|
299
|
+
# ```
|
300
|
+
#
|
301
|
+
# After calling `.and(age: 20)`, the conditions would look like:
|
302
|
+
#
|
303
|
+
# ```
|
304
|
+
# [{name: "Alice", age: 20}, {name: "Bob", age: 20}]
|
305
|
+
# ```
|
306
|
+
#
|
307
|
+
# Or in pseudocode logical representation:
|
308
|
+
#
|
309
|
+
# ```
|
310
|
+
# (name = "Alice" OR name = "Bob") AND age = 20
|
311
|
+
# ```
|
312
|
+
#
|
313
|
+
# You can also pass multiple condition hashes to `.and`, in which case
|
314
|
+
# it will treat them as OR-separated, e.g.:
|
315
|
+
#
|
316
|
+
# ```
|
317
|
+
# .query({ name: "Alice" }, { name: "Bob" }).and({ age: 20 }, { age: 30 })
|
318
|
+
# ```
|
319
|
+
#
|
320
|
+
# Would result in the following conditions:
|
321
|
+
#
|
322
|
+
# ```
|
323
|
+
# [
|
324
|
+
# {name: "Alice", age: 20 },
|
325
|
+
# {name: "Alice", age: 30 },
|
326
|
+
# {name: "Bob", age: 20 },
|
327
|
+
# {name: "Bob", age: 30 }
|
328
|
+
# ]
|
329
|
+
# ```
|
330
|
+
#
|
331
|
+
# In pseudocode:
|
332
|
+
#
|
333
|
+
# ```
|
334
|
+
# (name = "Alice" OR name = "Bob") AND (age = 20 OR age = 30)
|
335
|
+
# ```
|
336
|
+
#
|
337
|
+
# You can call this method with or without parameters. If parameters are
|
338
|
+
# given they will be passed down to `.query` (and those conditions
|
339
|
+
# immediately set), otherwise it just prepares the next
|
340
|
+
# conditions-setting method (e.g. `match`) to use AND.
|
341
|
+
#
|
342
|
+
# Note that if you use this method on fields that already had conditions
|
343
|
+
# set you may end up with an unsatisfiable condition (e.g. name matches
|
344
|
+
# 'Bob' AND 'Alice' simultaneously). In that case fmrest-ruby will
|
345
|
+
# replace your given values with an expression that's guaranteed to
|
346
|
+
# return zero results, as that is the logically expected result.
|
347
|
+
#
|
348
|
+
# @example
|
349
|
+
# # Add conditions directly on .and call:
|
350
|
+
# Person.query(name: "=Alice").and(city: "=Wonderland")
|
351
|
+
# # Add exact match conditions through method chaining:
|
352
|
+
# Person.match(name: "Alice").and.match(city: "Wonderland")
|
353
|
+
# # With multiple condition hashes:
|
354
|
+
# Person.query(name: "=Alice").and({ city: "=Wonderland" }, { city: "=London" })
|
355
|
+
# # With conflicting criteria:
|
356
|
+
# Person.match(name: "Alice").and.match(name: "Bob")
|
357
|
+
# # => JSON: { "name": "1001..1000" } -> forced empty result set
|
358
|
+
def and(*params)
|
359
|
+
clone = with_clone { |r| r.chain_flag = :and }
|
268
360
|
params.empty? ? clone : clone.query(*params)
|
269
361
|
end
|
270
362
|
|
@@ -397,8 +489,33 @@ module FmRest
|
|
397
489
|
end
|
398
490
|
end
|
399
491
|
|
492
|
+
def cartesian_product_query_params(params)
|
493
|
+
if (query_params + params).any? { |p| p.key?(NORMALIZED_OMIT_KEY) }
|
494
|
+
raise ArgumentError, "Cannot use `and' with queries containing `omit'"
|
495
|
+
end
|
496
|
+
|
497
|
+
self.query_params =
|
498
|
+
query_params
|
499
|
+
.product(params)
|
500
|
+
.map { |a, b| a.merge(b) { |k, v1, v2| v1 == v2 ? v1 : unsatisfiable(k, v1, v2) } }
|
501
|
+
end
|
502
|
+
|
400
503
|
private
|
401
504
|
|
505
|
+
def unsatisfiable(field, a, b)
|
506
|
+
unless a == UNSATISFIABLE_QUERY_VALUE || b == UNSATISFIABLE_QUERY_VALUE
|
507
|
+
# TODO: Add a setting to make this an exception instead of a warning?
|
508
|
+
warn(
|
509
|
+
"An FmRest query using `and' required that `#{field}' match " \
|
510
|
+
"'#{a}' and '#{b}' at the same time which can't be satisified. " \
|
511
|
+
"This will appear in the find request as '#{UNSATISFIABLE_QUERY_VALUE}' " \
|
512
|
+
"and may result in an empty resultset."
|
513
|
+
)
|
514
|
+
end
|
515
|
+
|
516
|
+
UNSATISFIABLE_QUERY_VALUE
|
517
|
+
end
|
518
|
+
|
402
519
|
def normalize_sort_param(param)
|
403
520
|
if param.kind_of?(Symbol) || param.kind_of?(String)
|
404
521
|
_, attr, descend = param.to_s.match(SORT_PARAM_MATCHER).to_a
|
@@ -432,10 +549,10 @@ module FmRest
|
|
432
549
|
|
433
550
|
def normalize_query_params(params)
|
434
551
|
params.each_with_object({}) do |(k, v), normalized|
|
435
|
-
if k == :omit || k ==
|
552
|
+
if k == :omit || k == NORMALIZED_OMIT_KEY
|
436
553
|
# FM Data API wants omit values as strings, e.g. "true" or "false"
|
437
554
|
# rather than true/false
|
438
|
-
normalized[
|
555
|
+
normalized[NORMALIZED_OMIT_KEY] = v.to_s
|
439
556
|
next
|
440
557
|
end
|
441
558
|
|
metadata
CHANGED
@@ -1,14 +1,14 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: fmrest-spyke
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: 0.
|
4
|
+
version: 0.25.0.rc1
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- Pedro Carbajal
|
8
8
|
autorequire:
|
9
9
|
bindir: bin
|
10
10
|
cert_chain: []
|
11
|
-
date:
|
11
|
+
date: 2023-04-13 00:00:00.000000000 Z
|
12
12
|
dependencies:
|
13
13
|
- !ruby/object:Gem::Dependency
|
14
14
|
name: fmrest-core
|
@@ -16,14 +16,14 @@ dependencies:
|
|
16
16
|
requirements:
|
17
17
|
- - '='
|
18
18
|
- !ruby/object:Gem::Version
|
19
|
-
version: 0.
|
19
|
+
version: 0.25.0.rc1
|
20
20
|
type: :runtime
|
21
21
|
prerelease: false
|
22
22
|
version_requirements: !ruby/object:Gem::Requirement
|
23
23
|
requirements:
|
24
24
|
- - '='
|
25
25
|
- !ruby/object:Gem::Version
|
26
|
-
version: 0.
|
26
|
+
version: 0.25.0.rc1
|
27
27
|
- !ruby/object:Gem::Dependency
|
28
28
|
name: spyke
|
29
29
|
requirement: !ruby/object:Gem::Requirement
|
@@ -88,9 +88,9 @@ required_ruby_version: !ruby/object:Gem::Requirement
|
|
88
88
|
version: '0'
|
89
89
|
required_rubygems_version: !ruby/object:Gem::Requirement
|
90
90
|
requirements:
|
91
|
-
- - "
|
91
|
+
- - ">"
|
92
92
|
- !ruby/object:Gem::Version
|
93
|
-
version:
|
93
|
+
version: 1.3.1
|
94
94
|
requirements: []
|
95
95
|
rubygems_version: 3.3.3
|
96
96
|
signing_key:
|