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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 954c6e2dd1011830b68f910538d55ec53dac556a2ba3a32980460084d4489573
4
- data.tar.gz: 3cea22766eae0f7c90451077fd2fe6e8e5a009e3183eca13793ab6e66d4e2340
3
+ metadata.gz: 4bb37c3774e0bfdc3bde166671d245f5928fd1a15a3320e3f6736d9100cc667c
4
+ data.tar.gz: 842260600fdc60be2fef3beebbc9d5b8418ba0f90c0f5d4bd97b7d2c3d760255
5
5
  SHA512:
6
- metadata.gz: c1d0da31992fda270ccd51a37896a871c7108d38ebd31b812b2d0bb6f77a1e587ad2f5adabb3f14b584cf65635587154ace4a6a5b76c6a52f6ad765bdad1e00e
7
- data.tar.gz: a25603e080fecb8edbc0d4e21c470d819a5e4900aee60253cdcff04e49e263fff89dc44ac908e696b2cdc0fb9527b8c680157e9a6f08f1852ed2eb0c559af465
6
+ metadata.gz: 8be8f0d5ec703707d5c7fc3ac2a836a91cfd4704d837a61aaa99191f033521cdb99a9268b37384e35b8300bd0cdf0f7f2b6b3de65a4f8d88ddcb409133d574c8
7
+ data.tar.gz: 0160566bfeb48fb811e6bb7d9dda0065c88a09040b13f35423ca4020296c7247e5cf70aead0d7e3dd1e482205051c831d21247a15b04e4657aecffd3a8ca9d45
@@ -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
@@ -1,11 +1,69 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Uc3DmpId
4
- class Uc3DmpIdDeleterError < StandardError; end
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
@@ -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 = _append_versions(p_key: dmp['dmp']['PK'], dmp: dmp, client: client, debug: debug)
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
@@ -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
@@ -1,11 +1,75 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Uc3DmpId
4
- class Uc3DmpIdUpdaterError < StandardError; end
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
@@ -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
- raise Uc3DmpIdValidatorError, MSG_BAD_JSON % { msg: e.message, trace: e.backtrace }
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 "Loading schema -- ::Uc3DmpId::Schemas::#{mode.to_s.downcase.capitalize}"
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 = "::Uc3DmpId::Schemas::#{mode.to_s.downcase.capitalize}".split('::').inject(Object) { |o,c| o.const_get c }
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
 
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Uc3DmpId
4
- VERSION = '0.0.26'
4
+ VERSION = '0.0.27'
5
5
  end
@@ -1,11 +1,116 @@
1
1
  # frozen_string_literal: true
2
2
 
3
+ require 'uc3-dmp-dynamo'
4
+
3
5
  module Uc3DmpId
4
- class Uc3DmpIdVersionerError < StandardError; end
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.26
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-06 00:00:00.000000000 Z
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