diff --git a/package-lock.json b/package-lock.json index 92a291fda..5ffcc5441 100644 --- a/package-lock.json +++ b/package-lock.json @@ -30,7 +30,9 @@ "@types/jsonschema": "^1.1.1", "@user-office-software/duo-logger": "^2.1.1", "@user-office-software/duo-message-broker": "^1.4.0", - "ajv": "^8.12.0", + "ajv": "^8.17.1", + "ajv-formats": "^3.0.1", + "ajv-keywords": "^5.1.0", "amqplib": "^0.10.5", "bcrypt": "^6.0.0", "class-transformer": "^0.5.1", @@ -4637,7 +4639,7 @@ "version": "3.0.1", "resolved": "https://registry.npmjs.org/ajv-formats/-/ajv-formats-3.0.1.tgz", "integrity": "sha512-8iUql50EUR+uUcdRQ3HDqa6EVyo3docL8g5WJ3FNcWmu62IbkGUue/pEyLBW8VGKKucTPgqeks4fIU1DA4yowQ==", - "dev": true, + "license": "MIT", "dependencies": { "ajv": "^8.0.0" }, @@ -4654,7 +4656,7 @@ "version": "5.1.0", "resolved": "https://registry.npmjs.org/ajv-keywords/-/ajv-keywords-5.1.0.tgz", "integrity": "sha512-YCS/JNFAUyr5vAuhk1DWm1CBxRHW9LbJ2ozWeemrIqpbsqKjHVxYPyi5GC0rjZIT5JxJ3virVTS8wk4i/Z+krw==", - "dev": true, + "license": "MIT", "dependencies": { "fast-deep-equal": "^3.1.3" }, diff --git a/package.json b/package.json index 21e6223f2..2654e4aef 100644 --- a/package.json +++ b/package.json @@ -54,7 +54,9 @@ "@types/jsonschema": "^1.1.1", "@user-office-software/duo-logger": "^2.1.1", "@user-office-software/duo-message-broker": "^1.4.0", - "ajv": "^8.12.0", + "ajv": "^8.17.1", + "ajv-formats": "^3.0.1", + "ajv-keywords": "^5.1.0", "amqplib": "^0.10.5", "bcrypt": "^6.0.0", "class-transformer": "^0.5.1", diff --git a/publishedDataConfig.example.json b/publishedDataConfig.example.json index 6d09b4286..336227f3f 100644 --- a/publishedDataConfig.example.json +++ b/publishedDataConfig.example.json @@ -1,757 +1,369 @@ { "metadataSchema": { "type": "object", - "properties": { - "creators": { - "type": "array", - "minItems": 1, - "items": { - "type": "object", - "properties": { - "name": { - "type": "string", - "title": "Name", - "$dataciteRequired": true - }, - "nameType": { - "type": "string", - "title": "Name Type", - "$dataciteRequired": false, - "enum": ["Personal", "Organizational"] - }, - "givenName": { - "type": "string", - "title": "Given Name", - "$dataciteRequired": false - }, - "familyName": { - "type": "string", - "title": "Family Name", - "$dataciteRequired": false - }, - "nameIdentifiers": { - "type": "array", - "title": "Name Identifiers", - "items": { - "type": "object", - "properties": { - "nameIdentifier": { - "type": "string", - "title": "Identifier", - "$dataciteRequired": false - }, - "nameIdentifierScheme": { - "type": "string", - "title": "Identifier Scheme", - "$dataciteRequired": true - }, - "schemeUri": { - "type": "string", - "title": "Scheme URI", - "$dataciteRequired": false - } + "allOf": [ + { "dynamicDefaults": { "publicationYear": "currentYear" } }, + { + "properties": { + "creators": { + "type": "array", + "minItems": 1, + "items": { + "type": "object", + "properties": { + "name": { + "type": "string", + "title": "Name", + "$dataciteRequired": true }, - "required": ["nameIdentifierScheme"] - }, - "$dataciteRequired": false - }, - "affiliation": { - "type": "array", - "title": "Affiliations", - "items": { - "type": "object", - "properties": { - "name": { - "type": "string", - "title": "Affiliation Name", - "$dataciteRequired": true - }, - "affiliationIdentifier": { - "type": "string", - "title": "Affiliation Identifier", - "$dataciteRequired": false - }, - "affiliationIdentifierScheme": { - "type": "string", - "title": "Affiliation Identifier Scheme", - "$dataciteRequired": true - }, - "schemeUri": { - "type": "string", - "title": "Scheme URI", - "$dataciteRequired": false - } + "nameType": { + "type": "string", + "title": "Name Type", + "$dataciteRequired": false, + "enum": [ "Personal", "Organizational" ] }, - "required": ["name", "affiliationIdentifierScheme"] - }, - "$dataciteRequired": false - } - }, - "required": ["name"] - }, - "$dataciteRequired": true - }, - "publisher": { - "type:": "object", - "title": "Publisher", - "properties": { - "name": { - "type": "string", - "title": "Publisher Name", - "$dataciteRequired": true - }, - "publisherIdentifier": { - "type": "string", - "title": "Publisher Identifier", - "$dataciteRequired": false - }, - "publisherIdentifierScheme": { - "type": "string", - "title": "Publisher Identifier Scheme", - "$dataciteRequired": false - }, - "schemeUri": { - "type": "string", - "title": "Scheme URI", - "$dataciteRequired": false - } - }, - "required": ["name", "publisherIdentifierScheme"], - "$dataciteRequired": true - }, - "publicationYear": { - "type": "integer", - "title": "Publication Year", - "$dataciteRequired": true - }, - "contributors": { - "type": "array", - "items": { - "type": "object", - "properties": { - "contributorType": { - "type": "string", - "title": "Contributor Type", - "$dataciteRequired": true, - "enum": [ - "ContactPerson", - "DataCollector", - "DataCurator", - "DataManager", - "Distributor", - "Editor", - "HostingInstitution", - "Producer", - "ProjectLeader", - "ProjectManager", - "RegistrationAgency", - "RegistrationAuthority", - "RelatedPerson", - "Researcher", - "RightsHolder", - "Sponsor", - "Supervisor", - "Translator", - "WorkPackageLeader", - "Other" - ] - }, - "name": { - "type": "string", - "title": "Contributor Name", - "$dataciteRequired": true - }, - "nameType": { - "type": "string", - "title": "Name Type", - "$dataciteRequired": false, - "enum": ["Personal", "Organizational"] - }, - "givenName": { - "type": "string", - "title": "Given Name", - "$dataciteRequired": false - }, - "familyName": { - "type": "string", - "title": "Family Name", - "$dataciteRequired": false - }, - "nameIdentifiers": { - "type": "array", - "title": "Name Identifiers", - "items": { - "type": "object", - "properties": { - "nameIdentifier": { - "type": "string", - "title": "Identifier", - "$dataciteRequired": false - }, - "nameIdentifierScheme": { - "type": "string", - "title": "Identifier Scheme", - "$dataciteRequired": true - }, - "schemeUri": { - "type": "string", - "title": "Scheme URI", - "$dataciteRequired": false - } + "givenName": { + "type": "string", + "title": "Given Name", + "$dataciteRequired": false }, - "required": ["nameIdentifierScheme"] - }, - "$dataciteRequired": false - }, - "affiliation": { - "type": "array", - "title": "Affiliations", - "items": { - "type": "object", - "properties": { - "name": { - "type": "string", - "title": "Affiliation Name", - "$dataciteRequired": true - }, - "affiliationIdentifier": { - "type": "string", - "title": "Affiliation Identifier", - "$dataciteRequired": false - }, - "affiliationIdentifierScheme": { - "type": "string", - "title": "Affiliation Identifier Scheme", - "$dataciteRequired": true + "familyName": { + "type": "string", + "title": "Family Name", + "$dataciteRequired": false + }, + "nameIdentifiers": { + "type": "array", + "title": "Name Identifiers", + "items": { + "type": "object", + "properties": { + "nameIdentifier": { + "type": "string", + "title": "Identifier", + "$dataciteRequired": false + }, + "nameIdentifierScheme": { + "type": "string", + "title": "Identifier Scheme", + "$dataciteRequired": true + }, + "schemeUri": { + "type": "string", + "title": "Scheme URI", + "$dataciteRequired": false + } + }, + "required": [ "nameIdentifierScheme" ] }, - "schemeUri": { - "type": "string", - "title": "Scheme URI", - "$dataciteRequired": false - } + "$dataciteRequired": false }, - "required": ["name", "affiliationIdentifierScheme"] + "affiliation": { + "type": "array", + "title": "Affiliations", + "items": { + "type": "object", + "properties": { + "name": { + "type": "string", + "title": "Affiliation Name", + "$dataciteRequired": true + }, + "affiliationIdentifier": { + "type": "string", + "title": "Affiliation Identifier", + "$dataciteRequired": false + }, + "affiliationIdentifierScheme": { + "type": "string", + "title": "Affiliation Identifier Scheme", + "$dataciteRequired": true + }, + "schemeUri": { + "type": "string", + "title": "Scheme URI", + "$dataciteRequired": false + } + }, + "required": [ "name", "affiliationIdentifierScheme" ] + }, + "$dataciteRequired": false + } }, - "$dataciteRequired": false - } - }, - "required": ["name", "contributorType"], - "$dataciteRequired": false - } - }, - "subjects": { - "type": "array", - "items": { - "type": "object", - "properties": { - "subject": { - "type": "string", - "title": "Subject", - "$dataciteRequired": false - }, - "subjectScheme": { - "type": "string", - "title": "Subject Scheme", - "$dataciteRequired": false - }, - "schemeUri": { - "type": "string", - "title": "Subject URI", - "$dataciteRequired": false - }, - "valueUri": { - "type": "string", - "title": "Value URI", - "$dataciteRequired": false - }, - "lang": { - "type": "string", - "title": "Language", - "$dataciteRequired": false - }, - "classificationCode": { - "type": "string", - "title": "Classification Code", - "$dataciteRequired": false - } - }, - "$dataciteRequired": false - } - }, - "dates": { - "type": "array", - "items": { - "type": "object", - "properties": { - "date": { - "type": "string", - "format": "date-time", - "title": "Date", - "$dataciteRequired": true - }, - "dateType": { - "type": "string", - "title": "Date Type", - "$dataciteRequired": true, - "enum": [ - "Accepted", - "Available", - "Copyrighted", - "Collected", - "Coverage", - "Created", - "Issued", - "Submitted", - "Updated", - "Valid", - "Withdrawn", - "Other" - ] - }, - "dateInformation": { - "type": "string", - "title": "Date Information", - "$dataciteRequired": false - } - }, - "required": ["date", "dateType"] - }, - "$dataciteRequired": false - }, - "language": { - "type": "string", - "title": "Language", - "$dataciteRequired": false - }, - "resourceType": { - "type": "string", - "title": "Resource Type", - "$dataciteRequired": true - }, - "alternateIdentifiers": { - "type": "array", - "items": { - "type": "object", - "properties": { - "alternateIdentifier": { - "type": "string", - "title": "Alternate Identifier", - "$dataciteRequired": true + "required": [ "name" ] }, - "alternateIdentifierType": { - "type": "string", - "title": "Alternate Identifier Type", - "$dataciteRequired": true - } + "$dataciteRequired": true }, - "required": ["alternateIdentifier", "alternateIdentifierType"] - }, - "$dataciteRequired": false - }, - "relatedIdentifiers": { - "type": "array", - "items": { - "type": "object", - "properties": { - "relatedIdentifier": { - "type": "string", - "title": "Related Identifier", - "$dataciteRequired": true - }, - "relatedIdentifierType": { - "type": "string", - "title": "Related Identifier Type", - "$dataciteRequired": true, - "enum": [ - "ARK", - "DOI", - "Handle", - "ISBN", - "ISSN", - "ISTC", - "LSID", - "PURL", - "URL", - "arXiv", - "bibcode", - "CSTR", - "EAN13", - "EISSN", - "PMID", - "UPC", - "URN", - "w3id", - "RRID", - "LISSN", - "IGSN" - ] - }, - "relationType": { - "type": "string", - "title": "Relation Type", - "$dataciteRequired": true, - "enum": [ - "IsCitedBy", - "Cites", - "IsSupplementTo", - "IsSupplementedBy", - "IsContinuedBy", - "Continues", - "IsDescribedBy", - "Describes", - "IsMetadataFor", - "HasMetadata", - "IsVersionOf", - "HasVersion", - "IsNewVersionOf", - "IsPreviousVersionOf", - "IsPartOf", - "HasPart", - "IsPublishedIn", - "IsReferencedBy", - "References", - "IsDocumentedBy", - "Documents", - "IsCompiledBy", - "Compiles", - "IsVariantFormOf", - "IsOriginalFormOf", - "IsIdenticalTo", - "IsReviewedBy", - "Reviews", - "IsDerivedFrom", - "IsSourceOf", - "IsRequiredBy", - "Requires", - "IsObsoletedBy", - "Obsoletes", - "IsCollectedBy", - "Collects", - "IsTranslationOf", - "HasTranslation" - ] - }, - "relatedMetadataScheme": { - "type": "string", - "title": "Related Metadata Scheme", - "$dataciteRequired": false - }, - "schemeUri": { - "type": "string", - "title": "Scheme URI", - "$dataciteRequired": false + "publisher": { + "type:": "object", + "title": "Publisher", + "properties": { + "name": { + "type": "string", + "title": "Publisher Name", + "$dataciteRequired": true + }, + "publisherIdentifier": { + "type": "string", + "title": "Publisher Identifier", + "$dataciteRequired": false + }, + "publisherIdentifierScheme": { + "type": "string", + "title": "Publisher Identifier Scheme", + "$dataciteRequired": false + }, + "schemeUri": { + "type": "string", + "title": "Scheme URI", + "$dataciteRequired": false + } }, - "schemeType": { - "type": "string", - "title": "Scheme Type", - "$dataciteRequired": false, - "enum": ["DOI", "Handle", "URL", "Other"] - } + "required": [ "name", "publisherIdentifierScheme" ], + "$dataciteRequired": true }, - "required": [ - "relatedIdentifier", - "relatedIdentifierType", - "relationType" - ] - }, - "$dataciteRequired": false - }, - "sizes": { - "type": "array", - "items": { - "type": "string", - "title": "Size", - "$dataciteRequired": false - }, - "$dataciteRequired": false - }, - "formats": { - "type": "array", - "items": { - "type": "string", - "title": "Format", - "$dataciteRequired": false - } - }, - "rightsList": { - "title": "Rights", - "type": "array", - "items": { - "type": "object", - "properties": { - "rights": { - "type": "string", - "title": "Rights", - "$dataciteRequired": false - }, - "rightsUri": { - "type": "string", - "title": "Rights URI", - "$dataciteRequired": false - }, - "rightsIdentifier": { - "type": "string", - "title": "Rights Identifier", - "$dataciteRequired": false - }, - "rightsIdentifierScheme": { - "type": "string", - "title": "Rights Identifier Scheme", - "$dataciteRequired": false - }, - "schemeUri": { - "type": "string", - "title": "Scheme URI", - "$dataciteRequired": false - } - } - }, - "$dataciteRequired": false - }, - "descriptions": { - "type": "array", - "items": { - "type": "object", - "properties": { - "description": { - "type": "string", - "title": "Description", - "$dataciteRequired": true - }, - "descriptionType": { - "type": "string", - "title": "Description Type", - "$dataciteRequired": true, - "enum": [ - "Abstract", - "Methods", - "SeriesInformation", - "TableOfContents", - "TechnicalInfo", - "Other" - ] - } + "publicationYear": { + "type": "integer", + "title": "Publication Year", + "$dataciteRequired": true }, - "required": ["description", "descriptionType"] - }, - "$dataciteRequired": false - }, - "geoLocations": { - "type": "array", - "items": { - "type": "object", - "properties": { - "geoLocationPlace": { - "type": "string", - "title": "Geolocation Place", - "$dataciteRequired": false - }, - "geoLocationPoint": { + "contributors": { + "type": "array", + "items": { "type": "object", "properties": { - "pointLongitude": { - "type": "number", - "title": "Longitude", - "$dataciteRequired": false, - "minimum": -180, - "maximum": 180 + "contributorType": { + "type": "string", + "title": "Contributor Type", + "$dataciteRequired": true, + "enum": [ + "ContactPerson", + "DataCollector", + "DataCurator", + "DataManager", + "Distributor", + "Editor", + "HostingInstitution", + "Producer", + "ProjectLeader", + "ProjectManager", + "RegistrationAgency", + "RegistrationAuthority", + "RelatedPerson", + "Researcher", + "RightsHolder", + "Sponsor", + "Supervisor", + "Translator", + "WorkPackageLeader", + "Other" + ] }, - "pointLatitude": { - "type": "number", - "title": "Latitude", + "name": { + "type": "string", + "title": "Contributor Name", + "$dataciteRequired": true + }, + "nameType": { + "type": "string", + "title": "Name Type", "$dataciteRequired": false, - "minimum": -90, - "maximum": 90 + "enum": [ "Personal", "Organizational" ] + }, + "givenName": { + "type": "string", + "title": "Given Name", + "$dataciteRequired": false + }, + "familyName": { + "type": "string", + "title": "Family Name", + "$dataciteRequired": false + }, + "nameIdentifiers": { + "type": "array", + "title": "Name Identifiers", + "items": { + "type": "object", + "properties": { + "nameIdentifier": { + "type": "string", + "title": "Identifier", + "$dataciteRequired": false + }, + "nameIdentifierScheme": { + "type": "string", + "title": "Identifier Scheme", + "$dataciteRequired": true + }, + "schemeUri": { + "type": "string", + "title": "Scheme URI", + "$dataciteRequired": false + } + }, + "required": [ "nameIdentifierScheme" ] + }, + "$dataciteRequired": false + }, + "affiliation": { + "type": "array", + "title": "Affiliations", + "items": { + "type": "object", + "properties": { + "name": { + "type": "string", + "title": "Affiliation Name", + "$dataciteRequired": true + }, + "affiliationIdentifier": { + "type": "string", + "title": "Affiliation Identifier", + "$dataciteRequired": false + }, + "affiliationIdentifierScheme": { + "type": "string", + "title": "Affiliation Identifier Scheme", + "$dataciteRequired": true + }, + "schemeUri": { + "type": "string", + "title": "Scheme URI", + "$dataciteRequired": false + } + }, + "required": [ "name", "affiliationIdentifierScheme" ] + }, + "$dataciteRequired": false } }, - "required": ["pointLongitude", "pointLatitude"], + "required": [ "name", "contributorType" ], "$dataciteRequired": false - }, - "geoLocationBox": { + } + }, + "subjects": { + "type": "array", + "items": { "type": "object", "properties": { - "westBoundLongitude": { - "type": "number", - "title": "West Bound Longitude", - "$dataciteRequired": false, - "minimum": -180, - "maximum": 180 + "subject": { + "type": "string", + "title": "Subject", + "$dataciteRequired": false }, - "eastBoundLongitude": { - "type": "number", - "title": "East Bound Longitude", - "$dataciteRequired": false, - "minimum": -180, - "maximum": 180 + "subjectScheme": { + "type": "string", + "title": "Subject Scheme", + "$dataciteRequired": false }, - "southBoundLatitude": { - "type": "number", - "title": "South Bound Latitude", - "$dataciteRequired": false, - "minimum": -90, - "maximum": 90 + "schemeUri": { + "type": "string", + "title": "Subject URI", + "$dataciteRequired": false }, - "northBoundLatitude": { - "type": "number", - "title": "North Bound Latitude", - "$dataciteRequired": false, - "minimum": -90, - "maximum": 90 + "valueUri": { + "type": "string", + "title": "Value URI", + "$dataciteRequired": false + }, + "lang": { + "type": "string", + "title": "Language", + "$dataciteRequired": false + }, + "classificationCode": { + "type": "string", + "title": "Classification Code", + "$dataciteRequired": false } }, - "required": [ - "westBoundLongitude", - "eastBoundLongitude", - "southBoundLatitude", - "northBoundLatitude" - ], "$dataciteRequired": false } }, - "$dataciteRequired": false - } - }, - "fundingReferences": { - "type": "array", - "items": { - "type": "object", - "properties": { - "funderName": { - "type": "string", - "title": "Funder Name", - "$dataciteRequired": true - }, - "funderIdentifier": { - "type": "string", - "title": "Funder Identifier", - "$dataciteRequired": false - }, - "funderIdentifierType": { - "type": "string", - "title": "Funder Identifier Type", - "$dataciteRequired": true, - "enum": ["ROR", "GRID", "Crossref Funder ID", "ISNI", "Other"] - }, - "awardTitle": { - "type": "string", - "title": "Award Title", - "$dataciteRequired": false - }, - "awardNumber": { - "type": "string", - "title": "Award Number", - "$dataciteRequired": false + "dates": { + "type": "array", + "items": { + "type": "object", + "properties": { + "date": { + "type": "string", + "format": "date-time", + "title": "Date", + "$dataciteRequired": true + }, + "dateType": { + "type": "string", + "title": "Date Type", + "$dataciteRequired": true, + "enum": [ + "Accepted", + "Available", + "Copyrighted", + "Collected", + "Coverage", + "Created", + "Issued", + "Submitted", + "Updated", + "Valid", + "Withdrawn", + "Other" + ] + }, + "dateInformation": { + "type": "string", + "title": "Date Information", + "$dataciteRequired": false + } + }, + "required": [ "date", "dateType" ] }, - "awardUri": { - "type": "string", - "title": "Award URI", - "$dataciteRequired": false - } + "$dataciteRequired": false }, - "required": ["funderName", "funderIdentifierType"], - "$dataciteRequired": false - } - }, - "relatedItems": { - "type": "array", - "items": { - "type": "object", - "properties": { - "relatedItemType": { - "type": "string", - "title": "Related Item Type", - "$dataciteRequired": true, - "enum": [ - "Audiovisual", - "Award", - "Book", - "BookChapter", - "Collection", - "ComputationalNotebook", - "ConferencePaper", - "ConferenceProceeding", - "DataPaper", - "Dataset", - "Dissertation", - "Event", - "Image", - "InteractiveResource", - "Instrument", - "Journal", - "JournalArticle", - "Model", - "OutputManagementPlan", - "PeerReview", - "PhysicalObject", - "Preprint", - "Project", - "Report", - "Service", - "Software", - "Sound", - "Standard", - "StudyRegistration", - "Text", - "Workflow", - "Other" - ] - }, - "relationType": { - "type": "string", - "title": "Relation Type", - "$dataciteRequired": true, - "enum": [ - "IsCitedBy", - "Cites", - "IsSupplementTo", - "IsSupplementedBy", - "IsContinuedBy", - "Continues", - "IsDescribedBy", - "Describes", - "HasMetadata", - "IsMetadataFor", - "HasVersion", - "IsVersionOf", - "IsNewVersionOf", - "IsPreviousVersionOf", - "IsPartOf", - "HasPart", - "IsPublishedIn", - "IsReferencedBy", - "References", - "IsDocumentedBy", - "Documents", - "IsCompiledBy", - "Compiles", - "IsVariantFormOf", - "IsOriginalFormOf", - "IsIdenticalTo", - "IsReviewedBy", - "Reviews", - "IsDerivedFrom", - "IsSourceOf", - "IsRequiredBy", - "Requires", - "IsObsoletedBy", - "Obsoletes", - "IsCollectedBy", - "Collects", - "IsTranslationOf", - "HasTranslation" - ] + "language": { + "type": "string", + "title": "Language", + "$dataciteRequired": false + }, + "resourceType": { + "type": "string", + "title": "Resource Type", + "$dataciteRequired": true + }, + "alternateIdentifiers": { + "type": "array", + "items": { + "type": "object", + "properties": { + "alternateIdentifier": { + "type": "string", + "title": "Alternate Identifier", + "$dataciteRequired": true + }, + "alternateIdentifierType": { + "type": "string", + "title": "Alternate Identifier Type", + "$dataciteRequired": true + } + }, + "required": [ "alternateIdentifier", "alternateIdentifierType" ] }, - "relatedItemIdentifier": { + "$dataciteRequired": false + }, + "relatedIdentifiers": { + "type": "array", + "items": { "type": "object", "properties": { - "relatedItemIdentifier": { + "relatedIdentifier": { "type": "string", - "title": "Related Item Identifier", + "title": "Related Identifier", "$dataciteRequired": true }, - "relatedItemIdentifierType": { + "relatedIdentifierType": { "type": "string", - "title": "Related Item Identifier Type", + "title": "Related Identifier Type", "$dataciteRequired": true, "enum": [ "ARK", @@ -777,6 +389,51 @@ "IGSN" ] }, + "relationType": { + "type": "string", + "title": "Relation Type", + "$dataciteRequired": true, + "enum": [ + "IsCitedBy", + "Cites", + "IsSupplementTo", + "IsSupplementedBy", + "IsContinuedBy", + "Continues", + "IsDescribedBy", + "Describes", + "IsMetadataFor", + "HasMetadata", + "IsVersionOf", + "HasVersion", + "IsNewVersionOf", + "IsPreviousVersionOf", + "IsPartOf", + "HasPart", + "IsPublishedIn", + "IsReferencedBy", + "References", + "IsDocumentedBy", + "Documents", + "IsCompiledBy", + "Compiles", + "IsVariantFormOf", + "IsOriginalFormOf", + "IsIdenticalTo", + "IsReviewedBy", + "Reviews", + "IsDerivedFrom", + "IsSourceOf", + "IsRequiredBy", + "Requires", + "IsObsoletedBy", + "Obsoletes", + "IsCollectedBy", + "Collects", + "IsTranslationOf", + "HasTranslation" + ] + }, "relatedMetadataScheme": { "type": "string", "title": "Related Metadata Scheme", @@ -790,183 +447,531 @@ "schemeType": { "type": "string", "title": "Scheme Type", - "$dataciteRequired": false + "$dataciteRequired": false, + "enum": [ "DOI", "Handle", "URL", "Other" ] } - } - }, - "creators": { - "type": "array", - "items": { - "type": "object", - "properties": { - "name": { - "type": "string", - "title": "Creator Name", - "$dataciteRequired": true - }, - "nameType": { - "type": "string", - "title": "Name Type", - "$dataciteRequired": false, - "enum": ["Personal", "Organizational"] - }, - "givenName": { - "type": "string", - "title": "Given Name", - "$dataciteRequired": false - }, - "familyName": { - "type": "string", - "title": "Family Name", - "$dataciteRequired": false - } - }, - "required": ["name"] }, - "$dataciteRequired": false - }, - "titles": { - "type": "array", - "minItems": 1, - "items": { - "type": "object", - "properties": { - "title": { - "type": "string", - "title": "Title", - "$dataciteRequired": true - }, - "titleType": { - "type": "string", - "title": "Title Type", - "$dataciteRequired": false, - "enum": [ - "AlternativeTitle", - "Subtitle", - "TranslatedTitle", - "Other" - ] - } - }, - "required": ["title"] - }, - "$dataciteRequired": false - }, - "publicationYear": { - "type": "string", - "title": "Publication Year", - "$dataciteRequired": false - }, - "volume": { - "type": "string", - "title": "Volume", - "$dataciteRequired": false - }, - "issue": { - "type": "string", - "title": "Issue", - "$dataciteRequired": false - }, - "number": { - "type": "string", - "title": "Number", - "$dataciteRequired": false - }, - "numberType": { - "type": "string", - "title": "Number Type", - "$dataciteRequired": false, - "enum": ["Article", "Chapter", "Report", "Other"] - }, - "firstPage": { - "type": "string", - "title": "First Page", - "$dataciteRequired": false + "required": [ + "relatedIdentifier", + "relatedIdentifierType", + "relationType" + ] }, - "lastPage": { + "$dataciteRequired": false + }, + "sizes": { + "type": "array", + "items": { "type": "string", - "title": "Last Page", + "title": "Size", "$dataciteRequired": false }, - "publisher": { + "$dataciteRequired": false + }, + "formats": { + "type": "array", + "items": { "type": "string", - "title": "Publisher", + "title": "Format", "$dataciteRequired": false + } + }, + "rightsList": { + "title": "Rights", + "type": "array", + "items": { + "type": "object", + "properties": { + "rights": { + "type": "string", + "title": "Rights", + "$dataciteRequired": false + }, + "rightsUri": { + "type": "string", + "title": "Rights URI", + "$dataciteRequired": false + }, + "rightsIdentifier": { + "type": "string", + "title": "Rights Identifier", + "$dataciteRequired": false + }, + "rightsIdentifierScheme": { + "type": "string", + "title": "Rights Identifier Scheme", + "$dataciteRequired": false + }, + "schemeUri": { + "type": "string", + "title": "Scheme URI", + "$dataciteRequired": false + } + } }, - "edition": { - "type": "string", - "title": "Edition", - "$dataciteRequired": false + "$dataciteRequired": false + }, + "descriptions": { + "type": "array", + "items": { + "type": "object", + "properties": { + "description": { + "type": "string", + "title": "Description", + "$dataciteRequired": true + }, + "descriptionType": { + "type": "string", + "title": "Description Type", + "$dataciteRequired": true, + "enum": [ + "Abstract", + "Methods", + "SeriesInformation", + "TableOfContents", + "TechnicalInfo", + "Other" + ] + } + }, + "required": [ "description", "descriptionType" ] }, - "contributors": { - "type": "array", - "items": { - "type": "object", - "properties": { - "contributorType": { - "type": "string", - "title": "Contributor Type", - "$dataciteRequired": true, - "enum": [ - "ContactPerson", - "DataCollector", - "DataCurator", - "DataManager", - "Distributor", - "Editor", - "HostingInstitution", - "Producer", - "ProjectLeader", - "ProjectManager", - "RegistrationAgency", - "RegistrationAuthority", - "RelatedPerson", - "Researcher", - "RightsHolder", - "Sponsor", - "Supervisor", - "Translator", - "WorkPackageLeader", - "Other" - ] + "$dataciteRequired": false + }, + "geoLocations": { + "type": "array", + "items": { + "type": "object", + "properties": { + "geoLocationPlace": { + "type": "string", + "title": "Geolocation Place", + "$dataciteRequired": false + }, + "geoLocationPoint": { + "type": "object", + "properties": { + "pointLongitude": { + "type": "number", + "title": "Longitude", + "$dataciteRequired": false, + "minimum": -180, + "maximum": 180 + }, + "pointLatitude": { + "type": "number", + "title": "Latitude", + "$dataciteRequired": false, + "minimum": -90, + "maximum": 90 + } }, - "name": { - "type": "string", - "title": "Contributor Name", - "$dataciteRequired": true + "required": [ "pointLongitude", "pointLatitude" ], + "$dataciteRequired": false + }, + "geoLocationBox": { + "type": "object", + "properties": { + "westBoundLongitude": { + "type": "number", + "title": "West Bound Longitude", + "$dataciteRequired": false, + "minimum": -180, + "maximum": 180 + }, + "eastBoundLongitude": { + "type": "number", + "title": "East Bound Longitude", + "$dataciteRequired": false, + "minimum": -180, + "maximum": 180 + }, + "southBoundLatitude": { + "type": "number", + "title": "South Bound Latitude", + "$dataciteRequired": false, + "minimum": -90, + "maximum": 90 + }, + "northBoundLatitude": { + "type": "number", + "title": "North Bound Latitude", + "$dataciteRequired": false, + "minimum": -90, + "maximum": 90 + } }, - "nameType": { - "type": "string", - "title": "Name Type", - "$dataciteRequired": false, - "enum": ["Personal", "Organizational"] + "required": [ + "westBoundLongitude", + "eastBoundLongitude", + "southBoundLatitude", + "northBoundLatitude" + ], + "$dataciteRequired": false + } + }, + "$dataciteRequired": false + } + }, + "fundingReferences": { + "type": "array", + "items": { + "type": "object", + "properties": { + "funderName": { + "type": "string", + "title": "Funder Name", + "$dataciteRequired": true + }, + "funderIdentifier": { + "type": "string", + "title": "Funder Identifier", + "$dataciteRequired": false + }, + "funderIdentifierType": { + "type": "string", + "title": "Funder Identifier Type", + "$dataciteRequired": true, + "enum": [ "ROR", "GRID", "Crossref Funder ID", "ISNI", "Other" ] + }, + "awardTitle": { + "type": "string", + "title": "Award Title", + "$dataciteRequired": false + }, + "awardNumber": { + "type": "string", + "title": "Award Number", + "$dataciteRequired": false + }, + "awardUri": { + "type": "string", + "title": "Award URI", + "$dataciteRequired": false + } + }, + "required": [ "funderName", "funderIdentifierType" ], + "$dataciteRequired": false + } + }, + "relatedItems": { + "type": "array", + "items": { + "type": "object", + "properties": { + "relatedItemType": { + "type": "string", + "title": "Related Item Type", + "$dataciteRequired": true, + "enum": [ + "Audiovisual", + "Award", + "Book", + "BookChapter", + "Collection", + "ComputationalNotebook", + "ConferencePaper", + "ConferenceProceeding", + "DataPaper", + "Dataset", + "Dissertation", + "Event", + "Image", + "InteractiveResource", + "Instrument", + "Journal", + "JournalArticle", + "Model", + "OutputManagementPlan", + "PeerReview", + "PhysicalObject", + "Preprint", + "Project", + "Report", + "Service", + "Software", + "Sound", + "Standard", + "StudyRegistration", + "Text", + "Workflow", + "Other" + ] + }, + "relationType": { + "type": "string", + "title": "Relation Type", + "$dataciteRequired": true, + "enum": [ + "IsCitedBy", + "Cites", + "IsSupplementTo", + "IsSupplementedBy", + "IsContinuedBy", + "Continues", + "IsDescribedBy", + "Describes", + "HasMetadata", + "IsMetadataFor", + "HasVersion", + "IsVersionOf", + "IsNewVersionOf", + "IsPreviousVersionOf", + "IsPartOf", + "HasPart", + "IsPublishedIn", + "IsReferencedBy", + "References", + "IsDocumentedBy", + "Documents", + "IsCompiledBy", + "Compiles", + "IsVariantFormOf", + "IsOriginalFormOf", + "IsIdenticalTo", + "IsReviewedBy", + "Reviews", + "IsDerivedFrom", + "IsSourceOf", + "IsRequiredBy", + "Requires", + "IsObsoletedBy", + "Obsoletes", + "IsCollectedBy", + "Collects", + "IsTranslationOf", + "HasTranslation" + ] + }, + "relatedItemIdentifier": { + "type": "object", + "properties": { + "relatedItemIdentifier": { + "type": "string", + "title": "Related Item Identifier", + "$dataciteRequired": true + }, + "relatedItemIdentifierType": { + "type": "string", + "title": "Related Item Identifier Type", + "$dataciteRequired": true, + "enum": [ + "ARK", + "DOI", + "Handle", + "ISBN", + "ISSN", + "ISTC", + "LSID", + "PURL", + "URL", + "arXiv", + "bibcode", + "CSTR", + "EAN13", + "EISSN", + "PMID", + "UPC", + "URN", + "w3id", + "RRID", + "LISSN", + "IGSN" + ] + }, + "relatedMetadataScheme": { + "type": "string", + "title": "Related Metadata Scheme", + "$dataciteRequired": false + }, + "schemeUri": { + "type": "string", + "title": "Scheme URI", + "$dataciteRequired": false + }, + "schemeType": { + "type": "string", + "title": "Scheme Type", + "$dataciteRequired": false + } + } + }, + "creators": { + "type": "array", + "items": { + "type": "object", + "properties": { + "name": { + "type": "string", + "title": "Creator Name", + "$dataciteRequired": true + }, + "nameType": { + "type": "string", + "title": "Name Type", + "$dataciteRequired": false, + "enum": [ "Personal", "Organizational" ] + }, + "givenName": { + "type": "string", + "title": "Given Name", + "$dataciteRequired": false + }, + "familyName": { + "type": "string", + "title": "Family Name", + "$dataciteRequired": false + } + }, + "required": [ "name" ] }, - "givenName": { - "type": "string", - "title": "Given Name", - "$dataciteRequired": false + "$dataciteRequired": false + }, + "titles": { + "type": "array", + "minItems": 1, + "items": { + "type": "object", + "properties": { + "title": { + "type": "string", + "title": "Title", + "$dataciteRequired": true + }, + "titleType": { + "type": "string", + "title": "Title Type", + "$dataciteRequired": false, + "enum": [ + "AlternativeTitle", + "Subtitle", + "TranslatedTitle", + "Other" + ] + } + }, + "required": [ "title" ] }, - "familyName": { - "type": "string", - "title": "Family Name", - "$dataciteRequired": false - } + "$dataciteRequired": false + }, + "publicationYear": { + "type": "string", + "title": "Publication Year", + "$dataciteRequired": false }, - "required": ["name", "contributorType"] + "volume": { + "type": "string", + "title": "Volume", + "$dataciteRequired": false + }, + "issue": { + "type": "string", + "title": "Issue", + "$dataciteRequired": false + }, + "number": { + "type": "string", + "title": "Number", + "$dataciteRequired": false + }, + "numberType": { + "type": "string", + "title": "Number Type", + "$dataciteRequired": false, + "enum": [ "Article", "Chapter", "Report", "Other" ] + }, + "firstPage": { + "type": "string", + "title": "First Page", + "$dataciteRequired": false + }, + "lastPage": { + "type": "string", + "title": "Last Page", + "$dataciteRequired": false + }, + "publisher": { + "type": "string", + "title": "Publisher", + "$dataciteRequired": false + }, + "edition": { + "type": "string", + "title": "Edition", + "$dataciteRequired": false + }, + "contributors": { + "type": "array", + "items": { + "type": "object", + "properties": { + "contributorType": { + "type": "string", + "title": "Contributor Type", + "$dataciteRequired": true, + "enum": [ + "ContactPerson", + "DataCollector", + "DataCurator", + "DataManager", + "Distributor", + "Editor", + "HostingInstitution", + "Producer", + "ProjectLeader", + "ProjectManager", + "RegistrationAgency", + "RegistrationAuthority", + "RelatedPerson", + "Researcher", + "RightsHolder", + "Sponsor", + "Supervisor", + "Translator", + "WorkPackageLeader", + "Other" + ] + }, + "name": { + "type": "string", + "title": "Contributor Name", + "$dataciteRequired": true + }, + "nameType": { + "type": "string", + "title": "Name Type", + "$dataciteRequired": false, + "enum": [ "Personal", "Organizational" ] + }, + "givenName": { + "type": "string", + "title": "Given Name", + "$dataciteRequired": false + }, + "familyName": { + "type": "string", + "title": "Family Name", + "$dataciteRequired": false + } + }, + "required": [ "name", "contributorType" ] + }, + "$dataciteRequired": false + } }, + "required": [ + "relatedItemType", + "relationType", + "relatedItemIdentifier" + ], "$dataciteRequired": false } - }, - "required": [ - "relatedItemType", - "relationType", - "relatedItemIdentifier" - ], - "$dataciteRequired": false + } } } - }, - "required": ["creators", "publisher", "publicationYear", "resourceType"] + ], + "required": [ "creators", "publisher", "publicationYear", "resourceType" ] }, "uiSchema": { diff --git a/src/config/configuration.ts b/src/config/configuration.ts index 1115bfbc1..e1ca0333f 100644 --- a/src/config/configuration.ts +++ b/src/config/configuration.ts @@ -64,6 +64,8 @@ const configuration = () => { const jobConfigurationFile = process.env.JOB_CONFIGURATION_FILE || ""; + const ajvCustomDefinitions = process.env.AJV_CUSTOM_DEFINITIONS_FILE || ""; + const defaultLogger = { type: "DefaultLogger", modulePath: "./loggingProviders/defaultLogger", @@ -420,6 +422,7 @@ const configuration = () => { frontendConfig: jsonConfigMap.frontendConfig, frontendTheme: jsonConfigMap.frontendTheme, publishedDataConfig: jsonConfigMap.publishedDataConfig, + ajvCustomDefinitions: ajvCustomDefinitions, }; return merge(config, localconfiguration); }; diff --git a/src/published-data/published-data.module.ts b/src/published-data/published-data.module.ts index 3878ac7bd..657caf892 100644 --- a/src/published-data/published-data.module.ts +++ b/src/published-data/published-data.module.ts @@ -4,6 +4,7 @@ import { ConfigModule, ConfigService } from "@nestjs/config"; import { MongooseModule } from "@nestjs/mongoose"; import { AttachmentsModule } from "src/attachments/attachments.module"; import { CaslModule } from "src/casl/casl.module"; +import { applyHistoryPluginOnce } from "src/common/mongoose/plugins/history.plugin.util"; import { DatasetsModule } from "src/datasets/datasets.module"; import { ProposalsModule } from "src/proposals/proposals.module"; import { @@ -12,12 +13,12 @@ import { } from "../common/schemas/generic-history.schema"; import { PublishedDataController } from "./published-data.controller"; import { PublishedDataService } from "./published-data.service"; +import { PublishedDataV4Controller } from "./published-data.v4.controller"; import { PublishedData, PublishedDataSchema, } from "./schemas/published-data.schema"; -import { applyHistoryPluginOnce } from "src/common/mongoose/plugins/history.plugin.util"; -import { PublishedDataV4Controller } from "./published-data.v4.controller"; +import { ValidatorService } from "./validator.service"; @Module({ imports: [ @@ -65,6 +66,6 @@ import { PublishedDataV4Controller } from "./published-data.v4.controller"; ProposalsModule, ], controllers: [PublishedDataController, PublishedDataV4Controller], - providers: [PublishedDataService], + providers: [PublishedDataService, ValidatorService], }) export class PublishedDataModule {} diff --git a/src/published-data/published-data.service.ts b/src/published-data/published-data.service.ts index d3d929fbf..22fc4f3a8 100644 --- a/src/published-data/published-data.service.ts +++ b/src/published-data/published-data.service.ts @@ -67,13 +67,6 @@ export class PublishedDataService { ), ); - if ( - createdPublished.metadata && - !createdPublished.metadata.publicationYear - ) { - createdPublished.metadata.publicationYear = new Date().getFullYear(); - } - return createdPublished.save(); } diff --git a/src/published-data/published-data.v4.controller.spec.ts b/src/published-data/published-data.v4.controller.spec.ts index eb3fa3974..44cc1fa1e 100644 --- a/src/published-data/published-data.v4.controller.spec.ts +++ b/src/published-data/published-data.v4.controller.spec.ts @@ -9,6 +9,7 @@ import { ProposalsService } from "src/proposals/proposals.service"; import { PublishedDataService } from "./published-data.service"; import { PublishedDataV4Controller } from "./published-data.v4.controller"; import { PublishedData } from "./schemas/published-data.schema"; +import { ValidatorService } from "./validator.service"; class AttachmentsServiceMock {} @@ -23,6 +24,8 @@ class PublishedDataServiceMock {} class CaslAbilityFactoryMock {} +class ValidatorServiceMock {} + class ConfigServiceMock { get(key: string) { const config = { @@ -69,6 +72,7 @@ describe("PublishedDataController", () => { { provide: PublishedDataService, useClass: PublishedDataServiceMock }, { provide: CaslAbilityFactory, useClass: CaslAbilityFactoryMock }, { provide: ConfigService, useClass: ConfigServiceMock }, + { provide: ValidatorService, useClass: ValidatorServiceMock }, ], }).compile(); diff --git a/src/published-data/published-data.v4.controller.ts b/src/published-data/published-data.v4.controller.ts index 79346a164..b27389f43 100644 --- a/src/published-data/published-data.v4.controller.ts +++ b/src/published-data/published-data.v4.controller.ts @@ -25,7 +25,7 @@ import { ApiTags, } from "@nestjs/swagger"; import { Request } from "express"; -import { Validator } from "jsonschema"; +import { cloneDeep } from "lodash"; import { FilterQuery, QueryOptions } from "mongoose"; import { firstValueFrom } from "rxjs"; import { AttachmentsService } from "src/attachments/attachments.service"; @@ -36,6 +36,7 @@ import { AppAbility, CaslAbilityFactory } from "src/casl/casl-ability.factory"; import { CheckPolicies } from "src/casl/decorators/check-policies.decorator"; import { AuthenticatedPoliciesGuard } from "src/casl/guards/auth-check.guard"; import { PoliciesGuard } from "src/casl/guards/policies.guard"; +import { ILimitsFilter } from "src/common/interfaces/common.interface"; import { handleAxiosRequestError } from "src/common/utils"; import { DatasetsService } from "src/datasets/datasets.service"; import { DatasetsV4Controller } from "src/datasets/datasets.v4.controller"; @@ -50,15 +51,14 @@ import { IRegister, PublishedDataStatus, } from "./interfaces/published-data.interface"; +import { V4_FILTER_PIPE } from "./pipes/filter.pipe"; import { RegisteredFilterPipe } from "./pipes/registered.pipe"; import { PublishedDataService } from "./published-data.service"; import { PublishedData, PublishedDataDocument, } from "./schemas/published-data.schema"; -import { V4_FILTER_PIPE } from "./pipes/filter.pipe"; -import { ILimitsFilter } from "src/common/interfaces/common.interface"; -import { cloneDeep } from "lodash"; +import { ValidatorService } from "./validator.service"; @ApiBearerAuth() @ApiTags("published data v4") @@ -78,6 +78,7 @@ export class PublishedDataV4Controller { private readonly proposalsService: ProposalsService, private readonly publishedDataService: PublishedDataService, private caslAbilityFactory: CaslAbilityFactory, + private validatorService: ValidatorService, ) {} @AllowAny() @@ -95,6 +96,7 @@ export class PublishedDataV4Controller { async create( @Body() createPublishedDataDto: CreatePublishedDataV4Dto, ): Promise { + await this.validatorService.validate(createPublishedDataDto); return this.publishedDataService.create(createPublishedDataDto); } @@ -373,6 +375,7 @@ export class PublishedDataV4Controller { } } + await this.validatorService.validate(updatePublishedDataDto); return this.publishedDataService.update( { doi: id }, updatePublishedDataDto, @@ -409,7 +412,11 @@ export class PublishedDataV4Controller { ); } - await this.validateMetadata(publishedData.metadata); + const validationErrors = + await this.validatorService.validate(publishedData); + if (validationErrors) { + throw new HttpException(validationErrors, HttpStatus.BAD_REQUEST); + } // Make datasets in publishedData datasetPids array public const datasetPids = publishedData.datasetPids; @@ -477,29 +484,6 @@ export class PublishedDataV4Controller { ); } - async validateMetadata(metadata?: object) { - const validator = new Validator(); - const metadataConfig = await this.getConfig(); - if (!metadataConfig?.metadataSchema) { - throw new HttpException( - "Published data schema is not defined in the configuration.", - HttpStatus.INTERNAL_SERVER_ERROR, - ); - } - - const validationResult = validator.validate( - metadata, - metadataConfig.metadataSchema, - ); - - if (!validationResult.valid) { - throw new HttpException( - validationResult.errors.map((error) => error.stack), - HttpStatus.BAD_REQUEST, - ); - } - } - // DELETE /publisheddata/:id @UseGuards(AuthenticatedPoliciesGuard) @CheckPolicies("publisheddata", (ability: AppAbility) => @@ -568,7 +552,11 @@ export class PublishedDataV4Controller { publishedData.registeredTime = data.registeredTime; publishedData.status = data.status; - await this.validateMetadata(publishedData.metadata); + const validationErrors = + await this.validatorService.validate(publishedData); + if (validationErrors) { + throw new HttpException(validationErrors, HttpStatus.BAD_REQUEST); + } const mergePatchRequest = cloneDeep(request); mergePatchRequest.headers["content-type"] = "application/merge-patch+json"; diff --git a/src/published-data/validator.service.spec.ts b/src/published-data/validator.service.spec.ts new file mode 100644 index 000000000..df4af1ee5 --- /dev/null +++ b/src/published-data/validator.service.spec.ts @@ -0,0 +1,199 @@ +import { HttpException, HttpStatus } from "@nestjs/common"; +import { ConfigService } from "@nestjs/config"; +import { Test, TestingModule } from "@nestjs/testing"; +import { AttachmentsService } from "src/attachments/attachments.service"; +import { DatasetsService } from "src/datasets/datasets.service"; +import { ProposalsService } from "src/proposals/proposals.service"; +import { ReadOnlyDatasetsService, ValidatorService } from "./validator.service"; + +describe("ValidatorService", () => { + let service: ValidatorService; + + const mockConfigService = { + get: jest.fn(), + }; + + const mockDataService = { + findOne: jest.fn(), + findAll: jest.fn(), + count: jest.fn(), + }; + + beforeEach(async () => { + const module: TestingModule = await Test.createTestingModule({ + providers: [ + ValidatorService, + { provide: ConfigService, useValue: mockConfigService }, + { provide: ProposalsService, useValue: mockDataService }, + { provide: DatasetsService, useValue: mockDataService }, + { provide: AttachmentsService, useValue: mockDataService }, + ], + }).compile(); + + service = module.get(ValidatorService); + }); + + it("should be defined", () => { + expect(service).toBeDefined(); + }); + + describe("validate", () => { + it("should throw INTERNAL_SERVER_ERROR if metadataSchema is missing", async () => { + mockConfigService.get.mockReturnValue({}); + + const mockData = { metadata: {} }; + + await expect(service.validate(mockData)).rejects.toThrow( + new HttpException( + "Published data schema is not defined in the configuration.", + HttpStatus.INTERNAL_SERVER_ERROR, + ), + ); + }); + + it("should return null when metadata is valid", async () => { + const schema = { + type: "object", + properties: { name: { type: "string" } }, + required: ["name"], + }; + + mockConfigService.get.mockImplementation((key: string) => { + if (key === "publishedDataConfig") return { metadataSchema: schema }; + return null; + }); + + const mockData = { metadata: { name: "abc" } }; + const errors = await service.validate(mockData); + expect(errors).toBeNull(); + }); + + it("should return errors when metadata is invalid", async () => { + const schema = { + type: "object", + properties: { name: { type: "string" } }, + required: ["name"], + }; + + mockConfigService.get.mockImplementation((key: string) => { + if (key === "publishedDataConfig") return { metadataSchema: schema }; + return null; + }); + + const mockData = { metadata: { invalidKey: 5 } }; + const errors = await service.validate(mockData); + + expect(errors).toBeDefined(); + expect(errors?.length).toBe(1); + expect(errors?.[0].keyword).toBe("required"); + }); + }); + + describe("Dynamic Defaults", () => { + it("should handle pre-defined dynamic default (currentYear)", async () => { + const schema = { + type: "object", + required: ["publicationYear"], + allOf: [ + { dynamicDefaults: { publicationYear: "currentYear" } }, + { properties: { publicationYear: { type: "number" } } }, + ], + }; + + mockConfigService.get.mockImplementation((key: string) => { + if (key === "publishedDataConfig") return { metadataSchema: schema }; + return null; + }); + + const mockData = { metadata: {} }; + const errors = await service.validate(mockData); + expect(errors).toBeNull(); + + const metadata = mockData.metadata as Record; + expect(metadata.publicationYear).toBe(new Date().getFullYear()); + }); + + it("should handle user-defined dynamic default (sync)", async () => { + const schema = { + type: "object", + required: ["publicationYear"], + allOf: [ + { dynamicDefaults: { publicationYear: "userDefinedFunction" } }, + { properties: { publicationYear: { type: "number" } } }, + ], + }; + + mockConfigService.get.mockImplementation((key: string) => { + if (key === "publishedDataConfig") return { metadataSchema: schema }; + return null; + }); + + // eslint-disable-next-line @typescript-eslint/no-explicit-any + (service as any).dynamicDefaults.set( + "userDefinedFunction", + () => () => 5, + ); + + const mockData = { metadata: {} }; + const errors = await service.validate(mockData); + expect(errors).toBeNull(); + + const metadata = mockData.metadata as Record; + expect(metadata.publicationYear).toBe(5); + }); + + it("should handle user-defined dynamic default (async)", async () => { + const schema = { + type: "object", + required: ["publicationYear"], + allOf: [ + { dynamicDefaults: { publicationYear: "userDefinedAsyncFunction" } }, + { properties: { publicationYear: { type: "number" } } }, + ], + }; + + mockConfigService.get.mockImplementation((key: string) => { + if (key === "publishedDataConfig") return { metadataSchema: schema }; + return null; + }); + + mockDataService.count.mockImplementation(() => 6); + // eslint-disable-next-line @typescript-eslint/no-explicit-any + (service as any).dynamicDefaults.set( + "userDefinedAsyncFunction", + async function (ctx: { datasetsService: ReadOnlyDatasetsService }) { + const datasetsCount = await ctx.datasetsService.count({}); + return () => datasetsCount; + }, + ); + + const mockData = { metadata: {} }; + const errors = await service.validate(mockData); + expect(errors).toBeNull(); + + const metadata = mockData.metadata as Record; + expect(metadata.publicationYear).toBe(6); + }); + + it("should error on unknown dynamicDefaults functions", async () => { + const schema = { + type: "object", + required: ["publicationYear"], + allOf: [ + { dynamicDefaults: { publicationYear: "notImplemented" } }, + { properties: { publicationYear: { type: "number" } } }, + ], + }; + + mockConfigService.get.mockImplementation((key: string) => { + if (key === "publishedDataConfig") return { metadataSchema: schema }; + return null; + }); + + const mockData = { metadata: {} }; + await expect(service.validate(mockData)).rejects.toThrow( + 'invalid "dynamicDefaults" keyword property value: notImplemented', + ); + }); + }); +}); diff --git a/src/published-data/validator.service.ts b/src/published-data/validator.service.ts new file mode 100644 index 000000000..0a5d003f5 --- /dev/null +++ b/src/published-data/validator.service.ts @@ -0,0 +1,148 @@ +import { HttpException, HttpStatus, Injectable, Logger } from "@nestjs/common"; +import { ConfigService } from "@nestjs/config"; +import addFormats from "ajv-formats"; +import addKeywords from "ajv-keywords"; +import def, { + DynamicDefaultFunc, +} from "ajv-keywords/dist/definitions/dynamicDefaults"; +import Ajv2019 from "ajv/dist/2019"; +import { isArray, isEmpty, isMap } from "lodash"; +import { AttachmentsService } from "src/attachments/attachments.service"; +import { DatasetsService } from "src/datasets/datasets.service"; +import { ProposalsService } from "src/proposals/proposals.service"; +import { CreatePublishedDataV4Dto } from "./dto/create-published-data.v4.dto"; +import { + PartialUpdatePublishedDataV4Dto, + UpdatePublishedDataV4Dto, +} from "./dto/update-published-data.v4.dto"; + +export type ReadOnlyProposalsService = Pick< + ProposalsService, + "findOne" | "findAll" | "count" +>; +export type ReadOnlyDatasetsService = Pick< + DatasetsService, + "findOne" | "findAll" | "count" +>; +export type ReadOnlyAttachmentsService = Pick< + AttachmentsService, + "findOne" | "findAll" | "count" +>; + +@Injectable() +export class ValidatorService { + private ajv: Ajv2019; + private static logger: Logger = new Logger(ValidatorService.name); + private dynamicDefaults: Map = new Map([ + ["currentYear", () => () => new Date().getFullYear()], + ]); + + constructor( + private readonly configService: ConfigService, + private readonly proposalsService: ProposalsService, + private readonly datasetsService: DatasetsService, + private readonly attachmentsService: AttachmentsService, + ) { + this.ajv = new Ajv2019({ + useDefaults: true, + allErrors: true, + strict: false, + }); + addFormats(this.ajv); + addKeywords(this.ajv); + + const modulePath = this.configService.get("ajvCustomDefinitions"); + if (!isEmpty(modulePath)) { + try { + const externalModule = this.loadExternalModule(modulePath!); + + if (isArray(externalModule.keywords)) { + for (const definition of externalModule.keywords) { + ValidatorService.logger.log( + `Adding ajv keyword: '${definition.keyword}'`, + ); + this.ajv.addKeyword(definition); + } + } + + if (isMap(externalModule.dynamicDefaults)) { + this.dynamicDefaults = new Map([ + ...this.dynamicDefaults, + ...externalModule.dynamicDefaults, + ]); + } + } catch (error) { + ValidatorService.logger.error( + `Failed to load module at '${modulePath}'`, + error, + ); + throw error; + } + } + } + + async validate( + publishedData: + | CreatePublishedDataV4Dto + | UpdatePublishedDataV4Dto + | PartialUpdatePublishedDataV4Dto, + ) { + await this.loadDynamicDefaultFunctions(publishedData); + const config = this.configService.get>( + "publishedDataConfig", + ); + if (!config?.metadataSchema) { + throw new HttpException( + "Published data schema is not defined in the configuration.", + HttpStatus.INTERNAL_SERVER_ERROR, + ); + } + const validateFn = this.ajv.compile(config.metadataSchema); + validateFn(publishedData.metadata); + return validateFn.errors; + } + + private loadExternalModule(path: string) { + ValidatorService.logger.debug(`Loading custom ajv code at ${path}`); + // eslint-disable-next-line @typescript-eslint/no-require-imports + const externalModule = require(path); + + return externalModule; + } + + private async loadDynamicDefaultFunctions( + publishedData: + | CreatePublishedDataV4Dto + | UpdatePublishedDataV4Dto + | PartialUpdatePublishedDataV4Dto, + ) { + for (const [name, implementation] of this.dynamicDefaults.entries()) { + if (typeof implementation !== "function") { + ValidatorService.logger.error( + `Ignoring dynamic defaults function ${name} should be of type 'function' not '${typeof implementation}'.`, + ); + continue; + } + + switch (implementation.constructor.name) { + case "Function": + def.DEFAULTS[name] = implementation; + break; + case "AsyncFunction": + /** + * Ajv cannot 'await' during validation. To get around this, we run the + * AsyncFunction now to perform any setup (like DB queries). + */ + const syncFunc = await implementation({ + publishedData: publishedData, + proposalService: this.proposalsService as ReadOnlyProposalsService, + datasetsService: this.datasetsService as ReadOnlyDatasetsService, + attachmentsService: this + .attachmentsService as ReadOnlyAttachmentsService, + }); + def.DEFAULTS[name] = () => syncFunc; + break; + } + } + } +} diff --git a/test/PublishedDataV4.js b/test/PublishedDataV4.js index 86b13aa68..8bbc00a85 100644 --- a/test/PublishedDataV4.js +++ b/test/PublishedDataV4.js @@ -1,16 +1,17 @@ "use strict"; +const { cloneDeep } = require("lodash"); const utils = require("./LoginUtils"); const { TestData } = require("./TestData"); const sandbox = require("sinon").createSandbox(); let accessTokenArchiveManager = null, accessTokenAdminIngestor = null, - idOrigDatablock = null, pid = null, pidnonpublic = null, attachmentId = null, - doi = null; + doi = null, + doi2 = null; const publishedData = { ...TestData.PublishedDataV4 }; @@ -88,12 +89,100 @@ describe("1600: PublishedDataV4: Test of access to published data v4 endpoints", .then((res) => { res.body.should.have.property("title").and.be.string; res.body.should.have.property("metadata"); + res.body.metadata.should.have + .property("publicationYear") + .and.equal(publishedData.metadata.publicationYear); res.body.metadata.should.have.property("publisher"); res.body.should.have.property("status").and.equal(defaultStatus); doi = encodeURIComponent(res.body["doi"]); }); }); + it("0016: publicationYear should default to the current year", async () => { + const payload = cloneDeep(publishedData); + delete payload.metadata.publicationYear; + delete payload.metadata.publisher; + return request(appUrl) + .post("/api/v4/PublishedData") + .send(payload) + .set("Accept", "application/json") + .set({ Authorization: `Bearer ${accessTokenAdminIngestor}` }) + .expect(TestData.EntryCreatedStatusCode) + .expect("Content-Type", /json/) + .then((res) => { + res.body.should.have.property("title").and.be.string; + res.body.should.have.property("metadata"); + res.body.metadata.should.have + .property("publicationYear") + .and.equal(new Date().getFullYear()); + res.body.should.have.property("status").and.equal(defaultStatus); + doi2 = encodeURIComponent(res.body["doi"]); + }); + }); + + it("0017: should not be able to publish if metadata is invalid", async () => { + return request(appUrl) + .post("/api/v4/PublishedData/" + doi2 + "/publish") + .set("Accept", "application/json") + .set({ Authorization: `Bearer ${accessTokenAdminIngestor}` }) + .expect(TestData.BadRequestStatusCode) + .expect("Content-Type", /json/) + .then((res) => { + res.body.should.have.property("length").and.equal(1); + res.body[0].should.have + .property("message") + .and.equal("must have required property 'publisher'"); + }); + }); + + it("0018: should be able to overwrite publicationYear", async () => { + return request(appUrl) + .patch("/api/v4/PublishedData/" + doi2) + .send(modifiedPublishedData) + .set("Accept", "application/json") + .set({ Authorization: `Bearer ${accessTokenAdminIngestor}` }) + .expect(TestData.SuccessfulGetStatusCode) + .expect("Content-Type", /json/) + .then((res) => { + res.body.should.have.property("metadata"); + res.body.metadata.should.have + .property("publicationYear") + .and.equal(publishedData.metadata.publicationYear); + res.body.metadata.publisher.should.have + .property("name") + .and.equal("ESS"); + res.body.metadata.publisher.should.have + .property("publisherIdentifierScheme") + .and.equal("testSchemeUpdated"); + res.body.should.have.property("status").and.equal(defaultStatus); + }); + }); + + it("0019: publicationYear should default to current year if removed by update", async () => { + const payload = cloneDeep(modifiedPublishedData); + delete payload.metadata.publicationYear; + return request(appUrl) + .patch("/api/v4/PublishedData/" + doi2) + .send(payload) + .set("Accept", "application/json") + .set({ Authorization: `Bearer ${accessTokenAdminIngestor}` }) + .expect(TestData.SuccessfulGetStatusCode) + .expect("Content-Type", /json/) + .then((res) => { + res.body.should.have.property("metadata"); + res.body.metadata.should.have + .property("publicationYear") + .and.equal(new Date().getFullYear()); + res.body.metadata.publisher.should.have + .property("name") + .and.equal("ESS"); + res.body.metadata.publisher.should.have + .property("publisherIdentifierScheme") + .and.equal("testSchemeUpdated"); + res.body.should.have.property("status").and.equal(defaultStatus); + }); + }); + it("0020: should not be able to fetch this new published data in private state anonymously", async () => { return request(appUrl) .get("/api/v4/PublishedData/" + doi) @@ -111,7 +200,7 @@ describe("1600: PublishedDataV4: Test of access to published data v4 endpoints", .expect(TestData.SuccessfulGetStatusCode) .expect("Content-Type", /json/) .then((res) => { - res.body.should.be.instanceof(Array).and.to.have.length(1); + res.body.should.be.instanceof(Array).and.to.have.length(2); }); });