fmrest 0.9.0 → 0.12.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 +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
|