ecoportal-api-graphql 0.4.7 → 1.1.1

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: b968296728e5236f8a3aae38661d2caa27467e40c20e5b3d81daf436f02d48dd
4
- data.tar.gz: bbd52cdb4e42afce5e2e7c7712c57d8829d0837ab3f7adc5ef3d5259c6640d8b
3
+ metadata.gz: d43b5c70e8ce94e96f2028bcc80b6bdf45ac8f78afa7a1689b2149e2175d39db
4
+ data.tar.gz: c7e932aea0d5b4976677cecb565a64c3523b2cd8af2fac49e7ecf35f8265d636
5
5
  SHA512:
6
- metadata.gz: f1449e74769d6006fd330316f8741525a5c972f9a23654ab4ad08faf9f59e34ad14075e5bf30b1123da20a0cdef8023b19a4e286255d15923118ee92571af11e
7
- data.tar.gz: 01605be05f707c622ce48f03c722b9c638287b32d57c64e98241115c7ed39d57041809f6445984410d39cb1fce555edf760f56020d6c3fd3549bf9a8ceeaebb6
6
+ metadata.gz: dbd441319d44294084754bcc03f8660b6361930617ae23bf66faeae3d2f204d2c28d0523b63fe0985629cd196c7445ff0eaa01c18740baf02522c7c37f982f65
7
+ data.tar.gz: 9cae64f7110b5df6e3995ff36af69220b3e19cdd1cb7f8e1914128fdffadea5a916e429b2e28e009be618a709d5765b51419d441f1fd576135ae74f19a1d6292
data/.gitignore CHANGED
@@ -19,4 +19,6 @@ Gemfile.lock
19
19
  # rspec failure tracking
20
20
  .rspec_status
21
21
  scratch.rb
22
- tests/.byebug_history
22
+ tests/.byebug_history
23
+
24
+ .solargraph.yml
data/.rubocop.yml CHANGED
@@ -4,13 +4,15 @@ AllCops:
4
4
  - 'config/routes.rb'
5
5
  NewCops: enable
6
6
 
7
+ Metrics/ClassLength:
8
+ Max: 350
9
+ Metrics/ModuleLength:
10
+ Max: 400
11
+ Metrics/MethodLength:
12
+ Max: 100
7
13
  Metrics/BlockLength:
8
14
  CountAsOne: ['array', 'heredoc', 'method_call']
9
15
  Max: 50
10
- Metrics/MethodLength:
11
- Max: 50
12
- Metrics/ClassLength:
13
- Max: 200
14
16
  Metrics/AbcSize:
15
17
  Max: 30
16
18
  Metrics/ParameterLists:
data/CHANGELOG.md CHANGED
@@ -9,19 +9,44 @@ All notable changes to this project will be documented in this file.
9
9
  - Analyse how to "DSL" currentOrganization.action.activities
10
10
  - review `path` tracking
11
11
 
12
- ## [0.4.8] - 2025-04-xx
12
+ ## [1.1.2] - 2025-05-xx
13
13
 
14
14
  ### Added
15
15
 
16
- - `page.id` to Action linked resources.
16
+ ### Changed
17
17
 
18
- ## [0.4.7] - 2025-04-02
18
+ ### Fixed
19
+
20
+ ## [1.1.1] - 2025-05-14
19
21
 
20
22
  ### Added
21
23
 
24
+ - `MemberChanges` helpers for `Base::ContractorEntity`
25
+ - Custom **Diff** logic that can be used in GraphQL content:
26
+ - `Common::GraphQL::Model::Diffable::HashDiffNesting`
27
+ - Reused from `HashDiffPatch` (`ecoportal-api-v2`):
28
+ 1. It does not use `patch_ver`, but rather just `id` property alone.
29
+ 2. The _operation_ property has been renamed to `api_operation` (to ensure no collisions). And the operation types are symbols, and renamed to: `:delete`, `:update` and `:create`. This can show up to be useful when, for instance, reusing the output of `#as_update` to generate actual input objects (i.e. `#as_input`) that feed some GraphQL query.
30
+ 3. The _data_ property has been renamed to `change_data`.
31
+ 4. `Hash` **keys** are of type `Symbol`.
32
+ - `Common::GraphQL::Model::Diffable::DiffService`. In a GraphQL context, this aims to be able to implement a function called `#as_input` (equivalent to `#as_update`):
33
+ 1. It uses the `HashDiffNesting` helpers.
34
+ 2. It has capability to `ignore` certain keys.
35
+ 3. It can calculate **diff** excluding nested models (i.e. those that are `root?` and, therefore, not part of the changed target model). This functionality is an optimization to `cascaded_callbacks` throughout the nested models.
36
+
22
37
  ### Changed
23
38
 
24
- ### Fixed
39
+ - `Common::GraphQL::Model::Diffable` by using `Diffable::DiffService` (with `cascaded_callbacks` implementation).
40
+ - `#as_update`
41
+ - `#dirty?`
42
+ - upgraded `ecoportal-api` gem
43
+ - updraded `ecoportal-api-v2` gem
44
+
45
+ ## [0.4.7] - 2025-04-02
46
+
47
+ ### Added
48
+
49
+ - `page.id` to Action linked resources.
25
50
 
26
51
  ## [0.4.6] - 2025-04-02
27
52
 
@@ -33,7 +33,7 @@ Gem::Specification.new do |spec|
33
33
  spec.add_development_dependency 'yard', '>= 0.9.34', '< 1'
34
34
 
35
35
  spec.add_dependency 'ecoportal-api', '~> 0.10', '>= 0.10.9'
36
- spec.add_dependency 'ecoportal-api-v2', '~> 2.0', '>= 2.0.16'
36
+ spec.add_dependency 'ecoportal-api-v2', '~> 3.1', '>= 3.1.1'
37
37
  spec.add_dependency 'graphlient', '>= 0.8.0', '< 0.9'
38
38
  end
39
39
 
@@ -3,6 +3,12 @@ module Ecoportal
3
3
  module Common
4
4
  module GraphQL
5
5
  module HashHelpers
6
+ class << self
7
+ def included(base)
8
+ base.send(:include, InstanceMethods)
9
+ end
10
+ end
11
+
6
12
  module InstanceMethods
7
13
  def keys_to_sym_deep(value)
8
14
  transform_keys_deep(value, &:to_sym)
@@ -91,17 +97,7 @@ module Ecoportal
91
97
  end
92
98
  end
93
99
 
94
- module ClassMethods
95
- end
96
-
97
- class << self
98
- include InstanceMethods
99
-
100
- def included(base)
101
- base.send(:include, InstanceMethods)
102
- base.extend(ClassMethods)
103
- end
104
- end
100
+ extend InstanceMethods
105
101
  end
106
102
  end
107
103
  end
@@ -3,10 +3,21 @@ module Ecoportal
3
3
  module Common
4
4
  module GraphQL
5
5
  class Model
6
+ # @todo
7
+ # 1. `#as_input` vs `#to_input` **or** named argument `klass`
8
+ # One gets the `input` to be already used in a mutation query.
9
+ # The other gets the `Input` class object out of the `Base/Model`.
10
+ # - To figure out when `as_update` should be called.
11
+ # 2. Each Base/Model should have its `Input` class counterpart.
12
+ # This could be auto-inflected (via namespace constant lookup)
13
+ # at some point.
14
+ # 3. Think if a service class can be used to this purpose
15
+ # (i.e. decorator of DiffService).
6
16
  module AsInput
7
17
  class << self
8
18
  def included(base)
9
19
  super
20
+
10
21
  base.send(:include, Model::Diffable)
11
22
  base.extend ClassMethods
12
23
  end
@@ -0,0 +1,47 @@
1
+ module Ecoportal::API::Common::GraphQL::Model::Diffable
2
+ class ClassicDiffService
3
+ include Ecoportal::API::Common::GraphQL::Model::Diffable::HashDiffNesting
4
+
5
+ attr_reader :subject
6
+
7
+ def initialize(subject, ignore: [])
8
+ msg = 'Expecting Ecoportal::API::Common::Content::DoubleModel. '
9
+ msg << "Given: #{subject.class}"
10
+ raise ArgumentError, msg unless subject.is_a?(Ecoportal::API::Common::Content::DoubleModel)
11
+
12
+ @subject = subject
13
+ @ignored = to_a(ignore)
14
+ end
15
+
16
+ def diff(ignore: [])
17
+ classic_diff(ignore: ignore)
18
+ end
19
+
20
+ protected
21
+
22
+ def classic_diff(ignore: [])
23
+ change_diff(
24
+ curr_doc,
25
+ prev_doc,
26
+ ignore: to_a(ignored, ignore)
27
+ )
28
+ end
29
+
30
+ private
31
+
32
+ attr_reader :ignored
33
+
34
+ def curr_doc(target = subject)
35
+ return target.as_json if target.respond_to?(:as_json)
36
+ return target.doc if target.respond_to?(:doc)
37
+
38
+ raise "Can't fetch underlying document for instance of #{target.class}."
39
+ end
40
+
41
+ def prev_doc(target = subject, flat: flat?)
42
+ return target.original_doc if target.respond_to?(:original_doc)
43
+
44
+ raise "Can't fetch original underlying model for instance of #{target.class}."
45
+ end
46
+ end
47
+ end
@@ -0,0 +1,180 @@
1
+ module Ecoportal::API::Common::GraphQL::Model::Diffable
2
+ class DiffService < ClassicDiffService
3
+ include Ecoportal::API::Common::GraphQL::Model::Diffable::HashDiffNesting
4
+ include Ecoportal::API::Common::GraphQL::HashHelpers
5
+
6
+ def initialize(subject, flat: false, ignore: [])
7
+ super(subject, ignore: ignore)
8
+
9
+ @flat = flat
10
+ end
11
+
12
+ def flat?
13
+ @flat
14
+ end
15
+
16
+ # @param flat [Boolean] whethere it should skip nested models.
17
+ # @param cascaded [Boolean] whether it should cascade the calls through
18
+ # the nested models.
19
+ # - When `flat` is `true` will only cascade the 1st level of nested models.
20
+ # - When `flat` is `false` will do a full cascade.
21
+ def diff(flat: flat?, ignore: [])
22
+ classic_diff(
23
+ flat: true,
24
+ ignore: ignore
25
+ ).then do |value|
26
+ next value if flat
27
+
28
+ diff_reduce(
29
+ value,
30
+ ignore: ignore
31
+ )
32
+ end
33
+ end
34
+
35
+ protected
36
+
37
+ def classic_diff(flat: flat?, ignore: [])
38
+ change_diff(
39
+ curr_doc(flat: flat),
40
+ prev_doc(flat: flat),
41
+ ignore: to_a(ignored, ignore)
42
+ )
43
+ end
44
+
45
+ # @note it calls `as_update` for nested / cascaded attributes, which in turn
46
+ # will use this `DiffService`. Therefore, we aim for a first flat classic diff,
47
+ # onto `self`, and cascaded callbacks for the nested attributes.
48
+ # - This will translate into `key_path` being lost on each jump between
49
+ # `#as_update` and `DiffService#diff`.
50
+ # - The `result` object regards, therefore, to the **current level** alone.
51
+ # With the aim of building it up (accumulating through the call chain).
52
+ def diff_reduce(init = {}, ignore: [])
53
+ subject.cascaded_reduce(init, recurs: false) do |result, obj, _key, key_path, _trace|
54
+ next result if key_path.empty? # first iteration already done
55
+ next result if obj == subject # first call had done it (same as above: path relative)
56
+
57
+ dig_delete!(result, key_path) if dig_path?(result, key_path)
58
+
59
+ # discard rooted (i.e. read_only) objects from the diff.
60
+ next result if root?(obj)
61
+ next result unless dig_path?(result, key_path)
62
+ next result unless obj.respond_to?(:as_update)
63
+
64
+ value = obj.as_update(ignore: ignore)
65
+
66
+ dig_set!(
67
+ result,
68
+ key_path,
69
+ value
70
+ )
71
+
72
+ result
73
+ end
74
+ end
75
+
76
+ # PROBLEM: trace can't be used with knowing if it's the root of the call!
77
+ # see `#cascaded_reduce` (when calling `_cascaded_attributes_trace`).
78
+ def diff_reduce_with_as_update_and_trace(init = {}, ignore: [], trace: {})
79
+ subject.cascaded_reduce(init, recurs: false, trace: trace) do |result, obj, _key, key_path, _trace|
80
+ next result if key_path.empty? # first iteration already done
81
+ next result if obj == subject # first call had done it
82
+
83
+ dig_delete!(result, key_path) if dig_path?(result, key_path)
84
+
85
+ # discard rooted (i.e. read_only) objects from the diff.
86
+ next result if root?(obj)
87
+ next result unless dig_path?(result, key_path)
88
+ next result unless obj.respond_to?(:as_update)
89
+
90
+ value = obj.as_update(ignore: ignore)
91
+
92
+ dig_set!(
93
+ result,
94
+ key_path,
95
+ value
96
+ )
97
+
98
+ result
99
+ end
100
+ end
101
+
102
+ # FAILS to CALL #as_update on each object!!!
103
+ def diff_reduce_preserve_keys(init, flat: flat?, ignore: [])
104
+ subject.cascaded_reduce(init, recurs: !flat) do |result, obj, _key, key_path, _trace|
105
+ # discard rooted (i.e. read_only) objects from the diff.
106
+ if root?(obj)
107
+ dig_delete!(result, key_path)
108
+ else
109
+ value = classic_diff(
110
+ obj,
111
+ flat: true,
112
+ ignore: ignore
113
+ )
114
+
115
+ dig_set!(result, key_path, value)
116
+ end
117
+
118
+ result
119
+ end
120
+ end
121
+
122
+ def cascaded_diff(flat: flat?, ignore: [])
123
+ subject.cascaded_callback(
124
+ method: :as_update,
125
+ kargs: {
126
+ ignore: to_a(ignored, ignore),
127
+ flat: flat
128
+ },
129
+ recurs: !flat
130
+ ) do |result, value, key_path, obj|
131
+ result.tap do
132
+ # on first call key_path is empty
133
+ next result if key_path.empty? # this condition isn't met, first call doesn't yield.
134
+
135
+ # discard rooted (i.e. read_only) objects from the diff.
136
+ if root?(obj)
137
+ dig_delete!(result, key_path)
138
+ else
139
+ dig_set!(result, key_path, value)
140
+ end
141
+ end
142
+ end
143
+ end
144
+
145
+ private
146
+
147
+ attr_reader :ignored
148
+
149
+ def root?(obj)
150
+ return false unless obj.respond_to?(:root?)
151
+
152
+ obj.root?
153
+ end
154
+
155
+ def curr_doc(target = subject, flat: flat?)
156
+ may_use_flat_diff(flat) { super(target) }
157
+ end
158
+
159
+ def prev_doc(target = subject, flat: flat?)
160
+ may_use_flat_diff(flat) { super(target) }
161
+ end
162
+
163
+ # Remove nested attributes
164
+ def may_use_flat_diff(flat = flat?)
165
+ yield.then do |json|
166
+ next json unless flat
167
+
168
+ doc_with_non_cascaded_attributes(json)
169
+ end
170
+ end
171
+
172
+ # Remove from `json` doc all the nested properties.
173
+ def doc_with_non_cascaded_attributes(json)
174
+ return json unless subject.respond_to?(:_cascaded_doc_keys)
175
+ return json unless (excluded = subject._cascaded_doc_keys)
176
+
177
+ except_keys(json, *excluded)
178
+ end
179
+ end
180
+ end
@@ -1,56 +1,51 @@
1
1
  # rubocop:disable Naming/MethodParameterName
2
+ module Ecoportal::API::Common::GraphQL::Model::Diffable
3
+ # @note this is a literal copy of ecoportal-api gem diff function.
4
+ # ecoportal-api-graphql uses APIv2 DoubleModel inheritance chain,
5
+ # and should rather adapt `HashDiffPatch`
6
+ module HashDiff
7
+ ID_KEYS = %w[id].freeze
2
8
 
3
- module Ecoportal
4
- module API
5
- module Common
6
- module GraphQL
7
- class Model
8
- module Diffable
9
- module HashDiff
10
- class << self
11
- def included(base)
12
- super
13
- base.extend ClassMethods
14
- end
15
- end
9
+ class << self
10
+ def included(base)
11
+ super
16
12
 
17
- module ClassMethods
18
- # @todo: refactor to only reach the current level
19
- # @note the aim is to delegate `hash_diff` to the specific classes
20
- # of the model... bulding the payload/input_base in a cascaded
21
- # way (rather than in a one-off way from the top alone).
22
- def hash_diff(a, b, ignore: [])
23
- case a
24
- when Hash
25
- {}.tap do |diffed|
26
- a.each do |key, a_value|
27
- b_value = b && b[key]
28
- no_changes = (a_value == b_value) || ignore.include?(key)
29
- next if !ID_KEYS.include?(key) && no_changes
30
-
31
- diffed[key] = diff(a_value, b_value, ignore: ignore)
32
- diffed.delete(key) if diffed[key] == {}
33
- end
13
+ base.send :include, InstanceMethods
14
+ end
15
+ end
34
16
 
35
- # All keys are IDs, so it's actually blank
36
- return {} if (diffed.keys - ID_KEYS).empty?
37
- end
38
- when Array
39
- return a unless b.is_a?(Array) && a.length == b.length
17
+ module InstanceMethods
18
+ # @todo: refactor to only reach the current level
19
+ # @note the aim is to delegate `hash_diff` to the specific classes
20
+ # of the model... bulding the payload/input_base in a cascaded
21
+ # way (rather than in a one-off way from the top alone).
22
+ def diff(a, b, ignore: [])
23
+ case a
24
+ when Hash
25
+ {}.tap do |diffed|
26
+ a.each do |key, a_value|
27
+ b_value = b[key] if b
28
+ no_changes = (a_value == b_value) || ignore.include?(key)
29
+ next if !ID_KEYS.include?(key) && no_changes
40
30
 
41
- a.map.with_index do |a_value, idx|
42
- b_value = b[idx]
43
- diff(a_value, b_value, ignore: ignore)
44
- end.reject do |el|
45
- el == {}
46
- end
47
- else
48
- a
49
- end
50
- end
51
- end
31
+ diffed[key] = diff(a_value, b_value, ignore: ignore)
32
+ diffed.delete(key) if diffed[key] == {}
52
33
  end
34
+
35
+ # All keys are IDs, so it's actually blank
36
+ return {} if (diffed.keys - ID_KEYS).empty?
37
+ end
38
+ when Array
39
+ return a unless b.is_a?(Array) && a.length == b.length
40
+
41
+ a.map.with_index do |a_value, idx|
42
+ b_value = b[idx]
43
+ diff(a_value, b_value, ignore: ignore)
44
+ end.reject do |el|
45
+ el == {}
53
46
  end
47
+ else
48
+ a
54
49
  end
55
50
  end
56
51
  end
@@ -0,0 +1,242 @@
1
+ # rubocop:disable Naming/MethodParameterName
2
+ module Ecoportal::API::Common::GraphQL::Model::Diffable
3
+ # @note this is a literal copy of ecoportal-api-v2 gem `has_diff` function.
4
+ # @todo the aim is to adapt it by:
5
+ # 1. Removing the `patch_ver` model ✅
6
+ # 2. Using symbol keys ✅
7
+ module HashDiffNesting
8
+ class << self
9
+ def included(base)
10
+ super
11
+
12
+ base.send :include, InstanceMethods
13
+ end
14
+ end
15
+
16
+ META_KEYS = %i[id].freeze
17
+ NO_CHANGES = "%not-changed!%".freeze
18
+
19
+ module InstanceMethods
20
+ # The `change data` is built as follows:
21
+ # 1. detect changes that have occurred translate into one `api_operation` of `OP_TYPE`:
22
+ # * `:update`: meaning that the object has changed (existed and has not been removed)
23
+ # * `:delete`: the object has been removed
24
+ # * `:create`: the object is new
25
+ # 2. at the level of the target object of the model, the object is opened for change
26
+ # with `id` and `api_operation` as follows:
27
+ #
28
+ # ```json
29
+ # {
30
+ # "id": "objectID",
31
+ # "api_operation": "OP_TYPE",
32
+ # "change_data" {
33
+ # "property": "value",
34
+ # "...": "..."
35
+ # }
36
+ # }
37
+ # ```
38
+ # 3. the `change_data` property holds the specific changes of the object
39
+ # - the properties that have changed
40
+ # @note
41
+ # * there should not be difference between `null` and `""` (empty string)
42
+ # @param a [Hash] current hash model
43
+ # @param b [Hash] previous hash model
44
+ # @return [Hash] a `change data`
45
+ def change_diff(a, b, ignore: [])
46
+ ignore = to_a(ignore)
47
+
48
+ if b.is_a?(Hash) && !empty?(b) && empty?(a)
49
+ patch_delete(b)
50
+ elsif a.is_a?(Hash) && !empty?(a) && empty?(b)
51
+ patch_new(a, ignore: ignore)
52
+ elsif a.is_a?(Hash) && b.is_a?(Hash)
53
+ patch_update(a, b, ignore: ignore)
54
+ elsif any_array?(a, b)
55
+ patch_data_array(a, b, ignore: ignore)
56
+ else
57
+ a
58
+ end
59
+ end
60
+
61
+ private
62
+
63
+ # Compares `a` as carrying changes of `b`
64
+ # @return [Hash] change data object with only changes
65
+ def change_data(a, b = nil, delete: false, ignore: [])
66
+ {}.tap do |data_hash|
67
+ next if delete
68
+
69
+ a.each do |key, a_value|
70
+ key = key.to_sym
71
+
72
+ next if meta_keys.include?(key)
73
+ next if ignore.include?(key)
74
+
75
+ b_value = nil
76
+
77
+ if b&.key?(key)
78
+ b_value = b[key]
79
+ next if equal_values?(a_value, b_value)
80
+ end
81
+
82
+ data_hash[key] = change_diff(a_value, b_value)
83
+
84
+ data_hash.delete(key) if data_hash[key] == NO_CHANGES
85
+ end
86
+
87
+ return NO_CHANGES if (data_hash.keys - meta_keys).empty?
88
+ end
89
+ end
90
+
91
+ def patch_delete(b)
92
+ return NO_CHANGES unless b.is_a?(Hash)
93
+ return unless (id = get_id(b, exception: false))
94
+
95
+ {
96
+ id: id,
97
+ api_operation: :delete,
98
+ change_data: change_data(b, delete: true)
99
+ }
100
+ end
101
+
102
+ def patch_new(a, ignore: [])
103
+ return NO_CHANGES unless a.is_a?(Hash)
104
+ return a unless (id = get_id(a, exception: false))
105
+
106
+ {
107
+ id: id,
108
+ api_operation: :create,
109
+ change_data: change_data(a, ignore: ignore)
110
+ }
111
+ end
112
+
113
+ def patch_update(a, b, ignore: [])
114
+ return NO_CHANGES unless a.is_a?(Hash)
115
+ return a unless (id = get_id(a, exception: false))
116
+
117
+ {
118
+ id: id,
119
+ api_operation: :update,
120
+ change_data: change_data(a, b, ignore: ignore)
121
+ }.tap do |update_hash|
122
+ return nil unless update_hash[:change_data] != NO_CHANGES
123
+ end
124
+ end
125
+
126
+ def patch_data_array(a, b, ignore: [])
127
+ return patch_data_nested_array(a, b, ignore: ignore) if nested_array?(a, b)
128
+
129
+ patch_data_flat_array(a, b)
130
+ end
131
+
132
+ def patch_data_flat_array(a, b)
133
+ original_b = b
134
+ a ||= []
135
+ b ||= []
136
+
137
+ same_elements =
138
+ a.length == b.length &&
139
+ (a & b).length == b.length
140
+
141
+ return a unless same_elements
142
+ return NO_CHANGES if original_b
143
+
144
+ a
145
+ end
146
+
147
+ def patch_data_nested_array(a, b, ignore: [])
148
+ a ||= []
149
+ b ||= []
150
+
151
+ # array with nested elements
152
+ a_ids = array_ids(a)
153
+ b_ids = array_ids(b)
154
+
155
+ del_ids = b_ids - a_ids
156
+ oth_ids = b_ids & a_ids
157
+ new_ids = a_ids - b_ids
158
+
159
+ arr_delete = del_ids.map do |id|
160
+ patch_delete(array_id_item(b, id))
161
+ end.compact
162
+
163
+ arr_update = oth_ids.map do |id|
164
+ patch_update(array_id_item(a, id), array_id_item(b, id), ignore: ignore)
165
+ end.compact
166
+
167
+ arr_new = new_ids.map do |id|
168
+ patch_new(array_id_item(a, id), ignore: ignore)
169
+ end.compact
170
+
171
+ arr_delete.concat(arr_update).concat(arr_new).tap do |patch_array|
172
+ # remove data with no `id`
173
+ patch_array.select! {|item| item.is_a?(Hash)}
174
+ return NO_CHANGES if patch_array.empty?
175
+ end
176
+ end
177
+
178
+ def nested_array?(*arr)
179
+ if arr.length > 1
180
+ arr.any? {|a| nested_array?(a)}
181
+ elsif arr.length == 1
182
+ arr = arr.first || []
183
+ arr.any? do |item|
184
+ next false unless item.is_a?(Hash)
185
+ next true if item.key?(:id)
186
+
187
+ false
188
+ end
189
+ else
190
+ false
191
+ end
192
+ end
193
+
194
+ def any_array?(a, b)
195
+ [a, b].any? {|item| item.is_a?(Array)}
196
+ end
197
+
198
+ def get_id(doc, exception: true)
199
+ id = nil
200
+ id ||= doc.id if doc.respond_to?(:id)
201
+ id ||= doc['id'] if doc.is_a?(Hash)
202
+ id ||= doc[:id] if doc.is_a?(Hash)
203
+ id ||= doc if doc.is_a?(String)
204
+
205
+ raise 'No ID has been given!' unless id || !exception
206
+
207
+ id
208
+ end
209
+
210
+ def meta_keys
211
+ return self::META_KEYS if respond_to?(:const_get)
212
+
213
+ self.class::META_KEYS
214
+ end
215
+
216
+ def equal_values?(a, b)
217
+ if a.is_a?(String) || b.is_a?(String)
218
+ a_empty = a.to_s.strip.empty?
219
+ b_empty = b.to_s.strip.empty?
220
+
221
+ return true if a_empty && b_empty
222
+ end
223
+
224
+ a == b
225
+ end
226
+
227
+ def empty?(a)
228
+ bool = !a
229
+ bool ||= a.respond_to?(:empty?) && a.empty?
230
+ bool
231
+ end
232
+
233
+ def to_a(*values)
234
+ [values].flatten.compact.uniq
235
+ end
236
+ end
237
+
238
+ extend InstanceMethods
239
+ end
240
+ end
241
+
242
+ # rubocop:enable Naming/MethodParameterName
@@ -1,28 +1,50 @@
1
- require 'ecoportal/api/common/graphql/model/diffable/hash_diff'
2
1
  module Ecoportal
3
2
  module API
4
3
  module Common
5
4
  module GraphQL
6
5
  class Model
7
6
  module Diffable
8
- class << self
9
- def included(base)
10
- super
11
- base.send(:include, HashDiff)
12
- end
13
- end
7
+ require_relative 'diffable/hash_diff_nesting'
8
+ require_relative 'diffable/classic_diff_service'
9
+ require_relative 'diffable/diff_service'
10
+
11
+ DIFF_CLASS = DiffService
14
12
 
15
13
  # INSTANCE METHODS
16
14
 
17
- def as_update(ref = :last, ignore: [])
18
- new_doc = as_json
19
- ref_doc = ref == :total ? initial_doc : original_doc
15
+ # @note **cascaded callbacks**
16
+ # 1. Can **skip** rooted objects (where `root?` is `true`).
17
+ # This is because nested rooted objects are usually look-ups
18
+ # (not really part of the target model).
19
+ # 2. Allow to redefine `as_update` on each model.
20
+ # @param flat [Boolean] whether it should NOT perform a cascaded callback
21
+ # througout all the instance objects of the model hierarchy (nested).
22
+ # @param ignored [Array<String>] the keys that should be ignored, not part
23
+ # of the result of `as_update`
24
+ # @return [nil, Hash] the patch `Hash` model including only
25
+ # the changes between `original_doc` and `doc`.
26
+ def as_update(**kargs)
27
+ diff_class.new(
28
+ self,
29
+ **kargs
30
+ ).diff
31
+ end
32
+
33
+ # @note `cascaded` will be more accurate in a `GraphQL` scenario.
34
+ # @return [Boolean] stating if there are changes.
35
+ def dirty?(...)
36
+ au = as_update(...)
37
+
38
+ return false if au.nil?
39
+ return false if au == {}
40
+
41
+ true
42
+ end
43
+
44
+ private
20
45
 
21
- self.class.hash_diff(
22
- new_doc,
23
- ref_doc,
24
- ignore: ignore
25
- )
46
+ def diff_class
47
+ self.class::DIFF_CLASS
26
48
  end
27
49
  end
28
50
  end
@@ -0,0 +1,67 @@
1
+ module Ecoportal
2
+ module API
3
+ class GraphQL
4
+ module Base
5
+ class ContractorEntity
6
+ module MemberChanges
7
+ def associate(*values, lead: false)
8
+ ids = to_person_ids(*values)
9
+
10
+ associatedPeopleIds.tap do |associated|
11
+ associated.push!(*ids)
12
+ next unless lead
13
+
14
+ lead!(*ids)
15
+ end
16
+ end
17
+
18
+ def dissassociate(*values)
19
+ ids = to_person_ids(*values)
20
+
21
+ associatedPeopleIds.tap do |associated|
22
+ unlead!(*ids)
23
+ associated.delete!(*ids)
24
+ end
25
+ end
26
+
27
+ def lead!(*values)
28
+ ids = to_person_ids(*values)
29
+
30
+ leadContractorIds.tap do |leads|
31
+ associate(*ids)
32
+ leads.push!(*ids)
33
+ end
34
+ end
35
+
36
+ def unlead!(*values)
37
+ ids = to_person_ids(*values)
38
+
39
+ leadContractorIds.tap do |leads|
40
+ leads.delete!(*ids)
41
+ end
42
+ end
43
+
44
+ private
45
+
46
+ def to_person_ids(*values)
47
+ to_person_id(values.flatten)
48
+ end
49
+
50
+ def to_person_id(value)
51
+ case value
52
+ when String
53
+ value
54
+ when Ecoportal::API::V1::Person
55
+ value.id
56
+ when Hash
57
+ to_person_id(['id'])
58
+ when Enumarable
59
+ value.map {|val| to_person_id(val)}.compact
60
+ end
61
+ end
62
+ end
63
+ end
64
+ end
65
+ end
66
+ end
67
+ end
@@ -3,6 +3,10 @@ module Ecoportal
3
3
  class GraphQL
4
4
  module Base
5
5
  class ContractorEntity < Ecoportal::API::GraphQL::Base::Model
6
+ require 'ecoportal/api/graphql/base/contractor_entity/member_changes'
7
+
8
+ include MemberChanges
9
+
6
10
  root!
7
11
 
8
12
  passkey :id
@@ -12,7 +16,7 @@ module Ecoportal
12
16
  passthrough :schemaId
13
17
 
14
18
  passarray :associatedPeopleIds, :leadContractorIds
15
- embeds_one :creator, klass: "Ecoportal::API::GraphQL::Model::User"
19
+ embeds_one :creator, klass: 'Ecoportal::API::GraphQL::Model::User'
16
20
  end
17
21
  end
18
22
  end
@@ -28,6 +28,7 @@ module Ecoportal
28
28
  puts "Internal Error with these input ('#{input.class}'):"
29
29
  pp input
30
30
  raise
31
+ # rescue Graphlient::Errors::TimeoutError => _err
31
32
  end
32
33
 
33
34
  def response_class
@@ -1,5 +1,5 @@
1
1
  module Ecoportal
2
2
  module API
3
- GRAPQL_VERSION = '0.4.7'.freeze
3
+ GRAPQL_VERSION = '1.1.1'.freeze
4
4
  end
5
5
  end
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: ecoportal-api-graphql
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.4.7
4
+ version: 1.1.1
5
5
  platform: ruby
6
6
  authors:
7
7
  - Oscar Segura
8
8
  autorequire:
9
9
  bindir: exe
10
10
  cert_chain: []
11
- date: 2025-04-02 00:00:00.000000000 Z
11
+ date: 2025-05-13 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: pry
@@ -158,20 +158,20 @@ dependencies:
158
158
  requirements:
159
159
  - - "~>"
160
160
  - !ruby/object:Gem::Version
161
- version: '2.0'
161
+ version: '3.1'
162
162
  - - ">="
163
163
  - !ruby/object:Gem::Version
164
- version: 2.0.16
164
+ version: 3.1.1
165
165
  type: :runtime
166
166
  prerelease: false
167
167
  version_requirements: !ruby/object:Gem::Requirement
168
168
  requirements:
169
169
  - - "~>"
170
170
  - !ruby/object:Gem::Version
171
- version: '2.0'
171
+ version: '3.1'
172
172
  - - ">="
173
173
  - !ruby/object:Gem::Version
174
- version: 2.0.16
174
+ version: 3.1.1
175
175
  - !ruby/object:Gem::Dependency
176
176
  name: graphlient
177
177
  requirement: !ruby/object:Gem::Requirement
@@ -225,7 +225,10 @@ files:
225
225
  - lib/ecoportal/api/common/graphql/model.rb
226
226
  - lib/ecoportal/api/common/graphql/model/as_input.rb
227
227
  - lib/ecoportal/api/common/graphql/model/diffable.rb
228
+ - lib/ecoportal/api/common/graphql/model/diffable/classic_diff_service.rb
229
+ - lib/ecoportal/api/common/graphql/model/diffable/diff_service.rb
228
230
  - lib/ecoportal/api/common/graphql/model/diffable/hash_diff.rb
231
+ - lib/ecoportal/api/common/graphql/model/diffable/hash_diff_nesting.rb
229
232
  - lib/ecoportal/api/common/graphql/patches.rb
230
233
  - lib/ecoportal/api/common/graphql/query_integration.rb
231
234
  - lib/ecoportal/api/graphql.rb
@@ -233,6 +236,7 @@ files:
233
236
  - lib/ecoportal/api/graphql/base/action.rb
234
237
  - lib/ecoportal/api/graphql/base/action_category.rb
235
238
  - lib/ecoportal/api/graphql/base/contractor_entity.rb
239
+ - lib/ecoportal/api/graphql/base/contractor_entity/member_changes.rb
236
240
  - lib/ecoportal/api/graphql/base/date_time.rb
237
241
  - lib/ecoportal/api/graphql/base/field.rb
238
242
  - lib/ecoportal/api/graphql/base/file_attachment.rb