@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/maDMP.js
ADDED
|
@@ -0,0 +1,982 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.validateDMPToolExtensions = exports.validateRDACommonStandard = void 0;
|
|
4
|
+
exports.planToDMPCommonStandard = planToDMPCommonStandard;
|
|
5
|
+
const jsonschema_1 = require("jsonschema");
|
|
6
|
+
const rds_1 = require("./rds");
|
|
7
|
+
const general_1 = require("./general");
|
|
8
|
+
const types_1 = require("@dmptool/types");
|
|
9
|
+
const maDMPTypes_1 = require("./maDMPTypes");
|
|
10
|
+
const ROR_REGEX = /^https?:\/\/ror\.org\/[0-9a-zA-Z]+$/;
|
|
11
|
+
const DOI_REGEX = /^(https?:\/\/)?(doi\.org\/)?(doi:)?(10\.\d{4,9}\/[-._;()/:\w]+)$/;
|
|
12
|
+
const ORCID_BASE_URL = process.env.ENV && ['stg', 'prd'].includes(process.env.ENV)
|
|
13
|
+
? 'https://orcid.org/'
|
|
14
|
+
: 'https://sandbox.orcid.org/';
|
|
15
|
+
const ORCID_REGEX = /^(https?:\/\/)?(www\.|pub\.)?(sandbox\.)?(orcid\.org\/)?([0-9]{4}-[0-9]{4}-[0-9]{4}-[0-9]{3}[0-9X])$/;
|
|
16
|
+
class DMPValidationError extends Error {
|
|
17
|
+
constructor(message) {
|
|
18
|
+
super(message);
|
|
19
|
+
this.name = 'DMPValidationError';
|
|
20
|
+
}
|
|
21
|
+
}
|
|
22
|
+
/**
|
|
23
|
+
* Ensure that the ORCID is in the correct format (https://orcid.org/0000-0000-0000-0000)
|
|
24
|
+
*
|
|
25
|
+
* @param orcidIn the ORCID to check
|
|
26
|
+
* @returns the ORCID in the correct format or null if it is not in the correct format
|
|
27
|
+
*/
|
|
28
|
+
function formatORCID(orcidIn) {
|
|
29
|
+
// If it is blank or already in the correct format, return it
|
|
30
|
+
if (orcidIn && (orcidIn.match(ORCID_REGEX) && orcidIn.startsWith('http'))) {
|
|
31
|
+
return (0, general_1.normaliseHttpProtocol)(orcidIn);
|
|
32
|
+
}
|
|
33
|
+
// If it matches the ORCID format but didn't start with http then its just the id
|
|
34
|
+
if (orcidIn && orcidIn.match(ORCID_REGEX)) {
|
|
35
|
+
return (0, general_1.normaliseHttpProtocol)(`${ORCID_BASE_URL}${orcidIn.split('/').pop()}`);
|
|
36
|
+
}
|
|
37
|
+
// Otherwise it's not an ORCID
|
|
38
|
+
return null;
|
|
39
|
+
}
|
|
40
|
+
/**
|
|
41
|
+
* Determine the identifier type for the given URI string
|
|
42
|
+
*
|
|
43
|
+
* @param uri the URI string to check
|
|
44
|
+
* @returns the RDA Common Standard identifier type for the given URI string
|
|
45
|
+
*/
|
|
46
|
+
function determineIdentifierType(uri) {
|
|
47
|
+
if ((0, general_1.isNullOrUndefined)(uri)) {
|
|
48
|
+
return 'other';
|
|
49
|
+
}
|
|
50
|
+
if (uri.match(ORCID_REGEX)) {
|
|
51
|
+
return 'orcid';
|
|
52
|
+
}
|
|
53
|
+
else if (uri.match(DOI_REGEX)) {
|
|
54
|
+
return 'doi';
|
|
55
|
+
}
|
|
56
|
+
else if (uri.match(ROR_REGEX)) {
|
|
57
|
+
return 'ror';
|
|
58
|
+
}
|
|
59
|
+
else if (uri.startsWith('http')) {
|
|
60
|
+
return 'url';
|
|
61
|
+
}
|
|
62
|
+
else {
|
|
63
|
+
return 'other';
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
/**
|
|
67
|
+
* Function to convert a PlanFunding status to an RDA Common Standard funding_status
|
|
68
|
+
*
|
|
69
|
+
* @param status the PlanFunding status to convert
|
|
70
|
+
* @returns the RDA Common Standard funding_status
|
|
71
|
+
*/
|
|
72
|
+
// Helper function to convert a ProjectFundingStatus to a DMPFundingStatus
|
|
73
|
+
function planFundingStatusToDMPFundingStatus(status) {
|
|
74
|
+
switch (status) {
|
|
75
|
+
case 'DENIED':
|
|
76
|
+
return 'rejected';
|
|
77
|
+
case 'GRANTED':
|
|
78
|
+
return 'granted';
|
|
79
|
+
default:
|
|
80
|
+
return 'planned';
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
/**
|
|
84
|
+
* Function to convert a 5 character language code (e.g. en-US) to a 3 character code (e.g. eng)
|
|
85
|
+
*
|
|
86
|
+
* @param language the 5 character language code to convert
|
|
87
|
+
* @returns the 3 character language code
|
|
88
|
+
*/
|
|
89
|
+
function convertFiveCharToThreeChar(language) {
|
|
90
|
+
switch (language) {
|
|
91
|
+
case 'pt-BR':
|
|
92
|
+
return 'por';
|
|
93
|
+
default:
|
|
94
|
+
return 'eng';
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
/**
|
|
98
|
+
* Function to generate the base of an internal ID namespace
|
|
99
|
+
*
|
|
100
|
+
* @param projectId the Project ID to use for the internal ID namespace
|
|
101
|
+
* @param planId the Plan ID to use for the internal ID namespace
|
|
102
|
+
* @returns the base of the internal ID namespace
|
|
103
|
+
*/
|
|
104
|
+
function internalIdBase(projectId, planId) {
|
|
105
|
+
return `${process.env.APPLICATION_NAME}.projects.${projectId}.dmp.${planId}`;
|
|
106
|
+
}
|
|
107
|
+
/**
|
|
108
|
+
* Fetch the default MemberRole from the MySQL database
|
|
109
|
+
*
|
|
110
|
+
* @returns the default MemberRole as a string (or undefined if there is no default)
|
|
111
|
+
*/
|
|
112
|
+
const loadDefaultMemberRole = async () => {
|
|
113
|
+
const sql = 'SELECT * FROM memberRoles WHERE isDefault = 1';
|
|
114
|
+
const resp = await (0, rds_1.queryTable)(sql, []);
|
|
115
|
+
if (resp && Array.isArray(resp.results) && resp.results.length > 0) {
|
|
116
|
+
return resp.results[0].id;
|
|
117
|
+
}
|
|
118
|
+
return undefined;
|
|
119
|
+
};
|
|
120
|
+
/**
|
|
121
|
+
* Fetches the Plan information needed to construct the DMP Common Standard from
|
|
122
|
+
* the MySQL database
|
|
123
|
+
*
|
|
124
|
+
* @param planId the Plan ID to fetch the Plan information for
|
|
125
|
+
* @returns the Plan information needed to construct the DMP Common Standard
|
|
126
|
+
*/
|
|
127
|
+
// Fetch the Plan info needed from the MySQL database
|
|
128
|
+
const loadPlanInfo = async (planId) => {
|
|
129
|
+
const sql = `
|
|
130
|
+
SELECT id, dmpId, projectId, versionedTemplateId,
|
|
131
|
+
createdById, created, modifiedById, modified, title,
|
|
132
|
+
status, visibility, featured, registeredBy, registered,
|
|
133
|
+
languageId
|
|
134
|
+
FROM plans
|
|
135
|
+
WHERE id = ?
|
|
136
|
+
`;
|
|
137
|
+
const resp = await (0, rds_1.queryTable)(sql, [planId.toString()]);
|
|
138
|
+
if (resp && Array.isArray(resp.results) && resp.results.length > 0) {
|
|
139
|
+
return resp.results[0];
|
|
140
|
+
}
|
|
141
|
+
return undefined;
|
|
142
|
+
};
|
|
143
|
+
/**
|
|
144
|
+
* Fetches the Project information needed to construct the DMP Common Standard
|
|
145
|
+
* from the MySQL database
|
|
146
|
+
*
|
|
147
|
+
* @param projectId the Project ID to fetch the Project information for
|
|
148
|
+
* @returns the Project information needed to construct the DMP Common Standard
|
|
149
|
+
*/
|
|
150
|
+
const loadProjectInfo = async (projectId) => {
|
|
151
|
+
const sql = `
|
|
152
|
+
SELECT id, title, abstractText, startDate, endDate
|
|
153
|
+
FROM projects
|
|
154
|
+
WHERE id = ?
|
|
155
|
+
`;
|
|
156
|
+
const resp = await (0, rds_1.queryTable)(sql, [projectId.toString()]);
|
|
157
|
+
if (resp && Array.isArray(resp.results) && resp.results.length > 0) {
|
|
158
|
+
return resp.results[0];
|
|
159
|
+
}
|
|
160
|
+
return undefined;
|
|
161
|
+
};
|
|
162
|
+
/**
|
|
163
|
+
* Fetches the PlanFunding information needed to construct the DMP Common Standard
|
|
164
|
+
* from the MySQL database
|
|
165
|
+
*
|
|
166
|
+
* @param planId the Plan ID to fetch the PlanFunding information for
|
|
167
|
+
* @returns the Funding information needed to construct the DMP Common Standard
|
|
168
|
+
*/
|
|
169
|
+
const loadFundingInfo = async (planId) => {
|
|
170
|
+
const sql = `
|
|
171
|
+
SELECT pf.id, a.uri, a.name, prf.status, prf.grantId,
|
|
172
|
+
prf.funderProjectNumber, prf.funderOpportunityNumber
|
|
173
|
+
FROM planFundings pf
|
|
174
|
+
LEFT JOIN projectFundings prf ON pf.projectFundingId = prf.id
|
|
175
|
+
LEFT JOIN affiliations a ON prf.affiliationId = a.uri
|
|
176
|
+
WHERE pf.planId = ?
|
|
177
|
+
`;
|
|
178
|
+
const resp = await (0, rds_1.queryTable)(sql, [planId.toString()]);
|
|
179
|
+
if (resp && Array.isArray(resp.results) && resp.results.length > 0) {
|
|
180
|
+
const fundings = resp.results.filter((row) => !(0, general_1.isNullOrUndefined)(row));
|
|
181
|
+
fundings.forEach((funding) => funding.status = planFundingStatusToDMPFundingStatus(funding.status));
|
|
182
|
+
return resp.results;
|
|
183
|
+
}
|
|
184
|
+
return [];
|
|
185
|
+
};
|
|
186
|
+
/**
|
|
187
|
+
* Fetches the Plan's owner information needed to construct the DMP Common Standard
|
|
188
|
+
* from the MySQL database
|
|
189
|
+
*
|
|
190
|
+
* @param ownerId the user id for the plan's owner
|
|
191
|
+
* @returns the contact information needed to construct the DMP Common Standard
|
|
192
|
+
*/
|
|
193
|
+
async function loadContactFromPlanOwner(ownerId) {
|
|
194
|
+
const sql = `
|
|
195
|
+
SELECT u.id, u.givenName, u.surName, u.orcid, a.uri, a.name,
|
|
196
|
+
(SELECT ue.email
|
|
197
|
+
FROM userEmails ue
|
|
198
|
+
WHERE ue.userId = u.id AND ue.isPrimary = 1 LIMIT 1) as email
|
|
199
|
+
FROM users u
|
|
200
|
+
LEFT JOIN affiliations a ON u.affiliationId = a.id
|
|
201
|
+
WHERE u.id = ?
|
|
202
|
+
`;
|
|
203
|
+
const resp = await (0, rds_1.queryTable)(sql, [ownerId.toString()]);
|
|
204
|
+
if (resp && Array.isArray(resp.results) && resp.results.length > 0) {
|
|
205
|
+
return resp.results.filter((row) => !(0, general_1.isNullOrUndefined)(row))[0];
|
|
206
|
+
}
|
|
207
|
+
return undefined;
|
|
208
|
+
}
|
|
209
|
+
/**
|
|
210
|
+
* Fetches the PlanMember information needed to construct the DMP Common Standard
|
|
211
|
+
* from the MySQL database
|
|
212
|
+
*
|
|
213
|
+
* @param planId the Plan ID to fetch the PlanMember information for
|
|
214
|
+
* @returns the contributor information needed to construct the DMP Common Standard
|
|
215
|
+
*/
|
|
216
|
+
const loadMemberInfo = async (planId) => {
|
|
217
|
+
const sql = `
|
|
218
|
+
SELECT pc.id, a.uri, a.name, pctr.email, pctr.givenName, pctr.surName,
|
|
219
|
+
pctr.orcid, pc.isPrimaryContact, GROUP_CONCAT(r.uri) as roles
|
|
220
|
+
FROM planMembers pc
|
|
221
|
+
LEFT JOIN planMemberRoles pcr ON pc.id = pcr.planMemberId
|
|
222
|
+
LEFT JOIN memberRoles r ON pcr.memberRoleId = r.id
|
|
223
|
+
LEFT JOIN projectMembers pctr ON pc.projectMemberId = pctr.id
|
|
224
|
+
LEFT JOIN affiliations a ON pctr.affiliationId = a.uri
|
|
225
|
+
WHERE pc.planId = ?
|
|
226
|
+
GROUP BY a.uri, a.name, pctr.email, pctr.givenName, pctr.surName,
|
|
227
|
+
pctr.orcid, pc.isPrimaryContact;
|
|
228
|
+
`;
|
|
229
|
+
const resp = await (0, rds_1.queryTable)(sql, [planId.toString()]);
|
|
230
|
+
if (resp && Array.isArray(resp.results) && resp.results.length > 0) {
|
|
231
|
+
return resp.results.filter((row) => !(0, general_1.isNullOrUndefined)(row));
|
|
232
|
+
}
|
|
233
|
+
return [];
|
|
234
|
+
};
|
|
235
|
+
/**
|
|
236
|
+
* Returns a default RDA Common Standard Dataset entry for the DMP.
|
|
237
|
+
* This is used when the Plan has no Answers to a Research Outputs question.
|
|
238
|
+
*
|
|
239
|
+
* @param projectId the Project ID to use for the Dataset entry
|
|
240
|
+
* @param planId the Plan ID to use for the Dataset entry
|
|
241
|
+
* @returns a generic default Dataset entry
|
|
242
|
+
*/
|
|
243
|
+
const defaultDataset = (projectId, planId) => {
|
|
244
|
+
return {
|
|
245
|
+
dataset_id: {
|
|
246
|
+
identifier: `${internalIdBase(projectId, planId)}.outputs.1`,
|
|
247
|
+
type: 'other'
|
|
248
|
+
},
|
|
249
|
+
personal_data: 'unknown',
|
|
250
|
+
sensitive_data: 'unknown',
|
|
251
|
+
title: 'Generic Dataset',
|
|
252
|
+
type: 'dataset'
|
|
253
|
+
};
|
|
254
|
+
};
|
|
255
|
+
/**
|
|
256
|
+
* Fetches the Dataset information needed to construct the DMP Common Standard
|
|
257
|
+
* from the MySQL database this information is extracted from the Answers table
|
|
258
|
+
* for Research Output Questions
|
|
259
|
+
*
|
|
260
|
+
* @param projectId the Project ID to fetch the Dataset information for
|
|
261
|
+
* @param planId the Plan ID to fetch the Dataset information for
|
|
262
|
+
* @param language the language to use for the Dataset information
|
|
263
|
+
* @returns the dataset information needed to construct the DMP Common Standard
|
|
264
|
+
*/
|
|
265
|
+
const loadDatasetInfo = async (projectId, planId, language = 'eng') => {
|
|
266
|
+
const datasets = [];
|
|
267
|
+
const sql = `
|
|
268
|
+
SELECT a.json
|
|
269
|
+
FROM answers a
|
|
270
|
+
WHERE a.planId = ?
|
|
271
|
+
AND a.json LIKE '%"researchOutputsTable"%';
|
|
272
|
+
`;
|
|
273
|
+
const resp = await (0, rds_1.queryTable)(sql, [planId.toString()]);
|
|
274
|
+
// There would typically only be one research outputs question per plan but
|
|
275
|
+
// we need to allow for multiples just in case.
|
|
276
|
+
if (resp && Array.isArray(resp.results) && resp.results.length > 0) {
|
|
277
|
+
for (const result of resp.results) {
|
|
278
|
+
// Extract the column headings and the rows
|
|
279
|
+
const json = result.json ? JSON.parse(result.json) : {};
|
|
280
|
+
const lang = language === 'eng' ? 'eng' : convertFiveCharToThreeChar(language);
|
|
281
|
+
// Loop through the rows and construct the RDA Common Standard Dataset object
|
|
282
|
+
for (let idx = 0; idx < json.answer.length; idx++) {
|
|
283
|
+
const row = json.answer[idx];
|
|
284
|
+
datasets.push(buildDataset(idx, row, projectId, planId, lang));
|
|
285
|
+
}
|
|
286
|
+
}
|
|
287
|
+
}
|
|
288
|
+
else {
|
|
289
|
+
return [defaultDataset(projectId, planId)];
|
|
290
|
+
}
|
|
291
|
+
return datasets;
|
|
292
|
+
};
|
|
293
|
+
/**
|
|
294
|
+
* Builds the RDA Common Standard Related Identifier entries for the DMP
|
|
295
|
+
*
|
|
296
|
+
* @param projectId the Project ID to fetch the Related Works information for
|
|
297
|
+
* @returns the RDA Common Standard Related Identifier entries for the DMP
|
|
298
|
+
*/
|
|
299
|
+
const loadRelatedWorksInfo = async (projectId) => {
|
|
300
|
+
const sql = `
|
|
301
|
+
SELECT w.doi AS identifier, LOWER(wv.workType) AS workType,
|
|
302
|
+
FROM relatedWorks rw
|
|
303
|
+
JOIN workVersions wv ON rw.workVersionId = wv.id
|
|
304
|
+
JOIN works w ON wv.workId = w.id
|
|
305
|
+
WHERE rw.projectId = ?;
|
|
306
|
+
`;
|
|
307
|
+
const resp = await (0, rds_1.queryTable)(sql, [projectId.toString()]);
|
|
308
|
+
if (resp && Array.isArray(resp.results) && resp.results.length > 0) {
|
|
309
|
+
const works = resp.results.filter((row) => !(0, general_1.isNullOrUndefined)(row));
|
|
310
|
+
// Determine the identifier types
|
|
311
|
+
return works.map((work) => {
|
|
312
|
+
var _a;
|
|
313
|
+
return {
|
|
314
|
+
relation_type: 'cites',
|
|
315
|
+
identifier: work.identifier,
|
|
316
|
+
type: determineIdentifierType(work.identifier),
|
|
317
|
+
resource_type: (_a = work.workType) === null || _a === void 0 ? void 0 : _a.toLowerCase(),
|
|
318
|
+
};
|
|
319
|
+
});
|
|
320
|
+
}
|
|
321
|
+
return [];
|
|
322
|
+
};
|
|
323
|
+
/**
|
|
324
|
+
* Builds the DMP Tool Narrative extension for the DMP
|
|
325
|
+
*
|
|
326
|
+
* @param planId the Plan ID to fetch the narrative information for
|
|
327
|
+
* @returns the DMP Tool Narrative extension for the DMP
|
|
328
|
+
*/
|
|
329
|
+
const loadNarrativeTemplateInfo = async (planId) => {
|
|
330
|
+
// Fetch the template, sections, questions and answers all at once
|
|
331
|
+
const sql = `
|
|
332
|
+
SELECT t.id templateId, t.name templateTitle, t.description templateDescription,
|
|
333
|
+
t.version templateVersion,
|
|
334
|
+
s.id sectionId, s.name sectionTitle, s.introduction sectionDescription,
|
|
335
|
+
s.displayOrder sectionOrder, q.id questionId, q.questionText questionText,
|
|
336
|
+
q.displayOrder questionOrder, a.id answerId, a.json answerJSON
|
|
337
|
+
FROM plans p
|
|
338
|
+
INNER JOIN versionedTemplates t ON p.versionedTemplateId = t.id
|
|
339
|
+
LEFT JOIN versionedSections s ON s.versionedTemplateId = t.id
|
|
340
|
+
LEFT JOIN versionedQuestions q ON q.versionedSectionId = s.id
|
|
341
|
+
LEFT JOIN answers a ON a.versionedQuestionId = q.id AND p.id = a.planId
|
|
342
|
+
WHERE p.id = ?
|
|
343
|
+
ORDER BY s.displayOrder, q.displayOrder;
|
|
344
|
+
`;
|
|
345
|
+
const resp = await (0, rds_1.queryTable)(sql, [planId.toString()]);
|
|
346
|
+
let results = [];
|
|
347
|
+
// Filter out any null or undefined results
|
|
348
|
+
if (resp && Array.isArray(resp.results) && resp.results.length > 0) {
|
|
349
|
+
results = resp.results.filter((row) => !(0, general_1.isNullOrUndefined)(row));
|
|
350
|
+
}
|
|
351
|
+
if (!Array.isArray(results) || results.length === 0) {
|
|
352
|
+
return undefined;
|
|
353
|
+
}
|
|
354
|
+
// Sort the questions by display order
|
|
355
|
+
results[0].section.forEach((section) => {
|
|
356
|
+
section.question.sort((a, b) => a.questionOrder - b.questionOrder);
|
|
357
|
+
});
|
|
358
|
+
// Sort the sections by display order
|
|
359
|
+
results[0].section.sort((a, b) => {
|
|
360
|
+
return a.sectionOrder - b.sectionOrder;
|
|
361
|
+
});
|
|
362
|
+
return {
|
|
363
|
+
id: results[0].templateId,
|
|
364
|
+
title: results[0].templateTitle,
|
|
365
|
+
description: results[0].templateDescription,
|
|
366
|
+
version: results[0].templateVersion,
|
|
367
|
+
section: results[0].section.map((section) => {
|
|
368
|
+
return {
|
|
369
|
+
id: section.sectionId,
|
|
370
|
+
title: section.sectionTitle,
|
|
371
|
+
description: section.sectionDescription,
|
|
372
|
+
order: section.sectionOrder,
|
|
373
|
+
question: section.question.map((question) => {
|
|
374
|
+
return {
|
|
375
|
+
id: question.questionId,
|
|
376
|
+
order: question.questionOrder,
|
|
377
|
+
text: question.questionText,
|
|
378
|
+
answer: {
|
|
379
|
+
id: question.answerId,
|
|
380
|
+
json: question.answerJSON
|
|
381
|
+
}
|
|
382
|
+
};
|
|
383
|
+
})
|
|
384
|
+
};
|
|
385
|
+
})
|
|
386
|
+
};
|
|
387
|
+
};
|
|
388
|
+
/**
|
|
389
|
+
* Builds the RDA Common Standard Contact entry for the DMP
|
|
390
|
+
*
|
|
391
|
+
* @param plan the Plan information retrieve from the MySQL database
|
|
392
|
+
* @param members the PlanMembers information retrieve from the MySQL database
|
|
393
|
+
* @returns the RDA Common Standard Contact entry for the DMP
|
|
394
|
+
* @throws DMPValidationError if no primary contact is found for the DMP
|
|
395
|
+
*/
|
|
396
|
+
const buildContact = async (plan, members) => {
|
|
397
|
+
// Extract the primary contact from the members
|
|
398
|
+
const memberContact = members.find((c) => {
|
|
399
|
+
return c === null || c === void 0 ? void 0 : c.isPrimaryContact;
|
|
400
|
+
});
|
|
401
|
+
// If no primary contact is available, use the plan owner
|
|
402
|
+
const primary = memberContact && memberContact.email
|
|
403
|
+
? memberContact
|
|
404
|
+
: await loadContactFromPlanOwner(Number(plan.createdById));
|
|
405
|
+
if (primary && primary.email) {
|
|
406
|
+
const orcid = primary.orcid ? formatORCID(primary.orcid) : null;
|
|
407
|
+
// Build the contact entry for the DMP
|
|
408
|
+
const contactEntry = {
|
|
409
|
+
contact_id: [{
|
|
410
|
+
identifier: orcid !== null ? orcid : primary.email,
|
|
411
|
+
type: orcid !== null ? determineIdentifierType(orcid) : 'other',
|
|
412
|
+
}],
|
|
413
|
+
mbox: primary.email,
|
|
414
|
+
name: [primary.givenName, primary.surName].filter((n) => n).join(' ').trim(),
|
|
415
|
+
};
|
|
416
|
+
// Add the affiliation to the Contact if it exists
|
|
417
|
+
if (primary.name) {
|
|
418
|
+
const contactAffiliation = {
|
|
419
|
+
name: primary.name
|
|
420
|
+
};
|
|
421
|
+
// Add the URI if it exists
|
|
422
|
+
if (primary.uri) {
|
|
423
|
+
contactAffiliation.affiliation_id = {
|
|
424
|
+
identifier: primary.uri,
|
|
425
|
+
type: determineIdentifierType(primary.uri),
|
|
426
|
+
};
|
|
427
|
+
}
|
|
428
|
+
contactEntry.affiliation = [contactAffiliation];
|
|
429
|
+
}
|
|
430
|
+
return contactEntry;
|
|
431
|
+
}
|
|
432
|
+
else {
|
|
433
|
+
throw new DMPValidationError('Unable to find a primary contact for the DMP');
|
|
434
|
+
}
|
|
435
|
+
};
|
|
436
|
+
/**
|
|
437
|
+
* Builds the RDA Common Standard Contributor array for the DMP from the PlanMembers
|
|
438
|
+
*
|
|
439
|
+
* @param planId the Plan ID
|
|
440
|
+
* @param projectId the Project ID
|
|
441
|
+
* @param members the PlanMembers information retrieve from the MySQL database
|
|
442
|
+
* @param defaultRole the default role to use if the member doesn't have a role'
|
|
443
|
+
* @returns the RDA Common Standard Contributor array for the DMP
|
|
444
|
+
*/
|
|
445
|
+
const buildContributors = (planId, projectId, members, defaultRole) => {
|
|
446
|
+
if (!Array.isArray(members) || members.length <= 0) {
|
|
447
|
+
return [];
|
|
448
|
+
}
|
|
449
|
+
return members.map((member) => {
|
|
450
|
+
// Make sure that we always have roles as an array
|
|
451
|
+
const roles = member.roles && member.roles.includes('[')
|
|
452
|
+
? JSON.parse(member.roles)
|
|
453
|
+
: [defaultRole];
|
|
454
|
+
// Combine the member's given name and surname into a single name'
|
|
455
|
+
const contrib = {
|
|
456
|
+
name: [member.givenName, member.surName]
|
|
457
|
+
.filter((n) => n).join(' ').trim(),
|
|
458
|
+
role: roles,
|
|
459
|
+
};
|
|
460
|
+
// Use the member's ORCID if it exists, otherwise generate a new one'
|
|
461
|
+
const formatted = member.orcid ? formatORCID(member.orcid) : null;
|
|
462
|
+
if (formatted !== null) {
|
|
463
|
+
contrib.contributor_id = [{
|
|
464
|
+
identifier: formatted,
|
|
465
|
+
type: determineIdentifierType(formatted)
|
|
466
|
+
}];
|
|
467
|
+
}
|
|
468
|
+
else {
|
|
469
|
+
// RDA Common Standard requires an id so generate one
|
|
470
|
+
contrib.contributor_id = [{
|
|
471
|
+
identifier: `${internalIdBase(projectId, planId)}.members.${member.id}`,
|
|
472
|
+
type: 'other'
|
|
473
|
+
}];
|
|
474
|
+
}
|
|
475
|
+
// Add the affiliation to the Contributor if it exists
|
|
476
|
+
if (member.name && member.uri) {
|
|
477
|
+
contrib.affiliation = [{
|
|
478
|
+
name: member.name,
|
|
479
|
+
affiliation_id: {
|
|
480
|
+
identifier: member.uri,
|
|
481
|
+
type: determineIdentifierType(member.uri),
|
|
482
|
+
}
|
|
483
|
+
}];
|
|
484
|
+
}
|
|
485
|
+
return contrib;
|
|
486
|
+
});
|
|
487
|
+
};
|
|
488
|
+
/**
|
|
489
|
+
* Builds the DMP Tool extensions to the RDA Common Standard
|
|
490
|
+
*
|
|
491
|
+
* @param plan the Plan information retrieve from the MySQL database
|
|
492
|
+
* @param project the Project information retrieve from the MySQL database
|
|
493
|
+
* @param funding the Funding information retrieve from the MySQL database
|
|
494
|
+
* @returns the DMP metadata with extensions from the DMP Tool
|
|
495
|
+
*/
|
|
496
|
+
const buildDMPToolExtensions = async (plan, project, funding) => {
|
|
497
|
+
var _a, _b, _c, _d, _e, _f;
|
|
498
|
+
const extensions = {
|
|
499
|
+
rda_schema_version: types_1.RDA_COMMON_STANDARD_VERSION,
|
|
500
|
+
// Ignoring the `!` assertion here because we know we check the env variable
|
|
501
|
+
// when the entrypoint function is called.
|
|
502
|
+
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
|
|
503
|
+
provenance: process.env.APPLICATION_NAME,
|
|
504
|
+
featured: plan.featured ? 'yes' : 'no',
|
|
505
|
+
privacy: (_b = (_a = plan.visibility) === null || _a === void 0 ? void 0 : _a.toLowerCase()) !== null && _b !== void 0 ? _b : 'private',
|
|
506
|
+
status: (_d = (_c = plan.status) === null || _c === void 0 ? void 0 : _c.toLowerCase()) !== null && _d !== void 0 ? _d : 'draft',
|
|
507
|
+
};
|
|
508
|
+
// Generate the DMP Narrative
|
|
509
|
+
const narrative = await loadNarrativeTemplateInfo(plan.id);
|
|
510
|
+
// Fetch the research domain if one was specified
|
|
511
|
+
const research_domain = project.dmptool_research_domain
|
|
512
|
+
? { name: project.dmptool_research_domain }
|
|
513
|
+
: undefined;
|
|
514
|
+
let funderProject = undefined;
|
|
515
|
+
let funderOpportunity = undefined;
|
|
516
|
+
if (funding) {
|
|
517
|
+
const projectId = {
|
|
518
|
+
identifier: internalIdBase(project.id, plan.id),
|
|
519
|
+
type: maDMPTypes_1.StandardIdentifierType.OTHER
|
|
520
|
+
};
|
|
521
|
+
const funderId = funding.uri === undefined
|
|
522
|
+
? {
|
|
523
|
+
identifier: `${internalIdBase(project.id, plan.id)}.fundings.${funding.id}`,
|
|
524
|
+
type: maDMPTypes_1.StandardIdentifierType.OTHER
|
|
525
|
+
}
|
|
526
|
+
: {
|
|
527
|
+
identifier: funding.uri,
|
|
528
|
+
type: funding.uri.match(ROR_REGEX)
|
|
529
|
+
? maDMPTypes_1.StandardIdentifierType.ROR
|
|
530
|
+
: maDMPTypes_1.StandardIdentifierType.URL
|
|
531
|
+
};
|
|
532
|
+
// Define the Funder's project number if applicable. project_id and funder_id
|
|
533
|
+
// are used to help tie the project_identifier to the correct
|
|
534
|
+
// project[?].funding[?] in the RDA Common Standard.
|
|
535
|
+
if (funding.funderProjectNumber !== undefined) {
|
|
536
|
+
funderProject = {
|
|
537
|
+
project_id: projectId,
|
|
538
|
+
funder_id: funderId,
|
|
539
|
+
project_identifier: {
|
|
540
|
+
identifier: funding.funderProjectNumber,
|
|
541
|
+
type: ((_e = funding.funderProjectNumber) === null || _e === void 0 ? void 0 : _e.startsWith('http'))
|
|
542
|
+
? maDMPTypes_1.StandardIdentifierType.URL
|
|
543
|
+
: maDMPTypes_1.StandardIdentifierType.OTHER
|
|
544
|
+
}
|
|
545
|
+
};
|
|
546
|
+
}
|
|
547
|
+
// Define the Funder's opportunity number if applicable. project_id and
|
|
548
|
+
// funder_id are used to help tie the opportunity_identifier to the correct
|
|
549
|
+
// project[?].funding[?] in the RDA Common Standard.
|
|
550
|
+
if (funding.funderOpportunityNumber !== undefined) {
|
|
551
|
+
funderOpportunity = {
|
|
552
|
+
project_id: projectId,
|
|
553
|
+
funder_id: funderId,
|
|
554
|
+
opportunity_identifier: {
|
|
555
|
+
identifier: funding.funderOpportunityNumber,
|
|
556
|
+
type: ((_f = funding.funderOpportunityNumber) === null || _f === void 0 ? void 0 : _f.startsWith('http'))
|
|
557
|
+
? maDMPTypes_1.StandardIdentifierType.URL
|
|
558
|
+
: maDMPTypes_1.StandardIdentifierType.OTHER
|
|
559
|
+
}
|
|
560
|
+
};
|
|
561
|
+
}
|
|
562
|
+
}
|
|
563
|
+
// Only add these properties if they have values we don't want 'undefined' or
|
|
564
|
+
// 'null' in the JSON
|
|
565
|
+
const regDate = (0, general_1.convertMySQLDateTimeToRFC3339)(plan.registered);
|
|
566
|
+
if (!(0, general_1.isNullOrUndefined)(plan.registered) && regDate !== null) {
|
|
567
|
+
extensions.registered = regDate;
|
|
568
|
+
}
|
|
569
|
+
if (!(0, general_1.isNullOrUndefined)(project.dmptool_research_domain) && research_domain !== undefined) {
|
|
570
|
+
extensions.research_domain = research_domain;
|
|
571
|
+
}
|
|
572
|
+
if (funderProject !== undefined) {
|
|
573
|
+
extensions.funding_project = [funderProject];
|
|
574
|
+
}
|
|
575
|
+
if (funderOpportunity !== undefined) {
|
|
576
|
+
extensions.funding_opportunity = [funderOpportunity];
|
|
577
|
+
}
|
|
578
|
+
if (!(0, general_1.isNullOrUndefined)(narrative)) {
|
|
579
|
+
extensions.narrative = {
|
|
580
|
+
download_url: `https://${process.env.DOMAIN_NAME}/dmps/${plan.dmpId}/narrative`,
|
|
581
|
+
template: narrative
|
|
582
|
+
};
|
|
583
|
+
}
|
|
584
|
+
return extensions;
|
|
585
|
+
};
|
|
586
|
+
/**
|
|
587
|
+
* Builds the Project and Funding info for the RDA Common Standard
|
|
588
|
+
*
|
|
589
|
+
* @param planId the Plan ID
|
|
590
|
+
* @param project the Project information retrieve from the MySQL database
|
|
591
|
+
* @param funding the Funding information retrieve from the MySQL database
|
|
592
|
+
* @returns the Project and Funding info for the RDA Common Standard
|
|
593
|
+
*/
|
|
594
|
+
const buildProject = (planId, project, funding) => {
|
|
595
|
+
var _a, _b, _c, _d, _e;
|
|
596
|
+
if ((0, general_1.isNullOrUndefined)(project)) {
|
|
597
|
+
return undefined;
|
|
598
|
+
}
|
|
599
|
+
let fundingObject = undefined;
|
|
600
|
+
if (funding && funding.name && funding.status) {
|
|
601
|
+
const grantIdObject = funding.grantId
|
|
602
|
+
? {
|
|
603
|
+
identifier: funding.grantId,
|
|
604
|
+
type: ((_a = funding.grantId) === null || _a === void 0 ? void 0 : _a.startsWith('http')) ? 'url' : 'other'
|
|
605
|
+
}
|
|
606
|
+
: undefined;
|
|
607
|
+
// RDA Common Standard requires the funder id to be present
|
|
608
|
+
const funderIdObject = funding.uri
|
|
609
|
+
? {
|
|
610
|
+
identifier: funding.uri,
|
|
611
|
+
type: ((_b = (funding).uri) === null || _b === void 0 ? void 0 : _b.match(ROR_REGEX)) ? 'ror' : 'url'
|
|
612
|
+
}
|
|
613
|
+
: {
|
|
614
|
+
identifier: `${internalIdBase(project.id, planId)}.fundings.${funding.id}`,
|
|
615
|
+
type: 'other'
|
|
616
|
+
};
|
|
617
|
+
// The RDA Common Standard requires the funder name and status to be present
|
|
618
|
+
fundingObject = !(0, general_1.isNullOrUndefined)(funding)
|
|
619
|
+
? [{
|
|
620
|
+
name: (funding).name,
|
|
621
|
+
funding_status: planFundingStatusToDMPFundingStatus((funding).status),
|
|
622
|
+
grant_id: grantIdObject,
|
|
623
|
+
funder_id: funderIdObject
|
|
624
|
+
}]
|
|
625
|
+
: undefined;
|
|
626
|
+
if (grantIdObject !== undefined && fundingObject !== undefined) {
|
|
627
|
+
fundingObject[0].grant_id = grantIdObject;
|
|
628
|
+
}
|
|
629
|
+
}
|
|
630
|
+
return {
|
|
631
|
+
title: project.title,
|
|
632
|
+
description: (_c = project.abstractText) !== null && _c !== void 0 ? _c : undefined,
|
|
633
|
+
start: (_d = project.startDate) !== null && _d !== void 0 ? _d : undefined,
|
|
634
|
+
end: (_e = project.endDate) !== null && _e !== void 0 ? _e : undefined,
|
|
635
|
+
project_id: [{
|
|
636
|
+
identifier: internalIdBase(project.id, planId),
|
|
637
|
+
type: 'other'
|
|
638
|
+
}],
|
|
639
|
+
funding: fundingObject
|
|
640
|
+
};
|
|
641
|
+
};
|
|
642
|
+
/**
|
|
643
|
+
* Extracts the specified column from the columns of a ResearchOutputTable answer row.
|
|
644
|
+
* @param id the ID of the column to extract
|
|
645
|
+
* @param columns the columns of the answer row
|
|
646
|
+
* @returns the specified column if it exists, otherwise undefined
|
|
647
|
+
*/
|
|
648
|
+
const findColumnById = (id, columns) => {
|
|
649
|
+
return columns.find((col) => {
|
|
650
|
+
return (col === null || col === void 0 ? void 0 : col.commonStandardId) === id;
|
|
651
|
+
});
|
|
652
|
+
};
|
|
653
|
+
/**
|
|
654
|
+
* Converts the size + context into a byte size.
|
|
655
|
+
*
|
|
656
|
+
* @param size the NumberWithContextAnswerType
|
|
657
|
+
* (e.g. `{ answer: { context: 'GB', value: 5 } }`) to convert
|
|
658
|
+
* @returns the byte size if it could be converted, otherwise undefined
|
|
659
|
+
*/
|
|
660
|
+
const byteSizeToBytes = (size) => {
|
|
661
|
+
var _a;
|
|
662
|
+
if ((0, general_1.isNullOrUndefined)(size) || (0, general_1.isNullOrUndefined)(size.answer.value)) {
|
|
663
|
+
return undefined;
|
|
664
|
+
}
|
|
665
|
+
const multipliers = {
|
|
666
|
+
'TB': 1e12,
|
|
667
|
+
'GB': 1e9,
|
|
668
|
+
'MB': 1e6,
|
|
669
|
+
'KB': 1e3,
|
|
670
|
+
};
|
|
671
|
+
const context = size.answer.context.toUpperCase();
|
|
672
|
+
// If the context has a match in our multipliers, use it, otherwise use 1 as a fallback
|
|
673
|
+
const multiplier = (_a = multipliers[context]) !== null && _a !== void 0 ? _a : 1;
|
|
674
|
+
return size.answer.value * multiplier;
|
|
675
|
+
};
|
|
676
|
+
/**
|
|
677
|
+
* Convert a @dmptool/types researchOutputTable answer row into an RDA Common
|
|
678
|
+
* Standard Dataset object.
|
|
679
|
+
*
|
|
680
|
+
* @param rowIdx the index of the answer row
|
|
681
|
+
* @param row the answer row
|
|
682
|
+
* @param projectId the ID of the project that the dataset belongs to
|
|
683
|
+
* @param planId the ID of the plan that the dataset belongs to
|
|
684
|
+
* @param language the language of the dataset (defaults to 'eng')
|
|
685
|
+
* @returns a RDA Common Standard Dataset object
|
|
686
|
+
*/
|
|
687
|
+
const buildDataset = (rowIdx, row, projectId, planId, language = 'eng') => {
|
|
688
|
+
const title = findColumnById('title', row.columns);
|
|
689
|
+
const desc = findColumnById('description', row.columns);
|
|
690
|
+
const typ = findColumnById('type', row.columns);
|
|
691
|
+
const flags = findColumnById('data_flags', row.columns);
|
|
692
|
+
const access_date = findColumnById('issued', row.columns);
|
|
693
|
+
const access = findColumnById('data_access', row.columns);
|
|
694
|
+
const byte_size = findColumnById('byte_size', row.columns);
|
|
695
|
+
const host = findColumnById('host', row.columns);
|
|
696
|
+
const meta = findColumnById('metadata', row.columns);
|
|
697
|
+
const license = findColumnById('license_ref', row.columns);
|
|
698
|
+
// The large RDA Common Standard Research Output representation.
|
|
699
|
+
// Any properties that are commented out are ones that we do not currently support.
|
|
700
|
+
// Build the Metadata object from the Metadata Standards defined on the Research Output
|
|
701
|
+
const metadata = (0, general_1.isNullOrUndefined)(meta)
|
|
702
|
+
? undefined
|
|
703
|
+
: meta.answer.map((m) => {
|
|
704
|
+
return {
|
|
705
|
+
description: m.metadataStandardName,
|
|
706
|
+
// RDA Common Standard requires the language, but we can't get it from
|
|
707
|
+
// the metadata standard repository we use, so just default it
|
|
708
|
+
language: 'eng',
|
|
709
|
+
metadata_standard_id: [{
|
|
710
|
+
identifier: m.metadataStandardId,
|
|
711
|
+
type: determineIdentifierType(m.metadataStandardId)
|
|
712
|
+
}]
|
|
713
|
+
};
|
|
714
|
+
});
|
|
715
|
+
// Get the Anticipated Release Date for the dataset
|
|
716
|
+
const issued = (0, general_1.isNullOrUndefined)(access_date) ? undefined : access_date.answer;
|
|
717
|
+
// Build the License object from the Licenses defined on the Research Output
|
|
718
|
+
const licenses = (0, general_1.isNullOrUndefined)(license)
|
|
719
|
+
? undefined
|
|
720
|
+
: license.answer.map((l) => {
|
|
721
|
+
return {
|
|
722
|
+
license_ref: l.licenseId,
|
|
723
|
+
start_date: issued
|
|
724
|
+
};
|
|
725
|
+
});
|
|
726
|
+
// Build the Distribution object from the Repositories defined on the Research Output
|
|
727
|
+
const distribution = (0, general_1.isNullOrUndefined)(host)
|
|
728
|
+
? undefined
|
|
729
|
+
: host.answer.map((h) => {
|
|
730
|
+
return {
|
|
731
|
+
title: (0, general_1.isNullOrUndefined)(title) ? `Dataset ${rowIdx + 1}` : title.answer,
|
|
732
|
+
// description: 'This is a test distribution',
|
|
733
|
+
// access_url: 'https://example.com/dataset/123/distribution/123456789',
|
|
734
|
+
// download_url: 'https://example.com/dataset/123/distribution/123456789/download',
|
|
735
|
+
byte_size: (0, general_1.isNullOrUndefined)(byte_size) ? undefined : byteSizeToBytes(byte_size),
|
|
736
|
+
// format: ['application/zip'],
|
|
737
|
+
data_access: (0, general_1.isNullOrUndefined)(access) ? 'restricted' : access.answer,
|
|
738
|
+
issued,
|
|
739
|
+
license: licenses,
|
|
740
|
+
host: {
|
|
741
|
+
title: h.repositoryName,
|
|
742
|
+
// description: 'This is a test host',
|
|
743
|
+
url: h.repositoryId,
|
|
744
|
+
host_id: [{
|
|
745
|
+
identifier: h.repositoryId,
|
|
746
|
+
type: determineIdentifierType(h.repositoryId)
|
|
747
|
+
}],
|
|
748
|
+
// availability: '99.99',
|
|
749
|
+
// backup_frequency: 'weekly',
|
|
750
|
+
// backup_type: 'tapes',
|
|
751
|
+
// certified_with: 'coretrustseal',
|
|
752
|
+
// geo_location: 'US',
|
|
753
|
+
// pid_system: ['doi', 'ark'],
|
|
754
|
+
// storage_type: 'LTO-8 tape',
|
|
755
|
+
// support_versioning: 'yes'
|
|
756
|
+
}
|
|
757
|
+
};
|
|
758
|
+
});
|
|
759
|
+
return {
|
|
760
|
+
title: (0, general_1.isNullOrUndefined)(title) ? `Dataset ${rowIdx + 1}` : title.answer,
|
|
761
|
+
type: (0, general_1.isNullOrUndefined)(typ) ? 'dataset' : typ.answer,
|
|
762
|
+
description: (0, general_1.isNullOrUndefined)(desc) ? undefined : desc.answer,
|
|
763
|
+
dataset_id: {
|
|
764
|
+
identifier: `${internalIdBase(projectId, planId)}.outputs.${rowIdx + 1}`,
|
|
765
|
+
type: 'other'
|
|
766
|
+
},
|
|
767
|
+
personal_data: (0, general_1.isNullOrUndefined)(flags) ?
|
|
768
|
+
'unknown'
|
|
769
|
+
: (flags.answer.includes('personal') ? 'yes' : 'no'),
|
|
770
|
+
sensitive_data: (0, general_1.isNullOrUndefined)(flags)
|
|
771
|
+
? 'unknown'
|
|
772
|
+
: (flags.answer.includes('sensitive') ? 'yes' : 'no'),
|
|
773
|
+
issued,
|
|
774
|
+
language,
|
|
775
|
+
metadata,
|
|
776
|
+
distribution,
|
|
777
|
+
// data_quality_assurance: [''],
|
|
778
|
+
// is_reused: false,
|
|
779
|
+
// keyword: ['test', 'dataset'],
|
|
780
|
+
// preservation_statement: 'Statement about preservation',
|
|
781
|
+
// security_and_privacy: [{
|
|
782
|
+
// title: 'Security and Privacy Statement',
|
|
783
|
+
// description: 'Description of security and privacy statement'
|
|
784
|
+
// }],
|
|
785
|
+
// alternate_identifier: [{
|
|
786
|
+
// identifier: 'https://example.com/dataset/123',
|
|
787
|
+
// type: 'url'
|
|
788
|
+
// }],
|
|
789
|
+
// technical_resource: [{
|
|
790
|
+
// name: 'Test Server',
|
|
791
|
+
// description: 'This is a test server',
|
|
792
|
+
// technical_resource_id: [{
|
|
793
|
+
// identifier: 'https://example.com/server/123',
|
|
794
|
+
// type: 'url'
|
|
795
|
+
// }],
|
|
796
|
+
// }],
|
|
797
|
+
};
|
|
798
|
+
};
|
|
799
|
+
/**
|
|
800
|
+
* Validate the specified DMP metadata record against the RDA Common Standard
|
|
801
|
+
* and DMP Tool extensions schema
|
|
802
|
+
*
|
|
803
|
+
* @param dmp The DMP metadata record to validate
|
|
804
|
+
* @returns the DMP metadata record if it is valid
|
|
805
|
+
* @throws DMPValidationError if the record is invalid with the error message(s)
|
|
806
|
+
*/
|
|
807
|
+
const validateRDACommonStandard = (dmp) => {
|
|
808
|
+
const validationErrors = [];
|
|
809
|
+
const validator = new jsonschema_1.Validator();
|
|
810
|
+
// Validate against the RDA Common Standard schema
|
|
811
|
+
const rdaResult = validator.validate(dmp, types_1.RDACommonStandardDMPJSONSchema);
|
|
812
|
+
if (rdaResult && rdaResult.errors.length > 0) {
|
|
813
|
+
validationErrors.push(...rdaResult.errors.map(e => `${e.path.join('.')} - ${e.message}`));
|
|
814
|
+
}
|
|
815
|
+
if (validationErrors.length > 0) {
|
|
816
|
+
throw new DMPValidationError(`Invalid RDA Common Standard: ${validationErrors.join('; ')}`);
|
|
817
|
+
}
|
|
818
|
+
return dmp;
|
|
819
|
+
};
|
|
820
|
+
exports.validateRDACommonStandard = validateRDACommonStandard;
|
|
821
|
+
/**
|
|
822
|
+
* Validate the specified DMP metadata record against the RDA Common Standard
|
|
823
|
+
* and DMP Tool extensions schema
|
|
824
|
+
*
|
|
825
|
+
* @param dmp The DMP metadata record to validate
|
|
826
|
+
* @returns the DMP metadata record if it is valid
|
|
827
|
+
* @throws DMPValidationError if the record is invalid with the error message(s)
|
|
828
|
+
*/
|
|
829
|
+
const validateDMPToolExtensions = (dmp) => {
|
|
830
|
+
var _a;
|
|
831
|
+
const validationErrors = [];
|
|
832
|
+
// Next validate against the DMP Tool extension schema
|
|
833
|
+
const extResult = types_1.DMPToolExtensionSchema.safeParse(dmp);
|
|
834
|
+
if (extResult && !extResult.success && ((_a = extResult.error.issues) === null || _a === void 0 ? void 0 : _a.length) > 0) {
|
|
835
|
+
validationErrors.push(...extResult.error.issues.map(e => `${e.path.join('.')} - ${e.message}`));
|
|
836
|
+
}
|
|
837
|
+
if (validationErrors.length > 0) {
|
|
838
|
+
throw new DMPValidationError(`Invalid DMP Tool extensions: ${validationErrors.join('; ')}`);
|
|
839
|
+
}
|
|
840
|
+
return dmp;
|
|
841
|
+
};
|
|
842
|
+
exports.validateDMPToolExtensions = validateDMPToolExtensions;
|
|
843
|
+
/**
|
|
844
|
+
* Clean the RDA Common Standard portion of the DMP metadata record
|
|
845
|
+
*
|
|
846
|
+
* @param plan The plan information
|
|
847
|
+
* @param dmp
|
|
848
|
+
* @returns The cleaned DMP metadata record
|
|
849
|
+
*/
|
|
850
|
+
const cleanRDACommonStandard = (plan, dmp) => {
|
|
851
|
+
// Make sure some of the required properties have a default value
|
|
852
|
+
if (!dmp.dmp.created)
|
|
853
|
+
dmp.dmp.created = plan.created;
|
|
854
|
+
if (!dmp.dmp.modified)
|
|
855
|
+
dmp.dmp.modified = plan.modified;
|
|
856
|
+
if (!dmp.dmp.language)
|
|
857
|
+
dmp.dmp.language = 'eng';
|
|
858
|
+
if (!dmp.dmp.ethical_issues_exist)
|
|
859
|
+
dmp.dmp.ethical_issues_exist = 'unknown';
|
|
860
|
+
return dmp;
|
|
861
|
+
};
|
|
862
|
+
/**
|
|
863
|
+
*
|
|
864
|
+
* Generate a JSON representation for a DMP that confirms to the RDA Common Metadata
|
|
865
|
+
* standard and includes DMP Tool specific extensions to that standard.
|
|
866
|
+
*
|
|
867
|
+
* Some things of note about the JSON representation:
|
|
868
|
+
* - There are no primitive booleans, booleans are represented as: 'yes',
|
|
869
|
+
* 'no', 'unknown'
|
|
870
|
+
* - There are no primitive dates, strings formatted as 'YYYY-MM-DD hh:mm:ss:msZ'
|
|
871
|
+
* are used
|
|
872
|
+
* - The `provenance` is used to store the ID of the system that created the DMP
|
|
873
|
+
* - The `privacy` should be used to determine authorization for viewing the
|
|
874
|
+
* DMP narrative
|
|
875
|
+
* - The `featured` indicates whether the DMP is featured on the DMP Tool
|
|
876
|
+
* website's public plans page
|
|
877
|
+
* - The `registered` indicates whether the DMP is published/registered with DataCite/EZID
|
|
878
|
+
* - The `tombstoned` indicates that it was published/registered but is now removed
|
|
879
|
+
*
|
|
880
|
+
* @param planId the ID of the plan to generate the DMP for
|
|
881
|
+
* @returns a JSON representation of the DMP
|
|
882
|
+
*/
|
|
883
|
+
async function planToDMPCommonStandard(planId) {
|
|
884
|
+
if ((0, general_1.isNullOrUndefined)(process.env.ENV)) {
|
|
885
|
+
throw new Error('ENV environment variable is not set');
|
|
886
|
+
}
|
|
887
|
+
if ((0, general_1.isNullOrUndefined)(process.env.DOMAIN_NAME)) {
|
|
888
|
+
throw new Error('DOMAIN_NAME environment variable is not set');
|
|
889
|
+
}
|
|
890
|
+
if ((0, general_1.isNullOrUndefined)(process.env.APPLICATION_NAME)) {
|
|
891
|
+
throw new Error('APPLICATION_NAME environment variable is not set');
|
|
892
|
+
}
|
|
893
|
+
// Fetch the Plan data
|
|
894
|
+
const plan = await loadPlanInfo(planId);
|
|
895
|
+
if (plan === undefined) {
|
|
896
|
+
throw new DMPValidationError(`Plan not found: ${planId}`);
|
|
897
|
+
}
|
|
898
|
+
if ((0, general_1.isNullOrUndefined)(plan.title)) {
|
|
899
|
+
throw new DMPValidationError(`Plan title not found for plan: ${planId}`);
|
|
900
|
+
}
|
|
901
|
+
if ((0, general_1.isNullOrUndefined)(plan.dmpId)) {
|
|
902
|
+
throw new DMPValidationError(`DMP ID not found for plan: ${planId}`);
|
|
903
|
+
}
|
|
904
|
+
// Get the Project data
|
|
905
|
+
const project = await loadProjectInfo(plan.projectId);
|
|
906
|
+
if (project === undefined || !project.title) {
|
|
907
|
+
throw new DMPValidationError(`Project not found: ${plan.projectId}`);
|
|
908
|
+
}
|
|
909
|
+
// Get all the plan members and determine the primary contact
|
|
910
|
+
const members = plan.id ? await loadMemberInfo(plan.id) : [];
|
|
911
|
+
const contact = await buildContact(plan, members);
|
|
912
|
+
if (!contact) {
|
|
913
|
+
throw new DMPValidationError(`Could not establish a primary contact for plan: ${planId}`);
|
|
914
|
+
}
|
|
915
|
+
// Get all the funding and narrative info
|
|
916
|
+
const datasets = await loadDatasetInfo(project.id, plan.id, plan.languageId);
|
|
917
|
+
// We only allow one funding per plan at this time
|
|
918
|
+
const fundings = await loadFundingInfo(plan.id);
|
|
919
|
+
const funding = fundings.length > 0 ? fundings[0] : undefined;
|
|
920
|
+
const works = await loadRelatedWorksInfo(plan.projectId);
|
|
921
|
+
const defaultRole = await loadDefaultMemberRole();
|
|
922
|
+
// If the plan is registered, use the DOI as the identifier, otherwise convert to a URL
|
|
923
|
+
const dmpId = plan.registered
|
|
924
|
+
? {
|
|
925
|
+
identifier: plan.dmpId,
|
|
926
|
+
type: 'doi'
|
|
927
|
+
}
|
|
928
|
+
: {
|
|
929
|
+
identifier: `https://${process.env.DOMAIN_NAME}/projects/${project.id}/dmp/${plan.id}`,
|
|
930
|
+
type: 'url'
|
|
931
|
+
};
|
|
932
|
+
// Examine the datasets to determine the status of ethical issues
|
|
933
|
+
// If the datasets array only contains the default/generic dataset (meaning there
|
|
934
|
+
// were no research outputs defined) then the status is `unknown`
|
|
935
|
+
const isDefaultDataset = datasets.length === 1 && datasets[0].title === 'Generic Dataset';
|
|
936
|
+
const yesFlag = datasets.find((d) => {
|
|
937
|
+
return d.sensitive_data === 'yes' || d.personal_data === 'yes';
|
|
938
|
+
});
|
|
939
|
+
const noFlag = datasets.find((d) => {
|
|
940
|
+
return d.sensitive_data === 'no' || d.personal_data === 'no';
|
|
941
|
+
});
|
|
942
|
+
const unknownEthicalState = isDefaultDataset
|
|
943
|
+
|| ((0, general_1.isNullOrUndefined)(yesFlag) && (0, general_1.isNullOrUndefined)(noFlag));
|
|
944
|
+
// If we just
|
|
945
|
+
const hasEthicalIssues = unknownEthicalState
|
|
946
|
+
? 'unknown'
|
|
947
|
+
: ((0, general_1.isNullOrUndefined)(yesFlag) ? 'no' : 'yes');
|
|
948
|
+
// Generate the RDA Common Standard DMP metadata record
|
|
949
|
+
const dmp = {
|
|
950
|
+
dmp: {
|
|
951
|
+
title: plan.title,
|
|
952
|
+
ethical_issues_exist: hasEthicalIssues,
|
|
953
|
+
language: convertFiveCharToThreeChar(plan.languageId),
|
|
954
|
+
created: (0, general_1.convertMySQLDateTimeToRFC3339)(plan.created),
|
|
955
|
+
modified: (0, general_1.convertMySQLDateTimeToRFC3339)(plan.modified),
|
|
956
|
+
dmp_id: dmpId,
|
|
957
|
+
contact: contact,
|
|
958
|
+
dataset: datasets,
|
|
959
|
+
},
|
|
960
|
+
};
|
|
961
|
+
const dmpProject = buildProject(plan.id, project, funding);
|
|
962
|
+
const dmpContributor = buildContributors(plan.id, project.id, members, defaultRole !== null && defaultRole !== void 0 ? defaultRole : 'other');
|
|
963
|
+
// Add the contributor, project and related identifier properties if they have values
|
|
964
|
+
if (!(0, general_1.isNullOrUndefined)(dmpProject)) {
|
|
965
|
+
dmp.dmp.project = [(0, general_1.removeNullAndUndefinedFromObject)(dmpProject)];
|
|
966
|
+
}
|
|
967
|
+
if (!(0, general_1.isNullOrUndefined)(dmpContributor)
|
|
968
|
+
&& Array.isArray(dmpContributor)
|
|
969
|
+
&& dmpContributor.length > 0) {
|
|
970
|
+
dmp.dmp.contributor = (0, general_1.removeNullAndUndefinedFromObject)(dmpContributor);
|
|
971
|
+
}
|
|
972
|
+
if (!(0, general_1.isNullOrUndefined)(works) && Array.isArray(works) && works.length > 0) {
|
|
973
|
+
dmp.dmp.related_identifier = (0, general_1.removeNullAndUndefinedFromObject)(works);
|
|
974
|
+
}
|
|
975
|
+
const cleaned = cleanRDACommonStandard(plan, dmp);
|
|
976
|
+
// Generate the DMP Tool extensions to the RDA Common Standard
|
|
977
|
+
const extensions = await buildDMPToolExtensions(plan, project, funding);
|
|
978
|
+
// Return the combined DMP metadata record
|
|
979
|
+
return {
|
|
980
|
+
dmp: Object.assign(Object.assign({}, (0, exports.validateRDACommonStandard)(cleaned).dmp), (0, exports.validateDMPToolExtensions)(extensions))
|
|
981
|
+
};
|
|
982
|
+
}
|