fmrest 0.4.1 → 0.5.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 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