fmrest-spyke 0.17.1 → 0.18.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 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