Class: FmRest::Spyke::Relation

Inherits:
Spyke::Relation
  • Object
show all
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

Instance Method Summary collapse

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_flagObject

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_portalsObject

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_valueObject

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_valueObject

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_paramsObject

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_paramsObject

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_paramsObject

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_paramsObject

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.

Examples:

# Add conditions directly on .and call:
Person.query(name: "=Alice").and(city: "=Wonderland")
# Add exact match conditions through method chaining:
Person.match(name: "Alice").and.match(city: "Wonderland")
# With multiple condition hashes:
Person.query(name: "=Alice").and({ city: "=Wonderland" }, { city: "=London" })
# With conflicting criteria:
Person.match(name: "Alice").and.match(name: "Bob")
# => JSON: { "name": "1001..1000" } -> forced empty result set


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

Examples:

Person.find_each do |person|
  person.greet
end

Person.query(name: "==Mitch").find_each do |person|
  person.say_hi
end

Parameters:

  • batch_size (Integer) (defaults to: 1000)

    Specifies the size of the batch.

Returns:

  • (Enumerator)

    if called without a block.



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

Parameters:

  • batch_size (Integer) (defaults to: 1000)

    Specifies the size of the batch.

Returns:

  • (Enumerator)

    if called without a block.



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

Returns:



374
375
376
377
378
379
380
381
382
383
# File 'lib/fmrest/spyke/relation.rb', line 374

def find_one(options = {})
  @find_one ||=
    if primary_key_set?
      without_collection_params { super() }
    else
      klass.new_collection_from_result(limit(1).fetch(options)).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).

Returns:



394
395
396
# File 'lib/fmrest/spyke/relation.rb', line 394

def find_one!(options = {})
  find_one(options.merge(raise_on_no_matching_records: true))
end

#has_query?Boolean

Returns whether a query was set on this relation.

Returns:

  • (Boolean)

    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.

Examples:

Person.limit(10) # Set layout limit
Person.limit(children: 10) # Set portal limit

Parameters:

  • value_or_hash (Integer, Hash)

    the limit value for this layout, or a hash with limits for the layout's portals

Returns:



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.

Examples:

Person.query(email: "[email protected]") # Find exact email

Returns:



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.

Examples:

Person.offset(10) # Set layout offset
Person.offset(children: 10) # Set portal offset

Parameters:

  • value_or_hash (Integer, Hash)

    the offset value for this layout, or a hash with offsets for the layout's portals

Returns:



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.

Returns:



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.

Examples:

# Add conditions directly on .or call:
Person.query(name: "=Alice").or(name: "=Bob")
# Add exact match conditions through method chaining
Person.match(email: "[email protected]").or.match(email: "[email protected]")


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.

Examples:

Person.portal(:relatives, :pets)
Person.portal(false) # Disables portals
Person.portal(true) # Enables portals (includes all)

Parameters:

  • args (Array<Symbol, String>, true, false)

    the names of portals to include, or false to request no portals

Returns:

Raises:

  • (ArgumentError)


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.

Examples:

Person.query(name: "=Alice") # Simple query
Person.query(age: (20..29)) # Query using a Ruby range
Person.query(created_on: Date.today..Date.today-1)
Person.query(name: "=Alice", age: ">20") # Query multiple fields (logical AND)
Person.query(name: "=Alice").query(age: ">20") # Same effect as above example
Person.query(name: "=Bob", omit: true) # Negate a query (i.e. find people not named Bob)
Person.query(pets: { name: "=Snuggles" }) # Query portal fields
Person.query({ name: "=Alice" }, { name: "=Bob" }) # Separate conditions through logical OR
Person.query(name: "=Alice").or.query(name: "=Bob") # Same effect as above example

Returns:



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.

Examples:

# Find records and run the script named "My script"
Person.script("My script").find_some

# Find records and run the script named "My script" with param "the param"
Person.script(["My script", "the param"]).find_some

# Find records and run a prerequest, presort and after (normal) script
Person.script(after: "Script", prerequest: "Prereq script", presort: "Presort script").find_some

# Same as above, but passing parameters too
Person.script(
  after:      ["After script", "the param"],
  prerequest: ["Prereq script", "the param"],
  presort: o  ["Presort script", "the param"]
).find_some

Person.script(nil).find_some # Disable script execution
Person.script(false).find_some # Disable script execution

Parameters:

  • options (String, Array, Hash, nil, false)

    sets script params to execute in the next get or find request

Returns:



73
74
75
76
77
78
79
80
81
# File 'lib/fmrest/spyke/relation.rb', line 73

def script(options)
  with_clone do |r|
    if options.eql?(false) || options.eql?(nil)
      r.script_params = {}
    else
      r.script_params = script_params.merge(FmRest::V1.convert_script_params(options))
    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.

Examples:

Person.sort(:first_name, :age!)
Person.sort(:first_name, :age__desc)
Person.sort(:first_name, :age__descend)
Person.sort({ fieldName: "FirstName" }, { fieldName: "Age", sortOrder: "descend" })

Parameters:

  • args (Array<Symbol, Hash>)

    the names of attributes to sort by with optional ! or __desc suffix, or a hash of options as expected by the FM Data API

Returns:



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_portalsFmRest::Spyke::Relation

Same as calling portal(true)

Returns:



168
169
170
# File 'lib/fmrest/spyke/relation.rb', line 168

def with_all_portals
  portal(true)
end

#without_portalsFmRest::Spyke::Relation

Same as calling portal(false)

Returns:



175
176
177
# File 'lib/fmrest/spyke/relation.rb', line 175

def without_portals
  portal(false)
end