dmp-dynamo_adapter 0.1.0

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.
Files changed (3) hide show
  1. checksums.yaml +7 -0
  2. data/lib/dmp/dynamo_adapter.rb +310 -0
  3. metadata +86 -0
checksums.yaml ADDED
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA256:
3
+ metadata.gz: d17eb8403b1a660fc3fbd2c2e448378bd62ecbd128773ff9a3a45f47610ad639
4
+ data.tar.gz: 7ed274dc112c5995d97cb72749934d32db3ed58fd632505c3a7c5cfea421957f
5
+ SHA512:
6
+ metadata.gz: 88facef86a8b0111dbeb2b9cab72aa633de24d8a002b163dae8bd74f4511570d14251c7399c9b63f15a92eb620f655186ed61a25a6f3bfe333a513b8d220ba2f
7
+ data.tar.gz: 9281c7617218aa3565792e405942ed87f5ed8b73ce7063060cbb645018f5e9fa4089ef0dbab1c2c73034208dd66cb3e615bd35df13583e0a573a1ed34b80e425
@@ -0,0 +1,310 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'json'
4
+ require 'uc3-ssm'
5
+ require 'aws-sdk-dynamodb'
6
+
7
+ require 'dmp/dmp_id_handler'
8
+ require 'dmp/metadata_handler'
9
+
10
+ module Dmp
11
+ # DMP adapter for an AWS DynamoDB Table
12
+ # rubocop:disable Metrics/ClassLength
13
+ class DynamoAdapter
14
+ MSG_DEFAULT = 'Unable to process your request.'
15
+ MSG_EXISTS = 'DMP already exists. Try :update instead.'
16
+ MSG_NOT_FOUND = 'DMP does not exist.'
17
+ MSG_FORBIDDEN = 'You cannot update the DMP.'
18
+ MSG_NO_DMP_ID = 'A DMP ID could not be registered at this time.'
19
+ MSG_UNKNOWN = 'DMP does not exist. Try :create instead.'
20
+ MSG_NO_HISTORICALS = 'You cannot modify a historical version of the DMP.'
21
+
22
+ # Initialize an instance by setting the provenance and connecting to the DB
23
+ def initialize(provenance:, debug: false)
24
+ @provenance = Dmp::MetadataHandler.append_pk_prefix(provenance: provenance)
25
+ @debug_mode = debug
26
+
27
+ @client = Aws::DynamoDB::Client.new(
28
+ region: ENV['AWS_REGION']
29
+ )
30
+ end
31
+
32
+ # Fetch the DMPs for the provenance
33
+ # rubocop:disable Metrics/MethodLength
34
+ def dmps_for_provenance
35
+ return { status: 404, error: MSG_NOT_FOUND } if @provenance.nil?
36
+
37
+ response = @client.query(
38
+ {
39
+ table_name: ENV['AWS_DYNAMO_TABLE_NAME'],
40
+ key_conditions: {
41
+ PK: {
42
+ attribute_value_list: ["PROVENANCE##{@provenance}"],
43
+ comparison_operator: 'EQ'
44
+ },
45
+ SK: {
46
+ attribute_value_list: ['DMPS'],
47
+ comparison_operator: 'EQ'
48
+ }
49
+ }
50
+ }
51
+ )
52
+ { status: 200, items: response.items.map(&:item).compact.uniq }
53
+ rescue Aws::Errors::ServiceError
54
+ { status: 500, error: MSG_DEFAULT }
55
+ end
56
+ # rubocop:enable Metrics/MethodLength
57
+
58
+ # Find the DMP by its PK and SK
59
+ def find_by_pk(p_key:, s_key: Dmp::MetadataHandler::LATEST_VERSION)
60
+ return { status: 404, error: MSG_NOT_FOUND } if p_key.nil?
61
+
62
+ response = @client.get_item(
63
+ {
64
+ table_name: ENV['AWS_DYNAMO_TABLE_NAME'],
65
+ key: { PK: p_key, SK: s_key },
66
+ consistent_read: false,
67
+ return_consumed_capacity: @debug_mode ? 'TOTAL' : 'NONE'
68
+ }
69
+ )
70
+ return { status: 404, error: MSG_NOT_FOUND } if response.items.empty?
71
+
72
+ { status: 200, items: response.items.map(&:item).compact.uniq }
73
+ rescue Aws::Errors::ServiceError
74
+ { status: 500, error: MSG_DEFAULT }
75
+ end
76
+
77
+ # Find a DMP based on the contents of the incoming JSON
78
+ # rubocop:disable Metrics/AbcSize, Metrics/CyclomaticComplexity, Metrics/PerceivedComplexity
79
+ def find_by_json(json:)
80
+ return { status: 404, error: MSG_NOT_FOUND } if json.nil? ||
81
+ (json['PK'].nil? && json['dmp_id'].nil?)
82
+
83
+ pk = json['PK']
84
+ # Translate the incoming :dmp_id into a PK
85
+ pk = pk_from_dmp_id(json: json.fetch('dmp_id', {})) if pk.nil?
86
+
87
+ # find_by_PK
88
+ response = find_by_pk(p_key: pk, s_key: json['SK']) unless pk.nil?
89
+ return response if response[:status] == 500
90
+ return response unless response[:items].nil? || response[:items].empty?
91
+
92
+ # find_by_dmphub_provenance_id -> if no PK and no dmp_id result
93
+ find_by_dmphub_provenance_identifier(json: json)
94
+ end
95
+ # rubocop:enable Metrics/AbcSize, Metrics/CyclomaticComplexity, Metrics/PerceivedComplexity
96
+
97
+ # Add a record to the table
98
+ # rubocop:disable Metrics/MethodLength, Metrics/CyclomaticComplexity
99
+ def create(json: {})
100
+ json = prepare_json(json: json)
101
+ return { status: 400, error: MSG_DEFAULT } if json.nil? || @provenance.nil?
102
+
103
+ # Try to find it first
104
+ result = find_by_json(json: json)
105
+ return { status: 500, error: MSG_DEFAULT } if result[:status] == 500
106
+ # Abort if found
107
+ return { status: 400, error: MSG_EXISTS } if result[:items].any?
108
+
109
+ # allocate a DMP ID
110
+ p_key = preregister_dmp_id
111
+ return { status: 500, error: MSG_NO_DMP_ID } if p_key.nil?
112
+
113
+ # Add the DMPHub specific attributes and then save
114
+ json = Dmp::MetadataHandler.annotate_json(provenance: @provenance, json: json, p_key: p_key)
115
+ response = @client.put_item(
116
+ {
117
+ table_name: ENV['AWS_DYNAMO_TABLE_NAME'],
118
+ item: json,
119
+ return_consumed_capacity: @debug_mode ? 'TOTAL' : 'NONE'
120
+ }
121
+ )
122
+ { status: 201, items: response.items.map(&:item).compact.uniq }
123
+ rescue Aws::DynamoDB::Errors::DuplicateItemException
124
+ { status: 405, error: MSG_EXISTS }
125
+ rescue Aws::Errors::ServiceError
126
+ { status: 500, error: MSG_DEFAULT }
127
+ end
128
+ # rubocop:enable Metrics/MethodLength, Metrics/CyclomaticComplexity
129
+
130
+ # Update a record in the table
131
+ # rubocop:disable Metrics/AbcSize, Metrics/MethodLength, Metrics/CyclomaticComplexity, Metrics/PerceivedComplexity
132
+ def update(p_key:, json: {})
133
+ json = prepare_json(json: json)
134
+ return { status: 400, error: MSG_DEFAULT } if json.nil? || p_key.nil? || @provenance.nil?
135
+
136
+ # Verify that the JSON is for the same DMP in the PK
137
+ dmp_id = json.fetch('dmp_id', {})
138
+ return { status: 403, error: MSG_FORBIDDEN } unless Dmp::DmpIdHandler.dmp_id_to_pk(json: dmp_id) == p_key
139
+
140
+ # Try to find it first
141
+ result = find_by_json(json: json)
142
+ return { status: 500, error: MSG_DEFAULT } if result[:status] == 500
143
+
144
+ dmp = result[:items].first&.item
145
+ return { status: 404, error: MSG_NOT_FOUND } if dmp.nil?
146
+ # Only allow this if the provenance is the owner of the DMP!
147
+ return { status: 403, error: MSG_FORBIDDEN } unless dmp['dmphub_provenance_id'] == @provenance
148
+ # Make sure they're not trying to update a historical copy of the DMP
149
+ return { status: 405, error: MSG_NO_HISTORICALS } if dmp['SK'] != Dmp::MetadataHandler::LATEST_VERSION
150
+
151
+ # version the old :latest
152
+ version_result = version_it(dmp: dmp)
153
+ return version_result if version_result[:status] != 200
154
+
155
+ # Add the DMPHub specific attributes and then save it
156
+ json = Dmp::MetadataHandler.annotate_json(provenance: @provenance, json: json, p_key: p_key)
157
+
158
+ p "BEFORE:"
159
+ pp json
160
+ p '==================================='
161
+ p ''
162
+
163
+ json = splice_json(original_version: version_result[:items].first&.item, new_version: json)
164
+
165
+ p ''
166
+ p "AFTER:"
167
+ pp json
168
+
169
+ response = @client.put_item(
170
+ {
171
+ table_name: ENV['AWS_DYNAMO_TABLE_NAME'],
172
+ item: json,
173
+ return_consumed_capacity: @debug_mode ? 'TOTAL' : 'NONE'
174
+ }
175
+ )
176
+
177
+ # Update the provenance keys!
178
+ # Update the ancillary keys for orcids, affiliations, provenance
179
+
180
+ { status: 200, items: response.items.map(&:item).compact.uniq }
181
+ rescue Aws::DynamoDB::Errors::DuplicateItemException
182
+ { status: 405, error: MSG_EXISTS }
183
+ rescue Aws::Errors::ServiceError
184
+ { status: 500, error: MSG_DEFAULT }
185
+ end
186
+ # rubocop:enable Metrics/AbcSize, Metrics/MethodLength, Metrics/CyclomaticComplexity, Metrics/PerceivedComplexity
187
+
188
+ # Delete/Tombstone a record in the table
189
+ # rubocop:disable Metrics/MethodLength, Metrics/AbcSize, Metrics/CyclomaticComplexity, Metrics/PerceivedComplexity
190
+ def delete(p_key:, json: {})
191
+ json = prepare_json(json: json)
192
+ return { status: 400, error: MSG_DEFAULT } if json.nil? || p_key.nil? || @provenance.nil?
193
+
194
+ # Verify that the JSON is for the same DMP in the PK
195
+ dmp_id = json.fetch('dmp_id', {})
196
+ return { status: 403, error: MSG_FORBIDDEN } unless Dmp::DmpIdHandler.dmp_id_to_pk(json: dmp_id) == p_key
197
+
198
+ # Try to find it first
199
+ result = find_by_json(json: json)
200
+ return { status: 500, error: MSG_DEFAULT } if result[:status] == 500
201
+ # Abort if NOT found
202
+ return { status: 404, error: MSG_NOT_FOUND } unless result[:status] == 200 && result.fetch(:items, []).any?
203
+
204
+ dmp = result[:items].first&.item
205
+ return { status: 404, error: MSG_NOT_FOUND } if dmp.nil?
206
+ # Only allow this if the provenance is the owner of the DMP!
207
+ return { status: 403, error: MSG_FORBIDDEN } unless dmp['dmphub_provenance_id'] == @provenance
208
+ # Make sure they're not trying to update a historical copy of the DMP
209
+ return { status: 405, error: MSG_NO_HISTORICALS } if dmp['SK'] != Dmp::MetadataHandler::LATEST_VERSION
210
+
211
+ response = @client.update_item(
212
+ {
213
+ table_name: ENV['AWS_DYNAMO_TABLE_NAME'],
214
+ key: {
215
+ PK: dmp['PK'],
216
+ SK: Dmp::MetadataHandler::LATEST_VERSION
217
+ },
218
+ update_expression: 'SET SK = :sk, dmphub_deleted_at = :deletion_date',
219
+ expression_attribute_values: {
220
+ sk: Dmp::MetadataHandler::TOMBSTONE_VERSION,
221
+ deletion_date: Time.now.iso8601
222
+ },
223
+ return_consumed_capacity: @debug_mode ? 'TOTAL' : 'NONE',
224
+ return_values: 'ALL_NEW'
225
+ }
226
+ )
227
+ { status: 200, items: response.items.map(&:item).compact.uniq }
228
+ rescue Aws::Errors::ServiceError
229
+ { status: 500, error: MSG_DEFAULT }
230
+ end
231
+ # rubocop:enable Metrics/MethodLength, Metrics/AbcSize, Metrics/CyclomaticComplexity, Metrics/PerceivedComplexity
232
+
233
+ private
234
+
235
+ attr_accessor :provenance
236
+ attr_accessor :debug_mode
237
+ attr_accessor :client
238
+
239
+ # Attempt to find the DMP item by its 'is_metadata_for' :dmproadmap_related_identifier
240
+ # rubocop:disable Metrics/MethodLength, Metrics/AbcSize
241
+ def find_by_dmphub_provenance_identifier(json:)
242
+ return { status: 400, error: MSG_DEFAULT } if json.nil? || json.fetch('dmp_id', {})['identifier'].nil?
243
+
244
+ response = @client.query(
245
+ {
246
+ table_name: ENV['AWS_DYNAMO_TABLE_NAME'],
247
+ index_name: 'dmphub_provenance_identifier_gsi',
248
+ key_conditions: {
249
+ dmphub_provenance_identifier: {
250
+ attribute_value_list: [json['dmp_id']['identifier']],
251
+ comparison_operator: 'EQ'
252
+ }
253
+ },
254
+ filter_expression: 'SK = :version',
255
+ expression_attribute_values: {
256
+ ':SK': Dmp::MetadataHandler::LATEST_VERSION
257
+ },
258
+ return_consumed_capacity: @debug_mode ? 'TOTAL' : 'NONE'
259
+ }
260
+ )
261
+ return { status: 404, error: MSG_NOT_FOUND } if response.nil? || response.items.empty?
262
+
263
+ # If we got a hit, fetch the DMP and return it.
264
+ find_by_pk(p_key: response.items.first.item[:PK])
265
+ rescue Aws::Errors::ServiceError
266
+ { status: 500, error: MSG_DEFAULT }
267
+ end
268
+ # rubocop:enable Metrics/MethodLength, Metrics/AbcSize
269
+
270
+ # Convert the latest version into a historical version
271
+ # rubocop:disable Metrics/MethodLength
272
+ def version_it(dmp:)
273
+ return { status: 400, error: MSG_DEFAULT } if dmp.nil? || dmp['PK'].nil? ||
274
+ !dmp['PK'].start_with?(Dmp::MetadataHandler::PK_DMP_PREFIX)
275
+ return { status: 403, error: MSG_NO_HISTORICALS } if dmp['SK'] != Dmp::MetadataHandler::LATEST_VERSION
276
+
277
+ response = @client.update_item(
278
+ {
279
+ table_name: ENV['AWS_DYNAMO_TABLE_NAME'],
280
+ key: {
281
+ PK: dmp['PK'],
282
+ SK: Dmp::MetadataHandler::LATEST_VERSION
283
+ },
284
+ update_expression: 'SET SK = :sk',
285
+ expression_attribute_values: {
286
+ sk: "#{Dmp::MetadataHandler::SK_PREFIX}#{dmp['dmphub_updated_at'] || Time.now.iso8601}"
287
+ },
288
+ return_consumed_capacity: @debug_mode ? 'TOTAL' : 'NONE',
289
+ return_values: 'NONE'
290
+ }
291
+ )
292
+ return { status: 404, error: MSG_NOT_FOUND } if response.nil? || response.items.empty?
293
+
294
+ { status: 200, items: response.items.map(&:item).compact.uniq }
295
+ rescue Aws::Errors::ServiceError
296
+ { status: 500, error: MSG_DEFAULT }
297
+ end
298
+ # rubocop:enable Metrics/MethodLength
299
+
300
+ # Parse the incoming JSON if necessary or return as is if it's already a Hash
301
+ def prepare_json(json:)
302
+ return json if json.is_a?(Hash)
303
+
304
+ json.is_a?(String) ? JSON.parse(json) : nil
305
+ rescue JSON::ParserError
306
+ nil
307
+ end
308
+ end
309
+ # rubocop:enable Metrics/ClassLength
310
+ end
metadata ADDED
@@ -0,0 +1,86 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: dmp-dynamo_adapter
3
+ version: !ruby/object:Gem::Version
4
+ version: 0.1.0
5
+ platform: ruby
6
+ authors:
7
+ - briri
8
+ autorequire:
9
+ bindir: bin
10
+ cert_chain: []
11
+ date: 2022-07-15 00:00:00.000000000 Z
12
+ dependencies:
13
+ - !ruby/object:Gem::Dependency
14
+ name: aws-sdk-dynamodb
15
+ requirement: !ruby/object:Gem::Requirement
16
+ requirements:
17
+ - - "~>"
18
+ - !ruby/object:Gem::Version
19
+ version: '1.74'
20
+ type: :runtime
21
+ prerelease: false
22
+ version_requirements: !ruby/object:Gem::Requirement
23
+ requirements:
24
+ - - "~>"
25
+ - !ruby/object:Gem::Version
26
+ version: '1.74'
27
+ - !ruby/object:Gem::Dependency
28
+ name: rspec
29
+ requirement: !ruby/object:Gem::Requirement
30
+ requirements:
31
+ - - "~>"
32
+ - !ruby/object:Gem::Version
33
+ version: '3.11'
34
+ type: :development
35
+ prerelease: false
36
+ version_requirements: !ruby/object:Gem::Requirement
37
+ requirements:
38
+ - - "~>"
39
+ - !ruby/object:Gem::Version
40
+ version: '3.11'
41
+ - !ruby/object:Gem::Dependency
42
+ name: rubocop
43
+ requirement: !ruby/object:Gem::Requirement
44
+ requirements:
45
+ - - "~>"
46
+ - !ruby/object:Gem::Version
47
+ version: '1.29'
48
+ type: :development
49
+ prerelease: false
50
+ version_requirements: !ruby/object:Gem::Requirement
51
+ requirements:
52
+ - - "~>"
53
+ - !ruby/object:Gem::Version
54
+ version: '1.29'
55
+ description:
56
+ email: briley@ucop.edu
57
+ executables: []
58
+ extensions: []
59
+ extra_rdoc_files: []
60
+ files:
61
+ - lib/dmp/dynamo_adapter.rb
62
+ homepage: https://github.com/CDLUC3/dmphub-v2/tree/main/gems/dmp-dynamo_adapter
63
+ licenses:
64
+ - MIT
65
+ metadata:
66
+ source_code_uri: https://github.com/CDLUC3/dmphub-v2/tree/main/gems/dmp-dynamo_adapter
67
+ post_install_message:
68
+ rdoc_options: []
69
+ require_paths:
70
+ - lib
71
+ required_ruby_version: !ruby/object:Gem::Requirement
72
+ requirements:
73
+ - - "~>"
74
+ - !ruby/object:Gem::Version
75
+ version: '2.7'
76
+ required_rubygems_version: !ruby/object:Gem::Requirement
77
+ requirements:
78
+ - - ">="
79
+ - !ruby/object:Gem::Version
80
+ version: '0'
81
+ requirements: []
82
+ rubygems_version: 3.1.6
83
+ signing_key:
84
+ specification_version: 4
85
+ summary: DMP adapter for an AWS DynamoDB Table
86
+ test_files: []