ecoportal-api 0.8.2 → 0.8.5

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: 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