fmrest-spyke 0.13.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 +7 -0
- data/.yardopts +4 -0
- data/CHANGELOG.md +127 -0
- data/LICENSE.txt +21 -0
- data/README.md +523 -0
- data/lib/fmrest-spyke.rb +3 -0
- data/lib/fmrest/spyke.rb +21 -0
- data/lib/fmrest/spyke/base.rb +9 -0
- data/lib/fmrest/spyke/container_field.rb +59 -0
- data/lib/fmrest/spyke/model.rb +33 -0
- data/lib/fmrest/spyke/model/associations.rb +166 -0
- data/lib/fmrest/spyke/model/attributes.rb +159 -0
- data/lib/fmrest/spyke/model/auth.rb +43 -0
- data/lib/fmrest/spyke/model/connection.rb +163 -0
- data/lib/fmrest/spyke/model/container_fields.rb +40 -0
- data/lib/fmrest/spyke/model/global_fields.rb +40 -0
- data/lib/fmrest/spyke/model/http.rb +77 -0
- data/lib/fmrest/spyke/model/orm.rb +256 -0
- data/lib/fmrest/spyke/model/record_id.rb +96 -0
- data/lib/fmrest/spyke/model/serialization.rb +115 -0
- data/lib/fmrest/spyke/model/uri.rb +29 -0
- data/lib/fmrest/spyke/portal.rb +65 -0
- data/lib/fmrest/spyke/relation.rb +359 -0
- data/lib/fmrest/spyke/spyke_formatter.rb +274 -0
- data/lib/fmrest/spyke/validation_error.rb +25 -0
- metadata +96 -0
@@ -0,0 +1,274 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require "json"
|
4
|
+
require "ostruct"
|
5
|
+
|
6
|
+
module FmRest
|
7
|
+
module Spyke
|
8
|
+
# Metadata class to be passed to Spyke::Collection#metadata
|
9
|
+
class Metadata < Struct.new(:messages, :script, :data_info)
|
10
|
+
alias_method :scripts, :script
|
11
|
+
end
|
12
|
+
|
13
|
+
class DataInfo < OpenStruct
|
14
|
+
def total_record_count; totalRecordCount; end
|
15
|
+
def found_count; foundCount; end
|
16
|
+
def returned_count; returnedCount; end
|
17
|
+
end
|
18
|
+
|
19
|
+
# Response Faraday middleware for converting FM API's response JSON into
|
20
|
+
# Spyke's expected format
|
21
|
+
class SpykeFormatter < ::Faraday::Response::Middleware
|
22
|
+
SINGLE_RECORD_RE = %r(/records/\d+\z).freeze
|
23
|
+
MULTIPLE_RECORDS_RE = %r(/records\z).freeze
|
24
|
+
CONTAINER_RE = %r(/records/\d+/containers/[^/]+/\d+\z).freeze
|
25
|
+
FIND_RECORDS_RE = %r(/_find\b).freeze
|
26
|
+
SCRIPT_REQUEST_RE = %r(/script/[^/]+\z).freeze
|
27
|
+
|
28
|
+
VALIDATION_ERROR_RANGE = 500..599
|
29
|
+
|
30
|
+
# @param app [#call]
|
31
|
+
# @param model [Class<FmRest::Spyke::Base>]
|
32
|
+
def initialize(app, model)
|
33
|
+
super(app)
|
34
|
+
@model = model
|
35
|
+
end
|
36
|
+
|
37
|
+
# @param env [Faraday::Env]
|
38
|
+
def on_complete(env)
|
39
|
+
return unless env.body.is_a?(Hash)
|
40
|
+
|
41
|
+
json = env.body
|
42
|
+
|
43
|
+
case
|
44
|
+
when single_record_request?(env)
|
45
|
+
env.body = prepare_single_record(json)
|
46
|
+
when multiple_records_request?(env), find_request?(env)
|
47
|
+
env.body = prepare_collection(json)
|
48
|
+
when create_request?(env), update_request?(env), delete_request?(env), container_upload_request?(env)
|
49
|
+
env.body = prepare_save_response(json)
|
50
|
+
when execute_script_request?(env)
|
51
|
+
env.body = build_base_hash(json)
|
52
|
+
else
|
53
|
+
# Attempt to parse unknown requests too
|
54
|
+
env.body = build_base_hash(json)
|
55
|
+
end
|
56
|
+
end
|
57
|
+
|
58
|
+
private
|
59
|
+
|
60
|
+
# @param json [Hash]
|
61
|
+
# @return [Hash] the response in Spyke format
|
62
|
+
def prepare_save_response(json)
|
63
|
+
response = json[:response]
|
64
|
+
|
65
|
+
data = {}
|
66
|
+
data[:__mod_id] = response[:modId] if response[:modId]
|
67
|
+
data[:__record_id] = response[:recordId] if response[:recordId]
|
68
|
+
data[:__new_portal_record_info] = response[:newPortalRecordInfo] if response[:newPortalRecordInfo]
|
69
|
+
|
70
|
+
build_base_hash(json, true).merge!(data: data)
|
71
|
+
end
|
72
|
+
|
73
|
+
# (see #prepare_save_response)
|
74
|
+
def prepare_single_record(json)
|
75
|
+
data =
|
76
|
+
json[:response][:data] &&
|
77
|
+
prepare_record_data(json[:response][:data].first)
|
78
|
+
|
79
|
+
build_base_hash(json).merge!(data: data)
|
80
|
+
end
|
81
|
+
|
82
|
+
# (see #prepare_save_response)
|
83
|
+
def prepare_collection(json)
|
84
|
+
data =
|
85
|
+
json[:response][:data] &&
|
86
|
+
json[:response][:data].map { |record_data| prepare_record_data(record_data) }
|
87
|
+
|
88
|
+
build_base_hash(json).merge!(data: data)
|
89
|
+
end
|
90
|
+
|
91
|
+
# @param json [Hash]
|
92
|
+
# @param include_errors [Boolean]
|
93
|
+
# @return [FmRest::Spyke::Metadata] the skeleton structure for a
|
94
|
+
# Spyke-formatted response
|
95
|
+
def build_base_hash(json, include_errors = false)
|
96
|
+
{
|
97
|
+
metadata: Metadata.new(
|
98
|
+
prepare_messages(json),
|
99
|
+
prepare_script_results(json),
|
100
|
+
prepare_data_info(json)
|
101
|
+
).freeze,
|
102
|
+
errors: include_errors ? prepare_errors(json) : {}
|
103
|
+
}
|
104
|
+
end
|
105
|
+
|
106
|
+
# @param json [Hash]
|
107
|
+
# @return [Array<OpenStruct>] the skeleton structure for a
|
108
|
+
# Spyke-formatted response
|
109
|
+
def prepare_messages(json)
|
110
|
+
return [] unless json[:messages]
|
111
|
+
json[:messages].map { |m| OpenStruct.new(m).freeze }.freeze
|
112
|
+
end
|
113
|
+
|
114
|
+
# @param json [Hash]
|
115
|
+
# @return [OpenStruct] the script(s) execution results for Spyke metadata
|
116
|
+
# format
|
117
|
+
def prepare_script_results(json)
|
118
|
+
results = {}
|
119
|
+
|
120
|
+
[:prerequest, :presort].each do |s|
|
121
|
+
if json[:response][:"scriptError.#{s}"]
|
122
|
+
results[s] = OpenStruct.new(
|
123
|
+
result: json[:response][:"scriptResult.#{s}"],
|
124
|
+
error: json[:response][:"scriptError.#{s}"]
|
125
|
+
).freeze
|
126
|
+
end
|
127
|
+
end
|
128
|
+
|
129
|
+
if json[:response][:scriptError]
|
130
|
+
results[:after] = OpenStruct.new(
|
131
|
+
result: json[:response][:scriptResult],
|
132
|
+
error: json[:response][:scriptError]
|
133
|
+
).freeze
|
134
|
+
end
|
135
|
+
|
136
|
+
results.present? ? OpenStruct.new(results).freeze : nil
|
137
|
+
end
|
138
|
+
|
139
|
+
# @param json [Hash]
|
140
|
+
# @return [OpenStruct] the script(s) execution results for
|
141
|
+
# Spyke metadata format
|
142
|
+
def prepare_data_info(json)
|
143
|
+
data_info = json[:response] && json[:response][:dataInfo]
|
144
|
+
|
145
|
+
return nil unless data_info.present?
|
146
|
+
|
147
|
+
DataInfo.new(data_info).freeze
|
148
|
+
end
|
149
|
+
|
150
|
+
# @param json [Hash]
|
151
|
+
# @return [Hash] the errors hash in Spyke format
|
152
|
+
def prepare_errors(json)
|
153
|
+
# Code 0 means "No Error"
|
154
|
+
# https://fmhelp.filemaker.com/help/17/fmp/en/index.html#page/FMP_Help/error-codes.html
|
155
|
+
return {} if json[:messages][0][:code].to_i == 0
|
156
|
+
|
157
|
+
json[:messages].each_with_object(base: []) do |message, hash|
|
158
|
+
# Only include validation errors
|
159
|
+
next unless VALIDATION_ERROR_RANGE.include?(message[:code].to_i)
|
160
|
+
|
161
|
+
hash[:base] << "#{message[:message]} (#{message[:code]})"
|
162
|
+
end
|
163
|
+
end
|
164
|
+
|
165
|
+
# `json_data` is expected in this format:
|
166
|
+
#
|
167
|
+
# {
|
168
|
+
# "fieldData": {
|
169
|
+
# "fieldName1" : "fieldValue1",
|
170
|
+
# "fieldName2" : "fieldValue2",
|
171
|
+
# ...
|
172
|
+
# },
|
173
|
+
# "portalData": {
|
174
|
+
# "portal1" : [
|
175
|
+
# { <portalRecord1> },
|
176
|
+
# { <portalRecord2> },
|
177
|
+
# ...
|
178
|
+
# ],
|
179
|
+
# "portal2" : [
|
180
|
+
# { <portalRecord1> },
|
181
|
+
# { <portalRecord2> },
|
182
|
+
# ...
|
183
|
+
# ]
|
184
|
+
# },
|
185
|
+
# "modId": <Id_for_last_modification>,
|
186
|
+
# "recordId": <Unique_internal_ID_for_this_record>
|
187
|
+
# }
|
188
|
+
#
|
189
|
+
# @param json_data [Hash]
|
190
|
+
# @return [Hash] the record data in Spyke format
|
191
|
+
def prepare_record_data(json_data)
|
192
|
+
out = { __record_id: json_data[:recordId], __mod_id: json_data[:modId] }
|
193
|
+
out.merge!(json_data[:fieldData])
|
194
|
+
out.merge!(prepare_portal_data(json_data[:portalData])) if json_data[:portalData]
|
195
|
+
out
|
196
|
+
end
|
197
|
+
|
198
|
+
# Extracts `recordId` and strips the `"PortalName::"` field prefix for each
|
199
|
+
# portal
|
200
|
+
#
|
201
|
+
# Sample `json_portal_data`:
|
202
|
+
#
|
203
|
+
# "portalData": {
|
204
|
+
# "Orders":[
|
205
|
+
# { "Orders::DeliveryDate": "3/7/2017", "recordId": "23" }
|
206
|
+
# ]
|
207
|
+
# }
|
208
|
+
#
|
209
|
+
# @param json_portal_data [Hash]
|
210
|
+
# @return [Hash] the portal data in Spyke format
|
211
|
+
def prepare_portal_data(json_portal_data)
|
212
|
+
json_portal_data.each_with_object({}) do |(portal_name, portal_records), out|
|
213
|
+
portal_options = @model.portal_options[portal_name.to_s] || {}
|
214
|
+
|
215
|
+
out[portal_name] =
|
216
|
+
portal_records.map do |portal_fields|
|
217
|
+
attributes = { __record_id: portal_fields[:recordId] }
|
218
|
+
attributes[:__mod_id] = portal_fields[:modId] if portal_fields[:modId]
|
219
|
+
|
220
|
+
prefix = portal_options[:attribute_prefix] || portal_name
|
221
|
+
prefix_matcher = /\A#{prefix}::/
|
222
|
+
|
223
|
+
portal_fields.each do |k, v|
|
224
|
+
next if :recordId == k || :modId == k
|
225
|
+
attributes[k.to_s.gsub(prefix_matcher, "").to_sym] = v
|
226
|
+
end
|
227
|
+
|
228
|
+
attributes
|
229
|
+
end
|
230
|
+
end
|
231
|
+
end
|
232
|
+
|
233
|
+
# @param env [Faraday::Env]
|
234
|
+
# @return [Boolean]
|
235
|
+
def single_record_request?(env)
|
236
|
+
env.method == :get && env.url.path.match(SINGLE_RECORD_RE)
|
237
|
+
end
|
238
|
+
|
239
|
+
# (see #single_record_request?)
|
240
|
+
def multiple_records_request?(env)
|
241
|
+
env.method == :get && env.url.path.match(MULTIPLE_RECORDS_RE)
|
242
|
+
end
|
243
|
+
|
244
|
+
# (see #single_record_request?)
|
245
|
+
def find_request?(env)
|
246
|
+
env.method == :post && env.url.path.match(FIND_RECORDS_RE)
|
247
|
+
end
|
248
|
+
|
249
|
+
# (see #single_record_request?)
|
250
|
+
def update_request?(env)
|
251
|
+
env.method == :patch && env.url.path.match(SINGLE_RECORD_RE)
|
252
|
+
end
|
253
|
+
|
254
|
+
# (see #single_record_request?)
|
255
|
+
def create_request?(env)
|
256
|
+
env.method == :post && env.url.path.match(MULTIPLE_RECORDS_RE)
|
257
|
+
end
|
258
|
+
|
259
|
+
# (see #single_record_request?)
|
260
|
+
def container_upload_request?(env)
|
261
|
+
env.method == :post && env.url.path.match(CONTAINER_RE)
|
262
|
+
end
|
263
|
+
|
264
|
+
# (see #single_record_request?)
|
265
|
+
def delete_request?(env)
|
266
|
+
env.method == :delete && env.url.path.match(SINGLE_RECORD_RE)
|
267
|
+
end
|
268
|
+
|
269
|
+
def execute_script_request?(env)
|
270
|
+
env.method == :get && env.url.path.match(SCRIPT_REQUEST_RE)
|
271
|
+
end
|
272
|
+
end
|
273
|
+
end
|
274
|
+
end
|
@@ -0,0 +1,25 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module FmRest
|
4
|
+
module Spyke
|
5
|
+
# ActiveModel 4 doesn't include a ValidationError class, which we want to
|
6
|
+
# raise when model.validate! fails.
|
7
|
+
#
|
8
|
+
# In order to break the least amount of code that uses AM5+, while still
|
9
|
+
# supporting AM4 we use this proxy class that inherits from
|
10
|
+
# AM::ValidationError if it's there, or reimplements it otherwise
|
11
|
+
if defined?(::ActiveModel::ValidationError)
|
12
|
+
class ValidationError < ::ActiveModel::ValidationError; end
|
13
|
+
else
|
14
|
+
class ValidationError < StandardError
|
15
|
+
attr_reader :model
|
16
|
+
|
17
|
+
def initialize(model)
|
18
|
+
@model = model
|
19
|
+
errors = @model.errors.full_messages.join(", ")
|
20
|
+
super("Invalid model: #{errors}")
|
21
|
+
end
|
22
|
+
end
|
23
|
+
end
|
24
|
+
end
|
25
|
+
end
|
metadata
ADDED
@@ -0,0 +1,96 @@
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
2
|
+
name: fmrest-spyke
|
3
|
+
version: !ruby/object:Gem::Version
|
4
|
+
version: 0.13.0
|
5
|
+
platform: ruby
|
6
|
+
authors:
|
7
|
+
- Pedro Carbajal
|
8
|
+
autorequire:
|
9
|
+
bindir: bin
|
10
|
+
cert_chain: []
|
11
|
+
date: 2021-02-11 00:00:00.000000000 Z
|
12
|
+
dependencies:
|
13
|
+
- !ruby/object:Gem::Dependency
|
14
|
+
name: fmrest-core
|
15
|
+
requirement: !ruby/object:Gem::Requirement
|
16
|
+
requirements:
|
17
|
+
- - '='
|
18
|
+
- !ruby/object:Gem::Version
|
19
|
+
version: 0.13.0
|
20
|
+
type: :runtime
|
21
|
+
prerelease: false
|
22
|
+
version_requirements: !ruby/object:Gem::Requirement
|
23
|
+
requirements:
|
24
|
+
- - '='
|
25
|
+
- !ruby/object:Gem::Version
|
26
|
+
version: 0.13.0
|
27
|
+
- !ruby/object:Gem::Dependency
|
28
|
+
name: spyke
|
29
|
+
requirement: !ruby/object:Gem::Requirement
|
30
|
+
requirements:
|
31
|
+
- - ">="
|
32
|
+
- !ruby/object:Gem::Version
|
33
|
+
version: 5.3.3
|
34
|
+
type: :runtime
|
35
|
+
prerelease: false
|
36
|
+
version_requirements: !ruby/object:Gem::Requirement
|
37
|
+
requirements:
|
38
|
+
- - ">="
|
39
|
+
- !ruby/object:Gem::Version
|
40
|
+
version: 5.3.3
|
41
|
+
description: fmrest-spyke is an ActiveRecord-like ORM client library for the FileMaker
|
42
|
+
Data API built on top of fmrest-core and Spyke (https://github.com/balvig/spyke).
|
43
|
+
email:
|
44
|
+
- pedro_c@beezwax.net
|
45
|
+
executables: []
|
46
|
+
extensions: []
|
47
|
+
extra_rdoc_files: []
|
48
|
+
files:
|
49
|
+
- ".yardopts"
|
50
|
+
- CHANGELOG.md
|
51
|
+
- LICENSE.txt
|
52
|
+
- README.md
|
53
|
+
- lib/fmrest-spyke.rb
|
54
|
+
- lib/fmrest/spyke.rb
|
55
|
+
- lib/fmrest/spyke/base.rb
|
56
|
+
- lib/fmrest/spyke/container_field.rb
|
57
|
+
- lib/fmrest/spyke/model.rb
|
58
|
+
- lib/fmrest/spyke/model/associations.rb
|
59
|
+
- lib/fmrest/spyke/model/attributes.rb
|
60
|
+
- lib/fmrest/spyke/model/auth.rb
|
61
|
+
- lib/fmrest/spyke/model/connection.rb
|
62
|
+
- lib/fmrest/spyke/model/container_fields.rb
|
63
|
+
- lib/fmrest/spyke/model/global_fields.rb
|
64
|
+
- lib/fmrest/spyke/model/http.rb
|
65
|
+
- lib/fmrest/spyke/model/orm.rb
|
66
|
+
- lib/fmrest/spyke/model/record_id.rb
|
67
|
+
- lib/fmrest/spyke/model/serialization.rb
|
68
|
+
- lib/fmrest/spyke/model/uri.rb
|
69
|
+
- lib/fmrest/spyke/portal.rb
|
70
|
+
- lib/fmrest/spyke/relation.rb
|
71
|
+
- lib/fmrest/spyke/spyke_formatter.rb
|
72
|
+
- lib/fmrest/spyke/validation_error.rb
|
73
|
+
homepage: https://github.com/beezwax/fmrest-ruby
|
74
|
+
licenses:
|
75
|
+
- MIT
|
76
|
+
metadata: {}
|
77
|
+
post_install_message:
|
78
|
+
rdoc_options: []
|
79
|
+
require_paths:
|
80
|
+
- lib
|
81
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
82
|
+
requirements:
|
83
|
+
- - ">="
|
84
|
+
- !ruby/object:Gem::Version
|
85
|
+
version: '0'
|
86
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
87
|
+
requirements:
|
88
|
+
- - ">="
|
89
|
+
- !ruby/object:Gem::Version
|
90
|
+
version: '0'
|
91
|
+
requirements: []
|
92
|
+
rubygems_version: 3.2.3
|
93
|
+
signing_key:
|
94
|
+
specification_version: 4
|
95
|
+
summary: FileMaker Data API ORM client library
|
96
|
+
test_files: []
|