fmrest 0.7.1 → 0.11.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/.github/workflows/ci.yml +33 -0
- data/CHANGELOG.md +37 -0
- data/README.md +176 -26
- data/fmrest.gemspec +2 -2
- data/lib/fmrest.rb +8 -3
- data/lib/fmrest/connection_settings.rb +124 -0
- data/lib/fmrest/errors.rb +2 -0
- data/lib/fmrest/spyke/base.rb +2 -0
- data/lib/fmrest/spyke/model.rb +2 -0
- data/lib/fmrest/spyke/model/auth.rb +8 -0
- data/lib/fmrest/spyke/model/connection.rb +88 -18
- data/lib/fmrest/spyke/model/global_fields.rb +40 -0
- data/lib/fmrest/spyke/model/orm.rb +2 -1
- data/lib/fmrest/spyke/model/serialization.rb +16 -5
- data/lib/fmrest/spyke/relation.rb +73 -0
- data/lib/fmrest/spyke/spyke_formatter.rb +46 -9
- data/lib/fmrest/string_date.rb +46 -7
- data/lib/fmrest/token_store.rb +6 -0
- data/lib/fmrest/token_store/base.rb +3 -3
- data/lib/fmrest/v1.rb +8 -4
- data/lib/fmrest/v1/auth.rb +30 -0
- data/lib/fmrest/v1/connection.rb +54 -28
- data/lib/fmrest/v1/dates.rb +81 -0
- data/lib/fmrest/v1/raise_errors.rb +3 -1
- data/lib/fmrest/v1/token_session.rb +41 -49
- data/lib/fmrest/v1/type_coercer.rb +111 -36
- data/lib/fmrest/v1/utils.rb +0 -17
- data/lib/fmrest/version.rb +1 -1
- metadata +18 -13
data/lib/fmrest/errors.rb
CHANGED
@@ -21,6 +21,8 @@ module FmRest
|
|
21
21
|
class APIError::NoMatchingRecordsError < APIError::ParameterError; end
|
22
22
|
class APIError::ValidationError < APIError; end # error codes 500..599
|
23
23
|
class APIError::SystemError < APIError; end # error codes 800..899
|
24
|
+
class APIError::InvalidToken < APIError; end # error code 952
|
25
|
+
class APIError::MaximumDataAPICallsExceeded < APIError; end # error code 953
|
24
26
|
class APIError::ScriptError < APIError; end # error codes 1200..1299
|
25
27
|
class APIError::ODBCError < APIError; end # error codes 1400..1499
|
26
28
|
|
data/lib/fmrest/spyke/base.rb
CHANGED
@@ -8,6 +8,8 @@ module FmRest
|
|
8
8
|
|
9
9
|
class << self
|
10
10
|
def Base(config = nil)
|
11
|
+
warn "[DEPRECATION] Inheriting from `FmRest::Spyke::Base(config)` is deprecated and will be removed, inherit from `FmRest::Spyke::Base` (without arguments) and use `fmrest_config=` instead"
|
12
|
+
|
11
13
|
if config
|
12
14
|
return Class.new(::FmRest::Spyke::Base) do
|
13
15
|
self.fmrest_config = config
|
data/lib/fmrest/spyke/model.rb
CHANGED
@@ -7,6 +7,7 @@ require "fmrest/spyke/model/serialization"
|
|
7
7
|
require "fmrest/spyke/model/associations"
|
8
8
|
require "fmrest/spyke/model/orm"
|
9
9
|
require "fmrest/spyke/model/container_fields"
|
10
|
+
require "fmrest/spyke/model/global_fields"
|
10
11
|
require "fmrest/spyke/model/http"
|
11
12
|
require "fmrest/spyke/model/auth"
|
12
13
|
|
@@ -22,6 +23,7 @@ module FmRest
|
|
22
23
|
include Associations
|
23
24
|
include Orm
|
24
25
|
include ContainerFields
|
26
|
+
include GlobalFields
|
25
27
|
include Http
|
26
28
|
include Auth
|
27
29
|
|
@@ -28,6 +28,14 @@ module FmRest
|
|
28
28
|
rescue FmRest::V1::TokenSession::NoSessionTokenSet
|
29
29
|
false
|
30
30
|
end
|
31
|
+
|
32
|
+
def request_auth_token
|
33
|
+
FmRest::V1.request_auth_token(FmRest::V1.auth_connection(fmrest_config))
|
34
|
+
end
|
35
|
+
|
36
|
+
def request_auth_token!
|
37
|
+
FmRest::V1.request_auth_token!(FmRest::V1.auth_connection(fmrest_config))
|
38
|
+
end
|
31
39
|
end
|
32
40
|
end
|
33
41
|
end
|
@@ -4,19 +4,70 @@ module FmRest
|
|
4
4
|
module Spyke
|
5
5
|
module Model
|
6
6
|
module Connection
|
7
|
-
extend
|
7
|
+
extend ActiveSupport::Concern
|
8
8
|
|
9
9
|
included do
|
10
|
-
class_attribute :fmrest_config, instance_accessor: false, instance_predicate: false
|
11
|
-
|
12
10
|
class_attribute :faraday_block, instance_accessor: false, instance_predicate: false
|
13
11
|
class << self; private :faraday_block, :faraday_block=; end
|
14
12
|
|
15
|
-
# FM Data API expects PATCH for updates (Spyke's default
|
13
|
+
# FM Data API expects PATCH for updates (Spyke's default is PUT)
|
16
14
|
self.callback_methods = { create: :post, update: :patch }.freeze
|
17
15
|
end
|
18
16
|
|
19
17
|
class_methods do
|
18
|
+
def fmrest_config
|
19
|
+
if fmrest_config_overlay
|
20
|
+
return FmRest.default_connection_settings.merge(fmrest_config_overlay, skip_validation: true)
|
21
|
+
end
|
22
|
+
|
23
|
+
FmRest.default_connection_settings
|
24
|
+
end
|
25
|
+
|
26
|
+
# Behaves similar to ActiveSupport's class_attribute, redefining the
|
27
|
+
# reader method so it can be inherited and overwritten in subclasses
|
28
|
+
#
|
29
|
+
def fmrest_config=(settings)
|
30
|
+
settings = ConnectionSettings.new(settings, skip_validation: true)
|
31
|
+
|
32
|
+
redefine_singleton_method(:fmrest_config) do
|
33
|
+
overlay = fmrest_config_overlay
|
34
|
+
return settings.merge(overlay, skip_validation: true) if overlay
|
35
|
+
settings
|
36
|
+
end
|
37
|
+
end
|
38
|
+
|
39
|
+
# Allows overwriting some connection settings in a thread-local
|
40
|
+
# manner. Useful in the use case where you want to connect to the
|
41
|
+
# same database using different accounts (e.g. credentials provided
|
42
|
+
# by users in a web app context)
|
43
|
+
#
|
44
|
+
def fmrest_config_overlay=(settings)
|
45
|
+
Thread.current[fmrest_config_overlay_key] = settings
|
46
|
+
end
|
47
|
+
|
48
|
+
def fmrest_config_overlay
|
49
|
+
Thread.current[fmrest_config_overlay_key] || begin
|
50
|
+
superclass.fmrest_config_overlay
|
51
|
+
rescue NoMethodError
|
52
|
+
nil
|
53
|
+
end
|
54
|
+
end
|
55
|
+
|
56
|
+
def clear_fmrest_config_overlay
|
57
|
+
Thread.current[fmrest_config_overlay_key] = nil
|
58
|
+
end
|
59
|
+
|
60
|
+
def with_overlay(settings, &block)
|
61
|
+
Fiber.new do
|
62
|
+
begin
|
63
|
+
self.fmrest_config_overlay = settings
|
64
|
+
yield
|
65
|
+
ensure
|
66
|
+
self.clear_fmrest_config_overlay
|
67
|
+
end
|
68
|
+
end.resume
|
69
|
+
end
|
70
|
+
|
20
71
|
def connection
|
21
72
|
super || fmrest_connection
|
22
73
|
end
|
@@ -38,26 +89,45 @@ module FmRest
|
|
38
89
|
private
|
39
90
|
|
40
91
|
def fmrest_connection
|
41
|
-
|
42
|
-
|
43
|
-
|
92
|
+
memoize = false
|
93
|
+
|
94
|
+
# Don't memoize the connection if there's an overlay, since
|
95
|
+
# overlays are thread-local and so should be the connection
|
96
|
+
unless fmrest_config_overlay
|
97
|
+
return @fmrest_connection if @fmrest_connection
|
98
|
+
memoize = true
|
99
|
+
end
|
44
100
|
|
45
|
-
|
46
|
-
faraday_block.call(conn) if faraday_block
|
101
|
+
config = ConnectionSettings.wrap(fmrest_config)
|
47
102
|
|
48
|
-
|
49
|
-
|
50
|
-
|
51
|
-
# and need to be specified as options to `portal`
|
52
|
-
conn.use FmRest::Spyke::SpykeFormatter, self
|
103
|
+
connection =
|
104
|
+
FmRest::V1.build_connection(config) do |conn|
|
105
|
+
faraday_block.call(conn) if faraday_block
|
53
106
|
|
54
|
-
|
107
|
+
# Pass the class to SpykeFormatter's initializer so it can have
|
108
|
+
# access to extra context defined in the model, e.g. a portal
|
109
|
+
# where name of the portal and the attributes prefix don't match
|
110
|
+
# and need to be specified as options to `portal`
|
111
|
+
conn.use FmRest::Spyke::SpykeFormatter, self
|
55
112
|
|
56
|
-
|
57
|
-
|
58
|
-
|
113
|
+
conn.use FmRest::V1::TypeCoercer, config
|
114
|
+
|
115
|
+
# FmRest::Spyke::JsonParse expects symbol keys
|
116
|
+
conn.response :json, parser_options: { symbolize_names: true }
|
59
117
|
end
|
118
|
+
|
119
|
+
@fmrest_connection = connection if memoize
|
120
|
+
|
121
|
+
connection
|
60
122
|
end
|
123
|
+
|
124
|
+
def fmrest_config_overlay_key
|
125
|
+
:"#{object_id}.fmrest_config_overlay"
|
126
|
+
end
|
127
|
+
end
|
128
|
+
|
129
|
+
def fmrest_config
|
130
|
+
self.class.fmrest_config
|
61
131
|
end
|
62
132
|
end
|
63
133
|
end
|
@@ -0,0 +1,40 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module FmRest
|
4
|
+
module Spyke
|
5
|
+
module Model
|
6
|
+
module GlobalFields
|
7
|
+
extend ::ActiveSupport::Concern
|
8
|
+
|
9
|
+
FULLY_QUALIFIED_FIELD_NAME_MATCHER = /\A[^:]+::[^:]+\Z/.freeze
|
10
|
+
|
11
|
+
class_methods do
|
12
|
+
def set_globals(values_hash)
|
13
|
+
connection.patch(FmRest::V1.globals_path, {
|
14
|
+
globalFields: normalize_globals_hash(values_hash)
|
15
|
+
})
|
16
|
+
end
|
17
|
+
|
18
|
+
private
|
19
|
+
|
20
|
+
def normalize_globals_hash(hash)
|
21
|
+
hash.each_with_object({}) do |(k, v), normalized|
|
22
|
+
if v.kind_of?(Hash)
|
23
|
+
v.each do |k2, v2|
|
24
|
+
normalized["#{k}::#{k2}"] = v2
|
25
|
+
end
|
26
|
+
next
|
27
|
+
end
|
28
|
+
|
29
|
+
unless FULLY_QUALIFIED_FIELD_NAME_MATCHER === k.to_s
|
30
|
+
raise ArgumentError, "global fields must be given in fully qualified format (table name::field name)"
|
31
|
+
end
|
32
|
+
|
33
|
+
normalized[k] = v
|
34
|
+
end
|
35
|
+
end
|
36
|
+
end
|
37
|
+
end
|
38
|
+
end
|
39
|
+
end
|
40
|
+
end
|
@@ -24,7 +24,8 @@ module FmRest
|
|
24
24
|
# Methods delegated to FmRest::Spyke::Relation
|
25
25
|
delegate :limit, :offset, :sort, :order, :query, :omit, :portal,
|
26
26
|
:portals, :includes, :with_all_portals, :without_portals,
|
27
|
-
:script, :find_one, :
|
27
|
+
:script, :find_one, :first, :any, :find_some,
|
28
|
+
:find_in_batches, :find_each, to: :all
|
28
29
|
|
29
30
|
def all
|
30
31
|
# Use FmRest's Relation instead of Spyke's vanilla one
|
@@ -4,8 +4,8 @@ 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"
|
7
|
+
FM_DATE_FORMAT = "%m/%d/%Y"
|
8
|
+
FM_DATETIME_FORMAT = "#{FM_DATE_FORMAT} %H:%M:%S"
|
9
9
|
|
10
10
|
# Override Spyke's to_params to return FM Data API's expected JSON
|
11
11
|
# format, and including only modified fields
|
@@ -63,9 +63,9 @@ module FmRest
|
|
63
63
|
def serialize_values!(params)
|
64
64
|
params.transform_values! do |value|
|
65
65
|
case value
|
66
|
-
when DateTime, Time
|
67
|
-
value.strftime(FM_DATETIME_FORMAT)
|
68
|
-
when Date
|
66
|
+
when DateTime, Time, FmRest::StringDateTime
|
67
|
+
convert_datetime_timezone(value.to_datetime).strftime(FM_DATETIME_FORMAT)
|
68
|
+
when Date, FmRest::StringDate
|
69
69
|
value.strftime(FM_DATE_FORMAT)
|
70
70
|
else
|
71
71
|
value
|
@@ -74,6 +74,17 @@ module FmRest
|
|
74
74
|
|
75
75
|
params
|
76
76
|
end
|
77
|
+
|
78
|
+
def convert_datetime_timezone(dt)
|
79
|
+
case fmrest_config.timezone
|
80
|
+
when :utc, "utc"
|
81
|
+
dt.new_offset(0)
|
82
|
+
when :local, "local"
|
83
|
+
dt.new_offset(FmRest::V1.local_offset_for_datetime(dt))
|
84
|
+
when nil
|
85
|
+
dt
|
86
|
+
end
|
87
|
+
end
|
77
88
|
end
|
78
89
|
end
|
79
90
|
end
|
@@ -190,6 +190,79 @@ module FmRest
|
|
190
190
|
rescue ::Spyke::ConnectionError => error
|
191
191
|
fallback_or_reraise(error, default: nil)
|
192
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
|
193
266
|
|
194
267
|
protected
|
195
268
|
|
@@ -1,9 +1,21 @@
|
|
1
1
|
# frozen_string_literal: true
|
2
2
|
|
3
3
|
require "json"
|
4
|
+
require "ostruct"
|
4
5
|
|
5
6
|
module FmRest
|
6
7
|
module Spyke
|
8
|
+
# Metadata class to be passed to Spyke::Collection#metadata
|
9
|
+
class Metadata < Struct.new(:messages, :script, :data_info)
|
10
|
+
alias_method :scripts, :script
|
11
|
+
end
|
12
|
+
|
13
|
+
class DataInfo < OpenStruct
|
14
|
+
def total_record_count; totalRecordCount; end
|
15
|
+
def found_count; foundCount; end
|
16
|
+
def returned_count; returnedCount; end
|
17
|
+
end
|
18
|
+
|
7
19
|
# Response Faraday middleware for converting FM API's response JSON into
|
8
20
|
# Spyke's expected format
|
9
21
|
class SpykeFormatter < ::Faraday::Response::Middleware
|
@@ -77,36 +89,61 @@ module FmRest
|
|
77
89
|
|
78
90
|
# @param json [Hash]
|
79
91
|
# @param include_errors [Boolean]
|
80
|
-
# @return [
|
92
|
+
# @return [FmRest::Spyke::Metadata] the skeleton structure for a
|
93
|
+
# Spyke-formatted response
|
81
94
|
def build_base_hash(json, include_errors = false)
|
82
95
|
{
|
83
|
-
metadata:
|
84
|
-
|
96
|
+
metadata: Metadata.new(
|
97
|
+
prepare_messages(json),
|
98
|
+
prepare_script_results(json),
|
99
|
+
prepare_data_info(json)
|
100
|
+
).freeze,
|
101
|
+
errors: include_errors ? prepare_errors(json) : {}
|
85
102
|
}
|
86
103
|
end
|
87
104
|
|
88
105
|
# @param json [Hash]
|
89
|
-
# @return [
|
106
|
+
# @return [Array<OpenStruct>] the skeleton structure for a
|
107
|
+
# Spyke-formatted response
|
108
|
+
def prepare_messages(json)
|
109
|
+
return [] unless json[:messages]
|
110
|
+
json[:messages].map { |m| OpenStruct.new(m).freeze }.freeze
|
111
|
+
end
|
112
|
+
|
113
|
+
# @param json [Hash]
|
114
|
+
# @return [OpenStruct] the script(s) execution results for Spyke metadata
|
115
|
+
# format
|
90
116
|
def prepare_script_results(json)
|
91
117
|
results = {}
|
92
118
|
|
93
119
|
[:prerequest, :presort].each do |s|
|
94
120
|
if json[:response][:"scriptError.#{s}"]
|
95
|
-
results[s] =
|
121
|
+
results[s] = OpenStruct.new(
|
96
122
|
result: json[:response][:"scriptResult.#{s}"],
|
97
123
|
error: json[:response][:"scriptError.#{s}"]
|
98
|
-
|
124
|
+
).freeze
|
99
125
|
end
|
100
126
|
end
|
101
127
|
|
102
128
|
if json[:response][:scriptError]
|
103
|
-
results[:after] =
|
129
|
+
results[:after] = OpenStruct.new(
|
104
130
|
result: json[:response][:scriptResult],
|
105
131
|
error: json[:response][:scriptError]
|
106
|
-
|
132
|
+
).freeze
|
107
133
|
end
|
108
134
|
|
109
|
-
results
|
135
|
+
results.present? ? OpenStruct.new(results).freeze : nil
|
136
|
+
end
|
137
|
+
|
138
|
+
# @param json [Hash]
|
139
|
+
# @return [OpenStruct] the script(s) execution results for
|
140
|
+
# Spyke metadata format
|
141
|
+
def prepare_data_info(json)
|
142
|
+
data_info = json[:response] && json[:response][:dataInfo]
|
143
|
+
|
144
|
+
return nil unless data_info.present?
|
145
|
+
|
146
|
+
DataInfo.new(data_info).freeze
|
110
147
|
end
|
111
148
|
|
112
149
|
# @param json [Hash]
|