dmp-dynamo_adapter 0.1.0

Sign up to get free protection for your applications and to get access to all the features.
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: []