ecoportal-api 0.8.2 → 0.8.5

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: ba5ffaf73f0c065f6aa6572b5ac82132af9ee19f61692f80605ee156b29d24ee
4
- data.tar.gz: d82d89d09d4ae1edb88a3dc01f613478e7bd5ab38184dadb7c507e53e65bae54
3
+ metadata.gz: 4364abf59cd6460788d315eb5012df5e8ad254a208e8af7314a766463eb35ed0
4
+ data.tar.gz: d71fe7e38402e827aea0f95c6a0c379e191e525d597ab8b225990f48c6879764
5
5
  SHA512:
6
- metadata.gz: f3b637bd2bc2970b7f46eb8d92edc6b83a38ef4489d6d185e4cea47d8a5e6cd774d9bf9e8712bd0d76aa9a52c770e89207f0c7af283fcb85bc1cb481d31fca10
7
- data.tar.gz: 6a5dd4dd6fd2691971c526e79d2278a84351484a0d9ffdd93e3604392f4b59e21515255001c51016835a37506496d1d292f33c4dc69d4db7042e9c3d521721c3
6
+ metadata.gz: e055587b4aafbfabf25996cf4e67d85b0a0d9be936d4b0bdae194b431e4f2bb12b09fc99562d594cf355767aae3081a1fbea70d10a48dc204ce839284bbf0256
7
+ data.tar.gz: 5cd7284e455af271f1642775844636fd0ea11b590c0036d3b4a43868a9ffd6ece97ceebb9a3d6dfcdae7891dd65dedd0df9dac5944578cb18484b07342640c3e
data/CHANGELOG.md CHANGED
@@ -1,7 +1,92 @@
1
1
  # Change Log
2
2
  All notable changes to this project will be documented in this file.
3
3
 
4
- ## [0.8.2] - 2021-02-xx
4
+ ## [0.8.5] - 2022-02-28
5
+
6
+ ### Added
7
+ - `Ecoportal::API::V1::PersonDetails#[]` to raise a specific error type to allow handling
8
+ - `Ecoportal::API::V1::PersonDetails.key?` to allow to check if a field exists
9
+ - `Ecoportal::API::Internal::Account#force_send_invites` support for back-end new method
10
+
11
+ ### Fixed
12
+ - `Ecoportal::API::V1::People#get` fixed typo
13
+ - `Ecoportal::API::Common::BaseModel` `#original_doc` and `#initial_doc` maybe empty for the parent object
14
+
15
+ ### Changed
16
+ - `Ecoportal::API::V1::People#each` limited the `GET` retries to `5`
17
+ - `Ecoportal::API::Internal::Account#default_tag=`
18
+ - Controls input type to be `String` or `nil`
19
+ - Inherent `upcase`
20
+
21
+ ## [0.8.4] - 2021-11-05
22
+
23
+ ### Added
24
+ - `Ecoportal::API::Internal::Permissions` added abilities
25
+ - `visitor_management`, `cross_register_reporting` and `broadcast_notifications`
26
+ - Some yardocs too
27
+ - Some callbacks are done in a non-obvious way and the returned object type was not documented
28
+ - For this reason, some yardocs have been added to some of the parts that have been worked on.
29
+
30
+ ### Fixed
31
+ - `Ecoportal::API::V1::People#create_job`
32
+ - **Removed** call to `BatchOperation#process_response`
33
+ - The method was is already called by `job_result`
34
+ - As a consequence there was a double up of `callbacks`
35
+ - **Fixed** line in wrong position
36
+ - `Ecoportal::API::V1::People#batch`
37
+ - `Ecoportal::API::Common::ElasticApmIntegration#unexpected_server_error?`
38
+ - No code or code lesser than 100 is a server error as well
39
+
40
+ ### Changed
41
+ - `Ecoportal::API::Common::Client`: changed
42
+ - Logging the **response** of batches or batch jobs can be handy when debugging the back-end
43
+ - **removed** method `#without_response_logging`
44
+ - This change entailed to remove dependencies in `Ecoportal::API::V1::People`
45
+ - Specifically in methods `#batch`, `#job_result` and `#create_job`
46
+ - `@response_logging_enabled` to be set in initialization stage (added parameter for `.new`)
47
+
48
+ ## [0.8.3] - 2021-05-24
49
+
50
+ ### Added
51
+ - `Ecoportal::API::Errors` namespace
52
+ - `Ecoportal::API::Errors::Base` base error class.
53
+ - `Ecoportal::API::Errors::TimeOut` error when an api request fails with time out.
54
+ - This serves the purpose to allow a client script to re-start the process where it stopped by capturing this specific Error
55
+ - `Ecoportal::API::Common::BaseModel::UnlinkedModel` added more description to track down the source of the error.
56
+ - `Ecoportal::API::Common::BaseModel#reset!` added parameter `key`, which should try to recover `doc[key]` from `original_doc[key]`
57
+ - Thanks to this, you are supposed to be able to do things like:
58
+ - `person.account = nil && person.reset!("account")`
59
+ - `person.name = nil && person.reset!("name")`
60
+ - `Ecoportal::API::V1::People#job` methods to provide more information on failure.
61
+ - Specific changes due to eP **release `1.5.9.70`** (_Policy Group Abilities_)
62
+ - `Ecoportal::API::Internal::Account#permissions_merged`
63
+ - `Ecoportal::API::Internal::Permissions#person_abilities` (new ability)
64
+ - `Ecoportal::API::Internal::Account#user_id`
65
+
66
+ ### Fixed
67
+ - `Ecoportal::API::Internal::Account`: consistency in setting arrays (`uniq!` & `compact`)
68
+ - `#policy_group_ids=`, `#login_provider_ids=`, `#starred_ids=`
69
+ - `Ecoportal::API::Common::HashDiff.diff` was including empty `{}` objects
70
+ - This change sacrifices the case `account: {}` (which will be also removed from `as_update`), but it should be fine.
71
+
72
+ ### Changed
73
+ - `Ecoportal::API::V1::People#job` to raise specific error on time out `API::Errors::TimeOut`
74
+ - Specific changes due to eP **release `1.5.9.70`** (_Policy Group Abilities_)
75
+ - `Ecoportal::API::Internal::Account` **removed** methods: `#permissions_preset`, `#preset` and `#preset=`
76
+ - `Ecoportal::API::Internal::Person#account=` added support for `user_id` which should remain unchanged when existing
77
+ - **remove** from `as_update` **read-only** data
78
+ - `Ecoportal::API::Common::HashDiff.diff` added parameter `:ignore` (`Array`)
79
+ - `Ecoportal::API::Common::BaseModel#as_update` added parameter `:ignore`
80
+ - `Ecoportal::API::V1::Person#as_update` added method, which ignores `subordinates`
81
+ - `Ecoportal::API::Internal::Person#as_update` added method, which ignores `user_id`, `permissions_merged` and `prefilter`
82
+ - `Ecoportal::API::Internal::Account#as_update` added method, which ignores `user_id`, `permissions_merged` and `prefilter`
83
+ - `Ecoportal::API::Common::Client` native support for `elastic-apm`
84
+ - Via new module `Ecoportal::API::Common::ElasticApmIntegration` with method `log_unexpected_server_error`, which will only log an `UnexpectedServerError` to _ElasticAPM_ if
85
+ 1. There's a correct configuration: environmental variables `ELASTIC_APM_KEY` and `ELASTIC_APM_ACCOUNT_ID` are defined
86
+ 2. The `Response` from the server gave `code` in the range `5xx` (which are those under server responsibility)
87
+ - `Ecoportal::API::Common::Client` added retry logics when `response.status == 5xx`
88
+
89
+ ## [0.8.2] - 2021-02-24
5
90
 
6
91
  ### Added
7
92
 
@@ -22,7 +22,7 @@ Gem::Specification.new do |spec|
22
22
  spec.executables = spec.files.grep(%r{^exe/}) { |f| File.basename(f) }
23
23
  spec.require_paths = ["lib"]
24
24
 
25
- spec.add_development_dependency "bundler", ">= 2.2.11", "< 2.3"
25
+ spec.add_development_dependency "bundler", ">= 2.2.17", "< 2.3"
26
26
  spec.add_development_dependency "rspec", ">= 3.10.0", "< 3.11"
27
27
  spec.add_development_dependency "rake", ">= 13.0.3", "< 13.1"
28
28
  spec.add_development_dependency "yard", ">= 0.9.26", "< 0.10"
@@ -30,5 +30,7 @@ Gem::Specification.new do |spec|
30
30
  spec.add_development_dependency "pry" , "~> 0.14"
31
31
 
32
32
  spec.add_dependency 'http', '~> 4.4.1', "< 5"
33
+ spec.add_dependency 'dotenv', '>= 2.7.6', "< 2.8"
34
+ spec.add_dependency 'elastic-apm', '>= 4.0.0', "< 4.1"
33
35
  spec.add_dependency 'hash-polyfill', '~> 0'
34
36
  end
@@ -3,7 +3,9 @@ module Ecoportal
3
3
  module Common
4
4
  class BaseModel
5
5
  class UnlinkedModel < Exception
6
- def initialize (msg = "Something went wrong when linking the document.")
6
+ def initialize (msg = "Something went wrong when linking the document.", from: nil, key: nil)
7
+ msg += " From: #{from}." if from
8
+ msg += " key: #{key}." if key
7
9
  super(msg)
8
10
  end
9
11
  end
@@ -28,7 +30,12 @@ module Ecoportal
28
30
  var = "@#{method}".freeze
29
31
  key = key.to_s.freeze
30
32
  define_method(method) do
31
- return instance_variable_get(var) if instance_variable_defined?(var)
33
+ if instance_variable_defined?(var)
34
+ value = instance_variable_get(var)
35
+ return value unless nullable
36
+ return value if (value && doc[key]) || (!value && !doc[key])
37
+ remove_instance_variable(var)
38
+ end
32
39
  doc[key] ||= {} unless nullable
33
40
  return instance_variable_set(var, nil) unless doc[key]
34
41
 
@@ -53,21 +60,21 @@ module Ecoportal
53
60
  end
54
61
 
55
62
  def doc
56
- raise UnlinkedModel.new unless linked?
63
+ raise UnlinkedModel.new(from: "#{self.class}#doc", key: _key) unless linked?
57
64
  return @doc if is_root?
58
65
  _parent.doc.dig(*[_key].flatten)
59
66
  end
60
67
 
61
68
  def original_doc
62
- raise UnlinkedModel.new unless linked?
69
+ raise UnlinkedModel.new(from: "#{self.class}#original_doc", key: _key) unless linked?
63
70
  return @original_doc if is_root?
64
- _parent.original_doc.dig(*[_key].flatten)
71
+ _parent.original_doc&.dig(*[_key].flatten)
65
72
  end
66
73
 
67
74
  def initial_doc
68
- raise UnlinkedModel.new unless linked?
75
+ raise UnlinkedModel.new(from: "#{self.class}#initial_doc", key: _key) unless linked?
69
76
  return @initial_doc if is_root?
70
- _parent.initial_doc.dig(*[_key].flatten)
77
+ _parent.initial_doc&.dig(*[_key].flatten)
71
78
  end
72
79
 
73
80
  def as_json
@@ -78,18 +85,19 @@ module Ecoportal
78
85
  doc.to_json(*args)
79
86
  end
80
87
 
81
- def as_update(ref = :last)
88
+ def as_update(ref = :last, ignore: [])
82
89
  new_doc = as_json
83
90
  ref_doc = ref == :total ? initial_doc : original_doc
84
- Common::HashDiff.diff(new_doc, ref_doc)
91
+ Common::HashDiff.diff(new_doc, ref_doc, ignore: ignore)
85
92
  end
86
93
 
87
94
  def dirty?
88
95
  as_update != {}
89
96
  end
90
97
 
98
+ # It consolidates all the changes carried by `doc` by setting it as `original_doc`.
91
99
  def consolidate!
92
- raise UnlinkedModel.new unless linked?
100
+ raise UnlinkedModel.new(from: "#{self.class}#consolidate!", key: _key) unless linked?
93
101
  new_doc = JSON.parse(doc.to_json)
94
102
  if is_root?
95
103
  @original_doc = new_doc
@@ -98,13 +106,32 @@ module Ecoportal
98
106
  end
99
107
  end
100
108
 
101
- def reset!
102
- raise UnlinkedModel.new unless linked?
103
- new_doc = JSON.parse(original_doc.to_json)
104
- if is_root?
105
- @doc = new_doc
109
+ # It removes all the changes carried by `doc` by restoring `original_doc` into `doc`.
110
+ # @note
111
+ # 1. When there are nullable properties, it may be required to apply `reset!` from the parent
112
+ # i.e. `parent.reset!("child")` # when parent.child is `nil`
113
+ # 2. In such a case, only immediate childs are allowed to be reset
114
+ # @param key [String, Array<String>, nil] if given, it only resets the specified property
115
+ def reset!(key = nil)
116
+ raise "'key' should be a String. Given #{key}" unless !key || key.is_a?(String)
117
+ raise UnlinkedModel.new(from: "#{self.class}#reset!", key: _key) unless linked?
118
+
119
+ if key
120
+ if self.respond_to?(key) && child = self.send(key) && child.is_a?(Ecoportal::API::Common::BaseModel)
121
+ child.reset!
122
+ else
123
+ new_doc = original_doc && original_doc[key]
124
+ dig_set(doc, [key], new_doc && JSON.parse(new_doc.to_json))
125
+ # regenerate object if new_doc is null
126
+ self.send(key) if !new_doc && self.respond_to?(key)
127
+ end
106
128
  else
107
- dig_set(_parent.doc, [_key].flatten, new_doc)
129
+ new_doc = JSON.parse(original_doc.to_json)
130
+ if is_root?
131
+ @doc = new_doc
132
+ else
133
+ dig_set(_parent.doc, [_key].flatten, new_doc)
134
+ end
108
135
  end
109
136
  end
110
137
 
@@ -133,6 +160,17 @@ module Ecoportal
133
160
  end
134
161
  end
135
162
 
163
+ def set_uniq_array_keep_order(key, value)
164
+ unless value.is_a?(Array)
165
+ raise "#{key}= needs to be passed an Array, got #{value.class}"
166
+ end
167
+ ini_vals = (original_doc && original_doc[key]) || []
168
+
169
+ value = value.uniq
170
+ # preserve original order to avoid false updates
171
+ doc[key] = ((ini_vals & value) + (value - ini_vals)).compact
172
+ end
173
+
136
174
  end
137
175
  end
138
176
  end
@@ -10,6 +10,9 @@ module Ecoportal
10
10
  # - to return `HTTP::Response` ([response.rb](https://github.com/httprb/http/blob/master/lib/http/response.rb))
11
11
  # @attr_reader logger [Logger] the logger.
12
12
  class Client
13
+ include Common::ElasticApmIntegration
14
+ DELAY_REQUEST_RETRY = 5
15
+
13
16
  attr_accessor :logger
14
17
 
15
18
  # @note the `api_key` will be automatically added as parameter `X-ApiKey` in the header of the http requests.
@@ -17,11 +20,14 @@ module Ecoportal
17
20
  # @param version [String] it is part of the base url and will determine the api version we query against.
18
21
  # @param host [String] api server domain.
19
22
  # @param logger [Logger] an object with `Logger` interface to generate logs.
23
+ # @param response_logging [Boolean] whether or not batch responses should be logged
20
24
  # @return [Client] an object that holds the configuration of the api connection.
21
- def initialize(api_key:, version: "v1", host: "live.ecoportal.com", logger: nil)
25
+ def initialize(api_key:, version: "v1", host: "live.ecoportal.com", logger: nil, response_logging: false)
22
26
  @version = version
23
27
  @api_key = api_key
24
28
  @logger = logger
29
+ @host = host
30
+ @response_logging_enabled = response_logging
25
31
  if host.match(/^localhost|^127\.0\.0\.1/)
26
32
  @base_uri = "http://#{host}/api/"
27
33
  else
@@ -31,7 +37,6 @@ module Ecoportal
31
37
  if @api_key.nil? || @api_key.match(/\A\W*\z/)
32
38
  log(:error) { "Api-key missing!" }
33
39
  end
34
- @response_logging_enabled = true
35
40
  end
36
41
 
37
42
  # Logger interface.
@@ -106,7 +111,7 @@ module Ecoportal
106
111
  # basic HTTP connection to the block.
107
112
  # @yield [http] launch specific http request.
108
113
  # @yieldparam http [HTTP] the http connection.
109
- # @yieldreturn [Common::Response] the basic custom reponse object.
114
+ # @yieldreturn [Common::Response] the basic custom response object.
110
115
  # @return [Common::Reponse] the basic custom response object.
111
116
  def request
112
117
  wrap_response yield(base_request)
@@ -140,22 +145,15 @@ module Ecoportal
140
145
  @base_uri+@version+path
141
146
  end
142
147
 
143
- def without_response_logging(&block)
144
- begin
145
- @response_logging_enabled = false
146
- yield self
147
- ensure
148
- @response_logging_enabled = true
149
- end
150
- end
151
-
152
148
  private
153
149
 
154
- def instrument(method, path, data = nil)
150
+ def instrument(method, path, data = nil, &block)
151
+ raise "Expected block" unless block
155
152
  start_time = Time.now.to_f
156
- log(:info) { "#{method} #{url_for(path)}" }
153
+ log(:info) { "#{method} #{url_for(path)}" }
157
154
  log(:debug) { "Data: #{JSON.pretty_generate(data)}" }
158
- yield.tap do |result|
155
+
156
+ with_retry(&block).tap do |result|
159
157
  end_time = Time.now.to_f
160
158
  log(result.success?? :info : :warn) do
161
159
  "Took %.2fs, Status #{result.status}" % (end_time - start_time)
@@ -165,6 +163,33 @@ module Ecoportal
165
163
  end if @response_logging_enabled
166
164
  end
167
165
  end
166
+
167
+ # Helper to ensure unexpected server errors do not bring client scripts immediately down
168
+ def with_retry(attempts = 3, delay = DELAY_REQUEST_RETRY, error_safe: true, &block)
169
+ response = nil
170
+ attempts.times do |i|
171
+ remaining = attempts - i - 1
172
+ begin
173
+ response = block.call
174
+ rescue HTTP::ConnectionError => e
175
+ raise unless error_safe && remaining > 0
176
+ log(:error) { "Got connection error: #{e.message}" }
177
+ response = with_retry(remaining, error_safe: error_safe, &block)
178
+ rescue IOError => e
179
+ raise unless error_safe && remaining > 0
180
+ log(:error) { "Got IO error: #{e.message}" }
181
+ response = with_retry(remaining, error_safe: error_safe, &block)
182
+ end
183
+ return response unless unexpected_server_error?(response.status)
184
+ log_unexpected_server_error(response)
185
+ msg = "Got server error (#{response.status}): #{response.body}\n"
186
+ msg += "Going to retry (#{i} out of #{attempts})"
187
+ log(:error) { msg }
188
+ sleep(delay) if i < attempts
189
+ end
190
+ response
191
+ end
192
+
168
193
  end
169
194
  end
170
195
  end
@@ -0,0 +1,112 @@
1
+ require 'elastic-apm'
2
+ module Ecoportal
3
+ module API
4
+ module Common
5
+ module ElasticApmIntegration
6
+
7
+ class UnexpectedServerError < StandardError
8
+ def initialize(code, msg)
9
+ super("Code: #{code} -- Error: #{msg}")
10
+ end
11
+ end
12
+
13
+ APM_SERVICE_NAME = 'ecoportal-api-gem'
14
+
15
+ # Log only errors that are only server's responsibility
16
+ def log_unexpected_server_error(response)
17
+ raise "Expecting Ecoportal::API::Common::Response. Given: #{response.class}" unless response.is_a?(Common::Response)
18
+ return nil unless elastic_apm_service
19
+ return nil unless unexpected_server_error?(response.status)
20
+ if ElasticAPM.running?
21
+ ElasticAPM.report(UnexpectedServerError.new(response.status, response.body))
22
+ end
23
+ end
24
+
25
+ private
26
+
27
+ def unexpected_server_error?(code)
28
+ !code || ((code >= 500) && (code <= 599)) || (code <= 99)
29
+ end
30
+
31
+ # finalizer to stop the agent
32
+ close_elastic_apm = Proc.new do |id|
33
+ begin
34
+ if ElasticAPM.running?
35
+ puts "Stopping ElasticAPM service"
36
+ ElasticAPM.stop
37
+ end
38
+ rescue StandardError => e
39
+ # Silent
40
+ end
41
+ end
42
+ ObjectSpace.define_finalizer("ElasticAPM", close_elastic_apm)
43
+
44
+ def elastic_apm_service
45
+ return false if @disable_apm
46
+ begin
47
+ ElasticAPM.start(**elastic_apm_options) unless ElasticAPM.running?
48
+ rescue StandardError => e
49
+ @disable_apm = true
50
+ puts "ElasticAPM services not available: #{e}"
51
+ end
52
+ end
53
+
54
+ def elastic_apm_options
55
+ {
56
+ service_name: APM_SERVICE_NAME,
57
+ server_url: elastic_apm_url,
58
+ secret_token: elastic_apm_key,
59
+ environment: environment,
60
+ #http_compression: false,
61
+ transaction_sample_rate: 0.1,
62
+ transaction_max_spans: 100,
63
+ span_frames_min_duration: "5ms"
64
+ }.tap do |options|
65
+ options.merge!({
66
+ log_level: Logger::DEBUG,
67
+ log_path: File.join(__dir__, "elastic_apm.log")
68
+ }) if false
69
+ end
70
+ end
71
+
72
+ def elastic_apm_url
73
+ @elastic_apm_url ||= "https://".tap do |url|
74
+ url << "#{elastic_apm_account_id}"
75
+ url << ".#{elastic_apm_base_url}"
76
+ url << ":#{elastic_apm_port}"
77
+ end
78
+ end
79
+
80
+ def elastic_apm_key
81
+ @elastic_apm_key ||= ENV['ELASTIC_APM_KEY']
82
+ end
83
+
84
+ def elastic_apm_account_id
85
+ @elastic_apm_account_id ||= ENV['ELASTIC_APM_ACCOUNT_ID']
86
+ end
87
+
88
+ def elastic_apm_base_url
89
+ @elastic_apm_base_url ||= "apm.#{elastic_apm_region}.aws.cloud.es.io"
90
+ end
91
+
92
+ def elastic_apm_region
93
+ @elastic_apm_region ||= ENV['ELASTIC_APM_REGION'] || "ap-southeast-2"
94
+ end
95
+
96
+
97
+ def elastic_apm_port
98
+ @elastic_apm_port ||= ENV['ELASTIC_APM_PORT'] || "443"
99
+ end
100
+
101
+ def environment
102
+ @environment ||= "unknown".tap do |value|
103
+ if instance_variable_defined?(:@host) && env = @host.gsub(".ecoportal.com", '')
104
+ value.clear << env
105
+ end
106
+ end
107
+ end
108
+
109
+ end
110
+ end
111
+ end
112
+ end
@@ -5,15 +5,16 @@ module Ecoportal
5
5
  ID_KEYS = %w[id]
6
6
 
7
7
  class << self
8
- def diff(a, b)
9
- return a if a.class != b.class
8
+
9
+ def diff(a, b, ignore: [])
10
10
  case a
11
11
  when Hash
12
12
  {}.tap do |diffed|
13
13
  a.each do |key, a_value|
14
- b_value = b[key]
15
- next if a_value == b_value && !ID_KEYS.include?(key)
16
- diffed[key] = diff(a_value, b_value)
14
+ b_value = b && b[key]
15
+ no_changes = (a_value == b_value) || ignore.include?(key)
16
+ next if !ID_KEYS.include?(key) && no_changes
17
+ diffed[key] = diff(a_value, b_value, ignore: ignore)
17
18
  diffed.delete(key) if diffed[key] == {}
18
19
  end
19
20
  # All keys are IDs, so it's actually blank
@@ -22,10 +23,10 @@ module Ecoportal
22
23
  end
23
24
  end
24
25
  when Array
25
- return a unless a.length == b.length
26
+ return a unless b.is_a?(Array) && a.length == b.length
26
27
  a.map.with_index do |a_value, idx|
27
28
  b_value = b[idx]
28
- diff(a_value, b_value)
29
+ diff(a_value, b_value, ignore: ignore)
29
30
  end.reject do |el|
30
31
  el == {}
31
32
  end
@@ -10,6 +10,7 @@ require 'ecoportal/api/common/hash_diff'
10
10
  require 'ecoportal/api/common/base_model'
11
11
  require 'ecoportal/api/common/doc_helpers'
12
12
  require 'ecoportal/api/common/logging'
13
+ require 'ecoportal/api/common/elastic_apm_integration'
13
14
  require 'ecoportal/api/common/client'
14
15
  require 'ecoportal/api/common/response'
15
16
  require 'ecoportal/api/common/wrapped_response'
@@ -0,0 +1,8 @@
1
+ module Ecoportal
2
+ module API
3
+ module Errors
4
+ class Base < StandardError
5
+ end
6
+ end
7
+ end
8
+ end
@@ -0,0 +1,8 @@
1
+ module Ecoportal
2
+ module API
3
+ module Errors
4
+ class TimeOut < Errors::Base
5
+ end
6
+ end
7
+ end
8
+ end
@@ -0,0 +1,9 @@
1
+ module Ecoportal
2
+ module API
3
+ module Errors
4
+ end
5
+ end
6
+ end
7
+
8
+ require 'ecoportal/api/errors/base'
9
+ require 'ecoportal/api/errors/time_out'
@@ -2,29 +2,43 @@ module Ecoportal
2
2
  module API
3
3
  class Internal
4
4
  class Account < Common::BaseModel
5
- passthrough :policy_group_ids, :landing_page_id, :permissions_preset, :permissions_custom,
6
- :preferences, :prefilter, :login_provider_ids, :starred_ids, :accept_eula,
7
- :send_invites, :default_tag
5
+ PROPERTIES = [
6
+ "user_id", "policy_group_ids", "default_tag", "prefilter",
7
+ "permissions_custom", "permissions_merged", "preferences",
8
+ "login_provider_ids", "starred_ids", "landing_page_id",
9
+ "accept_eula", "send_invites", "force_send_invites"
10
+ ]
11
+ passthrough *PROPERTIES.map(&:to_sym)
8
12
 
9
13
  class_resolver :preferences_class, "Ecoportal::API::Internal::Preferences"
10
14
  class_resolver :permissions_class, "Ecoportal::API::Internal::Permissions"
11
15
 
12
16
  embeds_one :permissions, key: "permissions_custom", klass: :permissions_class
17
+ embeds_one :perms_merged, key: "permissions_merged", klass: :permissions_class
13
18
  embeds_one :preferences, klass: :preferences_class
14
19
 
20
+ # Sets the `default_tag` of the user
21
+ # @note it upcases the value
22
+ # @param value [String, nil] the tag
23
+ # @return [String, nil] the value set in `default_tag`
24
+ def default_tag=(value)
25
+ unless !value || value.is_a?(String)
26
+ raise ArgumentError.new("default_tag= needs to be passed a String or nil, got #{value.class}")
27
+ end
28
+ if value
29
+ unless value.match(Ecoportal::API::V1::Person::VALID_TAG_REGEX)
30
+ raise ArgumentError.new("Invalid default tag #{value.inspect}")
31
+ end
32
+ value = value.upcase
33
+ end
34
+ doc["default_tag"] = value
35
+ end
36
+
15
37
  # Sets the `policy_group_ids`
16
38
  # @note it preserves the original order
17
39
  # @param value [Array<String>] the policy group ids to be set.
18
40
  def policy_group_ids=(value)
19
- unless value.is_a?(Array)
20
- raise "policy_group_ids= needs to be passed an Array, got #{value.class}"
21
- end
22
-
23
- value.uniq!
24
- ini_ids = (original_doc && original_doc["policy_group_ids"]) || []
25
- # preserve original order to avoid false updates
26
- doc["policy_group_ids"] = (ini_ids & value) + (value - ini_ids)
27
- doc["policy_group_ids"].compact
41
+ set_uniq_array_keep_order("policy_group_ids", value)
28
42
  end
29
43
 
30
44
  # @return [Array<String>] the policy group ids of this user.
@@ -34,10 +48,7 @@ module Ecoportal
34
48
 
35
49
  # Sets the `login_provider_ids`
36
50
  def login_provider_ids=(value)
37
- unless value.is_a?(Array)
38
- raise "login_provider_ids= needs to be passed an Array, got #{value.class}"
39
- end
40
- doc["login_provider_ids"] = value.compact
51
+ set_uniq_array_keep_order("login_provider_ids", value)
41
52
  end
42
53
 
43
54
  # @return [Array<String>] the login provider ids of this user.
@@ -47,10 +58,7 @@ module Ecoportal
47
58
 
48
59
  # Sets the `starred_ids`
49
60
  def starred_ids=(value)
50
- unless value.is_a?(Array)
51
- raise "starred_ids= needs to be passed an Array, got #{value.class}"
52
- end
53
- doc["starred_ids"] = value.compact
61
+ set_uniq_array_keep_order("starred_ids", value)
54
62
  end
55
63
 
56
64
  # @return [Array<String>] the starred page ids of this user.
@@ -58,20 +66,6 @@ module Ecoportal
58
66
  doc["starred_ids"] ||= []
59
67
  end
60
68
 
61
- # Sets the `permissions_preset`.
62
- # @note basically the same as `permissions_preset=` but when `"custom"`, it's changed to `nil`
63
- # @param value [nil, String] preset name.
64
- def preset=(value)
65
- self.permissions_preset = value == "custom" ? nil : value
66
- end
67
-
68
- # Gets the `permissions_preset`.
69
- # @note basically the same as `permissions_preset` but when 'nil', it returns `"custom"` instead
70
- # @return [nil, String] preset name.
71
- def preset
72
- self.permissions_preset.nil? ? "custom" : self.permissions_preset
73
- end
74
-
75
69
  # It preserves the values of keys that are not defined in `value`.
76
70
  # @param value [Hash] the abilities that you want to update.
77
71
  def permissions_custom=(value)
@@ -88,15 +82,16 @@ module Ecoportal
88
82
 
89
83
  def as_json
90
84
  super.tap do |hash|
91
- if preset == "custom"
92
- hash["permissions_custom"] = permissions.as_json
93
- else
94
- hash.delete "permissions_custom"
95
- end
85
+ hash["permissions_custom"] = permissions.as_json
86
+ hash["permissions_merged"] = perms_merged.as_json
96
87
  hash["preferences"] = preferences.as_json
97
88
  end
98
89
  end
99
90
 
91
+ def as_update(ref = :last, ignore: [])
92
+ super(ref, ignore: ignore | ["user_id", "permissions_merged", "prefilter"])
93
+ end
94
+
100
95
  end
101
96
  end
102
97
  end
@@ -3,9 +3,10 @@ module Ecoportal
3
3
  class Internal
4
4
  class Permissions < Common::BaseModel
5
5
  passthrough :files, :data, :reports
6
- passthrough :organization, :person_core, :person_core_create, :person_core_edit
7
- passthrough :person_account, :person_details
8
- passthrough :pages, :page_editor, :registers, :tasks
6
+ passthrough :organization, :pages, :page_editor, :registers, :tasks
7
+ passthrough :person_core, :person_core_create, :person_core_edit
8
+ passthrough :person_details, :person_account, :person_abilities
9
+ passthrough :visitor_management, :broadcast_notifications, :cross_register_reporting
9
10
  end
10
11
  end
11
12
  end
@@ -11,10 +11,14 @@ module Ecoportal
11
11
  super.update("account" => account&.as_json)
12
12
  end
13
13
 
14
+ def as_update(ref = :last, ignore: [])
15
+ super(ref, ignore: ignore | ["user_id", "permissions_merged", "prefilter"])
16
+ end
17
+
14
18
  # Sets the Account to the person, depending on the paramter received:
15
19
  # - `nil`: blanks the account.
16
20
  # - `Account`: sets a copy of the object param as account.
17
- # - `Hash`: slices the properties of `Account`.
21
+ # - `Hash`: slices the properties of `Account` (keeping the value of `user_id` if there was already account).
18
22
  # @note this method does not make dirty the account (meaning that `as_json` will be an empty hash `{}`)
19
23
  # @param value [nil, Account, Hash] value to be set.
20
24
  # @return [nil, Account] the resulting `Account` set to the person.
@@ -25,7 +29,9 @@ module Ecoportal
25
29
  when Internal::Account
26
30
  doc["account"] = JSON.parse(value.to_json)
27
31
  when Hash
28
- doc["account"] = value.slice(*%w[policy_group_ids default_tag landing_page_id permissions_preset permissions_custom preferences prefilter login_provider_ids starred_ids])
32
+ user_id = account.user_id if account
33
+ doc["account"] = value.slice(*Internal::Account::PROPERTIES)
34
+ doc["account"]["user_id"] = user_id if user_id
29
35
  else
30
36
  # TODO
31
37
  raise "Invalid set on account: Need nil, Account or Hash; got #{value.class}"
@@ -36,8 +36,15 @@ module Ecoportal
36
36
  puts "\n" unless silent
37
37
  loop do
38
38
  params.update(cursor_id: cursor_id) if cursor_id
39
- response = client.get("/people", params: params)
40
- body = body_data(response.body)
39
+ body = nil; response = nil; count = 5
40
+ loop do
41
+ response = client.get("/people", params: params)
42
+ body = response && body_data(response.body)
43
+ break if response.success? || count <= 0
44
+ puts "Request failed - Status #{response.status}: #{body}"
45
+ count -= 1
46
+ sleep(0.5)
47
+ end
41
48
  raise "Request failed - Status #{response.status}: #{body}" unless response.success?
42
49
 
43
50
  unless silent || (total = body["total_results"]) == 0
@@ -77,7 +84,7 @@ module Ecoportal
77
84
  response = client.get("/people/"+CGI.escape(id))
78
85
  body = body_data(response.body)
79
86
  return person_class.new(body) if response.success?
80
- raise "Could not get person #{id} - Error #{reponse.status}: #{body}"
87
+ raise "Could not get person #{id} - Error #{response.status}: #{body}"
81
88
  end
82
89
 
83
90
  # Requests an update of a person via api.
@@ -117,29 +124,30 @@ module Ecoportal
117
124
  # Creates a `BatchOperation` and yields it to the given bock.
118
125
  # @yield [batch_op] adds multiple api requests for the current batch.
119
126
  # @yieldparam batch_op [BatchOperation]
127
+ # @param job_mode [Boolean] whether or not it should use batch jobs
128
+ # @return [Ecoportal::API::Common::Response] the results of the batch
120
129
  def batch(job_mode: true, &block)
121
130
  return job(&block) if job_mode
122
131
  operation = Common::BatchOperation.new("/people", person_class, logger: client.logger)
123
132
  yield operation
124
133
  # The batch operation is responsible for logging the output
125
- client.without_response_logging do
126
- client.post("/people/batch", data: operation.as_json).tap do |response|
127
- operation.process_response(response)
128
- end
134
+ client.post("/people/batch", data: operation.as_json).tap do |response|
135
+ operation.process_response(response)
129
136
  end
130
137
  end
131
138
 
139
+ # @return [Ecoportal::API::Common::Response] the results of the batch job
132
140
  def job
133
141
  operation = Common::BatchOperation.new("/people", person_class, logger: client.logger)
134
142
  yield operation
135
- # The batch operation is responsible for logging the output
136
143
  job_id = create_job(operation)
137
144
  status = wait_for_job_completion(job_id)
138
145
 
139
146
  if status&.complete?
140
- operation.process_response job_result(job_id, operation)
147
+ job_result(job_id, operation)
141
148
  else
142
- raise "Job `#{job_id}` not complete. Probably timeout after #{JOB_TIMEOUT} seconds. Current status: #{status}"
149
+ msg = "Job `#{job_id}` not complete. Probably timeout after #{JOB_TIMEOUT} seconds. Current status: #{status}"
150
+ raise API::Errors::TimeOut.new msg
143
151
  end
144
152
  end
145
153
 
@@ -154,8 +162,8 @@ module Ecoportal
154
162
  JobStatus = Struct.new(:id, :complete?, :errored?, :progress)
155
163
  def job_status(job_id)
156
164
  response = client.get("/people/job/#{CGI.escape(job_id)}/status")
157
- body = body_data(response.body)
158
- raise "Status error" unless response.success?
165
+ body = response && body_data(response.body)
166
+ raise "Status error (#{response.status}) - Errors: #{body}" unless response.success?
159
167
  JobStatus.new(
160
168
  body["id"],
161
169
  body["complete"],
@@ -164,12 +172,10 @@ module Ecoportal
164
172
  )
165
173
  end
166
174
 
175
+ # @return [Ecoportal::API::Common::Response] the results of the batch job
167
176
  def job_result(job_id, operation)
168
- # The batch operation is responsible for logging the output
169
- client.without_response_logging do
170
- client.get("/people/job/#{CGI.escape(job_id)}").tap do |response|
171
- operation.process_response(response)
172
- end
177
+ client.get("/people/job/#{CGI.escape(job_id)}").tap do |response|
178
+ operation.process_response(response)
173
179
  end
174
180
  end
175
181
 
@@ -186,12 +192,12 @@ module Ecoportal
186
192
  end
187
193
  end
188
194
 
195
+ # @return [String] the `id` of the created batch job
189
196
  def create_job(operation)
190
197
  job_id = nil
191
- client.without_response_logging do
192
- client.post("/people/job", data: operation.as_json).tap do |response|
193
- job_id = body_data(response.body)["id"]
194
- end
198
+ client.post("/people/job", data: operation.as_json).tap do |response|
199
+ job_id = body_data(response.body)["id"] if response.success?
200
+ raise "Could not create job - Error (#{response.status}): #{body_data(response.body)}" unless job_id
195
201
  end
196
202
  job_id
197
203
  end
@@ -64,12 +64,8 @@ module Ecoportal
64
64
  raise "Invalid filter tag #{tag.inspect}"
65
65
  end
66
66
  tag.upcase
67
- end.uniq
68
-
69
- ini_tags = (original_doc && original_doc["filter_tags"]) || []
70
- # preserve original order to avoid false updates
71
- doc["filter_tags"] = (ini_tags & end_tags) + (end_tags - ini_tags)
72
- doc["filter_tags"].compact
67
+ end
68
+ set_uniq_array_keep_order("filter_tags", end_tags)
73
69
  end
74
70
 
75
71
  # @return [Array<String>] the filter tags of this person.
@@ -81,6 +77,10 @@ module Ecoportal
81
77
  super.merge "details" => details&.as_json
82
78
  end
83
79
 
80
+ def as_update(ref = :last, ignore: [])
81
+ super(ignore: ignore | ["subordinates"])
82
+ end
83
+
84
84
  # Sets the PersonDetails to the person, depending on the paramter received:
85
85
  # - `nil`: blanks the details.
86
86
  # - `PersonDetails`: sets a copy of the object param as details.
@@ -2,6 +2,9 @@ module Ecoportal
2
2
  module API
3
3
  class V1
4
4
  class PersonDetails < Common::BaseModel
5
+ class MissingId < StandardError
6
+ end
7
+
5
8
  passthrough :schema_id
6
9
 
7
10
  class_resolver :schema_field_value_class, "Ecoportal::API::V1::SchemaFieldValue"
@@ -43,16 +46,24 @@ module Ecoportal
43
46
  end
44
47
 
45
48
  # Sets the value to one specific field of the PersonDetails.
49
+ # @raise MisssingId if the `id` or `alt_id` is missing.
46
50
  # @param id [String] the `id` or the `alt_id` of the target field.
47
51
  # @return [void]
48
52
  def []=(id, value)
49
53
  if field = get_field(id)
50
54
  field.value = value
51
55
  else
52
- raise "details[#{id.inspect}] is missing. Did you forget to load the schema?"
56
+ raise MissingId.new("details[#{id.inspect}] is missing. Did you forget to load the schema?")
53
57
  end
54
58
  end
55
59
 
60
+ # Checks if an `id` or `alt_id` exists
61
+ # @param id [String] the `id` or the `alt_id` of the target field.
62
+ # @return [Boolean] `true` if it exists, `false` otherwise
63
+ def key?(id)
64
+ @fields_by_id.key?(id) || @fields_by_alt_id.key?(id)
65
+ end
66
+
56
67
  protected
57
68
 
58
69
  # Rebuilds the internal `id` and `alt_id` references to the fields.
@@ -6,25 +6,24 @@ module Ecoportal
6
6
  passthrough :id, :alt_id, :type, :name, :shared, :multiple
7
7
 
8
8
  def value
9
- return @value if defined?(@value)
10
- @value = case type
11
- when "text", "phone_number", "number", "boolean", "select"
12
- doc["value"]
13
- when "date"
14
- if doc["value"]
15
- maybe_multiple(doc["value"]) do |v|
16
- Date.iso8601(v)
17
- end
18
- end
19
- else
20
- raise "Unknown type #{type}"
21
- end
9
+ case type
10
+ when "text", "phone_number", "number", "boolean", "select"
11
+ doc["value"]
12
+ when "date"
13
+ if doc["value"]
14
+ maybe_multiple(doc["value"]) do |v|
15
+ Date.iso8601(v)
16
+ end
17
+ end
18
+ else
19
+ raise "Unknown type #{type}"
20
+ end
22
21
  end
23
22
 
24
23
  def value=(value)
25
24
  case type
26
25
  when "text", "phone_number", "select"
27
- doc["value"] = @value = maybe_multiple(value) do |v|
26
+ doc["value"] = maybe_multiple(value) do |v|
28
27
  v&.to_s
29
28
  end
30
29
  when "number"
@@ -33,23 +32,21 @@ module Ecoportal
33
32
  raise "Invalid number type #{v.class}"
34
33
  end
35
34
  end
36
- doc["value"] = @value = value
35
+ doc["value"] = value
37
36
  when "boolean"
38
- doc["value"] = @value = !!value
37
+ doc["value"] = !!value
39
38
  when "date"
40
39
  maybe_multiple(value) do |v|
41
40
  unless v.nil? || v.respond_to?(:to_date)
42
41
  raise "Invalid date type #{v.class}"
43
42
  end
44
43
  end
45
- @value = value
46
- doc["value"] = maybe_multiple(@value) do |v|
44
+ doc["value"] = maybe_multiple(value) do |v|
47
45
  v&.to_date&.to_s
48
46
  end
49
47
  else
50
48
  raise "Unknown type #{type}"
51
49
  end
52
- @value
53
50
  end
54
51
 
55
52
  def maybe_multiple(value)
@@ -1,5 +1,5 @@
1
1
  module Ecoportal
2
2
  module API
3
- VERSION = "0.8.2"
3
+ VERSION = "0.8.5"
4
4
  end
5
5
  end
data/lib/ecoportal/api.rb CHANGED
@@ -2,6 +2,7 @@ require "cgi"
2
2
  require "logger"
3
3
  require "hash-polyfill"
4
4
  require "ecoportal/api/version"
5
+ require "dotenv/load"
5
6
 
6
7
  module Ecoportal
7
8
  module API
@@ -10,5 +11,6 @@ end
10
11
 
11
12
  require "ecoportal/api/logger"
12
13
  require "ecoportal/api/common"
14
+ require "ecoportal/api/errors"
13
15
  require "ecoportal/api/v1"
14
16
  require "ecoportal/api/internal"
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: ecoportal-api
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.8.2
4
+ version: 0.8.5
5
5
  platform: ruby
6
6
  authors:
7
7
  - Tapio Saarinen
8
8
  autorequire:
9
9
  bindir: exe
10
10
  cert_chain: []
11
- date: 2021-02-23 00:00:00.000000000 Z
11
+ date: 2022-03-31 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: bundler
@@ -16,7 +16,7 @@ dependencies:
16
16
  requirements:
17
17
  - - ">="
18
18
  - !ruby/object:Gem::Version
19
- version: 2.2.11
19
+ version: 2.2.17
20
20
  - - "<"
21
21
  - !ruby/object:Gem::Version
22
22
  version: '2.3'
@@ -26,7 +26,7 @@ dependencies:
26
26
  requirements:
27
27
  - - ">="
28
28
  - !ruby/object:Gem::Version
29
- version: 2.2.11
29
+ version: 2.2.17
30
30
  - - "<"
31
31
  - !ruby/object:Gem::Version
32
32
  version: '2.3'
@@ -144,6 +144,46 @@ dependencies:
144
144
  - - "<"
145
145
  - !ruby/object:Gem::Version
146
146
  version: '5'
147
+ - !ruby/object:Gem::Dependency
148
+ name: dotenv
149
+ requirement: !ruby/object:Gem::Requirement
150
+ requirements:
151
+ - - ">="
152
+ - !ruby/object:Gem::Version
153
+ version: 2.7.6
154
+ - - "<"
155
+ - !ruby/object:Gem::Version
156
+ version: '2.8'
157
+ type: :runtime
158
+ prerelease: false
159
+ version_requirements: !ruby/object:Gem::Requirement
160
+ requirements:
161
+ - - ">="
162
+ - !ruby/object:Gem::Version
163
+ version: 2.7.6
164
+ - - "<"
165
+ - !ruby/object:Gem::Version
166
+ version: '2.8'
167
+ - !ruby/object:Gem::Dependency
168
+ name: elastic-apm
169
+ requirement: !ruby/object:Gem::Requirement
170
+ requirements:
171
+ - - ">="
172
+ - !ruby/object:Gem::Version
173
+ version: 4.0.0
174
+ - - "<"
175
+ - !ruby/object:Gem::Version
176
+ version: '4.1'
177
+ type: :runtime
178
+ prerelease: false
179
+ version_requirements: !ruby/object:Gem::Requirement
180
+ requirements:
181
+ - - ">="
182
+ - !ruby/object:Gem::Version
183
+ version: 4.0.0
184
+ - - "<"
185
+ - !ruby/object:Gem::Version
186
+ version: '4.1'
147
187
  - !ruby/object:Gem::Dependency
148
188
  name: hash-polyfill
149
189
  requirement: !ruby/object:Gem::Requirement
@@ -189,10 +229,14 @@ files:
189
229
  - lib/ecoportal/api/common/batch_response.rb
190
230
  - lib/ecoportal/api/common/client.rb
191
231
  - lib/ecoportal/api/common/doc_helpers.rb
232
+ - lib/ecoportal/api/common/elastic_apm_integration.rb
192
233
  - lib/ecoportal/api/common/hash_diff.rb
193
234
  - lib/ecoportal/api/common/logging.rb
194
235
  - lib/ecoportal/api/common/response.rb
195
236
  - lib/ecoportal/api/common/wrapped_response.rb
237
+ - lib/ecoportal/api/errors.rb
238
+ - lib/ecoportal/api/errors/base.rb
239
+ - lib/ecoportal/api/errors/time_out.rb
196
240
  - lib/ecoportal/api/internal.rb
197
241
  - lib/ecoportal/api/internal/account.rb
198
242
  - lib/ecoportal/api/internal/login_provider.rb
@@ -237,7 +281,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
237
281
  - !ruby/object:Gem::Version
238
282
  version: '0'
239
283
  requirements: []
240
- rubygems_version: 3.0.3
284
+ rubygems_version: 3.3.5
241
285
  signing_key:
242
286
  specification_version: 4
243
287
  summary: A collection of helpers for interacting with the ecoPortal MS's various APIs