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
@@ -6,6 +6,10 @@ require "fmrest/spyke/validation_error"
|
|
6
6
|
module FmRest
|
7
7
|
module Spyke
|
8
8
|
module Model
|
9
|
+
# This module adds and extends various ORM features in Spyke models,
|
10
|
+
# including custom query methods, remote script execution and
|
11
|
+
# exception-raising persistence methods.
|
12
|
+
#
|
9
13
|
module Orm
|
10
14
|
extend ::ActiveSupport::Concern
|
11
15
|
|
@@ -21,22 +25,25 @@ module FmRest
|
|
21
25
|
end
|
22
26
|
|
23
27
|
class_methods do
|
24
|
-
# Methods delegated to FmRest::Spyke::Relation
|
28
|
+
# Methods delegated to `FmRest::Spyke::Relation`
|
25
29
|
delegate :limit, :offset, :sort, :order, :query, :omit, :portal,
|
26
30
|
:portals, :includes, :with_all_portals, :without_portals,
|
27
31
|
:script, :find_one, :first, :any, :find_some,
|
28
32
|
:find_in_batches, :find_each, to: :all
|
29
33
|
|
34
|
+
# Spyke override -- Use FmRest's Relation instead of Spyke's vanilla
|
35
|
+
# one
|
36
|
+
#
|
30
37
|
def all
|
31
|
-
# Use FmRest's Relation instead of Spyke's vanilla one
|
32
38
|
current_scope || Relation.new(self, uri: uri)
|
33
39
|
end
|
34
40
|
|
35
|
-
#
|
41
|
+
# Spyke override -- allows properly setting limit, offset and other
|
36
42
|
# options, as well as using the appropriate HTTP method/URL depending
|
37
|
-
# on whether there's a query present in the current scope
|
43
|
+
# on whether there's a query present in the current scope.
|
38
44
|
#
|
39
|
-
#
|
45
|
+
# @example
|
46
|
+
# Person.query(first_name: "Stefan").fetch # -> POST .../_find
|
40
47
|
#
|
41
48
|
def fetch
|
42
49
|
if current_scope.has_query?
|
@@ -62,12 +69,21 @@ module FmRest
|
|
62
69
|
self.current_scope = previous
|
63
70
|
end
|
64
71
|
|
65
|
-
#
|
72
|
+
# Exception-raising version of `#create`
|
73
|
+
#
|
74
|
+
# @param attributes [Hash] the attributes to initialize the
|
75
|
+
# record with
|
66
76
|
#
|
67
77
|
def create!(attributes = {})
|
68
78
|
new(attributes).tap(&:save!)
|
69
79
|
end
|
70
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
|
+
#
|
71
87
|
def execute_script(script_name, param: nil)
|
72
88
|
params = {}
|
73
89
|
params = {"script.param" => param} unless param.nil?
|
@@ -108,12 +124,20 @@ module FmRest
|
|
108
124
|
end
|
109
125
|
end
|
110
126
|
|
111
|
-
#
|
127
|
+
# Spyke override -- Adds a number of features to original `#save`:
|
112
128
|
#
|
113
129
|
# * Validations
|
114
130
|
# * Data API scripts execution
|
115
131
|
# * Refresh of dirty attributes
|
116
132
|
#
|
133
|
+
# @option options [String] :script the name of a FileMaker script to execute
|
134
|
+
# upon saving
|
135
|
+
# @option options [Boolean] :raise_validation_errors whether to raise an
|
136
|
+
# exception if validations fail
|
137
|
+
#
|
138
|
+
# @return [true] if saved successfully
|
139
|
+
# @return [false] if validations or persistence failed
|
140
|
+
#
|
117
141
|
def save(options = {})
|
118
142
|
callback = persisted? ? :update : :create
|
119
143
|
|
@@ -123,11 +147,34 @@ module FmRest
|
|
123
147
|
true
|
124
148
|
end
|
125
149
|
|
150
|
+
# Exception-raising version of `#save`.
|
151
|
+
#
|
152
|
+
# @option (see #save)
|
153
|
+
#
|
154
|
+
# @return [true] if saved successfully
|
155
|
+
#
|
156
|
+
# @raise if validations or presistence failed
|
157
|
+
#
|
126
158
|
def save!(options = {})
|
127
159
|
save(options.merge(raise_validation_errors: true))
|
128
160
|
end
|
129
161
|
|
130
|
-
#
|
162
|
+
# Exception-raising version of `#update`.
|
163
|
+
#
|
164
|
+
# @param new_attributes [Hash] a hash of record attributes to update
|
165
|
+
# the record with
|
166
|
+
#
|
167
|
+
# @option (see #save)
|
168
|
+
#
|
169
|
+
def update!(new_attributes, options = {})
|
170
|
+
self.attributes = new_attributes
|
171
|
+
save!(options)
|
172
|
+
end
|
173
|
+
|
174
|
+
# Spyke override -- Adds support for Data API script execution.
|
175
|
+
#
|
176
|
+
# @option options [String] :script the name of a FileMaker script to execute
|
177
|
+
# upon deletion
|
131
178
|
#
|
132
179
|
def destroy(options = {})
|
133
180
|
# For whatever reason the Data API wants the script params as query
|
@@ -142,22 +189,19 @@ module FmRest
|
|
142
189
|
self.attributes = delete(uri.to_s + script_query_string)
|
143
190
|
end
|
144
191
|
|
145
|
-
#
|
192
|
+
# (see #destroy)
|
193
|
+
#
|
194
|
+
# @option (see #destroy)
|
146
195
|
#
|
147
|
-
def update!(new_attributes, options = {})
|
148
|
-
self.attributes = new_attributes
|
149
|
-
save!(options)
|
150
|
-
end
|
151
|
-
|
152
196
|
def reload(options = {})
|
153
197
|
scope = self.class
|
154
198
|
scope = scope.script(options[:script]) if options.has_key?(:script)
|
155
|
-
reloaded = scope.find(
|
199
|
+
reloaded = scope.find(__record_id)
|
156
200
|
self.attributes = reloaded.attributes
|
157
|
-
self.
|
201
|
+
self.__mod_id = reloaded.mod_id
|
158
202
|
end
|
159
203
|
|
160
|
-
# ActiveModel 5+ implements this method, so we only
|
204
|
+
# ActiveModel 5+ implements this method, so we only need it if we're in
|
161
205
|
# the older AM4
|
162
206
|
if ActiveModel::VERSION::MAJOR == 4
|
163
207
|
def validate!(context = nil)
|
@@ -0,0 +1,78 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module FmRest
|
4
|
+
module Spyke
|
5
|
+
module Model
|
6
|
+
# Modifies Spyke models to use `__record_id` instead of `id` as the
|
7
|
+
# "primary key" method, so that we can map a model class to a FM layout
|
8
|
+
# with a field named `id` without clobbering it.
|
9
|
+
#
|
10
|
+
# The `id` reader method still maps to the record ID for backwards
|
11
|
+
# compatibility and because Spyke hardcodes its use at various points
|
12
|
+
# through its codebase, but it can be safely overwritten (e.g. to map to
|
13
|
+
# a FM field).
|
14
|
+
#
|
15
|
+
# The recommended way to deal with a layout that maps an `id` attribute
|
16
|
+
# is to remap it in the model to something else, e.g. `unique_id`.
|
17
|
+
#
|
18
|
+
module RecordID
|
19
|
+
extend ::ActiveSupport::Concern
|
20
|
+
|
21
|
+
included do
|
22
|
+
# @return [Integer] the record's recordId
|
23
|
+
attr_accessor :__record_id
|
24
|
+
alias_method :record_id, :__record_id
|
25
|
+
alias_method :id, :__record_id
|
26
|
+
|
27
|
+
# @return [Integer] the record's modId
|
28
|
+
attr_accessor :__mod_id
|
29
|
+
alias_method :mod_id, :__mod_id
|
30
|
+
|
31
|
+
# Get rid of Spyke's id= setter method, as we'll be using __record_id=
|
32
|
+
# instead
|
33
|
+
undef_method :id=
|
34
|
+
|
35
|
+
# Tell Spyke that we want __record_id as the PK
|
36
|
+
self.primary_key = :__record_id
|
37
|
+
end
|
38
|
+
|
39
|
+
def __record_id?
|
40
|
+
__record_id.present?
|
41
|
+
end
|
42
|
+
alias_method :record_id?, :__record_id?
|
43
|
+
alias_method :persisted?, :__record_id?
|
44
|
+
|
45
|
+
# Spyke override -- Use `__record_id` instead of `id`
|
46
|
+
#
|
47
|
+
def hash
|
48
|
+
__record_id.hash
|
49
|
+
end
|
50
|
+
|
51
|
+
# Spyke override -- Renders class string with layout name and
|
52
|
+
# `record_id`.
|
53
|
+
#
|
54
|
+
# @return [String] A string representation of the class
|
55
|
+
#
|
56
|
+
def inspect
|
57
|
+
"#<#{self.class}(layout: #{self.class.layout}) record_id: #{__record_id.inspect} #{inspect_attributes}>"
|
58
|
+
end
|
59
|
+
|
60
|
+
# Spyke override -- Use `__record_id` instead of `id`
|
61
|
+
#
|
62
|
+
# @param id [Integer] The id of the record to destroy
|
63
|
+
#
|
64
|
+
def destroy(id = nil)
|
65
|
+
new(__record_id: id).destroy
|
66
|
+
end
|
67
|
+
|
68
|
+
private
|
69
|
+
|
70
|
+
# Spyke override (private)
|
71
|
+
#
|
72
|
+
def conflicting_ids?(attributes)
|
73
|
+
false
|
74
|
+
end
|
75
|
+
end
|
76
|
+
end
|
77
|
+
end
|
78
|
+
end
|
@@ -4,11 +4,11 @@ module FmRest
|
|
4
4
|
module Spyke
|
5
5
|
module Model
|
6
6
|
module Serialization
|
7
|
-
FM_DATE_FORMAT = "%m/%d/%Y"
|
8
|
-
FM_DATETIME_FORMAT = "#{FM_DATE_FORMAT} %H:%M:%S"
|
7
|
+
FM_DATE_FORMAT = "%m/%d/%Y"
|
8
|
+
FM_DATETIME_FORMAT = "#{FM_DATE_FORMAT} %H:%M:%S"
|
9
9
|
|
10
|
-
#
|
11
|
-
#
|
10
|
+
# Spyke override -- Return FM Data API's expected JSON format,
|
11
|
+
# including only modified fields.
|
12
12
|
#
|
13
13
|
def to_params
|
14
14
|
params = {
|
@@ -63,9 +63,9 @@ module FmRest
|
|
63
63
|
def serialize_values!(params)
|
64
64
|
params.transform_values! do |value|
|
65
65
|
case value
|
66
|
-
when
|
67
|
-
value.strftime(FM_DATETIME_FORMAT)
|
68
|
-
when
|
66
|
+
when *datetime_classes
|
67
|
+
convert_datetime_timezone(value.to_datetime).strftime(FM_DATETIME_FORMAT)
|
68
|
+
when *date_classes
|
69
69
|
value.strftime(FM_DATE_FORMAT)
|
70
70
|
else
|
71
71
|
value
|
@@ -74,6 +74,25 @@ module FmRest
|
|
74
74
|
|
75
75
|
params
|
76
76
|
end
|
77
|
+
|
78
|
+
def convert_datetime_timezone(dt)
|
79
|
+
case fmrest_config.timezone
|
80
|
+
when :utc, "utc"
|
81
|
+
dt.new_offset(0)
|
82
|
+
when :local, "local"
|
83
|
+
dt.new_offset(FmRest::V1.local_offset_for_datetime(dt))
|
84
|
+
when nil
|
85
|
+
dt
|
86
|
+
end
|
87
|
+
end
|
88
|
+
|
89
|
+
def datetime_classes
|
90
|
+
[DateTime, Time, defined?(FmRest::StringDateTime) && FmRest::StringDateTime].compact
|
91
|
+
end
|
92
|
+
|
93
|
+
def date_classes
|
94
|
+
[Date, defined?(FmRest::StringDate) && FmRest::StringDate].compact
|
95
|
+
end
|
77
96
|
end
|
78
97
|
end
|
79
98
|
end
|
@@ -3,7 +3,7 @@
|
|
3
3
|
module FmRest
|
4
4
|
module Spyke
|
5
5
|
module Model
|
6
|
-
module
|
6
|
+
module URI
|
7
7
|
extend ::ActiveSupport::Concern
|
8
8
|
|
9
9
|
class_methods do
|
@@ -14,13 +14,12 @@ module FmRest
|
|
14
14
|
@layout ||= model_name.name
|
15
15
|
end
|
16
16
|
|
17
|
-
#
|
17
|
+
# Spyke override -- Extends `uri` to default to FM Data's URI schema
|
18
18
|
#
|
19
19
|
def uri(uri_template = nil)
|
20
20
|
if @uri.nil? && uri_template.nil?
|
21
|
-
return FmRest::V1.record_path(layout) + "(
|
21
|
+
return FmRest::V1.record_path(layout) + "(/:#{primary_key})"
|
22
22
|
end
|
23
|
-
|
24
23
|
super
|
25
24
|
end
|
26
25
|
end
|
@@ -63,8 +63,8 @@ module FmRest
|
|
63
63
|
response = json[:response]
|
64
64
|
|
65
65
|
data = {}
|
66
|
-
data[:
|
67
|
-
data[:
|
66
|
+
data[:__mod_id] = response[:modId] if response[:modId]
|
67
|
+
data[:__record_id] = response[:recordId].to_i if response[:recordId]
|
68
68
|
|
69
69
|
build_base_hash(json, true).merge!(data: data)
|
70
70
|
end
|
@@ -188,7 +188,7 @@ module FmRest
|
|
188
188
|
# @param json_data [Hash]
|
189
189
|
# @return [Hash] the record data in Spyke format
|
190
190
|
def prepare_record_data(json_data)
|
191
|
-
out = {
|
191
|
+
out = { __record_id: json_data[:recordId].to_i, __mod_id: json_data[:modId] }
|
192
192
|
out.merge!(json_data[:fieldData])
|
193
193
|
out.merge!(prepare_portal_data(json_data[:portalData])) if json_data[:portalData]
|
194
194
|
out
|
@@ -213,8 +213,8 @@ module FmRest
|
|
213
213
|
|
214
214
|
out[portal_name] =
|
215
215
|
portal_records.map do |portal_fields|
|
216
|
-
attributes = {
|
217
|
-
attributes[:
|
216
|
+
attributes = { __record_id: portal_fields[:recordId].to_i }
|
217
|
+
attributes[:__mod_id] = portal_fields[:modId] if portal_fields[:modId]
|
218
218
|
|
219
219
|
prefix = portal_options[:attribute_prefix] || portal_name
|
220
220
|
prefix_matcher = /\A#{prefix}::/
|
data/lib/fmrest/string_date.rb
CHANGED
@@ -79,17 +79,24 @@ module FmRest
|
|
79
79
|
class InvalidDate < ArgumentError; end
|
80
80
|
|
81
81
|
class << self
|
82
|
-
|
82
|
+
def strptime(str, date_format, *_)
|
83
|
+
begin
|
84
|
+
date = self::DELEGATE_CLASS.strptime(str, date_format)
|
85
|
+
rescue ArgumentError
|
86
|
+
raise InvalidDate
|
87
|
+
end
|
88
|
+
|
89
|
+
new(str, date)
|
90
|
+
end
|
83
91
|
end
|
84
92
|
|
85
|
-
def initialize(str,
|
93
|
+
def initialize(str, date, **str_args)
|
94
|
+
raise ArgumentError, "str must be of class String" unless str.is_a?(String)
|
95
|
+
raise ArgumentError, "date must be of class #{self.class::DELEGATE_CLASS.name}" unless date.is_a?(self.class::DELEGATE_CLASS)
|
96
|
+
|
86
97
|
super(str, **str_args)
|
87
98
|
|
88
|
-
|
89
|
-
@delegate = self.class::DELEGATE_CLASS.strptime(str, date_format)
|
90
|
-
rescue ArgumentError
|
91
|
-
raise InvalidDate
|
92
|
-
end
|
99
|
+
@delegate = date
|
93
100
|
|
94
101
|
freeze
|
95
102
|
end
|
@@ -178,4 +185,36 @@ module FmRest
|
|
178
185
|
@delegate
|
179
186
|
end
|
180
187
|
end
|
188
|
+
|
189
|
+
module StringDateAwareness
|
190
|
+
def _parse(v, *_)
|
191
|
+
if v.is_a?(StringDateTime)
|
192
|
+
return { year: v.year, mon: v.month, mday: v.mday, hour: v.hour, min: v.min, sec: v.sec, sec_fraction: v.sec_fraction, offset: v.offset }
|
193
|
+
end
|
194
|
+
if v.is_a?(StringDate)
|
195
|
+
return { year: v.year, mon: v.month, mday: v.mday }
|
196
|
+
end
|
197
|
+
super
|
198
|
+
end
|
199
|
+
|
200
|
+
def parse(v, *_)
|
201
|
+
if v.is_a?(StringDate)
|
202
|
+
return self == ::DateTime ? v.to_datetime : v.to_date
|
203
|
+
end
|
204
|
+
super
|
205
|
+
end
|
206
|
+
|
207
|
+
# Overriding case equality method so that it returns true for
|
208
|
+
# `FmRest::StringDate` instances
|
209
|
+
#
|
210
|
+
# Calls superclass method
|
211
|
+
#
|
212
|
+
def ===(other)
|
213
|
+
super || other.is_a?(StringDate)
|
214
|
+
end
|
215
|
+
|
216
|
+
def self.enable(classes: [Date, DateTime])
|
217
|
+
classes.each { |klass| klass.singleton_class.prepend(self) }
|
218
|
+
end
|
219
|
+
end
|
181
220
|
end
|
data/lib/fmrest/token_store.rb
CHANGED
@@ -2,5 +2,11 @@
|
|
2
2
|
|
3
3
|
module FmRest
|
4
4
|
module TokenStore
|
5
|
+
autoload :Base, "fmrest/token_store/base"
|
6
|
+
autoload :Memory, "fmrest/token_store/memory"
|
7
|
+
autoload :Null, "fmrest/token_store/null"
|
8
|
+
autoload :ActiveRecord, "fmrest/token_store/active_record"
|
9
|
+
autoload :Moneta, "fmrest/token_store/moneta"
|
10
|
+
autoload :Redis, "fmrest/token_store/redis"
|
5
11
|
end
|
6
12
|
end
|
@@ -10,15 +10,15 @@ module FmRest
|
|
10
10
|
end
|
11
11
|
|
12
12
|
def load(key)
|
13
|
-
raise
|
13
|
+
raise NotImplementedError
|
14
14
|
end
|
15
15
|
|
16
16
|
def store(key, value)
|
17
|
-
raise
|
17
|
+
raise NotImplementedError
|
18
18
|
end
|
19
19
|
|
20
20
|
def delete(key)
|
21
|
-
raise
|
21
|
+
raise NotImplementedError
|
22
22
|
end
|
23
23
|
end
|
24
24
|
end
|
@@ -0,0 +1,20 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require "singleton"
|
4
|
+
|
5
|
+
module FmRest
|
6
|
+
module TokenStore
|
7
|
+
module Null < Base
|
8
|
+
include Singleton
|
9
|
+
|
10
|
+
def delete(key)
|
11
|
+
end
|
12
|
+
|
13
|
+
def load(key)
|
14
|
+
end
|
15
|
+
|
16
|
+
def store(key, value)
|
17
|
+
end
|
18
|
+
end
|
19
|
+
end
|
20
|
+
end
|