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.
- checksums.yaml +7 -0
- data/lib/dmp/dynamo_adapter.rb +310 -0
- 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: []
|