uc3-dmp-id 0.0.26 → 0.0.27
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/lib/uc3-dmp-id/creator.rb +5 -2
- data/lib/uc3-dmp-id/deleter.rb +59 -1
- data/lib/uc3-dmp-id/finder.rb +1 -39
- data/lib/uc3-dmp-id/helper.rb +47 -0
- data/lib/uc3-dmp-id/splicer.rb +150 -0
- data/lib/uc3-dmp-id/updater.rb +65 -1
- data/lib/uc3-dmp-id/validator.rb +6 -3
- data/lib/uc3-dmp-id/version.rb +1 -1
- data/lib/uc3-dmp-id/versioner.rb +106 -1
- data/lib/uc3-dmp-id.rb +8 -3
- metadata +3 -2
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 4bb37c3774e0bfdc3bde166671d245f5928fd1a15a3320e3f6736d9100cc667c
|
4
|
+
data.tar.gz: 842260600fdc60be2fef3beebbc9d5b8418ba0f90c0f5d4bd97b7d2c3d760255
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 8be8f0d5ec703707d5c7fc3ac2a836a91cfd4704d837a61aaa99191f033521cdb99a9268b37384e35b8300bd0cdf0f7f2b6b3de65a4f8d88ddcb409133d574c8
|
7
|
+
data.tar.gz: 0160566bfeb48fb811e6bb7d9dda0065c88a09040b13f35423ca4020296c7247e5cf70aead0d7e3dd1e482205051c831d21247a15b04e4657aecffd3a8ca9d45
|
data/lib/uc3-dmp-id/creator.rb
CHANGED
@@ -7,7 +7,6 @@ module Uc3DmpId
|
|
7
7
|
|
8
8
|
class Creator
|
9
9
|
MSG_NO_BASE_URL = 'No base URL found for DMP ID (e.g. `doi.org`)'
|
10
|
-
MSG_NO_PROVENANCE_OWNER = 'No provenance system and/or owner defined.'
|
11
10
|
MSG_NO_SHOULDER = 'No DOI shoulder found. (e.g. `10.12345/`)'
|
12
11
|
MSG_UNABLE_TO_MINT = 'Unable to mint a unique DMP ID.'
|
13
12
|
|
@@ -16,12 +15,16 @@ module Uc3DmpId
|
|
16
15
|
raise CreatorError, MSG_NO_SHOULDER if ENV['DMP_ID_SHOULDER'].nil?
|
17
16
|
raise CreatorError, MSG_NO_BASE_URL if ENV['DMP_ID_BASE_URL'].nil?
|
18
17
|
|
18
|
+
# Fail if the provenance is not defined
|
19
|
+
raise DeleterError, MSG_DMP_FORBIDDEN unless provenance.is_a?(Hash) && !provenance['PK'].nil?
|
20
|
+
|
19
21
|
# Validate the incoming JSON first
|
20
22
|
errs = Validator.validate(mode: 'author', json: Helper.parse_json(json: json))&.fetch('dmp', {})
|
21
23
|
raise CreatorError, errs.join(', ') if errs.is_a?(Array) && errs.any?
|
22
24
|
|
23
25
|
# Fail if the provenance or owner affiliation are not defined
|
24
26
|
raise CreatorError, MSG_NO_PROVENANCE_OWNER if provenance.nil? || owner_org.nil?
|
27
|
+
raise CreatorError, MSG_NO_OWNER_ORG unless owner_org.is_a?(String) && !owner_org.strip.empty?
|
25
28
|
|
26
29
|
# Try to find it first and Fail if found
|
27
30
|
result = Finder.by_json(json: json, debug: debug)
|
@@ -78,7 +81,7 @@ module Uc3DmpId
|
|
78
81
|
# We are creating, so this is always true
|
79
82
|
json['dmphub_updater_is_provenance'] = true
|
80
83
|
# Publish the change to the EventBridge
|
81
|
-
EventPublisher.publish(source: 'DmpCreator', dmp: json, debug: @debug)
|
84
|
+
# EventPublisher.publish(source: 'DmpCreator', dmp: json, debug: @debug)
|
82
85
|
true
|
83
86
|
end
|
84
87
|
end
|
data/lib/uc3-dmp-id/deleter.rb
CHANGED
@@ -1,11 +1,69 @@
|
|
1
1
|
# frozen_string_literal: true
|
2
2
|
|
3
3
|
module Uc3DmpId
|
4
|
-
class
|
4
|
+
class DeleterError < StandardError; end
|
5
5
|
|
6
|
+
# Utility to Tombstone DMP ID'a
|
6
7
|
class Deleter
|
7
8
|
class << self
|
9
|
+
# Delete/Tombstone a record in the table
|
10
|
+
# rubocop:disable Metrics/MethodLength, Metrics/AbcSize, Metrics/CyclomaticComplexity, Metrics/PerceivedComplexity
|
11
|
+
# -------------------------------------------------------------------------
|
12
|
+
def tombstone(provenance:, p_key:, debug: false)
|
13
|
+
raise DeleterError, MSG_DMP_INVALID_DMP_ID unless p_key.is_a?(String) && !p_key.strip.empty?
|
8
14
|
|
15
|
+
# Fail if the provenance is not defined
|
16
|
+
raise DeleterError, MSG_DMP_FORBIDDEN unless provenance.is_a?(Hash) && !provenance['PK'].nil?
|
17
|
+
|
18
|
+
# Fetch the latest version of the DMP ID by it's PK
|
19
|
+
client = Uc3DmpDynamo::Client.new(debug: debug)
|
20
|
+
dmp = Finder.by_pk(p_key: p_key, client: client, debug: debug)
|
21
|
+
raise DeleterError, MSG_DMP_NOT_FOUND unless dmp.is_a?(Hash) && !dmp['dmp'].nil?
|
22
|
+
|
23
|
+
# Only allow this if the provenance is the owner of the DMP!
|
24
|
+
raise DeleterError, MSG_DMP_FORBIDDEN if dmp['dmp']['dmphub_provenance_id'] != provenance['PK']
|
25
|
+
# Make sure they're not trying to update a historical copy of the DMP
|
26
|
+
raise DeleterError, MSG_DMP_NO_HISTORICALS if dmp['dmp']['SK'] != Helper::DMP_LATEST_VERSION
|
27
|
+
|
28
|
+
# Annotate the DMP ID
|
29
|
+
dmp['dmp']['SK'] = Helper::DMP_TOMBSTONE_VERSION
|
30
|
+
dmp['dmp']['dmphub_tombstoned_at'] = Time.now.iso8601
|
31
|
+
dmp['dmp']['title'] = "OBSOLETE: #{dmp['title']}"
|
32
|
+
puts "Tombstoning DMP #{p_key}" if debug
|
33
|
+
|
34
|
+
# Create the Tombstone version
|
35
|
+
resp = client.put_item(json: dmp, debug: debug)
|
36
|
+
raise DeleterError, MSG_DMP_NO_TOMBSTONE if resp.nil?
|
37
|
+
|
38
|
+
# Delete the Latest version
|
39
|
+
resp = client.delete_item(p_key: p_key, s_key: Helper::SK_DMP_PREFIX, debug: debug)
|
40
|
+
|
41
|
+
# TODO: We should do a check here to see if it was successful!
|
42
|
+
puts resp.inspect
|
43
|
+
|
44
|
+
# Notify EZID about the removal
|
45
|
+
_post_process(json: dmp)
|
46
|
+
dmp
|
47
|
+
rescue Aws::Errors::ServiceError => e
|
48
|
+
Responder.log_error(source: source, message: e.message,
|
49
|
+
details: ([@provenance] << e.backtrace).flatten)
|
50
|
+
{ status: 500, error: Messages::MSG_SERVER_ERROR }
|
51
|
+
end
|
52
|
+
# rubocop:enable Metrics/MethodLength, Metrics/AbcSize, Metrics/CyclomaticComplexity, Metrics/PerceivedComplexity
|
53
|
+
|
54
|
+
private
|
55
|
+
|
56
|
+
# Once the DMP has been tombstoned, we need to notify EZID
|
57
|
+
# -------------------------------------------------------------------------
|
58
|
+
def _post_process(json:)
|
59
|
+
return false unless json.is_a?(Hash)
|
60
|
+
|
61
|
+
# Indicate whether or not the updater is the provenance system
|
62
|
+
json['dmphub_updater_is_provenance'] = true
|
63
|
+
# Publish the change to the EventBridge
|
64
|
+
# EventPublisher.publish(source: 'DmpDeleter', dmp: json, debug: @debug)
|
65
|
+
true
|
66
|
+
end
|
9
67
|
end
|
10
68
|
end
|
11
69
|
end
|
data/lib/uc3-dmp-id/finder.rb
CHANGED
@@ -21,22 +21,6 @@ module Uc3DmpId
|
|
21
21
|
end
|
22
22
|
# rubocop:enable Metrics/MethodLength
|
23
23
|
|
24
|
-
# Find the DMP's versions
|
25
|
-
# -------------------------------------------------------------------------
|
26
|
-
def versions(p_key:, client: nil, debug: false)
|
27
|
-
raise FinderError, MSG_MISSING_PK if p_key.nil?
|
28
|
-
|
29
|
-
args = {
|
30
|
-
key_conditions: {
|
31
|
-
PK: { attribute_value_list: [Helper.append_pk_prefix(p_key: p_key)], comparison_operator: 'EQ' }
|
32
|
-
},
|
33
|
-
projection_expression: 'modified',
|
34
|
-
scan_index_forward: false
|
35
|
-
}
|
36
|
-
client = client.nil? ? Uc3DmpDynamo::Client.new(debug: debug) : client
|
37
|
-
client.query(args: args, debug: debug)
|
38
|
-
end
|
39
|
-
|
40
24
|
# Find a DMP based on the contents of the incoming JSON
|
41
25
|
# -------------------------------------------------------------------------
|
42
26
|
def by_json(json:, debug: false)
|
@@ -73,7 +57,7 @@ module Uc3DmpId
|
|
73
57
|
dmp = resp['dmp'].nil? ? JSON.parse({ dmp: resp }.to_json) : resp
|
74
58
|
return nil if dmp['dmp']['PK'].nil?
|
75
59
|
|
76
|
-
dmp =
|
60
|
+
dmp = Versioner.append_versions(p_key: dmp['dmp']['PK'], dmp: dmp, client: client, debug: debug)
|
77
61
|
Helper.cleanse_dmp_json(json: dmp)
|
78
62
|
end
|
79
63
|
|
@@ -119,28 +103,6 @@ module Uc3DmpId
|
|
119
103
|
by_pk(p_key: dmp['dmp']['PK'], s_key: dmp['dmp']['SK'])
|
120
104
|
end
|
121
105
|
# rubocop:enable Metrics/AbcSize
|
122
|
-
|
123
|
-
private
|
124
|
-
|
125
|
-
# Build the dmphub_versions array and attach it to the dmp
|
126
|
-
# rubocop:disable Metrics/AbcSize
|
127
|
-
def _append_versions(p_key:, dmp:, client: nil, debug: false)
|
128
|
-
return dmp if p_key.nil? || !dmp.is_a?(Hash) || dmp['dmp'].nil?
|
129
|
-
|
130
|
-
results = versions(p_key: p_key, client: client, debug: debug)
|
131
|
-
return dmp unless results.length > 1
|
132
|
-
|
133
|
-
versions = results.map do |ver|
|
134
|
-
next if ver['modified'].nil?
|
135
|
-
{
|
136
|
-
timestamp: ver['modified'],
|
137
|
-
url: "#{Helper.api_base_url}dmps/#{Helper.remove_pk_prefix(p_key: p_key)}?version=#{ver['modified']}"
|
138
|
-
}
|
139
|
-
end
|
140
|
-
dmp['dmp']['dmphub_versions'] = JSON.parse(versions.to_json)
|
141
|
-
dmp
|
142
|
-
end
|
143
|
-
# rubocop:enable Metrics/AbcSize
|
144
106
|
end
|
145
107
|
end
|
146
108
|
end
|
data/lib/uc3-dmp-id/helper.rb
CHANGED
@@ -110,6 +110,29 @@ module Uc3DmpId
|
|
110
110
|
json.is_a?(String) ? JSON.parse(json) : nil
|
111
111
|
end
|
112
112
|
|
113
|
+
# Compare the DMP IDs to see if they are the same
|
114
|
+
def eql?(dmp_a:, dmp_b:)
|
115
|
+
return dmp_a == dmp_b unless dmp_a.is_a?(Hash) && !dmp_a['dmp'].nil? && dmp_b.is_a?(Hash) && !dmp_b['dmp'].nil?
|
116
|
+
|
117
|
+
# return true if they're identical
|
118
|
+
return true if dmp_a == dmp_b
|
119
|
+
|
120
|
+
# If the PK do not match, then they are not equivalent!
|
121
|
+
return false unless dmp_a.is_a?(Hash) && dmp_a['dmp'].fetch('PK', '').start_with?(Helper::PK_DMP_PREFIX) &&
|
122
|
+
dmp_b.is_a?(Hash) && dmp_b['dmp'].fetch('PK', '').start_with?(Helper::PK_DMP_PREFIX) &&
|
123
|
+
dmp_a['dmp']['PK'] == dmp_b['dmp']['PK']
|
124
|
+
|
125
|
+
a = deep_copy_dmp(obj: dmp_a)
|
126
|
+
b = deep_copy_dmp(obj: dmp_b)
|
127
|
+
|
128
|
+
# ignore some of the attributes before comparing
|
129
|
+
%w[SK dmphub_modification_day dmphub_updated_at dmphub_created_at].each do |key|
|
130
|
+
a['dmp'].delete(key) unless a['dmp'][key].nil?
|
131
|
+
b['dmp'].delete(key) unless b['dmp'][key].nil?
|
132
|
+
end
|
133
|
+
a == b
|
134
|
+
end
|
135
|
+
|
113
136
|
# Add DMPHub specific fields to the DMP ID JSON
|
114
137
|
def annotate_dmp_json(provenance:, owner_org:, p_key:, json:)
|
115
138
|
json = parse_json(json: json)
|
@@ -166,6 +189,30 @@ module Uc3DmpId
|
|
166
189
|
cleansed.keys.any? ? cleansed : nil
|
167
190
|
end
|
168
191
|
# rubocop:enable Metrics/AbcSize, Metrics/CyclomaticComplexity, Metrics/PerceivedComplexity
|
192
|
+
|
193
|
+
# Ruby's clone/dup methods do not clone/dup the children, so we need to do it here
|
194
|
+
# --------------------------------------------------------------
|
195
|
+
# rubocop:disable Metrics/AbcSize
|
196
|
+
def deep_copy_dmp(obj:)
|
197
|
+
case obj.class.name
|
198
|
+
when 'Array'
|
199
|
+
obj.map { |item| deep_copy_dmp(obj: item) }
|
200
|
+
when 'Hash'
|
201
|
+
hash = obj.dup
|
202
|
+
hash.each_pair do |key, value|
|
203
|
+
if key.is_a?(::String) || key.is_a?(::Symbol)
|
204
|
+
hash[key] = deep_copy_dmp(obj: value)
|
205
|
+
else
|
206
|
+
hash.delete(key)
|
207
|
+
hash[deep_copy_dmp(obj: key)] = deep_copy_dmp(obj: value)
|
208
|
+
end
|
209
|
+
end
|
210
|
+
hash
|
211
|
+
else
|
212
|
+
obj.dup
|
213
|
+
end
|
214
|
+
end
|
215
|
+
# rubocop:enable Metrics/AbcSize
|
169
216
|
end
|
170
217
|
end
|
171
218
|
end
|
@@ -0,0 +1,150 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Uc3DmpId
|
4
|
+
class SplicerError < StandardError; end
|
5
|
+
|
6
|
+
class Splicer
|
7
|
+
class << self
|
8
|
+
# Splice changes from other systems onto the system of provenance's updated record
|
9
|
+
# --------------------------------------------------------------
|
10
|
+
# rubocop:disable Metrics/AbcSize, Metrics/MethodLength
|
11
|
+
# rubocop:disable Metrics/CyclomaticComplexity, Metrics/PerceivedComplexity
|
12
|
+
def splice_for_owner(owner:, updater:, base:, mods:, debug: false)
|
13
|
+
return base if owner.nil? || updater.nil? || mods.nil?
|
14
|
+
return mods if base.nil?
|
15
|
+
|
16
|
+
provenance_regex = /"dmphub_provenance_id":"#{Helper::PK_PROVENANCE_PREFIX}[a-zA-Z\-_]+"/
|
17
|
+
others = base.to_json.match(provenance_regex)
|
18
|
+
# Just return it as is if there are no mods by other systems
|
19
|
+
return mods if others.nil?
|
20
|
+
|
21
|
+
spliced = Helper.deep_copy_dmp(obj: base)
|
22
|
+
cloned_mods = Helper.deep_copy_dmp(obj: mods)
|
23
|
+
|
24
|
+
# ensure that the :project and :funding are defined
|
25
|
+
spliced['project'] = [{}] if spliced['project'].nil? || spliced['project'].empty?
|
26
|
+
spliced['project'].first['funding'] = [] if spliced['project'].first['funding'].nil?
|
27
|
+
# get all the new funding and retain other system's funding metadata
|
28
|
+
mod_fundings = cloned_mods.fetch('project', [{}]).first.fetch('funding', [])
|
29
|
+
other_fundings = spliced['project'].first['funding'].reject { |fund| fund['dmphub_provenance_id'].nil? }
|
30
|
+
# process funding (just attach all funding not owned by the system of provenance)
|
31
|
+
spliced['project'].first['funding'] = mod_fundings
|
32
|
+
spliced['project'].first['funding'] << other_fundings if other_fundings.any?
|
33
|
+
return spliced if cloned_mods['dmproadmap_related_identifiers'].nil?
|
34
|
+
|
35
|
+
# process related_identifiers (just attach all related identifiers not owned by the system of provenance)
|
36
|
+
spliced['dmproadmap_related_identifiers'] = [] if spliced['dmproadmap_related_identifiers'].nil?
|
37
|
+
mod_relateds = cloned_mods.fetch('dmproadmap_related_identifiers', [])
|
38
|
+
other_relateds = spliced['dmproadmap_related_identifiers'].reject { |id| id['dmphub_provenance_id'].nil? }
|
39
|
+
spliced['dmproadmap_related_identifiers'] = mod_relateds
|
40
|
+
spliced['dmproadmap_related_identifiers'] << other_relateds if other_relateds.any?
|
41
|
+
|
42
|
+
puts 'JSON after splicing in changes from provenance' if debug
|
43
|
+
puts spliced if debug
|
44
|
+
|
45
|
+
spliced
|
46
|
+
end
|
47
|
+
# rubocop:enable Metrics/AbcSize, Metrics/MethodLength
|
48
|
+
# rubocop:enable Metrics/CyclomaticComplexity, Metrics/PerceivedComplexity
|
49
|
+
|
50
|
+
# Splice changes from the other system onto the system of provenance and other system's changes
|
51
|
+
# --------------------------------------------------------------
|
52
|
+
# rubocop:disable Metrics/AbcSize
|
53
|
+
def splice_for_others(owner:, updater:, base:, mods:, debug: false)
|
54
|
+
return base if owner.nil? || updater.nil? || base.nil? || mods.nil?
|
55
|
+
|
56
|
+
spliced = Helper.deep_copy_dmp(obj: base)
|
57
|
+
base_funds = spliced.fetch('project', [{}]).first.fetch('funding', [])
|
58
|
+
base_relateds = spliced.fetch('dmproadmap_related_identifiers', [])
|
59
|
+
|
60
|
+
mod_funds = mods.fetch('project', [{}]).first.fetch('funding', [])
|
61
|
+
mod_relateds = mods.fetch('dmproadmap_related_identifiers', [])
|
62
|
+
|
63
|
+
# process funding
|
64
|
+
spliced['project'].first['funding'] = _update_funding(
|
65
|
+
updater: updater, base: base_funds, mods: mod_funds
|
66
|
+
)
|
67
|
+
return spliced if mod_relateds.empty?
|
68
|
+
|
69
|
+
# process related_identifiers
|
70
|
+
spliced['dmproadmap_related_identifiers'] = _update_related_identifiers(
|
71
|
+
updater: updater, base: base_relateds, mods: mod_relateds
|
72
|
+
)
|
73
|
+
|
74
|
+
puts 'JSON after splicing in changes from non-provenance' if debug
|
75
|
+
puts spliced if debug
|
76
|
+
|
77
|
+
spliced
|
78
|
+
end
|
79
|
+
# rubocop:enable Metrics/AbcSize
|
80
|
+
|
81
|
+
private
|
82
|
+
|
83
|
+
# These Splicing operations could probably be refined or genericized to traverse the Hash
|
84
|
+
# and apply to each object
|
85
|
+
|
86
|
+
# Splice funding changes
|
87
|
+
# --------------------------------------------------------------
|
88
|
+
# rubocop:disable Metrics/AbcSize, Metrics/MethodLength
|
89
|
+
# rubocop:disable Metrics/CyclomaticComplexity, Metrics/PerceivedComplexity
|
90
|
+
def _update_funding(updater:, base:, mods:)
|
91
|
+
return base if updater.nil? || mods.nil? || mods.empty?
|
92
|
+
|
93
|
+
spliced = Helper.deep_copy_dmp(obj: base)
|
94
|
+
mods.each do |funding|
|
95
|
+
# Ignore it if it has no status or grant id
|
96
|
+
next if funding['funding_status'].nil? && funding['grant_id'].nil?
|
97
|
+
|
98
|
+
# See if there is an existing funding record for the funder that's waiting on an update
|
99
|
+
spliced = [] if spliced.nil?
|
100
|
+
items = spliced.select do |orig|
|
101
|
+
!orig['funder_id'].nil? &&
|
102
|
+
orig['funder_id'] == funding['funder_id'] &&
|
103
|
+
%w[applied planned].include?(orig['funding_status'])
|
104
|
+
end
|
105
|
+
# Always grab the most current
|
106
|
+
items = items.sort { |a, b| b.fetch('dmphub_created_at', '') <=> a.fetch('dmphub_created_at', '') }
|
107
|
+
item = items.first
|
108
|
+
|
109
|
+
# Out with the old and in with the new
|
110
|
+
spliced.delete(item) unless item.nil?
|
111
|
+
# retain the original name
|
112
|
+
funding['name'] = item['name'] unless item.nil?
|
113
|
+
item = Helper.deep_copy_dmp(obj: funding)
|
114
|
+
|
115
|
+
item['funding_status'] == funding['funding_status'] unless funding['funding_status'].nil?
|
116
|
+
spliced << item if funding['grant_id'].nil?
|
117
|
+
next if funding['grant_id'].nil?
|
118
|
+
|
119
|
+
item['grant_id'] = funding['grant_id']
|
120
|
+
item['funding_status'] = funding['grant_id'].nil? ? 'rejected' : 'granted'
|
121
|
+
|
122
|
+
# Add the provenance to the entry
|
123
|
+
item['grant_id']['dmphub_provenance_id'] = updater
|
124
|
+
item['grant_id']['dmphub_created_at'] = Time.now.iso8601
|
125
|
+
spliced << item
|
126
|
+
end
|
127
|
+
spliced
|
128
|
+
end
|
129
|
+
# rubocop:enable Metrics/AbcSize, Metrics/MethodLength
|
130
|
+
# rubocop:enable Metrics/CyclomaticComplexity, Metrics/PerceivedComplexity
|
131
|
+
|
132
|
+
# Splice related identifier changes
|
133
|
+
# --------------------------------------------------------------
|
134
|
+
def _update_related_identifiers(updater:, base:, mods:)
|
135
|
+
return base if updater.nil? || mods.nil? || mods.empty?
|
136
|
+
|
137
|
+
# Remove the updater's existing related identifiers and replace with the new set
|
138
|
+
spliced = base.nil? ? [] : Helper.deep_copy_dmp(obj: base)
|
139
|
+
spliced = spliced.reject { |related| related['dmphub_provenance_id'] == updater }
|
140
|
+
# Add the provenance to the entry
|
141
|
+
updates = mods.nil? ? [] : Helper.deep_copy_dmp(obj: mods)
|
142
|
+
updates = updates.map do |related|
|
143
|
+
related['dmphub_provenance_id'] = updater
|
144
|
+
related
|
145
|
+
end
|
146
|
+
spliced + updates
|
147
|
+
end
|
148
|
+
end
|
149
|
+
end
|
150
|
+
end
|
data/lib/uc3-dmp-id/updater.rb
CHANGED
@@ -1,11 +1,75 @@
|
|
1
1
|
# frozen_string_literal: true
|
2
2
|
|
3
3
|
module Uc3DmpId
|
4
|
-
class
|
4
|
+
class UpdaterError < StandardError; end
|
5
5
|
|
6
6
|
class Updater
|
7
7
|
class << self
|
8
|
+
# Update a record in the table
|
9
|
+
# rubocop:disable Metrics/AbcSize, Metrics/MethodLength
|
10
|
+
# rubocop:disable Metrics/CyclomaticComplexity, Metrics/PerceivedComplexity
|
11
|
+
# -------------------------------------------------------------------------
|
12
|
+
def update(provenance:, p_key:, json: {})
|
13
|
+
raise DeleterError, MSG_DMP_INVALID_DMP_ID unless p_key.is_a?(String) && !p_key.strip.empty?
|
8
14
|
|
15
|
+
dmp = Helper.parse_json(json: json)
|
16
|
+
errs = _updateable?(provenance: provenance, p_key: p_key, dmp: dmp)
|
17
|
+
raise DeleterError, errs if errs.is_a?(Array) && errs.any?
|
18
|
+
|
19
|
+
# Add the DMPHub specific attributes
|
20
|
+
annotated = Helper.annotate_dmp(provenance: provenance, json: dmp, p_key: p_key)
|
21
|
+
|
22
|
+
# fetch the existing latest version of the DMP ID
|
23
|
+
client = Uc3DmpDynamo::Client.new(debug: debug)
|
24
|
+
existing = Finder.by_pk(p_key: p_key, client: client, debug: debug)
|
25
|
+
# Don't continue if nothing has changed!
|
26
|
+
raise DeleterError, MSG_NO_CHANGE if Helper.eql?(dmp_a: existing, dmp_b: annotated)
|
27
|
+
|
28
|
+
# Generate a new version of the DMP. This involves versioning the current latest version
|
29
|
+
new_version = versioner.new_version(p_key: p_key, dmp: json)
|
30
|
+
raise DeleterError, MSG_DMP_UNABLE_TO_VERSION if new_version.nil?
|
31
|
+
|
32
|
+
# Save the changes as the new latest version
|
33
|
+
resp = client.put_item(json: new_version, debug: debug)
|
34
|
+
raise DeleterError, MSG_DMP_UNABLE_TO_VERSION if resp.nil?
|
35
|
+
|
36
|
+
# Send the updates to EZID, notify the provenance and download the PDF if applicable
|
37
|
+
_post_process(json: dmp)
|
38
|
+
|
39
|
+
resp
|
40
|
+
end
|
41
|
+
# rubocop:enable Metrics/AbcSize, Metrics/MethodLength
|
42
|
+
# rubocop:enable Metrics/CyclomaticComplexity, Metrics/PerceivedComplexity
|
43
|
+
|
44
|
+
private
|
45
|
+
|
46
|
+
# Check if the DMP ID is updateable by the provenance
|
47
|
+
def _updateable?(provenance:, p_key:, json:)
|
48
|
+
# Validate the incoming JSON first
|
49
|
+
errs = Validator.validate(mode: 'author', json: json)&.fetch('dmp', {})
|
50
|
+
return errs.join(', ') if errs.is_a?(Array) && errs.any?
|
51
|
+
|
52
|
+
# Fail if the provenance is not defined
|
53
|
+
return [MSG_DMP_FORBIDDEN] unless provenance.is_a?(Hash) && !provenance['PK'].nil?
|
54
|
+
|
55
|
+
# Verify that the JSON is for the same DMP in the PK
|
56
|
+
dmp_id = json.fetch('dmp_id', {})
|
57
|
+
return [MSG_DMP_FORBIDDEN] unless Helper.dmp_id_to_pk(json: dmp_id) == p_key
|
58
|
+
# Make sure they're not trying to update a historical copy of the DMP
|
59
|
+
return [MSG_DMP_NO_HISTORICALS] if json['SK'] != Helper::DMP_LATEST_VERSION
|
60
|
+
end
|
61
|
+
|
62
|
+
# Once the DMP has been updated, we need to register it's DMP ID and download any PDF if applicable
|
63
|
+
# -------------------------------------------------------------------------
|
64
|
+
def _post_process(json:)
|
65
|
+
return false unless json.is_a?(Hash)
|
66
|
+
|
67
|
+
# Indicate whether or not the updater is the provenance system
|
68
|
+
json['dmphub_updater_is_provenance'] = @provenance['PK'] == json['dmphub_provenance_id']
|
69
|
+
# Publish the change to the EventBridge
|
70
|
+
# EventPublisher.publish(source: 'DmpUpdater', dmp: json, debug: @debug)
|
71
|
+
true
|
72
|
+
end
|
9
73
|
end
|
10
74
|
end
|
11
75
|
end
|
data/lib/uc3-dmp-id/validator.rb
CHANGED
@@ -34,7 +34,7 @@ module Uc3DmpId
|
|
34
34
|
errors = ([MSG_INVALID_JSON] << errors).flatten.compact.uniq unless errors.empty?
|
35
35
|
errors.map { |err| err.gsub(/in schema [a-z0-9-]+/, '').strip }
|
36
36
|
rescue JSON::Schema::ValidationError => e
|
37
|
-
|
37
|
+
["#{MSG_INVALID_JSON} - #{e.message}"]
|
38
38
|
end
|
39
39
|
|
40
40
|
# ------------------------------------------------------------------------------------
|
@@ -45,10 +45,13 @@ module Uc3DmpId
|
|
45
45
|
# ------------------------------------------------------------------------------------
|
46
46
|
def _load_schema(mode:)
|
47
47
|
|
48
|
-
puts "
|
48
|
+
puts "::Uc3DmpId::Schemas::Author defined? #{Object.const_defined?('::Uc3DmpId::Schemas::Author')}"
|
49
|
+
puts "Uc3DmpId::Schemas::Author defined? #{Object.const_defined?('Uc3DmpId::Schemas::Author')}"
|
50
|
+
puts "Schemas::Author defined? #{Object.const_defined?('Schemas::Author')}"
|
51
|
+
puts "Author defined? #{Object.const_defined?('Author')}"
|
49
52
|
|
50
53
|
# Instatiate the matching schema
|
51
|
-
schema = "
|
54
|
+
schema = "Schemas::#{mode.to_s.downcase.capitalize}".split('::').inject(Object) { |o,c| o.const_get c }
|
52
55
|
schema.respond_to?(:load) ? schema.load : nil
|
53
56
|
end
|
54
57
|
|
data/lib/uc3-dmp-id/version.rb
CHANGED
data/lib/uc3-dmp-id/versioner.rb
CHANGED
@@ -1,11 +1,116 @@
|
|
1
1
|
# frozen_string_literal: true
|
2
2
|
|
3
|
+
require 'uc3-dmp-dynamo'
|
4
|
+
|
3
5
|
module Uc3DmpId
|
4
|
-
class
|
6
|
+
class VersionerError < StandardError; end
|
5
7
|
|
6
8
|
class Versioner
|
7
9
|
class << self
|
8
10
|
|
11
|
+
# Find the DMP ID's versions
|
12
|
+
# -------------------------------------------------------------------------
|
13
|
+
def get_versions(p_key:, client: nil, debug: false)
|
14
|
+
return [] unless p_key.is_a?(String) && !p_key.strip.empty?
|
15
|
+
|
16
|
+
args = {
|
17
|
+
key_conditions: {
|
18
|
+
PK: { attribute_value_list: [Helper.append_pk_prefix(p_key: p_key)], comparison_operator: 'EQ' }
|
19
|
+
},
|
20
|
+
projection_expression: 'modified',
|
21
|
+
scan_index_forward: false
|
22
|
+
}
|
23
|
+
client = client.nil? ? Uc3DmpDynamo::Client.new(debug: debug) : client
|
24
|
+
client.query(args: args, debug: debug)
|
25
|
+
end
|
26
|
+
|
27
|
+
# Create a new version of the DMP. This involves:
|
28
|
+
# - Cloning the existing `VERSION#latest` and setting it's `SK` to `VERSION=yyyy-mm-ddThh:mm:ss+zz:zz`
|
29
|
+
# - Saving the new `VERSION=yyyy-mm-ddThh:mm:ss+zz:zz` item
|
30
|
+
# - Splicing in the current changes onto the existing `VERSION#latest` item
|
31
|
+
# - Returning the spliced `VERSION#latest` back to this method
|
32
|
+
def new_version(client:, provenance:, p_key:, client: nil, dmp:, latest_version: {})
|
33
|
+
return nil unless p_key.is_a?(String) && !p_key.strip.empty? && _versionable?(dmp: dmp)
|
34
|
+
|
35
|
+
client = Uc3DmpDynamo::Client.new(debug: debug) if client.nil?
|
36
|
+
latest_version = Finder.by_p_key(client: client, p_key: p_key) unless latest_version.is_a?(Hash) &&
|
37
|
+
!latest_version['PK'].nil?
|
38
|
+
|
39
|
+
# Only continue if there was an existing record and its the latest version
|
40
|
+
return nil unless latest['SK'] != Helper::DMP_LATEST_VERSION
|
41
|
+
|
42
|
+
owner = latest['dmphub_provenance_id']
|
43
|
+
updater = provenance['PK']
|
44
|
+
prior = _generate_version(client: client, latest_version: latest, owner: owner, updater: updater)
|
45
|
+
return nil if prior.nil?
|
46
|
+
|
47
|
+
args = { owner: owner, updater: updater, base: prior, mods: dmp, debug: debug }
|
48
|
+
puts 'DMP ID update prior to splicing changes' if debug
|
49
|
+
puts dmp
|
50
|
+
|
51
|
+
args = { owner: owner, updater: updater, base: prior, mods: dmp, debug: debug }
|
52
|
+
# If the system of provenance is making the change then just use the
|
53
|
+
# new version as the base and then splice in any mods made by others
|
54
|
+
# args = args.merge({ base: new_version, mods: original_version })
|
55
|
+
new_version = Splicer.splice_for_owner(args) if owner == updater
|
56
|
+
# Otherwise use the original version as the base and then update the
|
57
|
+
# metadata owned by the updater system
|
58
|
+
new_version = Splicer.splice_for_others(args) if new_version.nil?
|
59
|
+
new_version
|
60
|
+
end
|
61
|
+
|
62
|
+
# Build the :dmphub_versions array and attach it to the DMP JSON
|
63
|
+
# rubocop:disable Metrics/AbcSize
|
64
|
+
def append_versions(p_key:, dmp:, client: nil, debug: false)
|
65
|
+
json = Helper.parse_json(json: dmp)
|
66
|
+
return json unless p_key.is_a?(String) && !p_key.strip.empty? && json.is_a?(Hash) && !json['dmp'].nil?
|
67
|
+
|
68
|
+
results = get_versions(p_key: p_key, client: client, debug: debug)
|
69
|
+
return json unless results.length > 1
|
70
|
+
|
71
|
+
versions = results.map do |ver|
|
72
|
+
next if ver['modified'].nil?
|
73
|
+
{
|
74
|
+
timestamp: ver['modified'],
|
75
|
+
url: "#{Helper.api_base_url}dmps/#{Helper.remove_pk_prefix(p_key: p_key)}?version=#{ver['modified']}"
|
76
|
+
}
|
77
|
+
end
|
78
|
+
json['dmp']['dmphub_versions'] = JSON.parse(versions.to_json)
|
79
|
+
json
|
80
|
+
end
|
81
|
+
# rubocop:enable Metrics/AbcSize
|
82
|
+
|
83
|
+
private
|
84
|
+
|
85
|
+
# Determine whether the specified DMP metadata is versionable - returns boolean
|
86
|
+
def _versionable?(dmp:)
|
87
|
+
return false unless dmp.is_a?(Hash) && dmp['PK'].nil? && dmp['SK'].nil?
|
88
|
+
|
89
|
+
# It's versionable if it has a DMP ID
|
90
|
+
!dmp.fetch('dmp_id', {})['identifier'].nil?
|
91
|
+
end
|
92
|
+
|
93
|
+
# Generate a version
|
94
|
+
# rubocop:disable Metrics/AbcSize, Metrics/CyclomaticComplexity, Metrics/PerceivedComplexity
|
95
|
+
def _generate_version(client:, latest_version:, owner:, updater:, debug: false)
|
96
|
+
# Only create a version if the Updater is not the Owner OR the changes have happened on a different day
|
97
|
+
mod_time = Time.parse(latest_version.fetch('dmphub_updated_at', Time.now.iso8601))
|
98
|
+
now = Time.now
|
99
|
+
return latest_version if mod_time.nil? || !(now - mod_time).is_a?(Float)
|
100
|
+
|
101
|
+
same_hour = (now - mod_time).round <= 3600
|
102
|
+
return latest_version if owner != updater || (owner == updater && same_hour)
|
103
|
+
|
104
|
+
latest_version['SK'] = "#{Helper::SK_DMP_PREFIX}#{latest_version['dmphub_updated_at'] || Time.now.iso8601}"
|
105
|
+
|
106
|
+
# Create the prior version record
|
107
|
+
resp = client.put_item(json: latest_version, debug: debug)
|
108
|
+
return nil if resp.nil?
|
109
|
+
|
110
|
+
puts "Created new version #{latest_version['PK']} - #{latest_version['SK']}" if debug
|
111
|
+
latest_version
|
112
|
+
end
|
113
|
+
# rubocop:enable Metrics/AbcSize, Metrics/CyclomaticComplexity, Metrics/PerceivedComplexity
|
9
114
|
end
|
10
115
|
end
|
11
116
|
end
|
data/lib/uc3-dmp-id.rb
CHANGED
@@ -8,20 +8,25 @@ require 'uc3-dmp-id/creator'
|
|
8
8
|
require 'uc3-dmp-id/deleter'
|
9
9
|
require 'uc3-dmp-id/finder'
|
10
10
|
require 'uc3-dmp-id/helper'
|
11
|
+
require 'uc3-dmp-id/splicer'
|
11
12
|
require 'uc3-dmp-id/updater'
|
12
13
|
require 'uc3-dmp-id/validator'
|
13
14
|
require 'uc3-dmp-id/versioner'
|
15
|
+
|
14
16
|
require 'uc3-dmp-id/schemas/amend'
|
15
17
|
require 'uc3-dmp-id/schemas/author'
|
16
18
|
|
17
19
|
module Uc3DmpId
|
18
20
|
MSG_DMP_EXISTS = 'DMP already exists. Try :update instead.'
|
19
|
-
MSG_DMP_UNKNOWN = 'DMP does not exist. Try :create instead.'
|
20
|
-
MSG_DMP_NOT_FOUND = 'DMP does not exist.'
|
21
21
|
MSG_DMP_FORBIDDEN = 'You do not have permission.'
|
22
|
-
MSG_DMP_NO_DMP_ID = 'A DMP ID could not be registered at this time.'
|
23
22
|
MSG_DMP_INVALID_DMP_ID = 'Invalid DMP ID format.'
|
23
|
+
MSG_DMP_NO_DMP_ID = 'A DMP ID could not be registered at this time.'
|
24
24
|
MSG_DMP_NO_HISTORICALS = 'You cannot modify a historical version of the DMP.'
|
25
|
+
MSG_NO_OWNER_ORG = 'Could not determine ownership of the DMP ID.'
|
26
|
+
MSG_DMP_NO_TOMBSTONE = 'Unable to tombstone the DMP ID at this time.'
|
27
|
+
MSG_DMP_NO_UPDATE = 'Unable to update the DMP ID at this time.'
|
28
|
+
MSG_DMP_NOT_FOUND = 'DMP does not exist.'
|
25
29
|
MSG_DMP_UNABLE_TO_VERSION = 'Unable to version this DMP.'
|
30
|
+
MSG_DMP_UNKNOWN = 'DMP does not exist. Try :create instead.'
|
26
31
|
end
|
27
32
|
# rubocop:enable Naming/FileName
|
metadata
CHANGED
@@ -1,14 +1,14 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: uc3-dmp-id
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: 0.0.
|
4
|
+
version: 0.0.27
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- Brian Riley
|
8
8
|
autorequire:
|
9
9
|
bindir: bin
|
10
10
|
cert_chain: []
|
11
|
-
date: 2023-06-
|
11
|
+
date: 2023-06-07 00:00:00.000000000 Z
|
12
12
|
dependencies:
|
13
13
|
- !ruby/object:Gem::Dependency
|
14
14
|
name: json
|
@@ -123,6 +123,7 @@ files:
|
|
123
123
|
- lib/uc3-dmp-id/helper.rb
|
124
124
|
- lib/uc3-dmp-id/schemas/amend.rb
|
125
125
|
- lib/uc3-dmp-id/schemas/author.rb
|
126
|
+
- lib/uc3-dmp-id/splicer.rb
|
126
127
|
- lib/uc3-dmp-id/updater.rb
|
127
128
|
- lib/uc3-dmp-id/validator.rb
|
128
129
|
- lib/uc3-dmp-id/version.rb
|