Skip to content

Commit 1da9ccb

Browse files
committed
feat: export metadata use case
1 parent 86e2619 commit 1da9ccb

9 files changed

Lines changed: 205 additions & 2 deletions

File tree

CHANGELOG.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@ This changelog follows the principles of [Keep a Changelog](https://keepachangel
1414
- Guestbooks: Added optional `includeStats` support to `getGuestbooksByCollectionId`, returning `usageCount` and `responseCount` when requested.
1515
- Files: Added `getFileCitationByFormat` use case, repository method, and `FileCitationFormat` enum to support Dataverse file citation exports in `EndNote`, `RIS`, `BibTeX`, `CSL`, and `Internal` formats.
1616
- 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.
17+
- Datasets: Added `exportDatasetMetadata` use case, repository method, `ExportedDatasetMetadata` response type, and `DatasetMetadataExportVersion` enum to support exporting dataset metadata by numeric id or persistent id through Dataverse endpoint `GET /datasets/export`.
1718
- 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.
1819
- Collections: Added theme information when retrieving a collection using `getCollection`.
1920

docs/useCases.md

Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -46,6 +46,8 @@ The different use cases currently available in the package are classified below,
4646
- [Get a Dataset](#get-a-dataset)
4747
- [Get Dataset By Private URL Token](#get-dataset-by-private-url-token)
4848
- [Get Dataset Citation Text](#get-dataset-citation-text)
49+
- [Get Dataset Citation In Other Formats](#get-dataset-citation-in-other-formats)
50+
- [Export Dataset Metadata](#export-dataset-metadata)
4951
- [Get Dataset Citation Text By Private URL Token](#get-dataset-citation-text-by-private-url-token)
5052
- [Get Dataset Locks](#get-dataset-locks)
5153
- [Get Dataset Summary Field Names](#get-dataset-summary-field-names)
@@ -1053,6 +1055,35 @@ The `datasetId` parameter can be a string, for persistent identifiers, or a numb
10531055

10541056
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`.
10551057

1058+
#### Export Dataset Metadata
1059+
1060+
Exports dataset metadata in a specified metadata export format.
1061+
1062+
##### Example call:
1063+
1064+
```typescript
1065+
import {
1066+
exportDatasetMetadata,
1067+
DatasetMetadataExportVersion,
1068+
ExportedDatasetMetadata
1069+
} from '@iqss/dataverse-client-javascript'
1070+
1071+
/* ... */
1072+
1073+
const datasetId = 'doi:10.77777/FK2/AAAAAA'
1074+
const exporter = 'ddi'
1075+
1076+
exportDatasetMetadata
1077+
.execute(datasetId, exporter, DatasetMetadataExportVersion.DRAFT)
1078+
.then((metadata: ExportedDatasetMetadata) => {
1079+
/* ... */
1080+
})
1081+
1082+
/* ... */
1083+
```
1084+
1085+
_See [use case](../src/datasets/domain/useCases/ExportDatasetMetadata.ts) implementation_.
1086+
10561087
#### Get Dataset Citation Text By Private URL Token
10571088

10581089
Returns the Dataset citation text, given an associated Private URL Token.
Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
export type ExportedDatasetMetadata = {
2+
content: string
3+
contentType: string
4+
}
5+
6+
export enum DatasetMetadataExportVersion {
7+
LATEST_PUBLISHED = ':latest-published',
8+
DRAFT = ':draft'
9+
}

src/datasets/domain/repositories/IDatasetsRepository.ts

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,8 @@ import { DatasetTypeDTO } from '../dtos/DatasetTypeDTO'
1919
import { StorageDriver } from '../../../core/domain/models/StorageDriver'
2020
import { DatasetUploadLimits } from '../models/DatasetUploadLimits'
2121
import { DatasetReview } from '../models/DatasetReview'
22+
import { ExportedDatasetMetadata } from '../models/ExportedDatasetMetadata'
23+
import { DatasetMetadataExportVersion } from '../models/ExportedDatasetMetadata'
2224

2325
export interface IDatasetsRepository {
2426
getDataset(
@@ -86,6 +88,11 @@ export interface IDatasetsRepository {
8688
format: CitationFormat,
8789
includeDeaccessioned?: boolean
8890
): Promise<FormattedCitation>
91+
exportDatasetMetadata(
92+
datasetId: number | string,
93+
exporter: string,
94+
version?: DatasetMetadataExportVersion
95+
): Promise<ExportedDatasetMetadata>
8996
getDatasetAvailableDatasetTypes(): Promise<DatasetType[]>
9097
getDatasetAvailableDatasetType(datasetTypeId: number | string): Promise<DatasetType>
9198
addDatasetType(datasetType: DatasetTypeDTO): Promise<DatasetType>
Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,28 @@
1+
import { UseCase } from '../../../core/domain/useCases/UseCase'
2+
import { DatasetMetadataExportVersion } from '../models/ExportedDatasetMetadata'
3+
import { ExportedDatasetMetadata } from '../models/ExportedDatasetMetadata'
4+
import { IDatasetsRepository } from '../repositories/IDatasetsRepository'
5+
6+
export class ExportDatasetMetadata implements UseCase<ExportedDatasetMetadata> {
7+
private datasetsRepository: IDatasetsRepository
8+
9+
constructor(datasetsRepository: IDatasetsRepository) {
10+
this.datasetsRepository = datasetsRepository
11+
}
12+
13+
/**
14+
* Exports dataset metadata in the specified metadata export format.
15+
*
16+
* @param {number | string} datasetId - The dataset identifier, which can be a string for persistent identifiers or a number for numeric identifiers.
17+
* @param {string} exporter - The metadata exporter format name.
18+
* @param {DatasetMetadataExportVersion} [version] - The dataset version to export. If omitted, Dataverse defaults to :latest-published.
19+
* @returns {Promise<ExportedDatasetMetadata>} The exported metadata content and content type.
20+
*/
21+
async execute(
22+
datasetId: number | string,
23+
exporter: string,
24+
version?: DatasetMetadataExportVersion
25+
): Promise<ExportedDatasetMetadata> {
26+
return await this.datasetsRepository.exportDatasetMetadata(datasetId, exporter, version)
27+
}
28+
}

src/datasets/index.ts

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -36,6 +36,7 @@ import { UpdateDatasetLicense } from './domain/useCases/UpdateDatasetLicense'
3636
import { GetDatasetStorageDriver } from './domain/useCases/GetDatasetStorageDriver'
3737
import { GetDatasetUploadLimits } from './domain/useCases/GetDatasetUploadLimits'
3838
import { GetDatasetReviews } from './domain/useCases/GetDatasetReviews'
39+
import { ExportDatasetMetadata } from './domain/useCases/ExportDatasetMetadata'
3940

4041
const datasetsRepository = new DatasetsRepository()
4142

@@ -88,6 +89,7 @@ const updateDatasetLicense = new UpdateDatasetLicense(datasetsRepository)
8889
const getDatasetStorageDriver = new GetDatasetStorageDriver(datasetsRepository)
8990
const getDatasetUploadLimits = new GetDatasetUploadLimits(datasetsRepository)
9091
const getDatasetReviews = new GetDatasetReviews(datasetsRepository)
92+
const exportDatasetMetadata = new ExportDatasetMetadata(datasetsRepository)
9193

9294
export {
9395
getDataset,
@@ -121,9 +123,14 @@ export {
121123
updateDatasetLicense,
122124
getDatasetStorageDriver,
123125
getDatasetUploadLimits,
124-
getDatasetReviews
126+
getDatasetReviews,
127+
exportDatasetMetadata
125128
}
126129
export { DatasetNotNumberedVersion } from './domain/models/DatasetNotNumberedVersion'
130+
export {
131+
DatasetMetadataExportVersion,
132+
ExportedDatasetMetadata
133+
} from './domain/models/ExportedDatasetMetadata'
127134
export { DatasetUserPermissions } from './domain/models/DatasetUserPermissions'
128135
export { DatasetLock, DatasetLockType } from './domain/models/DatasetLock'
129136
export {

src/datasets/infra/repositories/DatasetsRepository.ts

Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -26,13 +26,16 @@ import { transformDatasetLinkedCollectionsResponseToDatasetLinkedCollection } fr
2626
import { FormattedCitation } from '../../domain/models/FormattedCitation'
2727
import { DatasetType } from '../../domain/models/DatasetType'
2828
import { TermsOfAccess } from '../../domain/models/Dataset'
29+
import { DatasetNotNumberedVersion } from '../../domain/models/DatasetNotNumberedVersion'
2930
import { transformTermsOfAccessToUpdatePayload } from './transformers/termsOfAccessTransformers'
3031
import { DatasetLicenseUpdateRequest } from '../../domain/dtos/DatasetLicenseUpdateRequest'
3132
import { DatasetTypeDTO } from '../../domain/dtos/DatasetTypeDTO'
3233
import { StorageDriver } from '../../../core/domain/models/StorageDriver'
3334
import { DatasetUploadLimits } from '../../domain/models/DatasetUploadLimits'
3435
import { DatasetReview } from '../../domain/models/DatasetReview'
3536
import { transformDatasetReviewsResponseToDatasetReviews } from './transformers/datasetReviewTransformers'
37+
import { ExportedDatasetMetadata } from '../../domain/models/ExportedDatasetMetadata'
38+
import { DatasetMetadataExportVersion } from '../../domain/models/ExportedDatasetMetadata'
3639

3740
export interface GetAllDatasetPreviewsQueryParams {
3841
per_page?: number
@@ -133,6 +136,39 @@ export class DatasetsRepository extends ApiRepository implements IDatasetsReposi
133136
}
134137
}
135138

139+
public async exportDatasetMetadata(
140+
datasetId: number | string,
141+
exporter: string,
142+
version?: DatasetMetadataExportVersion
143+
): Promise<ExportedDatasetMetadata> {
144+
const persistentId =
145+
typeof datasetId === 'number'
146+
? (
147+
await this.getDataset(
148+
datasetId,
149+
version ?? DatasetNotNumberedVersion.LATEST_PUBLISHED,
150+
false,
151+
false
152+
)
153+
).persistentId
154+
: datasetId
155+
156+
const response = await this.doGet('/datasets/export', true, {
157+
exporter,
158+
persistentId,
159+
...(version && { version })
160+
})
161+
162+
const contentType = response.headers['content-type']
163+
const content =
164+
typeof response.data === 'string' ? response.data : JSON.stringify(response.data)
165+
166+
return {
167+
content,
168+
contentType
169+
}
170+
}
171+
136172
public async getPrivateUrlDatasetCitation(token: string): Promise<string> {
137173
return this.doGet(
138174
this.buildApiEndpoint(this.datasetsResourceName, `privateUrlDatasetVersion/${token}/citation`)

test/integration/datasets/DatasetsRepository.test.ts

Lines changed: 39 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -35,7 +35,8 @@ import {
3535
updateTermsOfAccess,
3636
DatasetLicenseUpdateRequest,
3737
getDatasetReviews,
38-
DatasetReview
38+
DatasetReview,
39+
DatasetMetadataExportVersion
3940
} from '../../../src/datasets'
4041
import { ApiConfig, WriteError } from '../../../src'
4142
import { DataverseApiAuthMechanism } from '../../../src/core/infra/repositories/ApiConfig'
@@ -772,6 +773,43 @@ describe('DatasetsRepository', () => {
772773
})
773774
})
774775

776+
describe('exportDatasetMetadata', () => {
777+
let draftDatasetIds: CreatedDatasetIdentifiers
778+
let publishedDatasetIds: CreatedDatasetIdentifiers
779+
780+
beforeAll(async () => {
781+
draftDatasetIds = await createDataset.execute(TestConstants.TEST_NEW_DATASET_DTO)
782+
publishedDatasetIds = await createDataset.execute(TestConstants.TEST_NEW_DATASET_DTO)
783+
await publishDatasetViaApi(publishedDatasetIds.numericId)
784+
await waitForNoLocks(publishedDatasetIds.numericId, 10)
785+
})
786+
787+
afterAll(async () => {
788+
await deleteUnpublishedDatasetViaApi(draftDatasetIds.numericId)
789+
await deletePublishedDatasetViaApi(publishedDatasetIds.persistentId)
790+
})
791+
792+
test('should export latest published dataset metadata in DDI format by default', async () => {
793+
const metadata = await sut.exportDatasetMetadata(publishedDatasetIds.persistentId, 'ddi')
794+
795+
expect(typeof metadata.content).toBe('string')
796+
expect(metadata.content.length).toBeGreaterThan(0)
797+
expect(metadata.contentType).toMatch(/xml/)
798+
})
799+
800+
test('should export draft dataset metadata in DDI format', async () => {
801+
const metadata = await sut.exportDatasetMetadata(
802+
draftDatasetIds.numericId,
803+
'ddi',
804+
DatasetMetadataExportVersion.DRAFT
805+
)
806+
807+
expect(typeof metadata.content).toBe('string')
808+
expect(metadata.content.length).toBeGreaterThan(0)
809+
expect(metadata.contentType).toMatch(/xml/)
810+
})
811+
})
812+
775813
describe('getDatasetVersionDiff', () => {
776814
let testDatasetIds: CreatedDatasetIdentifiers
777815

Lines changed: 46 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,46 @@
1+
import { ExportDatasetMetadata } from '../../../src/datasets/domain/useCases/ExportDatasetMetadata'
2+
import { IDatasetsRepository } from '../../../src/datasets/domain/repositories/IDatasetsRepository'
3+
import { ReadError } from '../../../src/core/domain/repositories/ReadError'
4+
import { ExportedDatasetMetadata } from '../../../src/datasets/domain/models/ExportedDatasetMetadata'
5+
import { DatasetMetadataExportVersion } from '../../../src/datasets/domain/models/ExportedDatasetMetadata'
6+
7+
describe('ExportDatasetMetadata.execute', () => {
8+
const testDatasetId = 1
9+
const testExporter = 'ddi'
10+
11+
test('should return exported dataset metadata on repository success', async () => {
12+
const expectedMetadata: ExportedDatasetMetadata = {
13+
content: '<codeBook></codeBook>',
14+
contentType: 'application/xml'
15+
}
16+
17+
const datasetsRepositoryStub: IDatasetsRepository = {
18+
exportDatasetMetadata: jest.fn().mockResolvedValue(expectedMetadata)
19+
} as unknown as IDatasetsRepository
20+
21+
const sut = new ExportDatasetMetadata(datasetsRepositoryStub)
22+
23+
const actual = await sut.execute(
24+
testDatasetId,
25+
testExporter,
26+
DatasetMetadataExportVersion.DRAFT
27+
)
28+
29+
expect(actual).toEqual(expectedMetadata)
30+
expect(datasetsRepositoryStub.exportDatasetMetadata).toHaveBeenCalledWith(
31+
testDatasetId,
32+
testExporter,
33+
DatasetMetadataExportVersion.DRAFT
34+
)
35+
})
36+
37+
test('should throw ReadError on repository failure', async () => {
38+
const datasetsRepositoryStub: IDatasetsRepository = {
39+
exportDatasetMetadata: jest.fn().mockRejectedValue(new ReadError())
40+
} as unknown as IDatasetsRepository
41+
42+
const sut = new ExportDatasetMetadata(datasetsRepositoryStub)
43+
44+
await expect(sut.execute(testDatasetId, testExporter)).rejects.toThrow(ReadError)
45+
})
46+
})

0 commit comments

Comments
 (0)