fmrest 0.9.0 → 0.12.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 +35 -0
- data/README.md +94 -17
- data/UPGRADING +15 -0
- data/fmrest.gemspec +16 -5
- data/lib/fmrest.rb +10 -3
- data/lib/fmrest/connection_settings.rb +124 -0
- data/lib/fmrest/errors.rb +2 -0
- data/lib/fmrest/spyke/base.rb +0 -12
- data/lib/fmrest/spyke/container_field.rb +2 -2
- data/lib/fmrest/spyke/model.rb +3 -6
- data/lib/fmrest/spyke/model/associations.rb +15 -11
- data/lib/fmrest/spyke/model/attributes.rb +21 -29
- data/lib/fmrest/spyke/model/auth.rb +8 -0
- data/lib/fmrest/spyke/model/connection.rb +122 -24
- data/lib/fmrest/spyke/model/container_fields.rb +15 -0
- data/lib/fmrest/spyke/model/http.rb +42 -2
- data/lib/fmrest/spyke/model/orm.rb +61 -17
- data/lib/fmrest/spyke/model/record_id.rb +78 -0
- data/lib/fmrest/spyke/model/serialization.rb +26 -7
- data/lib/fmrest/spyke/model/uri.rb +3 -4
- data/lib/fmrest/spyke/spyke_formatter.rb +5 -5
- data/lib/fmrest/string_date.rb +46 -7
- data/lib/fmrest/token_store.rb +6 -0
- data/lib/fmrest/token_store/base.rb +3 -3
- data/lib/fmrest/token_store/null.rb +20 -0
- data/lib/fmrest/v1.rb +8 -4
- data/lib/fmrest/v1/auth.rb +30 -0
- data/lib/fmrest/v1/connection.rb +51 -25
- data/lib/fmrest/v1/dates.rb +81 -0
- data/lib/fmrest/v1/raise_errors.rb +3 -3
- data/lib/fmrest/v1/token_session.rb +41 -50
- data/lib/fmrest/v1/type_coercer.rb +111 -36
- data/lib/fmrest/v1/utils.rb +0 -17
- data/lib/fmrest/version.rb +1 -1
- metadata +41 -19
@@ -42,7 +42,7 @@ module FmRest
|
|
42
42
|
)
|
43
43
|
|
44
44
|
# Update mod id on record
|
45
|
-
@base.
|
45
|
+
@base.__mod_id = response.body[:data][:__mod_id]
|
46
46
|
|
47
47
|
true
|
48
48
|
end
|
@@ -52,7 +52,7 @@ module FmRest
|
|
52
52
|
# @param repetition [Integer]
|
53
53
|
# @return [String] the path for uploading a file to the container
|
54
54
|
def upload_path(repetition)
|
55
|
-
FmRest::V1.container_field_path(@base.class.layout, @base.
|
55
|
+
FmRest::V1.container_field_path(@base.class.layout, @base.__record_id, name, repetition)
|
56
56
|
end
|
57
57
|
end
|
58
58
|
end
|
data/lib/fmrest/spyke/model.rb
CHANGED
@@ -2,6 +2,7 @@
|
|
2
2
|
|
3
3
|
require "fmrest/spyke/model/connection"
|
4
4
|
require "fmrest/spyke/model/uri"
|
5
|
+
require "fmrest/spyke/model/record_id"
|
5
6
|
require "fmrest/spyke/model/attributes"
|
6
7
|
require "fmrest/spyke/model/serialization"
|
7
8
|
require "fmrest/spyke/model/associations"
|
@@ -17,7 +18,8 @@ module FmRest
|
|
17
18
|
extend ::ActiveSupport::Concern
|
18
19
|
|
19
20
|
include Connection
|
20
|
-
include
|
21
|
+
include URI
|
22
|
+
include RecordID
|
21
23
|
include Attributes
|
22
24
|
include Serialization
|
23
25
|
include Associations
|
@@ -26,11 +28,6 @@ module FmRest
|
|
26
28
|
include GlobalFields
|
27
29
|
include Http
|
28
30
|
include Auth
|
29
|
-
|
30
|
-
included do
|
31
|
-
# @return [Integer] the record's modId
|
32
|
-
attr_accessor :mod_id
|
33
|
-
end
|
34
31
|
end
|
35
32
|
end
|
36
33
|
end
|
@@ -5,6 +5,8 @@ require "fmrest/spyke/portal"
|
|
5
5
|
module FmRest
|
6
6
|
module Spyke
|
7
7
|
module Model
|
8
|
+
# This module adds portal support to Spyke models.
|
9
|
+
#
|
8
10
|
module Associations
|
9
11
|
extend ::ActiveSupport::Concern
|
10
12
|
|
@@ -22,17 +24,18 @@ module FmRest
|
|
22
24
|
end
|
23
25
|
|
24
26
|
class_methods do
|
25
|
-
# Based on
|
27
|
+
# Based on `has_many`, but creates a special Portal association
|
26
28
|
# instead.
|
27
29
|
#
|
28
|
-
#
|
30
|
+
# @option :portal_key [String] The key used for the portal in the FM
|
31
|
+
# Data JSON portalData
|
32
|
+
# @option :attribute_prefix [String] The prefix used for portal
|
33
|
+
# attributes in the FM Data JSON
|
29
34
|
#
|
30
|
-
#
|
31
|
-
#
|
32
|
-
#
|
33
|
-
#
|
34
|
-
#
|
35
|
-
# has_portal :jobs, portal_key: "JobsTable", attribute_prefix: "Job"
|
35
|
+
# @example
|
36
|
+
# class Person < FmRest::Spyke::Base
|
37
|
+
# has_portal :jobs, portal_key: "JobsTable", attribute_prefix: "Job"
|
38
|
+
# end
|
36
39
|
#
|
37
40
|
def has_portal(name, options = {})
|
38
41
|
create_association(name, Portal, options)
|
@@ -47,9 +50,8 @@ module FmRest
|
|
47
50
|
end
|
48
51
|
end
|
49
52
|
|
50
|
-
#
|
51
|
-
#
|
52
|
-
# each time.
|
53
|
+
# Spyke override -- Keep a cache of loaded portals. Spyke's default
|
54
|
+
# behavior is to reload the association each time.
|
53
55
|
#
|
54
56
|
def association(name)
|
55
57
|
@loaded_portals ||= {}
|
@@ -64,6 +66,8 @@ module FmRest
|
|
64
66
|
end
|
65
67
|
end
|
66
68
|
|
69
|
+
# Spyke override -- Add portals awareness
|
70
|
+
#
|
67
71
|
def reload(*_)
|
68
72
|
super.tap { @loaded_portals = nil }
|
69
73
|
end
|
@@ -5,6 +5,10 @@ require "fmrest/spyke/model/orm"
|
|
5
5
|
module FmRest
|
6
6
|
module Spyke
|
7
7
|
module Model
|
8
|
+
# Extends Spyke models with support for mapped attributes,
|
9
|
+
# `ActiveModel::Dirty` and forbidden attributes (e.g. Rails'
|
10
|
+
# `params.permit`).
|
11
|
+
#
|
8
12
|
module Attributes
|
9
13
|
extend ::ActiveSupport::Concern
|
10
14
|
|
@@ -20,9 +24,6 @@ module FmRest
|
|
20
24
|
# by Spyke
|
21
25
|
self.attribute_method_matchers.shift
|
22
26
|
|
23
|
-
# ActiveModel::Dirty methods for id
|
24
|
-
define_attribute_method(:id)
|
25
|
-
|
26
27
|
# Keep track of attribute mappings so we can get the FM field names
|
27
28
|
# for changed attributes
|
28
29
|
class_attribute :mapped_attributes, instance_writer: false, instance_predicate: false
|
@@ -36,10 +37,12 @@ module FmRest
|
|
36
37
|
end
|
37
38
|
|
38
39
|
class_methods do
|
40
|
+
# Spyke override
|
41
|
+
#
|
39
42
|
# Similar to Spyke::Base.attributes, but allows defining attribute
|
40
43
|
# methods that map to FM attributes with different names.
|
41
44
|
#
|
42
|
-
#
|
45
|
+
# @example
|
43
46
|
#
|
44
47
|
# class Person < Spyke::Base
|
45
48
|
# include FmRest::Spyke::Model
|
@@ -61,9 +64,10 @@ module FmRest
|
|
61
64
|
|
62
65
|
private
|
63
66
|
|
64
|
-
#
|
65
|
-
#
|
66
|
-
#
|
67
|
+
# Spyke override (private)
|
68
|
+
#
|
69
|
+
# Called whenever loading records from the HTTP API, so we can reset
|
70
|
+
# dirty info on freshly loaded records
|
67
71
|
#
|
68
72
|
# See: https://github.com/balvig/spyke/blob/master/lib/spyke/http.rb
|
69
73
|
#
|
@@ -88,8 +92,6 @@ module FmRest
|
|
88
92
|
end
|
89
93
|
|
90
94
|
def _fmrest_define_attribute(from, to)
|
91
|
-
raise ArgumentError, "attribute name `id' is reserved for the recordId" if from.to_s == "id"
|
92
|
-
|
93
95
|
# We use a setter here instead of injecting the hash key/value pair
|
94
96
|
# directly with #[]= so that we don't change the mapped_attributes
|
95
97
|
# hash on the parent class. The resulting hash is frozen for the
|
@@ -112,35 +114,25 @@ module FmRest
|
|
112
114
|
end
|
113
115
|
end
|
114
116
|
|
115
|
-
|
116
|
-
|
117
|
-
super
|
118
|
-
end
|
119
|
-
|
117
|
+
# Spyke override -- Adds AM::Dirty support
|
118
|
+
#
|
120
119
|
def reload(*args)
|
121
120
|
super.tap { |r| clear_changes_information }
|
122
121
|
end
|
123
122
|
|
123
|
+
# Spyke override -- Adds AM::Dirty support
|
124
|
+
#
|
124
125
|
def save(*args)
|
125
126
|
super.tap { |r| changes_applied_after_save if r }
|
126
127
|
end
|
127
128
|
|
128
|
-
#
|
129
|
-
#
|
130
|
-
# we override the instance variable name used by Spyke to avoid issues.
|
131
|
-
#
|
132
|
-
# TODO: Submit a pull request to Spyke so this isn't needed
|
133
|
-
#
|
134
|
-
def attributes
|
135
|
-
@_spyke_attributes
|
136
|
-
end
|
137
|
-
|
138
|
-
# In addition to the comments above on `attributes`, this also adds
|
139
|
-
# support for forbidden attributes
|
129
|
+
# Spyke override -- Adds support for forbidden attributes (i.e. Rails'
|
130
|
+
# `params.permit`, etc.)
|
140
131
|
#
|
141
132
|
def attributes=(new_attributes)
|
142
|
-
@
|
143
|
-
|
133
|
+
@spyke_attributes ||= ::Spyke::Attributes.new(scope.params)
|
134
|
+
return unless new_attributes && !new_attributes.empty?
|
135
|
+
use_setters(sanitize_for_mass_assignment(new_attributes))
|
144
136
|
end
|
145
137
|
|
146
138
|
private
|
@@ -153,7 +145,7 @@ module FmRest
|
|
153
145
|
mapped_attributes.values_at(*changed)
|
154
146
|
end
|
155
147
|
|
156
|
-
# Use known mapped_attributes for inspect
|
148
|
+
# Spyke override (private) -- Use known mapped_attributes for inspect
|
157
149
|
#
|
158
150
|
def inspect_attributes
|
159
151
|
mapped_attributes.except(primary_key).map do |k, v|
|
@@ -28,6 +28,14 @@ module FmRest
|
|
28
28
|
rescue FmRest::V1::TokenSession::NoSessionTokenSet
|
29
29
|
false
|
30
30
|
end
|
31
|
+
|
32
|
+
def request_auth_token
|
33
|
+
FmRest::V1.request_auth_token(FmRest::V1.auth_connection(fmrest_config))
|
34
|
+
end
|
35
|
+
|
36
|
+
def request_auth_token!
|
37
|
+
FmRest::V1.request_auth_token!(FmRest::V1.auth_connection(fmrest_config))
|
38
|
+
end
|
31
39
|
end
|
32
40
|
end
|
33
41
|
end
|
@@ -3,33 +3,112 @@
|
|
3
3
|
module FmRest
|
4
4
|
module Spyke
|
5
5
|
module Model
|
6
|
+
# This module provides methods for configuring the Farday connection for
|
7
|
+
# the model, as well as setting up the connection itself.
|
8
|
+
#
|
6
9
|
module Connection
|
7
|
-
extend
|
10
|
+
extend ActiveSupport::Concern
|
8
11
|
|
9
12
|
included do
|
10
|
-
class_attribute :fmrest_config, instance_accessor: false, instance_predicate: false
|
11
|
-
|
12
13
|
class_attribute :faraday_block, instance_accessor: false, instance_predicate: false
|
13
14
|
class << self; private :faraday_block, :faraday_block=; end
|
14
15
|
|
15
|
-
# FM Data API expects PATCH for updates (Spyke
|
16
|
+
# FM Data API expects PATCH for updates (Spyke uses PUT by default)
|
16
17
|
self.callback_methods = { create: :post, update: :patch }.freeze
|
17
18
|
end
|
18
19
|
|
19
20
|
class_methods do
|
21
|
+
def fmrest_config
|
22
|
+
if fmrest_config_overlay
|
23
|
+
return FmRest.default_connection_settings.merge(fmrest_config_overlay, skip_validation: true)
|
24
|
+
end
|
25
|
+
|
26
|
+
FmRest.default_connection_settings
|
27
|
+
end
|
28
|
+
|
29
|
+
# Sets the FileMaker connection settings for the model.
|
30
|
+
#
|
31
|
+
# Behaves similar to ActiveSupport's `class_attribute`, so it can be
|
32
|
+
# inherited and safely overwritten in subclasses.
|
33
|
+
#
|
34
|
+
# @param settings [Hash] The settings hash
|
35
|
+
#
|
36
|
+
def fmrest_config=(settings)
|
37
|
+
settings = ConnectionSettings.new(settings, skip_validation: true)
|
38
|
+
|
39
|
+
singleton_class.redefine_method(:fmrest_config) do
|
40
|
+
overlay = fmrest_config_overlay
|
41
|
+
return settings.merge(overlay, skip_validation: true) if overlay
|
42
|
+
settings
|
43
|
+
end
|
44
|
+
end
|
45
|
+
|
46
|
+
# Allows overriding some connection settings in a thread-local
|
47
|
+
# manner. Useful in the use case where you want to connect to the
|
48
|
+
# same database using different accounts (e.g. credentials provided
|
49
|
+
# by users in a web app context).
|
50
|
+
#
|
51
|
+
# @param (see #fmrest_config=)
|
52
|
+
#
|
53
|
+
def fmrest_config_overlay=(settings)
|
54
|
+
Thread.current[fmrest_config_overlay_key] = settings
|
55
|
+
end
|
56
|
+
|
57
|
+
# @return [FmRest::ConnectionSettings] the connection settings
|
58
|
+
# overlay if any is in use
|
59
|
+
#
|
60
|
+
def fmrest_config_overlay
|
61
|
+
Thread.current[fmrest_config_overlay_key] || begin
|
62
|
+
superclass.fmrest_config_overlay
|
63
|
+
rescue NoMethodError
|
64
|
+
nil
|
65
|
+
end
|
66
|
+
end
|
67
|
+
|
68
|
+
# Clears the connection settings overlay.
|
69
|
+
#
|
70
|
+
def clear_fmrest_config_overlay
|
71
|
+
Thread.current[fmrest_config_overlay_key] = nil
|
72
|
+
end
|
73
|
+
|
74
|
+
# Runs a block of code in the context of the given connection
|
75
|
+
# settings without affecting the connection settings outside said
|
76
|
+
# block.
|
77
|
+
#
|
78
|
+
# @param (see #fmrest_config=)
|
79
|
+
#
|
80
|
+
# @example
|
81
|
+
# Honeybee.with_overlay(username: "...", password: "...") do
|
82
|
+
# Honeybee.query(...)
|
83
|
+
# end
|
84
|
+
#
|
85
|
+
def with_overlay(settings, &block)
|
86
|
+
Fiber.new do
|
87
|
+
begin
|
88
|
+
self.fmrest_config_overlay = settings
|
89
|
+
yield
|
90
|
+
ensure
|
91
|
+
self.clear_fmrest_config_overlay
|
92
|
+
end
|
93
|
+
end.resume
|
94
|
+
end
|
95
|
+
|
96
|
+
# Spyke override -- Defaults to `fmrest_connection`
|
97
|
+
#
|
20
98
|
def connection
|
21
99
|
super || fmrest_connection
|
22
100
|
end
|
23
101
|
|
24
102
|
# Sets a block for injecting custom middleware into the Faraday
|
25
|
-
# connection.
|
103
|
+
# connection.
|
26
104
|
#
|
27
|
-
#
|
28
|
-
#
|
29
|
-
#
|
30
|
-
#
|
31
|
-
#
|
105
|
+
# @example
|
106
|
+
# class MyModel < FmRest::Spyke::Base
|
107
|
+
# faraday do |conn|
|
108
|
+
# # Set up a custom logger for the model
|
109
|
+
# conn.response :logger, MyApp.logger, bodies: true
|
32
110
|
# end
|
111
|
+
# end
|
33
112
|
#
|
34
113
|
def faraday(&block)
|
35
114
|
self.faraday_block = block
|
@@ -38,26 +117,45 @@ module FmRest
|
|
38
117
|
private
|
39
118
|
|
40
119
|
def fmrest_connection
|
41
|
-
|
42
|
-
|
43
|
-
|
120
|
+
memoize = false
|
121
|
+
|
122
|
+
# Don't memoize the connection if there's an overlay, since
|
123
|
+
# overlays are thread-local and so should be the connection
|
124
|
+
unless fmrest_config_overlay
|
125
|
+
return @fmrest_connection if @fmrest_connection
|
126
|
+
memoize = true
|
127
|
+
end
|
128
|
+
|
129
|
+
config = ConnectionSettings.wrap(fmrest_config)
|
44
130
|
|
45
|
-
|
46
|
-
|
131
|
+
connection =
|
132
|
+
FmRest::V1.build_connection(config) do |conn|
|
133
|
+
faraday_block.call(conn) if faraday_block
|
47
134
|
|
48
|
-
|
49
|
-
|
50
|
-
|
51
|
-
|
52
|
-
|
135
|
+
# Pass the class to SpykeFormatter's initializer so it can have
|
136
|
+
# access to extra context defined in the model, e.g. a portal
|
137
|
+
# where name of the portal and the attributes prefix don't match
|
138
|
+
# and need to be specified as options to `portal`
|
139
|
+
conn.use FmRest::Spyke::SpykeFormatter, self
|
53
140
|
|
54
|
-
|
141
|
+
conn.use FmRest::V1::TypeCoercer, config
|
55
142
|
|
56
|
-
|
57
|
-
|
58
|
-
end
|
143
|
+
# FmRest::Spyke::JsonParse expects symbol keys
|
144
|
+
conn.response :json, parser_options: { symbolize_names: true }
|
59
145
|
end
|
146
|
+
|
147
|
+
@fmrest_connection = connection if memoize
|
148
|
+
|
149
|
+
connection
|
60
150
|
end
|
151
|
+
|
152
|
+
def fmrest_config_overlay_key
|
153
|
+
:"#{object_id}.fmrest_config_overlay"
|
154
|
+
end
|
155
|
+
end
|
156
|
+
|
157
|
+
def fmrest_config
|
158
|
+
self.class.fmrest_config
|
61
159
|
end
|
62
160
|
end
|
63
161
|
end
|
@@ -5,10 +5,25 @@ require "fmrest/spyke/container_field"
|
|
5
5
|
module FmRest
|
6
6
|
module Spyke
|
7
7
|
module Model
|
8
|
+
# This module adds support for container fields.
|
9
|
+
#
|
8
10
|
module ContainerFields
|
9
11
|
extend ::ActiveSupport::Concern
|
10
12
|
|
11
13
|
class_methods do
|
14
|
+
# Defines a container field on the model.
|
15
|
+
#
|
16
|
+
# @param name [Symbol] the name of the container field
|
17
|
+
#
|
18
|
+
# @option options [String] :field_name (nil) the name of the container
|
19
|
+
# field in the FileMaker layout (only needed if it doesn't match
|
20
|
+
# the name given)
|
21
|
+
#
|
22
|
+
# @example
|
23
|
+
# class Honeybee < FmRest::Spyke::Base
|
24
|
+
# container :photo, field_name: "Beehive Photo ID"
|
25
|
+
# end
|
26
|
+
#
|
12
27
|
def container(name, options = {})
|
13
28
|
field_name = options[:field_name] || name
|
14
29
|
|
@@ -1,7 +1,5 @@
|
|
1
1
|
# frozen_string_literal: true
|
2
2
|
|
3
|
-
require "fmrest/spyke/relation"
|
4
|
-
|
5
3
|
module FmRest
|
6
4
|
module Spyke
|
7
5
|
module Model
|
@@ -15,6 +13,8 @@ module FmRest
|
|
15
13
|
# execution results after a save, etc.
|
16
14
|
|
17
15
|
|
16
|
+
# Spyke override -- Keeps metadata in thread-local class variable.
|
17
|
+
#
|
18
18
|
def request(*args)
|
19
19
|
super.tap do |r|
|
20
20
|
Thread.current[last_request_metadata_key] = r.metadata
|
@@ -32,6 +32,46 @@ module FmRest
|
|
32
32
|
end
|
33
33
|
end
|
34
34
|
end
|
35
|
+
|
36
|
+
# Spyke override -- Uses `__record_id` for building the record URI.
|
37
|
+
#
|
38
|
+
def uri
|
39
|
+
::Spyke::Path.new(@uri_template, fmrest_uri_attributes) if @uri_template
|
40
|
+
end
|
41
|
+
|
42
|
+
private
|
43
|
+
|
44
|
+
# Spyke override (private) -- Use `__record_id` instead of `id`
|
45
|
+
#
|
46
|
+
def resolve_path_from_action(action)
|
47
|
+
case action
|
48
|
+
when Symbol then uri.join(action)
|
49
|
+
when String then ::Spyke::Path.new(action, fmrest_uri_attributes)
|
50
|
+
else uri
|
51
|
+
end
|
52
|
+
end
|
53
|
+
|
54
|
+
def fmrest_uri_attributes
|
55
|
+
if persisted?
|
56
|
+
{ __record_id: __record_id }
|
57
|
+
else
|
58
|
+
# NOTE: it seems silly to be calling attributes.slice(:__record_id)
|
59
|
+
# when the record is supposed to not have a record_id set (since
|
60
|
+
# persisted? is false here), but it makes sense in the context of how
|
61
|
+
# Spyke works:
|
62
|
+
#
|
63
|
+
# When calling Model.find(id), Spyke will internally create a scope
|
64
|
+
# with .where(primary_key => id) and call .find_one on it. Then,
|
65
|
+
# somewhere down the line Spyke creates a new empty instance of the
|
66
|
+
# current model class to get its .uri property (the one we're
|
67
|
+
# partially building through this method and which contains these URI
|
68
|
+
# attributes). When initializing a record Spyke first forcefully
|
69
|
+
# assigns the .where()-set attributes from the current scope onto
|
70
|
+
# that instance's attributes hash, which then leads us right here,
|
71
|
+
# where we might have __record_id assigned as a scope attribute:
|
72
|
+
attributes.slice(:__record_id)
|
73
|
+
end
|
74
|
+
end
|
35
75
|
end
|
36
76
|
end
|
37
77
|
end
|