uc3-dmp-id 0.0.26 → 0.0.27

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 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