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 +4 -4
- data/.gitignore +3 -1
- data/.rubocop.yml +6 -4
- data/CHANGELOG.md +29 -4
- data/ecoportal-api-graphql.gemspec +1 -1
- data/lib/ecoportal/api/common/graphql/hash_helpers.rb +7 -11
- data/lib/ecoportal/api/common/graphql/model/as_input.rb +11 -0
- data/lib/ecoportal/api/common/graphql/model/diffable/classic_diff_service.rb +47 -0
- data/lib/ecoportal/api/common/graphql/model/diffable/diff_service.rb +180 -0
- data/lib/ecoportal/api/common/graphql/model/diffable/hash_diff.rb +41 -46
- data/lib/ecoportal/api/common/graphql/model/diffable/hash_diff_nesting.rb +242 -0
- data/lib/ecoportal/api/common/graphql/model/diffable.rb +37 -15
- data/lib/ecoportal/api/graphql/base/contractor_entity/member_changes.rb +67 -0
- data/lib/ecoportal/api/graphql/base/contractor_entity.rb +5 -1
- data/lib/ecoportal/api/graphql/logic/mutation.rb +1 -0
- data/lib/ecoportal/api/graphql_version.rb +1 -1
- metadata +10 -6
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: d43b5c70e8ce94e96f2028bcc80b6bdf45ac8f78afa7a1689b2149e2175d39db
|
4
|
+
data.tar.gz: c7e932aea0d5b4976677cecb565a64c3523b2cd8af2fac49e7ecf35f8265d636
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: dbd441319d44294084754bcc03f8660b6361930617ae23bf66faeae3d2f204d2c28d0523b63fe0985629cd196c7445ff0eaa01c18740baf02522c7c37f982f65
|
7
|
+
data.tar.gz: 9cae64f7110b5df6e3995ff36af69220b3e19cdd1cb7f8e1914128fdffadea5a916e429b2e28e009be618a709d5765b51419d441f1fd576135ae74f19a1d6292
|
data/.gitignore
CHANGED
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
|
-
## [
|
12
|
+
## [1.1.2] - 2025-05-xx
|
13
13
|
|
14
14
|
### Added
|
15
15
|
|
16
|
-
|
16
|
+
### Changed
|
17
17
|
|
18
|
-
|
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
|
-
|
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', '~>
|
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
|
-
|
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
|
-
|
4
|
-
|
5
|
-
|
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
|
-
|
18
|
-
|
19
|
-
|
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
|
-
|
36
|
-
|
37
|
-
|
38
|
-
|
39
|
-
|
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
|
-
|
42
|
-
|
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
|
-
|
9
|
-
|
10
|
-
|
11
|
-
|
12
|
-
|
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
|
-
|
18
|
-
|
19
|
-
|
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
|
-
|
22
|
-
|
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:
|
19
|
+
embeds_one :creator, klass: 'Ecoportal::API::GraphQL::Model::User'
|
16
20
|
end
|
17
21
|
end
|
18
22
|
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:
|
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-
|
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: '
|
161
|
+
version: '3.1'
|
162
162
|
- - ">="
|
163
163
|
- !ruby/object:Gem::Version
|
164
|
-
version:
|
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: '
|
171
|
+
version: '3.1'
|
172
172
|
- - ">="
|
173
173
|
- !ruby/object:Gem::Version
|
174
|
-
version:
|
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
|