fmrest-spyke 0.14.0 → 0.15.0
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 +4 -4
- data/CHANGELOG.md +5 -0
- data/README.md +27 -11
- data/lib/fmrest/spyke.rb +1 -7
- data/lib/fmrest/spyke/model/orm.rb +4 -4
- data/lib/fmrest/spyke/model/serialization.rb +4 -26
- data/lib/fmrest/spyke/relation.rb +183 -6
- metadata +4 -4
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 48d4905462bd97acb37ec58ef98ef8ef58cc4362aa52adfc496de521231f8937
|
4
|
+
data.tar.gz: f99542f9578cfdbb894f4a9a5e6635da077f272f7cfa73911cc1e1563604b7e1
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 4a9c6772950abe865cf776c4cf3bba052bdb49f744429e475555d18e16289c39ed14a65e8251ecceb324d0ddc5d96c637dc1ce16e924af20dd44135fac41dbd6
|
7
|
+
data.tar.gz: bf8b8ab7ec6b55daf2ba55a5930fcb2323cff7823853c671139ec1e30feda7a7bc81657ab511ef0aa75730e4e98e95d7562df3f2a44252a8bd550ad9c749d584
|
data/CHANGELOG.md
CHANGED
@@ -1,5 +1,10 @@
|
|
1
1
|
## Changelog
|
2
2
|
|
3
|
+
### 0.15.0
|
4
|
+
|
5
|
+
* Much improved querying API (see documentation on querying), adding new
|
6
|
+
`.query` capabilities, as well as two new methods: `.match` and `.or`
|
7
|
+
|
3
8
|
### 0.14.0
|
4
9
|
|
5
10
|
* Aliased `FmRest::Spyke::Base` as `FmRest::Layout` (now preferred), and
|
data/README.md
CHANGED
@@ -54,13 +54,28 @@ class Honeybee < FmRest::Layout("Honeybees Web")
|
|
54
54
|
}
|
55
55
|
|
56
56
|
# Mapped attributes
|
57
|
-
attributes name: "Bee Name", age: "Bee Age"
|
57
|
+
attributes name: "Bee Name", age: "Bee Age", created_on: "Created On"
|
58
58
|
|
59
|
-
#
|
60
|
-
has_portal :
|
59
|
+
# Portal associations
|
60
|
+
has_portal :tasks
|
61
61
|
|
62
|
-
# File
|
62
|
+
# File containers
|
63
63
|
container :photo, field_name: "Bee Photo"
|
64
|
+
|
65
|
+
# Scopes
|
66
|
+
scope :can_legally_fly, -> { query(age: ">18") }
|
67
|
+
|
68
|
+
# Client-side validations
|
69
|
+
validates :name, presence: true
|
70
|
+
|
71
|
+
# Callbacks
|
72
|
+
before_save :set_created_on
|
73
|
+
|
74
|
+
private
|
75
|
+
|
76
|
+
def set_created_on
|
77
|
+
self.created_on = Date.today
|
78
|
+
end
|
64
79
|
end
|
65
80
|
|
66
81
|
# Find a record by id
|
@@ -69,7 +84,7 @@ bee = Honeybee.find(9)
|
|
69
84
|
bee.name = "Hutch"
|
70
85
|
|
71
86
|
# Add a new record to portal
|
72
|
-
bee.
|
87
|
+
bee.tasks.build(urgency: "Today")
|
73
88
|
|
74
89
|
bee.save
|
75
90
|
```
|
@@ -129,9 +144,9 @@ You can also pass a `:log` option for basic request logging, see the section on
|
|
129
144
|
Option | Description | Format | Default
|
130
145
|
--------------------|--------------------------------------------|-----------------------------|--------
|
131
146
|
`:host` | Hostname with optional port, e.g. `"example.com:9000"` | String | None
|
132
|
-
`:database` |
|
133
|
-
`:username` |
|
134
|
-
`:password` |
|
147
|
+
`:database` | The name of the database to connect to | String | None
|
148
|
+
`:username` | A Data API-ready account | String | None
|
149
|
+
`:password` | Your password | String | None
|
135
150
|
`:account_name` | Alias of `:username` | String | None
|
136
151
|
`:ssl` | SSL options to be forwarded to Faraday | Faraday SSL options | None
|
137
152
|
`:proxy` | Proxy options to be forwarded to Faraday | Faraday proxy options | None
|
@@ -288,7 +303,8 @@ class Honeybee < FmRest::Layout
|
|
288
303
|
end
|
289
304
|
```
|
290
305
|
|
291
|
-
Alternatively, you
|
306
|
+
Alternatively, if you're inheriting from `FmRest::Layout` directly you can set
|
307
|
+
the layout name in the class definition line:
|
292
308
|
|
293
309
|
```ruby
|
294
310
|
class Honeybee < FmRest::Layout("Honeybees Web")
|
@@ -388,8 +404,8 @@ Guides](https://guides.rubyonrails.org/active_model_basics.html#dirty).
|
|
388
404
|
Since Spyke is API-agnostic it only provides a wide-purpose `.where` method for
|
389
405
|
passing arbitrary parameters to the REST backend. fmrest-ruby however is well
|
390
406
|
aware of its backend API, so it extends Spkye models with a bunch of useful
|
391
|
-
querying methods: `.query`, `.
|
392
|
-
etc.
|
407
|
+
querying methods: `.query`, `.match`, `.omit`, `.limit`, `.offset`, `.sort`,
|
408
|
+
`.portal`, `.script`, etc.
|
393
409
|
|
394
410
|
See the [main document on querying](docs/Querying.md) for detailed information
|
395
411
|
on the query API methods.
|
data/lib/fmrest/spyke.rb
CHANGED
@@ -1,12 +1,6 @@
|
|
1
1
|
# frozen_string_literal: true
|
2
2
|
|
3
|
-
|
4
|
-
require "spyke"
|
5
|
-
rescue LoadError => e
|
6
|
-
e.message << " (Did you include Spyke in your Gemfile?)" unless e.message.frozen?
|
7
|
-
raise e
|
8
|
-
end
|
9
|
-
|
3
|
+
require "spyke"
|
10
4
|
require "fmrest"
|
11
5
|
require "fmrest/spyke/spyke_formatter"
|
12
6
|
require "fmrest/spyke/model"
|
@@ -26,10 +26,10 @@ module FmRest
|
|
26
26
|
|
27
27
|
class_methods do
|
28
28
|
# Methods delegated to `FmRest::Spyke::Relation`
|
29
|
-
delegate :limit, :offset, :sort, :order, :query, :
|
30
|
-
|
31
|
-
|
32
|
-
|
29
|
+
delegate :limit, :offset, :sort, :order, :query, :match, :omit,
|
30
|
+
:portal, :portals, :includes, :with_all_portals, :without_portals,
|
31
|
+
:script, :find_one, :first, :any, :find_some, :find_in_batches,
|
32
|
+
:find_each, to: :all
|
33
33
|
|
34
34
|
# Spyke override -- Use FmRest's Relation instead of Spyke's vanilla
|
35
35
|
# one
|
@@ -4,9 +4,6 @@ module FmRest
|
|
4
4
|
module Spyke
|
5
5
|
module Model
|
6
6
|
module Serialization
|
7
|
-
FM_DATE_FORMAT = "%m/%d/%Y"
|
8
|
-
FM_DATETIME_FORMAT = "#{FM_DATE_FORMAT} %H:%M:%S"
|
9
|
-
|
10
7
|
# Spyke override -- Return FM Data API's expected JSON format,
|
11
8
|
# including only modified fields.
|
12
9
|
#
|
@@ -79,10 +76,10 @@ module FmRest
|
|
79
76
|
def serialize_values!(params)
|
80
77
|
params.transform_values! do |value|
|
81
78
|
case value
|
82
|
-
when *datetime_classes
|
83
|
-
convert_datetime_timezone(value.to_datetime).strftime(FM_DATETIME_FORMAT)
|
84
|
-
when *date_classes
|
85
|
-
value.strftime(FM_DATE_FORMAT)
|
79
|
+
when *FmRest::V1.datetime_classes
|
80
|
+
FmRest::V1.convert_datetime_timezone(value.to_datetime, fmrest_config.timezone).strftime(FmRest::V1::Dates::FM_DATETIME_FORMAT)
|
81
|
+
when *FmRest::V1.date_classes
|
82
|
+
value.strftime(FmRest::V1::Dates::FM_DATE_FORMAT)
|
86
83
|
else
|
87
84
|
value
|
88
85
|
end
|
@@ -90,25 +87,6 @@ module FmRest
|
|
90
87
|
|
91
88
|
params
|
92
89
|
end
|
93
|
-
|
94
|
-
def convert_datetime_timezone(dt)
|
95
|
-
case fmrest_config.timezone
|
96
|
-
when :utc, "utc"
|
97
|
-
dt.new_offset(0)
|
98
|
-
when :local, "local"
|
99
|
-
dt.new_offset(FmRest::V1.local_offset_for_datetime(dt))
|
100
|
-
when nil
|
101
|
-
dt
|
102
|
-
end
|
103
|
-
end
|
104
|
-
|
105
|
-
def datetime_classes
|
106
|
-
[DateTime, Time, defined?(FmRest::StringDateTime) && FmRest::StringDateTime].compact
|
107
|
-
end
|
108
|
-
|
109
|
-
def date_classes
|
110
|
-
[Date, defined?(FmRest::StringDate) && FmRest::StringDate].compact
|
111
|
-
end
|
112
90
|
end
|
113
91
|
end
|
114
92
|
end
|
@@ -5,6 +5,8 @@ module FmRest
|
|
5
5
|
class Relation < ::Spyke::Relation
|
6
6
|
SORT_PARAM_MATCHER = /(.*?)(!|__desc(?:end)?)?\Z/.freeze
|
7
7
|
|
8
|
+
class UnknownQueryKey < ArgumentError; end
|
9
|
+
|
8
10
|
# NOTE: We need to keep limit, offset, sort, query and portal accessors
|
9
11
|
# separate from regular params because FM Data API uses either "limit" or
|
10
12
|
# "_limit" (or "_offset", etc.) as param keys depending on the type of
|
@@ -12,7 +14,7 @@ module FmRest
|
|
12
14
|
|
13
15
|
|
14
16
|
attr_accessor :limit_value, :offset_value, :sort_params, :query_params,
|
15
|
-
:included_portals, :portal_params, :script_params
|
17
|
+
:or_flag, :included_portals, :portal_params, :script_params
|
16
18
|
|
17
19
|
def initialize(*_args)
|
18
20
|
super
|
@@ -161,16 +163,111 @@ module FmRest
|
|
161
163
|
portal(false)
|
162
164
|
end
|
163
165
|
|
166
|
+
# Sets conditions for a find request. Conditions must be given in
|
167
|
+
# `{ field: condition }` format, where `condition` is normally a string
|
168
|
+
# sent raw to the Data API server, so you can use FileMaker find
|
169
|
+
# operators. You can also pass Ruby range or date/datetime objects for
|
170
|
+
# condition values, and they'll be converted to the appropriate Data API
|
171
|
+
# representation.
|
172
|
+
#
|
173
|
+
# Passing `omit: true` in a conditions set will negate all conditions in
|
174
|
+
# that set.
|
175
|
+
#
|
176
|
+
# You can modify the way conditions are added (i.e. through logical AND
|
177
|
+
# or OR) by pre-chaining `.or`. By default it adds conditions through
|
178
|
+
# logical AND.
|
179
|
+
#
|
180
|
+
# Note that because of the way the Data API works, logical AND conditions
|
181
|
+
# on a single field are not possible. Because of that, if you try to set
|
182
|
+
# two AND conditions for the same field, the previously existing one will
|
183
|
+
# be overwritten with the new condition.
|
184
|
+
#
|
185
|
+
# It is recommended that you learn how the Data API represents conditions
|
186
|
+
# in its find requests (i.e. an array of JSON objects with conditions on
|
187
|
+
# fields). This method internally uses that same representation, which
|
188
|
+
# you can view by inspecting the resulting relations. Understanding that
|
189
|
+
# representation will also make the limitations of this Ruby API clear.
|
190
|
+
#
|
191
|
+
# @example
|
192
|
+
# Person.query(name: "=Alice") # Simple query
|
193
|
+
# Person.query(age: (20..29)) # Query using a Ruby range
|
194
|
+
# Person.query(created_on: Date.today..Date.today-1)
|
195
|
+
# Person.query(name: "=Alice", age: ">20") # Query multiple fields (logical AND)
|
196
|
+
# Person.query(name: "=Alice").query(age: ">20") # Same effect as above example
|
197
|
+
# Person.query(name: "=Bob", omit: true) # Negate a query (i.e. find people not named Bob)
|
198
|
+
# Person.query(pets: { name: "=Snuggles" }) # Query portal fields
|
199
|
+
# Person.query({ name: "=Alice" }, { name: "=Bob" }) # Separate conditions through logical OR
|
200
|
+
# Person.query(name: "=Alice").or.query(name: "=Bob") # Same effect as above example
|
201
|
+
# @return [FmRest::Spyke::Relation] a new relation with the given find
|
202
|
+
# conditions applied
|
164
203
|
def query(*params)
|
165
204
|
with_clone do |r|
|
166
|
-
|
205
|
+
params = params.flatten.map { |p| normalize_query_params(p) }
|
206
|
+
|
207
|
+
if r.or_flag || r.query_params.empty?
|
208
|
+
r.query_params += params
|
209
|
+
r.or_flag = nil
|
210
|
+
elsif params.length > r.query_params.length
|
211
|
+
params[0, r.query_params.length].each_with_index do |p, i|
|
212
|
+
r.query_params[i].merge!(p)
|
213
|
+
end
|
214
|
+
|
215
|
+
remainder = params.length - r.query_params.length
|
216
|
+
r.query_params += params[-remainder, remainder]
|
217
|
+
else
|
218
|
+
params.each_with_index { |p, i| r.query_params[i].merge!(p) }
|
219
|
+
end
|
167
220
|
end
|
168
221
|
end
|
169
222
|
|
223
|
+
# Similar to `.query`, but sets exact string match queries (i.e.
|
224
|
+
# prefixes queries with ==) and escapes find operators in the given
|
225
|
+
# queries using `FmRest.e`.
|
226
|
+
#
|
227
|
+
# @example
|
228
|
+
# Person.query(email: "bob@example.com") # Find exact email
|
229
|
+
# @return [FmRest::Spyke::Relation] a new relation with the exact match
|
230
|
+
# conditions applied
|
231
|
+
def match(*params)
|
232
|
+
query(transform_query_values(params) { |v| "==#{FmRest::V1.escape_find_operators(v)}" })
|
233
|
+
end
|
234
|
+
|
235
|
+
# Negated version of `.query`, sets conditions to omit in a find request.
|
236
|
+
#
|
237
|
+
# This is the same as passing `omit: true` to `.query`.
|
238
|
+
#
|
239
|
+
# @return [FmRest::Spyke::Relation] a new relation with the given find
|
240
|
+
# conditions applied negated
|
170
241
|
def omit(params)
|
171
242
|
query(params.merge(omit: true))
|
172
243
|
end
|
173
244
|
|
245
|
+
# Signals that the next query conditions to be set (through `.query`,
|
246
|
+
# `.match`, etc.) should be added as a logical OR relative to previously
|
247
|
+
# set conditions (rather than the default AND).
|
248
|
+
#
|
249
|
+
# In practice this means the JSON query request will have a new
|
250
|
+
# conditions object appended, e.g.:
|
251
|
+
#
|
252
|
+
# ```
|
253
|
+
# {"query": [{"field": "condition"}, {"field": "OR-added condition"}]}
|
254
|
+
# ```
|
255
|
+
#
|
256
|
+
# You can call this method with or without parameters. If parameters are
|
257
|
+
# given they will be passed down to `.query` (and those conditions
|
258
|
+
# immediately set), otherwise it just prepares the next
|
259
|
+
# conditions-setting method to use OR.
|
260
|
+
#
|
261
|
+
# @example
|
262
|
+
# # Add conditions directly in .or call:
|
263
|
+
# Person.query(name: "=Alice").or(name: "=Bob")
|
264
|
+
# # Add exact match conditions through method chaining
|
265
|
+
# Person.match(email: "alice@example.com").or.match(email: "bob@example.com")
|
266
|
+
def or(*params)
|
267
|
+
clone = with_clone { |r| r.or_flag = true }
|
268
|
+
params.empty? ? clone : clone.query(*params)
|
269
|
+
end
|
270
|
+
|
174
271
|
# @return [Boolean] whether a query was set on this relation
|
175
272
|
def has_query?
|
176
273
|
query_params.present?
|
@@ -328,11 +425,91 @@ module FmRest
|
|
328
425
|
next
|
329
426
|
end
|
330
427
|
|
331
|
-
#
|
332
|
-
if
|
333
|
-
|
428
|
+
# Portal fields query (nested hash), e.g. { contact: { name: "Hutch" } }
|
429
|
+
if v.kind_of?(Hash)
|
430
|
+
if k.kind_of?(Symbol)
|
431
|
+
portal_key, = klass.portal_options.find { |_, opts| opts[:name].to_s == k.to_s }
|
432
|
+
|
433
|
+
if portal_key
|
434
|
+
portal_model = klass.associations[k].klass
|
435
|
+
|
436
|
+
portal_normalized = v.each_with_object({}) do |(pk, pv), h|
|
437
|
+
normalize_single_query_param_for_model(portal_model, pk, pv, h)
|
438
|
+
end
|
439
|
+
|
440
|
+
normalized.merge!(portal_normalized.transform_keys { |pf| "#{portal_key}::#{pf}" })
|
441
|
+
else
|
442
|
+
raise UnknownQueryKey, "No portal matches the query key `:#{k}` on #{klass.name}. If you are trying to use the literal string '#{k}' pass it as a string instead of a symbol."
|
443
|
+
end
|
444
|
+
else
|
445
|
+
normalized.merge!(v.transform_keys { |pf| "#{k}::#{pf}" })
|
446
|
+
end
|
447
|
+
|
448
|
+
next
|
449
|
+
end
|
450
|
+
|
451
|
+
# Attribute query (scalar values), e.g. { name: "Hutch" }
|
452
|
+
normalize_single_query_param_for_model(klass, k, v, normalized)
|
453
|
+
end
|
454
|
+
end
|
455
|
+
|
456
|
+
def normalize_single_query_param_for_model(model, k, v, hash)
|
457
|
+
if k.kind_of?(Symbol)
|
458
|
+
if model.mapped_attributes.has_key?(k)
|
459
|
+
hash[model.mapped_attributes[k].to_s] = format_query_condition(v)
|
460
|
+
else
|
461
|
+
raise UnknownQueryKey, "No attribute matches the query key `:#{k}` on #{model.name}. If you are trying to use the literal string '#{k}' pass it as a string instead of a symbol."
|
462
|
+
end
|
463
|
+
else
|
464
|
+
hash[k.to_s] = format_query_condition(v)
|
465
|
+
end
|
466
|
+
end
|
467
|
+
|
468
|
+
# Transforms various Ruby data types to FileMaker search condition
|
469
|
+
# strings
|
470
|
+
#
|
471
|
+
def format_query_condition(condition)
|
472
|
+
case condition
|
473
|
+
when nil
|
474
|
+
"=" # Search for empty field
|
475
|
+
when Range
|
476
|
+
format_range_condition(condition)
|
477
|
+
when *FmRest::V1.datetime_classes
|
478
|
+
FmRest::V1.convert_datetime_timezone(condition.to_datetime, klass.fmrest_config.timezone)
|
479
|
+
.strftime(FmRest::V1::Dates::FM_DATETIME_FORMAT)
|
480
|
+
when *FmRest::V1.date_classes
|
481
|
+
condition.strftime(FmRest::V1::Dates::FM_DATE_FORMAT)
|
482
|
+
else
|
483
|
+
condition
|
484
|
+
end
|
485
|
+
end
|
486
|
+
|
487
|
+
def format_range_condition(range)
|
488
|
+
if range.first.kind_of?(Numeric)
|
489
|
+
if range.first == Float::INFINITY || range.end == -Float::INFINITY
|
490
|
+
raise ArgumentError, "Can't search for a range that begins at +Infinity or ends at -Infinity"
|
491
|
+
elsif range.first == -Float::INFINITY
|
492
|
+
if range.end == Float::INFINITY || range.end.nil?
|
493
|
+
"*" # Search for non-empty field
|
494
|
+
else
|
495
|
+
range.exclude_end? ? "<#{range.end}" : "<=#{range.end}"
|
496
|
+
end
|
497
|
+
elsif range.end == Float::INFINITY || range.end.nil?
|
498
|
+
">=#{range.first}"
|
499
|
+
elsif range.exclude_end? && range.last.respond_to?(:pred)
|
500
|
+
"#{range.first}..#{range.last.pred}"
|
334
501
|
else
|
335
|
-
|
502
|
+
"#{range.first}..#{range.last}"
|
503
|
+
end
|
504
|
+
else
|
505
|
+
"#{format_query_condition(range.first)}..#{format_query_condition(range.last)}"
|
506
|
+
end
|
507
|
+
end
|
508
|
+
|
509
|
+
def transform_query_values(*params, &block)
|
510
|
+
params.flatten.map do |p|
|
511
|
+
p.transform_values do |v|
|
512
|
+
v.kind_of?(Hash) ? v.transform_values(&block) : yield(v)
|
336
513
|
end
|
337
514
|
end
|
338
515
|
end
|
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.15.0
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- Pedro Carbajal
|
8
8
|
autorequire:
|
9
9
|
bindir: bin
|
10
10
|
cert_chain: []
|
11
|
-
date: 2021-
|
11
|
+
date: 2021-04-01 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.15.0
|
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.15.0
|
27
27
|
- !ruby/object:Gem::Dependency
|
28
28
|
name: spyke
|
29
29
|
requirement: !ruby/object:Gem::Requirement
|