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 +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
|