diff --git a/.github/workflows/deploy_alpha.yml b/.github/workflows/deploy_alpha.yml index 5a442fe7..b3eda993 100644 --- a/.github/workflows/deploy_alpha.yml +++ b/.github/workflows/deploy_alpha.yml @@ -71,17 +71,19 @@ jobs: NODE_AUTH_TOKEN: ${{ secrets.GITHUB_TOKEN }} run: | echo "//npm.pkg.github.com/:_authToken=${{ secrets.GITHUB_TOKEN }}" > ~/.npmrc + CURRENT_PACKAGE_VERSION=$(jq -r '.version' package.json) + BASE_VERSION=${CURRENT_PACKAGE_VERSION%%-alpha.*} - LATEST_VERSION=$(npm show @IQSS/dataverse-client-javascript versions --registry=https://npm.pkg.github.com/ --json | jq -r '.[]' | grep "^2.0.0-alpha." | sort -V | tail -n 1) + LATEST_VERSION=$(npm show @iqss/dataverse-client-javascript versions --registry=https://npm.pkg.github.com/ --json | jq -r '.[]' | grep "^${BASE_VERSION}-alpha\\." | sort -V | tail -n 1) if [ -z "$LATEST_VERSION" ]; then NEW_INCREMENTAL_NUMBER=1 else - CURRENT_INCREMENTAL_NUMBER=$(echo $LATEST_VERSION | sed 's/2.0.0-alpha.//') + CURRENT_INCREMENTAL_NUMBER=$(echo "$LATEST_VERSION" | sed "s/^${BASE_VERSION}-alpha\\.//") NEW_INCREMENTAL_NUMBER=$((CURRENT_INCREMENTAL_NUMBER + 1)) fi - NEW_VERSION="2.0.0-alpha.${NEW_INCREMENTAL_NUMBER}" + NEW_VERSION="${BASE_VERSION}-alpha.${NEW_INCREMENTAL_NUMBER}" echo "Latest version: $LATEST_VERSION" echo "New version: $NEW_VERSION" @@ -92,7 +94,6 @@ jobs: - name: Publish package run: | echo "$(jq '.publishConfig.registry = "https://npm.pkg.github.com"' package.json)" > package.json - echo "$( jq '.name = "@IQSS/dataverse-client-javascript"' package.json )" > package.json - npm publish --@IQSS:registry=https://npm.pkg.github.com + npm publish --@iqss:registry=https://npm.pkg.github.com env: NODE_AUTH_TOKEN: ${{ secrets.GITHUB_TOKEN }} diff --git a/.github/workflows/deploy_pr.yml b/.github/workflows/deploy_pr.yml index f8cba6dd..8635e0f3 100644 --- a/.github/workflows/deploy_pr.yml +++ b/.github/workflows/deploy_pr.yml @@ -73,7 +73,6 @@ jobs: - name: Publish package run: | echo "$(jq '.publishConfig.registry = "https://npm.pkg.github.com"' package.json)" > package.json - echo "$( jq '.name = "@IQSS/dataverse-client-javascript"' package.json )" > package.json - npm publish --@IQSS:registry=https://npm.pkg.github.com + npm publish --@iqss:registry=https://npm.pkg.github.com env: NODE_AUTH_TOKEN: ${{ secrets.GITHUB_TOKEN }} diff --git a/CHANGELOG.md b/CHANGELOG.md index 2ea3e0f7..d042fa65 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -14,7 +14,45 @@ This changelog follows the principles of [Keep a Changelog](https://keepachangel ### Removed -[Unreleased]: https://github.com/IQSS/dataverse-client-javascript/compare/v2.1.0...develop +## [v2.2.0] -- 2026-04-24 + +### Added + +- Datasets: Added `updateDatasetLicense` use case and repository method to support Dataverse endpoint `PUT /datasets/{id}/license`, for updating dataset license or custom terms. +- Datasets: Added `getDatasetStorageDriver` use case and repository method to support Dataverse endpoint `GET /datasets/{identifier}/storageDriver`, for retrieving dataset storage driver configuration with properties: name, type, label, directUpload, directDownload, and uploadOutOfBand. +- Datasets: Added `getDatasetUploadLimits` use case and repository method to support Dataverse endpoint `GET /datasets/{id}/uploadlimits`, for retrieving remaining storage upload quotas, if present. +- New Use Case: [Get Collections For Linking Use Case](./docs/useCases.md#get-collections-for-linking). +- New Use Case: [Create a Template](./docs/useCases.md#create-a-template) under Templates. +- New Use Case: [Get a Template](./docs/useCases.md#get-a-template) under Templates. +- New Use Case: [Delete a Template](./docs/useCases.md#delete-a-template) under Templates. +- Templates: Added `setTemplateAsDefault` use case and repository method to support Dataverse endpoint `POST /dataverses/{id}/template/default/{templateId}`. +- Templates: Added `unsetTemplateAsDefault` use case and repository method to support Dataverse endpoint `DELETE /dataverses/{id}/template/default`. +- New Use Case: [Update Terms of Access](./docs/useCases.md#update-terms-of-access). +- Guestbooks: Added use cases and repository support for guestbook creation, listing, and enabling/disabling. +- Guestbooks: Added dataset-level guestbook assignment and removal support via `assignDatasetGuestbook` (`PUT /api/datasets/{identifier}/guestbook`) and `removeDatasetGuestbook` (`DELETE /api/datasets/{identifier}/guestbook`). +- Datasets/Guestbooks: Added `guestbookId` in `getDataset` responses. +- Access: Added`access` module for guestbook-at-request and download terms/guestbook submission endpoints. +- New Use Case: [Get Publish Dataset Disclaimer Text](./docs/useCases.md#get-publish-dataset-disclaimer-text). +- New Use Case: [Get Dataset Publish Popup Custom Text](./docs/useCases.md#get-dataset-publish-popup-custom-text). +- DatasetType: Updated datasetType data model. Added two more fields: description and displayName. + +### Changed + +- Add pagination query parameters to Dataset Version Summeries and File Version Summaries use cases. +- Templates: Rename `CreateDatasetTemplateDTO` to `CreateTemplateDTO`. +- Templates: Rename `createDatasetTemplate` repository method to `createTemplate`. +- Templates: Rename `getDatasetTemplates` repository method to `getTemplatesByCollectionId`. + +### Fixed + +- In GetAllNotificationsByUser use case, additionalInfo field is returned as an object instead of a string. +- In GetAllNotificationsByUser use case, added support for filtering unread messages and pagination. + +### Removed + +- Removed date fields validations in create and update dataset use cases, since validation is already handled in the backend and SPA frontend (other clients should perform client side validation also). This avoids duplicated logic and keeps the package focused on its core responsibility. + +[Unreleased]: https://github.com/IQSS/dataverse-client-javascript/compare/v2.2.0...develop --- @@ -38,7 +76,7 @@ This changelog follows the principles of [Keep a Changelog](https://keepachangel - Use cases for External Tools: GetExternalTools, GetDatasetExternalToolResolved, GetFileExternalToolResolved. -- Use case: GetDatasetTemplates. +- Use case: GetTemplatesByCollectionId. - Use case: GetAvailableStandardLicenses. diff --git a/README.md b/README.md index 44b2266d..69ee09e0 100644 --- a/README.md +++ b/README.md @@ -36,18 +36,18 @@ getDataset.execute(datasetIdentifier, datasetVersion).then((dataset: Dataset) => /* ... */ ``` -For detailed information about available use cases see [Use Cases Docs](https://github.com/IQSS/dataverse-client-javascript/blob/main/docs/useCases.md). +For detailed information about available use cases see [Use Cases Docs](docs/useCases.md). -For detailed information about usage see [Usage Docs](https://github.com/IQSS/dataverse-client-javascript/blob/main/docs/usage.md). +For detailed information about usage see [Usage Docs](docs/usage.md). ## Changelog -See [CHANGELOG.md](https://github.com/IQSS/dataverse-client-javascript/blob/main/CHANGELOG.md) for a detailed history of changes to this project. +See [CHANGELOG.md](CHANGELOG.md) for a detailed history of changes to this project. ## Contributing -Want to add a new use case or improve an existing one? Please check the [Contributing](https://github.com/IQSS/dataverse-client-javascript/blob/main/CONTRIBUTING.md) section. +Want to add a new use case or improve an existing one? Please check the [Contributing](CONTRIBUTING.md) section. ## License -This project is open source and available under the [MIT License](https://github.com/IQSS/dataverse-client-javascript/blob/main/LICENSE). +This project is open source and available under the [MIT License](LICENSE). diff --git a/docs/useCases.md b/docs/useCases.md index 60704c23..4fb7a6f1 100644 --- a/docs/useCases.md +++ b/docs/useCases.md @@ -16,6 +16,7 @@ The different use cases currently available in the package are classified below, - [List All Collection Items](#list-all-collection-items) - [List My Data Collection Items](#list-my-data-collection-items) - [Get Collection Featured Items](#get-collection-featured-items) + - [Get Collections for Linking](#get-collections-for-linking) - [Collections write use cases](#collections-write-use-cases) - [Create a Collection](#create-a-collection) - [Update a Collection](#update-a-collection) @@ -24,6 +25,15 @@ The different use cases currently available in the package are classified below, - [Update Collection Featured Items](#update-collection-featured-items) - [Delete Collection Featured Items](#delete-collection-featured-items) - [Delete a Collection Featured Item](#delete-a-collection-featured-item) +- [Templates](#Templates) + - [Templates read use cases](#templates-read-use-cases) + - [Get a Template](#get-a-template) + - [Get Templates By Collection Id](#get-templates-by-collection-id) + - [Templates write use cases](#templates-write-use-cases) + - [Create a Template](#create-a-template) + - [Delete a Template](#delete-a-template) + - [Set Template As Default](#set-template-as-default) + - [Unset Template As Default](#unset-template-as-default) - [Datasets](#Datasets) - [Datasets read use cases](#datasets-read-use-cases) - [Get a Dataset](#get-a-dataset) @@ -39,11 +49,14 @@ The different use cases currently available in the package are classified below, - [Get Dataset Linked Collections](#get-dataset-linked-collections) - [Get Dataset Available Categories](#get-dataset-available-categories) - [Get Dataset Templates](#get-dataset-templates) + - [Get Dataset Storage Driver](#get-dataset-storage-driver) - [Get Dataset Available Dataset Types](#get-dataset-available-dataset-types) - [Get Dataset Available Dataset Type](#get-dataset-available-dataset-type) + - [Get Dataset Upload Limits](#get-dataset-upload-limits) - [Datasets write use cases](#datasets-write-use-cases) - [Create a Dataset](#create-a-dataset) - [Update a Dataset](#update-a-dataset) + - [Update a Dataset License](#update-a-dataset-license) - [Publish a Dataset](#publish-a-dataset) - [Deaccession a Dataset](#deaccession-a-dataset) - [Delete a Draft Dataset](#delete-a-draft-dataset) @@ -113,6 +126,21 @@ The different use cases currently available in the package are classified below, - [Get External Tools](#get-external-tools) - [Get Dataset External Tool Resolved](#get-dataset-external-tool-resolved) - [Get File External Tool Resolved](#get-file-external-tool-resolved) +- [Guestbooks](#Guestbooks) + - [Guestbooks read use cases](#guestbooks-read-use-cases) + - [Get a Guestbook](#get-a-guestbook) + - [Get Guestbooks By Collection Id](#get-guestbooks-by-collection-id) + - [Guestbooks write use cases](#guestbooks-write-use-cases) + - [Create a Guestbook](#create-a-guestbook) + - [Set Guestbook Enabled](#set-guestbook-enabled) + - [Assign Dataset Guestbook](#assign-dataset-guestbook) + - [Remove Dataset Guestbook](#remove-dataset-guestbook) +- [Access](#Access) + - [Access write use cases](#access-write-use-cases) + - [Submit Guestbook For Datafile Download](#submit-guestbook-for-datafile-download) + - [Submit Guestbook For Datafiles Download](#submit-guestbook-for-datafiles-download) + - [Submit Guestbook For Dataset Download](#submit-guestbook-for-dataset-download) + - [Submit Guestbook For Dataset Version Download](#submit-guestbook-for-dataset-version-download) ## Collections @@ -336,6 +364,69 @@ The `collectionIdOrAlias` is a generic collection identifier, which can be eithe If no collection identifier is specified, the default collection identifier; `:root` will be used. If you want to search for a different collection, you must add the collection identifier as a parameter in the use case call. +#### Get Collections for Linking + +Returns an array of [CollectionSummary](../src/collections/domain/models/CollectionSummary.ts) (id, alias, displayName) representing the Dataverse collections to which a given Dataverse collection or Dataset may be linked. + +This use case supports an optional `searchTerm` to filter by collection name. + +##### Example calls: + +```typescript +import { getCollectionsForLinking } from '@iqss/dataverse-client-javascript' + +/* ... */ + +// Case 1: For a given Dataverse collection (by numeric id or alias) +const collectionIdOrAlias: number | string = 'collectionAlias' // or 123 +const searchTerm = 'searchOn' + +getCollectionsForLinking + .execute('collection', collectionIdOrAlias, searchTerm) + .then((collections) => { + // collections: CollectionSummary[] + /* ... */ + }) + .catch((error: Error) => { + /* ... */ + }) + +/* ... */ + +// Case 2: For a given Dataset (by persistent identifier) +const persistentId = 'doi:10.5072/FK2/J8SJZB' + +getCollectionsForLinking + .execute('dataset', persistentId, searchTerm) + .then((collections) => { + // collections: CollectionSummary[] + /* ... */ + }) + .catch((error: Error) => { + /* ... */ + }) + +// Case 3: [alreadyLinked] Optional flag. When true, returns collections currently linked (candidates to unlink). Defaults to false. +const alreadyLinked = true + +getCollectionsForLinking + .execute('dataset', persistentId, searchTerm, alreadyLinked) + .then((collections) => { + // collections: CollectionSummary[] + /* ... */ + }) + .catch((error: Error) => { + /* ... */ + }) +``` + +_See [use case](../src/collections/domain/useCases/GetCollectionsForLinking.ts) implementation_. + +Notes: + +- When the first argument is `'collection'`, the second argument can be a numeric collection id or a collection alias. +- When the first argument is `'dataset'`, the second argument must be the dataset persistent identifier string (e.g., `doi:...`). + ### Collections Write Use Cases #### Create a Collection @@ -503,6 +594,136 @@ deleteCollectionFeaturedItem.execute(featuredItemId) _See [use case](../src/collections/domain/useCases/DeleteCollectionFeaturedItem.ts)_ definition. +## Templates + +### Templates Read Use Cases + +#### Get a Template + +Returns a [Template](../src/templates/domain/models/Template.ts) by its template id. + +##### Example call: + +```typescript +import { getTemplate } from '@iqss/dataverse-client-javascript' + +const templateId = 12345 + +getTemplate.execute(templateId).then((template: Template) => { + /* ... */ +}) +``` + +_See [use case](../src/templates/domain/useCases/GetTemplate.ts)_ definition. + +#### Get Templates By Collection Id + +Returns a [Template](../src/templates/domain/models/Template.ts) array containing the templates of the requested collection, given the collection identifier or alias. + +##### Example call: + +```typescript +import { getTemplatesByCollectionId } from '@iqss/dataverse-client-javascript' + +const collectionIdOrAlias = 12345 + +getTemplatesByCollectionId.execute(collectionIdOrAlias).then((template: Template[]) => { + /* ... */ +}) +``` + +_See [use case](../src/templates/domain/useCases/GetTemplatesByCollectionId.ts)_ definition. + +### Templates Write Use Cases + +#### Create a Template + +Creates a template for a given Dataverse collection id or alias. + +##### Example call: + +```typescript +import { createTemplate } from '@iqss/dataverse-client-javascript' +import { CreateTemplateDTO } from '@iqss/dataverse-client-javascript' + +const collectionAlias = ':root' +const template: CreateTemplateDTO = { + name: 'Dataverse template', + isDefault: true, + fields: [ + { + typeName: 'author', + typeClass: 'compound', + multiple: true, + value: [ + { + authorName: { typeName: 'authorName', value: 'Belicheck, Bill' }, + authorAffiliation: { typeName: 'authorIdentifierScheme', value: 'ORCID' } + } + ] + } + ], + instructions: [{ instructionField: 'author', instructionText: 'The author data' }] +} + +await createTemplate.execute(template, collectionAlias) +``` + +_See [use case](../src/templates/domain/useCases/CreateTemplate.ts) implementation_. + +#### Delete a Template + +Deletes a template by its template id. + +##### Example call: + +```typescript +import { deleteTemplate } from '@iqss/dataverse-client-javascript' + +const templateId = 12345 + +await deleteTemplate.execute(templateId) +``` + +_See [use case](../src/templates/domain/useCases/DeleteTemplate.ts)_ definition. + +#### Set Template As Default + +Sets the default template for a given Dataverse collection. + +You must have edit permissions on the collection in order to use this endpoint. + +##### Example call: + +```typescript +import { setTemplateAsDefault } from '@iqss/dataverse-client-javascript' + +const collectionIdOrAlias = ':root' +const templateId = 12345 + +await setTemplateAsDefault.execute(templateId, collectionIdOrAlias) +``` + +_See [use case](../src/templates/domain/useCases/SetTemplateAsDefault.ts)_ definition. + +#### Unset Template As Default + +Removes the default template from a given Dataverse collection. + +You must have edit permissions on the collection in order to use this endpoint. + +##### Example call: + +```typescript +import { unsetTemplateAsDefault } from '@iqss/dataverse-client-javascript' + +const collectionIdOrAlias = ':root' + +await unsetTemplateAsDefault.execute(collectionIdOrAlias) +``` + +_See [use case](../src/templates/domain/useCases/UnsetTemplateAsDefault.ts)_ definition. + ## Datasets ### Datasets Read Use Cases @@ -772,7 +993,7 @@ The `DatasetPreviewSubset`returned instance contains a property called `totalDat #### Get Dataset Versions Summaries -Returns an array of [DatasetVersionSummaryInfo](../src/datasets/domain/models/DatasetVersionSummaryInfo.ts) that contains information about what changed in every specific version. +Returns the total count of versions and an array of [DatasetVersionSummaryInfo](../src/datasets/domain/models/DatasetVersionSummaryInfo.ts) that contains information about what changed in every specific version. ##### Example call: @@ -785,7 +1006,7 @@ const datasetId = 'doi:10.77777/FK2/AAAAAA' getDatasetVersionsSummaries .execute(datasetId) - .then((datasetVersionsSummaries: DatasetVersionSummaryInfo[]) => { + .then((datasetVersionsSummaries: DatasetVersionSummarySubset) => { /* ... */ }) @@ -794,7 +1015,9 @@ getDatasetVersionsSummaries _See [use case](../src/datasets/domain/useCases/GetDatasetVersionsSummaries.ts) implementation_. -The `datasetId` parameter can be a string, for persistent identifiers, or a number, for numeric identifiers. +- The `datasetId` parameter can be a string, for persistent identifiers, or a number, for numeric identifiers. +- **limit**: (number) Limit for pagination. +- **offset**: (number) Offset for pagination. #### Get Dataset Linked Collections @@ -840,7 +1063,7 @@ _See [use case](../src/datasets/domain/useCases/GetDatasetAvailableDatasetTypes. #### Get Dataset Available Dataset Type -Returns an available dataset types that can be used at dataset creation. +Returns a single available dataset type that can be used at dataset creation. ###### Example call: @@ -849,13 +1072,31 @@ import { getDatasetAvailableDatasetType } from '@iqss/dataverse-client-javascrip /* ... */ -getDatasetAvailableDatasetType.execute().then((datasetType: DatasetType) => { +const datasetTypeIdOrName = 'dataset' + +getDatasetAvailableDatasetType.execute(datasetTypeIdOrName).then((datasetType: DatasetType) => { /* ... */ }) ``` _See [use case](../src/datasets/domain/useCases/GetDatasetAvailableDatasetType.ts) implementation_. +The `datasetTypeIdOrName` parameter can be either the numeric dataset type id or its name. + +Example returned value: + +```typescript +{ + id: 1, + name: 'dataset', + displayName: 'Dataset', + linkedMetadataBlocks: [], + availableLicenses: [], + description: + 'A study, experiment, set of observations, or publication. A dataset can comprise a single file or multiple files.' +} +``` + ### Datasets Write Use Cases #### Create a Dataset @@ -977,6 +1218,43 @@ updateDataset.execute(datasetId, datasetDTO) _See [use case](../src/datasets/domain/useCases/UpdateDataset.ts) implementation_. +#### Update a Dataset License + +Updates the license of a dataset by applying it to the draft version. If no draft exists, a new one is automatically created by the API. Supports predefined licenses (by name) or custom terms of use and access. + +##### Example calls: + +```typescript +import { + updateDatasetLicense, + DatasetLicenseUpdateRequest +} from '@iqss/dataverse-client-javascript' + +/* ... */ + +const datasetId = 1 + +const predefinedPayload: DatasetLicenseUpdateRequest = { name: 'CC BY 4.0' } +await updateDatasetLicense.execute(datasetId, predefinedPayload) + +const customPayload: DatasetLicenseUpdateRequest = { + customTerms: { + termsOfUse: 'Your terms of use', + confidentialityDeclaration: 'Your confidentiality declaration', + specialPermissions: 'Your special permissions', + restrictions: 'Your restrictions', + citationRequirements: 'Your citation requirements', + depositorRequirements: 'Your depositor requirements', + conditions: 'Your conditions', + disclaimer: 'Your disclaimer' + } +} + +updateDatasetLicense.execute(datasetId, customPayload) +``` + +_See [use case](../src/datasets/domain/useCases/UpdateDatasetLicense.ts) implementation_. + The `datasetId` parameter can be a string, for persistent identifiers, or a number, for numeric identifiers. #### Publish a Dataset @@ -1012,6 +1290,38 @@ The `versionUpdateType` parameter can be a [VersionUpdateType](../src/datasets/d - `VersionUpdateType.MAJOR` - `VersionUpdateType.UPDATE_CURRENT` +#### Update Terms of Access + +Updates the Terms of Access for restricted files on a dataset. + +##### Example call: + +```typescript +import { updateTermsOfAccess } from '@iqss/dataverse-client-javascript' + +/* ... */ + +const datasetId = 3 + +await updateTermsOfAccess.execute(datasetId, { + fileAccessRequest: true, + termsOfAccessForRestrictedFiles: 'Your terms of access for restricted files', + dataAccessPlace: 'Your data access place', + originalArchive: 'Your original archive', + availabilityStatus: 'Your availability status', + contactForAccess: 'Your contact for access', + sizeOfCollection: 'Your size of collection', + studyCompletion: 'Your study completion' +}) +``` + +_See [use case](../src/datasets/domain/useCases/UpdateTermsOfAccess.ts) implementation_. + +Notes: + +- If the dataset is already published, this action creates a DRAFT version containing the new terms. +- Unspecified fields are treated as omissions: sending only `fileAccessRequest` will update that field and leave all other terms absent (undefined). In practice, the new values you send fully replace the previous set of terms — so if you omit a field, you are effectively clearing it unless you include its original value in the new input. + #### Deaccession a Dataset Deaccession a Dataset, given its identifier, version, and deaccessionDatasetDTO to perform. @@ -1179,9 +1489,33 @@ getDatasetTemplates.execute(collectionIdOrAlias).then((datasetTemplates: Dataset _See [use case](../src/datasets/domain/useCases/GetDatasetTemplates.ts)_ definition. +#### Get Dataset Storage Driver + +Returns a [StorageDriver](../src/datasets/domain/models/StorageDriver.ts) instance with storage driver configuration for a dataset, including properties like name, type, label, and upload/download capabilities. + +##### Example call: + +```typescript +import { getDatasetStorageDriver } from '@iqss/dataverse-client-javascript' + +/* ... */ + +const datasetId = 'doi:10.77777/FK2/AAAAAA' + +getDatasetStorageDriver.execute(datasetId).then((storageDriver: StorageDriver) => { + /* ... */ +}) + +/* ... */ +``` + +_See [use case](../src/datasets/domain/useCases/GetDatasetStorageDriver.ts) implementation_. + +The `datasetId` parameter can be a string, for persistent identifiers, or a number, for numeric identifiers. + #### Add a Dataset Type -Adds a dataset types that can be used at dataset creation. +Adds a dataset type that can be used at dataset creation. ###### Example call: @@ -1190,6 +1524,14 @@ import { addDatasetType } from '@iqss/dataverse-client-javascript' /* ... */ +const datasetType = { + name: 'software', + displayName: 'Software', + linkedMetadataBlocks: ['codeMeta20'], + availableLicenses: ['MIT', 'Apache-2.0'], + description: 'Software data and metadata.' +} + addDatasetType.execute(datasetType).then((datasetType: DatasetType) => { /* ... */ }) @@ -1197,6 +1539,8 @@ addDatasetType.execute(datasetType).then((datasetType: DatasetType) => { _See [use case](../src/datasets/domain/useCases/AddDatasetType.ts) implementation_. +The `datasetType` parameter must match [DatasetTypeDTO](../src/datasets/domain/dtos/DatasetTypeDTO.ts) and includes all [DatasetType](../src/datasets/domain/models/DatasetType.ts) fields except `id`. + #### Link Dataset Type with Metadata Blocks Link a dataset type with metadata blocks. @@ -1251,6 +1595,30 @@ deleteDatasetType.execute(datasetTypeId).then(() => { _See [use case](../src/datasets/domain/useCases/DeleteDatasetType.ts) implementation_. +#### Get Dataset Upload Limits + +Returns a [DatasetUploadLimits](../src/datasets/domain/models/DatasetUploadLimits.ts) instance with the remaining dataset storage and/or file upload quotas, if present. + +##### Example call: + +```typescript +import { getDatasetUploadLimits } from '@iqss/dataverse-client-javascript' + +/* ... */ + +const datasetId = 'doi:10.77777/FK2/AAAAAA' + +getDatasetUploadLimits.execute(datasetId).then((uploadLimits: DatasetUploadLimits) => { + /* ... */ +}) + +/* ... */ +``` + +_See [use case](../src/datasets/domain/useCases/GetDatasetUploadLimits.ts) implementation_. + +If the backend does not define any quota limits for the dataset, the returned object can be empty (`{}`). + ## Files ### Files read use cases @@ -1900,7 +2268,7 @@ The `fileId` parameter can be a string, for persistent identifiers, or a number, #### Get File Version Summaries -Get the file versions summaries, return a list of summaries for each version +Get the file versions summaries, return a total count of versions and a list of summaries for each version ##### Example call: @@ -1911,7 +2279,7 @@ import { getFileVersionSummaries } from '@iqss/dataverse-client-javascript' const fileId = 1 -getFileVersionSummaries.execute(fileId).then((fileVersionSummaries: fileVersionSummaryInfo[]) => { +getFileVersionSummaries.execute(fileId).then((fileVersionSummaries: fileVersionSummarySubset) => { /* ... */ }) @@ -1920,6 +2288,9 @@ getFileVersionSummaries.execute(fileId).then((fileVersionSummaries: fileVersionS _See [use case](../src/files/domain/useCases/GetFileVersionSummaries.ts) implementation_. +- **limit**: (number) Limit for pagination. +- **offset**: (number) Offset for pagination. + ## Metadata Blocks ### Metadata Blocks read use cases @@ -2257,6 +2628,42 @@ getAvailableDatasetMetadataExportFormats _See [use case](../src/info/domain/useCases/GetAvailableDatasetMetadataExportFormats.ts) implementation_. +#### Get Dataset Publish Popup Custom Text + +Returns the custom text displayed in the dataset publish confirmation popup + +##### Example call: + +```typescript +import { getDatasetPublishPopupCustomText } from '@iqss/dataverse-client-javascript' + +/* ... */ + +getDatasetPublishPopupCustomText.execute().then((text: string) => { + /* ... */ +}) +``` + +_See [use case](../src/info/domain/useCases/GetDatasetPublishPopupCustomText.ts) implementation_. + +#### Get Publish Dataset Disclaimer Text + +Returns the disclaimer text displayed in the dataset publish flow. + +##### Example calls: + +```typescript +import { getPublishDatasetDisclaimerText } from '@iqss/dataverse-client-javascript' + +/* ... */ + +getPublishDatasetDisclaimerText.execute().then((disclaimerText: string) => { + /* ... */ +}) +``` + +_See [use case](../src/info/domain/useCases/GetPublishDatasetDisclaimerText.ts) implementation_. + ## Licenses ### Get Available Standard License Terms @@ -2291,7 +2698,7 @@ import { submitContactInfo } from '@iqss/dataverse-client-javascript' /* ... */ const contactDTO: ContactDTO = { - targedId: 1 + targetId: 1, subject: 'Data Question', body: 'Please help me understand your data. Thank you!', fromEmail: 'test@gmail.com' @@ -2499,3 +2906,277 @@ getFileExternalToolResolved ``` _See [use case](../src/externalTools/domain/useCases/GetfileExternalToolResolved.ts) implementation_. + +## Guestbooks + +### Guestbooks Read Use Cases + +#### Get a Guestbook + +Returns a [Guestbook](../src/guestbooks/domain/models/Guestbook.ts) by its id. + +##### Example call: + +```typescript +import { getGuestbook } from '@iqss/dataverse-client-javascript' + +const guestbookId = 123 + +getGuestbook.execute(guestbookId).then((guestbook: Guestbook) => { + /* ... */ +}) +``` + +_See [use case](../src/guestbooks/domain/useCases/GetGuestbook.ts) implementation_. + +#### Get Guestbooks By Collection Id + +Returns all [Guestbook](../src/guestbooks/domain/models/Guestbook.ts) entries available for a collection. + +##### Example call: + +```typescript +import { getGuestbooksByCollectionId } from '@iqss/dataverse-client-javascript' + +const collectionIdOrAlias = 'root' + +getGuestbooksByCollectionId.execute(collectionIdOrAlias).then((guestbooks: Guestbook[]) => { + /* ... */ +}) +``` + +_See [use case](../src/guestbooks/domain/useCases/GetGuestbooksByCollectionId.ts) implementation_. + +### Guestbooks Write Use Cases + +#### Create a Guestbook + +Creates a guestbook on a collection using [CreateGuestbookDTO](../src/guestbooks/domain/dtos/CreateGuestbookDTO.ts). + +##### Example call: + +```typescript +import { createGuestbook } from '@iqss/dataverse-client-javascript' + +const collectionIdOrAlias = 'root' +const guestbook: CreateGuestbookDTO = { + name: 'my test guestbook', + enabled: true, + emailRequired: true, + nameRequired: true, + institutionRequired: false, + positionRequired: false, + customQuestions: [ + { + question: 'Describe yourself', + required: false, + displayOrder: 1, + type: 'textarea', + hidden: false + } + ] +} + +createGuestbook.execute(guestbook, collectionIdOrAlias).then(() => { + /* ... */ +}) +``` + +_See [use case](../src/guestbooks/domain/useCases/CreateGuestbook.ts) implementation_. + +#### Set Guestbook Enabled + +Enables or disables a guestbook in a collection. + +##### Example call: + +```typescript +import { setGuestbookEnabled } from '@iqss/dataverse-client-javascript' + +const collectionIdOrAlias = 'root' +const guestbookId = 123 +const enabled = false + +setGuestbookEnabled.execute(collectionIdOrAlias, guestbookId, false).then(() => { + /* ... */ +}) +``` + +_See [use case](../src/guestbooks/domain/useCases/SetGuestbookEnabled.ts) implementation_. + +#### Assign Dataset Guestbook + +Assigns a guestbook to a dataset using `PUT /api/datasets/{identifier}/guestbook`. + +##### Example call: + +```typescript +import { assignDatasetGuestbook } from '@iqss/dataverse-client-javascript' + +const datasetIdOrPersistentId = 123 +const guestbookId = 456 + +assignDatasetGuestbook.execute(datasetIdOrPersistentId, guestbookId).then(() => { + /* ... */ +}) +``` + +_See [use case](../src/guestbooks/domain/useCases/AssignDatasetGuestbook.ts) implementation_. + +#### Remove Dataset Guestbook + +Removes the guestbook assignment for a dataset using `DELETE /api/datasets/{identifier}/guestbook`. + +##### Example call: + +```typescript +import { removeDatasetGuestbook } from '@iqss/dataverse-client-javascript' + +const datasetIdOrPersistentId = 123 + +removeDatasetGuestbook.execute(datasetIdOrPersistentId).then(() => { + /* ... */ +}) +``` + +_See [use case](../src/guestbooks/domain/useCases/RemoveDatasetGuestbook.ts) implementation_. + +## Access + +### Access Read Use Cases + +### Access Write Use Cases + +#### Submit Guestbook For Datafile Download + +Submits guestbook answers for a datafile and returns a signed URL. + +##### Example call: + +```typescript +import { submitGuestbookForDatafileDownload } from '@iqss/dataverse-client-javascript' + +submitGuestbookForDatafileDownload + .execute( + 10, + { + guestbookResponse: { + answers: [ + { id: 123, value: 'Good' }, + { id: 124, value: ['Multi', 'Line'] } + ] + } + }, + 'original' + ) + .then((signedUrl: string) => { + /* ... */ + }) +``` + +_See [use case](../src/access/domain/useCases/SubmitGuestbookForDatafileDownload.ts) implementation_. + +#### Submit Guestbook For Datafiles Download + +Submits guestbook answers for multiple files and returns a signed URL. + +##### Example call: + +```typescript +import { submitGuestbookForDatafilesDownload } from '@iqss/dataverse-client-javascript' + +submitGuestbookForDatafilesDownload + .execute( + [10, 11], + { + guestbookResponse: { + answers: [ + { id: 123, value: 'Good' }, + { id: 124, value: ['Multi', 'Line'] }, + { id: 125, value: 'Yellow' } + ] + } + }, + 'original' + ) + .then((signedUrl: string) => { + /* ... */ + }) +``` + +_See [use case](../src/access/domain/useCases/SubmitGuestbookForDatafilesDownload.ts) implementation_. + +#### Submit Guestbook For Dataset Download + +Submits guestbook answers for dataset download and returns a signed URL. + +##### Example call: + +```typescript +import { submitGuestbookForDatasetDownload } from '@iqss/dataverse-client-javascript' + +submitGuestbookForDatasetDownload + .execute( + 'doi:10.5072/FK2/XXXXXX', + { + guestbookResponse: { + answers: [ + { id: 123, value: 'Good' }, + { id: 124, value: ['Multi', 'Line'] }, + { id: 125, value: 'Yellow' } + ] + } + }, + 'original' + ) + .then((signedUrl: string) => { + /* ... */ + }) +``` + +_See [use case](../src/access/domain/useCases/SubmitGuestbookForDatasetDownload.ts) implementation_. + +#### Submit Guestbook For Dataset Version Download + +Submits guestbook answers for a specific dataset version and returns a signed URL. + +##### Example call: + +```typescript +import { submitGuestbookForDatasetVersionDownload } from '@iqss/dataverse-client-javascript' + +submitGuestbookForDatasetVersionDownload + .execute( + 10, + ':latest', + { + guestbookResponse: { + name: 'Jane Doe', + email: 'jane@example.org', + institution: 'Example University', + position: 'Researcher', + answers: [ + { id: 123, value: 'Good' }, + { id: 124, value: ['Multi', 'Line'] }, + { id: 125, value: 'Yellow' } + ] + } + }, + 'original' + ) + .then((signedUrl: string) => { + /* ... */ + }) +``` + +_See [use case](../src/access/domain/useCases/SubmitGuestbookForDatasetVersionDownload.ts) implementation_. + +The `datasetId` parameter can be a string, for persistent identifiers, or a number, for numeric identifiers. + +The `versionId` parameter accepts a numbered version such as `'1.0'` or a non-numbered version such as `':latest'`. + +The `guestbookResponse` parameter must match [GuestbookResponseDTO](../src/access/domain/dtos/GuestbookResponseDTO.ts). + +The optional `format` parameter is sent as a query parameter on the download endpoint. For example, pass `'original'` to request the original dataset or file format. + +The resolved value is a signed download URL as a string. diff --git a/package-lock.json b/package-lock.json index 40941f67..91410ed6 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,14 +1,15 @@ { "name": "@iqss/dataverse-client-javascript", - "version": "2.1.0", + "version": "2.2.0", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "@iqss/dataverse-client-javascript", - "version": "2.1.0", + "version": "2.2.0", "license": "MIT", "dependencies": { + "@iqss/dataverse-client-javascript": "^2.1.0", "@types/node": "^18.15.11", "@types/turndown": "^5.0.1", "axios": "^1.12.2", @@ -715,6 +716,19 @@ "integrity": "sha512-ZnQMnLV4e7hDlUvw8H+U8ASL02SS2Gn6+9Ac3wGGLIe7+je2AeAOxPY+izIPJDfFDb7eDjev0Us8MO1iFRN8hA==", "dev": true }, + "node_modules/@iqss/dataverse-client-javascript": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/@iqss/dataverse-client-javascript/-/dataverse-client-javascript-2.1.0.tgz", + "integrity": "sha512-5UVqAtRb7KZQi0c9W64f2G3vKFJrMLAIOuRwHR6hB0Z0ZVjEgCBjgP37N52T6sPh6bFbGv67Egy0+ZtmvgGMDg==", + "license": "MIT", + "dependencies": { + "@types/node": "^18.15.11", + "@types/turndown": "^5.0.1", + "axios": "^1.12.2", + "turndown": "^7.1.2", + "typescript": "^4.9.5" + } + }, "node_modules/@isaacs/cliui": { "version": "8.0.2", "resolved": "https://registry.npmjs.org/@isaacs/cliui/-/cliui-8.0.2.tgz", diff --git a/package.json b/package.json index 541fc681..06a0f896 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "@iqss/dataverse-client-javascript", - "version": "2.1.0", + "version": "2.2.0", "description": "Dataverse API wrapper package for JavaScript/TypeScript-based applications", "main": "./dist/index.js", "types": "./dist/index.d.ts", diff --git a/src/access/domain/dtos/GuestbookResponseDTO.ts b/src/access/domain/dtos/GuestbookResponseDTO.ts new file mode 100644 index 00000000..0d5eb4c0 --- /dev/null +++ b/src/access/domain/dtos/GuestbookResponseDTO.ts @@ -0,0 +1,14 @@ +export interface GuestbookAnswerDTO { + id: number | string + value: string | string[] +} + +export interface GuestbookResponseDTO { + guestbookResponse: { + name?: string + email?: string + institution?: string + position?: string + answers?: GuestbookAnswerDTO[] + } +} diff --git a/src/access/domain/repositories/IAccessRepository.ts b/src/access/domain/repositories/IAccessRepository.ts new file mode 100644 index 00000000..868734e7 --- /dev/null +++ b/src/access/domain/repositories/IAccessRepository.ts @@ -0,0 +1,28 @@ +import { GuestbookResponseDTO } from '../dtos/GuestbookResponseDTO' + +export interface IAccessRepository { + submitGuestbookForDatafileDownload( + fileId: number | string, + guestbookResponse: GuestbookResponseDTO, + format?: string + ): Promise + + submitGuestbookForDatafilesDownload( + fileIds: string | Array, + guestbookResponse: GuestbookResponseDTO, + format?: string + ): Promise + + submitGuestbookForDatasetDownload( + datasetId: number | string, + guestbookResponse: GuestbookResponseDTO, + format?: string + ): Promise + + submitGuestbookForDatasetVersionDownload( + datasetId: number | string, + versionId: string, + guestbookResponse: GuestbookResponseDTO, + format?: string + ): Promise +} diff --git a/src/access/domain/useCases/SubmitGuestbookForDatafileDownload.ts b/src/access/domain/useCases/SubmitGuestbookForDatafileDownload.ts new file mode 100644 index 00000000..c6234e1d --- /dev/null +++ b/src/access/domain/useCases/SubmitGuestbookForDatafileDownload.ts @@ -0,0 +1,27 @@ +import { UseCase } from '../../../core/domain/useCases/UseCase' +import { GuestbookResponseDTO } from '../dtos/GuestbookResponseDTO' +import { IAccessRepository } from '../repositories/IAccessRepository' + +export class SubmitGuestbookForDatafileDownload implements UseCase { + constructor(private readonly accessRepository: IAccessRepository) {} + + /** + * Submits a guestbook response for a single datafile download request and returns a signed URL. + * + * @param {number | string} fileId - Datafile identifier (numeric id or persistent id). + * @param {GuestbookResponseDTO} guestbookResponse - Guestbook response payload. + * @param {string} [format] - Optional download format passed as a query parameter. + * @returns {Promise} - Signed URL for the download. + */ + async execute( + fileId: number | string, + guestbookResponse: GuestbookResponseDTO, + format?: string + ): Promise { + return await this.accessRepository.submitGuestbookForDatafileDownload( + fileId, + guestbookResponse, + format + ) + } +} diff --git a/src/access/domain/useCases/SubmitGuestbookForDatafilesDownload.ts b/src/access/domain/useCases/SubmitGuestbookForDatafilesDownload.ts new file mode 100644 index 00000000..f8788e4c --- /dev/null +++ b/src/access/domain/useCases/SubmitGuestbookForDatafilesDownload.ts @@ -0,0 +1,27 @@ +import { UseCase } from '../../../core/domain/useCases/UseCase' +import { GuestbookResponseDTO } from '../dtos/GuestbookResponseDTO' +import { IAccessRepository } from '../repositories/IAccessRepository' + +export class SubmitGuestbookForDatafilesDownload implements UseCase { + constructor(private readonly accessRepository: IAccessRepository) {} + + /** + * Submits a guestbook response for multiple datafiles download request and returns a signed URL. + * + * @param {string | Array} fileIds - Comma-separated string or array of file ids. + * @param {GuestbookResponseDTO} guestbookResponse - Guestbook response payload. + * @param {string} [format] - Optional download format passed as a query parameter. + * @returns {Promise} - Signed URL for the download. + */ + async execute( + fileIds: string | Array, + guestbookResponse: GuestbookResponseDTO, + format?: string + ): Promise { + return await this.accessRepository.submitGuestbookForDatafilesDownload( + fileIds, + guestbookResponse, + format + ) + } +} diff --git a/src/access/domain/useCases/SubmitGuestbookForDatasetDownload.ts b/src/access/domain/useCases/SubmitGuestbookForDatasetDownload.ts new file mode 100644 index 00000000..b70c31c5 --- /dev/null +++ b/src/access/domain/useCases/SubmitGuestbookForDatasetDownload.ts @@ -0,0 +1,27 @@ +import { UseCase } from '../../../core/domain/useCases/UseCase' +import { GuestbookResponseDTO } from '../dtos/GuestbookResponseDTO' +import { IAccessRepository } from '../repositories/IAccessRepository' + +export class SubmitGuestbookForDatasetDownload implements UseCase { + constructor(private readonly accessRepository: IAccessRepository) {} + + /** + * Submits a guestbook response for dataset download request and returns a signed URL. + * + * @param {number | string} datasetId - Dataset identifier (numeric id or persistent id). + * @param {GuestbookResponseDTO} guestbookResponse - Guestbook response payload. + * @param {string} [format] - Optional download format passed as a query parameter. + * @returns {Promise} - Signed URL for the download. + */ + async execute( + datasetId: number | string, + guestbookResponse: GuestbookResponseDTO, + format?: string + ): Promise { + return await this.accessRepository.submitGuestbookForDatasetDownload( + datasetId, + guestbookResponse, + format + ) + } +} diff --git a/src/access/domain/useCases/SubmitGuestbookForDatasetVersionDownload.ts b/src/access/domain/useCases/SubmitGuestbookForDatasetVersionDownload.ts new file mode 100644 index 00000000..fee73048 --- /dev/null +++ b/src/access/domain/useCases/SubmitGuestbookForDatasetVersionDownload.ts @@ -0,0 +1,30 @@ +import { UseCase } from '../../../core/domain/useCases/UseCase' +import { GuestbookResponseDTO } from '../dtos/GuestbookResponseDTO' +import { IAccessRepository } from '../repositories/IAccessRepository' + +export class SubmitGuestbookForDatasetVersionDownload implements UseCase { + constructor(private readonly accessRepository: IAccessRepository) {} + + /** + * Submits a guestbook response for a specific dataset version download request and returns a signed URL. + * + * @param {number | string} datasetId - Dataset identifier (numeric id or persistent id). + * @param {string} versionId - Dataset version identifier (for example, ':latest' or '1.0'). + * @param {GuestbookResponseDTO} guestbookResponse - Guestbook response payload. + * @param {string} [format] - Optional download format passed as a query parameter. + * @returns {Promise} - Signed URL for the download. + */ + async execute( + datasetId: number | string, + versionId: string, + guestbookResponse: GuestbookResponseDTO, + format?: string + ): Promise { + return await this.accessRepository.submitGuestbookForDatasetVersionDownload( + datasetId, + versionId, + guestbookResponse, + format + ) + } +} diff --git a/src/access/index.ts b/src/access/index.ts new file mode 100644 index 00000000..ecea7218 --- /dev/null +++ b/src/access/index.ts @@ -0,0 +1,25 @@ +import { AccessRepository } from './infra/repositories/AccessRepository' +import { SubmitGuestbookForDatafileDownload } from './domain/useCases/SubmitGuestbookForDatafileDownload' +import { SubmitGuestbookForDatafilesDownload } from './domain/useCases/SubmitGuestbookForDatafilesDownload' +import { SubmitGuestbookForDatasetDownload } from './domain/useCases/SubmitGuestbookForDatasetDownload' +import { SubmitGuestbookForDatasetVersionDownload } from './domain/useCases/SubmitGuestbookForDatasetVersionDownload' + +const accessRepository = new AccessRepository() + +const submitGuestbookForDatafileDownload = new SubmitGuestbookForDatafileDownload(accessRepository) +const submitGuestbookForDatafilesDownload = new SubmitGuestbookForDatafilesDownload( + accessRepository +) +const submitGuestbookForDatasetDownload = new SubmitGuestbookForDatasetDownload(accessRepository) +const submitGuestbookForDatasetVersionDownload = new SubmitGuestbookForDatasetVersionDownload( + accessRepository +) + +export { + submitGuestbookForDatafileDownload, + submitGuestbookForDatafilesDownload, + submitGuestbookForDatasetDownload, + submitGuestbookForDatasetVersionDownload +} + +export { GuestbookResponseDTO } from './domain/dtos/GuestbookResponseDTO' diff --git a/src/access/infra/repositories/AccessRepository.ts b/src/access/infra/repositories/AccessRepository.ts new file mode 100644 index 00000000..a5f52a2a --- /dev/null +++ b/src/access/infra/repositories/AccessRepository.ts @@ -0,0 +1,179 @@ +import { ApiConfig, DataverseApiAuthMechanism } from '../../../core/infra/repositories/ApiConfig' +import { WriteError } from '../../../core/domain/repositories/WriteError' +import { ApiConstants } from '../../../core/infra/repositories/ApiConstants' +import { ApiRepository } from '../../../core/infra/repositories/ApiRepository' +import { + buildRequestConfig, + buildRequestUrl +} from '../../../core/infra/repositories/apiConfigBuilders' +import { GuestbookResponseDTO } from '../../domain/dtos/GuestbookResponseDTO' +import { IAccessRepository } from '../../domain/repositories/IAccessRepository' + +export class AccessRepository extends ApiRepository implements IAccessRepository { + private readonly accessResourceName = 'access' + + public async submitGuestbookForDatafileDownload( + fileId: number | string, + guestbookResponse: GuestbookResponseDTO, + format?: string + ): Promise { + const endpoint = this.buildApiEndpoint(`${this.accessResourceName}/datafile`, undefined, fileId) + const queryParams = format ? { signed: true, format } : { signed: true } + + return await this.submitGuestbookDownload(endpoint, guestbookResponse, queryParams) + } + + public async submitGuestbookForDatafilesDownload( + fileIds: Array, + guestbookResponse: GuestbookResponseDTO, + format?: string + ): Promise { + const queryParams = format ? { signed: true, format } : { signed: true } + + return await this.submitGuestbookDownload( + this.buildApiEndpoint( + this.accessResourceName, + `datafiles/${Array.isArray(fileIds) ? fileIds.join(',') : fileIds}` + ), + guestbookResponse, + queryParams + ) + } + + public async submitGuestbookForDatasetDownload( + datasetId: number | string, + guestbookResponse: GuestbookResponseDTO, + format?: string + ): Promise { + const endpoint = this.buildApiEndpoint( + `${this.accessResourceName}/dataset`, + undefined, + datasetId + ) + const queryParams = format ? { signed: true, format } : { signed: true } + + return await this.submitGuestbookDownload(endpoint, guestbookResponse, queryParams) + } + + public async submitGuestbookForDatasetVersionDownload( + datasetId: number | string, + versionId: string, + guestbookResponse: GuestbookResponseDTO, + format?: string + ): Promise { + const endpoint = this.buildApiEndpoint( + `${this.accessResourceName}/dataset`, + `versions/${versionId}`, + datasetId + ) + const queryParams = format ? { signed: true, format } : { signed: true } + + return await this.submitGuestbookDownload(endpoint, guestbookResponse, queryParams) + } + + private async submitGuestbookDownload( + apiEndpoint: string, + guestbookResponse: GuestbookResponseDTO, + queryParams: object + ): Promise { + const requestConfig = buildRequestConfig( + true, + queryParams, + ApiConstants.CONTENT_TYPE_APPLICATION_JSON + ) + const response = await fetch( + this.buildUrlWithQueryParams(buildRequestUrl(apiEndpoint), queryParams), + { + method: 'POST', + headers: this.buildFetchHeaders(requestConfig.headers), + credentials: this.getFetchCredentials(requestConfig.withCredentials), + body: JSON.stringify(guestbookResponse) + } + ).catch((error) => { + throw new WriteError(error instanceof Error ? error.message : String(error)) + }) + + const responseData = await this.parseResponseBody(response) + + if (!response.ok) { + throw new WriteError(this.buildFetchErrorMessage(response.status, responseData)) + } + + return this.getSignedUrlOrThrow(responseData) + } + + private getFetchCredentials(withCredentials?: boolean): RequestCredentials | undefined { + if (ApiConfig.dataverseApiAuthMechanism === DataverseApiAuthMechanism.BEARER_TOKEN) { + return 'omit' + } + + if (withCredentials) { + return 'include' + } + + return undefined + } + + private buildUrlWithQueryParams(requestUrl: string, queryParams: object): string { + const url = new URL(requestUrl) + + Object.entries(queryParams).forEach(([key, value]) => { + if (value !== undefined && value !== null) { + url.searchParams.append(key, String(value)) + } + }) + + return url.toString() + } + + private buildFetchHeaders(headers?: Record): Record { + const fetchHeaders: Record = {} + + if (!headers) { + return fetchHeaders + } + + Object.entries(headers).forEach(([key, value]) => { + if (value !== undefined) { + fetchHeaders[key] = String(value) + } + }) + + return fetchHeaders + } + + private async parseResponseBody(response: Response): Promise { + const contentType = response.headers.get('content-type') ?? '' + + if (contentType.includes('application/json')) { + return await response.json() + } + + const responseText = await response.text() + + try { + return JSON.parse(responseText) + } catch { + return responseText + } + } + + private buildFetchErrorMessage(status: number, responseData: any): string { + const message = + typeof responseData === 'string' + ? responseData + : responseData?.message || responseData?.data?.message || 'unknown error' + + return `[${status}] ${message}` + } + + private getSignedUrlOrThrow(responseData: any): string { + const signedUrl = responseData?.data?.signedUrl + + if (typeof signedUrl !== 'string' || signedUrl.length === 0) { + throw new WriteError('Missing signedUrl in access download response.') + } + + return signedUrl + } +} diff --git a/src/collections/domain/repositories/ICollectionsRepository.ts b/src/collections/domain/repositories/ICollectionsRepository.ts index 820a1356..bc8960c8 100644 --- a/src/collections/domain/repositories/ICollectionsRepository.ts +++ b/src/collections/domain/repositories/ICollectionsRepository.ts @@ -10,6 +10,8 @@ import { CollectionUserPermissions } from '../models/CollectionUserPermissions' import { PublicationStatus } from '../../../core/domain/models/PublicationStatus' import { CollectionItemType } from '../../../collections/domain/models/CollectionItemType' import { CollectionLinks } from '../models/CollectionLinks' +import { CollectionSummary } from '../models/CollectionSummary' +import { LinkingObjectType } from '../useCases/GetCollectionsForLinking' export interface ICollectionsRepository { getCollection(collectionIdOrAlias: number | string): Promise @@ -60,4 +62,10 @@ export interface ICollectionsRepository { linkingCollectionIdOrAlias: number | string ): Promise getCollectionLinks(collectionIdOrAlias: number | string): Promise + getCollectionsForLinking( + objectType: LinkingObjectType, + id: number | string, + searchTerm: string, + alreadyLinked: boolean + ): Promise } diff --git a/src/collections/domain/useCases/GetCollectionsForLinking.ts b/src/collections/domain/useCases/GetCollectionsForLinking.ts new file mode 100644 index 00000000..e01e156e --- /dev/null +++ b/src/collections/domain/useCases/GetCollectionsForLinking.ts @@ -0,0 +1,34 @@ +import { UseCase } from '../../../core/domain/useCases/UseCase' +import { ICollectionsRepository } from '../repositories/ICollectionsRepository' +import { CollectionSummary } from '../models/CollectionSummary' + +export type LinkingObjectType = 'collection' | 'dataset' + +export class GetCollectionsForLinking implements UseCase { + private collectionsRepository: ICollectionsRepository + + constructor(collectionsRepository: ICollectionsRepository) { + this.collectionsRepository = collectionsRepository + } + + /** + * Returns an array of CollectionSummary (id, alias, displayName) to which the given Dataverse collection or Dataset may be linked. + * @param objectType - 'collection' when providing a collection identifier/alias; 'dataset' when providing a dataset persistentId. + * @param id - For objectType 'collection', a numeric id or alias string. For 'dataset', the persistentId string (e.g., doi:...) + * @param searchTerm - Optional search term to filter by collection name. Defaults to empty string (no filtering). + * @param alreadyLinked - Optional flag. When true, returns collections currently linked (candidates to unlink). Defaults to false. + */ + async execute( + objectType: LinkingObjectType, + id: number | string, + searchTerm = '', + alreadyLinked = false + ): Promise { + return await this.collectionsRepository.getCollectionsForLinking( + objectType, + id, + searchTerm, + alreadyLinked + ) + } +} diff --git a/src/collections/index.ts b/src/collections/index.ts index 05e49954..59e2e50b 100644 --- a/src/collections/index.ts +++ b/src/collections/index.ts @@ -15,6 +15,7 @@ import { DeleteCollectionFeaturedItem } from './domain/useCases/DeleteCollection import { LinkCollection } from './domain/useCases/LinkCollection' import { UnlinkCollection } from './domain/useCases/UnlinkCollection' import { GetCollectionLinks } from './domain/useCases/GetCollectionLinks' +import { GetCollectionsForLinking } from './domain/useCases/GetCollectionsForLinking' const collectionsRepository = new CollectionsRepository() @@ -34,6 +35,7 @@ const deleteCollectionFeaturedItem = new DeleteCollectionFeaturedItem(collection const linkCollection = new LinkCollection(collectionsRepository) const unlinkCollection = new UnlinkCollection(collectionsRepository) const getCollectionLinks = new GetCollectionLinks(collectionsRepository) +const getCollectionsForLinking = new GetCollectionsForLinking(collectionsRepository) export { getCollection, @@ -51,7 +53,8 @@ export { deleteCollectionFeaturedItem, linkCollection, unlinkCollection, - getCollectionLinks + getCollectionLinks, + getCollectionsForLinking } export { Collection, CollectionInputLevel } from './domain/models/Collection' export { CollectionFacet } from './domain/models/CollectionFacet' @@ -62,3 +65,4 @@ export { CollectionItemType } from './domain/models/CollectionItemType' export { CollectionSearchCriteria } from './domain/models/CollectionSearchCriteria' export { FeaturedItem } from './domain/models/FeaturedItem' export { FeaturedItemsDTO } from './domain/dtos/FeaturedItemsDTO' +export { CollectionSummary } from './domain/models/CollectionSummary' diff --git a/src/collections/infra/repositories/CollectionsRepository.ts b/src/collections/infra/repositories/CollectionsRepository.ts index 704367e2..e0e459b0 100644 --- a/src/collections/infra/repositories/CollectionsRepository.ts +++ b/src/collections/infra/repositories/CollectionsRepository.ts @@ -38,6 +38,8 @@ import { ApiConstants } from '../../../core/infra/repositories/ApiConstants' import { PublicationStatus } from '../../../core/domain/models/PublicationStatus' import { ReadError } from '../../../core/domain/repositories/ReadError' import { CollectionLinks } from '../../domain/models/CollectionLinks' +import { CollectionSummary } from '../../domain/models/CollectionSummary' +import { LinkingObjectType } from '../../domain/useCases/GetCollectionsForLinking' export interface NewCollectionRequestPayload { alias: string @@ -93,7 +95,6 @@ export enum GetMyDataCollectionItemsQueryParams { export class CollectionsRepository extends ApiRepository implements ICollectionsRepository { private readonly collectionsResourceName: string = 'dataverses' - public async getCollection( collectionIdOrAlias: number | string = ROOT_COLLECTION_ID ): Promise { @@ -485,4 +486,46 @@ export class CollectionsRepository extends ApiRepository implements ICollections throw error }) } + + public async getCollectionsForLinking( + objectType: LinkingObjectType, + id: number | string, + searchTerm: string, + alreadyLinked: boolean + ): Promise { + let path: string + const queryParams = new URLSearchParams() + if (objectType === 'collection') { + path = `/${this.collectionsResourceName}/${id}/dataverse/linkingDataverses` + } else { + path = `/${this.collectionsResourceName}/:persistentId/dataset/linkingDataverses` + queryParams.set('persistentId', String(id)) + } + + if (searchTerm) { + queryParams.set('searchTerm', searchTerm) + } + + if (alreadyLinked) { + queryParams.set('alreadyLinking', 'true') + } + + return this.doGet(path, true, queryParams) + .then((response) => { + const payload = response.data.data as { + id: number + alias: string + name: string + }[] + + return payload.map((item) => ({ + id: item.id, + alias: item.alias, + displayName: item.name + })) + }) + .catch((error) => { + throw error + }) + } } diff --git a/src/collections/infra/repositories/transformers/collectionTransformers.ts b/src/collections/infra/repositories/transformers/collectionTransformers.ts index a26c4718..fa23b8ed 100644 --- a/src/collections/infra/repositories/transformers/collectionTransformers.ts +++ b/src/collections/infra/repositories/transformers/collectionTransformers.ts @@ -159,7 +159,13 @@ export const transformCollectionLinksResponseToCollectionLinks = ( const responseDataPayload = response.data.data const linkedCollections = responseDataPayload.linkedDataverses const collectionsLinkingToThis = responseDataPayload.dataversesLinkingToThis - const linkedDatasets = responseDataPayload.linkedDatasets + const linkedDatasets = responseDataPayload.linkedDatasets.map( + (ld: { identifier: string; title: string }) => ({ + persistentId: ld.identifier, + title: ld.title + }) + ) + return { linkedCollections, collectionsLinkingToThis, diff --git a/src/datasets/domain/dtos/DatasetLicenseUpdateRequest.ts b/src/datasets/domain/dtos/DatasetLicenseUpdateRequest.ts new file mode 100644 index 00000000..db20307e --- /dev/null +++ b/src/datasets/domain/dtos/DatasetLicenseUpdateRequest.ts @@ -0,0 +1,6 @@ +import { CustomTerms } from '../models/Dataset' + +export interface DatasetLicenseUpdateRequest { + name?: string + customTerms?: CustomTerms +} diff --git a/src/datasets/domain/dtos/DatasetTypeDTO.ts b/src/datasets/domain/dtos/DatasetTypeDTO.ts new file mode 100644 index 00000000..3f6b1c1b --- /dev/null +++ b/src/datasets/domain/dtos/DatasetTypeDTO.ts @@ -0,0 +1,3 @@ +import { DatasetType } from '../models/DatasetType' + +export type DatasetTypeDTO = Omit diff --git a/src/datasets/domain/models/Dataset.ts b/src/datasets/domain/models/Dataset.ts index e858de9e..ebd302f1 100644 --- a/src/datasets/domain/models/Dataset.ts +++ b/src/datasets/domain/models/Dataset.ts @@ -12,6 +12,7 @@ export interface Dataset { alternativePersistentId?: string publicationDate?: string citationDate?: string + guestbookId?: number metadataBlocks: DatasetMetadataBlocks isPartOf: DvObjectOwnerNode datasetType?: string @@ -22,7 +23,12 @@ export interface DatasetVersionInfo { minorNumber: number state: DatasetVersionState createTime: Date - lastUpdateTime: Date + /** + * The timestamp of the last update to this dataset version. + * Format: ISO 8601 string (e.g., "2023-06-01T12:34:56Z"). + * Used for optimistic concurrency control to detect concurrent updates. + */ + lastUpdateTime: string releaseTime?: Date deaccessionNote?: string } @@ -46,6 +52,7 @@ export interface CustomTerms { conditions?: string disclaimer?: string } + export interface TermsOfAccess { fileAccessRequest: boolean termsOfAccessForRestrictedFiles?: string diff --git a/src/datasets/domain/models/DatasetType.ts b/src/datasets/domain/models/DatasetType.ts index 56a5ed43..70bd9d41 100644 --- a/src/datasets/domain/models/DatasetType.ts +++ b/src/datasets/domain/models/DatasetType.ts @@ -1,6 +1,8 @@ export interface DatasetType { - id?: number + id: number name: string + displayName: string linkedMetadataBlocks?: string[] availableLicenses?: string[] + description?: string } diff --git a/src/datasets/domain/models/DatasetUploadLimits.ts b/src/datasets/domain/models/DatasetUploadLimits.ts new file mode 100644 index 00000000..6dbf2220 --- /dev/null +++ b/src/datasets/domain/models/DatasetUploadLimits.ts @@ -0,0 +1,4 @@ +export interface DatasetUploadLimits { + numberOfFilesRemaining?: number + storageQuotaRemaining?: number +} diff --git a/src/datasets/domain/models/DatasetVersionSummaryInfo.ts b/src/datasets/domain/models/DatasetVersionSummaryInfo.ts index 782a2e0f..c012a4b6 100644 --- a/src/datasets/domain/models/DatasetVersionSummaryInfo.ts +++ b/src/datasets/domain/models/DatasetVersionSummaryInfo.ts @@ -6,6 +6,11 @@ export interface DatasetVersionSummaryInfo { publishedOn?: string } +export interface DatasetVersionSummarySubset { + summaries: DatasetVersionSummaryInfo[] + totalCount: number +} + export type DatasetVersionSummary = { [key: string]: | SummaryUpdates diff --git a/src/datasets/domain/models/StorageDriver.ts b/src/datasets/domain/models/StorageDriver.ts new file mode 100644 index 00000000..9c04b400 --- /dev/null +++ b/src/datasets/domain/models/StorageDriver.ts @@ -0,0 +1,8 @@ +export interface StorageDriver { + name: string + type: string + label: string + directUpload: boolean + directDownload: boolean + uploadOutOfBand: boolean +} diff --git a/src/datasets/domain/repositories/IDatasetsRepository.ts b/src/datasets/domain/repositories/IDatasetsRepository.ts index e78816c4..02c0d2c3 100644 --- a/src/datasets/domain/repositories/IDatasetsRepository.ts +++ b/src/datasets/domain/repositories/IDatasetsRepository.ts @@ -8,12 +8,16 @@ import { DatasetDeaccessionDTO } from '../dtos/DatasetDeaccessionDTO' import { MetadataBlock } from '../../../metadataBlocks' import { DatasetVersionDiff } from '../models/DatasetVersionDiff' import { DatasetDownloadCount } from '../models/DatasetDownloadCount' -import { DatasetVersionSummaryInfo } from '../models/DatasetVersionSummaryInfo' +import { DatasetVersionSummarySubset } from '../models/DatasetVersionSummaryInfo' import { DatasetLinkedCollection } from '../models/DatasetLinkedCollection' import { CitationFormat } from '../models/CitationFormat' import { FormattedCitation } from '../models/FormattedCitation' -import { DatasetTemplate } from '../models/DatasetTemplate' import { DatasetType } from '../models/DatasetType' +import { TermsOfAccess } from '../models/Dataset' +import { DatasetLicenseUpdateRequest } from '../dtos/DatasetLicenseUpdateRequest' +import { DatasetTypeDTO } from '../dtos/DatasetTypeDTO' +import { StorageDriver } from '../models/StorageDriver' +import { DatasetUploadLimits } from '../models/DatasetUploadLimits' export interface IDatasetsRepository { getDataset( @@ -54,7 +58,7 @@ export interface IDatasetsRepository { datasetId: number | string, dataset: DatasetDTO, datasetMetadataBlocks: MetadataBlock[], - internalVersionNumber?: number + sourceLastUpdateTime?: string ): Promise deaccessionDataset( datasetId: number | string, @@ -65,10 +69,14 @@ export interface IDatasetsRepository { datasetId: number | string, includeMDC?: boolean ): Promise - getDatasetVersionsSummaries(datasetId: number | string): Promise + getDatasetVersionsSummaries( + datasetId: number | string, + limit?: number, + offset?: number + ): Promise deleteDatasetDraft(datasetId: number | string): Promise - linkDataset(datasetId: number, collectionAlias: string): Promise - unlinkDataset(datasetId: number, collectionAlias: string): Promise + linkDataset(datasetId: number | string, collectionIdOrAlias: number | string): Promise + unlinkDataset(datasetId: number | string, collectionIdOrAlias: number | string): Promise getDatasetLinkedCollections(datasetId: number | string): Promise getDatasetAvailableCategories(datasetId: number | string): Promise getDatasetCitationInOtherFormats( @@ -77,10 +85,9 @@ export interface IDatasetsRepository { format: CitationFormat, includeDeaccessioned?: boolean ): Promise - getDatasetTemplates(collectionIdOrAlias: number | string): Promise getDatasetAvailableDatasetTypes(): Promise getDatasetAvailableDatasetType(datasetTypeId: number | string): Promise - addDatasetType(datasetType: DatasetType): Promise + addDatasetType(datasetType: DatasetTypeDTO): Promise linkDatasetTypeWithMetadataBlocks( datasetTypeId: number | string, metadataBlocks: string[] @@ -90,4 +97,11 @@ export interface IDatasetsRepository { licenses: string[] ): Promise deleteDatasetType(datasetTypeId: number): Promise + updateTermsOfAccess(datasetId: number | string, termsOfAccess: TermsOfAccess): Promise + updateDatasetLicense( + datasetId: number | string, + payload: DatasetLicenseUpdateRequest + ): Promise + getDatasetStorageDriver(datasetId: number | string): Promise + getDatasetUploadLimits(datasetId: number | string): Promise } diff --git a/src/datasets/domain/useCases/AddDatasetType.ts b/src/datasets/domain/useCases/AddDatasetType.ts index 7e2e11c9..84a47bc1 100644 --- a/src/datasets/domain/useCases/AddDatasetType.ts +++ b/src/datasets/domain/useCases/AddDatasetType.ts @@ -1,4 +1,5 @@ import { UseCase } from '../../../core/domain/useCases/UseCase' +import { DatasetTypeDTO } from '../dtos/DatasetTypeDTO' import { DatasetType } from '../models/DatasetType' import { IDatasetsRepository } from '../repositories/IDatasetsRepository' @@ -12,7 +13,7 @@ export class AddDatasetType implements UseCase { /** * Add a dataset type that can be selected when creating a dataset. */ - async execute(datasetType: DatasetType): Promise { + async execute(datasetType: DatasetTypeDTO): Promise { return await this.datasetsRepository.addDatasetType(datasetType) } } diff --git a/src/datasets/domain/useCases/GetDatasetStorageDriver.ts b/src/datasets/domain/useCases/GetDatasetStorageDriver.ts new file mode 100644 index 00000000..361d1db9 --- /dev/null +++ b/src/datasets/domain/useCases/GetDatasetStorageDriver.ts @@ -0,0 +1,21 @@ +import { UseCase } from '../../../core/domain/useCases/UseCase' +import { IDatasetsRepository } from '../repositories/IDatasetsRepository' +import { StorageDriver } from '../models/StorageDriver' + +export class GetDatasetStorageDriver implements UseCase { + private datasetsRepository: IDatasetsRepository + + constructor(datasetsRepository: IDatasetsRepository) { + this.datasetsRepository = datasetsRepository + } + + /** + * Returns the storage driver information for a given Dataset. + * + * @param {number | string} [datasetId] - The dataset identifier, which can be a string (for persistent identifiers), or a number (for numeric identifiers). + * @returns {Promise} + */ + async execute(datasetId: number | string): Promise { + return this.datasetsRepository.getDatasetStorageDriver(datasetId) + } +} diff --git a/src/datasets/domain/useCases/GetDatasetTemplates.ts b/src/datasets/domain/useCases/GetDatasetTemplates.ts deleted file mode 100644 index 6878e625..00000000 --- a/src/datasets/domain/useCases/GetDatasetTemplates.ts +++ /dev/null @@ -1,25 +0,0 @@ -import { ROOT_COLLECTION_ID } from '../../../collections/domain/models/Collection' -import { UseCase } from '../../../core/domain/useCases/UseCase' -import { DatasetTemplate } from '../models/DatasetTemplate' -import { IDatasetsRepository } from '../repositories/IDatasetsRepository' - -export class GetDatasetTemplates implements UseCase { - private datasetsRepository: IDatasetsRepository - - constructor(datasetsRepository: IDatasetsRepository) { - this.datasetsRepository = datasetsRepository - } - - /** - * Returns a DatasetTemplate array containing the dataset templates of the requested collection, given the collection identifier or alias. - * - * @param {number | string} [collectionIdOrAlias = ':root'] - A generic collection identifier, which can be either a string (for queries by CollectionAlias), or a number (for queries by CollectionId) - * If this parameter is not set, the default value is: ':root' - * @returns {Promise} - */ - async execute( - collectionIdOrAlias: number | string = ROOT_COLLECTION_ID - ): Promise { - return await this.datasetsRepository.getDatasetTemplates(collectionIdOrAlias) - } -} diff --git a/src/datasets/domain/useCases/GetDatasetUploadLimits.ts b/src/datasets/domain/useCases/GetDatasetUploadLimits.ts new file mode 100644 index 00000000..4a5c018e --- /dev/null +++ b/src/datasets/domain/useCases/GetDatasetUploadLimits.ts @@ -0,0 +1,21 @@ +import { UseCase } from '../../../core/domain/useCases/UseCase' +import { IDatasetsRepository } from '../repositories/IDatasetsRepository' +import { DatasetUploadLimits } from '../models/DatasetUploadLimits' + +export class GetDatasetUploadLimits implements UseCase { + private datasetsRepository: IDatasetsRepository + + constructor(datasetsRepository: IDatasetsRepository) { + this.datasetsRepository = datasetsRepository + } + + /** + * Returns the remaining dataset storage and/or file upload quotas (if present). + * + * @param {number | string} datasetId - The dataset identifier, which can be a string (for persistent identifiers), or a number (for numeric identifiers). + * @returns {Promise} + */ + async execute(datasetId: number | string): Promise { + return this.datasetsRepository.getDatasetUploadLimits(datasetId) + } +} diff --git a/src/datasets/domain/useCases/GetDatasetVersionsSummaries.ts b/src/datasets/domain/useCases/GetDatasetVersionsSummaries.ts index 24458b00..d5ea8834 100644 --- a/src/datasets/domain/useCases/GetDatasetVersionsSummaries.ts +++ b/src/datasets/domain/useCases/GetDatasetVersionsSummaries.ts @@ -1,8 +1,8 @@ import { UseCase } from '../../../core/domain/useCases/UseCase' -import { DatasetVersionSummaryInfo } from '../models/DatasetVersionSummaryInfo' +import { DatasetVersionSummarySubset } from '../models/DatasetVersionSummaryInfo' import { IDatasetsRepository } from '../repositories/IDatasetsRepository' -export class GetDatasetVersionsSummaries implements UseCase { +export class GetDatasetVersionsSummaries implements UseCase { private datasetsRepository: IDatasetsRepository constructor(datasetsRepository: IDatasetsRepository) { @@ -14,9 +14,15 @@ export class GetDatasetVersionsSummaries implements UseCase} - An array of DatasetVersionSummaryInfo. + * @param {number} [limit] - Limit for pagination (optional). + * @param {number} [offset] - Offset for pagination (optional). + * @returns {Promise} - A DatasetVersionSummarySubset containing the summaries and total count. */ - async execute(datasetId: number | string): Promise { - return await this.datasetsRepository.getDatasetVersionsSummaries(datasetId) + async execute( + datasetId: number | string, + limit?: number, + offset?: number + ): Promise { + return await this.datasetsRepository.getDatasetVersionsSummaries(datasetId, limit, offset) } } diff --git a/src/datasets/domain/useCases/LinkDataset.ts b/src/datasets/domain/useCases/LinkDataset.ts index be7f732f..4e953b17 100644 --- a/src/datasets/domain/useCases/LinkDataset.ts +++ b/src/datasets/domain/useCases/LinkDataset.ts @@ -11,11 +11,11 @@ export class LinkDataset implements UseCase { /** * Creates a link between a Dataset and a Collection. * - * @param {number} [datasetId] - The dataset id. - * @param {string} [collectionAlias] - The collection alias. + * @param {number | string} [datasetId] - The dataset id (numeric) or persistent identifier string. + * @param {number | string} [collectionIdOrAlias] - The collection identifier (numeric id) or alias. * @returns {Promise} - This method does not return anything upon successful completion. */ - async execute(datasetId: number, collectionAlias: string): Promise { - return await this.datasetsRepository.linkDataset(datasetId, collectionAlias) + async execute(datasetId: number | string, collectionIdOrAlias: number | string): Promise { + return await this.datasetsRepository.linkDataset(datasetId, collectionIdOrAlias) } } diff --git a/src/datasets/domain/useCases/SetAvailableLicensesForDatasetType.ts b/src/datasets/domain/useCases/SetAvailableLicensesForDatasetType.ts index e4df9ab2..9741aa08 100644 --- a/src/datasets/domain/useCases/SetAvailableLicensesForDatasetType.ts +++ b/src/datasets/domain/useCases/SetAvailableLicensesForDatasetType.ts @@ -10,6 +10,10 @@ export class SetAvailableLicensesForDatasetType implements UseCase { /** * Sets the available licenses for a given dataset type. This limits the license options when creating a dataset of this type. + * + * @param {number | string} [datasetTypeId] - The dataset type identifier, which can be a string (for persistent identifiers), or a number (for numeric identifiers). + * @param {string[]} licenses - The licenses to set for the dataset type. + * @returns {Promise} - This method does not return anything upon successful completion. */ async execute(datasetTypeId: number | string, licenses: string[]): Promise { return await this.datasetsRepository.setAvailableLicensesForDatasetType(datasetTypeId, licenses) diff --git a/src/datasets/domain/useCases/UnlinkDataset.ts b/src/datasets/domain/useCases/UnlinkDataset.ts index d2d8eff5..8b2142fb 100644 --- a/src/datasets/domain/useCases/UnlinkDataset.ts +++ b/src/datasets/domain/useCases/UnlinkDataset.ts @@ -11,11 +11,11 @@ export class UnlinkDataset implements UseCase { /** * Removes a link between a Dataset and a Collection. * - * @param {number} [datasetId] - The dataset id. - * @param {string} [collectionAlias] - The collection alias. + * @param {number | string} [datasetId] - The dataset id (numeric) or persistent identifier string. + * @param {number | string} [collectionIdOrAlias] - The collection identifier (numeric id) or alias. * @returns {Promise} - This method does not return anything upon successful completion. */ - async execute(datasetId: number, collectionAlias: string): Promise { - return await this.datasetsRepository.unlinkDataset(datasetId, collectionAlias) + async execute(datasetId: number | string, collectionIdOrAlias: number | string): Promise { + return await this.datasetsRepository.unlinkDataset(datasetId, collectionIdOrAlias) } } diff --git a/src/datasets/domain/useCases/UpdateDataset.ts b/src/datasets/domain/useCases/UpdateDataset.ts index ed90f4d1..26ca23b9 100644 --- a/src/datasets/domain/useCases/UpdateDataset.ts +++ b/src/datasets/domain/useCases/UpdateDataset.ts @@ -18,7 +18,7 @@ export class UpdateDataset extends DatasetWriteUseCase { * * @param {number | string} [datasetId] - The dataset identifier, which can be a string (for persistent identifiers), or a number (for numeric identifiers). * @param {DatasetDTO} [updatedDataset] - DatasetDTO object including the updated dataset metadata field values for each metadata block. - * @param {number} [internalVersionNumber] - The internal version number of the dataset. If another user updates the dataset version metadata before you send the update request, data inconsistencies may occur. To prevent this, you can use the optional internalVersionNumber parameter. This parameter must include the internal version number corresponding to the dataset version being updated. Note that internal version numbers increase sequentially with each version update. + * @param {string} [sourceLastUpdateTime] - The lastUpdateTime value from the dataset. If another user updates the dataset version metadata before you send the update request, data inconsistencies may occur. To prevent this, you can use the optional sourceLastUpdateTime parameter. This parameter must include the lastUpdateTime value corresponding to the dataset version being updated. * @returns {Promise} - This method does not return anything upon successful completion. * @throws {ResourceValidationError} - If there are validation errors related to the provided information. * @throws {ReadError} - If there are errors while reading data. @@ -27,7 +27,7 @@ export class UpdateDataset extends DatasetWriteUseCase { async execute( datasetId: number | string, updatedDataset: DatasetDTO, - internalVersionNumber?: number + sourceLastUpdateTime?: string ): Promise { const metadataBlocks = await this.getNewDatasetMetadataBlocks(updatedDataset) this.getNewDatasetValidator().validate(updatedDataset, metadataBlocks) @@ -35,7 +35,7 @@ export class UpdateDataset extends DatasetWriteUseCase { datasetId, updatedDataset, metadataBlocks, - internalVersionNumber + sourceLastUpdateTime ) } } diff --git a/src/datasets/domain/useCases/UpdateDatasetLicense.ts b/src/datasets/domain/useCases/UpdateDatasetLicense.ts new file mode 100644 index 00000000..3886ed2b --- /dev/null +++ b/src/datasets/domain/useCases/UpdateDatasetLicense.ts @@ -0,0 +1,23 @@ +import { UseCase } from '../../../core/domain/useCases/UseCase' +import { IDatasetsRepository } from '../repositories/IDatasetsRepository' +import { DatasetLicenseUpdateRequest } from '../dtos/DatasetLicenseUpdateRequest' + +export class UpdateDatasetLicense implements UseCase { + private readonly datasetsRepository: IDatasetsRepository + + constructor(datasetsRepository: IDatasetsRepository) { + this.datasetsRepository = datasetsRepository + } + + /** + * Updates the license of a dataset by applying it to the draft version. If no draft exists, a new one is created by the API. + * Supports either predefined license by name or custom terms of use and access. + * + * @param {number | string} datasetId - The dataset identifier, which can be a string (for persistent identifiers), or a number (for numeric identifiers). + * @param {DatasetLicenseUpdateRequest} payload - The payload containing the license name or custom terms of use and access. + * @returns {Promise} - This method does not return anything upon successful completion. + */ + async execute(datasetId: number | string, payload: DatasetLicenseUpdateRequest): Promise { + return this.datasetsRepository.updateDatasetLicense(datasetId, payload) + } +} diff --git a/src/datasets/domain/useCases/UpdateTermsOfAccess.ts b/src/datasets/domain/useCases/UpdateTermsOfAccess.ts new file mode 100644 index 00000000..103d4f00 --- /dev/null +++ b/src/datasets/domain/useCases/UpdateTermsOfAccess.ts @@ -0,0 +1,22 @@ +import { UseCase } from '../../../core/domain/useCases/UseCase' +import { IDatasetsRepository } from '../repositories/IDatasetsRepository' +import { TermsOfAccess } from '../models/Dataset' + +export class UpdateTermsOfAccess implements UseCase { + private datasetsRepository: IDatasetsRepository + + constructor(datasetsRepository: IDatasetsRepository) { + this.datasetsRepository = datasetsRepository + } + + /** + * Sets the terms of access for a given dataset. + * + * @param {number | string} [datasetId] - The dataset identifier, which can be a string (for persistent identifiers), or a number (for numeric identifiers). + * @param {TermsOfAccess} termsOfAccess - The terms of access to set for the dataset. + * @returns {Promise} - This method does not return anything upon successful completion. + */ + async execute(datasetId: number | string, termsOfAccess: TermsOfAccess): Promise { + return await this.datasetsRepository.updateTermsOfAccess(datasetId, termsOfAccess) + } +} diff --git a/src/datasets/domain/useCases/validators/SingleMetadataFieldValidator.ts b/src/datasets/domain/useCases/validators/SingleMetadataFieldValidator.ts index f39f0ed4..f233e450 100644 --- a/src/datasets/domain/useCases/validators/SingleMetadataFieldValidator.ts +++ b/src/datasets/domain/useCases/validators/SingleMetadataFieldValidator.ts @@ -3,14 +3,12 @@ import { DatasetMetadataFieldAndValueInfo } from './BaseMetadataFieldValidator' import { ControlledVocabularyFieldError } from './errors/ControlledVocabularyFieldError' -import { DateFormatFieldError } from './errors/DateFormatFieldError' import { MetadataFieldValidator } from './MetadataFieldValidator' import { DatasetMetadataChildFieldValueDTO } from '../../dtos/DatasetDTO' import { MultipleMetadataFieldValidator } from './MultipleMetadataFieldValidator' import { MetadataFieldInfo, - MetadataFieldType, - MetadataFieldWatermark + MetadataFieldType } from '../../../../metadataBlocks/domain/models/MetadataBlock' export class SingleMetadataFieldValidator extends BaseMetadataFieldValidator { @@ -50,10 +48,6 @@ export class SingleMetadataFieldValidator extends BaseMetadataFieldValidator { this.validateControlledVocabularyFieldValue(datasetMetadataFieldAndValueInfo) } - if (metadataFieldInfo.type == MetadataFieldType.Date) { - this.validateDateFieldValue(datasetMetadataFieldAndValueInfo) - } - if (metadataFieldInfo.childMetadataFields != undefined) { this.validateChildMetadataFieldValues(datasetMetadataFieldAndValueInfo) } @@ -76,47 +70,6 @@ export class SingleMetadataFieldValidator extends BaseMetadataFieldValidator { } } - private validateDateFieldValue( - datasetMetadataFieldAndValueInfo: DatasetMetadataFieldAndValueInfo - ) { - const { - metadataFieldInfo: { watermark }, - metadataFieldValue - } = datasetMetadataFieldAndValueInfo - - const acceptsAllDateFormats = watermark === MetadataFieldWatermark.YYYYOrYYYYMMOrYYYYMMDD - - const YYYY_MM_DD_DATE_FORMAT_REGEX = /^\d{4}-(0[1-9]|1[0-2])-(0[1-9]|[12][0-9]|3[01])$/ - - const YYYY_MM_FORMAT_REGEX = /^\d{4}-(0[1-9]|1[0-2])$/ - - const YYYY_FORMAT_REGEX = /^\d{4}$/ - - const isValidDateFormat = (value: string): boolean => { - if (acceptsAllDateFormats) { - // Check if it matches any of the formats - return ( - YYYY_MM_DD_DATE_FORMAT_REGEX.test(value) || - YYYY_MM_FORMAT_REGEX.test(value) || - YYYY_FORMAT_REGEX.test(value) - ) - } else { - // Only accepts YYYY-MM-DD format - return YYYY_MM_DD_DATE_FORMAT_REGEX.test(value) - } - } - - if (!isValidDateFormat(metadataFieldValue as string)) { - throw new DateFormatFieldError( - datasetMetadataFieldAndValueInfo.metadataFieldKey, - datasetMetadataFieldAndValueInfo.metadataBlockName, - watermark, - datasetMetadataFieldAndValueInfo.metadataParentFieldKey, - datasetMetadataFieldAndValueInfo.metadataFieldPosition - ) - } - } - private validateChildMetadataFieldValues( datasetMetadataFieldAndValueInfo: DatasetMetadataFieldAndValueInfo ) { diff --git a/src/datasets/domain/useCases/validators/errors/DateFormatFieldError.ts b/src/datasets/domain/useCases/validators/errors/DateFormatFieldError.ts deleted file mode 100644 index 6a174837..00000000 --- a/src/datasets/domain/useCases/validators/errors/DateFormatFieldError.ts +++ /dev/null @@ -1,19 +0,0 @@ -import { FieldValidationError } from './FieldValidationError' - -export class DateFormatFieldError extends FieldValidationError { - constructor( - metadataFieldName: string, - citationBlockName: string, - validDateFormat: string, - parentMetadataFieldName?: string, - fieldPosition?: number - ) { - super( - metadataFieldName, - citationBlockName, - parentMetadataFieldName, - fieldPosition, - `The field requires a valid date format (${validDateFormat}).` - ) - } -} diff --git a/src/datasets/index.ts b/src/datasets/index.ts index 6b93a7cd..a129467f 100644 --- a/src/datasets/index.ts +++ b/src/datasets/index.ts @@ -31,7 +31,10 @@ import { LinkDatasetTypeWithMetadataBlocks } from './domain/useCases/LinkDataset import { SetAvailableLicensesForDatasetType } from './domain/useCases/SetAvailableLicensesForDatasetType' import { DeleteDatasetType } from './domain/useCases/DeleteDatasetType' import { GetDatasetCitationInOtherFormats } from './domain/useCases/GetDatasetCitationInOtherFormats' -import { GetDatasetTemplates } from './domain/useCases/GetDatasetTemplates' +import { UpdateTermsOfAccess } from './domain/useCases/UpdateTermsOfAccess' +import { UpdateDatasetLicense } from './domain/useCases/UpdateDatasetLicense' +import { GetDatasetStorageDriver } from './domain/useCases/GetDatasetStorageDriver' +import { GetDatasetUploadLimits } from './domain/useCases/GetDatasetUploadLimits' const datasetsRepository = new DatasetsRepository() @@ -79,7 +82,10 @@ const setAvailableLicensesForDatasetType = new SetAvailableLicensesForDatasetTyp ) const deleteDatasetType = new DeleteDatasetType(datasetsRepository) const getDatasetCitationInOtherFormats = new GetDatasetCitationInOtherFormats(datasetsRepository) -const getDatasetTemplates = new GetDatasetTemplates(datasetsRepository) +const updateTermsOfAccess = new UpdateTermsOfAccess(datasetsRepository) +const updateDatasetLicense = new UpdateDatasetLicense(datasetsRepository) +const getDatasetStorageDriver = new GetDatasetStorageDriver(datasetsRepository) +const getDatasetUploadLimits = new GetDatasetUploadLimits(datasetsRepository) export { getDataset, @@ -103,13 +109,16 @@ export { getDatasetLinkedCollections, getDatasetAvailableCategories, getDatasetCitationInOtherFormats, - getDatasetTemplates, + updateTermsOfAccess, getDatasetAvailableDatasetTypes, getDatasetAvailableDatasetType, addDatasetType, linkDatasetTypeWithMetadataBlocks, setAvailableLicensesForDatasetType, - deleteDatasetType + deleteDatasetType, + updateDatasetLicense, + getDatasetStorageDriver, + getDatasetUploadLimits } export { DatasetNotNumberedVersion } from './domain/models/DatasetNotNumberedVersion' export { DatasetUserPermissions } from './domain/models/DatasetUserPermissions' @@ -136,12 +145,17 @@ export { DatasetMetadataBlockValuesDTO, DatasetMetadataChildFieldValueDTO } from './domain/dtos/DatasetDTO' +export { DatasetLicenseUpdateRequest } from './domain/dtos/DatasetLicenseUpdateRequest' export { DatasetDeaccessionDTO } from './domain/dtos/DatasetDeaccessionDTO' export { CreatedDatasetIdentifiers } from './domain/models/CreatedDatasetIdentifiers' export { VersionUpdateType } from './domain/models/Dataset' export { DatasetVersionSummaryInfo, - DatasetVersionSummaryStringValues + DatasetVersionSummaryStringValues, + DatasetVersionSummarySubset } from './domain/models/DatasetVersionSummaryInfo' export { DatasetLinkedCollection } from './domain/models/DatasetLinkedCollection' export { DatasetType } from './domain/models/DatasetType' +export { DatasetTypeDTO } from './domain/dtos/DatasetTypeDTO' +export { StorageDriver } from './domain/models/StorageDriver' +export { DatasetUploadLimits } from './domain/models/DatasetUploadLimits' diff --git a/src/datasets/infra/repositories/DatasetsRepository.ts b/src/datasets/infra/repositories/DatasetsRepository.ts index 1545a43d..5bac498d 100644 --- a/src/datasets/infra/repositories/DatasetsRepository.ts +++ b/src/datasets/infra/repositories/DatasetsRepository.ts @@ -1,4 +1,3 @@ -import { AxiosResponse } from 'axios' import { ApiRepository } from '../../../core/infra/repositories/ApiRepository' import { IDatasetsRepository } from '../../domain/repositories/IDatasetsRepository' import { Dataset, VersionUpdateType } from '../../domain/models/Dataset' @@ -20,15 +19,18 @@ import { transformDatasetPreviewsResponseToDatasetPreviewSubset } from './transf import { DatasetVersionDiff } from '../../domain/models/DatasetVersionDiff' import { transformDatasetVersionDiffResponseToDatasetVersionDiff } from './transformers/datasetVersionDiffTransformers' import { DatasetDownloadCount } from '../../domain/models/DatasetDownloadCount' -import { DatasetVersionSummaryInfo } from '../../domain/models/DatasetVersionSummaryInfo' +import { DatasetVersionSummarySubset } from '../../domain/models/DatasetVersionSummaryInfo' import { DatasetLinkedCollection } from '../../domain/models/DatasetLinkedCollection' import { CitationFormat } from '../../domain/models/CitationFormat' import { transformDatasetLinkedCollectionsResponseToDatasetLinkedCollection } from './transformers/datasetLinkedCollectionsTransformers' import { FormattedCitation } from '../../domain/models/FormattedCitation' -import { DatasetTemplate } from '../../domain/models/DatasetTemplate' -import { DatasetTemplatePayload } from './transformers/DatasetTemplatePayload' -import { transformDatasetTemplatePayloadToDatasetTemplate } from './transformers/datasetTemplateTransformers' import { DatasetType } from '../../domain/models/DatasetType' +import { TermsOfAccess } from '../../domain/models/Dataset' +import { transformTermsOfAccessToUpdatePayload } from './transformers/termsOfAccessTransformers' +import { DatasetLicenseUpdateRequest } from '../../domain/dtos/DatasetLicenseUpdateRequest' +import { DatasetTypeDTO } from '../../domain/dtos/DatasetTypeDTO' +import { StorageDriver } from '../../domain/models/StorageDriver' +import { DatasetUploadLimits } from '../../domain/models/DatasetUploadLimits' export interface GetAllDatasetPreviewsQueryParams { per_page?: number @@ -252,16 +254,14 @@ export class DatasetsRepository extends ApiRepository implements IDatasetsReposi datasetId: string | number, dataset: DatasetDTO, datasetMetadataBlocks: MetadataBlock[], - internalVersionNumber?: number + sourceLastUpdateTime?: string ): Promise { return this.doPut( this.buildApiEndpoint(this.datasetsResourceName, `editMetadata`, datasetId), transformDatasetModelToUpdateDatasetRequestPayload(dataset, datasetMetadataBlocks), { replace: true, - ...(typeof internalVersionNumber === 'number' && { - sourceInternalVersionNumber: internalVersionNumber - }) + ...(sourceLastUpdateTime && { sourceLastUpdateTime }) } ) .then(() => undefined) @@ -307,13 +307,29 @@ export class DatasetsRepository extends ApiRepository implements IDatasetsReposi } public async getDatasetVersionsSummaries( - datasetId: string | number - ): Promise { + datasetId: string | number, + limit?: number, + offset?: number + ): Promise { + const queryParams = new URLSearchParams() + + if (limit) { + queryParams.set('limit', limit.toString()) + } + + if (offset) { + queryParams.set('offset', offset.toString()) + } + return this.doGet( this.buildApiEndpoint(this.datasetsResourceName, 'versions/compareSummary', datasetId), - true + true, + queryParams ) - .then((response) => response.data.data) + .then((response) => ({ + summaries: response.data.data, + totalCount: response.data.totalCount + })) .catch((error) => { throw error }) @@ -329,16 +345,32 @@ export class DatasetsRepository extends ApiRepository implements IDatasetsReposi }) } - public async linkDataset(datasetId: number, collectionAlias: string): Promise { - return this.doPut(`/${this.datasetsResourceName}/${datasetId}/link/${collectionAlias}`, {}) + public async linkDataset( + datasetId: number | string, + collectionIdOrAlias: number | string + ): Promise { + const endpoint = this.buildApiEndpoint( + this.datasetsResourceName, + `link/${collectionIdOrAlias}`, + datasetId + ) + return this.doPut(endpoint, {}) .then(() => undefined) .catch((error) => { throw error }) } - public async unlinkDataset(datasetId: number, collectionAlias: string): Promise { - return this.doDelete(`/${this.datasetsResourceName}/${datasetId}/deleteLink/${collectionAlias}`) + public async unlinkDataset( + datasetId: number | string, + collectionIdOrAlias: number | string + ): Promise { + const endpoint = this.buildApiEndpoint( + this.datasetsResourceName, + `deleteLink/${collectionIdOrAlias}`, + datasetId + ) + return this.doDelete(endpoint) .then(() => undefined) .catch((error) => { throw error @@ -368,18 +400,6 @@ export class DatasetsRepository extends ApiRepository implements IDatasetsReposi }) } - public async getDatasetTemplates( - collectionIdOrAlias: number | string - ): Promise { - return this.doGet(`/dataverses/${collectionIdOrAlias}/templates`, true) - .then((response: AxiosResponse<{ data: DatasetTemplatePayload[] }>) => - transformDatasetTemplatePayloadToDatasetTemplate(response.data.data) - ) - .catch((error) => { - throw error - }) - } - public async getDatasetAvailableDatasetTypes(): Promise { return this.doGet(this.buildApiEndpoint(this.datasetsResourceName, 'datasetTypes')) .then((response) => response.data.data) @@ -402,7 +422,7 @@ export class DatasetsRepository extends ApiRepository implements IDatasetsReposi }) } - public async addDatasetType(datasetType: DatasetType): Promise { + public async addDatasetType(datasetType: DatasetTypeDTO): Promise { return this.doPost( this.buildApiEndpoint(this.datasetsResourceName, 'datasetTypes'), datasetType @@ -453,4 +473,54 @@ export class DatasetsRepository extends ApiRepository implements IDatasetsReposi throw error }) } + + public async updateTermsOfAccess( + datasetId: number | string, + termsOfAccess: TermsOfAccess + ): Promise { + return this.doPut( + this.buildApiEndpoint(this.datasetsResourceName, 'access', datasetId), + transformTermsOfAccessToUpdatePayload(termsOfAccess) + ) + .then(() => undefined) + .catch((error) => { + throw error + }) + } + + public async updateDatasetLicense( + datasetId: number | string, + payload: DatasetLicenseUpdateRequest + ): Promise { + return this.doPut( + this.buildApiEndpoint(this.datasetsResourceName, 'license', datasetId), + payload + ) + .then(() => undefined) + .catch((error) => { + throw error + }) + } + + public async getDatasetStorageDriver(datasetId: number | string): Promise { + return this.doGet( + this.buildApiEndpoint(this.datasetsResourceName, 'storageDriver', datasetId), + true + ) + .then((response) => response.data.data as StorageDriver) + .catch((error) => { + throw error + }) + } + + public async getDatasetUploadLimits(datasetId: number | string): Promise { + return this.doGet( + this.buildApiEndpoint(this.datasetsResourceName, 'uploadlimits', datasetId), + true + ) + .then((response) => (response.data?.data?.uploadLimits ?? {}) as DatasetUploadLimits) + .catch((error) => { + throw error + }) + } } diff --git a/src/datasets/infra/repositories/transformers/DatasetPayload.ts b/src/datasets/infra/repositories/transformers/DatasetPayload.ts index b0535677..347bc5d1 100644 --- a/src/datasets/infra/repositories/transformers/DatasetPayload.ts +++ b/src/datasets/infra/repositories/transformers/DatasetPayload.ts @@ -17,6 +17,7 @@ export interface DatasetPayload { alternativePersistentId?: string publicationDate?: string citationDate?: string + guestbookId?: number fileAccessRequest: boolean termsOfAccess?: string dataAccessPlace?: string diff --git a/src/datasets/infra/repositories/transformers/datasetPreviewsTransformers.ts b/src/datasets/infra/repositories/transformers/datasetPreviewsTransformers.ts index 8b60828d..3ae56800 100644 --- a/src/datasets/infra/repositories/transformers/datasetPreviewsTransformers.ts +++ b/src/datasets/infra/repositories/transformers/datasetPreviewsTransformers.ts @@ -39,7 +39,7 @@ export const transformDatasetPreviewPayloadToDatasetPreview = ( minorNumber: datasetPreviewPayload.minorVersion, state: datasetPreviewPayload.versionState as DatasetVersionState, createTime: new Date(datasetPreviewPayload.createdAt), - lastUpdateTime: new Date(datasetPreviewPayload.updatedAt), + lastUpdateTime: datasetPreviewPayload.updatedAt, ...(datasetPreviewPayload.published_at && { releaseTime: new Date(datasetPreviewPayload.published_at) }) @@ -72,7 +72,7 @@ export const transformMyDataDatasetPreviewPayloadToDatasetPreview = ( minorNumber: datasetPreviewPayload.minorVersion, state: datasetPreviewPayload.versionState as DatasetVersionState, createTime: new Date(datasetPreviewPayload.createdAt), - lastUpdateTime: new Date(datasetPreviewPayload.updatedAt), + lastUpdateTime: datasetPreviewPayload.updatedAt, ...(datasetPreviewPayload.published_at && { releaseTime: new Date(datasetPreviewPayload.published_at) }) diff --git a/src/datasets/infra/repositories/transformers/datasetTemplateTransformers.ts b/src/datasets/infra/repositories/transformers/datasetTemplateTransformers.ts deleted file mode 100644 index 32486199..00000000 --- a/src/datasets/infra/repositories/transformers/datasetTemplateTransformers.ts +++ /dev/null @@ -1,57 +0,0 @@ -import { transformPayloadLicenseToLicense } from '../../../../licenses/domain/repositories/transformers/licenseTransformers' -import { DatasetTemplate } from '../../../domain/models/DatasetTemplate' -import { DatasetTemplatePayload } from './DatasetTemplatePayload' -import { transformPayloadToDatasetMetadataBlocks } from './datasetTransformers' - -export const transformDatasetTemplatePayloadToDatasetTemplate = ( - collectionDatasetTemplatePayload: DatasetTemplatePayload[] -): DatasetTemplate[] => { - return collectionDatasetTemplatePayload.map((payload) => { - const datasetTemplate: DatasetTemplate = { - id: payload.id, - name: payload.name, - collectionAlias: payload.dataverseAlias, - isDefault: payload.isDefault, - usageCount: payload.usageCount, - createTime: payload.createTime, - createDate: payload.createDate, - datasetMetadataBlocks: transformPayloadToDatasetMetadataBlocks(payload.datasetFields, false), - instructions: payload.instructions.map((instruction) => ({ - instructionField: instruction.instructionField, - instructionText: instruction.instructionText - })), - termsOfUse: { - termsOfAccess: { - fileAccessRequest: payload.termsOfUseAndAccess.fileAccessRequest, - termsOfAccessForRestrictedFiles: payload.termsOfUseAndAccess.termsOfAccess, - dataAccessPlace: payload.termsOfUseAndAccess.dataAccessPlace, - originalArchive: payload.termsOfUseAndAccess.originalArchive, - availabilityStatus: payload.termsOfUseAndAccess.availabilityStatus, - contactForAccess: payload.termsOfUseAndAccess.contactForAccess, - sizeOfCollection: payload.termsOfUseAndAccess.sizeOfCollection, - studyCompletion: payload.termsOfUseAndAccess.studyCompletion - } - } - } - - if (payload.termsOfUseAndAccess.license) { - datasetTemplate.license = transformPayloadLicenseToLicense( - payload.termsOfUseAndAccess.license - ) - } else { - datasetTemplate.termsOfUse.customTerms = { - termsOfUse: payload.termsOfUseAndAccess.termsOfUse as string, - confidentialityDeclaration: payload.termsOfUseAndAccess - .confidentialityDeclaration as string, - specialPermissions: payload.termsOfUseAndAccess.specialPermissions as string, - restrictions: payload.termsOfUseAndAccess.restrictions as string, - citationRequirements: payload.termsOfUseAndAccess.citationRequirements as string, - depositorRequirements: payload.termsOfUseAndAccess.depositorRequirements as string, - conditions: payload.termsOfUseAndAccess.conditions as string, - disclaimer: payload.termsOfUseAndAccess.disclaimer as string - } - } - - return datasetTemplate - }) -} diff --git a/src/datasets/infra/repositories/transformers/datasetTransformers.ts b/src/datasets/infra/repositories/transformers/datasetTransformers.ts index bbb4c9fc..1c1e31d8 100644 --- a/src/datasets/infra/repositories/transformers/datasetTransformers.ts +++ b/src/datasets/infra/repositories/transformers/datasetTransformers.ts @@ -235,7 +235,7 @@ export const transformVersionPayloadToDataset = ( minorNumber: versionPayload.versionMinorNumber, state: versionPayload.versionState as DatasetVersionState, createTime: new Date(versionPayload.createTime), - lastUpdateTime: new Date(versionPayload.lastUpdateTime), + lastUpdateTime: versionPayload.lastUpdateTime, releaseTime: new Date(versionPayload.releaseTime), deaccessionNote: versionPayload.deaccessionNote }, @@ -296,6 +296,9 @@ export const transformVersionPayloadToDataset = ( if ('citationDate' in versionPayload) { datasetModel.citationDate = versionPayload.citationDate } + if ('guestbookId' in versionPayload) { + datasetModel.guestbookId = versionPayload.guestbookId + } if ('datasetType' in versionPayload) { datasetModel.datasetType = versionPayload.datasetType } diff --git a/src/datasets/infra/repositories/transformers/termsOfAccessTransformers.ts b/src/datasets/infra/repositories/transformers/termsOfAccessTransformers.ts new file mode 100644 index 00000000..1e9be77d --- /dev/null +++ b/src/datasets/infra/repositories/transformers/termsOfAccessTransformers.ts @@ -0,0 +1,31 @@ +import { TermsOfAccess } from '../../../domain/models/Dataset' + +export const transformTermsOfAccessToUpdatePayload = ( + terms: TermsOfAccess & { termsOfAccess?: string } +) => { + const { + fileAccessRequest, + dataAccessPlace, + originalArchive, + availabilityStatus, + contactForAccess, + sizeOfCollection, + studyCompletion + } = terms + + const termsOfAccessForRestrictedFiles = + terms.termsOfAccess ?? terms.termsOfAccessForRestrictedFiles + + return { + customTermsOfAccess: { + fileAccessRequest, + termsOfAccess: termsOfAccessForRestrictedFiles, + dataAccessPlace, + originalArchive, + availabilityStatus, + contactForAccess, + sizeOfCollection, + studyCompletion + } + } +} diff --git a/src/files/domain/models/FileModel.ts b/src/files/domain/models/FileModel.ts index abf95d71..559396b8 100644 --- a/src/files/domain/models/FileModel.ts +++ b/src/files/domain/models/FileModel.ts @@ -30,6 +30,12 @@ export interface FileModel { tabularTags?: string[] creationDate?: string publicationDate?: string + /** + * The timestamp of the last update to this file record. + * Format: ISO 8601 string (e.g., "2023-06-01T12:34:56Z"). + * Used for optimistic concurrency control to detect concurrent updates. + */ + lastUpdateTime: string deleted: boolean tabularData: boolean fileAccessRequest?: boolean diff --git a/src/files/domain/models/FileVersionSummaryInfo.ts b/src/files/domain/models/FileVersionSummaryInfo.ts index 2ee0a7ba..3cf95315 100644 --- a/src/files/domain/models/FileVersionSummaryInfo.ts +++ b/src/files/domain/models/FileVersionSummaryInfo.ts @@ -11,6 +11,11 @@ export interface FileVersionSummaryInfo { versionNote?: string } +export interface FileVersionSummarySubset { + summaries: FileVersionSummaryInfo[] + totalCount: number +} + export type FileDifferenceSummary = { file?: FileChangeType fileAccess?: 'Restricted' | 'Unrestricted' diff --git a/src/files/domain/repositories/IFilesRepository.ts b/src/files/domain/repositories/IFilesRepository.ts index 8049010c..4890a38b 100644 --- a/src/files/domain/repositories/IFilesRepository.ts +++ b/src/files/domain/repositories/IFilesRepository.ts @@ -10,7 +10,7 @@ import { FileUploadDestination } from '../models/FileUploadDestination' import { UploadedFileDTO } from '../dtos/UploadedFileDTO' import { UpdateFileMetadataDTO } from '../dtos/UpdateFileMetadataDTO' import { RestrictFileDTO } from '../dtos/RestrictFileDTO' -import { FileVersionSummaryInfo } from '../models/FileVersionSummaryInfo' +import { FileVersionSummarySubset } from '../models/FileVersionSummaryInfo' export interface IFilesRepository { getDatasetFiles( @@ -72,7 +72,8 @@ export interface IFilesRepository { updateFileMetadata( fileId: number | string, - updateFileMetadataDTO: UpdateFileMetadataDTO + updateFileMetadataDTO: UpdateFileMetadataDTO, + sourceLastUpdateTime?: string ): Promise updateFileTabularTags( @@ -87,7 +88,11 @@ export interface IFilesRepository { replace?: boolean ): Promise - getFileVersionSummaries(fileId: number | string): Promise + getFileVersionSummaries( + fileId: number | string, + limit?: number, + offset?: number + ): Promise isFileDeleted(fileId: number | string): Promise } diff --git a/src/files/domain/useCases/GetFileVersionSummaries.ts b/src/files/domain/useCases/GetFileVersionSummaries.ts index c8bafb50..c46b14db 100644 --- a/src/files/domain/useCases/GetFileVersionSummaries.ts +++ b/src/files/domain/useCases/GetFileVersionSummaries.ts @@ -1,8 +1,8 @@ import { UseCase } from '../../../core/domain/useCases/UseCase' -import { FileVersionSummaryInfo } from '../models/FileVersionSummaryInfo' +import { FileVersionSummarySubset } from '../models/FileVersionSummaryInfo' import { IFilesRepository } from '../repositories/IFilesRepository' -export class GetFileVersionSummaries implements UseCase { +export class GetFileVersionSummaries implements UseCase { private filesRepository: IFilesRepository constructor(filesRepository: IFilesRepository) { @@ -13,9 +13,15 @@ export class GetFileVersionSummaries implements UseCase} - An array of FileVersionSummaryInfo. + * @param {number} [limit] - Limit for pagination (optional). + * @param {number} [offset] - Offset for pagination (optional). + * @returns {Promise} - A FileVersionSummarySubset containing the summaries and total count. */ - async execute(fileId: number | string): Promise { - return await this.filesRepository.getFileVersionSummaries(fileId) + async execute( + fileId: number | string, + limit?: number, + offset?: number + ): Promise { + return await this.filesRepository.getFileVersionSummaries(fileId, limit, offset) } } diff --git a/src/files/domain/useCases/UpdateFileMetadata.ts b/src/files/domain/useCases/UpdateFileMetadata.ts index f06c96c3..c3b62d8a 100644 --- a/src/files/domain/useCases/UpdateFileMetadata.ts +++ b/src/files/domain/useCases/UpdateFileMetadata.ts @@ -15,12 +15,18 @@ export class UpdateFileMetadata implements UseCase { * * @param {number | string} [fileId] - The file identifier, which can be a string (for persistent identifiers), or a number (for numeric identifiers). * @param {UpdateFileMetadataDTO} [updateFileMetadataDTO] - The DTO containing the metadata updates. + * @param {string} [sourceLastUpdateTime] - The lastUpdateTime value from the file. If another user updates the file metadata before you send the update request, data inconsistencies may occur. To prevent this, you can use the optional sourceLastUpdateTime parameter. This parameter must include the lastUpdateTime value corresponding to the file being updated. * @returns {Promise} */ async execute( fileId: number | string, - updateFileMetadataDTO: UpdateFileMetadataDTO + updateFileMetadataDTO: UpdateFileMetadataDTO, + sourceLastUpdateTime?: string ): Promise { - await this.filesRepository.updateFileMetadata(fileId, updateFileMetadataDTO) + await this.filesRepository.updateFileMetadata( + fileId, + updateFileMetadataDTO, + sourceLastUpdateTime + ) } } diff --git a/src/files/index.ts b/src/files/index.ts index b13bfc53..a9d38386 100644 --- a/src/files/index.ts +++ b/src/files/index.ts @@ -93,3 +93,10 @@ export { FilesSubset } from './domain/models/FilesSubset' export { FilePreview, FilePreviewChecksum } from './domain/models/FilePreview' export { UploadedFileDTO } from './domain/dtos/UploadedFileDTO' export { UpdateFileMetadataDTO } from './domain/dtos/UpdateFileMetadataDTO' +export { + FileVersionSummaryInfo, + FileDifferenceSummary, + FileChangeType, + FileMetadataChange, + FileVersionSummarySubset +} from './domain/models/FileVersionSummaryInfo' diff --git a/src/files/infra/repositories/FilesRepository.ts b/src/files/infra/repositories/FilesRepository.ts index 3430cd4d..00b70ba8 100644 --- a/src/files/infra/repositories/FilesRepository.ts +++ b/src/files/infra/repositories/FilesRepository.ts @@ -22,7 +22,7 @@ import { UploadedFileDTO } from '../../domain/dtos/UploadedFileDTO' import { UpdateFileMetadataDTO } from '../../domain/dtos/UpdateFileMetadataDTO' import { ApiConstants } from '../../../core/infra/repositories/ApiConstants' import { RestrictFileDTO } from '../../domain/dtos/RestrictFileDTO' -import { FileVersionSummaryInfo } from '../../domain/models/FileVersionSummaryInfo' +import { FileVersionSummarySubset } from '../../domain/models/FileVersionSummaryInfo' import { transformFileVersionSummaryInfoResponseToFileVersionSummaryInfo } from './transformers/fileVersionSummaryInfoTransformers' export interface GetFilesQueryParams { @@ -369,7 +369,8 @@ export class FilesRepository extends ApiRepository implements IFilesRepository { public async updateFileMetadata( fileId: string | number, - updateFileMetadata: UpdateFileMetadataDTO + updateFileMetadata: UpdateFileMetadataDTO, + sourceLastUpdateTime?: string ): Promise { const formData = new FormData() formData.append('jsonData', JSON.stringify(updateFileMetadata)) @@ -377,7 +378,9 @@ export class FilesRepository extends ApiRepository implements IFilesRepository { return this.doPost( this.buildApiEndpoint(this.filesResourceName, `${fileId}/metadata`), formData, - {}, + { + ...(sourceLastUpdateTime && { sourceLastUpdateTime }) + }, ApiConstants.CONTENT_TYPE_MULTIPART_FORM_DATA ) .then(() => undefined) @@ -420,10 +423,25 @@ export class FilesRepository extends ApiRepository implements IFilesRepository { }) } - public async getFileVersionSummaries(fileId: number | string): Promise { + public async getFileVersionSummaries( + fileId: number | string, + limit?: number, + offset?: number + ): Promise { + const queryParams = new URLSearchParams() + + if (limit) { + queryParams.set('limit', limit.toString()) + } + + if (offset) { + queryParams.set('offset', offset.toString()) + } + return this.doGet( this.buildApiEndpoint(this.filesResourceName, 'versionDifferences', fileId), - true + true, + queryParams ) .then((response) => transformFileVersionSummaryInfoResponseToFileVersionSummaryInfo(response)) .catch((error) => { diff --git a/src/files/infra/repositories/transformers/FilePayload.ts b/src/files/infra/repositories/transformers/FilePayload.ts index 58afcc4a..cc7ad6b1 100644 --- a/src/files/infra/repositories/transformers/FilePayload.ts +++ b/src/files/infra/repositories/transformers/FilePayload.ts @@ -29,6 +29,7 @@ export interface FilePayload { tabularTags?: string[] creationDate?: string publicationDate?: string + lastUpdateTime: string deleted: boolean tabularData: boolean fileAccessRequest?: boolean diff --git a/src/files/infra/repositories/transformers/fileTransformers.ts b/src/files/infra/repositories/transformers/fileTransformers.ts index 5350af01..a93b7674 100644 --- a/src/files/infra/repositories/transformers/fileTransformers.ts +++ b/src/files/infra/repositories/transformers/fileTransformers.ts @@ -93,7 +93,8 @@ const transformFilePayloadToFile = (filePayload: FilePayload): FileModel => { }), ...(filePayload.dataFile.isPartOf && { isPartOf: transformPayloadToOwnerNode(filePayload.dataFile.isPartOf) - }) + }), + lastUpdateTime: filePayload.dataFile.lastUpdateTime } } diff --git a/src/files/infra/repositories/transformers/fileVersionSummaryInfoTransformers.ts b/src/files/infra/repositories/transformers/fileVersionSummaryInfoTransformers.ts index 46eb4049..dd62262d 100644 --- a/src/files/infra/repositories/transformers/fileVersionSummaryInfoTransformers.ts +++ b/src/files/infra/repositories/transformers/fileVersionSummaryInfoTransformers.ts @@ -2,7 +2,8 @@ import { AxiosResponse } from 'axios' import { FileVersionSummaryInfo, FileMetadataChange, - FileDifferenceSummary + FileDifferenceSummary, + FileVersionSummarySubset } from '../../../domain/models/FileVersionSummaryInfo' import { DatasetVersionState } from '../../../../datasets/domain/models/Dataset' @@ -29,10 +30,11 @@ export interface FileVersionSummaryInfoPayload { export const transformFileVersionSummaryInfoResponseToFileVersionSummaryInfo = ( response: AxiosResponse -): FileVersionSummaryInfo[] => { +): FileVersionSummarySubset => { const payload = response.data.data + const totalCount = response.data.totalCount - return payload.map((item: FileVersionSummaryInfoPayload): FileVersionSummaryInfo => { + const summaries = payload.map((item: FileVersionSummaryInfoPayload): FileVersionSummaryInfo => { const summary = item.fileDifferenceSummary || {} const fileDifferenceSummary: FileDifferenceSummary = { @@ -54,4 +56,9 @@ export const transformFileVersionSummaryInfoResponseToFileVersionSummaryInfo = ( versionNote: item.versionNote } }) + + return { + summaries, + totalCount + } } diff --git a/src/guestbooks/domain/dtos/CreateGuestbookDTO.ts b/src/guestbooks/domain/dtos/CreateGuestbookDTO.ts new file mode 100644 index 00000000..02aa51ef --- /dev/null +++ b/src/guestbooks/domain/dtos/CreateGuestbookDTO.ts @@ -0,0 +1,25 @@ +export type CreateGuestbookQuestionTypeDTO = 'text' | 'textarea' | 'options' + +export interface CreateGuestbookOptionDTO { + value: string + displayOrder: number +} + +export interface CreateGuestbookCustomQuestionDTO { + question: string + required: boolean + displayOrder: number + type: CreateGuestbookQuestionTypeDTO + hidden: boolean + optionValues?: CreateGuestbookOptionDTO[] +} + +export interface CreateGuestbookDTO { + name: string + enabled: boolean + emailRequired: boolean + nameRequired: boolean + institutionRequired: boolean + positionRequired: boolean + customQuestions: CreateGuestbookCustomQuestionDTO[] +} diff --git a/src/guestbooks/domain/models/Guestbook.ts b/src/guestbooks/domain/models/Guestbook.ts new file mode 100644 index 00000000..2a2f3c5b --- /dev/null +++ b/src/guestbooks/domain/models/Guestbook.ts @@ -0,0 +1,28 @@ +export type GuestbookQuestionType = 'text' | 'textarea' | 'options' + +export interface GuestbookOption { + value: string + displayOrder: number +} + +export interface GuestbookCustomQuestion { + question: string + required: boolean + displayOrder: number + type: GuestbookQuestionType + hidden: boolean + optionValues?: GuestbookOption[] +} + +export interface Guestbook { + id: number + name: string + enabled: boolean + emailRequired: boolean + nameRequired: boolean + institutionRequired: boolean + positionRequired: boolean + customQuestions: GuestbookCustomQuestion[] + createTime: string + dataverseId: number +} diff --git a/src/guestbooks/domain/repositories/IGuestbooksRepository.ts b/src/guestbooks/domain/repositories/IGuestbooksRepository.ts new file mode 100644 index 00000000..87ce91ab --- /dev/null +++ b/src/guestbooks/domain/repositories/IGuestbooksRepository.ts @@ -0,0 +1,18 @@ +import { CreateGuestbookDTO } from '../dtos/CreateGuestbookDTO' +import { Guestbook } from '../models/Guestbook' + +export interface IGuestbooksRepository { + createGuestbook( + collectionIdOrAlias: number | string, + guestbook: CreateGuestbookDTO + ): Promise + getGuestbook(guestbookId: number): Promise + getGuestbooksByCollectionId(collectionIdOrAlias: number | string): Promise + setGuestbookEnabled( + collectionIdOrAlias: number | string, + guestbookId: number, + enabled: boolean + ): Promise + assignDatasetGuestbook(datasetId: number | string, guestbookId: number): Promise + removeDatasetGuestbook(datasetId: number | string): Promise +} diff --git a/src/guestbooks/domain/useCases/AssignDatasetGuestbook.ts b/src/guestbooks/domain/useCases/AssignDatasetGuestbook.ts new file mode 100644 index 00000000..dbc880a4 --- /dev/null +++ b/src/guestbooks/domain/useCases/AssignDatasetGuestbook.ts @@ -0,0 +1,17 @@ +import { UseCase } from '../../../core/domain/useCases/UseCase' +import { IGuestbooksRepository } from '../repositories/IGuestbooksRepository' + +export class AssignDatasetGuestbook implements UseCase { + constructor(private readonly guestbooksRepository: IGuestbooksRepository) {} + + /** + * Assigns a guestbook to a dataset. + * + * @param {number | string} datasetId - Dataset identifier (persistent id or numeric id). + * @param {number} guestbookId - Guestbook numeric identifier. + * @returns {Promise} + */ + async execute(datasetId: number | string, guestbookId: number): Promise { + return await this.guestbooksRepository.assignDatasetGuestbook(datasetId, guestbookId) + } +} diff --git a/src/guestbooks/domain/useCases/CreateGuestbook.ts b/src/guestbooks/domain/useCases/CreateGuestbook.ts new file mode 100644 index 00000000..3d44be3b --- /dev/null +++ b/src/guestbooks/domain/useCases/CreateGuestbook.ts @@ -0,0 +1,20 @@ +import { CreateGuestbookDTO } from '../dtos/CreateGuestbookDTO' +import { IGuestbooksRepository } from '../repositories/IGuestbooksRepository' + +export class CreateGuestbook { + constructor(private readonly guestbooksRepository: IGuestbooksRepository) {} + + /** + * Creates a guestbook for the given collection. + * + * @param {CreateGuestbookDTO} guestbook - Guestbook creation payload. + * @param {number | string} collectionIdOrAlias - Collection identifier (numeric id or alias). + * @returns {Promise} - The created guestbook identifier. + */ + async execute( + guestbook: CreateGuestbookDTO, + collectionIdOrAlias: number | string + ): Promise { + return await this.guestbooksRepository.createGuestbook(collectionIdOrAlias, guestbook) + } +} diff --git a/src/guestbooks/domain/useCases/GetGuestbook.ts b/src/guestbooks/domain/useCases/GetGuestbook.ts new file mode 100644 index 00000000..7ef85940 --- /dev/null +++ b/src/guestbooks/domain/useCases/GetGuestbook.ts @@ -0,0 +1,17 @@ +import { UseCase } from '../../../core/domain/useCases/UseCase' +import { IGuestbooksRepository } from '../repositories/IGuestbooksRepository' +import { Guestbook } from '../models/Guestbook' + +export class GetGuestbook implements UseCase { + constructor(private readonly guestbooksRepository: IGuestbooksRepository) {} + + /** + * Returns a guestbook by id. + * + * @param {number} guestbookId - Guestbook identifier. + * @returns {Promise} + */ + async execute(guestbookId: number): Promise { + return await this.guestbooksRepository.getGuestbook(guestbookId) + } +} diff --git a/src/guestbooks/domain/useCases/GetGuestbooksByCollectionId.ts b/src/guestbooks/domain/useCases/GetGuestbooksByCollectionId.ts new file mode 100644 index 00000000..003bdb07 --- /dev/null +++ b/src/guestbooks/domain/useCases/GetGuestbooksByCollectionId.ts @@ -0,0 +1,17 @@ +import { UseCase } from '../../../core/domain/useCases/UseCase' +import { IGuestbooksRepository } from '../repositories/IGuestbooksRepository' +import { Guestbook } from '../models/Guestbook' + +export class GetGuestbooksByCollectionId implements UseCase { + constructor(private readonly guestbooksRepository: IGuestbooksRepository) {} + + /** + * Returns all guestbooks available for a given collection. + * + * @param {number | string} collectionIdOrAlias - Collection identifier (numeric id or alias). + * @returns {Promise} + */ + async execute(collectionIdOrAlias: number | string): Promise { + return await this.guestbooksRepository.getGuestbooksByCollectionId(collectionIdOrAlias) + } +} diff --git a/src/guestbooks/domain/useCases/RemoveDatasetGuestbook.ts b/src/guestbooks/domain/useCases/RemoveDatasetGuestbook.ts new file mode 100644 index 00000000..c0b0c78f --- /dev/null +++ b/src/guestbooks/domain/useCases/RemoveDatasetGuestbook.ts @@ -0,0 +1,16 @@ +import { UseCase } from '../../../core/domain/useCases/UseCase' +import { IGuestbooksRepository } from '../repositories/IGuestbooksRepository' + +export class RemoveDatasetGuestbook implements UseCase { + constructor(private readonly guestbooksRepository: IGuestbooksRepository) {} + + /** + * Removes guestbook assignment from a dataset. + * + * @param {number | string} datasetId - Dataset identifier (persistent id or numeric id). + * @returns {Promise} + */ + async execute(datasetId: number | string): Promise { + return await this.guestbooksRepository.removeDatasetGuestbook(datasetId) + } +} diff --git a/src/guestbooks/domain/useCases/SetGuestbookEnabled.ts b/src/guestbooks/domain/useCases/SetGuestbookEnabled.ts new file mode 100644 index 00000000..85e55138 --- /dev/null +++ b/src/guestbooks/domain/useCases/SetGuestbookEnabled.ts @@ -0,0 +1,26 @@ +import { UseCase } from '../../../core/domain/useCases/UseCase' +import { IGuestbooksRepository } from '../repositories/IGuestbooksRepository' + +export class SetGuestbookEnabled implements UseCase { + constructor(private readonly guestbooksRepository: IGuestbooksRepository) {} + + /** + * Enables or disables a guestbook in a collection. + * + * @param {number | string} collectionIdOrAlias - Collection identifier (numeric id or alias). + * @param {number} guestbookId - Guestbook identifier. + * @param {boolean} enabled - Desired enabled state. + * @returns {Promise} + */ + async execute( + collectionIdOrAlias: number | string, + guestbookId: number, + enabled: boolean + ): Promise { + return await this.guestbooksRepository.setGuestbookEnabled( + collectionIdOrAlias, + guestbookId, + enabled + ) + } +} diff --git a/src/guestbooks/index.ts b/src/guestbooks/index.ts new file mode 100644 index 00000000..29d22988 --- /dev/null +++ b/src/guestbooks/index.ts @@ -0,0 +1,32 @@ +import { GuestbooksRepository } from './infra/repositories/GuestbooksRepository' +import { CreateGuestbook } from './domain/useCases/CreateGuestbook' +import { GetGuestbook } from './domain/useCases/GetGuestbook' +import { GetGuestbooksByCollectionId } from './domain/useCases/GetGuestbooksByCollectionId' +import { SetGuestbookEnabled } from './domain/useCases/SetGuestbookEnabled' +import { AssignDatasetGuestbook } from './domain/useCases/AssignDatasetGuestbook' +import { RemoveDatasetGuestbook } from './domain/useCases/RemoveDatasetGuestbook' + +const guestbooksRepository = new GuestbooksRepository() + +const createGuestbook = new CreateGuestbook(guestbooksRepository) +const getGuestbook = new GetGuestbook(guestbooksRepository) +const getGuestbooksByCollectionId = new GetGuestbooksByCollectionId(guestbooksRepository) +const setGuestbookEnabled = new SetGuestbookEnabled(guestbooksRepository) +const assignDatasetGuestbook = new AssignDatasetGuestbook(guestbooksRepository) +const removeDatasetGuestbook = new RemoveDatasetGuestbook(guestbooksRepository) + +export { + createGuestbook, + getGuestbook, + getGuestbooksByCollectionId, + setGuestbookEnabled, + assignDatasetGuestbook, + removeDatasetGuestbook +} + +export { + CreateGuestbookDTO, + CreateGuestbookCustomQuestionDTO, + CreateGuestbookOptionDTO +} from './domain/dtos/CreateGuestbookDTO' +export { Guestbook, GuestbookCustomQuestion, GuestbookOption } from './domain/models/Guestbook' diff --git a/src/guestbooks/infra/repositories/GuestbooksRepository.ts b/src/guestbooks/infra/repositories/GuestbooksRepository.ts new file mode 100644 index 00000000..6f9812e6 --- /dev/null +++ b/src/guestbooks/infra/repositories/GuestbooksRepository.ts @@ -0,0 +1,84 @@ +import { ApiRepository } from '../../../core/infra/repositories/ApiRepository' +import { CreateGuestbookDTO } from '../../domain/dtos/CreateGuestbookDTO' +import { Guestbook } from '../../domain/models/Guestbook' +import { IGuestbooksRepository } from '../../domain/repositories/IGuestbooksRepository' + +export class GuestbooksRepository extends ApiRepository implements IGuestbooksRepository { + private readonly guestbooksResourceName: string = 'guestbooks' + private readonly datasetsResourceName: string = 'datasets' + + public async createGuestbook( + collectionIdOrAlias: number | string, + guestbook: CreateGuestbookDTO + ): Promise { + return this.doPost( + this.buildApiEndpoint(this.guestbooksResourceName, `${collectionIdOrAlias}`), + guestbook + ) + .then((response) => response.data.data.id) + .catch((error) => { + throw error + }) + } + + public async getGuestbook(guestbookId: number): Promise { + return this.doGet( + this.buildApiEndpoint(this.guestbooksResourceName, undefined, guestbookId), + true + ) + .then((response) => response.data.data as Guestbook) + .catch((error) => { + throw error + }) + } + + public async getGuestbooksByCollectionId( + collectionIdOrAlias: number | string + ): Promise { + return this.doGet( + this.buildApiEndpoint(this.guestbooksResourceName, `${collectionIdOrAlias}/list`), + true + ) + .then((response) => response.data.data as Guestbook[]) + .catch((error) => { + throw error + }) + } + + public async setGuestbookEnabled( + collectionIdOrAlias: number | string, + guestbookId: number, + enabled: boolean + ): Promise { + const endpoint = this.buildApiEndpoint( + this.guestbooksResourceName, + `${collectionIdOrAlias}/${guestbookId}/enabled` + ) + return this.doPut(endpoint, enabled) + .then(() => undefined) + .catch((error) => { + throw error + }) + } + + public async assignDatasetGuestbook( + datasetId: number | string, + guestbookId: number + ): Promise { + const endpoint = this.buildApiEndpoint(this.datasetsResourceName, 'guestbook', datasetId) + return this.doPut(endpoint, guestbookId.toString()) + .then(() => undefined) + .catch((error) => { + throw error + }) + } + + public async removeDatasetGuestbook(datasetId: number | string): Promise { + const endpoint = this.buildApiEndpoint(this.datasetsResourceName, 'guestbook', datasetId) + return this.doDelete(endpoint) + .then(() => undefined) + .catch((error) => { + throw error + }) + } +} diff --git a/src/index.ts b/src/index.ts index 9e64baa6..efe54b0d 100644 --- a/src/index.ts +++ b/src/index.ts @@ -12,3 +12,6 @@ export * from './notifications' export * from './search' export * from './licenses' export * from './externalTools' +export * from './templates' +export * from './guestbooks' +export * from './access' diff --git a/src/info/domain/repositories/IDataverseInfoRepository.ts b/src/info/domain/repositories/IDataverseInfoRepository.ts index e0e85644..7ce0769a 100644 --- a/src/info/domain/repositories/IDataverseInfoRepository.ts +++ b/src/info/domain/repositories/IDataverseInfoRepository.ts @@ -7,4 +7,6 @@ export interface IDataverseInfoRepository { getMaxEmbargoDurationInMonths(): Promise getApplicationTermsOfUse(lang?: string): Promise getAvailableDatasetMetadataExportFormats(): Promise + getDatasetPublishPopupCustomText(): Promise + getPublishDatasetDisclaimerText(): Promise } diff --git a/src/info/domain/useCases/GetDatasetPublishPopupCustomText.ts b/src/info/domain/useCases/GetDatasetPublishPopupCustomText.ts new file mode 100644 index 00000000..b8286474 --- /dev/null +++ b/src/info/domain/useCases/GetDatasetPublishPopupCustomText.ts @@ -0,0 +1,19 @@ +import { UseCase } from '../../../core/domain/useCases/UseCase' +import { IDataverseInfoRepository } from '../repositories/IDataverseInfoRepository' + +export class GetDatasetPublishPopupCustomText implements UseCase { + private dataverseInfoRepository: IDataverseInfoRepository + + constructor(dataverseInfoRepository: IDataverseInfoRepository) { + this.dataverseInfoRepository = dataverseInfoRepository + } + + /** + * Returns a string containing custom text for the Publish Dataset modal. + * + * @returns {Promise} + */ + async execute(): Promise { + return await this.dataverseInfoRepository.getDatasetPublishPopupCustomText() + } +} diff --git a/src/info/domain/useCases/GetPublishDatasetDisclaimerText.ts b/src/info/domain/useCases/GetPublishDatasetDisclaimerText.ts new file mode 100644 index 00000000..db30a615 --- /dev/null +++ b/src/info/domain/useCases/GetPublishDatasetDisclaimerText.ts @@ -0,0 +1,19 @@ +import { UseCase } from '../../../core/domain/useCases/UseCase' +import { IDataverseInfoRepository } from '../repositories/IDataverseInfoRepository' + +export class GetPublishDatasetDisclaimerText implements UseCase { + private dataverseInfoRepository: IDataverseInfoRepository + + constructor(dataverseInfoRepository: IDataverseInfoRepository) { + this.dataverseInfoRepository = dataverseInfoRepository + } + + /** + * Returns a string containing the disclaimer text for the Publish Dataset modal. + * + * @returns {Promise} + */ + async execute(): Promise { + return await this.dataverseInfoRepository.getPublishDatasetDisclaimerText() + } +} diff --git a/src/info/index.ts b/src/info/index.ts index 3837e282..6b0d02b8 100644 --- a/src/info/index.ts +++ b/src/info/index.ts @@ -4,6 +4,8 @@ import { GetZipDownloadLimit } from './domain/useCases/GetZipDownloadLimit' import { GetMaxEmbargoDurationInMonths } from './domain/useCases/GetMaxEmbargoDurationInMonths' import { GetApplicationTermsOfUse } from './domain/useCases/GetApplicationTermsOfUse' import { GetAvailableDatasetMetadataExportFormats } from './domain/useCases/GetAvailableDatasetMetadataExportFormats' +import { GetDatasetPublishPopupCustomText } from './domain/useCases/GetDatasetPublishPopupCustomText' +import { GetPublishDatasetDisclaimerText } from './domain/useCases/GetPublishDatasetDisclaimerText' const dataverseInfoRepository = new DataverseInfoRepository() @@ -14,13 +16,19 @@ const getApplicationTermsOfUse = new GetApplicationTermsOfUse(dataverseInfoRepos const getAvailableDatasetMetadataExportFormats = new GetAvailableDatasetMetadataExportFormats( dataverseInfoRepository ) +const getPublishDatasetDisclaimerText = new GetPublishDatasetDisclaimerText(dataverseInfoRepository) +const getDatasetPublishPopupCustomText = new GetDatasetPublishPopupCustomText( + dataverseInfoRepository +) export { getDataverseVersion, getZipDownloadLimit, getMaxEmbargoDurationInMonths, getApplicationTermsOfUse, - getAvailableDatasetMetadataExportFormats + getAvailableDatasetMetadataExportFormats, + getDatasetPublishPopupCustomText, + getPublishDatasetDisclaimerText } export { DatasetMetadataExportFormats } from './domain/models/DatasetMetadataExportFormats' diff --git a/src/info/infra/repositories/DataverseInfoRepository.ts b/src/info/infra/repositories/DataverseInfoRepository.ts index 5e8aa5e0..a7306cbd 100644 --- a/src/info/infra/repositories/DataverseInfoRepository.ts +++ b/src/info/infra/repositories/DataverseInfoRepository.ts @@ -66,4 +66,26 @@ export class DataverseInfoRepository extends ApiRepository implements IDataverse throw error }) } + public async getDatasetPublishPopupCustomText(): Promise { + return this.doGet( + this.buildApiEndpoint(this.infoResourceName, `settings/:DatasetPublishPopupCustomText`) + ) + .then((response: AxiosResponse<{ data: { message: string } }>) => { + return response.data.data.message + }) + .catch((error) => { + throw error + }) + } + public async getPublishDatasetDisclaimerText(): Promise { + return this.doGet( + this.buildApiEndpoint(this.infoResourceName, `settings/:PublishDatasetDisclaimerText`) + ) + .then((response: AxiosResponse<{ data: { message: string } }>) => { + return response.data.data.message + }) + .catch((error) => { + throw error + }) + } } diff --git a/src/notifications/domain/models/Notification.ts b/src/notifications/domain/models/Notification.ts index 001d0933..f60b0ced 100644 --- a/src/notifications/domain/models/Notification.ts +++ b/src/notifications/domain/models/Notification.ts @@ -67,6 +67,6 @@ export interface Notification { dataFileId?: number dataFileDisplayName?: string currentCurationStatus?: string - additionalInfo?: string + additionalInfo?: Record objectDeleted?: boolean } diff --git a/src/notifications/domain/models/NotificationSubset.ts b/src/notifications/domain/models/NotificationSubset.ts new file mode 100644 index 00000000..fe4644b6 --- /dev/null +++ b/src/notifications/domain/models/NotificationSubset.ts @@ -0,0 +1,6 @@ +import { Notification } from './Notification' + +export interface NotificationSubset { + notifications: Notification[] + totalNotificationCount: number +} diff --git a/src/notifications/domain/repositories/INotificationsRepository.ts b/src/notifications/domain/repositories/INotificationsRepository.ts index 9392c543..6835f119 100644 --- a/src/notifications/domain/repositories/INotificationsRepository.ts +++ b/src/notifications/domain/repositories/INotificationsRepository.ts @@ -1,7 +1,12 @@ -import { Notification } from '../models/Notification' +import { NotificationSubset } from '../models/NotificationSubset' export interface INotificationsRepository { - getAllNotificationsByUser(inAppNotificationFormat?: boolean): Promise + getAllNotificationsByUser( + inAppNotificationFormat?: boolean, + onlyUnread?: boolean, + limit?: number, + offset?: number + ): Promise deleteNotification(notificationId: number): Promise getUnreadNotificationsCount(): Promise markNotificationAsRead(notificationId: number): Promise diff --git a/src/notifications/domain/useCases/GetAllNotificationsByUser.ts b/src/notifications/domain/useCases/GetAllNotificationsByUser.ts index 43555ccc..b4b2e6f6 100644 --- a/src/notifications/domain/useCases/GetAllNotificationsByUser.ts +++ b/src/notifications/domain/useCases/GetAllNotificationsByUser.ts @@ -1,19 +1,30 @@ import { UseCase } from '../../../core/domain/useCases/UseCase' -import { Notification } from '../models/Notification' import { INotificationsRepository } from '../repositories/INotificationsRepository' +import { NotificationSubset } from '../models/NotificationSubset' -export class GetAllNotificationsByUser implements UseCase { +export class GetAllNotificationsByUser implements UseCase { constructor(private readonly notificationsRepository: INotificationsRepository) {} /** * Use case for retrieving all notifications for the current user. * * @param inAppNotificationFormat - Optional parameter to retrieve fields needed for in-app notifications - * @returns {Promise} - A promise that resolves to an array of Notification instances. + * @param onlyUnread - Optional parameter to filter only unread notifications + * @param limit - Optional parameter to limit the number of notifications returned + * @param offset - Optional parameter to skip a number of notifications (for pagination) + * @returns {Promise} - A promise that resolves to an array of Notification instances. */ - async execute(inAppNotificationFormat?: boolean): Promise { + async execute( + inAppNotificationFormat?: boolean, + onlyUnread?: boolean, + limit?: number, + offset?: number + ): Promise { return (await this.notificationsRepository.getAllNotificationsByUser( - inAppNotificationFormat - )) as Notification[] + inAppNotificationFormat, + onlyUnread, + limit, + offset + )) as NotificationSubset } } diff --git a/src/notifications/infra/repositories/NotificationsRepository.ts b/src/notifications/infra/repositories/NotificationsRepository.ts index f310c34a..99e37c82 100644 --- a/src/notifications/infra/repositories/NotificationsRepository.ts +++ b/src/notifications/infra/repositories/NotificationsRepository.ts @@ -2,22 +2,30 @@ import { ApiRepository } from '../../../core/infra/repositories/ApiRepository' import { INotificationsRepository } from '../../domain/repositories/INotificationsRepository' import { Notification } from '../../domain/models/Notification' import { NotificationPayload } from '../transformers/NotificationPayload' +import { NotificationSubset } from '../../domain/models/NotificationSubset' export class NotificationsRepository extends ApiRepository implements INotificationsRepository { private readonly notificationsResourceName: string = 'notifications' public async getAllNotificationsByUser( - inAppNotificationFormat?: boolean - ): Promise { - const queryParams = inAppNotificationFormat ? { inAppNotificationFormat: 'true' } : undefined + inAppNotificationFormat?: boolean, + onlyUnread?: boolean, + limit?: number, + offset?: number + ): Promise { + const queryParams = new URLSearchParams() + + if (inAppNotificationFormat) queryParams.set('inAppNotificationFormat', 'true') + if (onlyUnread) queryParams.set('onlyUnread', 'true') + if (limit !== undefined) queryParams.set('limit', limit.toString()) + if (offset !== undefined) queryParams.set('offset', offset.toString()) return this.doGet( this.buildApiEndpoint(this.notificationsResourceName, 'all'), true, queryParams ) .then((response) => { - const notifications = response.data.data.notifications - return notifications.map((notification: NotificationPayload) => { + const notifications = response.data.data.map((notification: NotificationPayload) => { const { dataverseDisplayName, dataverseAlias, ...restNotification } = notification return { ...restNotification, @@ -25,6 +33,8 @@ export class NotificationsRepository extends ApiRepository implements INotificat ...(dataverseAlias && { collectionAlias: dataverseAlias }) } }) as Notification[] + const totalNotificationCount = response.data.totalCount + return { notifications, totalNotificationCount } }) .catch((error) => { throw error diff --git a/src/notifications/infra/transformers/NotificationPayload.ts b/src/notifications/infra/transformers/NotificationPayload.ts index 96d381ac..d63bde79 100644 --- a/src/notifications/infra/transformers/NotificationPayload.ts +++ b/src/notifications/infra/transformers/NotificationPayload.ts @@ -25,6 +25,6 @@ export interface NotificationPayload { dataFileId?: number dataFileDisplayName?: string currentCurationStatus?: string - additionalInfo?: string + additionalInfo?: Record objectDeleted?: boolean } diff --git a/src/templates/domain/dtos/CreateTemplateDTO.ts b/src/templates/domain/dtos/CreateTemplateDTO.ts new file mode 100644 index 00000000..871fee8b --- /dev/null +++ b/src/templates/domain/dtos/CreateTemplateDTO.ts @@ -0,0 +1,45 @@ +import { MetadataFieldTypeClass } from '../../../metadataBlocks/domain/models/MetadataBlock' + +export interface CreateTemplateDTO { + name: string + isDefault?: boolean + fields?: TemplateFieldDTO[] + instructions?: TemplateInstructionDTO[] +} + +export interface TemplateFieldDTO { + typeName: string + multiple: boolean + typeClass?: MetadataFieldTypeClass + value?: TemplateFieldValueDTO[] +} + +export interface TemplateFieldValueDTO { + [key: string]: + | TemplateFieldValuePrimitiveDTO + | TemplateFieldValueCompoundDTO + | TemplateFieldValueControlledVocabularyDTO +} + +export interface TemplateFieldValuePrimitiveDTO { + typeName: string + typeClass: MetadataFieldTypeClass.Primitive + value: string | string[] +} + +export interface TemplateFieldValueCompoundDTO { + typeName: string + typeClass: MetadataFieldTypeClass.Compound + value: TemplateFieldValueDTO[] +} + +export interface TemplateFieldValueControlledVocabularyDTO { + typeName: string + typeClass: MetadataFieldTypeClass.ControlledVocabulary + value: string +} + +export interface TemplateInstructionDTO { + instructionField: string + instructionText: string +} diff --git a/src/datasets/domain/models/DatasetTemplate.ts b/src/templates/domain/models/Template.ts similarity index 72% rename from src/datasets/domain/models/DatasetTemplate.ts rename to src/templates/domain/models/Template.ts index 9be71f23..b5b93b62 100644 --- a/src/datasets/domain/models/DatasetTemplate.ts +++ b/src/templates/domain/models/Template.ts @@ -1,7 +1,7 @@ -import { DatasetMetadataBlock, TermsOfUse } from './Dataset' +import { DatasetMetadataBlock, TermsOfUse } from '../../../datasets/domain/models/Dataset' import { License } from '../../../licenses/domain/models/License' -export interface DatasetTemplate { +export interface Template { id: number name: string collectionAlias: string @@ -11,13 +11,13 @@ export interface DatasetTemplate { createDate: string // 👇 From Edit Template Metadata datasetMetadataBlocks: DatasetMetadataBlock[] - instructions: DatasetTemplateInstruction[] + instructions: TemplateInstruction[] // 👇 From Edit Template Terms termsOfUse: TermsOfUse license?: License // This license property is going to be present if not custom terms are added in the UI } -export interface DatasetTemplateInstruction { +export interface TemplateInstruction { instructionField: string instructionText: string } diff --git a/src/templates/domain/repositories/ITemplatesRepository.ts b/src/templates/domain/repositories/ITemplatesRepository.ts new file mode 100644 index 00000000..7b043f0c --- /dev/null +++ b/src/templates/domain/repositories/ITemplatesRepository.ts @@ -0,0 +1,11 @@ +import { CreateTemplateDTO } from '../dtos/CreateTemplateDTO' +import { Template } from '../models/Template' + +export interface ITemplatesRepository { + createTemplate(collectionIdOrAlias: number | string, template: CreateTemplateDTO): Promise + getTemplate(templateId: number): Promise