Skip to content
Open
Show file tree
Hide file tree
Changes from all 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 = String(response.headers['content-type'] ?? '')
const content =
typeof response.data === 'string' ? response.data : JSON.stringify(response.data)

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