fmrest 0.10.1 → 0.13.1

Sign up to get free protection for your applications and to get access to all the features.
Files changed (50) hide show
  1. checksums.yaml +4 -4
  2. data/.yardopts +2 -0
  3. data/CHANGELOG.md +36 -0
  4. data/README.md +193 -761
  5. metadata +71 -98
  6. data/.gitignore +0 -26
  7. data/.rspec +0 -3
  8. data/.travis.yml +0 -5
  9. data/Gemfile +0 -3
  10. data/Rakefile +0 -6
  11. data/fmrest.gemspec +0 -38
  12. data/lib/fmrest.rb +0 -29
  13. data/lib/fmrest/errors.rb +0 -28
  14. data/lib/fmrest/spyke.rb +0 -21
  15. data/lib/fmrest/spyke/base.rb +0 -23
  16. data/lib/fmrest/spyke/container_field.rb +0 -59
  17. data/lib/fmrest/spyke/model.rb +0 -36
  18. data/lib/fmrest/spyke/model/associations.rb +0 -82
  19. data/lib/fmrest/spyke/model/attributes.rb +0 -171
  20. data/lib/fmrest/spyke/model/auth.rb +0 -35
  21. data/lib/fmrest/spyke/model/connection.rb +0 -74
  22. data/lib/fmrest/spyke/model/container_fields.rb +0 -25
  23. data/lib/fmrest/spyke/model/global_fields.rb +0 -40
  24. data/lib/fmrest/spyke/model/http.rb +0 -37
  25. data/lib/fmrest/spyke/model/orm.rb +0 -212
  26. data/lib/fmrest/spyke/model/serialization.rb +0 -91
  27. data/lib/fmrest/spyke/model/uri.rb +0 -30
  28. data/lib/fmrest/spyke/portal.rb +0 -55
  29. data/lib/fmrest/spyke/relation.rb +0 -359
  30. data/lib/fmrest/spyke/spyke_formatter.rb +0 -273
  31. data/lib/fmrest/spyke/validation_error.rb +0 -25
  32. data/lib/fmrest/string_date.rb +0 -220
  33. data/lib/fmrest/token_store.rb +0 -6
  34. data/lib/fmrest/token_store/active_record.rb +0 -74
  35. data/lib/fmrest/token_store/base.rb +0 -25
  36. data/lib/fmrest/token_store/memory.rb +0 -26
  37. data/lib/fmrest/token_store/moneta.rb +0 -41
  38. data/lib/fmrest/token_store/redis.rb +0 -45
  39. data/lib/fmrest/v1.rb +0 -21
  40. data/lib/fmrest/v1/connection.rb +0 -91
  41. data/lib/fmrest/v1/container_fields.rb +0 -114
  42. data/lib/fmrest/v1/dates.rb +0 -81
  43. data/lib/fmrest/v1/paths.rb +0 -47
  44. data/lib/fmrest/v1/raise_errors.rb +0 -57
  45. data/lib/fmrest/v1/token_session.rb +0 -142
  46. data/lib/fmrest/v1/token_store/active_record.rb +0 -13
  47. data/lib/fmrest/v1/token_store/memory.rb +0 -13
  48. data/lib/fmrest/v1/type_coercer.rb +0 -192
  49. data/lib/fmrest/v1/utils.rb +0 -95
  50. data/lib/fmrest/version.rb +0 -5
@@ -1,359 +0,0 @@
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
@@ -1,273 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- require "json"
4
- require "ostruct"
5
-
6
- module FmRest
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
-
19
- # Response Faraday middleware for converting FM API's response JSON into
20
- # Spyke's expected format
21
- class SpykeFormatter < ::Faraday::Response::Middleware
22
- SINGLE_RECORD_RE = %r(/records/\d+\z).freeze
23
- MULTIPLE_RECORDS_RE = %r(/records\z).freeze
24
- CONTAINER_RE = %r(/records/\d+/containers/[^/]+/\d+\z).freeze
25
- FIND_RECORDS_RE = %r(/_find\b).freeze
26
- SCRIPT_REQUEST_RE = %r(/script/[^/]+\z).freeze
27
-
28
- VALIDATION_ERROR_RANGE = 500..599
29
-
30
- # @param app [#call]
31
- # @param model [Class<FmRest::Spyke::Base>]
32
- def initialize(app, model)
33
- super(app)
34
- @model = model
35
- end
36
-
37
- # @param env [Faraday::Env]
38
- def on_complete(env)
39
- return unless env.body.is_a?(Hash)
40
-
41
- json = env.body
42
-
43
- case
44
- when single_record_request?(env)
45
- env.body = prepare_single_record(json)
46
- when multiple_records_request?(env), find_request?(env)
47
- env.body = prepare_collection(json)
48
- when create_request?(env), update_request?(env), delete_request?(env), container_upload_request?(env)
49
- env.body = prepare_save_response(json)
50
- when execute_script_request?(env)
51
- env.body = build_base_hash(json)
52
- else
53
- # Attempt to parse unknown requests too
54
- env.body = build_base_hash(json)
55
- end
56
- end
57
-
58
- private
59
-
60
- # @param json [Hash]
61
- # @return [Hash] the response in Spyke format
62
- def prepare_save_response(json)
63
- response = json[:response]
64
-
65
- data = {}
66
- data[:mod_id] = response[:modId] if response[:modId]
67
- data[:id] = response[:recordId].to_i if response[:recordId]
68
-
69
- build_base_hash(json, true).merge!(data: data)
70
- end
71
-
72
- # (see #prepare_save_response)
73
- def prepare_single_record(json)
74
- data =
75
- json[:response][:data] &&
76
- prepare_record_data(json[:response][:data].first)
77
-
78
- build_base_hash(json).merge!(data: data)
79
- end
80
-
81
- # (see #prepare_save_response)
82
- def prepare_collection(json)
83
- data =
84
- json[:response][:data] &&
85
- json[:response][:data].map { |record_data| prepare_record_data(record_data) }
86
-
87
- build_base_hash(json).merge!(data: data)
88
- end
89
-
90
- # @param json [Hash]
91
- # @param include_errors [Boolean]
92
- # @return [FmRest::Spyke::Metadata] the skeleton structure for a
93
- # Spyke-formatted response
94
- def build_base_hash(json, include_errors = false)
95
- {
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) : {}
102
- }
103
- end
104
-
105
- # @param json [Hash]
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
116
- def prepare_script_results(json)
117
- results = {}
118
-
119
- [:prerequest, :presort].each do |s|
120
- if json[:response][:"scriptError.#{s}"]
121
- results[s] = OpenStruct.new(
122
- result: json[:response][:"scriptResult.#{s}"],
123
- error: json[:response][:"scriptError.#{s}"]
124
- ).freeze
125
- end
126
- end
127
-
128
- if json[:response][:scriptError]
129
- results[:after] = OpenStruct.new(
130
- result: json[:response][:scriptResult],
131
- error: json[:response][:scriptError]
132
- ).freeze
133
- end
134
-
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
147
- end
148
-
149
- # @param json [Hash]
150
- # @return [Hash] the errors hash in Spyke format
151
- def prepare_errors(json)
152
- # Code 0 means "No Error"
153
- # https://fmhelp.filemaker.com/help/17/fmp/en/index.html#page/FMP_Help/error-codes.html
154
- return {} if json[:messages][0][:code].to_i == 0
155
-
156
- json[:messages].each_with_object(base: []) do |message, hash|
157
- # Only include validation errors
158
- next unless VALIDATION_ERROR_RANGE.include?(message[:code].to_i)
159
-
160
- hash[:base] << "#{message[:message]} (#{message[:code]})"
161
- end
162
- end
163
-
164
- # `json_data` is expected in this format:
165
- #
166
- # {
167
- # "fieldData": {
168
- # "fieldName1" : "fieldValue1",
169
- # "fieldName2" : "fieldValue2",
170
- # ...
171
- # },
172
- # "portalData": {
173
- # "portal1" : [
174
- # { <portalRecord1> },
175
- # { <portalRecord2> },
176
- # ...
177
- # ],
178
- # "portal2" : [
179
- # { <portalRecord1> },
180
- # { <portalRecord2> },
181
- # ...
182
- # ]
183
- # },
184
- # "modId": <Id_for_last_modification>,
185
- # "recordId": <Unique_internal_ID_for_this_record>
186
- # }
187
- #
188
- # @param json_data [Hash]
189
- # @return [Hash] the record data in Spyke format
190
- def prepare_record_data(json_data)
191
- out = { id: json_data[:recordId].to_i, mod_id: json_data[:modId] }
192
- out.merge!(json_data[:fieldData])
193
- out.merge!(prepare_portal_data(json_data[:portalData])) if json_data[:portalData]
194
- out
195
- end
196
-
197
- # Extracts `recordId` and strips the `"PortalName::"` field prefix for each
198
- # portal
199
- #
200
- # Sample `json_portal_data`:
201
- #
202
- # "portalData": {
203
- # "Orders":[
204
- # { "Orders::DeliveryDate": "3/7/2017", "recordId": "23" }
205
- # ]
206
- # }
207
- #
208
- # @param json_portal_data [Hash]
209
- # @return [Hash] the portal data in Spyke format
210
- def prepare_portal_data(json_portal_data)
211
- json_portal_data.each_with_object({}) do |(portal_name, portal_records), out|
212
- portal_options = @model.portal_options[portal_name.to_s] || {}
213
-
214
- out[portal_name] =
215
- portal_records.map do |portal_fields|
216
- attributes = { id: portal_fields[:recordId].to_i }
217
- attributes[:mod_id] = portal_fields[:modId] if portal_fields[:modId]
218
-
219
- prefix = portal_options[:attribute_prefix] || portal_name
220
- prefix_matcher = /\A#{prefix}::/
221
-
222
- portal_fields.each do |k, v|
223
- next if :recordId == k || :modId == k
224
- attributes[k.to_s.gsub(prefix_matcher, "").to_sym] = v
225
- end
226
-
227
- attributes
228
- end
229
- end
230
- end
231
-
232
- # @param env [Faraday::Env]
233
- # @return [Boolean]
234
- def single_record_request?(env)
235
- env.method == :get && env.url.path.match(SINGLE_RECORD_RE)
236
- end
237
-
238
- # (see #single_record_request?)
239
- def multiple_records_request?(env)
240
- env.method == :get && env.url.path.match(MULTIPLE_RECORDS_RE)
241
- end
242
-
243
- # (see #single_record_request?)
244
- def find_request?(env)
245
- env.method == :post && env.url.path.match(FIND_RECORDS_RE)
246
- end
247
-
248
- # (see #single_record_request?)
249
- def update_request?(env)
250
- env.method == :patch && env.url.path.match(SINGLE_RECORD_RE)
251
- end
252
-
253
- # (see #single_record_request?)
254
- def create_request?(env)
255
- env.method == :post && env.url.path.match(MULTIPLE_RECORDS_RE)
256
- end
257
-
258
- # (see #single_record_request?)
259
- def container_upload_request?(env)
260
- env.method == :post && env.url.path.match(CONTAINER_RE)
261
- end
262
-
263
- # (see #single_record_request?)
264
- def delete_request?(env)
265
- env.method == :delete && env.url.path.match(SINGLE_RECORD_RE)
266
- end
267
-
268
- def execute_script_request?(env)
269
- env.method == :get && env.url.path.match(SCRIPT_REQUEST_RE)
270
- end
271
- end
272
- end
273
- end