diff --git a/.nvmrc b/.nvmrc index 726a201e6..645ae0c87 100644 --- a/.nvmrc +++ b/.nvmrc @@ -1 +1 @@ -20.11.1 \ No newline at end of file +20.15.0 \ No newline at end of file diff --git a/config/datapubs.js b/config/datapubs.js index 48dcb69e5..c28032940 100644 --- a/config/datapubs.js +++ b/config/datapubs.js @@ -1,24 +1,83 @@ module.exports.datapubs = { - + "rootCollection": { + targetRepoNamespace: "uts_public_data_repo", + rootCollectionId: "arcp://name,data_repo/root_collection", + targetRepoColId: "root_collection", + targetRepoColName: "", + targetRepoColDescription: "This is a sample data portal. For any questions, please get in touch with us at info@redboxresearchdata.com.au", + dsType: ["Dataset", "RepositoryCollection"], + enableDatasetToUseDefaultLicense: true, + defaultLicense: { + "@id": "http://creativecommons.org/licenses/by/4.0", + "@type": "OrganizationReuseLicense", + "name": "Attribution 4.0 International (CC BY 4.0)", + "description": "You are free to share (copy and redistribute the material in any medium or format) and adapt (remix, transform and build upon the material for any purpose, even commercially)." + }, + }, "sites": { "staging": { - "dir": "/publication/staging", - "url": "http://localhost:8080/staging" + "dir": "/opt/oni/staged/ocfl", + "tempDir": "/opt/oni/staged/temp", + "url": "http://localhost:11000" }, "public": { - "dir": "/publication/public", - "url": "http://localhost:8080/public" + "dir": "/opt/oni/public/ocfl", + "url": "http://localhost:11000/publication" } }, - - "datacrate": { - "catalog_html": "CATALOG.html", - "catalog_json": "CATALOG.json", + "metadata": { + "html_filename": "ro-crate-preview.html", + "jsonld_filename": "ro-crate-metadata.jsonld", "datapub_json": "datapub.json", + "identifier_namespace": "public_ocfl", + "render_script": "", "organization": { - "id": "https://www.uts.edu.au/", - "name": "University of Technology Sydney" + "@id": "https://www.redboxresearchdata.com.au", + "@type": "Organization", + "identifier": "https://www.redboxresearchdata.com.au", + "name": "ReDBox Research Data" + }, + related_works: [ + { + field: 'publications', + type: 'ScholarlyArticle' + }, + { + field: 'websites', + type: 'WebSite' + }, + { + field: 'metadata', + type: 'CreativeWork' + }, + { + field: 'data', + type: 'Dataset' + }, + { + field: 'services', + type: 'CreativeWork' + } + ], + funders: [ + 'foaf:fundedBy_foaf:Agent', + 'foaf:fundedBy_vivo:Grant' + ], + subjects: [ + 'dc:subject_anzsrc:for', + 'dc:subject_anzsrc:seo' + ], + DEFAULT_IRI_PREFS: { + 'about': { + 'dc:subject_anzsrc:for': '_:FOR/', + 'dc:subject_anzsrc:seo': '_:SEO/' + }, + 'spatialCoverage': '_:spatial/', + 'funder': '_:funder/', + 'license': '_:license/', + 'citation': '_:citation/', + 'contact': '_:contact/', + 'location': '_:location/' } } - }; diff --git a/typescript/api/services/OniService.ts b/typescript/api/services/OniService.ts new file mode 100644 index 000000000..2f7bd2ee9 --- /dev/null +++ b/typescript/api/services/OniService.ts @@ -0,0 +1,609 @@ +// Copyright (c) 2023 Queensland Cyber Infrastructure Foundation (http://www.qcif.edu.au/) +// +// GNU GENERAL PUBLIC LICENSE +// Version 2, June 1991 +// +// This program is free software; you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation; either version 2 of the License, or +// (at your option) any later version. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. +// +// You should have received a copy of the GNU General Public License along +// with this program; if not, write to the Free Software Foundation, Inc., +// 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. + +import { Services as services, DatastreamService, RBValidationError } from '@researchdatabox/redbox-core-types'; +import { Sails } from "sails"; +import 'rxjs/add/operator/toPromise'; +import { promises as fs } from 'fs'; +import path from 'node:path'; +import {Collector, generateArcpId} from "oni-ocfl"; +import { createWriteStream } from 'fs'; +import { promisify } from 'util'; +import * as stream from 'stream'; +const finished = promisify(stream.finished); +import {languageProfileURI} from "language-data-commons-vocabs"; +import * as mime from 'mime-types'; +import {ROCrate} from "ro-crate"; +const { convertToWK } = require('wkt-parser-helper'); + +declare var sails: Sails; +declare var RecordsService, UsersService; +declare var _; + +const URL_PLACEHOLDER = '{ID_WILL_BE_HERE}'; // config +const DEFAULT_IDENTIFIER_NAMESPACE = 'redbox'; + +export module Services { + /** + * + * a Service to extract a DataPub and put it in a RO-Crate with the + * metadata crosswalked into the right JSON-LD + * + * @author Mike Lynch + * + */ + export class OniService extends services.Core.Service { + + protected _exportedMethods: any = [ + 'exportDataset' + ]; + + datastreamService: DatastreamService = null; + + constructor() { + super(); + this.logHeader = "OniService::"; + let that = this; + sails.on('ready', function () { + that.getDatastreamService(); + }); + } + + getDatastreamService() { + this.datastreamService = sails.services[sails.config.storage.serviceName]; + } + + private getRBError(logPrefix: string, message: string) { + let customError: RBValidationError = new RBValidationError(message) + sails.log.error(`${logPrefix}->${message}`); + return customError; + } + + /** + * Converts a RB Data Publication record into RO-Crate and writes it to the OCFL repository + * + * @param oid + * @param record + * @param options + * @param user + */ + + public async exportDataset(oid, record, options, user) { + if( this.metTriggerCondition(oid, record, options) === "true") { + const rootColConfig = sails.config.datapubs.rootCollection; + const site = sails.config.datapubs.sites[options['site']]; + if( ! site ) { + throw this.getRBError(`${this.logHeader } exportDataset()`, "Unknown publication site " + options['site']); + } + const md = record['metadata']; + const drec = md['dataRecord']; + const drid = drec ? drec['oid'] : undefined; + if(!drid) { + const err = `Couldn't find dataRecord or id for data pub: ${oid}`; + throw this.getRBError(`${this.logHeader} exportDataset()`, err); + } + if(! user || ! user['email']) { + user = { 'email': '' }; + const err = `Empty user or no email found: ${oid}`; + // TODO: should we throw here? + throw this.getRBError(`${this.logHeader} exportDataset()`, err); + } + // set the dataset URL and DOI + const datasetUrl = site['url'] + '/' + oid + '/'; + md['citation_url'] = datasetUrl; + md['citation_doi'] = md['citation_doi'].replace(URL_PLACEHOLDER, datasetUrl); + + // get the repository, then write out the attachments and the RO-Crate + const targetCollector = new Collector({repoPath: site.dir, namespace: rootColConfig.targetRepoNamespace}); + try { + await targetCollector.connect(); + } catch (err) { + throw this.getRBError(`${this.logHeader} exportDataset()`, `Error connecting to target collector ${site.dir}: ${err}`); + } + let rootCollection = targetCollector.repo.object(rootColConfig.rootCollectionId); + try { + await rootCollection.load(); + // check if the root collection exists in disk, otherwise populate the root collection's properties + rootCollection = targetCollector.newObject(); + rootCollection.crate.addProfile(languageProfileURI("Collection")); + rootCollection.rootDataset["@type"] = rootColConfig.dsType; + rootCollection.mintArcpId(rootColConfig.targetRepoColId); + rootCollection.rootId = generateArcpId(targetCollector.namespace, rootColConfig.targetRepoColId); + rootCollection.rootDataset.name = rootColConfig.targetRepoColName; + rootCollection.rootDataset.description = rootColConfig.targetRepoColDescription; + rootCollection.rootDataset.license = rootColConfig.defaultLicense; + if (await this.pathExists(rootCollection.root) === false) { + await rootCollection.addToRepo(); + } + } catch (err) { + throw this.getRBError(`${this.logHeader} exportDataset()`, `Error loading root collection ${site.rootCollectionId}: ${err}`); + } + + let creator = await UsersService.getUserWithUsername(record['metaMetadata']['createdBy']).toPromise(); + + if (_.isEmpty(creator)) { + throw this.getRBError(`${this.logHeader} exportDataset()`, `Error getting creator for record ${oid} :: ${record['metaMetadata']['createdBy']}`); + } + try { + await this.writeDatasetObject(creator, user, oid, drid, targetCollector, rootCollection, record, site.tempDir); + } catch (err) { + throw this.getRBError(`${this.logHeader} exportDataset()`, `Error writing dataset object for ${oid}: ${err}`); + } + + try { + await RecordsService.updateMeta(sails.config.auth.defaultBrand, oid, record, null, true, false); + } catch (err) { + this.recordPublicationError(oid, record, err); + throw this.getRBError(`${this.logHeader} exportDataset()`, `Error updating record metadata for ${oid}: ${err}`); + } + } else { + sails.log.debug(`Not publishing: ${oid}, condition not met: ${_.get(options, "triggerCondition", "")}`); + } + } + /** + * Write the dataset object to the OCFL repository + * + * @param creator + * @param approver + * @param oid + * @param drid + * @param targetCollector + * @param rootCollection + * @param record + * @param tempDir + */ + private async writeDatasetObject(creator: Object, approver: Object, oid: string, drid: string, targetCollector: Collector, rootCollection: any, record: Object, tempDir:string): Promise { + const metadata = record['metadata']; + const metaMetadata = record['metaMetadata']; + const mdOnly = metadata['accessRightsToggle']; + const attachments = metadata['dataLocations'].filter( + (a) => ( !mdOnly && a['type'] === 'attachment' && a['selected'] ) + ); + // write all valid attachments to temp directory + try { + attachments.map((a) => { + // a['fileId'] necessary to avoid file name clashes within the dataset + a['parentDir'] = path.join(tempDir, oid, a['fileId']); + a['path'] = path.join(a['parentDir'] , a['name']); + }); + for (let a of attachments) { + const datastream = await this.datastreamService.getDatastream(drid, a['fileId']); + let dataFile; + if (datastream.readstream) { + dataFile = datastream.readstream; + } else { + dataFile = Buffer.from(datastream.body); + } + await this.writeDatastream(dataFile, a['parentDir'], a['name']); + } + } catch (err) { + throw this.getRBError(`${this.logHeader} writeDatasetObject()`, `Error writing attachments for dataset ${oid}: ${err}`); + } + await this.writeDatasetROCrate(creator, approver, oid, attachments, record, targetCollector, rootCollection); + } + + + /** + * + * Builds and persists the rocrate object to the OCFL repository + * + * @param creator + * @param approver + * @param oid + * @param attachments + * @param record + * @param targetCollector + * @param rootCollection + */ + private async writeDatasetROCrate(creator: Object, approver: Object, oid:string, attachments: any[], record: Object, targetCollector: Collector, rootCollection: any) { + const metadata = record['metadata']; + const metaMetadata = record['metaMetadata']; + // Create Dataset/Repository Object + let targetRepoObj = targetCollector.newObject(); + let targetCrate = targetRepoObj.crate; + let extraContext = {}; + // use the OID as the root + targetRepoObj.mintArcpId(oid); + targetCrate.rootId = generateArcpId(targetCollector.namespace, oid); + targetCrate.addProfile(languageProfileURI("Object")); + targetRepoObj.rootDataset["@type"] = ["Dataset", "RepositoryObject"]; + targetRepoObj.mintArcpId(oid); + // Set the basic properties + targetRepoObj.rootDataset.identifier = oid; + targetRepoObj.rootDataset.name = metadata['title']; + targetRepoObj.rootDataset.description = metadata['description']; + const now = (new Date()).toISOString(); + targetRepoObj.rootDataset.dateCreated = metaMetadata['createdOn']; + targetRepoObj.rootDataset.yearCreated = this.getYearFromDate(targetRepoObj.rootDataset.dateCreated); + targetRepoObj.rootDataset.datePublished = now; + targetRepoObj.rootDataset.yearPublished = this.getYearFromDate(targetRepoObj.rootDataset.datePublished); + targetRepoObj.rootDataset.keywords = metadata['finalKeywords']; + // Set the publisher + // https://www.researchobject.org/ro-crate/specification/1.1/contextual-entities.html#organizations-as-values + targetRepoObj.rootDataset.publisher = sails.config.datapubs.metadata.organization; + // Set the author + // https://www.researchobject.org/ro-crate/specification/1.1/contextual-entities.html#people + targetRepoObj.rootDataset.author = this.getCreators(metadata, sails.config.datapubs.metadata.organization); + // Set the contact point + // https://www.researchobject.org/ro-crate/specification/1.1/contextual-entities.html#contact-information + const contactPoint = this.getPerson(metadata['contributor_data_manager'], "ContactPoint"); + if (contactPoint) { + contactPoint['contactType'] = "Data Manager"; + contactPoint['identifier'] = contactPoint['id']; + const author = _.find(targetRepoObj.rootDataset.author, (a) => a['@id'] === contactPoint['@id']); + contactPoint['@id'] = `mailto:${contactPoint['email']}` + if (author) { + author['contactPoint'] = contactPoint; + } else { + // Add the contact point as a contributor + const contactPointPerson = this.getPerson(metadata['contributor_data_manager'], "Person"); + contactPointPerson['contactPoint'] = contactPoint; + targetRepoObj.rootDataset['contributor'] = [contactPointPerson]; + } + } + // Set the license + targetRepoObj.rootDataset.license = this.getLicense(metadata); + // Set the files + await this.addFiles(targetRepoObj, record, attachments); + // Set the related works + this.addRelatedWorks(targetRepoObj, metadata); + // Set the spatial coverage + this.addSpatialCoverage(targetRepoObj, metadata, extraContext); + // Set the temporal coverage + this.addTemporalCoverage(targetRepoObj, metadata, extraContext); + // Set the funders + this.addFunders(targetRepoObj, metadata, extraContext); + // Set about + this.addSubjects(targetRepoObj, metadata, extraContext); + // Set the provenance + this.addHistory(targetRepoObj, metadata, creator, approver); + // Finally... + if (!_.isEmpty(extraContext)) { + targetCrate.addContext(extraContext); + } + targetRepoObj.rootDataset["memberOf"] = rootCollection.rootDataset; + await targetRepoObj.addToRepo(); + } + + private addHistory(targetRepoObj: any, metadata: Object, creator: Object, approver: Object) { + // check if the creator and approver are in the author list, if not add them + sails.log.verbose(`${this.logHeader} addHistory() -> adding creator and approver to the author list`); + const people = _.concat(targetRepoObj.rootDataset.author, targetRepoObj.rootDataset.contributor); + let creatorPerson = _.find(people, (a) => a && a['email'] == creator['email']); + let approverPerson = _.find(people, (a) => a && a['email'] == approver['email']); + + if (!creatorPerson) { + creatorPerson = this.getPerson(creator, "Person"); + targetRepoObj.crate.addEntity(creatorPerson); + } + if (!approverPerson) { + approverPerson = this.getPerson(approver, "Person"); + targetRepoObj.crate.addEntity(approverPerson); + } + // add the history entries + sails.log.verbose(`${this.logHeader} addHistory() -> adding history entries`); + targetRepoObj.crate.addEntity({ + '@id': 'history1', + '@type': 'CreateAction', + 'name': 'Create', + 'description': 'Data record created', + 'endTime': targetRepoObj.rootDataset.dateCreated, + 'object': {'@id': targetRepoObj.crate.rootId}, + 'agent': {'@id': creatorPerson['@id']} + }); + targetRepoObj.crate.addEntity({ + '@id': 'history2', + '@type': 'UpdateAction', + 'name': 'Publish', + 'endTime': targetRepoObj.rootDataset.datePublished, + 'object': {'@id': targetRepoObj.crate.rootId}, + 'agent': {'@id': approverPerson['@id']} + }); + } + + private addSubjects(targetRepoObj: any, metadata: Object, extraContext: Object) { + sails.log.verbose(`${this.logHeader} addSubjects() -> adding subjects to the dataset`); + const subjects = []; + for (let subjectField of sails.config.datapubs.metadata.subjects) { + const fieldVals = _.isArray(metadata[subjectField]) ? metadata[subjectField] : [metadata[subjectField]]; + sails.log.verbose(`${subjectField} -> fieldVal: ${JSON.stringify(fieldVals)}`); + if (!_.isEmpty(fieldVals)) { + for (let fieldVal of fieldVals) { + if (!_.isEmpty(fieldVal)) { + const id = `${sails.config.datapubs.metadata.DEFAULT_IRI_PREFS['about'][subjectField]}${fieldVal['notation']}`; + const subject = { + '@id': id, + '@type': 'StructuredValue', + 'url': id, + 'identifier': id, + 'name': fieldVal['name'] + }; + subjects.push(subject); + } + } + } + } + targetRepoObj.rootDataset['about'] = subjects; + } + + private addFunders(targetRepoObj: any, metadata: Object, extraContext: Object) { + sails.log.verbose(`${this.logHeader} addFunders() -> adding funders to the dataset`); + let funders = []; + for (let fundingField of sails.config.datapubs.metadata.funders) { + const fieldVals = _.isArray(metadata[fundingField]) ? metadata[fundingField] : [metadata[fundingField]]; + sails.log.verbose(`${fundingField} -> fieldVal: ${JSON.stringify(fieldVals)}`); + + for (let fieldVal of fieldVals) { + if (!_.isEmpty(fieldVal) && !_.isEmpty(_.get(fieldVal, 'dc_identifier[0]'))) { + const id = `${sails.config.datapubs.metadata.DEFAULT_IRI_PREFS['funder']}${fieldVal['dc_identifier'][0]}`; + const funder = { + '@id': id, + '@type': 'Organization', + 'name': fieldVal['dc_title'], + 'identifier': id + }; + funders.push(funder); + } + } + } + targetRepoObj.rootDataset['funder'] = funders; + } + + private addTemporalCoverage(targetRepoObj: any, metadata: Object, extraContext: Object) { + sails.log.verbose(`${this.logHeader} addTemporalCoverage() -> adding temporal coverage to the dataset`); + var tc = ''; + if( metadata['startDate'] ) { + tc = metadata['startDate']; + if( metadata['endDate'] ) { + tc += '/' + metadata['endDate']; + } + } else if ( metadata['endDate'] ) { + tc = metadata['endDate']; + } + if( metadata['timePeriod'] ) { + if( tc ) { + tc = tc + '; ' + metadata['timePeriod']; + } else { + tc = metadata['timePeriod']; + } + } + if (!_.isEmpty(tc)) { + targetRepoObj.rootDataset.temporalCoverage = tc; + } + } + + private addSpatialCoverage(targetRepoOjb: any, metadata: Object, extraContext: Object) { + sails.log.verbose(`${this.logHeader} addSpatialCoverage() -> adding spatial coverage to the dataset`); + if (metadata['geospatial']) { + if (_.isEmpty(extraContext['Geometry'])) { + extraContext['Geometry'] = "http://www.opengis.net/ont/geosparql#Geometry"; + extraContext['asWKT'] = "http://www.opengis.net/ont/geosparql#asWKT"; + } + let geospatial = metadata['geospatial']; + sails.log.verbose(`spatialCoverage -> ${JSON.stringify(geospatial)}`); + if (!_.isArray(geospatial)) { + geospatial = [geospatial]; + } + // COmmenting out the "proper way" of GEOMETRYCOLLECTION as it doesn't show up in the UI! + const convertedGeoJson = _.map(geospatial, (geoJson, idx) => { + return { + "@id": `_:place-${idx}`, + "@type": "Place", + "geo": this.convertToWkt(`_:geo-${idx}`, geoJson) + }; + }); + // NOTE: the "proper way" of GEOMETRYCOLLECTION doesn't show up in the UI (unsupported) + // Code below is for converting each feature collection entry as a separate place in the hopes that it will show up in the UI + // const convertedGeoJson = []; + // _.each(geospatial, (geoJson, idx) => { + // if (geoJson['type'] === 'FeatureCollection') { + // _.each(geoJson['features'], (feature, fIdx) => { + // convertedGeoJson.push({ + // "@id": `_:place-${idx}-${fIdx}`, + // "@type": "Place", + // "geo": this.convertToWkt(`_:geo-${idx}-${fIdx}`, feature) + // }); + // }); + // } else { + // convertedGeoJson.push({ + // "@id": `_:place-${idx}`, + // "@type": "Place", + // "geo": this.convertToWkt(`_:geo-${idx}`, geoJson) + // }); + // } + + // }); + sails.log.verbose(`Converted spatialCoverage -> ${JSON.stringify(convertedGeoJson)}`) + targetRepoOjb.rootDataset.spatialCoverage = convertedGeoJson; + } else { + sails.log.verbose(`No geospatial field found in metadata`); + } + } + + private convertToWkt(id: string, geoJsonSrc:any) { + let geoJson = _.cloneDeep(geoJsonSrc); + _.unset(geoJson, '@type'); + const wkt = convertToWK(geoJson); + sails.log.verbose(`Converted WKT -> ${wkt}`); + return { + "@id": id, + "@type": "Geometry", + "asWKT": wkt + }; + } + + private addRelatedWorks(targetRepoObj: any, metadata: Object) { + for (let relatedFieldConf of sails.config.datapubs.metadata.related_works) { + let relatedWorks = []; + const fieldVals = _.isArray(metadata[`related_${relatedFieldConf.field}`]) ? metadata[`related_${relatedFieldConf.field}`] : [metadata[`related_${relatedFieldConf.field}`]]; + sails.log.verbose(`related_${relatedFieldConf.field} -> fieldVal: ${JSON.stringify(fieldVals)}`); + + for (let fieldVal of fieldVals) { + if (!_.isEmpty(fieldVal) && !_.isEmpty(fieldVal['related_url'])) { + const relatedWork = { + '@id': fieldVal['related_url'], + '@type': relatedFieldConf.type, + 'name': fieldVal['related_title'], + 'identifier': fieldVal['related_url'] + }; + if (!_.isEmpty(fieldVal.related_notes)) { + relatedWork['description'] = fieldVal.related_notes; + } + relatedWorks.push(relatedWork); + } + } + targetRepoObj.rootDataset[relatedFieldConf.field] = relatedWorks; + } + } + + private async addFiles(targetRepoObj: any, record: Object, attachments: any[]) { + for (let a of attachments) { + const fileAttMetaPart = { + "name": a['name'], + "@id": a['name'], + "@type": ["File"], + "encodingFormat": mime.lookup(a['name']) + }; + await targetRepoObj.addFile(fileAttMetaPart, a['parentDir'], a['name']); + } + } + + private getLicense(metadata: Object): Object { + const licenses = []; + if (!_.isEmpty(metadata['license_other_url']) || !_.isEmpty(metadata['license_notes'])) { + if(metadata['license_other_url'] ) { + licenses.push({ + '@id': metadata['license_other_url'], + '@type': 'CreativeWork', + 'url': metadata['license_other_url'], + 'name': ( metadata['license_notes'] || metadata['license_other_url']) + }); + } else { + licenses.push({ + '@id': sails.config.datapubs.metadata.DEFAULT_IRI_PREFS['license'] + 'other', + '@type': 'CreativeWork', + 'name': metadata['license_notes'] + }); + } + } + if(metadata['license_identifier'] && metadata['license_identifier'] !== 'undefined' ) { + licenses.push({ + '@id': metadata['license_identifier'], + '@type': 'CreativeWork', + 'name': metadata['license_identifier'], + 'url': metadata['license_identifier'] + }); + } + + if(metadata['accessRights_url']) { + licenses.push({ + '@id': metadata['accessRights_url'], + '@type': 'WebSite', + 'name': "Conditions of Access", + 'url': metadata['accessRights_url'] + }); + } + // if no license is found, use the default license because the ONI indexers require a license + if (_.isEmpty(licenses) && sails.config.datapubs.rootCollection.enableDatasetToUseDefaultLicense) { + licenses.push(sails.config.datapubs.rootCollection.defaultLicense); + } + return licenses; + } + + private getCreators(metadata: Object, organization: Object): Object[] { + let creators = []; + if (metadata['creators']) { + creators = _.compact(metadata['creators'].map((creator) => { + const person = this.getPerson(creator, "Person"); + if (person) { + person['affiliation'] = organization; + return person; + } + })); + } + return creators; + } + + private getPerson(rbPerson: Object, type:string): Object { + // ORCID prioritised as per https://www.researchobject.org/ro-crate/specification/1.1/contextual-entities.html#identifiers-for-contextual-entities + const id = rbPerson['orcid'] || rbPerson['email'] || rbPerson['text_full_name']; + if (!id) { + return undefined; + } + return { + "@id": id, + "@type": type, + "name": rbPerson['text_full_name'], + "givenName": rbPerson['givenName'], + "familyName": rbPerson['familyName'], + "email": rbPerson['email'] + }; + } + + private getYearFromDate(dateString):string { + const date = new Date(dateString); + const year = date.getFullYear(); + return year.toString(); + } + + private async ensureDir(dirPath: string): Promise { + try { + await fs.access(dirPath, fs.constants.F_OK); + // Directory exists, nothing to do + } catch { + // Directory does not exist, create it + await fs.mkdir(dirPath, { recursive: true }); + } + } + + private async pathExists(path: string): Promise { + try { + await fs.access(path, fs.constants.F_OK); + return true; + } catch { + return false; + } + } + + private async writeToFileUsingStream(filePath: string, inputStream: any): Promise { + const writeStream = createWriteStream(filePath); + inputStream.pipe(writeStream); + await finished(writeStream); // Wait for the stream to finish + } + + // writeDatastream works for new redbox-storage -- using sails-hook-redbox-storage-mongo. + + private async writeDatastream(stream: any, dir: string, fn: string) { + await this.ensureDir(dir); + await this.writeToFileUsingStream(path.join(dir, fn), stream); + } + + private async recordPublicationError(oid: string, record: Object, err: Error): Promise { + const branding = sails.config.auth.defaultBrand; + // turn off postsave triggers + sails.log.verbose(`${this.logHeader} recordPublicationError() -> recording publication error in record metadata`); + record['metadata']['publication_error'] = "Data publication failed with error: " + err.name + " " + err.message; + await RecordsService.updateMeta(branding, oid, record, null, true, false); + } + + } + +} + +module.exports = new Services.OniService().exports(); \ No newline at end of file