extraction_metadata_changes 0.0.1a
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +7 -0
- data/LICENSE +21 -0
- data/README.md +83 -0
- data/lib/extraction_metadata_changes.rb +5 -0
- data/lib/extraction_metadata_changes/disjoint_list.rb +252 -0
- data/lib/extraction_metadata_changes/fact_changes.rb +652 -0
- metadata +132 -0
checksums.yaml
ADDED
@@ -0,0 +1,7 @@
|
|
1
|
+
---
|
2
|
+
SHA256:
|
3
|
+
metadata.gz: c7e5c61dc16159a4a8c989eb63689b147af5c70c35a3a96bb15d0d320374d914
|
4
|
+
data.tar.gz: bcf65732c185b576ccc4d15c10e6ffba1d2edab4d58cfa368433ea884858d973
|
5
|
+
SHA512:
|
6
|
+
metadata.gz: 7d6f2b597f0a240f16e72eef7d16bb25fa8d37a7d894b1dcd12e16b23da55706c131ea900b215ec726fd30790b02cf3f41cf5ef99e76c0487c1ad9fe1d69bcff
|
7
|
+
data.tar.gz: 3670c2f5811142be4940514162cc9320aa2d2fd3a6e3379bba601fbc7bb712c383eb1af6d9e59bf4b8cb9b772de692a43b9ce6d31119f8643f1b7763bf0204f5
|
data/LICENSE
ADDED
@@ -0,0 +1,21 @@
|
|
1
|
+
MIT License
|
2
|
+
|
3
|
+
Copyright (c) 2020 Eduardo Martín Rojo
|
4
|
+
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
7
|
+
in the Software without restriction, including without limitation the rights
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
10
|
+
furnished to do so, subject to the following conditions:
|
11
|
+
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
13
|
+
copies or substantial portions of the Software.
|
14
|
+
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
21
|
+
SOFTWARE.
|
data/README.md
ADDED
@@ -0,0 +1,83 @@
|
|
1
|
+
# extraction-metadata-changes
|
2
|
+
Client interface tool that talks with the metadata service to store and apply all
|
3
|
+
metadata modifications in a single transaction.
|
4
|
+
|
5
|
+
# How to use it
|
6
|
+
|
7
|
+
Add this line to your Gemfile:
|
8
|
+
|
9
|
+
```ruby
|
10
|
+
gem "extraction_metadata_changes"
|
11
|
+
```
|
12
|
+
|
13
|
+
# Getting started
|
14
|
+
|
15
|
+
## Building a modifications object
|
16
|
+
|
17
|
+
To build a set of metadata modifications, first we need to build a FactChanges object:
|
18
|
+
|
19
|
+
```ruby
|
20
|
+
> changes = FactChanges.new
|
21
|
+
```
|
22
|
+
|
23
|
+
This object will store the modifications that we want to apply in a single transaction so
|
24
|
+
we can run different methods to create these modifications. The list of available
|
25
|
+
modifications is:
|
26
|
+
|
27
|
+
* Create/Delete assets
|
28
|
+
|
29
|
+
```ruby
|
30
|
+
> changes.create_assets(["00000000-0000-0000-0000"])
|
31
|
+
> changes.create_assets(["?my_car", "?my_previous_car", "?your_car"])
|
32
|
+
> changes.delete_assets(["00000000-0000-0000-0000"])
|
33
|
+
```
|
34
|
+
|
35
|
+
Created assets can be described by either an uuid or by using a *variable* notation. A variable is any string starting with '?' that will identify the created asset in subsequent modifications, so we can refer to it in a more meaningful way than with a uuid.
|
36
|
+
|
37
|
+
* Add/Remove properties
|
38
|
+
|
39
|
+
```ruby
|
40
|
+
> changes.add("?my_car", "color", "Red")
|
41
|
+
> changes.remove_where("00000000-0000-0000-0000", "size", "Big")
|
42
|
+
```
|
43
|
+
|
44
|
+
* Add/Remove relations
|
45
|
+
|
46
|
+
``` ruby
|
47
|
+
> changes.add("?my_car", "quickerThan", "?your_car")
|
48
|
+
> changes.removeWhere("?my_car", "quickerThan", "?my_previous_car")
|
49
|
+
> changes.removeWhere("?my_previous_car", "quickerThan", "?my_car")
|
50
|
+
```
|
51
|
+
|
52
|
+
* Create/Delete groups of assets
|
53
|
+
|
54
|
+
```ruby
|
55
|
+
> changes.create_asset_groups(["?my_parking", "?a_parking_with_fees"])
|
56
|
+
> changes.delete_asset_groups(["00000000-0000-0000-0000"])
|
57
|
+
```
|
58
|
+
|
59
|
+
* Add/Remove assets to/from groups
|
60
|
+
|
61
|
+
```ruby
|
62
|
+
> changes.add_assets_to_group("?my_parking", ["?my_car"])
|
63
|
+
> changes.remove_assets_from_group("?a_parking_with_fees", ["?my_car"])
|
64
|
+
```
|
65
|
+
|
66
|
+
* Specify errors that will avoid the transaction to apply
|
67
|
+
|
68
|
+
```ruby
|
69
|
+
> changes.set_errors(["This set of modifications are wrong."])
|
70
|
+
```
|
71
|
+
|
72
|
+
## Applying the modifications
|
73
|
+
|
74
|
+
Once we have completed all changes we want to apply in a single transaction, we can run
|
75
|
+
the apply method:
|
76
|
+
|
77
|
+
```ruby
|
78
|
+
> changes.apply(transaction_id)
|
79
|
+
```
|
80
|
+
|
81
|
+
All modifications will be joined under a single transaction, so we need to provide a unique
|
82
|
+
identifier of this set of modifications. One way of doing this is keeping a separate table that will keep track of the transactions creation an using the id of this table as the transaction id for the metadata service. This transaction id will be the reference of the changes we have apply so we can roll it back later if we ever need it.
|
83
|
+
that we have apply.
|
@@ -0,0 +1,252 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require 'securerandom'
|
4
|
+
require 'google_hash'
|
5
|
+
|
6
|
+
module ExtractionMetadataChanges
|
7
|
+
# This class DisjointList allow us to establish a relation between a list and a one or
|
8
|
+
# more dependent lists where all lists behave as disjoint sets of elements where any
|
9
|
+
# element added to any of the list cannot exist in anyother list at the same time so all
|
10
|
+
# lists negate elements to each other.
|
11
|
+
#
|
12
|
+
class DisjointList
|
13
|
+
SEED_FOR_UNIQUE_IDS = Random.rand(1000)
|
14
|
+
MAX_DEEP_UNIQUE_ID = 3
|
15
|
+
|
16
|
+
include Enumerable
|
17
|
+
|
18
|
+
attr_accessor :location_for_unique_id
|
19
|
+
attr_accessor :disjoint_lists
|
20
|
+
attr_accessor :list
|
21
|
+
attr_reader :name
|
22
|
+
|
23
|
+
DISABLED_NAME = 'DISABLED'
|
24
|
+
|
25
|
+
def initialize(list)
|
26
|
+
@name = "object_id_#{object_id}"
|
27
|
+
|
28
|
+
# Replace with a normal hash if we want to stop using it
|
29
|
+
@location_for_unique_id = GoogleHashDenseLongToRuby.new
|
30
|
+
|
31
|
+
@list = []
|
32
|
+
@disjoint_lists = [self]
|
33
|
+
list.each { |o| add(o) }
|
34
|
+
end
|
35
|
+
|
36
|
+
def add_disjoint_list(disjoint_list)
|
37
|
+
disjoints = (disjoint_list.disjoint_lists - disjoint_lists)
|
38
|
+
disjoint_lists.concat(disjoints).uniq
|
39
|
+
|
40
|
+
disjoints.each do |disjoint|
|
41
|
+
_synchronize_with_list(disjoint)
|
42
|
+
|
43
|
+
disjoint.location_for_unique_id = location_for_unique_id
|
44
|
+
disjoint.disjoint_lists = disjoint_lists
|
45
|
+
end
|
46
|
+
end
|
47
|
+
|
48
|
+
def store_for(element)
|
49
|
+
_store_for(unique_id_for_element(element))
|
50
|
+
end
|
51
|
+
|
52
|
+
def _store_for(unique_id)
|
53
|
+
store_name = location_for_unique_id[unique_id]
|
54
|
+
return nil if store_name.nil? || store_name == DISABLED_NAME
|
55
|
+
|
56
|
+
@disjoint_lists.select { |l| l.name == store_name }.first
|
57
|
+
end
|
58
|
+
|
59
|
+
def enabled?(element)
|
60
|
+
!store_for(element).nil?
|
61
|
+
end
|
62
|
+
|
63
|
+
def disabled?(element)
|
64
|
+
disabled_key?(unique_id_for_element(element))
|
65
|
+
end
|
66
|
+
|
67
|
+
def disabled_key?(key)
|
68
|
+
@location_for_unique_id[key] == DISABLED_NAME
|
69
|
+
end
|
70
|
+
|
71
|
+
def enabled_in_other_list?(element)
|
72
|
+
enabled?(element) && !include?(element)
|
73
|
+
end
|
74
|
+
|
75
|
+
def include?(element)
|
76
|
+
include_key?(unique_id_for_element(element))
|
77
|
+
end
|
78
|
+
|
79
|
+
def include_key?(key)
|
80
|
+
@location_for_unique_id[key] == name
|
81
|
+
end
|
82
|
+
|
83
|
+
def remove(element)
|
84
|
+
unique_id = unique_id_for_element(element)
|
85
|
+
remove_from_raw_list_by_id(unique_id)
|
86
|
+
@location_for_unique_id.delete(unique_id)
|
87
|
+
end
|
88
|
+
|
89
|
+
def remove_from_raw_list_by_id(unique_id)
|
90
|
+
@list.delete_if do |a|
|
91
|
+
unique_id_for_element(a) == unique_id
|
92
|
+
end
|
93
|
+
end
|
94
|
+
|
95
|
+
def length
|
96
|
+
@list.length
|
97
|
+
end
|
98
|
+
|
99
|
+
def each(&block)
|
100
|
+
@list.each(&block)
|
101
|
+
end
|
102
|
+
|
103
|
+
def [](index)
|
104
|
+
@list[index]
|
105
|
+
end
|
106
|
+
|
107
|
+
def flatten
|
108
|
+
@list.flatten
|
109
|
+
end
|
110
|
+
|
111
|
+
def uniq!
|
112
|
+
@list.uniq!
|
113
|
+
end
|
114
|
+
|
115
|
+
def <<(element)
|
116
|
+
if element.is_a?(Array)
|
117
|
+
element.each { |e| add(e) }
|
118
|
+
else
|
119
|
+
add(element)
|
120
|
+
end
|
121
|
+
end
|
122
|
+
|
123
|
+
def concat(element)
|
124
|
+
if element.is_a?(Array)
|
125
|
+
element.each { |e| add(e) }
|
126
|
+
else
|
127
|
+
add(element)
|
128
|
+
end
|
129
|
+
end
|
130
|
+
|
131
|
+
def push(element)
|
132
|
+
add(element)
|
133
|
+
end
|
134
|
+
|
135
|
+
def add(element)
|
136
|
+
return concat_disjoint_list(element) if element.is_a?(
|
137
|
+
ExtractionMetadataChanges::DisjointList
|
138
|
+
)
|
139
|
+
|
140
|
+
if enabled_in_other_list?(element)
|
141
|
+
disable(element)
|
142
|
+
elsif include?(element)
|
143
|
+
# It is already in our list, so we do not add it again
|
144
|
+
return false
|
145
|
+
else
|
146
|
+
enable(element)
|
147
|
+
end
|
148
|
+
self
|
149
|
+
end
|
150
|
+
|
151
|
+
def sum_function_for(value)
|
152
|
+
value.hash
|
153
|
+
# Value to create checksum and seed
|
154
|
+
# XXhash.xxh32(value, SEED_FOR_UNIQUE_IDS)
|
155
|
+
end
|
156
|
+
|
157
|
+
def unique_id_for_element(element)
|
158
|
+
_unique_id_for_element(element, 0)
|
159
|
+
end
|
160
|
+
|
161
|
+
def concat_disjoint_list(disjoint_list)
|
162
|
+
disjoint_list.location_for_unique_id.keys.each do |key|
|
163
|
+
_disable(key) if disjoint_list.disabled_key?(key)
|
164
|
+
end
|
165
|
+
disjoint_list.to_a.each { |val| add(val) }
|
166
|
+
self
|
167
|
+
end
|
168
|
+
|
169
|
+
def merge(disjoint_list)
|
170
|
+
disjoint_list.location_for_unique_id.keys.each do |key|
|
171
|
+
_disable(key) if !disjoint_list.include_key?(key) || disjoint_list.disabled_key?(key)
|
172
|
+
end
|
173
|
+
disjoint_list.to_a.each { |val| add(val) }
|
174
|
+
self
|
175
|
+
end
|
176
|
+
|
177
|
+
def enable(element)
|
178
|
+
return if disabled?(element)
|
179
|
+
|
180
|
+
unique_id = unique_id_for_element(element)
|
181
|
+
# Is not in any of the lists so we can add it
|
182
|
+
if element.is_a?(Enumerable) && !element.is_a?(Hash)
|
183
|
+
@list.concat(element)
|
184
|
+
else
|
185
|
+
@list.push(element)
|
186
|
+
end
|
187
|
+
@location_for_unique_id[unique_id] = name
|
188
|
+
end
|
189
|
+
|
190
|
+
def disable(element)
|
191
|
+
_disable(unique_id_for_element(element))
|
192
|
+
end
|
193
|
+
|
194
|
+
protected
|
195
|
+
|
196
|
+
def _synchronize_with_list(disjoint_list)
|
197
|
+
disjoint_list.location_for_unique_id.keys.each do |key|
|
198
|
+
unless location_for_unique_id[key] == DISABLED_NAME
|
199
|
+
# If my disjoint lists do not have the element
|
200
|
+
if location_for_unique_id[key].nil?
|
201
|
+
location_for_unique_id[key] = disjoint_list.location_for_unique_id[key]
|
202
|
+
_disable(key) if location_for_unique_id[key] == DISABLED_NAME
|
203
|
+
elsif location_for_unique_id[key] != disjoint_list.location_for_unique_id[key]
|
204
|
+
# If my lists have the element alredy
|
205
|
+
_disable(key)
|
206
|
+
end
|
207
|
+
end
|
208
|
+
end
|
209
|
+
end
|
210
|
+
|
211
|
+
def _disable(unique_id)
|
212
|
+
store = _store_for(unique_id)
|
213
|
+
store&.remove_from_raw_list_by_id(unique_id)
|
214
|
+
location_for_unique_id[unique_id] = DISABLED_NAME
|
215
|
+
end
|
216
|
+
|
217
|
+
def _unique_id_for_element(element, deep = 0)
|
218
|
+
return sum_function_for(SecureRandom.uuid) if deep == MAX_DEEP_UNIQUE_ID
|
219
|
+
|
220
|
+
if element.is_a?(String)
|
221
|
+
sum_function_for(element)
|
222
|
+
elsif element.respond_to?(:uuid) && !element.uuid.nil?
|
223
|
+
sum_function_for(element.uuid)
|
224
|
+
elsif element.respond_to?(:id) && !element.id.nil?
|
225
|
+
sum_function_for("#{element.class}_#{element.id}")
|
226
|
+
elsif element.is_a?(Hash)
|
227
|
+
if element.key?(:uuid) && !element[:uuid].nil?
|
228
|
+
sum_function_for(element[:uuid])
|
229
|
+
elsif element.key?(:predicate)
|
230
|
+
_unique_id_for_fact(element)
|
231
|
+
else
|
232
|
+
sum_function_for(element.keys.dup.concat(element.values.map do |val|
|
233
|
+
_unique_id_for_element(val, deep + 1)
|
234
|
+
end).join(''))
|
235
|
+
end
|
236
|
+
elsif element.is_a?(Enumerable)
|
237
|
+
sum_function_for(element.map { |o| _unique_id_for_element(o, deep + 1) }.join(''))
|
238
|
+
else
|
239
|
+
sum_function_for(element.to_s)
|
240
|
+
end
|
241
|
+
end
|
242
|
+
|
243
|
+
def _unique_id_for_fact(element)
|
244
|
+
sum_function_for([
|
245
|
+
(element[:asset_id] || element[:asset].id || element[:asset].object_id),
|
246
|
+
element[:predicate],
|
247
|
+
(element[:object] || element[:object_asset_id] || element[:object_asset].id ||
|
248
|
+
element[:object_asset].object_id)
|
249
|
+
].join('_'))
|
250
|
+
end
|
251
|
+
end
|
252
|
+
end
|
@@ -0,0 +1,652 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require 'securerandom'
|
4
|
+
require 'extraction_token_util'
|
5
|
+
|
6
|
+
module ExtractionMetadataChanges
|
7
|
+
#
|
8
|
+
# Any change of metadata is composed of small modifications related with each other
|
9
|
+
# that can be applied in a transactional way so they can be cancelled, rolledback, etc.
|
10
|
+
#
|
11
|
+
# *About DisjointList*
|
12
|
+
#
|
13
|
+
# To be able to merge different configurations of changes, we need a way to keep track
|
14
|
+
# on the properties and values that have been added and removed to keep the modifications
|
15
|
+
# consistent, to avoid performing the operation twice, or in a wrong way.
|
16
|
+
#
|
17
|
+
# For instance, in these operations:
|
18
|
+
#
|
19
|
+
# > changes.add('?my_car', 'color', 'Red')
|
20
|
+
# > changes.remove_where('?my_car', 'color', 'Red')
|
21
|
+
#
|
22
|
+
# This modifications have opposite meaning so they do not apply at all, but if we add this:
|
23
|
+
#
|
24
|
+
# > changes.add('?my_car', 'color', 'Bright')
|
25
|
+
#
|
26
|
+
# It can apply because it refers to a different value. All this logic about when a property
|
27
|
+
# should not be part of the final transaction is performed using the DisjointList class,
|
28
|
+
# by establishing a disjoint relations between opposite lists (added properties,
|
29
|
+
# removed properties, etc...)
|
30
|
+
#
|
31
|
+
#
|
32
|
+
class FactChanges
|
33
|
+
attr_accessor :facts_to_destroy, :facts_to_add, :assets_to_create, :assets_to_destroy,
|
34
|
+
:assets_to_add, :assets_to_remove, :wildcards, :instances_from_uuid,
|
35
|
+
:asset_groups_to_create, :asset_groups_to_destroy, :errors_added,
|
36
|
+
:already_added_to_list, :instances_by_unique_id,
|
37
|
+
:facts_to_set_to_remote
|
38
|
+
|
39
|
+
attr_accessor :operations
|
40
|
+
|
41
|
+
def initialize(json = nil)
|
42
|
+
@assets_updated = []
|
43
|
+
reset
|
44
|
+
parse_json(json) if json
|
45
|
+
end
|
46
|
+
|
47
|
+
def parsing_valid?
|
48
|
+
@parsing_valid
|
49
|
+
end
|
50
|
+
|
51
|
+
def reset
|
52
|
+
@parsing_valid = false
|
53
|
+
@errors_added = []
|
54
|
+
|
55
|
+
@facts_to_set_to_remote = []
|
56
|
+
|
57
|
+
build_disjoint_lists(:facts_to_add, :facts_to_destroy)
|
58
|
+
build_disjoint_lists(:assets_to_create, :assets_to_destroy)
|
59
|
+
build_disjoint_lists(:asset_groups_to_create, :asset_groups_to_destroy)
|
60
|
+
build_disjoint_lists(:assets_to_add, :assets_to_remove)
|
61
|
+
|
62
|
+
@instances_from_uuid = GoogleHashDenseRubyToRuby.new
|
63
|
+
@wildcards = GoogleHashDenseRubyToRuby.new
|
64
|
+
end
|
65
|
+
|
66
|
+
def build_disjoint_lists(list, opposite)
|
67
|
+
list1 = ExtractionMetadataChanges::DisjointList.new([])
|
68
|
+
list2 = ExtractionMetadataChanges::DisjointList.new([])
|
69
|
+
|
70
|
+
list1.add_disjoint_list(list2)
|
71
|
+
|
72
|
+
send("#{list}=", list1)
|
73
|
+
send("#{opposite}=", list2)
|
74
|
+
end
|
75
|
+
|
76
|
+
def asset_group_asset_to_h(asset_group_asset_str)
|
77
|
+
obj = asset_group_asset_str.each_with_object({}) do |o, memo|
|
78
|
+
key = o[:asset_group]&.uuid || nil
|
79
|
+
memo[key] = [] unless memo[key]
|
80
|
+
memo[key].push(o[:asset].uuid)
|
81
|
+
end
|
82
|
+
obj.map do |k, v|
|
83
|
+
[k, v]
|
84
|
+
end
|
85
|
+
end
|
86
|
+
|
87
|
+
def to_h
|
88
|
+
{
|
89
|
+
'set_errors': @errors_added,
|
90
|
+
'create_assets': @assets_to_create.map(&:uuid),
|
91
|
+
'create_asset_groups': @asset_groups_to_create.map(&:uuid),
|
92
|
+
'delete_asset_groups': @asset_groups_to_destroy.map(&:uuid),
|
93
|
+
'delete_assets': @assets_to_destroy.map(&:uuid),
|
94
|
+
'add_facts': @facts_to_add.map do |f|
|
95
|
+
[
|
96
|
+
f[:asset].nil? ? nil : f[:asset].uuid,
|
97
|
+
f[:predicate],
|
98
|
+
(f[:object] || f[:object_asset].uuid)
|
99
|
+
]
|
100
|
+
end,
|
101
|
+
'remove_facts': @facts_to_destroy.map do |f|
|
102
|
+
if f[:id]
|
103
|
+
fact = Fact.find(f[:id])
|
104
|
+
[fact.asset.uuid, fact.predicate, fact.object_value_or_uuid]
|
105
|
+
else
|
106
|
+
[
|
107
|
+
f[:asset].nil? ? nil : f[:asset].uuid,
|
108
|
+
f[:predicate],
|
109
|
+
(f[:object] || f[:object_asset].uuid)
|
110
|
+
]
|
111
|
+
end
|
112
|
+
end,
|
113
|
+
'add_assets': asset_group_asset_to_h(@assets_to_add),
|
114
|
+
'remove_assets': asset_group_asset_to_h(@assets_to_remove)
|
115
|
+
}.reject { |_k, v| v.empty? }
|
116
|
+
end
|
117
|
+
|
118
|
+
def to_json(*_args)
|
119
|
+
JSON.pretty_generate(to_h)
|
120
|
+
end
|
121
|
+
|
122
|
+
def parse_json(json)
|
123
|
+
obj = JSON.parse(json)
|
124
|
+
%w[set_errors create_assets create_asset_groups delete_asset_groups
|
125
|
+
remove_facts add_facts delete_assets add_assets remove_assets].each do |action_type|
|
126
|
+
send(action_type, obj[action_type]) if obj[action_type]
|
127
|
+
end
|
128
|
+
@parsing_valid = true
|
129
|
+
end
|
130
|
+
|
131
|
+
def values_for_predicate(asset, predicate)
|
132
|
+
actual_values = asset.facts.with_predicate(predicate).map(&:object)
|
133
|
+
values_to_add = facts_to_add.map do |f|
|
134
|
+
f[:object] if (f[:asset] == asset) && (f[:predicate] == predicate)
|
135
|
+
end.compact
|
136
|
+
values_to_destroy = facts_to_destroy.map do |f|
|
137
|
+
f[:object] if (f[:asset] == asset) && (f[:predicate] == predicate)
|
138
|
+
end.compact
|
139
|
+
(actual_values + values_to_add - values_to_destroy)
|
140
|
+
end
|
141
|
+
|
142
|
+
def _build_fact_attributes(asset, predicate, object, options = {})
|
143
|
+
t = [asset, predicate, object, options]
|
144
|
+
params = { asset: t[0], predicate: t[1], literal: !t[2].is_a?(Asset) }
|
145
|
+
params[:literal] ? params[:object] = t[2] : params[:object_asset] = t[2]
|
146
|
+
params = params.merge(t[3]) if t[3]
|
147
|
+
params
|
148
|
+
end
|
149
|
+
|
150
|
+
def add(asset, predicate, object, options = {})
|
151
|
+
asset = find_asset(asset)
|
152
|
+
object = options[:literal] == true ? literal_token(object) : find_asset(object)
|
153
|
+
|
154
|
+
fact = _build_fact_attributes(asset, predicate, object, options)
|
155
|
+
|
156
|
+
facts_to_add << fact if fact
|
157
|
+
end
|
158
|
+
|
159
|
+
def literal_token(str)
|
160
|
+
ExtractionTokenUtil.quote_if_uuid(str)
|
161
|
+
end
|
162
|
+
|
163
|
+
def add_facts(lists)
|
164
|
+
lists.each { |list| add(list[0], list[1], list[2]) }
|
165
|
+
self
|
166
|
+
end
|
167
|
+
|
168
|
+
def remove_facts(lists)
|
169
|
+
lists.each { |list| remove_where(list[0], list[1], list[2]) }
|
170
|
+
self
|
171
|
+
end
|
172
|
+
|
173
|
+
def add_remote(asset, predicate, object, options = {})
|
174
|
+
return unless asset && predicate && object
|
175
|
+
|
176
|
+
add(asset, predicate, object, options.merge({ is_remote?: true }))
|
177
|
+
end
|
178
|
+
|
179
|
+
def replace_remote_relation(asset, predicate, object_asset, options = {})
|
180
|
+
replace_remote(asset, predicate, object_asset, options.merge({ literal: false }))
|
181
|
+
end
|
182
|
+
|
183
|
+
def replace_remote_property(asset, predicate, value, options = {})
|
184
|
+
replace_remote(asset, predicate, value, options.merge({ literal: true }))
|
185
|
+
end
|
186
|
+
|
187
|
+
def replace_remote(asset, predicate, object, options = {})
|
188
|
+
return unless asset && predicate && object
|
189
|
+
|
190
|
+
asset.facts.with_predicate(predicate).each do |fact|
|
191
|
+
# The value is updated from the remote instance so we remove the previous value
|
192
|
+
remove(fact)
|
193
|
+
# In any case they will be set as Remote, even if they are not removed in this update
|
194
|
+
facts_to_set_to_remote << fact
|
195
|
+
end
|
196
|
+
add_remote(asset, predicate, object, options)
|
197
|
+
end
|
198
|
+
|
199
|
+
def remove(fact)
|
200
|
+
return if fact.nil?
|
201
|
+
|
202
|
+
if fact.is_a?(Enumerable)
|
203
|
+
facts_to_destroy << fact.map { |o| o.attributes.symbolize_keys }
|
204
|
+
elsif fact.is_a?(Fact)
|
205
|
+
facts_to_destroy << fact.attributes.symbolize_keys if fact
|
206
|
+
end
|
207
|
+
end
|
208
|
+
|
209
|
+
def remove_where(subject, predicate, object)
|
210
|
+
subject = find_asset(subject)
|
211
|
+
object = find_asset(object)
|
212
|
+
|
213
|
+
fact = _build_fact_attributes(subject, predicate, object)
|
214
|
+
|
215
|
+
facts_to_destroy << fact if fact
|
216
|
+
end
|
217
|
+
|
218
|
+
def errors?
|
219
|
+
to_h.key?(:set_errors)
|
220
|
+
end
|
221
|
+
|
222
|
+
def merge_hash(hash1, hash2)
|
223
|
+
hash2.keys.each do |k|
|
224
|
+
hash1[k] = hash2[k]
|
225
|
+
end
|
226
|
+
hash1
|
227
|
+
end
|
228
|
+
|
229
|
+
def merge(fact_changes)
|
230
|
+
if fact_changes
|
231
|
+
# To keep track of already added object after merging with another fact changes object
|
232
|
+
# _add_already_added_from_other_object(fact_changes)
|
233
|
+
errors_added.concat(fact_changes.errors_added)
|
234
|
+
asset_groups_to_create.concat(fact_changes.asset_groups_to_create)
|
235
|
+
assets_to_create.concat(fact_changes.assets_to_create)
|
236
|
+
facts_to_add.concat(fact_changes.facts_to_add)
|
237
|
+
assets_to_add.concat(fact_changes.assets_to_add)
|
238
|
+
assets_to_remove.concat(fact_changes.assets_to_remove)
|
239
|
+
facts_to_destroy.concat(fact_changes.facts_to_destroy)
|
240
|
+
assets_to_destroy.concat(fact_changes.assets_to_destroy)
|
241
|
+
asset_groups_to_destroy.concat(fact_changes.asset_groups_to_destroy)
|
242
|
+
merge_hash(instances_from_uuid, fact_changes.instances_from_uuid)
|
243
|
+
merge_hash(wildcards, fact_changes.wildcards)
|
244
|
+
end
|
245
|
+
self
|
246
|
+
end
|
247
|
+
|
248
|
+
def apply(step, with_operations = true)
|
249
|
+
_handle_errors(step) unless errors_added.empty?
|
250
|
+
ActiveRecord::Base.transaction do |_t|
|
251
|
+
# We need step to have an allocated id to be able to link it with the operations
|
252
|
+
# so we have to create a new record if is not already stored
|
253
|
+
step.save unless step.persisted?
|
254
|
+
|
255
|
+
# Callbacks execution
|
256
|
+
_on_apply(step) if respond_to?(:_on_apply)
|
257
|
+
|
258
|
+
_set_remote_facts(facts_to_set_to_remote)
|
259
|
+
|
260
|
+
# Creates the facts and generate from it the list of operations
|
261
|
+
operations = [
|
262
|
+
_create_asset_groups(step, asset_groups_to_create, with_operations),
|
263
|
+
_create_assets(step, assets_to_create, with_operations),
|
264
|
+
_add_assets(step, assets_to_add, with_operations),
|
265
|
+
_remove_assets(step, assets_to_remove, with_operations),
|
266
|
+
_remove_facts(step, facts_to_destroy, with_operations),
|
267
|
+
_detach_assets(step, assets_to_destroy, with_operations),
|
268
|
+
_detach_asset_groups(step, asset_groups_to_destroy, with_operations),
|
269
|
+
_create_facts(step, facts_to_add, with_operations)
|
270
|
+
].flatten.compact
|
271
|
+
|
272
|
+
# Writes all operations in a single call
|
273
|
+
unless operations.empty?
|
274
|
+
Operation.import(operations)
|
275
|
+
@operations = operations
|
276
|
+
end
|
277
|
+
step.save if step.changed?
|
278
|
+
_handle_errors(step) unless errors_added.empty?
|
279
|
+
reset
|
280
|
+
end
|
281
|
+
end
|
282
|
+
|
283
|
+
def assets_updated
|
284
|
+
return [] unless @operations
|
285
|
+
|
286
|
+
@assets_updated = Asset.where(id: @operations.pluck(:asset_id).uniq).distinct
|
287
|
+
end
|
288
|
+
|
289
|
+
def assets_for_printing
|
290
|
+
return [] unless @operations
|
291
|
+
|
292
|
+
asset_ids = @operations.select do |operation|
|
293
|
+
(operation.action_type == 'createAssets')
|
294
|
+
end.pluck(:object).uniq
|
295
|
+
|
296
|
+
ready_for_print_ids = @operations.select do |operation|
|
297
|
+
((operation.action_type == 'addFacts') &&
|
298
|
+
(operation.predicate == 'is') &&
|
299
|
+
(operation.object == 'readyForPrint'))
|
300
|
+
end.map(&:asset).compact.uniq.map(&:uuid)
|
301
|
+
|
302
|
+
ids_for_print = asset_ids.concat(ready_for_print_ids).flatten.uniq
|
303
|
+
@assets_for_printing = Asset.for_printing.where(uuid: ids_for_print)
|
304
|
+
end
|
305
|
+
|
306
|
+
def find_asset(asset_or_uuid)
|
307
|
+
find_instance_of_class_by_uuid(Asset, asset_or_uuid)
|
308
|
+
end
|
309
|
+
|
310
|
+
def find_asset_group(asset_group_or_id)
|
311
|
+
find_instance_of_class_by_uuid(AssetGroup, asset_group_or_id)
|
312
|
+
end
|
313
|
+
|
314
|
+
def find_assets(assets_or_uuids)
|
315
|
+
assets_or_uuids.uniq.map do |asset_or_uuid|
|
316
|
+
find_instance_of_class_by_uuid(Asset, asset_or_uuid)
|
317
|
+
end
|
318
|
+
end
|
319
|
+
|
320
|
+
def build_assets(assets)
|
321
|
+
assets.uniq.map do |asset_or_uuid|
|
322
|
+
find_instance_of_class_by_uuid(Asset, asset_or_uuid, true)
|
323
|
+
end
|
324
|
+
end
|
325
|
+
|
326
|
+
def find_asset_groups(asset_groups_or_uuids)
|
327
|
+
asset_groups_or_uuids.uniq.map do |asset_group_or_uuid|
|
328
|
+
find_instance_of_class_by_uuid(AssetGroup, asset_group_or_uuid)
|
329
|
+
end
|
330
|
+
end
|
331
|
+
|
332
|
+
def build_asset_groups(asset_groups)
|
333
|
+
asset_groups.uniq.map do |asset_group_or_uuid|
|
334
|
+
find_instance_of_class_by_uuid(AssetGroup, asset_group_or_uuid, true)
|
335
|
+
end
|
336
|
+
end
|
337
|
+
|
338
|
+
def new_record?(uuid)
|
339
|
+
(instances_from_uuid[uuid] && instances_from_uuid[uuid].new_record?) == true
|
340
|
+
end
|
341
|
+
|
342
|
+
def find_instance_of_class_by_uuid(klass, instance_or_uuid_or_id, create = false)
|
343
|
+
if ExtractionTokenUtil.wildcard?(instance_or_uuid_or_id)
|
344
|
+
uuid = uuid_for_wildcard(instance_or_uuid_or_id)
|
345
|
+
# Do not try to find it if it is a new wildcard created
|
346
|
+
found = find_instance_from_uuid(klass, uuid) unless create
|
347
|
+
found = ((instances_from_uuid[uuid] ||= klass.new(uuid: uuid))) if !found && create
|
348
|
+
elsif ExtractionTokenUtil.uuid?(instance_or_uuid_or_id)
|
349
|
+
found = find_instance_from_uuid(klass, instance_or_uuid_or_id)
|
350
|
+
if !found && create
|
351
|
+
found = ((instances_from_uuid[instance_or_uuid_or_id] ||= klass.new(
|
352
|
+
uuid: instance_or_uuid_or_id
|
353
|
+
)))
|
354
|
+
end
|
355
|
+
else
|
356
|
+
found = instance_or_uuid_or_id
|
357
|
+
end
|
358
|
+
unless found
|
359
|
+
_produce_error([%(
|
360
|
+
Element identified by #{instance_or_uuid_or_id} should be declared before using it
|
361
|
+
)])
|
362
|
+
end
|
363
|
+
found
|
364
|
+
end
|
365
|
+
|
366
|
+
def uuid_for_wildcard(wildcard)
|
367
|
+
wildcards[wildcard] ||= SecureRandom.uuid
|
368
|
+
end
|
369
|
+
|
370
|
+
def wildcard_for_uuid(uuid)
|
371
|
+
wildcards.keys.select { |key| wildcards[key] == uuid }.first
|
372
|
+
end
|
373
|
+
|
374
|
+
def find_instance_from_uuid(klass, uuid)
|
375
|
+
found = klass.find_by(uuid: uuid) unless new_record?(uuid)
|
376
|
+
return found if found
|
377
|
+
|
378
|
+
instances_from_uuid[uuid]
|
379
|
+
end
|
380
|
+
|
381
|
+
def validate_instances(instances)
|
382
|
+
if instances.is_a?(Array)
|
383
|
+
instances.each { |a| raise StandardError, a if a.nil? }
|
384
|
+
else
|
385
|
+
raise StandardError, a if instances.nil?
|
386
|
+
end
|
387
|
+
instances
|
388
|
+
end
|
389
|
+
|
390
|
+
def set_errors(errors)
|
391
|
+
errors_added.concat(errors)
|
392
|
+
self
|
393
|
+
end
|
394
|
+
|
395
|
+
def create_assets(assets)
|
396
|
+
assets_to_create << validate_instances(build_assets(assets))
|
397
|
+
# assets_to_create.concat(validate_instances(build_assets(assets)))
|
398
|
+
self
|
399
|
+
end
|
400
|
+
|
401
|
+
def create_asset_groups(asset_groups)
|
402
|
+
asset_groups_to_create << validate_instances(build_asset_groups(asset_groups))
|
403
|
+
self
|
404
|
+
end
|
405
|
+
|
406
|
+
def delete_asset_groups(asset_groups)
|
407
|
+
asset_groups_to_destroy << validate_instances(find_asset_groups(asset_groups))
|
408
|
+
self
|
409
|
+
end
|
410
|
+
|
411
|
+
def delete_assets(assets)
|
412
|
+
assets_to_destroy << validate_instances(find_assets(assets))
|
413
|
+
self
|
414
|
+
end
|
415
|
+
|
416
|
+
def add_assets_to_group(group, assets)
|
417
|
+
add_assets([[group, assets]])
|
418
|
+
end
|
419
|
+
|
420
|
+
def remove_assets_from_group(group, assets)
|
421
|
+
remove_assets([[group, assets]])
|
422
|
+
end
|
423
|
+
|
424
|
+
def add_assets(list)
|
425
|
+
list.each do |elem|
|
426
|
+
if !elem.empty? && elem[1].is_a?(Array)
|
427
|
+
asset_group = elem[0].nil? ? nil : validate_instances(find_asset_group(elem[0]))
|
428
|
+
asset_ids = elem[1]
|
429
|
+
else
|
430
|
+
asset_group = nil
|
431
|
+
asset_ids = elem
|
432
|
+
end
|
433
|
+
assets = validate_instances(find_assets(asset_ids))
|
434
|
+
assets_to_add << assets.map { |asset| { asset_group: asset_group, asset: asset } }
|
435
|
+
end
|
436
|
+
self
|
437
|
+
end
|
438
|
+
|
439
|
+
def remove_assets(list)
|
440
|
+
list.each do |elem|
|
441
|
+
if !elem.empty? && elem[1].is_a?(Array)
|
442
|
+
asset_group = elem[0].nil? ? nil : validate_instances(find_asset_group(elem[0]))
|
443
|
+
asset_ids = elem[1]
|
444
|
+
else
|
445
|
+
asset_group = nil
|
446
|
+
asset_ids = elem
|
447
|
+
end
|
448
|
+
assets = validate_instances(find_assets(asset_ids))
|
449
|
+
assets_to_remove << assets.map { |asset| { asset_group: asset_group, asset: asset } }
|
450
|
+
end
|
451
|
+
self
|
452
|
+
end
|
453
|
+
|
454
|
+
private
|
455
|
+
|
456
|
+
def _handle_errors(step)
|
457
|
+
step.set_errors(errors_added)
|
458
|
+
_produce_error(errors_added) unless errors_added.empty?
|
459
|
+
end
|
460
|
+
|
461
|
+
def _produce_error(errors_added)
|
462
|
+
raise StandardError.new(message: errors_added.join("\n"))
|
463
|
+
end
|
464
|
+
|
465
|
+
def _set_remote_facts(facts)
|
466
|
+
Fact.where(id: facts.map(&:id).uniq.compact).update_all(is_remote?: true)
|
467
|
+
end
|
468
|
+
|
469
|
+
def _add_assets(step, asset_group_assets, with_operations = true)
|
470
|
+
modified_list = asset_group_assets.map do |o|
|
471
|
+
# If is nil, it will use the asset group from the step
|
472
|
+
o[:asset_group] = o[:asset_group] || step.asset_group
|
473
|
+
o
|
474
|
+
end
|
475
|
+
_instance_builder_for_import(AssetGroupsAsset, modified_list) do |instances|
|
476
|
+
_asset_group_operations('addAssets', step, instances) if with_operations
|
477
|
+
end
|
478
|
+
end
|
479
|
+
|
480
|
+
def _remove_assets(step, assets_to_remove, with_operations = true)
|
481
|
+
modified_list = assets_to_remove.map do |obj|
|
482
|
+
AssetGroupsAsset.where(
|
483
|
+
asset_group: obj[:asset_group] || step.asset_group,
|
484
|
+
asset: obj[:asset]
|
485
|
+
)
|
486
|
+
end
|
487
|
+
_instances_deletion(AssetGroupsAsset, modified_list) do |asset_group_assets|
|
488
|
+
_asset_group_operations('removeAssets', step, asset_group_assets) if with_operations
|
489
|
+
end
|
490
|
+
end
|
491
|
+
|
492
|
+
def _create_assets(step, assets, with_operations = true)
|
493
|
+
return unless assets
|
494
|
+
|
495
|
+
count = Asset.count + 1
|
496
|
+
assets = assets.each_with_index.map do |asset, barcode_index|
|
497
|
+
_build_barcode(asset, count + barcode_index)
|
498
|
+
asset
|
499
|
+
end
|
500
|
+
_instance_builder_for_import(Asset, assets) do |_instances|
|
501
|
+
_asset_operations('createAssets', step, assets) if with_operations
|
502
|
+
end
|
503
|
+
end
|
504
|
+
|
505
|
+
## TODO:
|
506
|
+
# Possibly it could be moved to Asset before_save callback
|
507
|
+
#
|
508
|
+
def _build_barcode(asset, num)
|
509
|
+
barcode_type = values_for_predicate(asset, 'barcodeType').first
|
510
|
+
|
511
|
+
return if barcode_type == 'NoBarcode'
|
512
|
+
|
513
|
+
barcode = values_for_predicate(asset, 'barcode').first
|
514
|
+
if barcode
|
515
|
+
asset.barcode = barcode
|
516
|
+
else
|
517
|
+
asset.build_barcode(num)
|
518
|
+
end
|
519
|
+
end
|
520
|
+
|
521
|
+
def _detach_assets(step, assets, with_operations = true)
|
522
|
+
operations = _asset_operations('deleteAssets', step, assets) if with_operations
|
523
|
+
_instances_deletion(Fact, assets.map(&:facts).flatten.compact)
|
524
|
+
_instances_deletion(AssetGroupsAsset, assets.map(&:asset_groups_assets).flatten.compact)
|
525
|
+
operations
|
526
|
+
end
|
527
|
+
|
528
|
+
def _create_asset_groups(step, asset_groups, with_operations = true)
|
529
|
+
return unless asset_groups
|
530
|
+
|
531
|
+
asset_groups.each_with_index do |asset_group, _index|
|
532
|
+
asset_group.update_attributes(
|
533
|
+
name: ExtractionTokenUtil.to_asset_group_name(wildcard_for_uuid(asset_group.uuid)),
|
534
|
+
activity_owner: step.activity
|
535
|
+
)
|
536
|
+
asset_group.save
|
537
|
+
end
|
538
|
+
_asset_group_building_operations('createAssetGroups', step, asset_groups) if with_operations
|
539
|
+
end
|
540
|
+
|
541
|
+
def _detach_asset_groups(step, asset_groups, with_operations = true)
|
542
|
+
if with_operations
|
543
|
+
operations = _asset_group_building_operations('deleteAssetGroups', step, asset_groups)
|
544
|
+
end
|
545
|
+
instances = asset_groups.flatten
|
546
|
+
ids_to_remove = instances.map(&:id).compact.uniq
|
547
|
+
|
548
|
+
if ids_to_remove && !ids_to_remove.empty?
|
549
|
+
AssetGroup.where(id: ids_to_remove).update_all(activity_owner_id: nil)
|
550
|
+
end
|
551
|
+
operations
|
552
|
+
end
|
553
|
+
|
554
|
+
def _create_facts(step, params_for_facts, with_operations = true)
|
555
|
+
_instance_builder_for_import(Fact, params_for_facts) do |facts|
|
556
|
+
_fact_operations('addFacts', step, facts) if with_operations
|
557
|
+
end
|
558
|
+
end
|
559
|
+
|
560
|
+
def _remove_facts(step, facts_to_remove, with_operations = true)
|
561
|
+
ids = []
|
562
|
+
modified_list = facts_to_remove.each_with_object([]) do |data, memo|
|
563
|
+
if data[:id]
|
564
|
+
ids.push(data[:id])
|
565
|
+
elsif data[:object].is_a? String
|
566
|
+
elems = Fact.where(asset: data[:asset], predicate: data[:predicate],
|
567
|
+
object: data[:object])
|
568
|
+
else
|
569
|
+
elems = Fact.where(asset: data[:asset], predicate: data[:predicate],
|
570
|
+
object_asset: data[:object_asset])
|
571
|
+
end
|
572
|
+
memo.concat(elems) if elems
|
573
|
+
end.concat(Fact.where(id: ids))
|
574
|
+
_instances_deletion(Fact, modified_list) do
|
575
|
+
_fact_operations('removeFacts', step, modified_list) if with_operations
|
576
|
+
end
|
577
|
+
end
|
578
|
+
|
579
|
+
def _asset_group_building_operations(action_type, step, asset_groups)
|
580
|
+
asset_groups.map do |asset_group|
|
581
|
+
Operation.new(action_type: action_type, step: step, object: asset_group.uuid)
|
582
|
+
end
|
583
|
+
end
|
584
|
+
|
585
|
+
def _asset_group_operations(action_type, step, asset_group_assets)
|
586
|
+
asset_group_assets.map do |asset_group_asset, _index|
|
587
|
+
Operation.new(action_type: action_type, step: step,
|
588
|
+
asset: asset_group_asset.asset, object: asset_group_asset.asset_group.uuid)
|
589
|
+
end
|
590
|
+
end
|
591
|
+
|
592
|
+
def _asset_operations(action_type, step, assets)
|
593
|
+
assets.map do |asset, _index|
|
594
|
+
# refer = (action_type == 'deleteAsset' ? nil : asset)
|
595
|
+
Operation.new(action_type: action_type, step: step, object: asset.uuid)
|
596
|
+
end
|
597
|
+
end
|
598
|
+
|
599
|
+
def listening_to_predicate?(predicate)
|
600
|
+
predicate == 'parent'
|
601
|
+
end
|
602
|
+
|
603
|
+
def _fact_operations(action_type, step, facts)
|
604
|
+
modified_assets = []
|
605
|
+
operations = facts.map do |fact|
|
606
|
+
modified_assets.push(fact.object_asset) if listening_to_predicate?(fact.predicate)
|
607
|
+
Operation.new(action_type: action_type, step: step,
|
608
|
+
asset: fact.asset, predicate: fact.predicate, object: fact.object,
|
609
|
+
object_asset: fact.object_asset)
|
610
|
+
end
|
611
|
+
modified_assets.flatten.compact.uniq.each(&:touch)
|
612
|
+
operations
|
613
|
+
end
|
614
|
+
|
615
|
+
def all_values_are_new_records(hash)
|
616
|
+
hash.values.all? do |value|
|
617
|
+
(value.respond_to?(:new_record?) && value.new_record?)
|
618
|
+
end
|
619
|
+
end
|
620
|
+
|
621
|
+
def _instance_builder_for_import(klass, params_list)
|
622
|
+
instances = params_list.map do |params_for_instance|
|
623
|
+
if params_for_instance.is_a?(klass)
|
624
|
+
params_for_instance if params_for_instance.new_record?
|
625
|
+
elsif all_values_are_new_records(params_for_instance) ||
|
626
|
+
!klass.exists?(params_for_instance)
|
627
|
+
klass.new(params_for_instance)
|
628
|
+
end
|
629
|
+
end.compact.uniq
|
630
|
+
instances.each do |instance|
|
631
|
+
instance.run_callbacks(:save) { false }
|
632
|
+
instance.run_callbacks(:create) { false }
|
633
|
+
end
|
634
|
+
return unless instances && !instances.empty?
|
635
|
+
|
636
|
+
klass.import(instances)
|
637
|
+
# import does not return the ids for the instances, so we need to reload
|
638
|
+
# again. Uuid is the only identificable attribute set
|
639
|
+
klass.synchronize(instances, [:uuid]) if klass == Asset
|
640
|
+
yield instances
|
641
|
+
end
|
642
|
+
|
643
|
+
def _instances_deletion(klass, instances)
|
644
|
+
operations = block_given? ? yield(instances) : instances
|
645
|
+
instances = instances.flatten
|
646
|
+
ids_to_remove = instances.map(&:id).compact.uniq
|
647
|
+
|
648
|
+
klass.where(id: ids_to_remove).delete_all if ids_to_remove && !ids_to_remove.empty?
|
649
|
+
operations
|
650
|
+
end
|
651
|
+
end
|
652
|
+
end
|
metadata
ADDED
@@ -0,0 +1,132 @@
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
2
|
+
name: extraction_metadata_changes
|
3
|
+
version: !ruby/object:Gem::Version
|
4
|
+
version: 0.0.1a
|
5
|
+
platform: ruby
|
6
|
+
authors:
|
7
|
+
- Eduardo Martin Rojo
|
8
|
+
autorequire:
|
9
|
+
bindir: bin
|
10
|
+
cert_chain: []
|
11
|
+
date: 2020-02-22 00:00:00.000000000 Z
|
12
|
+
dependencies:
|
13
|
+
- !ruby/object:Gem::Dependency
|
14
|
+
name: extraction_token_util
|
15
|
+
requirement: !ruby/object:Gem::Requirement
|
16
|
+
requirements:
|
17
|
+
- - "~>"
|
18
|
+
- !ruby/object:Gem::Version
|
19
|
+
version: 0.0.3a11
|
20
|
+
type: :runtime
|
21
|
+
prerelease: false
|
22
|
+
version_requirements: !ruby/object:Gem::Requirement
|
23
|
+
requirements:
|
24
|
+
- - "~>"
|
25
|
+
- !ruby/object:Gem::Version
|
26
|
+
version: 0.0.3a11
|
27
|
+
- !ruby/object:Gem::Dependency
|
28
|
+
name: google_hash
|
29
|
+
requirement: !ruby/object:Gem::Requirement
|
30
|
+
requirements:
|
31
|
+
- - "~>"
|
32
|
+
- !ruby/object:Gem::Version
|
33
|
+
version: '0.9'
|
34
|
+
type: :runtime
|
35
|
+
prerelease: false
|
36
|
+
version_requirements: !ruby/object:Gem::Requirement
|
37
|
+
requirements:
|
38
|
+
- - "~>"
|
39
|
+
- !ruby/object:Gem::Version
|
40
|
+
version: '0.9'
|
41
|
+
- !ruby/object:Gem::Dependency
|
42
|
+
name: factory_bot
|
43
|
+
requirement: !ruby/object:Gem::Requirement
|
44
|
+
requirements:
|
45
|
+
- - "~>"
|
46
|
+
- !ruby/object:Gem::Version
|
47
|
+
version: '5'
|
48
|
+
type: :development
|
49
|
+
prerelease: false
|
50
|
+
version_requirements: !ruby/object:Gem::Requirement
|
51
|
+
requirements:
|
52
|
+
- - "~>"
|
53
|
+
- !ruby/object:Gem::Version
|
54
|
+
version: '5'
|
55
|
+
- !ruby/object:Gem::Dependency
|
56
|
+
name: rspec
|
57
|
+
requirement: !ruby/object:Gem::Requirement
|
58
|
+
requirements:
|
59
|
+
- - "~>"
|
60
|
+
- !ruby/object:Gem::Version
|
61
|
+
version: '3'
|
62
|
+
type: :development
|
63
|
+
prerelease: false
|
64
|
+
version_requirements: !ruby/object:Gem::Requirement
|
65
|
+
requirements:
|
66
|
+
- - "~>"
|
67
|
+
- !ruby/object:Gem::Version
|
68
|
+
version: '3'
|
69
|
+
- !ruby/object:Gem::Dependency
|
70
|
+
name: rubocop
|
71
|
+
requirement: !ruby/object:Gem::Requirement
|
72
|
+
requirements:
|
73
|
+
- - "~>"
|
74
|
+
- !ruby/object:Gem::Version
|
75
|
+
version: '0.80'
|
76
|
+
type: :development
|
77
|
+
prerelease: false
|
78
|
+
version_requirements: !ruby/object:Gem::Requirement
|
79
|
+
requirements:
|
80
|
+
- - "~>"
|
81
|
+
- !ruby/object:Gem::Version
|
82
|
+
version: '0.80'
|
83
|
+
- !ruby/object:Gem::Dependency
|
84
|
+
name: rubocop-rspec
|
85
|
+
requirement: !ruby/object:Gem::Requirement
|
86
|
+
requirements:
|
87
|
+
- - "~>"
|
88
|
+
- !ruby/object:Gem::Version
|
89
|
+
version: '1.38'
|
90
|
+
type: :development
|
91
|
+
prerelease: false
|
92
|
+
version_requirements: !ruby/object:Gem::Requirement
|
93
|
+
requirements:
|
94
|
+
- - "~>"
|
95
|
+
- !ruby/object:Gem::Version
|
96
|
+
version: '1.38'
|
97
|
+
description:
|
98
|
+
email:
|
99
|
+
executables: []
|
100
|
+
extensions: []
|
101
|
+
extra_rdoc_files: []
|
102
|
+
files:
|
103
|
+
- LICENSE
|
104
|
+
- README.md
|
105
|
+
- lib/extraction_metadata_changes.rb
|
106
|
+
- lib/extraction_metadata_changes/disjoint_list.rb
|
107
|
+
- lib/extraction_metadata_changes/fact_changes.rb
|
108
|
+
homepage: https://rubygems.org/gems/extraction_metadata_changes
|
109
|
+
licenses:
|
110
|
+
- MIT
|
111
|
+
metadata: {}
|
112
|
+
post_install_message:
|
113
|
+
rdoc_options: []
|
114
|
+
require_paths:
|
115
|
+
- lib
|
116
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
117
|
+
requirements:
|
118
|
+
- - ">="
|
119
|
+
- !ruby/object:Gem::Version
|
120
|
+
version: '0'
|
121
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
122
|
+
requirements:
|
123
|
+
- - ">"
|
124
|
+
- !ruby/object:Gem::Version
|
125
|
+
version: 1.3.1
|
126
|
+
requirements: []
|
127
|
+
rubygems_version: 3.0.3
|
128
|
+
signing_key:
|
129
|
+
specification_version: 4
|
130
|
+
summary: Client interface tool that talks with the metadata service to store and apply
|
131
|
+
all metadata modifications in a single transaction
|
132
|
+
test_files: []
|