fmrest 0.4.1 → 0.5.0

Sign up to get free protection for your applications and to get access to all the features.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 368ed06195288490b1388c451b00bb33f5c19e526348422a3760a14902459d76
4
- data.tar.gz: a445ef1bd8ef708b6aea2c5f8c2362f77e8c1afbd80bb6ef1d354c669ef8c9f6
3
+ metadata.gz: 61e4602ba4797de70a866ea14197056bd0d09ca0bab4d09e44ddade7719395ac
4
+ data.tar.gz: 77982322ac28bef17e648888dc67753a56fb02c3b31e616d7d4129c6031ba676
5
5
  SHA512:
6
- metadata.gz: 1d2e8cb78c9a232f8ec13dde7ad3407f5dbfc14c49c0e5aadeb7a940c1fe07f14e919832b53841e4ac054f81c74a64fb7721614218c342835a2274c3b187aa96
7
- data.tar.gz: 9695ef91a8eef005420c0003e62eb1712eda8f11667e87f345c311b008c712113b4ff52be31e1fc207237efc7e7279d5952b2d0b2ed3b4a26a2a8aa55ad38638
6
+ metadata.gz: c872d1d1b37710720d0cd5398f985c1abc6d59105fe117905518d86f2617f82e539e743b8d57693bde34558381a500d32f1d942f240705db21d5415ca2c86f14
7
+ data.tar.gz: e71b4ca034162ed34b847b66d7ae0063f3fa654be1a73ddf12c9f995ca62956a186737ca15ba06939d5982a3b62bab30c2ad7d125498b07024276a7b0518d00c
@@ -1,5 +1,12 @@
1
1
  ## Changelog
2
2
 
3
+ ### 0.5.0
4
+
5
+ * Much improved script execution support
6
+ ([#20](https://github.com/beezwax/fmrest-ruby/issues/20))
7
+ * Fixed bug when setting `default_limi` and trying to find a record
8
+ ([35](https://github.com/beezwax/fmrest-ruby/issues/35))
9
+
3
10
  ### 0.4.1
4
11
 
5
12
  * Prevent raising an exception when a /\_find request yields no results
data/README.md CHANGED
@@ -587,6 +587,16 @@ Honeybee.query(name: "Hutch", omit: true)
587
587
  # JSON -> {"query": [{"Bee Name": "Hutch", "omit": "true"}]}
588
588
  ```
589
589
 
590
+ #### .script
591
+
592
+ `.script` enables the execution of scripts during query requests.
593
+
594
+ ```ruby
595
+ Honeybee.script("My script").find_some # Fetch records and execute a script
596
+ ```
597
+
598
+ See section on [script execution](#script-execution) below for more info.
599
+
590
600
  #### Other notes on querying
591
601
 
592
602
  You can chain all query methods together:
@@ -625,14 +635,18 @@ force `.limit(1)`):
625
635
  Honeybee.query(name: "Hutch").find_one # => <Honeybee...>
626
636
  ```
627
637
 
628
- NOTE: If you know the id of the record you should use `.find(id)` instead of
629
- `.query(id: id).find_one` (so that the request is sent as `GET ../:layout/records/:id`
630
- instead of `POST ../:layout/_find`).
638
+ If you know the id of the record you should use `.find(id)` instead of
639
+ `.query(id: id).find_one` (so that the sent request is
640
+ `GET ../:layout/records/:id` instead of `POST ../:layout/_find`).
631
641
 
632
642
  ```ruby
633
643
  Honeybee.find(89) # => <Honeybee...>
634
644
  ```
635
645
 
646
+ Note also that if you use `.find(id)` your `.query()` parameters (as well as
647
+ limit, offset and sort parameters) will be discarded as they're not supported
648
+ by the single record endpoint.
649
+
636
650
  ### Container fields
637
651
 
638
652
  You can define container fields on your model class with `container`:
@@ -670,6 +684,118 @@ bee.photo.upload(filename_or_io) # Upload a file to the container
670
684
  * `:content_type` - The MIME content type to use (defaults to
671
685
  `application/octet-stream`)
672
686
 
687
+ ### Script execution
688
+
689
+ The Data API allows running scripts as part of many types of requests.
690
+
691
+ #### Model.execute_script
692
+ As of FM18 you can execute scripts directly. To do that for a specific model
693
+ use `Model.execute_script`:
694
+
695
+ ```ruby
696
+ result = Honeybee.execute_script("My Script", param: "optional parameter")
697
+ ```
698
+
699
+ This will return a `Spyke::Result` object containing among other things the
700
+ result of the script execution:
701
+
702
+ ```ruby
703
+ result.metadata[:script][:after]
704
+ # => { result: "oh hi", error: "0" }
705
+ ```
706
+
707
+ #### Script options object format
708
+
709
+ All other script-capable requests take one or more of three possible script
710
+ execution options: `script.prerequest`, `script.presort` and plain `script`
711
+ (which fmrest-ruby dubs `after` for convenience).
712
+
713
+ Because of that fmrest-ruby uses a common object format for specifying script options
714
+ across multiple methods. That object format is as follows:
715
+
716
+ ```ruby
717
+ # Just a string means to execute that `after' script without a parameter
718
+ "My Script"
719
+
720
+ # A 2-elemnent array means [script name, script parameter]
721
+ ["My Script", "parameter"]
722
+
723
+ # A hash with keys :prerequest, :presort and/or :after sets those scripts for
724
+ {
725
+ prerequest: "My Prerequest Script",
726
+ presort: "My Presort Script",
727
+ after: "My Script"
728
+ }
729
+
730
+ # Using 2-element arrays as objects in the hash allows specifying parameters
731
+ {
732
+ prerequest: ["My Prerequest Script", "parameter"],
733
+ presort: ["My Presort Script", "parameter"],
734
+ after: ["My Script", "parameter"]
735
+ }
736
+ ```
737
+
738
+ #### Script execution on record save, destroy and reload
739
+
740
+ A record instance's `.save` and `.destroy` methods both accept a `script:`
741
+ option to which you can pass a script options object with
742
+ [the above format](#script-options-object-format):
743
+
744
+ ```ruby
745
+ # Save the record and execute an `after' script called "My Script"
746
+ bee.save(script: "My Script")
747
+
748
+ # Same as above but with an added parameter
749
+ bee.save(script: ["My Script", "parameter"])
750
+
751
+ # Save the record and execute a presort script and an `after' script
752
+ bee.save(script: { presort: "My Presort Script", after: "My Script" })
753
+
754
+ # Destroy the record and execute a prerequest script with a parameter
755
+ bee.destroy(script: { prerequest: ["My Prerequest Script", "parameter"] })
756
+
757
+ # Reload the record and execute a prerequest script with a parameter
758
+ bee.reload(script: { prerequest: ["My Prerequest Script", "parameter"] })
759
+ ```
760
+
761
+ #### Retrieving script execution results
762
+
763
+ Every time a request is ran on a model or record instance of a model, a
764
+ thread-local `Model.last_request_metadata` attribute is set on that model,
765
+ which is a hash containing the results of script executions, if any were
766
+ performed, among other metadata.
767
+
768
+ The results for `:after`, `:prerequest` and `:presort` scripts are stored
769
+ separately, under their matching key.
770
+
771
+ ```ruby
772
+ bee.save(script: { presort: "My Presort Script", after: "My Script" })
773
+
774
+ Honeybee.last_request_metadata[:script]
775
+ # => { after: { result: "oh hi", error: "0" }, presort: { result: "lo", error: "0" } }
776
+ ```
777
+
778
+ #### Executing scripts through query requests
779
+
780
+ As mentioned under the [Query API](#query-api) section, you can use the
781
+ `.script` query method to specify that you want scripts executed when a query
782
+ is performed on that scope.
783
+
784
+ `.script` takes the same options object specified [above](#script-options-object-format):
785
+
786
+ ```ruby
787
+ # Find one Honeybee record executing a presort and after script
788
+ Honeybee.script(presort: ["My Presort Script", "parameter"], after: "My Script").find_one
789
+ ```
790
+
791
+ The model class' `.last_request_metadata` will be set in case you need to get the result.
792
+
793
+ In the case of retrieving multiple results (i.e. via `.find_some`) the
794
+ resulting collection will have a `.metadata` attribute method containing the
795
+ same metadata hash with script execution results. Note that this does not apply
796
+ to retrieving single records, in that case you'll have to use
797
+ `.last_request_metadata`.
798
+
673
799
  ## Logging
674
800
 
675
801
  If using fmrest-ruby + Spyke in a Rails app pretty log output will be set up
@@ -30,7 +30,7 @@ Gem::Specification.new do |spec|
30
30
  spec.add_development_dependency "webmock"
31
31
  spec.add_development_dependency "pry-byebug"
32
32
  spec.add_development_dependency "activerecord"
33
- spec.add_development_dependency "sqlite3", "~> 1.3.6"
33
+ spec.add_development_dependency "sqlite3"
34
34
  spec.add_development_dependency "mock_redis"
35
35
  spec.add_development_dependency "moneta"
36
36
  end
@@ -11,6 +11,7 @@ module FmRest
11
11
  MULTIPLE_RECORDS_RE = %r(/records\z).freeze
12
12
  CONTAINER_RE = %r(/records/\d+/containers/[^/]+/\d+\z).freeze
13
13
  FIND_RECORDS_RE = %r(/_find\b).freeze
14
+ SCRIPT_REQUEST_RE = %r(/script/[^/]+\z).freeze
14
15
 
15
16
  VALIDATION_ERROR_RANGE = 500..599
16
17
 
@@ -32,6 +33,11 @@ module FmRest
32
33
  env.body = prepare_collection(json)
33
34
  when create_request?(env), update_request?(env), delete_request?(env), container_upload_request?(env)
34
35
  env.body = prepare_save_response(json)
36
+ when execute_script_request?(env)
37
+ env.body = build_base_hash(json)
38
+ else
39
+ # Attempt to parse unknown requests too
40
+ env.body = build_base_hash(json)
35
41
  end
36
42
  end
37
43
 
@@ -72,11 +78,35 @@ module FmRest
72
78
  # @return [Hash] the skeleton structure for a Spyke-formatted response
73
79
  def build_base_hash(json, include_errors = false)
74
80
  {
75
- metadata: { messages: json[:messages] },
81
+ metadata: { messages: json[:messages] }.merge(script: prepare_script_results(json).presence),
76
82
  errors: include_errors ? prepare_errors(json) : {}
77
83
  }
78
84
  end
79
85
 
86
+ # @param json [Hash]
87
+ # @return [Hash] the script(s) execution results for Spyke metadata format
88
+ def prepare_script_results(json)
89
+ results = {}
90
+
91
+ [:prerequest, :presort].each do |s|
92
+ if json[:response][:"scriptError.#{s}"]
93
+ results[s] = {
94
+ result: json[:response][:"scriptResult.#{s}"],
95
+ error: json[:response][:"scriptError.#{s}"]
96
+ }
97
+ end
98
+ end
99
+
100
+ if json[:response][:scriptError]
101
+ results[:after] = {
102
+ result: json[:response][:scriptResult],
103
+ error: json[:response][:scriptError]
104
+ }
105
+ end
106
+
107
+ results
108
+ end
109
+
80
110
  # @param json [Hash]
81
111
  # @return [Hash] the errors hash in Spyke format
82
112
  def prepare_errors(json)
@@ -196,6 +226,10 @@ module FmRest
196
226
  env.method == :delete && env.url.path.match(SINGLE_RECORD_RE)
197
227
  end
198
228
 
229
+ def execute_script_request?(env)
230
+ env.method == :get && env.url.path.match(SCRIPT_REQUEST_RE)
231
+ end
232
+
199
233
  # @param source [String] a JSON string
200
234
  # @return [Hash] the parsed JSON
201
235
  def parse_json(source)
@@ -7,6 +7,7 @@ require "fmrest/spyke/model/serialization"
7
7
  require "fmrest/spyke/model/associations"
8
8
  require "fmrest/spyke/model/orm"
9
9
  require "fmrest/spyke/model/container_fields"
10
+ require "fmrest/spyke/model/http"
10
11
 
11
12
  module FmRest
12
13
  module Spyke
@@ -20,6 +21,7 @@ module FmRest
20
21
  include Associations
21
22
  include Orm
22
23
  include ContainerFields
24
+ include Http
23
25
 
24
26
  included do
25
27
  # @return [Integer] the record's modId
@@ -64,7 +64,7 @@ module FmRest
64
64
  end
65
65
  end
66
66
 
67
- def reload
67
+ def reload(*_)
68
68
  super.tap { @loaded_portals = nil }
69
69
  end
70
70
 
@@ -0,0 +1,37 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "fmrest/spyke/relation"
4
+
5
+ module FmRest
6
+ module Spyke
7
+ module Model
8
+ module Http
9
+ extend ::ActiveSupport::Concern
10
+
11
+ class_methods do
12
+
13
+ # Override Spyke's request method to keep a thread-local copy of the
14
+ # last request's metadata, so that we can access things like script
15
+ # execution results after a save, etc.
16
+
17
+
18
+ def request(*args)
19
+ super.tap do |r|
20
+ Thread.current[last_request_metadata_key] = r.metadata
21
+ end
22
+ end
23
+
24
+ def last_request_metadata(key: last_request_metadata_key)
25
+ Thread.current[key]
26
+ end
27
+
28
+ private
29
+
30
+ def last_request_metadata_key
31
+ "#{to_s}.last_request_metadata"
32
+ end
33
+ end
34
+ end
35
+ end
36
+ end
37
+ end
@@ -23,10 +23,10 @@ module FmRest
23
23
  # Methods delegated to FmRest::Spyke::Relation
24
24
  delegate :limit, :offset, :sort, :order, :query, :omit, :portal,
25
25
  :portals, :includes, :with_all_portals, :without_portals,
26
- to: :all
26
+ :script, to: :all
27
27
 
28
28
  def all
29
- # Use FmRest's Relation insdead of Spyke's vanilla one
29
+ # Use FmRest's Relation instead of Spyke's vanilla one
30
30
  current_scope || Relation.new(self, uri: uri)
31
31
  end
32
32
 
@@ -66,6 +66,12 @@ module FmRest
66
66
  new(attributes).tap(&:save!)
67
67
  end
68
68
 
69
+ def execute_script(script_name, param: nil)
70
+ params = {}
71
+ params = {"script.param" => param} unless param.nil?
72
+ request(:get, FmRest::V1::script_path(layout, script_name), params)
73
+ end
74
+
69
75
  private
70
76
 
71
77
  def extend_scope_with_fm_params(scope, prefixed: false)
@@ -92,11 +98,15 @@ module FmRest
92
98
  end
93
99
  end
94
100
 
101
+ if scope.script_params.present?
102
+ where_options.merge!(scope.script_params)
103
+ end
104
+
95
105
  scope.where(where_options)
96
106
  end
97
107
  end
98
108
 
99
- # Completely override Spyke's save to provide a number of features:
109
+ # Overwrite Spyke's save to provide a number of features:
100
110
  #
101
111
  # * Validations
102
112
  # * Data API scripts execution
@@ -115,15 +125,32 @@ module FmRest
115
125
  save(options.merge(raise_validation_errors: true))
116
126
  end
117
127
 
128
+ # Overwrite Spyke's destroy to provide Data API script execution
129
+ #
130
+ def destroy(options = {})
131
+ # For whatever reason the Data API wants the script params as query
132
+ # string params for DELETE requests, making this more complicated
133
+ # than it should be
134
+ script_query_string = if options.has_key?(:script)
135
+ "?" + Faraday::Utils.build_query(FmRest::V1.convert_script_params(options[:script]))
136
+ else
137
+ ""
138
+ end
139
+
140
+ self.attributes = delete(uri.to_s + script_query_string)
141
+ end
142
+
118
143
  # API-error-raising version of #update
119
144
  #
120
- def update!(new_attributes)
145
+ def update!(new_attributes, options = {})
121
146
  self.attributes = new_attributes
122
- save!
147
+ save!(options)
123
148
  end
124
149
 
125
- def reload
126
- reloaded = self.class.find(id)
150
+ def reload(options = {})
151
+ scope = self.class
152
+ scope = scope.script(options[:script]) if options.has_key?(:script)
153
+ reloaded = scope.find(id)
127
154
  self.attributes = reloaded.attributes
128
155
  self.mod_id = reloaded.mod_id
129
156
  end
@@ -12,7 +12,7 @@ module FmRest
12
12
 
13
13
 
14
14
  attr_accessor :limit_value, :offset_value, :sort_params, :query_params,
15
- :included_portals, :portal_params
15
+ :included_portals, :portal_params, :script_params
16
16
 
17
17
  def initialize(*_args)
18
18
  super
@@ -27,6 +27,42 @@ module FmRest
27
27
 
28
28
  @included_portals = nil
29
29
  @portal_params = {}
30
+ @script_params = {}
31
+ end
32
+
33
+ # @param options [String, Array, Hash, nil, false] sets script params to
34
+ # execute in the next get or find request
35
+ #
36
+ # @example
37
+ # # Find records and run the script named "My script"
38
+ # Person.script("My script").find_some
39
+ #
40
+ # # Find records and run the script named "My script" with param "the param"
41
+ # Person.script(["My script", "the param"]).find_some
42
+ #
43
+ # # Find records and run a prerequest, presort and after (normal) script
44
+ # Person.script(after: "Script", prerequest: "Prereq script", presort: "Presort script").find_some
45
+ #
46
+ # # Same as above, but passing parameters too
47
+ # Person.script(
48
+ # after: ["After script", "the param"],
49
+ # prerequest: ["Prereq script", "the param"],
50
+ # presort: o ["Presort script", "the param"]
51
+ # ).find_some
52
+ #
53
+ # Person.script(nil).find_some # Disable script execution
54
+ # Person.script(false).find_some # Disable script execution
55
+ #
56
+ # @return [FmRest::Spyke::Relation] a new relation with the script
57
+ # options applied
58
+ def script(options)
59
+ with_clone do |r|
60
+ if options.eql?(false) || options.eql?(nil)
61
+ r.script_params = {}
62
+ else
63
+ r.script_params = script_params.merge(FmRest::V1.convert_script_params(options))
64
+ end
65
+ end
30
66
  end
31
67
 
32
68
  # @param value_or_hash [Integer, Hash] the limit value for this layout,
@@ -140,12 +176,17 @@ module FmRest
140
176
  query_params.present?
141
177
  end
142
178
 
143
- # Finds a single instance of the model by forcing limit = 1
179
+ # Finds a single instance of the model by forcing limit = 1, or simply
180
+ # fetching the record by id if the primary key was set
144
181
  #
145
182
  # @return [FmRest::Spyke::Base]
146
183
  def find_one
147
- return super if params[klass.primary_key].present?
148
- @find_one ||= klass.new_collection_from_result(limit(1).fetch).first
184
+ @find_one ||=
185
+ if primary_key_set?
186
+ without_collection_params { super }
187
+ else
188
+ klass.new_collection_from_result(limit(1).fetch).first
189
+ end
149
190
  rescue ::Spyke::ConnectionError => error
150
191
  fallback_or_reraise(error, default: nil)
151
192
  end
@@ -223,6 +264,18 @@ module FmRest
223
264
  end
224
265
  end
225
266
 
267
+ def primary_key_set?
268
+ params[klass.primary_key].present?
269
+ end
270
+
271
+ def without_collection_params
272
+ orig_values = limit_value, offset_value, sort_params, query_params
273
+ self.limit_value = self.offset_value = self.sort_params = self.query_params = nil
274
+ yield
275
+ ensure
276
+ self.limit_value, self.offset_value, self.sort_params, self.query_params = orig_values
277
+ end
278
+
226
279
  def with_clone
227
280
  clone.tap do |relation|
228
281
  yield relation
@@ -26,6 +26,10 @@ module FmRest
26
26
  "layouts/#{url_encode(layout)}/_find"
27
27
  end
28
28
 
29
+ def script_path(layout, script)
30
+ "layouts/#{url_encode(layout)}/script/#{url_encode(script)}"
31
+ end
32
+
29
33
  def globals_path
30
34
  "globals"
31
35
  end
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module FmRest
4
- VERSION = "0.4.1"
4
+ VERSION = "0.5.0"
5
5
  end
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: fmrest
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.4.1
4
+ version: 0.5.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Pedro Carbajal
8
8
  autorequire:
9
9
  bindir: exe
10
10
  cert_chain: []
11
- date: 2019-10-28 00:00:00.000000000 Z
11
+ date: 2020-02-13 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: faraday
@@ -152,16 +152,16 @@ dependencies:
152
152
  name: sqlite3
153
153
  requirement: !ruby/object:Gem::Requirement
154
154
  requirements:
155
- - - "~>"
155
+ - - ">="
156
156
  - !ruby/object:Gem::Version
157
- version: 1.3.6
157
+ version: '0'
158
158
  type: :development
159
159
  prerelease: false
160
160
  version_requirements: !ruby/object:Gem::Requirement
161
161
  requirements:
162
- - - "~>"
162
+ - - ">="
163
163
  - !ruby/object:Gem::Version
164
- version: 1.3.6
164
+ version: '0'
165
165
  - !ruby/object:Gem::Dependency
166
166
  name: mock_redis
167
167
  requirement: !ruby/object:Gem::Requirement
@@ -220,6 +220,7 @@ files:
220
220
  - lib/fmrest/spyke/model/attributes.rb
221
221
  - lib/fmrest/spyke/model/connection.rb
222
222
  - lib/fmrest/spyke/model/container_fields.rb
223
+ - lib/fmrest/spyke/model/http.rb
223
224
  - lib/fmrest/spyke/model/orm.rb
224
225
  - lib/fmrest/spyke/model/serialization.rb
225
226
  - lib/fmrest/spyke/model/uri.rb
@@ -260,8 +261,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
260
261
  - !ruby/object:Gem::Version
261
262
  version: '0'
262
263
  requirements: []
263
- rubyforge_project:
264
- rubygems_version: 2.7.8
264
+ rubygems_version: 3.0.6
265
265
  signing_key:
266
266
  specification_version: 4
267
267
  summary: FileMaker Data API client using Faraday