@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/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>;