fmrest-spyke 0.17.1 → 0.18.0

Sign up to get free protection for your applications and to get access to all the features.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 19e4983cff9d5959b2e9263687daaf18d779fcf6d284cbb6ec8844c1623beabf
4
- data.tar.gz: 0e558a94cf0b26f5ffe1258f33385f0e016a73ae55aa200ea6768160c746ebd1
3
+ metadata.gz: 19357358bdb618c8e8f600c7a427bd7b779d17a192145e040f455403fe53e000
4
+ data.tar.gz: da2227cf47f7c920e367fdfda08bb9e0ac19e696b73930beacf98d975dac8fee
5
5
  SHA512:
6
- metadata.gz: 1af0c0918fe48374fd2dbe5f20bbeb8f0f7e9f73537233f9b530ea53ec37c3ceffba5612c244ce0c9553c66a2090dcd1d8330581940f6713660cbc6b1a362dd0
7
- data.tar.gz: 75abe05bdbfec9e21d0bc3e6dcc5c10d3bbdb66213382c494e3b7511f4c00c628b5ad054ca81e486c4c614a3c9832f60787000a5d990399488ce7027c59a45e1
6
+ metadata.gz: b00414dc29c9ec01317b47210fd4808f766fcaaa2744ed56ffd943180dd80e994c67b7749c3a619b5d1176e7c5bb3bf0b884f82517617e3e39b963e4094d84e7
7
+ data.tar.gz: 45ba0c02eede56150923029863a6a6301271967b8c6cde342b0ab1baf6c6089f7452f763e463138e4095a6b2b9de1178e8135b0e1d045ed00913b774780a531f
data/CHANGELOG.md CHANGED
@@ -1,5 +1,17 @@
1
1
  ## Changelog
2
2
 
3
+ ### 0.18.0
4
+
5
+ * Better support for portals with mismatching field qualifiers
6
+ * Better ergonomics for script execution, improved documentation
7
+ * Defining an attribute on a model that would collide with an existing method
8
+ now raises an error
9
+ * Cleared Faraday deprecation messages on authentication methods
10
+ * Handle FileMaker Cloud case where HTTP 401 Unauthorized with content-type
11
+ text/html is returned after token expiry
12
+ * Add retry option to Rescuable mixin
13
+ * Added fmrest-ruby/VERSION to User-Agent headers
14
+
3
15
  ### 0.17.1
4
16
 
5
17
  * Fixed crash when `fmid_token` is set but `username` isn't
data/README.md CHANGED
@@ -543,6 +543,10 @@ class LoggyBee < FmRest::Layout
543
543
  end
544
544
  ```
545
545
 
546
+ ## Gotchas
547
+
548
+ Read about unexpected scenarios in the [gotchas doc](docs/Gotchas.md).
549
+
546
550
  ## API implementation completeness table
547
551
 
548
552
  FM Data API reference: https://fmhelp.filemaker.com/docs/18/en/dataapi/
@@ -1,5 +1,6 @@
1
1
  # frozen_string_literal: true
2
2
 
3
+ require "fmrest/spyke/portal_builder"
3
4
  require "fmrest/spyke/portal"
4
5
 
5
6
  module FmRest
@@ -13,6 +14,8 @@ module FmRest
13
14
  included do
14
15
  # Keep track of portal options by their FM keys as we could need it
15
16
  # to parse the portalData JSON in SpykeFormatter
17
+ #
18
+ # TODO: Replace this with options in PortalBuilder
16
19
  class_attribute :portal_options, instance_accessor: false, instance_predicate: false
17
20
 
18
21
  # class_attribute supports a :default option since ActiveSupport 5.2,
@@ -40,11 +43,13 @@ module FmRest
40
43
  # end
41
44
  #
42
45
  def has_portal(name, options = {})
43
- create_association(name, Portal, options)
46
+ # This is analogous to Spyke's create_association method, but using
47
+ # our custom builder instead
48
+ self.associations = associations.merge(name => PortalBuilder.new(self, name, Portal, options))
44
49
 
45
50
  # Store options for SpykeFormatter to use if needed
46
51
  portal_key = options[:portal_key] || name
47
- self.portal_options = portal_options.merge(portal_key.to_s => options.dup.merge(name: name.to_s)).freeze
52
+ self.portal_options = portal_options.merge(portal_key.to_s => options.dup.merge(name: name.to_s).freeze).freeze
48
53
 
49
54
  define_method "#{name.to_s.singularize}_ids" do
50
55
  association(name).map(&:id)
@@ -94,11 +94,17 @@ module FmRest
94
94
  end
95
95
 
96
96
  def _fmrest_define_attribute(from, to)
97
+ if existing_method = ((method_defined?(from) || private_method_defined?(from)) && from) ||
98
+ ((method_defined?("#{from}=") || private_method_defined?("#{from}=")) && "#{from}=")
99
+
100
+ raise ArgumentError, "You tried to define an attribute named `#{from}' on `#{name}', but this will generate a instance method `#{existing_method}', which is already defined by FmRest::Layout."
101
+ end
102
+
97
103
  # We use a setter here instead of injecting the hash key/value pair
98
104
  # directly with #[]= so that we don't change the mapped_attributes
99
105
  # hash on the parent class. The resulting hash is frozen for the
100
106
  # same reason.
101
- self.mapped_attributes = mapped_attributes.merge(from => to).freeze
107
+ self.mapped_attributes = mapped_attributes.merge(from => to.to_s).freeze
102
108
 
103
109
  _fmrest_attribute_methods_container.module_eval do
104
110
  define_method(from) do
@@ -140,8 +140,8 @@ module FmRest
140
140
 
141
141
  conn.use FmRest::V1::TypeCoercer, config
142
142
 
143
- # FmRest::Spyke::JsonParse expects symbol keys
144
- conn.response :json, parser_options: { symbolize_names: true }
143
+ # FmRest::Spyke::SpykeFormatter expects symbol keys
144
+ conn.response :json, parser_options: { symbolize_names: true }, content_type: /\bjson$/
145
145
  end
146
146
 
147
147
  @fmrest_connection = connection if memoize
@@ -6,8 +6,6 @@ module FmRest
6
6
  module GlobalFields
7
7
  extend ::ActiveSupport::Concern
8
8
 
9
- FULLY_QUALIFIED_FIELD_NAME_MATCHER = /\A[^:]+::[^:]+\Z/.freeze
10
-
11
9
  class_methods do
12
10
  def set_globals(values_hash)
13
11
  connection.patch(FmRest::V1.globals_path, {
@@ -26,7 +24,7 @@ module FmRest
26
24
  next
27
25
  end
28
26
 
29
- unless FULLY_QUALIFIED_FIELD_NAME_MATCHER === k.to_s
27
+ unless V1.is_fully_qualified?(k.to_s)
30
28
  raise ArgumentError, "global fields must be given in fully qualified format (table name::field name)"
31
29
  end
32
30
 
@@ -78,18 +78,6 @@ module FmRest
78
78
  new(attributes).tap(&:save!)
79
79
  end
80
80
 
81
- # Requests execution of a FileMaker script.
82
- #
83
- # @param script_name [String] the name of the FileMaker script to
84
- # execute
85
- # @param param [String] an optional paramater for the script
86
- #
87
- def execute_script(script_name, param: nil)
88
- params = {}
89
- params = {"script.param" => param} unless param.nil?
90
- request(:get, FmRest::V1::script_path(layout, script_name), params)
91
- end
92
-
93
81
  private
94
82
 
95
83
  def extend_scope_with_fm_params(scope, prefixed: false)
@@ -3,9 +3,46 @@
3
3
  module FmRest
4
4
  module Spyke
5
5
  module Model
6
+ # This mixin allows rescuing from errors raised during HTTP requests,
7
+ # with optional retry (useful for solving expired auth). This is based
8
+ # off ActiveSupport::Rescuable, with minimal added functionality. Its
9
+ # usage is analogous to `rescue_from` in Rails' controllers.
10
+ #
11
+ # Example usage:
12
+ #
13
+ # MyLayout < FmRest::Layout
14
+ # # Mix-in module
15
+ # include FmRest::Spyke::Model::Rescuable
16
+ #
17
+ # # Define an error handler
18
+ # rescue_from FmRest::APIError::SomeError, with: :report_error
19
+ #
20
+ # # Define block-based error handler
21
+ # rescue_from FmRest::APIError::SomeOtherError, with: -> { ... }
22
+ #
23
+ # private
24
+ #
25
+ # def report_error(exception)
26
+ # ErrorNotifier.notify(exception)
27
+ # ene
28
+ # end
29
+ #
30
+ # This module also extends upon ActiveSupport's implementation by
31
+ # allowing to request a retry of the failed request, which can be useful
32
+ # in situations where an auth token has expired and credentials need to
33
+ # be manually reset.
34
+ #
35
+ # To request a retry use `throw :retry` within the handler method.
36
+ #
37
+ # Finally, since it's the most common use case, there's a shorthand
38
+ # method for handling Data API authentication errors:
39
+ #
40
+ # rescue_account_error with: -> { CredentialsManager.refresh_credentials }
41
+ #
42
+ # This method will always issue a retry.
43
+ #
6
44
  module Rescuable
7
45
  extend ::ActiveSupport::Concern
8
-
9
46
  include ::ActiveSupport::Rescuable
10
47
 
11
48
  class_methods do
@@ -13,12 +50,19 @@ module FmRest
13
50
  begin
14
51
  super
15
52
  rescue => e
16
- rescue_with_handler(e) || raise
53
+ catch :retry do
54
+ rescue_with_handler(e) || raise
55
+ return
56
+ end
57
+ super
17
58
  end
18
59
  end
19
60
 
20
- def rescue_account_error(with: nil, &block)
21
- rescue_from APIError::AccountError, with: with, &block
61
+ def rescue_account_error(with: nil)
62
+ rescue_from(APIError::AccountError, with: with) do
63
+ yield
64
+ throw :retry
65
+ end
22
66
  end
23
67
  end
24
68
  end
@@ -0,0 +1,64 @@
1
+ # frozen_string_literal: true
2
+
3
+ module FmRest
4
+ module Spyke
5
+ module Model
6
+ # This module adds and extends various ORM features in Spyke models,
7
+ # including custom query methods, remote script execution and
8
+ # exception-raising persistence methods.
9
+ #
10
+ module ScriptExecution
11
+ extend ::ActiveSupport::Concern
12
+
13
+ class_methods do
14
+ # Requests execution of a FileMaker script, returning its result
15
+ # object.
16
+ #
17
+ # @example
18
+ # response = MyLayout.execute("Uppercasing Script", "hello")
19
+ #
20
+ # response.result # => "HELLO"
21
+ # response.error # => "0"
22
+ # response.success? # => true
23
+ #
24
+ # @param script_name [String] the name of the FileMaker script to
25
+ # execute
26
+ # @param param [String] an optional paramater for the script
27
+ # @return [FmRest::Spyke::ScriptResult] the script result object
28
+ # containing the return value and error code
29
+ #
30
+ def execute(script_name, param = nil)
31
+ # Allow keyword argument format for compatibility with execute_script
32
+ if param.respond_to?(:has_key?) && param.has_key?(:param)
33
+ param = param[:param]
34
+ end
35
+
36
+ response = execute_script(script_name, param: param)
37
+ response.metadata.script.after
38
+ end
39
+
40
+ # Requests execution of a FileMaker script, returning the entire
41
+ # response object.
42
+ #
43
+ # The execution results will be in `response.metadata.script.after`
44
+ #
45
+ # In general you'd want to use the simpler `.execute` instead of this
46
+ # method, as it provides more direct access to the script results.
47
+ #
48
+ # @example
49
+ # response = MyLayout.execute_script("My Script", param: "hello")
50
+ #
51
+ # @param script_name [String] the name of the FileMaker script to
52
+ # execute
53
+ # @param param [String] an optional paramater for the script
54
+ # @return the complete response object
55
+ #
56
+ def execute_script(script_name, param: nil)
57
+ params = param.nil? ? {} : {"script.param" => param}
58
+ request(:get, FmRest::V1::script_path(layout, script_name), params)
59
+ end
60
+ end
61
+ end
62
+ end
63
+ end
64
+ end
@@ -26,7 +26,7 @@ module FmRest
26
26
  def serialize_for_portal(portal)
27
27
  params =
28
28
  changed_params.except(:__record_id).transform_keys do |key|
29
- "#{portal.attribute_prefix}::#{key}"
29
+ V1.is_fully_qualified?(key) ? key : "#{portal.attribute_prefix}::#{key}"
30
30
  end
31
31
 
32
32
  params[:recordId] = __record_id.to_s if __record_id
@@ -11,6 +11,7 @@ require "fmrest/spyke/model/container_fields"
11
11
  require "fmrest/spyke/model/global_fields"
12
12
  require "fmrest/spyke/model/http"
13
13
  require "fmrest/spyke/model/auth"
14
+ require "fmrest/spyke/model/script_execution"
14
15
 
15
16
  module FmRest
16
17
  module Spyke
@@ -28,6 +29,7 @@ module FmRest
28
29
  include GlobalFields
29
30
  include Http
30
31
  include Auth
32
+ include ScriptExecution
31
33
 
32
34
  autoload :Rescuable, "fmrest/spyke/model/rescuable"
33
35
  end
@@ -0,0 +1,17 @@
1
+ # frozen_string_literal: true
2
+
3
+ module FmRest
4
+ module Spyke
5
+ class PortalBuilder < ::Spyke::Associations::Builder
6
+ attr_reader :options
7
+
8
+ def klass
9
+ begin
10
+ super
11
+ rescue NameError => e
12
+ ::FmRest::Layout
13
+ end
14
+ end
15
+ end
16
+ end
17
+ end
@@ -1,12 +1,11 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- require "json"
4
3
  require "ostruct"
5
4
 
6
5
  module FmRest
7
6
  module Spyke
8
7
  # Metadata class to be passed to Spyke::Collection#metadata
9
- class Metadata < Struct.new(:messages, :script, :data_info)
8
+ Metadata = Struct.new(:messages, :script, :data_info) do
10
9
  alias_method :scripts, :script
11
10
  end
12
11
 
@@ -16,6 +15,11 @@ module FmRest
16
15
  def returned_count; returnedCount; end
17
16
  end
18
17
 
18
+ ScriptResult = Struct.new(:result, :error) do
19
+ def success?; error == "0"; end
20
+ def error?; !success?; end
21
+ end
22
+
19
23
  # Response Faraday middleware for converting FM API's response JSON into
20
24
  # Spyke's expected format
21
25
  class SpykeFormatter < ::Faraday::Response::Middleware
@@ -119,17 +123,18 @@ module FmRest
119
123
 
120
124
  [:prerequest, :presort].each do |s|
121
125
  if json[:response][:"scriptError.#{s}"]
122
- results[s] = OpenStruct.new(
123
- result: json[:response][:"scriptResult.#{s}"],
124
- error: json[:response][:"scriptError.#{s}"]
126
+ results[s] = ScriptResult.new(
127
+ json[:response][:"scriptResult.#{s}"],
128
+ json[:response][:"scriptError.#{s}"]
125
129
  ).freeze
126
130
  end
127
131
  end
128
132
 
133
+ # after/default script
129
134
  if json[:response][:scriptError]
130
- results[:after] = OpenStruct.new(
131
- result: json[:response][:scriptResult],
132
- error: json[:response][:scriptError]
135
+ results[:after] = ScriptResult.new(
136
+ json[:response][:scriptResult],
137
+ json[:response][:scriptError]
133
138
  ).freeze
134
139
  end
135
140
 
@@ -195,8 +200,8 @@ module FmRest
195
200
  out
196
201
  end
197
202
 
198
- # Extracts `recordId` and strips the `"PortalName::"` field prefix for each
199
- # portal
203
+ # Extracts `recordId` and strips the `"tableName::"` field qualifier for
204
+ # each portal
200
205
  #
201
206
  # Sample `json_portal_data`:
202
207
  #
@@ -210,19 +215,33 @@ module FmRest
210
215
  # @return [Hash] the portal data in Spyke format
211
216
  def prepare_portal_data(json_portal_data)
212
217
  json_portal_data.each_with_object({}) do |(portal_name, portal_records), out|
218
+
213
219
  portal_options = @model.portal_options[portal_name.to_s] || {}
220
+ portal_builder = portal_options[:name] && @model.associations[portal_options[:name].to_sym]
221
+ portal_class = portal_builder && portal_builder.klass
222
+ portal_attributes = (portal_class && portal_class.mapped_attributes.values) || []
214
223
 
215
224
  out[portal_name] =
216
225
  portal_records.map do |portal_fields|
217
226
  attributes = { __record_id: portal_fields[:recordId] }
218
227
  attributes[:__mod_id] = portal_fields[:modId] if portal_fields[:modId]
219
228
 
220
- prefix = portal_options[:attribute_prefix] || portal_name
221
- prefix_matcher = /\A#{prefix}::/
229
+ qualifier = portal_options[:attribute_prefix] || portal_name
230
+ qualifier_matcher = /\A#{qualifier}::/
222
231
 
223
232
  portal_fields.each do |k, v|
224
233
  next if :recordId == k || :modId == k
225
- attributes[k.to_s.gsub(prefix_matcher, "").to_sym] = v
234
+
235
+ stripped_field_name = k.to_s.gsub(qualifier_matcher, "")
236
+
237
+ # Only use the non-qualified attribute name if it was defined
238
+ # that way on the portal model, otherwise default to the fully
239
+ # qualified name
240
+ if portal_attributes.include?(stripped_field_name)
241
+ attributes[stripped_field_name.to_sym] = v
242
+ else
243
+ attributes[k.to_sym] = v
244
+ end
226
245
  end
227
246
 
228
247
  attributes
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: fmrest-spyke
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.17.1
4
+ version: 0.18.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Pedro Carbajal
8
8
  autorequire:
9
9
  bindir: bin
10
10
  cert_chain: []
11
- date: 2021-08-29 00:00:00.000000000 Z
11
+ date: 2021-09-16 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: fmrest-core
@@ -16,14 +16,14 @@ dependencies:
16
16
  requirements:
17
17
  - - '='
18
18
  - !ruby/object:Gem::Version
19
- version: 0.17.1
19
+ version: 0.18.0
20
20
  type: :runtime
21
21
  prerelease: false
22
22
  version_requirements: !ruby/object:Gem::Requirement
23
23
  requirements:
24
24
  - - '='
25
25
  - !ruby/object:Gem::Version
26
- version: 0.17.1
26
+ version: 0.18.0
27
27
  - !ruby/object:Gem::Dependency
28
28
  name: spyke
29
29
  requirement: !ruby/object:Gem::Requirement
@@ -65,9 +65,11 @@ files:
65
65
  - lib/fmrest/spyke/model/orm.rb
66
66
  - lib/fmrest/spyke/model/record_id.rb
67
67
  - lib/fmrest/spyke/model/rescuable.rb
68
+ - lib/fmrest/spyke/model/script_execution.rb
68
69
  - lib/fmrest/spyke/model/serialization.rb
69
70
  - lib/fmrest/spyke/model/uri.rb
70
71
  - lib/fmrest/spyke/portal.rb
72
+ - lib/fmrest/spyke/portal_builder.rb
71
73
  - lib/fmrest/spyke/relation.rb
72
74
  - lib/fmrest/spyke/spyke_formatter.rb
73
75
  - lib/fmrest/spyke/validation_error.rb