Skip to content
Merged
Show file tree
Hide file tree
Changes from 3 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ This changelog follows the principles of [Keep a Changelog](https://keepachangel
- Guestbooks: Added optional `includeStats` support to `getGuestbooksByCollectionId`, returning `usageCount` and `responseCount` when requested.
- Files: Added `getFileCitationByFormat` use case, repository method, and `FileCitationFormat` enum to support Dataverse file citation exports in `EndNote`, `RIS`, `BibTeX`, `CSL`, and `Internal` formats.
- Datasets: Added `getDatasetReviews` use case and repository method to support Dataverse endpoint `GET /datasets/{identifier}/reviews`, for retrieving review datasets associated with a dataset by persistent id or numeric id.
- Datasets: Added `exportDatasetMetadata` use case, repository method, and `ExportedDatasetMetadata` response type to support exporting dataset metadata by numeric id or persistent id through Dataverse endpoint `GET /datasets/export`.
- Collections: Added `allowedDatasetTypes` field to the [Collection](./src/collections/domain/models/Collection.ts) model. This field is optional and only populated the feature is enabled on the installation and configured on the collection.
- Collections: Added theme information when retrieving a collection using `getCollection`.

Expand Down
33 changes: 33 additions & 0 deletions docs/useCases.md
Original file line number Diff line number Diff line change
Expand Up @@ -46,6 +46,8 @@ The different use cases currently available in the package are classified below,
- [Get a Dataset](#get-a-dataset)
- [Get Dataset By Private URL Token](#get-dataset-by-private-url-token)
- [Get Dataset Citation Text](#get-dataset-citation-text)
- [Get Dataset Citation In Other Formats](#get-dataset-citation-in-other-formats)
- [Export Dataset Metadata](#export-dataset-metadata)
- [Get Dataset Citation Text By Private URL Token](#get-dataset-citation-text-by-private-url-token)
- [Get Dataset Locks](#get-dataset-locks)
- [Get Dataset Summary Field Names](#get-dataset-summary-field-names)
Expand Down Expand Up @@ -1053,6 +1055,37 @@ The `datasetId` parameter can be a string, for persistent identifiers, or a numb

There is an optional third parameter called `includeDeaccessioned`, which indicates whether to consider deaccessioned versions or not in the dataset search. If not set, the default value is `false`.

#### Export Dataset Metadata

Exports dataset metadata in a specified metadata export format.

##### Example call:

```typescript
import {
exportDatasetMetadata,
DatasetNotNumberedVersion,
ExportedDatasetMetadata
} from '@iqss/dataverse-client-javascript'

/* ... */

const datasetId = 'doi:10.77777/FK2/AAAAAA'
const exporter = 'ddi'

exportDatasetMetadata
.execute(datasetId, exporter, DatasetNotNumberedVersion.DRAFT)
.then((metadata: ExportedDatasetMetadata) => {
/* ... */
})

/* ... */
```

_See [use case](../src/datasets/domain/useCases/ExportDatasetMetadata.ts) implementation_.

The `datasetId` parameter can be a string for persistent identifiers or a number for numeric identifiers. The optional `version` parameter accepts `DatasetNotNumberedVersion.LATEST_PUBLISHED` or `DatasetNotNumberedVersion.DRAFT`. If not set, Dataverse defaults to `DatasetNotNumberedVersion.LATEST_PUBLISHED`. Draft exports require configured authentication with access to the draft.

#### Get Dataset Citation Text By Private URL Token

Returns the Dataset citation text, given an associated Private URL Token.
Expand Down
4 changes: 4 additions & 0 deletions src/datasets/domain/models/ExportedDatasetMetadata.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
export type ExportedDatasetMetadata = {
content: string
contentType: string
}
7 changes: 7 additions & 0 deletions src/datasets/domain/repositories/IDatasetsRepository.ts
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,8 @@ import { DatasetTypeDTO } from '../dtos/DatasetTypeDTO'
import { StorageDriver } from '../../../core/domain/models/StorageDriver'
import { DatasetUploadLimits } from '../models/DatasetUploadLimits'
import { DatasetReview } from '../models/DatasetReview'
import { ExportedDatasetMetadata } from '../models/ExportedDatasetMetadata'
import { DatasetNotNumberedVersion } from '../models/DatasetNotNumberedVersion'

export interface IDatasetsRepository {
getDataset(
Expand Down Expand Up @@ -86,6 +88,11 @@ export interface IDatasetsRepository {
format: CitationFormat,
includeDeaccessioned?: boolean
): Promise<FormattedCitation>
exportDatasetMetadata(
datasetId: number | string,
exporter: string,
version?: DatasetNotNumberedVersion.LATEST_PUBLISHED | DatasetNotNumberedVersion.DRAFT
): Promise<ExportedDatasetMetadata>
getDatasetAvailableDatasetTypes(): Promise<DatasetType[]>
getDatasetAvailableDatasetType(datasetTypeId: number | string): Promise<DatasetType>
addDatasetType(datasetType: DatasetTypeDTO): Promise<DatasetType>
Expand Down
28 changes: 28 additions & 0 deletions src/datasets/domain/useCases/ExportDatasetMetadata.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
import { UseCase } from '../../../core/domain/useCases/UseCase'
import { DatasetNotNumberedVersion } from '../models/DatasetNotNumberedVersion'
import { ExportedDatasetMetadata } from '../models/ExportedDatasetMetadata'
import { IDatasetsRepository } from '../repositories/IDatasetsRepository'

export class ExportDatasetMetadata implements UseCase<ExportedDatasetMetadata> {
private datasetsRepository: IDatasetsRepository

constructor(datasetsRepository: IDatasetsRepository) {
this.datasetsRepository = datasetsRepository
}

/**
* Exports dataset metadata in the specified metadata export format.
*
* @param {number | string} datasetId - The dataset identifier, which can be a string for persistent identifiers or a number for numeric identifiers.
* @param {string} exporter - The metadata exporter format name.
* @param {DatasetNotNumberedVersion.LATEST_PUBLISHED | DatasetNotNumberedVersion.DRAFT} [version] - The dataset version to export. If omitted, Dataverse defaults to :latest-published.
* @returns {Promise<ExportedDatasetMetadata>} The exported metadata content and content type.
*/
async execute(
datasetId: number | string,
exporter: string,
version?: DatasetNotNumberedVersion.LATEST_PUBLISHED | DatasetNotNumberedVersion.DRAFT
): Promise<ExportedDatasetMetadata> {
return await this.datasetsRepository.exportDatasetMetadata(datasetId, exporter, version)
}
}
6 changes: 5 additions & 1 deletion src/datasets/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,7 @@ import { UpdateDatasetLicense } from './domain/useCases/UpdateDatasetLicense'
import { GetDatasetStorageDriver } from './domain/useCases/GetDatasetStorageDriver'
import { GetDatasetUploadLimits } from './domain/useCases/GetDatasetUploadLimits'
import { GetDatasetReviews } from './domain/useCases/GetDatasetReviews'
import { ExportDatasetMetadata } from './domain/useCases/ExportDatasetMetadata'

const datasetsRepository = new DatasetsRepository()

Expand Down Expand Up @@ -88,6 +89,7 @@ const updateDatasetLicense = new UpdateDatasetLicense(datasetsRepository)
const getDatasetStorageDriver = new GetDatasetStorageDriver(datasetsRepository)
const getDatasetUploadLimits = new GetDatasetUploadLimits(datasetsRepository)
const getDatasetReviews = new GetDatasetReviews(datasetsRepository)
const exportDatasetMetadata = new ExportDatasetMetadata(datasetsRepository)

export {
getDataset,
Expand Down Expand Up @@ -121,9 +123,11 @@ export {
updateDatasetLicense,
getDatasetStorageDriver,
getDatasetUploadLimits,
getDatasetReviews
getDatasetReviews,
exportDatasetMetadata
}
export { DatasetNotNumberedVersion } from './domain/models/DatasetNotNumberedVersion'
export { ExportedDatasetMetadata } from './domain/models/ExportedDatasetMetadata'
export { DatasetUserPermissions } from './domain/models/DatasetUserPermissions'
export { DatasetLock, DatasetLockType } from './domain/models/DatasetLock'
export {
Expand Down
35 changes: 35 additions & 0 deletions src/datasets/infra/repositories/DatasetsRepository.ts
Original file line number Diff line number Diff line change
Expand Up @@ -26,13 +26,15 @@ import { transformDatasetLinkedCollectionsResponseToDatasetLinkedCollection } fr
import { FormattedCitation } from '../../domain/models/FormattedCitation'
import { DatasetType } from '../../domain/models/DatasetType'
import { TermsOfAccess } from '../../domain/models/Dataset'
import { DatasetNotNumberedVersion } from '../../domain/models/DatasetNotNumberedVersion'
import { transformTermsOfAccessToUpdatePayload } from './transformers/termsOfAccessTransformers'
import { DatasetLicenseUpdateRequest } from '../../domain/dtos/DatasetLicenseUpdateRequest'
import { DatasetTypeDTO } from '../../domain/dtos/DatasetTypeDTO'
import { StorageDriver } from '../../../core/domain/models/StorageDriver'
import { DatasetUploadLimits } from '../../domain/models/DatasetUploadLimits'
import { DatasetReview } from '../../domain/models/DatasetReview'
import { transformDatasetReviewsResponseToDatasetReviews } from './transformers/datasetReviewTransformers'
import { ExportedDatasetMetadata } from '../../domain/models/ExportedDatasetMetadata'

export interface GetAllDatasetPreviewsQueryParams {
per_page?: number
Expand Down Expand Up @@ -133,6 +135,39 @@ export class DatasetsRepository extends ApiRepository implements IDatasetsReposi
}
}

public async exportDatasetMetadata(
datasetId: number | string,
exporter: string,
version?: DatasetNotNumberedVersion.LATEST_PUBLISHED | DatasetNotNumberedVersion.DRAFT
): Promise<ExportedDatasetMetadata> {
const persistentId =
typeof datasetId === 'number'
? (
await this.getDataset(
datasetId,
version ?? DatasetNotNumberedVersion.LATEST_PUBLISHED,
false,
false
)
).persistentId
: datasetId

const response = await this.doGet('/datasets/export', true, {
exporter,
persistentId,
...(version && { version })
})

const contentType = response.headers['content-type']
const content =
typeof response.data === 'string' ? response.data : JSON.stringify(response.data)
Comment thread
Copilot marked this conversation as resolved.
Outdated

return {
content,
contentType
}
}

public async getPrivateUrlDatasetCitation(token: string): Promise<string> {
return this.doGet(
this.buildApiEndpoint(this.datasetsResourceName, `privateUrlDatasetVersion/${token}/citation`)
Expand Down
11 changes: 7 additions & 4 deletions test/functional/collections/UnlinkCollection.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import {
linkCollection,
deleteCollection,
getCollectionItems,
getCollectionLinks,
unlinkCollection
} from '../../../src'
import { TestConstants } from '../../testHelpers/TestConstants'
Expand Down Expand Up @@ -43,12 +44,14 @@ describe('execute', () => {
// Verify that the collections are linked
const collectionItemSubset = await getCollectionItems.execute(firstCollectionAlias)
expect(collectionItemSubset.items.length).toBe(1)
const collectionLinksBefore = await getCollectionLinks.execute(firstCollectionId)
expect(collectionLinksBefore.linkedCollections.map((collection) => collection.alias)).toContain(
secondCollectionAlias
)

await unlinkCollection.execute(secondCollectionAlias, firstCollectionAlias)
// Wait for the unlinking to be processed by Solr
await new Promise((resolve) => setTimeout(resolve, 5000))
const collectionItemSubset2 = await getCollectionItems.execute(firstCollectionAlias)
expect(collectionItemSubset2.items.length).toBe(0)
const collectionLinksAfter = await getCollectionLinks.execute(firstCollectionId)
expect(collectionLinksAfter.linkedCollections).toStrictEqual([])
})

test('should throw an error when unlinking a non-existent collection', async () => {
Expand Down
10 changes: 7 additions & 3 deletions test/integration/collections/CollectionsRepository.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2143,12 +2143,16 @@ describe('CollectionsRepository', () => {
const firstCollection = await sut.getCollection(firstCollectionAlias)
const secondCollection = await sut.getCollection(secondCollectionAlias)

const collectionLinksBefore = await sut.getCollectionLinks(firstCollection.id)
expect(
collectionLinksBefore.linkedCollections.map((collection) => collection.alias)
).toContain(secondCollection.alias)

await sut.unlinkCollection(secondCollection.id, firstCollection.id)
await new Promise((res) => setTimeout(res, 2000))

await sut.getCollection(secondCollectionAlias)
const collectionItemSubset = await sut.getCollectionItems(firstCollection.alias)
expect(collectionItemSubset.items).toStrictEqual([])
const collectionLinksAfter = await sut.getCollectionLinks(firstCollection.id)
expect(collectionLinksAfter.linkedCollections).toStrictEqual([])
})

test('should throw error when unlinking a non-existent collection', async () => {
Expand Down
37 changes: 37 additions & 0 deletions test/integration/datasets/DatasetsRepository.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -772,6 +772,43 @@ describe('DatasetsRepository', () => {
})
})

describe('exportDatasetMetadata', () => {
let draftDatasetIds: CreatedDatasetIdentifiers
let publishedDatasetIds: CreatedDatasetIdentifiers

beforeAll(async () => {
draftDatasetIds = await createDataset.execute(TestConstants.TEST_NEW_DATASET_DTO)
publishedDatasetIds = await createDataset.execute(TestConstants.TEST_NEW_DATASET_DTO)
await publishDatasetViaApi(publishedDatasetIds.numericId)
await waitForNoLocks(publishedDatasetIds.numericId, 10)
})

afterAll(async () => {
await deleteUnpublishedDatasetViaApi(draftDatasetIds.numericId)
await deletePublishedDatasetViaApi(publishedDatasetIds.persistentId)
})

test('should export latest published dataset metadata in DDI format by default', async () => {
const metadata = await sut.exportDatasetMetadata(publishedDatasetIds.persistentId, 'ddi')

expect(typeof metadata.content).toBe('string')
expect(metadata.content.length).toBeGreaterThan(0)
expect(metadata.contentType).toMatch(/xml/)
})

test('should export draft dataset metadata in DDI format', async () => {
const metadata = await sut.exportDatasetMetadata(
draftDatasetIds.numericId,
'ddi',
DatasetNotNumberedVersion.DRAFT
)

expect(typeof metadata.content).toBe('string')
expect(metadata.content.length).toBeGreaterThan(0)
expect(metadata.contentType).toMatch(/xml/)
})
})

describe('getDatasetVersionDiff', () => {
let testDatasetIds: CreatedDatasetIdentifiers

Expand Down
42 changes: 42 additions & 0 deletions test/unit/datasets/ExportDatasetMetadata.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
import { ExportDatasetMetadata } from '../../../src/datasets/domain/useCases/ExportDatasetMetadata'
import { IDatasetsRepository } from '../../../src/datasets/domain/repositories/IDatasetsRepository'
import { ReadError } from '../../../src/core/domain/repositories/ReadError'
import { ExportedDatasetMetadata } from '../../../src/datasets/domain/models/ExportedDatasetMetadata'
import { DatasetNotNumberedVersion } from '../../../src/datasets/domain/models/DatasetNotNumberedVersion'

describe('ExportDatasetMetadata.execute', () => {
const testDatasetId = 1
const testExporter = 'ddi'

test('should return exported dataset metadata on repository success', async () => {
const expectedMetadata: ExportedDatasetMetadata = {
content: '<codeBook></codeBook>',
contentType: 'application/xml'
}

const datasetsRepositoryStub: IDatasetsRepository = {
exportDatasetMetadata: jest.fn().mockResolvedValue(expectedMetadata)
} as unknown as IDatasetsRepository

const sut = new ExportDatasetMetadata(datasetsRepositoryStub)

const actual = await sut.execute(testDatasetId, testExporter, DatasetNotNumberedVersion.DRAFT)

expect(actual).toEqual(expectedMetadata)
expect(datasetsRepositoryStub.exportDatasetMetadata).toHaveBeenCalledWith(
testDatasetId,
testExporter,
DatasetNotNumberedVersion.DRAFT
)
})

test('should throw ReadError on repository failure', async () => {
const datasetsRepositoryStub: IDatasetsRepository = {
exportDatasetMetadata: jest.fn().mockRejectedValue(new ReadError())
} as unknown as IDatasetsRepository

const sut = new ExportDatasetMetadata(datasetsRepositoryStub)

await expect(sut.execute(testDatasetId, testExporter)).rejects.toThrow(ReadError)
})
})
Loading