fmrest-spyke 0.13.0
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +7 -0
- data/.yardopts +4 -0
- data/CHANGELOG.md +127 -0
- data/LICENSE.txt +21 -0
- data/README.md +523 -0
- data/lib/fmrest-spyke.rb +3 -0
- data/lib/fmrest/spyke.rb +21 -0
- data/lib/fmrest/spyke/base.rb +9 -0
- data/lib/fmrest/spyke/container_field.rb +59 -0
- data/lib/fmrest/spyke/model.rb +33 -0
- data/lib/fmrest/spyke/model/associations.rb +166 -0
- data/lib/fmrest/spyke/model/attributes.rb +159 -0
- data/lib/fmrest/spyke/model/auth.rb +43 -0
- data/lib/fmrest/spyke/model/connection.rb +163 -0
- data/lib/fmrest/spyke/model/container_fields.rb +40 -0
- data/lib/fmrest/spyke/model/global_fields.rb +40 -0
- data/lib/fmrest/spyke/model/http.rb +77 -0
- data/lib/fmrest/spyke/model/orm.rb +256 -0
- data/lib/fmrest/spyke/model/record_id.rb +96 -0
- data/lib/fmrest/spyke/model/serialization.rb +115 -0
- data/lib/fmrest/spyke/model/uri.rb +29 -0
- data/lib/fmrest/spyke/portal.rb +65 -0
- data/lib/fmrest/spyke/relation.rb +359 -0
- data/lib/fmrest/spyke/spyke_formatter.rb +274 -0
- data/lib/fmrest/spyke/validation_error.rb +25 -0
- metadata +96 -0
@@ -0,0 +1,96 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module FmRest
|
4
|
+
module Spyke
|
5
|
+
module Model
|
6
|
+
# Modifies Spyke models to use `__record_id` instead of `id` as the
|
7
|
+
# "primary key" method, so that we can map a model class to a FM layout
|
8
|
+
# with a field named `id` without clobbering it.
|
9
|
+
#
|
10
|
+
# The `id` reader method still maps to the record ID for backwards
|
11
|
+
# compatibility and because Spyke hardcodes its use at various points
|
12
|
+
# through its codebase, but it can be safely overwritten (e.g. to map to
|
13
|
+
# a FM field).
|
14
|
+
#
|
15
|
+
# The recommended way to deal with a layout that maps an `id` attribute
|
16
|
+
# is to remap it in the model to something else, e.g. `unique_id`.
|
17
|
+
#
|
18
|
+
module RecordID
|
19
|
+
extend ::ActiveSupport::Concern
|
20
|
+
|
21
|
+
included do
|
22
|
+
# @return [Integer] the record's recordId
|
23
|
+
attr_reader :__record_id
|
24
|
+
alias_method :record_id, :__record_id
|
25
|
+
alias_method :id, :__record_id
|
26
|
+
|
27
|
+
# @return [Integer] the record's modId
|
28
|
+
attr_reader :__mod_id
|
29
|
+
alias_method :mod_id, :__mod_id
|
30
|
+
|
31
|
+
# Get rid of Spyke's id= setter method, as we'll be using __record_id=
|
32
|
+
# instead
|
33
|
+
undef_method :id=
|
34
|
+
|
35
|
+
# Tell Spyke that we want __record_id as the PK
|
36
|
+
self.primary_key = :__record_id
|
37
|
+
end
|
38
|
+
|
39
|
+
# Sets the recordId and converts it to integer if it's not nil
|
40
|
+
#
|
41
|
+
# @param value [String, Integer, nil] The new recordId
|
42
|
+
#
|
43
|
+
# @return [Integer] the record's recordId
|
44
|
+
def __record_id=(value)
|
45
|
+
@__record_id = value.nil? ? nil : value.to_i
|
46
|
+
end
|
47
|
+
|
48
|
+
# Sets the modId and converts it to integer if it's not nil
|
49
|
+
#
|
50
|
+
# @param value [String, Integer, nil] The new modId
|
51
|
+
#
|
52
|
+
# @return [Integer] the record's modId
|
53
|
+
def __mod_id=(value)
|
54
|
+
@__mod_id = value.nil? ? nil : value.to_i
|
55
|
+
end
|
56
|
+
|
57
|
+
def __record_id?
|
58
|
+
__record_id.present?
|
59
|
+
end
|
60
|
+
alias_method :record_id?, :__record_id?
|
61
|
+
alias_method :persisted?, :__record_id?
|
62
|
+
|
63
|
+
# Spyke override -- Use `__record_id` instead of `id`
|
64
|
+
#
|
65
|
+
def hash
|
66
|
+
__record_id.hash
|
67
|
+
end
|
68
|
+
|
69
|
+
# Spyke override -- Renders class string with layout name and
|
70
|
+
# `record_id`.
|
71
|
+
#
|
72
|
+
# @return [String] A string representation of the class
|
73
|
+
#
|
74
|
+
def inspect
|
75
|
+
"#<#{self.class}(layout: #{self.class.layout}) record_id: #{__record_id.inspect} #{inspect_attributes}>"
|
76
|
+
end
|
77
|
+
|
78
|
+
# Spyke override -- Use `__record_id` instead of `id`
|
79
|
+
#
|
80
|
+
# @param id [Integer] The id of the record to destroy
|
81
|
+
#
|
82
|
+
def destroy(id = nil)
|
83
|
+
new(__record_id: id).destroy
|
84
|
+
end
|
85
|
+
|
86
|
+
private
|
87
|
+
|
88
|
+
# Spyke override (private)
|
89
|
+
#
|
90
|
+
def conflicting_ids?(attributes)
|
91
|
+
false
|
92
|
+
end
|
93
|
+
end
|
94
|
+
end
|
95
|
+
end
|
96
|
+
end
|
@@ -0,0 +1,115 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module FmRest
|
4
|
+
module Spyke
|
5
|
+
module Model
|
6
|
+
module Serialization
|
7
|
+
FM_DATE_FORMAT = "%m/%d/%Y"
|
8
|
+
FM_DATETIME_FORMAT = "#{FM_DATE_FORMAT} %H:%M:%S"
|
9
|
+
|
10
|
+
# Spyke override -- Return FM Data API's expected JSON format,
|
11
|
+
# including only modified fields.
|
12
|
+
#
|
13
|
+
def to_params
|
14
|
+
params = {
|
15
|
+
fieldData: serialize_values!(changed_params_not_embedded_in_url).merge(serialize_portal_deletions)
|
16
|
+
}
|
17
|
+
|
18
|
+
params[:modId] = __mod_id.to_s if __mod_id
|
19
|
+
|
20
|
+
portal_data = serialize_portals
|
21
|
+
|
22
|
+
params[:portalData] = portal_data unless portal_data.empty?
|
23
|
+
|
24
|
+
params
|
25
|
+
end
|
26
|
+
|
27
|
+
protected
|
28
|
+
|
29
|
+
def serialize_for_portal(portal)
|
30
|
+
params =
|
31
|
+
changed_params.except(:__record_id).transform_keys do |key|
|
32
|
+
"#{portal.attribute_prefix}::#{key}"
|
33
|
+
end
|
34
|
+
|
35
|
+
params[:recordId] = __record_id.to_s if __record_id
|
36
|
+
params[:modId] = __mod_id.to_s if __mod_id
|
37
|
+
|
38
|
+
serialize_values!(params)
|
39
|
+
end
|
40
|
+
|
41
|
+
private
|
42
|
+
|
43
|
+
def serialize_portals
|
44
|
+
portal_data = {}
|
45
|
+
|
46
|
+
portals.each do |portal|
|
47
|
+
portal.each do |portal_record|
|
48
|
+
next unless portal_record.changed? && !portal_record.marked_for_destruction?
|
49
|
+
portal_params = portal_data[portal.portal_key] ||= []
|
50
|
+
portal_params << portal_record.serialize_for_portal(portal)
|
51
|
+
end
|
52
|
+
end
|
53
|
+
|
54
|
+
portal_data
|
55
|
+
end
|
56
|
+
|
57
|
+
def serialize_portal_deletions
|
58
|
+
deletions = []
|
59
|
+
|
60
|
+
portals.each do |portal|
|
61
|
+
portal.select(&:marked_for_destruction?).each do |portal_record|
|
62
|
+
next unless portal_record.persisted?
|
63
|
+
deletions << "#{portal.portal_key}.#{portal_record.__record_id}"
|
64
|
+
end
|
65
|
+
end
|
66
|
+
|
67
|
+
return {} if deletions.length == 0
|
68
|
+
|
69
|
+
{ deleteRelated: deletions.length == 1 ? deletions.first : deletions }
|
70
|
+
end
|
71
|
+
|
72
|
+
def changed_params_not_embedded_in_url
|
73
|
+
params_not_embedded_in_url.slice(*mapped_changed)
|
74
|
+
end
|
75
|
+
|
76
|
+
# Modifies the given hash in-place encoding non-string values (e.g.
|
77
|
+
# dates) to their string representation when appropriate.
|
78
|
+
#
|
79
|
+
def serialize_values!(params)
|
80
|
+
params.transform_values! do |value|
|
81
|
+
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)
|
86
|
+
else
|
87
|
+
value
|
88
|
+
end
|
89
|
+
end
|
90
|
+
|
91
|
+
params
|
92
|
+
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
|
+
end
|
113
|
+
end
|
114
|
+
end
|
115
|
+
end
|
@@ -0,0 +1,29 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module FmRest
|
4
|
+
module Spyke
|
5
|
+
module Model
|
6
|
+
module URI
|
7
|
+
extend ::ActiveSupport::Concern
|
8
|
+
|
9
|
+
class_methods do
|
10
|
+
# Accessor for FM layout (helps with building the URI)
|
11
|
+
#
|
12
|
+
def layout(layout = nil)
|
13
|
+
@layout = layout if layout
|
14
|
+
@layout ||= model_name.name
|
15
|
+
end
|
16
|
+
|
17
|
+
# Spyke override -- Extends `uri` to default to FM Data's URI schema
|
18
|
+
#
|
19
|
+
def uri(uri_template = nil)
|
20
|
+
if @uri.nil? && uri_template.nil?
|
21
|
+
return FmRest::V1.record_path(layout) + "(/:#{primary_key})"
|
22
|
+
end
|
23
|
+
super
|
24
|
+
end
|
25
|
+
end
|
26
|
+
end
|
27
|
+
end
|
28
|
+
end
|
29
|
+
end
|
@@ -0,0 +1,65 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module FmRest
|
4
|
+
module Spyke
|
5
|
+
# Extend Spyke's HasMany association with custom options
|
6
|
+
#
|
7
|
+
class Portal < ::Spyke::Associations::HasMany
|
8
|
+
def initialize(*args)
|
9
|
+
super
|
10
|
+
|
11
|
+
# Portals are always embedded, so no special URI
|
12
|
+
@options[:uri] = ""
|
13
|
+
end
|
14
|
+
|
15
|
+
def portal_key
|
16
|
+
(@options[:portal_key] || name).to_s
|
17
|
+
end
|
18
|
+
|
19
|
+
def attribute_prefix
|
20
|
+
(@options[:attribute_prefix] || portal_key).to_s
|
21
|
+
end
|
22
|
+
|
23
|
+
# Callback method, not meant to be used directly
|
24
|
+
#
|
25
|
+
def parent_changes_applied
|
26
|
+
each(&:changes_applied)
|
27
|
+
end
|
28
|
+
|
29
|
+
def <<(*records)
|
30
|
+
records.flatten.each { |r| add_to_parent(r) }
|
31
|
+
self
|
32
|
+
end
|
33
|
+
alias_method :push, :<<
|
34
|
+
alias_method :concat, :<<
|
35
|
+
|
36
|
+
def _remove_marked_for_destruction
|
37
|
+
find_some.reject!(&:marked_for_destruction?)
|
38
|
+
end
|
39
|
+
|
40
|
+
private
|
41
|
+
|
42
|
+
# Spyke::Associations::HasMany#initialize calls primary_key to build the
|
43
|
+
# default URI, which causes a NameError, so this is here just to prevent
|
44
|
+
# that. We don't care what it returns as we override the URI with nil
|
45
|
+
# anyway
|
46
|
+
def primary_key; end
|
47
|
+
|
48
|
+
# Make sure the association doesn't try to fetch records through URI
|
49
|
+
def uri; nil; end
|
50
|
+
|
51
|
+
def embedded_data
|
52
|
+
parent.attributes[portal_key]
|
53
|
+
end
|
54
|
+
|
55
|
+
# Spyke override
|
56
|
+
#
|
57
|
+
def add_to_parent(record)
|
58
|
+
raise ArgumentError, "Expected an instance of #{klass}, got a #{record.class} instead" unless record.kind_of?(klass)
|
59
|
+
find_some << record
|
60
|
+
record.embedded_in_portal
|
61
|
+
record
|
62
|
+
end
|
63
|
+
end
|
64
|
+
end
|
65
|
+
end
|
@@ -0,0 +1,359 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module FmRest
|
4
|
+
module Spyke
|
5
|
+
class Relation < ::Spyke::Relation
|
6
|
+
SORT_PARAM_MATCHER = /(.*?)(!|__desc(?:end)?)?\Z/.freeze
|
7
|
+
|
8
|
+
# NOTE: We need to keep limit, offset, sort, query and portal accessors
|
9
|
+
# separate from regular params because FM Data API uses either "limit" or
|
10
|
+
# "_limit" (or "_offset", etc.) as param keys depending on the type of
|
11
|
+
# request, so we can't set the params until the last moment
|
12
|
+
|
13
|
+
|
14
|
+
attr_accessor :limit_value, :offset_value, :sort_params, :query_params,
|
15
|
+
:included_portals, :portal_params, :script_params
|
16
|
+
|
17
|
+
def initialize(*_args)
|
18
|
+
super
|
19
|
+
|
20
|
+
@limit_value = klass.default_limit
|
21
|
+
|
22
|
+
if klass.default_sort.present?
|
23
|
+
@sort_params = Array.wrap(klass.default_sort).map { |s| normalize_sort_param(s) }
|
24
|
+
end
|
25
|
+
|
26
|
+
@query_params = []
|
27
|
+
|
28
|
+
@included_portals = nil
|
29
|
+
@portal_params = {}
|
30
|
+
@script_params = {}
|
31
|
+
end
|
32
|
+
|
33
|
+
# @param options [String, Array, Hash, nil, false] sets script params to
|
34
|
+
# execute in the next get or find request
|
35
|
+
#
|
36
|
+
# @example
|
37
|
+
# # Find records and run the script named "My script"
|
38
|
+
# Person.script("My script").find_some
|
39
|
+
#
|
40
|
+
# # Find records and run the script named "My script" with param "the param"
|
41
|
+
# Person.script(["My script", "the param"]).find_some
|
42
|
+
#
|
43
|
+
# # Find records and run a prerequest, presort and after (normal) script
|
44
|
+
# Person.script(after: "Script", prerequest: "Prereq script", presort: "Presort script").find_some
|
45
|
+
#
|
46
|
+
# # Same as above, but passing parameters too
|
47
|
+
# Person.script(
|
48
|
+
# after: ["After script", "the param"],
|
49
|
+
# prerequest: ["Prereq script", "the param"],
|
50
|
+
# presort: o ["Presort script", "the param"]
|
51
|
+
# ).find_some
|
52
|
+
#
|
53
|
+
# Person.script(nil).find_some # Disable script execution
|
54
|
+
# Person.script(false).find_some # Disable script execution
|
55
|
+
#
|
56
|
+
# @return [FmRest::Spyke::Relation] a new relation with the script
|
57
|
+
# options applied
|
58
|
+
def script(options)
|
59
|
+
with_clone do |r|
|
60
|
+
if options.eql?(false) || options.eql?(nil)
|
61
|
+
r.script_params = {}
|
62
|
+
else
|
63
|
+
r.script_params = script_params.merge(FmRest::V1.convert_script_params(options))
|
64
|
+
end
|
65
|
+
end
|
66
|
+
end
|
67
|
+
|
68
|
+
# @param value_or_hash [Integer, Hash] the limit value for this layout,
|
69
|
+
# or a hash with limits for the layout's portals
|
70
|
+
# @example
|
71
|
+
# Person.limit(10) # Set layout limit
|
72
|
+
# Person.limit(children: 10) # Set portal limit
|
73
|
+
# @return [FmRest::Spyke::Relation] a new relation with the limits
|
74
|
+
# applied
|
75
|
+
def limit(value_or_hash)
|
76
|
+
with_clone do |r|
|
77
|
+
if value_or_hash.respond_to?(:each)
|
78
|
+
r.set_portal_params(value_or_hash, :limit)
|
79
|
+
else
|
80
|
+
r.limit_value = value_or_hash
|
81
|
+
end
|
82
|
+
end
|
83
|
+
end
|
84
|
+
|
85
|
+
# @param value_or_hash [Integer, Hash] the offset value for this layout,
|
86
|
+
# or a hash with offsets for the layout's portals
|
87
|
+
# @example
|
88
|
+
# Person.offset(10) # Set layout offset
|
89
|
+
# Person.offset(children: 10) # Set portal offset
|
90
|
+
# @return [FmRest::Spyke::Relation] a new relation with the offsets
|
91
|
+
# applied
|
92
|
+
def offset(value_or_hash)
|
93
|
+
with_clone do |r|
|
94
|
+
if value_or_hash.respond_to?(:each)
|
95
|
+
r.set_portal_params(value_or_hash, :offset)
|
96
|
+
else
|
97
|
+
r.offset_value = value_or_hash
|
98
|
+
end
|
99
|
+
end
|
100
|
+
end
|
101
|
+
|
102
|
+
# Allows sort params given in either hash format (using FM Data API's
|
103
|
+
# format), or as a symbol, in which case the of the attribute must match
|
104
|
+
# a known mapped attribute, optionally suffixed with `!` or `__desc` to
|
105
|
+
# signify it should use descending order.
|
106
|
+
#
|
107
|
+
# @param args [Array<Symbol, Hash>] the names of attributes to sort by with
|
108
|
+
# optional `!` or `__desc` suffix, or a hash of options as expected by
|
109
|
+
# the FM Data API
|
110
|
+
# @example
|
111
|
+
# Person.sort(:first_name, :age!)
|
112
|
+
# Person.sort(:first_name, :age__desc)
|
113
|
+
# Person.sort(:first_name, :age__descend)
|
114
|
+
# Person.sort({ fieldName: "FirstName" }, { fieldName: "Age", sortOrder: "descend" })
|
115
|
+
# @return [FmRest::Spyke::Relation] a new relation with the sort options
|
116
|
+
# applied
|
117
|
+
def sort(*args)
|
118
|
+
with_clone do |r|
|
119
|
+
r.sort_params = args.flatten.map { |s| normalize_sort_param(s) }
|
120
|
+
end
|
121
|
+
end
|
122
|
+
alias order sort
|
123
|
+
|
124
|
+
# Sets the portals to include with each record in the response.
|
125
|
+
#
|
126
|
+
# @param args [Array<Symbol, String>, true, false] the names of portals to
|
127
|
+
# include, or `false` to request no portals
|
128
|
+
# @example
|
129
|
+
# Person.portal(:relatives, :pets)
|
130
|
+
# Person.portal(false) # Disables portals
|
131
|
+
# Person.portal(true) # Enables portals (includes all)
|
132
|
+
# @return [FmRest::Spyke::Relation] a new relation with the portal
|
133
|
+
# options applied
|
134
|
+
def portal(*args)
|
135
|
+
raise ArgumentError, "Call `portal' with at least one argument" if args.empty?
|
136
|
+
|
137
|
+
with_clone do |r|
|
138
|
+
if args.length == 1 && args.first.eql?(true) || args.first.eql?(false)
|
139
|
+
r.included_portals = args.first ? nil : []
|
140
|
+
else
|
141
|
+
r.included_portals ||= []
|
142
|
+
r.included_portals += args.flatten.map { |p| normalize_portal_param(p) }
|
143
|
+
r.included_portals.uniq!
|
144
|
+
end
|
145
|
+
end
|
146
|
+
end
|
147
|
+
alias includes portal
|
148
|
+
alias portals portal
|
149
|
+
|
150
|
+
# Same as calling `portal(true)`
|
151
|
+
#
|
152
|
+
# @return (see #portal)
|
153
|
+
def with_all_portals
|
154
|
+
portal(true)
|
155
|
+
end
|
156
|
+
|
157
|
+
# Same as calling `portal(false)`
|
158
|
+
#
|
159
|
+
# @return (see #portal)
|
160
|
+
def without_portals
|
161
|
+
portal(false)
|
162
|
+
end
|
163
|
+
|
164
|
+
def query(*params)
|
165
|
+
with_clone do |r|
|
166
|
+
r.query_params += params.flatten.map { |p| normalize_query_params(p) }
|
167
|
+
end
|
168
|
+
end
|
169
|
+
|
170
|
+
def omit(params)
|
171
|
+
query(params.merge(omit: true))
|
172
|
+
end
|
173
|
+
|
174
|
+
# @return [Boolean] whether a query was set on this relation
|
175
|
+
def has_query?
|
176
|
+
query_params.present?
|
177
|
+
end
|
178
|
+
|
179
|
+
# Finds a single instance of the model by forcing limit = 1, or simply
|
180
|
+
# fetching the record by id if the primary key was set
|
181
|
+
#
|
182
|
+
# @return [FmRest::Spyke::Base]
|
183
|
+
def find_one
|
184
|
+
@find_one ||=
|
185
|
+
if primary_key_set?
|
186
|
+
without_collection_params { super }
|
187
|
+
else
|
188
|
+
klass.new_collection_from_result(limit(1).fetch).first
|
189
|
+
end
|
190
|
+
rescue ::Spyke::ConnectionError => error
|
191
|
+
fallback_or_reraise(error, default: nil)
|
192
|
+
end
|
193
|
+
alias_method :first, :find_one
|
194
|
+
alias_method :any, :find_one
|
195
|
+
|
196
|
+
# Yields each batch of records that was found by the find options.
|
197
|
+
#
|
198
|
+
# NOTE: By its nature, batch processing is subject to race conditions if
|
199
|
+
# other processes are modifying the database
|
200
|
+
#
|
201
|
+
# @param batch_size [Integer] Specifies the size of the batch.
|
202
|
+
# @return [Enumerator] if called without a block.
|
203
|
+
def find_in_batches(batch_size: 1000)
|
204
|
+
unless block_given?
|
205
|
+
return to_enum(:find_in_batches, batch_size: batch_size) do
|
206
|
+
total = limit(1).find_some.metadata.data_info.found_count
|
207
|
+
(total - 1).div(batch_size) + 1
|
208
|
+
end
|
209
|
+
end
|
210
|
+
|
211
|
+
offset = 1 # DAPI offset is 1-based
|
212
|
+
|
213
|
+
loop do
|
214
|
+
relation = offset(offset).limit(batch_size)
|
215
|
+
|
216
|
+
records = relation.find_some
|
217
|
+
|
218
|
+
yield records if records.length > 0
|
219
|
+
|
220
|
+
break if records.length < batch_size
|
221
|
+
|
222
|
+
# Save one iteration if the total is a multiple of batch_size
|
223
|
+
if found_count = records.metadata.data_info && records.metadata.data_info.found_count
|
224
|
+
break if found_count == (offset - 1) + batch_size
|
225
|
+
end
|
226
|
+
|
227
|
+
offset += batch_size
|
228
|
+
end
|
229
|
+
end
|
230
|
+
|
231
|
+
# Looping through a collection of records from the database (using the
|
232
|
+
# #all method, for example) is very inefficient since it will fetch and
|
233
|
+
# instantiate all the objects at once.
|
234
|
+
#
|
235
|
+
# In that case, batch processing methods allow you to work with the
|
236
|
+
# records in batches, thereby greatly reducing memory consumption and be
|
237
|
+
# lighter on the Data API server.
|
238
|
+
#
|
239
|
+
# The find_each method uses #find_in_batches with a batch size of 1000
|
240
|
+
# (or as specified by the :batch_size option).
|
241
|
+
#
|
242
|
+
# NOTE: By its nature, batch processing is subject to race conditions if
|
243
|
+
# other processes are modifying the database
|
244
|
+
#
|
245
|
+
# @param (see #find_in_batches)
|
246
|
+
# @example
|
247
|
+
# Person.find_each do |person|
|
248
|
+
# person.greet
|
249
|
+
# end
|
250
|
+
#
|
251
|
+
# Person.query(name: "==Mitch").find_each do |person|
|
252
|
+
# person.say_hi
|
253
|
+
# end
|
254
|
+
# @return (see #find_in_batches)
|
255
|
+
def find_each(batch_size: 1000)
|
256
|
+
unless block_given?
|
257
|
+
return to_enum(:find_each, batch_size: batch_size) do
|
258
|
+
limit(1).find_some.metadata.data_info.found_count
|
259
|
+
end
|
260
|
+
end
|
261
|
+
|
262
|
+
find_in_batches(batch_size: batch_size) do |records|
|
263
|
+
records.each { |r| yield r }
|
264
|
+
end
|
265
|
+
end
|
266
|
+
|
267
|
+
protected
|
268
|
+
|
269
|
+
def set_portal_params(params_hash, param)
|
270
|
+
# Copy portal_params so we're not modifying the same hash as the parent
|
271
|
+
# scope
|
272
|
+
self.portal_params = portal_params.dup
|
273
|
+
|
274
|
+
params_hash.each do |portal_name, value|
|
275
|
+
# TODO: Use a hash like { portal_name: { param: value } } instead so
|
276
|
+
# we can intelligently avoid including portal params for excluded
|
277
|
+
# portals
|
278
|
+
key = "#{param}.#{normalize_portal_param(portal_name)}"
|
279
|
+
|
280
|
+
# Delete key if value is falsy
|
281
|
+
if !value && portal_params.has_key?(key)
|
282
|
+
portal_params.delete(key)
|
283
|
+
else
|
284
|
+
self.portal_params[key] = value
|
285
|
+
end
|
286
|
+
end
|
287
|
+
end
|
288
|
+
|
289
|
+
private
|
290
|
+
|
291
|
+
def normalize_sort_param(param)
|
292
|
+
if param.kind_of?(Symbol) || param.kind_of?(String)
|
293
|
+
_, attr, descend = param.to_s.match(SORT_PARAM_MATCHER).to_a
|
294
|
+
|
295
|
+
unless field_name = klass.mapped_attributes[attr]
|
296
|
+
raise ArgumentError, "Unknown attribute `#{attr}' given to sort as #{param.inspect}. If you want to use a custom sort pass a hash in the Data API format"
|
297
|
+
end
|
298
|
+
|
299
|
+
hash = { fieldName: field_name }
|
300
|
+
hash[:sortOrder] = "descend" if descend
|
301
|
+
return hash
|
302
|
+
end
|
303
|
+
|
304
|
+
# TODO: Sanitize sort hash param for FM Data API conformity?
|
305
|
+
param
|
306
|
+
end
|
307
|
+
|
308
|
+
def normalize_portal_param(param)
|
309
|
+
if param.kind_of?(Symbol)
|
310
|
+
portal_key, = klass.portal_options.find { |_, opts| opts[:name].to_s == param.to_s }
|
311
|
+
|
312
|
+
unless portal_key
|
313
|
+
raise ArgumentError, "Unknown portal #{param.inspect}. If you want to include a portal not defined in the model pass it as a string instead"
|
314
|
+
end
|
315
|
+
|
316
|
+
return portal_key
|
317
|
+
end
|
318
|
+
|
319
|
+
param
|
320
|
+
end
|
321
|
+
|
322
|
+
def normalize_query_params(params)
|
323
|
+
params.each_with_object({}) do |(k, v), normalized|
|
324
|
+
if k == :omit || k == "omit"
|
325
|
+
# FM Data API wants omit values as strings, e.g. "true" or "false"
|
326
|
+
# rather than true/false
|
327
|
+
normalized["omit"] = v.to_s
|
328
|
+
next
|
329
|
+
end
|
330
|
+
|
331
|
+
# TODO: Raise ArgumentError if an attribute given as symbol isn't defiend
|
332
|
+
if k.kind_of?(Symbol) && klass.mapped_attributes.has_key?(k)
|
333
|
+
normalized[klass.mapped_attributes[k].to_s] = v
|
334
|
+
else
|
335
|
+
normalized[k.to_s] = v
|
336
|
+
end
|
337
|
+
end
|
338
|
+
end
|
339
|
+
|
340
|
+
def primary_key_set?
|
341
|
+
params[klass.primary_key].present?
|
342
|
+
end
|
343
|
+
|
344
|
+
def without_collection_params
|
345
|
+
orig_values = limit_value, offset_value, sort_params, query_params
|
346
|
+
self.limit_value = self.offset_value = self.sort_params = self.query_params = nil
|
347
|
+
yield
|
348
|
+
ensure
|
349
|
+
self.limit_value, self.offset_value, self.sort_params, self.query_params = orig_values
|
350
|
+
end
|
351
|
+
|
352
|
+
def with_clone
|
353
|
+
clone.tap do |relation|
|
354
|
+
yield relation
|
355
|
+
end
|
356
|
+
end
|
357
|
+
end
|
358
|
+
end
|
359
|
+
end
|