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 +4 -4
- data/CHANGELOG.md +12 -0
- data/README.md +4 -0
- data/lib/fmrest/spyke/model/associations.rb +7 -2
- data/lib/fmrest/spyke/model/attributes.rb +7 -1
- data/lib/fmrest/spyke/model/connection.rb +2 -2
- data/lib/fmrest/spyke/model/global_fields.rb +1 -3
- data/lib/fmrest/spyke/model/orm.rb +0 -12
- data/lib/fmrest/spyke/model/rescuable.rb +48 -4
- data/lib/fmrest/spyke/model/script_execution.rb +64 -0
- data/lib/fmrest/spyke/model/serialization.rb +1 -1
- data/lib/fmrest/spyke/model.rb +2 -0
- data/lib/fmrest/spyke/portal_builder.rb +17 -0
- data/lib/fmrest/spyke/spyke_formatter.rb +32 -13
- metadata +6 -4
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 19357358bdb618c8e8f600c7a427bd7b779d17a192145e040f455403fe53e000
|
4
|
+
data.tar.gz: da2227cf47f7c920e367fdfda08bb9e0ac19e696b73930beacf98d975dac8fee
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
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
|
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::
|
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
|
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
|
-
|
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
|
21
|
-
rescue_from
|
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
|
data/lib/fmrest/spyke/model.rb
CHANGED
@@ -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
|
-
|
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] =
|
123
|
-
|
124
|
-
|
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] =
|
131
|
-
|
132
|
-
|
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 `"
|
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
|
-
|
221
|
-
|
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
|
-
|
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.
|
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-
|
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.
|
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.
|
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
|