@dmptool/utils 1.0.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.
- package/LICENSE +21 -0
- package/README.md +788 -0
- package/dist/cloudFormation.d.ts +8 -0
- package/dist/cloudFormation.js +54 -0
- package/dist/dynamo.d.ts +105 -0
- package/dist/dynamo.js +651 -0
- package/dist/eventBridge.d.ts +13 -0
- package/dist/eventBridge.js +48 -0
- package/dist/general.d.ts +56 -0
- package/dist/general.js +142 -0
- package/dist/index.d.ts +10 -0
- package/dist/index.js +27 -0
- package/dist/logger.d.ts +17 -0
- package/dist/logger.js +36 -0
- package/dist/maDMP.d.ts +41 -0
- package/dist/maDMP.js +982 -0
- package/dist/maDMPTypes.d.ts +273 -0
- package/dist/maDMPTypes.js +12 -0
- package/dist/rds.d.ts +11 -0
- package/dist/rds.js +108 -0
- package/dist/s3.d.ts +44 -0
- package/dist/s3.js +98 -0
- package/dist/ssm.d.ts +8 -0
- package/dist/ssm.js +33 -0
- package/package.json +66 -0
package/dist/dynamo.js
ADDED
|
@@ -0,0 +1,651 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __rest = (this && this.__rest) || function (s, e) {
|
|
3
|
+
var t = {};
|
|
4
|
+
for (var p in s) if (Object.prototype.hasOwnProperty.call(s, p) && e.indexOf(p) < 0)
|
|
5
|
+
t[p] = s[p];
|
|
6
|
+
if (s != null && typeof Object.getOwnPropertySymbols === "function")
|
|
7
|
+
for (var i = 0, p = Object.getOwnPropertySymbols(s); i < p.length; i++) {
|
|
8
|
+
if (e.indexOf(p[i]) < 0 && Object.prototype.propertyIsEnumerable.call(s, p[i]))
|
|
9
|
+
t[p[i]] = s[p[i]];
|
|
10
|
+
}
|
|
11
|
+
return t;
|
|
12
|
+
};
|
|
13
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
14
|
+
exports.deleteDMP = exports.tombstoneDMP = exports.updateDMP = exports.createDMP = exports.getDMPs = exports.getDMPVersions = exports.DMPExists = exports.DMP_TOMBSTONE_VERSION = exports.DMP_LATEST_VERSION = void 0;
|
|
15
|
+
const util_dynamodb_1 = require("@aws-sdk/util-dynamodb");
|
|
16
|
+
const client_dynamodb_1 = require("@aws-sdk/client-dynamodb");
|
|
17
|
+
const general_1 = require("./general");
|
|
18
|
+
const DMP_PK_PREFIX = 'DMP';
|
|
19
|
+
// VERSION records store the RDA Common Standard metadata
|
|
20
|
+
const DMP_VERSION_PREFIX = 'VERSION';
|
|
21
|
+
// EXTENSION records store the DMP Tool specific metadata
|
|
22
|
+
const DMP_EXTENSION_PREFIX = 'EXTENSION';
|
|
23
|
+
exports.DMP_LATEST_VERSION = 'latest';
|
|
24
|
+
exports.DMP_TOMBSTONE_VERSION = 'tombstone';
|
|
25
|
+
// The list of properties that are extensions to the RDA Common Standard
|
|
26
|
+
const EXTENSION_KEYS = [
|
|
27
|
+
'provenance',
|
|
28
|
+
'privacy',
|
|
29
|
+
'featured',
|
|
30
|
+
'registered',
|
|
31
|
+
'tombstoned',
|
|
32
|
+
'narrative',
|
|
33
|
+
'research_domain',
|
|
34
|
+
'research_facility',
|
|
35
|
+
'version',
|
|
36
|
+
'funding_opportunity',
|
|
37
|
+
'funding_project'
|
|
38
|
+
];
|
|
39
|
+
const dynamoConfigParams = {
|
|
40
|
+
region: process.env.AWS_REGION || 'us-west-2',
|
|
41
|
+
maxAttempts: process.env.DYNAMO_MAX_ATTEMPTS
|
|
42
|
+
? parseInt(process.env.DYNAMO_MAX_ATTEMPTS)
|
|
43
|
+
: 3,
|
|
44
|
+
};
|
|
45
|
+
class DMPToolDynamoError extends Error {
|
|
46
|
+
constructor(message) {
|
|
47
|
+
super(message);
|
|
48
|
+
this.name = 'DMPToolDynamoError';
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
// Initialize AWS SDK clients (outside the handler function)
|
|
52
|
+
const dynamoDBClient = new client_dynamodb_1.DynamoDBClient(dynamoConfigParams);
|
|
53
|
+
/**
|
|
54
|
+
* Lightweight query just to check if the DMP exists.
|
|
55
|
+
*
|
|
56
|
+
* @param dmpId
|
|
57
|
+
* @returns true if the DMP exists, false otherwise.
|
|
58
|
+
* @throws DMPToolDynamoError if the record could not be fetched due to an error
|
|
59
|
+
*/
|
|
60
|
+
const DMPExists = async (dmpId) => {
|
|
61
|
+
// Very lightweight here, just returning a PK if successful
|
|
62
|
+
const params = {
|
|
63
|
+
KeyConditionExpression: "PK = :pk AND SK = :sk",
|
|
64
|
+
ExpressionAttributeValues: {
|
|
65
|
+
":pk": { S: dmpIdToPK(dmpId) },
|
|
66
|
+
":sk": { S: versionToSK(exports.DMP_LATEST_VERSION) }
|
|
67
|
+
},
|
|
68
|
+
ProjectExpression: "PK"
|
|
69
|
+
};
|
|
70
|
+
try {
|
|
71
|
+
const response = await queryTable(params);
|
|
72
|
+
return !(0, general_1.isNullOrUndefined)(response)
|
|
73
|
+
&& Array.isArray(response.Items)
|
|
74
|
+
&& response.Items.length > 0;
|
|
75
|
+
}
|
|
76
|
+
catch (err) {
|
|
77
|
+
throw new DMPToolDynamoError(`Unable to check if DMP exists id: ${dmpId} - ${(0, general_1.toErrorMessage)(err)}`);
|
|
78
|
+
}
|
|
79
|
+
};
|
|
80
|
+
exports.DMPExists = DMPExists;
|
|
81
|
+
/**
|
|
82
|
+
* Fetch the version timestamps (including DMP_LATEST_VERSION) for the specified DMP ID.
|
|
83
|
+
*
|
|
84
|
+
* @param dmpId
|
|
85
|
+
* @returns The timestamps as strings (e.g. '2026-11-01T13:08:19Z' or 'latest')
|
|
86
|
+
* @throws DMPToolDynamoError if the records could not be fetched due to an error
|
|
87
|
+
*/
|
|
88
|
+
const getDMPVersions = async (dmpId) => {
|
|
89
|
+
const params = {
|
|
90
|
+
KeyConditionExpression: "PK = :pk AND begins_with(SK, :sk)",
|
|
91
|
+
ExpressionAttributeValues: {
|
|
92
|
+
":pk": { S: dmpIdToPK(dmpId) },
|
|
93
|
+
":sk": { S: DMP_VERSION_PREFIX }
|
|
94
|
+
},
|
|
95
|
+
ProjectionExpression: "PK, SK, modified"
|
|
96
|
+
};
|
|
97
|
+
try {
|
|
98
|
+
const response = await queryTable(params);
|
|
99
|
+
if (Array.isArray(response.Items) && response.Items.length > 0) {
|
|
100
|
+
const versions = [];
|
|
101
|
+
for (const item of response.Items) {
|
|
102
|
+
const unmarshalled = (0, util_dynamodb_1.unmarshall)(item);
|
|
103
|
+
if (unmarshalled.PK && unmarshalled.SK && unmarshalled.modified) {
|
|
104
|
+
versions.push({
|
|
105
|
+
PK: unmarshalled.PK,
|
|
106
|
+
SK: unmarshalled.SK,
|
|
107
|
+
modified: unmarshalled.modified
|
|
108
|
+
});
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
return versions;
|
|
112
|
+
}
|
|
113
|
+
return [];
|
|
114
|
+
}
|
|
115
|
+
catch (err) {
|
|
116
|
+
throw new DMPToolDynamoError(`Unable to fetch DMP versions id: ${dmpId} - ${(0, general_1.toErrorMessage)(err)}`);
|
|
117
|
+
}
|
|
118
|
+
};
|
|
119
|
+
exports.getDMPVersions = getDMPVersions;
|
|
120
|
+
/**
|
|
121
|
+
* Fetch the RDA Common Standard metadata record with DMP Tool specific extensions
|
|
122
|
+
* for the specified DMP ID.
|
|
123
|
+
*
|
|
124
|
+
* @param dmpId
|
|
125
|
+
* @param version The version of the DMP metadata record to persist
|
|
126
|
+
* (e.g. '2026-11-01T13:08:19Z').
|
|
127
|
+
* If not provided, the latest version will be used. Defaults to DMP_LATEST_VERSION.
|
|
128
|
+
* @param includeExtensions Whether or not to include the DMP Tool specific
|
|
129
|
+
* extensions in the returned record. Defaults to true.
|
|
130
|
+
* @returns The complete RDA Common Standard metadata record with the DMP extension
|
|
131
|
+
* metadata or an empty array if none were found.
|
|
132
|
+
* @throws DMPToolDynamoError if the records could not be fetched due to an error
|
|
133
|
+
*/
|
|
134
|
+
// Fetch the specified DMP metadata record
|
|
135
|
+
// - Version is optional, if it is not provided, ALL versions will be returned
|
|
136
|
+
// - If you just want the latest version, use the DMP_LATEST_VERSION constant
|
|
137
|
+
const getDMPs = async (dmpId, version, includeExtensions = true) => {
|
|
138
|
+
let params = {};
|
|
139
|
+
if (version) {
|
|
140
|
+
params = {
|
|
141
|
+
KeyConditionExpression: "PK = :pk and SK = :sk",
|
|
142
|
+
ExpressionAttributeValues: {
|
|
143
|
+
":pk": { S: dmpIdToPK(dmpId) },
|
|
144
|
+
":sk": { S: versionToSK(version) }
|
|
145
|
+
}
|
|
146
|
+
};
|
|
147
|
+
}
|
|
148
|
+
else {
|
|
149
|
+
params = {
|
|
150
|
+
KeyConditionExpression: "PK = :pk AND begins_with(SK, :sk)",
|
|
151
|
+
ExpressionAttributeValues: {
|
|
152
|
+
":pk": { S: dmpIdToPK(dmpId) },
|
|
153
|
+
":sk": { S: DMP_VERSION_PREFIX }
|
|
154
|
+
}
|
|
155
|
+
};
|
|
156
|
+
}
|
|
157
|
+
try {
|
|
158
|
+
const response = await queryTable(params);
|
|
159
|
+
if (response && response.Items && response.Items.length > 0) {
|
|
160
|
+
const unmarshalled = response.Items.map(item => (0, util_dynamodb_1.unmarshall)(item));
|
|
161
|
+
// sort the results by the SK (version) descending
|
|
162
|
+
const items = unmarshalled.sort((a, b) => {
|
|
163
|
+
return (b.SK).toString().localeCompare((a.SK).toString());
|
|
164
|
+
});
|
|
165
|
+
// If we are including the DMP Tool extensions, then fetch them
|
|
166
|
+
if (includeExtensions) {
|
|
167
|
+
// We need to remove properties specific to our DynamoDB table and then
|
|
168
|
+
// merge in any DMP Tool specific extensions to the RDA Common Standard
|
|
169
|
+
return await Promise.all(items.map(async (item) => {
|
|
170
|
+
// Fetch the DMP Tool extensions
|
|
171
|
+
const extensions = await getDMPExtensions(dmpId, item.SK.replace(`${DMP_VERSION_PREFIX}#`, ''));
|
|
172
|
+
// Destructure the Dynamo item because we don't need to return the PK and SK
|
|
173
|
+
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
|
174
|
+
const { PK, SK } = item, version = __rest(item, ["PK", "SK"]);
|
|
175
|
+
if (Array.isArray(extensions) && extensions.length > 0) {
|
|
176
|
+
return {
|
|
177
|
+
dmp: Object.assign(Object.assign({}, version), extensions[0])
|
|
178
|
+
};
|
|
179
|
+
}
|
|
180
|
+
return {
|
|
181
|
+
dmp: version,
|
|
182
|
+
};
|
|
183
|
+
}));
|
|
184
|
+
}
|
|
185
|
+
else {
|
|
186
|
+
// Just return the RDA Common Standard metadata record
|
|
187
|
+
return items.map(item => ({ dmp: (0, util_dynamodb_1.unmarshall)(item) }));
|
|
188
|
+
}
|
|
189
|
+
}
|
|
190
|
+
}
|
|
191
|
+
catch (err) {
|
|
192
|
+
throw new DMPToolDynamoError(`Unable to fetch DMP id: ${dmpId}, ver: ${version} - ${(0, general_1.toErrorMessage)(err)}`);
|
|
193
|
+
}
|
|
194
|
+
return [];
|
|
195
|
+
};
|
|
196
|
+
exports.getDMPs = getDMPs;
|
|
197
|
+
/**
|
|
198
|
+
* Fetch the specified DMP Extensions metadata record
|
|
199
|
+
*
|
|
200
|
+
* @param dmpId
|
|
201
|
+
* @param version The version of the DMP metadata record to persist
|
|
202
|
+
* (e.g. '2026-11-01T13:08:19Z').
|
|
203
|
+
* If not provided, the latest version will be used. Defaults to DMP_LATEST_VERSION.
|
|
204
|
+
* @returns The DMP extension metadata records or an empty array if none were found.
|
|
205
|
+
* @throws DMPToolDynamoError if the record could not be fetched
|
|
206
|
+
*/
|
|
207
|
+
const getDMPExtensions = async (dmpId, version) => {
|
|
208
|
+
let params = {};
|
|
209
|
+
if (version) {
|
|
210
|
+
params = {
|
|
211
|
+
KeyConditionExpression: "PK = :pk and SK = :sk",
|
|
212
|
+
ExpressionAttributeValues: {
|
|
213
|
+
":pk": { S: dmpIdToPK(dmpId) },
|
|
214
|
+
":sk": { S: versionToExtensionSK(version) }
|
|
215
|
+
}
|
|
216
|
+
};
|
|
217
|
+
}
|
|
218
|
+
else {
|
|
219
|
+
params = {
|
|
220
|
+
KeyConditionExpression: "PK = :pk AND begins_with(SK, :sk)",
|
|
221
|
+
ExpressionAttributeValues: {
|
|
222
|
+
":pk": { S: dmpIdToPK(dmpId) },
|
|
223
|
+
":sk": { S: DMP_EXTENSION_PREFIX }
|
|
224
|
+
}
|
|
225
|
+
};
|
|
226
|
+
}
|
|
227
|
+
const response = await queryTable(params);
|
|
228
|
+
if (response && response.Items && response.Items.length > 0) {
|
|
229
|
+
const unmarshalled = response.Items.map(item => (0, util_dynamodb_1.unmarshall)(item));
|
|
230
|
+
// sort the results by the SK (version) descending
|
|
231
|
+
const items = unmarshalled.sort((a, b) => {
|
|
232
|
+
return (b.SK).toString().localeCompare((a.SK).toString());
|
|
233
|
+
});
|
|
234
|
+
// Coerce the items to the DMP Tool Extension schema
|
|
235
|
+
return Promise.all(items.map(async (item) => {
|
|
236
|
+
// Destructure the Dynamo item because we don't need to return the PK and SK
|
|
237
|
+
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
|
238
|
+
const { PK, SK } = item, extension = __rest(item, ["PK", "SK"]);
|
|
239
|
+
// Fetch all the version timestamps
|
|
240
|
+
const versions = await (0, exports.getDMPVersions)(dmpId);
|
|
241
|
+
if (Array.isArray(versions) && versions.length > 0) {
|
|
242
|
+
// Return the versions sorted descending
|
|
243
|
+
extension.version = versions
|
|
244
|
+
.sort((a, b) => b.modified.localeCompare(a.modified))
|
|
245
|
+
.map((v) => {
|
|
246
|
+
// The latest version doesn't have a query param appended to the URL
|
|
247
|
+
const queryParam = v.SK.endsWith(exports.DMP_LATEST_VERSION)
|
|
248
|
+
? ''
|
|
249
|
+
: `?version=${v.modified}`;
|
|
250
|
+
const dmpIdWithoutProtocol = dmpId.replace(/^https?:\/\//, '');
|
|
251
|
+
const accessURLBase = `https://${process.env.DOMAIN_NAME}/dmps/`;
|
|
252
|
+
return {
|
|
253
|
+
access_url: `${accessURLBase}${dmpIdWithoutProtocol}${queryParam}`,
|
|
254
|
+
version: v.modified,
|
|
255
|
+
};
|
|
256
|
+
});
|
|
257
|
+
}
|
|
258
|
+
return extension;
|
|
259
|
+
}));
|
|
260
|
+
}
|
|
261
|
+
return [];
|
|
262
|
+
};
|
|
263
|
+
/**
|
|
264
|
+
* Persists the specified DMP metadata record to the DynamoDB table.
|
|
265
|
+
* This function will handle the separation of RDA Common Standard and DMP Tool
|
|
266
|
+
* specific metadata.
|
|
267
|
+
*
|
|
268
|
+
* @param dmpId The DMP ID (e.g. '123456789')
|
|
269
|
+
* @param dmp The DMP metadata record to persist as either an RDA Common Standard
|
|
270
|
+
* or the standard with DMP Tool specific extensions.
|
|
271
|
+
* @param version The version of the DMP metadata record to persist
|
|
272
|
+
* (e.g. '2026-11-01T13:08:19Z').
|
|
273
|
+
* If not provided, the latest version will be used. Defaults to DMP_LATEST_VERSION.
|
|
274
|
+
* @param includeExtensions Whether or not to include the DMP Tool specific
|
|
275
|
+
* extensions in the returned record. Defaults to true.
|
|
276
|
+
* @returns The persisted DMP metadata record as an RDA Common Standard DMP
|
|
277
|
+
* metadata record with the DMP Tool specific extensions merged in.
|
|
278
|
+
* @throws DMPToolDynamoError if the record could not be persisted
|
|
279
|
+
*/
|
|
280
|
+
const createDMP = async (dmpId, dmp, version = exports.DMP_LATEST_VERSION, includeExtensions = true) => {
|
|
281
|
+
var _a;
|
|
282
|
+
if (!dmpId || dmpId.trim().length === 0 || !dmp) {
|
|
283
|
+
throw new DMPToolDynamoError('Missing DMP ID or DMP metadata record');
|
|
284
|
+
}
|
|
285
|
+
// If the version is LATEST, then first make sure there is not already one present!
|
|
286
|
+
const exists = await (0, exports.DMPExists)(dmpId);
|
|
287
|
+
if (exists) {
|
|
288
|
+
throw new DMPToolDynamoError('Latest version already exists');
|
|
289
|
+
}
|
|
290
|
+
try {
|
|
291
|
+
// If the metadata is nested in a top level 'dmp' property, then unwrap it
|
|
292
|
+
const innerMetadata = (_a = dmp === null || dmp === void 0 ? void 0 : dmp.dmp) !== null && _a !== void 0 ? _a : dmp;
|
|
293
|
+
// Separate the RDA Common Standard metadata from the DMP Tool specific extensions
|
|
294
|
+
const dmptoolExtension = pick(innerMetadata, [...EXTENSION_KEYS]);
|
|
295
|
+
const rdaCommonStandard = pick(innerMetadata, Object.keys(innerMetadata).filter(k => !EXTENSION_KEYS.includes(k)));
|
|
296
|
+
const newVersionItem = Object.assign(Object.assign({}, rdaCommonStandard), { PK: dmpIdToPK(dmpId), SK: versionToSK(version) });
|
|
297
|
+
// Insert the RDA Common Standard metadata record into the DynamoDB table
|
|
298
|
+
await putItem((0, util_dynamodb_1.marshall)(newVersionItem, { removeUndefinedValues: true }));
|
|
299
|
+
// Create the DMP Tool extensions metadata record. We ALWAYS do this even if
|
|
300
|
+
// the caller does not want them returned
|
|
301
|
+
await createDMPExtensions(dmpId, dmptoolExtension, version);
|
|
302
|
+
// Fetch the complete DMP metadata record including the RDA Common Standard
|
|
303
|
+
// and the DMP Tool extensions
|
|
304
|
+
return (await (0, exports.getDMPs)(dmpId, exports.DMP_LATEST_VERSION, includeExtensions))[0];
|
|
305
|
+
}
|
|
306
|
+
catch (err) {
|
|
307
|
+
// If it was a DMPToolDynamoError that bubbled up, just throw it
|
|
308
|
+
if (err instanceof DMPToolDynamoError)
|
|
309
|
+
throw err;
|
|
310
|
+
throw new DMPToolDynamoError(`Unable to create DMP id: ${dmpId}, ver: ${version} - ${(0, general_1.toErrorMessage)(err)}`);
|
|
311
|
+
}
|
|
312
|
+
};
|
|
313
|
+
exports.createDMP = createDMP;
|
|
314
|
+
/**
|
|
315
|
+
* Create a new DMP Extensions metadata record
|
|
316
|
+
*
|
|
317
|
+
* @param dmpId
|
|
318
|
+
* @param dmp
|
|
319
|
+
* @param version The version of the DMP metadata record to persist
|
|
320
|
+
* (e.g. '2026-11-01T13:08:19Z').
|
|
321
|
+
* If not provided, the latest version will be used. Defaults to DMP_LATEST_VERSION.
|
|
322
|
+
* @returns The persisted DMP Tool extensions metadata record.
|
|
323
|
+
*/
|
|
324
|
+
const createDMPExtensions = async (dmpId, dmp, version = exports.DMP_LATEST_VERSION) => {
|
|
325
|
+
const newExtensionItem = Object.assign(Object.assign({}, dmp), { PK: dmpIdToPK(dmpId), SK: versionToExtensionSK(version) });
|
|
326
|
+
// Insert the DMP Tool Extensions metadata record into the DynamoDB table
|
|
327
|
+
await putItem((0, util_dynamodb_1.marshall)(newExtensionItem, { removeUndefinedValues: true }));
|
|
328
|
+
};
|
|
329
|
+
/**
|
|
330
|
+
* Update the specified DMP metadata record.
|
|
331
|
+
* This function will handle the separation of RDA Common Standard and DMP Tool
|
|
332
|
+
* specific metadata. We always update the latest version of the DMP metadata record.
|
|
333
|
+
* Historical versions are immutable.
|
|
334
|
+
*
|
|
335
|
+
* A snapshot of the current "latest" version of the DMP's metadata will be taken
|
|
336
|
+
* under the following circumstances:
|
|
337
|
+
* - If the `provenance` of the incoming record does not match the one on the
|
|
338
|
+
* latest record
|
|
339
|
+
* - If the `modified` timestamp of the latest record is older than 2 hours ago
|
|
340
|
+
*
|
|
341
|
+
* If a snapshot is made, the timestamp and link to retrieve it will appear
|
|
342
|
+
* in the `versions` array
|
|
343
|
+
*
|
|
344
|
+
* @param dmp
|
|
345
|
+
* @param includeExtensions Whether or not to include the DMP Tool specific
|
|
346
|
+
* extensions in the returned record. Defaults to true.
|
|
347
|
+
* @returns The persisted DMP metadata record as an RDA Common Standard DMP
|
|
348
|
+
* metadata record with the DMP Tool specific extensions merged in.
|
|
349
|
+
* @throws DMPToolDynamoError if the record could not be persisted
|
|
350
|
+
*/
|
|
351
|
+
const updateDMP = async (dmp, includeExtensions = true) => {
|
|
352
|
+
var _a, _b, _c, _d, _e, _f;
|
|
353
|
+
const dmpId = (_b = (_a = dmp.dmp) === null || _a === void 0 ? void 0 : _a.dmp_id) === null || _b === void 0 ? void 0 : _b.identifier;
|
|
354
|
+
if (!dmp || !dmpId) {
|
|
355
|
+
throw new DMPToolDynamoError('Missing DMP ID or DMP metadata record');
|
|
356
|
+
}
|
|
357
|
+
try {
|
|
358
|
+
// If the metadata is nested in a top level 'dmp' property, then unwrap it
|
|
359
|
+
const innerMetadata = (_c = dmp === null || dmp === void 0 ? void 0 : dmp.dmp) !== null && _c !== void 0 ? _c : dmp;
|
|
360
|
+
// Separate the RDA Common Standard metadata from the DMP Tool specific extensions
|
|
361
|
+
const dmptoolExtension = pick(innerMetadata, [...EXTENSION_KEYS]);
|
|
362
|
+
const rdaCommonStandard = pick(innerMetadata, Object.keys(innerMetadata).filter(k => !EXTENSION_KEYS.includes(k)));
|
|
363
|
+
// Fetch the current latest version of the plan's maDMP record. Always get
|
|
364
|
+
// the extensions because we need to check the provenance
|
|
365
|
+
const latest = (await (0, exports.getDMPs)(dmpId, exports.DMP_LATEST_VERSION, true))[0];
|
|
366
|
+
// Bail if there is no latest version (it has never been created yet or its tombstoned)
|
|
367
|
+
// Or if the incoming modified timestamp is newer than the latest version's
|
|
368
|
+
// modified timestamp (collision)
|
|
369
|
+
if ((0, general_1.isNullOrUndefined)(latest)
|
|
370
|
+
|| ((_d = latest.dmp) === null || _d === void 0 ? void 0 : _d.tombstoned)
|
|
371
|
+
|| ((_e = latest.dmp) === null || _e === void 0 ? void 0 : _e.modified) > innerMetadata.modified) {
|
|
372
|
+
throw new DMPToolDynamoError(`Cannot update a historical DMP id: ${dmpId}, ver: ${exports.DMP_LATEST_VERSION}`);
|
|
373
|
+
}
|
|
374
|
+
const lastModified = new Date((_f = latest.dmp) === null || _f === void 0 ? void 0 : _f.modified).getTime();
|
|
375
|
+
const now = Date.now();
|
|
376
|
+
const gracePeriod = process.env.VERSION_GRACE_PERIOD ? Number(process.env.VERSION_GRACE_PERIOD) : 7200000;
|
|
377
|
+
// We need to version the DMP if the provenance doesn't match or the modified
|
|
378
|
+
// timestamp is older than 2 hours ago
|
|
379
|
+
const needToVersion = dmptoolExtension.provenance !== latest.dmp.provenance
|
|
380
|
+
|| (now - lastModified) > gracePeriod;
|
|
381
|
+
// If it was determined that we need to version the DMP, then create a new snapshot
|
|
382
|
+
// using the modified date of the current latest version
|
|
383
|
+
if (needToVersion) {
|
|
384
|
+
await (0, exports.createDMP)(dmpId, latest.dmp, latest.dmp.modified);
|
|
385
|
+
}
|
|
386
|
+
// Updates can only ever occur on the latest version of the DMP (the Plan logic
|
|
387
|
+
// should handle creating a snapshot of the original version of the DMP when
|
|
388
|
+
// appropriate)
|
|
389
|
+
const versionItem = Object.assign(Object.assign({}, rdaCommonStandard), { PK: dmpIdToPK(dmpId), SK: versionToSK(exports.DMP_LATEST_VERSION) });
|
|
390
|
+
// Insert the RDA Common Standard metadata record into the DynamoDB table
|
|
391
|
+
await putItem((0, util_dynamodb_1.marshall)(versionItem, { removeUndefinedValues: true }));
|
|
392
|
+
// Update the DMP Tool extensions metadata record. We ALWAYS do this even if
|
|
393
|
+
// the caller does not want them returned
|
|
394
|
+
await updateDMPExtensions(dmpId, dmptoolExtension);
|
|
395
|
+
// Fetch the complete DMP metadata record including the RDA Common Standard
|
|
396
|
+
// and the DMP Tool extensions
|
|
397
|
+
return (await (0, exports.getDMPs)(dmpId, exports.DMP_LATEST_VERSION, includeExtensions))[0];
|
|
398
|
+
}
|
|
399
|
+
catch (err) {
|
|
400
|
+
// If it was a DMPToolDynamoError that bubbled up, just throw it
|
|
401
|
+
if (err instanceof DMPToolDynamoError)
|
|
402
|
+
throw err;
|
|
403
|
+
throw new DMPToolDynamoError(`Unable to create DMP id: ${dmpId}, ver: ${exports.DMP_LATEST_VERSION} - ${(0, general_1.toErrorMessage)(err)}`);
|
|
404
|
+
}
|
|
405
|
+
};
|
|
406
|
+
exports.updateDMP = updateDMP;
|
|
407
|
+
/**
|
|
408
|
+
* Update the specified DMP Extensions metadata record
|
|
409
|
+
* We always update the latest version of the DMP metadata record. Historical versions are immutable.
|
|
410
|
+
*
|
|
411
|
+
* @param dmpId
|
|
412
|
+
* @param dmp
|
|
413
|
+
* @returns The persisted DMP Tool extensions metadata record.
|
|
414
|
+
*/
|
|
415
|
+
const updateDMPExtensions = async (dmpId, dmp) => {
|
|
416
|
+
// Updates can only ever occur on the latest version of the DMP (the Plan logic
|
|
417
|
+
// should handle creating a snapshot of the original version of the DMP when appropriate)
|
|
418
|
+
const extensionItem = Object.assign(Object.assign({}, dmp), { PK: dmpIdToPK(dmpId), SK: versionToExtensionSK(exports.DMP_LATEST_VERSION) });
|
|
419
|
+
await putItem((0, util_dynamodb_1.marshall)(extensionItem, { removeUndefinedValues: true }));
|
|
420
|
+
};
|
|
421
|
+
/**
|
|
422
|
+
* Create a Tombstone for the specified DMP metadata record
|
|
423
|
+
* (registered/published DMPs only!)
|
|
424
|
+
*
|
|
425
|
+
* @param dmpId The DMP ID (e.g. '11.12345/A1B2C3')
|
|
426
|
+
* @param includeExtensions Whether or not to include the DMP Tool specific
|
|
427
|
+
* extensions in the returned record. Defaults to true.
|
|
428
|
+
* @returns The new tombstone DMP metadata record as an RDA Common Standard DMP
|
|
429
|
+
* metadata record with the DMP Tool specific extensions merged in.
|
|
430
|
+
* @throws DMPToolDynamoError if a tombstone could not be created
|
|
431
|
+
*/
|
|
432
|
+
const tombstoneDMP = async (dmpId, includeExtensions = true) => {
|
|
433
|
+
var _a, _b;
|
|
434
|
+
// Get the latest version of the DMP including the extensions because we need
|
|
435
|
+
// to check the registered status
|
|
436
|
+
const dmp = (await (0, exports.getDMPs)(dmpId, exports.DMP_LATEST_VERSION, true))[0];
|
|
437
|
+
if (!dmp) {
|
|
438
|
+
throw new DMPToolDynamoError(`Unable to find DMP id: ${dmpId}, ver: ${exports.DMP_LATEST_VERSION}`);
|
|
439
|
+
}
|
|
440
|
+
// If the DMP has been registered (aka published) we can create a tombstone
|
|
441
|
+
if (dmp.dmp.registered) {
|
|
442
|
+
// If the metadata is nested in a top level 'dmp' property, then unwrap it
|
|
443
|
+
const innerMetadata = (_a = dmp === null || dmp === void 0 ? void 0 : dmp.dmp) !== null && _a !== void 0 ? _a : dmp;
|
|
444
|
+
// Separate the RDA Common Standard metadata from the DMP Tool specific extensions.
|
|
445
|
+
const dmptoolExtension = pick(innerMetadata, [...EXTENSION_KEYS]);
|
|
446
|
+
const rdaCommonStandard = pick(innerMetadata, Object.keys(innerMetadata).filter(k => !EXTENSION_KEYS.includes(k)));
|
|
447
|
+
const now = (0, general_1.convertMySQLDateTimeToRFC3339)(new Date());
|
|
448
|
+
if ((0, general_1.isNullOrUndefined)(now)) {
|
|
449
|
+
throw new DMPToolDynamoError('Unable to create modified date');
|
|
450
|
+
}
|
|
451
|
+
const versionItem = Object.assign(Object.assign({}, rdaCommonStandard), { PK: dmpIdToPK(dmpId), SK: versionToSK(exports.DMP_TOMBSTONE_VERSION), title: `OBSOLETE: ${(_b = dmp.dmp) === null || _b === void 0 ? void 0 : _b.title}`, modified: now });
|
|
452
|
+
try {
|
|
453
|
+
// Update the RDA Common Standard metadata record
|
|
454
|
+
await putItem((0, util_dynamodb_1.marshall)(versionItem, { removeUndefinedValues: true }));
|
|
455
|
+
await deleteItem({
|
|
456
|
+
PK: { S: dmpIdToPK(dmpId) },
|
|
457
|
+
SK: { S: versionToSK(exports.DMP_LATEST_VERSION) }
|
|
458
|
+
});
|
|
459
|
+
// Tombstone the DMP Tool Extensions metadata record. We ALWAYS do this even
|
|
460
|
+
// if the caller does not want them returned
|
|
461
|
+
await tombstoneDMPExtensions(dmpId, dmptoolExtension);
|
|
462
|
+
// Fetch the complete DMP metadata record including the RDA Common Standard
|
|
463
|
+
// and the DMP Tool extensions
|
|
464
|
+
return (await (0, exports.getDMPs)(dmpId, exports.DMP_TOMBSTONE_VERSION, includeExtensions))[0];
|
|
465
|
+
}
|
|
466
|
+
catch (err) {
|
|
467
|
+
if (err instanceof DMPToolDynamoError)
|
|
468
|
+
throw err;
|
|
469
|
+
throw new DMPToolDynamoError(`Unable to tombstone id: ${dmpId}, ver: ${exports.DMP_LATEST_VERSION} - ${(0, general_1.toErrorMessage)(err)}`);
|
|
470
|
+
}
|
|
471
|
+
}
|
|
472
|
+
else {
|
|
473
|
+
throw new DMPToolDynamoError(`Unable to tombstone DMP id: ${dmpId} because it is not registered/published`);
|
|
474
|
+
}
|
|
475
|
+
};
|
|
476
|
+
exports.tombstoneDMP = tombstoneDMP;
|
|
477
|
+
/**
|
|
478
|
+
* Add a tombstone date to the specified DMP Extensions metadata record
|
|
479
|
+
* (registered/published DMPs only!)
|
|
480
|
+
*
|
|
481
|
+
* @param dmpId The DMP ID (e.g. '11.12345/A1B2C3')
|
|
482
|
+
* @param dmp The DMP Tool specific extensions record to update.
|
|
483
|
+
* @throws DMPToolDynamoError if the tombstone date could not be added
|
|
484
|
+
*/
|
|
485
|
+
const tombstoneDMPExtensions = async (dmpId, dmp) => {
|
|
486
|
+
const now = (0, general_1.convertMySQLDateTimeToRFC3339)(new Date());
|
|
487
|
+
if (!now) {
|
|
488
|
+
throw new DMPToolDynamoError('Unable to create tombstone date');
|
|
489
|
+
}
|
|
490
|
+
const extensionItem = Object.assign(Object.assign({}, dmp), { PK: dmpIdToPK(dmpId), SK: versionToExtensionSK(exports.DMP_TOMBSTONE_VERSION), tombstoned: now });
|
|
491
|
+
// Update the DMP Tool Extensions metadata record
|
|
492
|
+
await putItem((0, util_dynamodb_1.marshall)(extensionItem, { removeUndefinedValues: true }));
|
|
493
|
+
// Then delete the old latest version
|
|
494
|
+
await deleteItem({
|
|
495
|
+
PK: { S: dmpIdToPK(dmpId) },
|
|
496
|
+
SK: { S: versionToExtensionSK(exports.DMP_LATEST_VERSION) }
|
|
497
|
+
});
|
|
498
|
+
};
|
|
499
|
+
/**
|
|
500
|
+
* Delete the specified DMP metadata record and any associated DMP Tool extension records.
|
|
501
|
+
* This will NOT work on DMPs that have been registered/published.
|
|
502
|
+
*
|
|
503
|
+
* @param dmpId The DMP ID (e.g. '11.12345/A1B2C3')
|
|
504
|
+
* @param includeExtensions Whether or not to include the DMP Tool specific extensions
|
|
505
|
+
* in the returned record. Defaults to true.
|
|
506
|
+
* @returns The deleted DMP metadata record as an RDA Common Standard DMP metadata
|
|
507
|
+
* record with the DMP Tool specific extensions merged in.
|
|
508
|
+
* @throws DMPToolDynamoError if the record could not be deleted
|
|
509
|
+
*/
|
|
510
|
+
const deleteDMP = async (dmpId, includeExtensions = true) => {
|
|
511
|
+
// Get the latest version of the DMP. Always get the extensions because we need
|
|
512
|
+
// to check the registered status
|
|
513
|
+
const dmps = await (0, exports.getDMPs)(dmpId, exports.DMP_LATEST_VERSION, true);
|
|
514
|
+
if (Array.isArray(dmps) && dmps.length > 0) {
|
|
515
|
+
const latest = dmps[0];
|
|
516
|
+
// If the caller wants just the RDA Common Standard metadata, then reload the
|
|
517
|
+
// latest version without extensions
|
|
518
|
+
const rdaOnly = pick(latest.dmp, Object.keys(latest.dmp).filter(k => !EXTENSION_KEYS.includes(k)));
|
|
519
|
+
const toReturn = includeExtensions
|
|
520
|
+
? latest
|
|
521
|
+
: { dmp: rdaOnly };
|
|
522
|
+
// If the latest version was found, and it has NOT been registered/published
|
|
523
|
+
if (latest && !latest.dmp.registered) {
|
|
524
|
+
try {
|
|
525
|
+
// Delete all records with that DMP ID
|
|
526
|
+
await deleteItem({ PK: { S: dmpIdToPK(dmpId) } });
|
|
527
|
+
return toReturn;
|
|
528
|
+
}
|
|
529
|
+
catch (err) {
|
|
530
|
+
throw new DMPToolDynamoError(`Unable to delete id: ${dmpId}, ver: ${exports.DMP_LATEST_VERSION} - ${(0, general_1.toErrorMessage)(err)}`);
|
|
531
|
+
}
|
|
532
|
+
}
|
|
533
|
+
else {
|
|
534
|
+
throw new DMPToolDynamoError(`Unable to delete id: ${dmpId} because it does not exist or is registered`);
|
|
535
|
+
}
|
|
536
|
+
}
|
|
537
|
+
else {
|
|
538
|
+
throw new DMPToolDynamoError(`Unable to find id: ${dmpId}, ver: ${exports.DMP_LATEST_VERSION}`);
|
|
539
|
+
}
|
|
540
|
+
};
|
|
541
|
+
exports.deleteDMP = deleteDMP;
|
|
542
|
+
/**
|
|
543
|
+
* Scan the specified DynamoDB table using the specified criteria
|
|
544
|
+
* @param table
|
|
545
|
+
* @param params
|
|
546
|
+
* @returns an array of DynamoDB items
|
|
547
|
+
*/
|
|
548
|
+
// We're not currently using it, but did not want to remove it just in case
|
|
549
|
+
// we need it in the future
|
|
550
|
+
//
|
|
551
|
+
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
|
552
|
+
const scanTable = async (table, params) => {
|
|
553
|
+
let items = [];
|
|
554
|
+
let lastEvaluatedKey;
|
|
555
|
+
// Query the DynamoDB index table for all DMP metadata (with pagination)
|
|
556
|
+
do {
|
|
557
|
+
const command = new client_dynamodb_1.ScanCommand(Object.assign({ TableName: table, ExclusiveStartKey: lastEvaluatedKey, ConsistentRead: false, ReturnConsumedCapacity: 'TOTAL' }, params));
|
|
558
|
+
const response = await dynamoDBClient.send(command);
|
|
559
|
+
// Collect items and update the pagination key
|
|
560
|
+
items = items.concat(response.Items || []);
|
|
561
|
+
// LastEvaluatedKey is the position of the end cursor from the query that was just run
|
|
562
|
+
// when it is undefined, then the query reached the end of the results.
|
|
563
|
+
lastEvaluatedKey = response === null || response === void 0 ? void 0 : response.LastEvaluatedKey;
|
|
564
|
+
} while (lastEvaluatedKey);
|
|
565
|
+
// Deserialize and split items into multiple files if necessary
|
|
566
|
+
return items;
|
|
567
|
+
};
|
|
568
|
+
/**
|
|
569
|
+
* Query the specified DynamoDB table using the specified criteria
|
|
570
|
+
*
|
|
571
|
+
* @param params
|
|
572
|
+
* @returns an array of DynamoDB items
|
|
573
|
+
*/
|
|
574
|
+
const queryTable = async (params = {}) => {
|
|
575
|
+
// Query the DynamoDB index table for all DMP metadata (with pagination)
|
|
576
|
+
const command = new client_dynamodb_1.QueryCommand(Object.assign({ TableName: process.env.DYNAMODB_TABLE_NAME, ConsistentRead: false, ReturnConsumedCapacity: 'TOTAL' }, params));
|
|
577
|
+
return await dynamoDBClient.send(command);
|
|
578
|
+
};
|
|
579
|
+
/**
|
|
580
|
+
* Create/Update an item in the specified DynamoDB table
|
|
581
|
+
*
|
|
582
|
+
* @param item
|
|
583
|
+
*/
|
|
584
|
+
const putItem = async (item) => {
|
|
585
|
+
// Delete the item from the DynamoDB table
|
|
586
|
+
await dynamoDBClient.send(new client_dynamodb_1.PutItemCommand({
|
|
587
|
+
TableName: process.env.DYNAMO_TABLE_NAME,
|
|
588
|
+
ReturnConsumedCapacity: 'TOTAL',
|
|
589
|
+
Item: item
|
|
590
|
+
}));
|
|
591
|
+
return;
|
|
592
|
+
};
|
|
593
|
+
/**
|
|
594
|
+
* Delete an item from the specified DynamoDB table
|
|
595
|
+
*
|
|
596
|
+
* @param key
|
|
597
|
+
*/
|
|
598
|
+
const deleteItem = async (key) => {
|
|
599
|
+
// Delete the item from the DynamoDB table
|
|
600
|
+
await dynamoDBClient.send(new client_dynamodb_1.DeleteItemCommand({
|
|
601
|
+
TableName: process.env.DYNAMO_TABLE_NAME,
|
|
602
|
+
ReturnConsumedCapacity: 'TOTAL',
|
|
603
|
+
Key: key
|
|
604
|
+
}));
|
|
605
|
+
};
|
|
606
|
+
/**
|
|
607
|
+
* Convert a DMP ID into a PK for the DynamoDB table
|
|
608
|
+
*
|
|
609
|
+
* @param dmpId
|
|
610
|
+
*/
|
|
611
|
+
const dmpIdToPK = (dmpId) => {
|
|
612
|
+
// Remove the protocol and slashes from the DMP ID
|
|
613
|
+
const id = dmpId === null || dmpId === void 0 ? void 0 : dmpId.replace(/(^\w+:|^)\/\//, '');
|
|
614
|
+
return `${DMP_PK_PREFIX}#${id}`;
|
|
615
|
+
};
|
|
616
|
+
/**
|
|
617
|
+
* Convert a DMP ID version timestamp into a SK for the DynamoDB table for the
|
|
618
|
+
* RDA Common Standard metadata record
|
|
619
|
+
*
|
|
620
|
+
* @param version the version as a timestamp or "latest"
|
|
621
|
+
* (e.g. "2026-11-01T13:08:19Z", "latest")
|
|
622
|
+
*/
|
|
623
|
+
const versionToSK = (version = exports.DMP_LATEST_VERSION) => {
|
|
624
|
+
return `${DMP_VERSION_PREFIX}#${version}`;
|
|
625
|
+
};
|
|
626
|
+
/**
|
|
627
|
+
* Convert a DMP ID version timestamp into a SK for the DynamoDB table for a
|
|
628
|
+
* DMP Tool extension record
|
|
629
|
+
*
|
|
630
|
+
* @param version the version as a timestamp or "latest"
|
|
631
|
+
* (e.g. "2026-11-01T13:08:19Z", "latest")
|
|
632
|
+
* @returns string
|
|
633
|
+
*/
|
|
634
|
+
const versionToExtensionSK = (version = exports.DMP_LATEST_VERSION) => {
|
|
635
|
+
return `${DMP_EXTENSION_PREFIX}#${version}`;
|
|
636
|
+
};
|
|
637
|
+
/**
|
|
638
|
+
* Extract a subset of keys from an object
|
|
639
|
+
*
|
|
640
|
+
* @param obj
|
|
641
|
+
* @param keys
|
|
642
|
+
*/
|
|
643
|
+
function pick(obj, keys) {
|
|
644
|
+
const result = {};
|
|
645
|
+
keys.forEach((key) => {
|
|
646
|
+
if (key in obj) {
|
|
647
|
+
result[key] = obj[key];
|
|
648
|
+
}
|
|
649
|
+
});
|
|
650
|
+
return result;
|
|
651
|
+
}
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
export interface PutEventResponse {
|
|
2
|
+
status: number;
|
|
3
|
+
message: string;
|
|
4
|
+
eventId?: string;
|
|
5
|
+
}
|
|
6
|
+
/**
|
|
7
|
+
* Publishes an event to EventBridge.
|
|
8
|
+
*
|
|
9
|
+
* @param source The name of the caller (e.g. the Lambda Function or Application Function)
|
|
10
|
+
* @param detailType The type of event (resources typically watch for specific types of events)
|
|
11
|
+
* @param detail The payload of the event (will be accessible to the invoked resource)
|
|
12
|
+
*/
|
|
13
|
+
export declare const putEvent: (source: string, detailType: string, details: Record<string, unknown>) => Promise<PutEventResponse>;
|