Class: FmRest::Spyke::Relation
- Inherits:
-
Spyke::Relation
- Object
- Spyke::Relation
- FmRest::Spyke::Relation
- Defined in:
- lib/fmrest/spyke/relation.rb
Defined Under Namespace
Classes: UnknownQueryKey
Constant Summary collapse
- SORT_PARAM_MATCHER =
/(.*?)(!|__desc(?:end)?)?\Z/.freeze
- ZERO_RESULTS_QUERY =
This needs to use four-digit numbers in order to work with Date fields also, otherwise FileMaker will complain about date formatting
'1001..1000'
- UNSATISFIABLE_QUERY_VALUE =
Object.new.tap do |u| def u.inspect; 'Unsatisfiable'; end def u.to_s; ZERO_RESULTS_QUERY; end end.freeze
- NORMALIZED_OMIT_KEY =
'omit'
Instance Attribute Summary collapse
-
#chain_flag ⇒ Object
Returns the value of attribute chain_flag.
-
#included_portals ⇒ Object
Returns the value of attribute included_portals.
-
#limit_value ⇒ Object
Returns the value of attribute limit_value.
-
#offset_value ⇒ Object
Returns the value of attribute offset_value.
-
#portal_params ⇒ Object
Returns the value of attribute portal_params.
-
#query_params ⇒ Object
Returns the value of attribute query_params.
-
#script_params ⇒ Object
Returns the value of attribute script_params.
-
#sort_params ⇒ Object
Returns the value of attribute sort_params.
Instance Method Summary collapse
-
#and(*params) ⇒ Object
Signals that the next query conditions to be set (through
.query
,.match
, etc.) should be added as a logical AND relative to previously set conditions. -
#find_each(batch_size: 1000) ⇒ Enumerator
Looping through a collection of records from the database (using the #all method, for example) is very inefficient since it will fetch and instantiate all the objects at once.
-
#find_in_batches(batch_size: 1000) ⇒ Enumerator
Yields each batch of records that was found by the find options.
-
#find_one(options = {}) ⇒ FmRest::Spyke::Base
(also: #first, #any)
Finds a single instance of the model by forcing limit = 1, or simply fetching the record by id if the primary key was set.
-
#find_one!(options = {}) ⇒ FmRest::Spyke::Base
(also: #first!)
Same as
#find_one
, but raisesAPIError::NoMatchingRecordsError
when no records match. -
#has_query? ⇒ Boolean
Whether a query was set on this relation.
-
#initialize(*_args) ⇒ Relation
constructor
A new instance of Relation.
-
#limit(value_or_hash) ⇒ FmRest::Spyke::Relation
A new relation with the limits applied.
-
#match(*params) ⇒ FmRest::Spyke::Relation
Similar to
.query
, but sets exact string match queries (i.e. prefixes queries with ==) and escapes find operators in the given queries usingFmRest.e
. -
#offset(value_or_hash) ⇒ FmRest::Spyke::Relation
A new relation with the offsets applied.
-
#omit(params) ⇒ FmRest::Spyke::Relation
Adds a new set of conditions to omit in a find request.
-
#or(*params) ⇒ Object
Signals that the next query conditions to be set (through
.query
,.match
, etc.) should be added as a logical OR relative to previously set conditions. -
#portal(*args) ⇒ FmRest::Spyke::Relation
(also: #includes, #portals)
Sets the portals to include with each record in the response.
-
#query(*params) ⇒ FmRest::Spyke::Relation
Sets conditions for a find request.
-
#script(options) ⇒ FmRest::Spyke::Relation
A new relation with the script options applied.
-
#sort(*args) ⇒ FmRest::Spyke::Relation
(also: #order)
Allows sort params given in either hash format (using FM Data API's format), or as a symbol, in which case the of the attribute must match a known mapped attribute, optionally suffixed with
!
or__desc
to signify it should use descending order. -
#with_all_portals ⇒ FmRest::Spyke::Relation
Same as calling
portal(true)
. -
#without_portals ⇒ FmRest::Spyke::Relation
Same as calling
portal(false)
.
Constructor Details
#initialize(*_args) ⇒ Relation
Returns a new instance of Relation.
32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 |
# File 'lib/fmrest/spyke/relation.rb', line 32 def initialize(*_args) super @limit_value = klass.default_limit if klass.default_sort.present? @sort_params = Array.wrap(klass.default_sort).map { |s| normalize_sort_param(s) } end @query_params = [] @included_portals = nil @portal_params = {} @script_params = {} end |
Instance Attribute Details
#chain_flag ⇒ Object
Returns the value of attribute chain_flag.
28 29 30 |
# File 'lib/fmrest/spyke/relation.rb', line 28 def chain_flag @chain_flag end |
#included_portals ⇒ Object
Returns the value of attribute included_portals.
28 29 30 |
# File 'lib/fmrest/spyke/relation.rb', line 28 def included_portals @included_portals end |
#limit_value ⇒ Object
Returns the value of attribute limit_value.
28 29 30 |
# File 'lib/fmrest/spyke/relation.rb', line 28 def limit_value @limit_value end |
#offset_value ⇒ Object
Returns the value of attribute offset_value.
28 29 30 |
# File 'lib/fmrest/spyke/relation.rb', line 28 def offset_value @offset_value end |
#portal_params ⇒ Object
Returns the value of attribute portal_params.
28 29 30 |
# File 'lib/fmrest/spyke/relation.rb', line 28 def portal_params @portal_params end |
#query_params ⇒ Object
Returns the value of attribute query_params.
28 29 30 |
# File 'lib/fmrest/spyke/relation.rb', line 28 def query_params @query_params end |
#script_params ⇒ Object
Returns the value of attribute script_params.
28 29 30 |
# File 'lib/fmrest/spyke/relation.rb', line 28 def script_params @script_params end |
#sort_params ⇒ Object
Returns the value of attribute sort_params.
28 29 30 |
# File 'lib/fmrest/spyke/relation.rb', line 28 def sort_params @sort_params end |
Instance Method Details
#and(*params) ⇒ Object
Signals that the next query conditions to be set (through .query
,
.match
, etc.) should be added as a logical AND relative to previously
set conditions.
In practice this means the given conditions will be applied through cartesian product onto the previously defined conditions objects in the JSON query request.
For example, if you had these conditions:
[{name: "Alice"}, {name: "Bob"}]
After calling .and(age: 20)
, the conditions would look like:
[{name: "Alice", age: 20}, {name: "Bob", age: 20}]
Or in pseudocode logical representation:
(name = "Alice" OR name = "Bob") AND age = 20
You can also pass multiple condition hashes to .and
, in which case
it will treat them as OR-separated, e.g.:
.query({ name: "Alice" }, { name: "Bob" }).and({ age: 20 }, { age: 30 })
Would result in the following conditions:
[
{name: "Alice", age: 20 },
{name: "Alice", age: 30 },
{name: "Bob", age: 20 },
{name: "Bob", age: 30 }
]
In pseudocode:
(name = "Alice" OR name = "Bob") AND (age = 20 OR age = 30)
You can call this method with or without parameters. If parameters are
given they will be passed down to .query
(and those conditions
immediately set), otherwise it just prepares the next
conditions-setting method (e.g. match
) to use AND.
Note that if you use this method on fields that already had conditions set you may end up with an unsatisfiable condition (e.g. name matches 'Bob' AND 'Alice' simultaneously). In that case fmrest-ruby will replace your given values with an expression that's guaranteed to return zero results, as that is the logically expected result.
358 359 360 361 |
# File 'lib/fmrest/spyke/relation.rb', line 358 def and(*params) clone = with_clone { |r| r.chain_flag = :and } params.empty? ? clone : clone.query(*params) end |
#find_each(batch_size: 1000) ⇒ Enumerator
Looping through a collection of records from the database (using the
all method, for example) is very inefficient since it will fetch and
instantiate all the objects at once.
In that case, batch processing methods allow you to work with the records in batches, thereby greatly reducing memory consumption and be lighter on the Data API server.
The find_each method uses #find_in_batches with a batch size of 1000 (or as specified by the :batch_size option).
NOTE: By its nature, batch processing is subject to race conditions if other processes are modifying the database
458 459 460 461 462 463 464 465 466 467 468 |
# File 'lib/fmrest/spyke/relation.rb', line 458 def find_each(batch_size: 1000) unless block_given? return to_enum(:find_each, batch_size: batch_size) do limit(1).find_some..data_info.found_count end end find_in_batches(batch_size: batch_size) do |records| records.each { |r| yield r } end end |
#find_in_batches(batch_size: 1000) ⇒ Enumerator
Yields each batch of records that was found by the find options.
NOTE: By its nature, batch processing is subject to race conditions if other processes are modifying the database
406 407 408 409 410 411 412 413 414 415 416 417 418 419 420 421 422 423 424 425 426 427 428 429 430 431 432 |
# File 'lib/fmrest/spyke/relation.rb', line 406 def find_in_batches(batch_size: 1000) unless block_given? return to_enum(:find_in_batches, batch_size: batch_size) do total = limit(1).find_some..data_info.found_count (total - 1).div(batch_size) + 1 end end offset = 1 # DAPI offset is 1-based loop do relation = offset(offset).limit(batch_size) records = relation.find_some yield records if records.length > 0 break if records.length < batch_size # Save one iteration if the total is a multiple of batch_size if found_count = records..data_info && records..data_info.found_count break if found_count == (offset - 1) + batch_size end offset += batch_size end end |
#find_one(options = {}) ⇒ FmRest::Spyke::Base Also known as: first, any
Finds a single instance of the model by forcing limit = 1, or simply fetching the record by id if the primary key was set
374 375 376 377 378 379 380 381 382 383 |
# File 'lib/fmrest/spyke/relation.rb', line 374 def find_one( = {}) @find_one ||= if primary_key_set? without_collection_params { super() } else klass.new_collection_from_result(limit(1).fetch()).first end rescue ::Spyke::ConnectionError => error fallback_or_reraise(error, default: nil) end |
#find_one!(options = {}) ⇒ FmRest::Spyke::Base Also known as: first!
Same as #find_one
, but raises APIError::NoMatchingRecordsError
when
no records match.
Equivalent to calling find_one(raise_on_no_matching_records: true)
.
394 395 396 |
# File 'lib/fmrest/spyke/relation.rb', line 394 def find_one!( = {}) find_one(.merge(raise_on_no_matching_records: true)) end |
#has_query? ⇒ Boolean
Returns whether a query was set on this relation.
364 365 366 |
# File 'lib/fmrest/spyke/relation.rb', line 364 def has_query? query_params.present? end |
#limit(value_or_hash) ⇒ FmRest::Spyke::Relation
Returns a new relation with the limits applied.
90 91 92 93 94 95 96 97 98 |
# File 'lib/fmrest/spyke/relation.rb', line 90 def limit(value_or_hash) with_clone do |r| if value_or_hash.respond_to?(:each) r.set_portal_params(value_or_hash, :limit) else r.limit_value = value_or_hash end end end |
#match(*params) ⇒ FmRest::Spyke::Relation
Similar to .query
, but sets exact string match queries (i.e.
prefixes queries with ==) and escapes find operators in the given
queries using FmRest.e
.
247 248 249 |
# File 'lib/fmrest/spyke/relation.rb', line 247 def match(*params) query(transform_query_values(params) { |v| "==#{FmRest::V1.escape_find_operators(v.to_s)}" }) end |
#offset(value_or_hash) ⇒ FmRest::Spyke::Relation
Returns a new relation with the offsets applied.
107 108 109 110 111 112 113 114 115 |
# File 'lib/fmrest/spyke/relation.rb', line 107 def offset(value_or_hash) with_clone do |r| if value_or_hash.respond_to?(:each) r.set_portal_params(value_or_hash, :offset) else r.offset_value = value_or_hash end end end |
#omit(params) ⇒ FmRest::Spyke::Relation
Adds a new set of conditions to omit in a find request.
This is the same as passing omit: true
to .or
.
257 258 259 |
# File 'lib/fmrest/spyke/relation.rb', line 257 def omit(params) self.or(params.merge(omit: true)) end |
#or(*params) ⇒ Object
Signals that the next query conditions to be set (through .query
,
.match
, etc.) should be added as a logical OR relative to previously
set conditions.
In practice this means the JSON query request will have a new conditions object appended, e.g.:
{"query": [{"field": "condition"}, {"field": "OR-added condition"}]}
You can call this method with or without parameters. If parameters are
given they will be passed down to .query
(and those conditions
immediately set), otherwise it just prepares the next
conditions-setting method (e.g. match
) to use OR.
282 283 284 285 |
# File 'lib/fmrest/spyke/relation.rb', line 282 def or(*params) clone = with_clone { |r| r.chain_flag = :or } params.empty? ? clone : clone.query(*params) end |
#portal(*args) ⇒ FmRest::Spyke::Relation Also known as: includes, portals
Sets the portals to include with each record in the response.
149 150 151 152 153 154 155 156 157 158 159 160 161 |
# File 'lib/fmrest/spyke/relation.rb', line 149 def portal(*args) raise ArgumentError, "Call `portal' with at least one argument" if args.empty? with_clone do |r| if args.length == 1 && args.first.eql?(true) || args.first.eql?(false) r.included_portals = args.first ? nil : [] else r.included_portals ||= [] r.included_portals += args.flatten.map { |p| normalize_portal_param(p) } r.included_portals.uniq! end end end |
#query(*params) ⇒ FmRest::Spyke::Relation
Sets conditions for a find request. Conditions must be given in
{ field: condition }
format, where condition
is normally a string
sent raw to the Data API server, so you can use FileMaker find
operators. You can also pass Ruby range or date/datetime objects for
condition values, and they'll be converted to the appropriate Data API
representation.
Passing omit: true
in a conditions set will negate all conditions in
that set.
You can modify the way conditions are added (i.e. through logical AND
or OR) by pre-chaining .or
. By default it adds conditions through
logical AND.
Note that because of the way the Data API works, logical AND conditions on a single field are not possible. Because of that, if you try to set two AND conditions for the same field, the previously existing one will be overwritten with the new condition.
It is recommended that you learn how the Data API represents conditions in its find requests (i.e. an array of JSON objects with conditions on fields). This method internally uses that same representation, which you can view by inspecting the resulting relations. Understanding that representation will also make the limitations of this Ruby API clear.
216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 |
# File 'lib/fmrest/spyke/relation.rb', line 216 def query(*params) with_clone do |r| params = params.flatten.map { |p| normalize_query_params(p) } if r.chain_flag == :or || r.query_params.empty? r.query_params += params r.chain_flag = nil elsif r.chain_flag == :and r.cartesian_product_query_params(params) r.chain_flag = nil elsif params.length > r.query_params.length params[0, r.query_params.length].each_with_index do |p, i| r.query_params[i].merge!(p) end remainder = params.length - r.query_params.length r.query_params += params[-remainder, remainder] else params.each_with_index { |p, i| r.query_params[i].merge!(p) } end end end |
#script(options) ⇒ FmRest::Spyke::Relation
Returns a new relation with the script options applied.
73 74 75 76 77 78 79 80 81 |
# File 'lib/fmrest/spyke/relation.rb', line 73 def script() with_clone do |r| if .eql?(false) || .eql?(nil) r.script_params = {} else r.script_params = script_params.merge(FmRest::V1.convert_script_params()) end end end |
#sort(*args) ⇒ FmRest::Spyke::Relation Also known as: order
Allows sort params given in either hash format (using FM Data API's
format), or as a symbol, in which case the of the attribute must match
a known mapped attribute, optionally suffixed with !
or __desc
to
signify it should use descending order.
132 133 134 135 136 |
# File 'lib/fmrest/spyke/relation.rb', line 132 def sort(*args) with_clone do |r| r.sort_params = args.flatten.map { |s| normalize_sort_param(s) } end end |
#with_all_portals ⇒ FmRest::Spyke::Relation
Same as calling portal(true)
168 169 170 |
# File 'lib/fmrest/spyke/relation.rb', line 168 def with_all_portals portal(true) end |
#without_portals ⇒ FmRest::Spyke::Relation
Same as calling portal(false)
175 176 177 |
# File 'lib/fmrest/spyke/relation.rb', line 175 def without_portals portal(false) end |