Skip to content

Commit 8c14fdf

Browse files
authored
Merge pull request #340 from IQSS/Citation-in-other-format
Get Citation In Other Format Use Case
2 parents 97cba99 + a957946 commit 8c14fdf

File tree

10 files changed

+267
-6
lines changed

10 files changed

+267
-6
lines changed

docs/useCases.md

Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -564,6 +564,37 @@ The `datasetId` parameter can be a string, for persistent identifiers, or a numb
564564

565565
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`.
566566

567+
#### Get Dataset Citation In Other Formats
568+
569+
Retrieves the citation for a dataset in a specified bibliographic format.
570+
571+
##### Example call:
572+
573+
```typescript
574+
import { getDatasetCitationInOtherFormats } from '@iqss/dataverse-client-javascript'
575+
576+
/* ... */
577+
578+
const datasetId = 2
579+
const datasetVersionId = '1.0'
580+
581+
getDatasetCitationInOtherFormats
582+
.execute(datasetId, datasetVersionId, format)
583+
.then((citationText: FormattedCitation) => {
584+
/* ... */
585+
})
586+
587+
/* ... */
588+
```
589+
590+
_See [use case](../src/datasets/domain/useCases/GetDatasetCitationInOtherFormats.ts) implementation_.
591+
592+
Supported formats include 'EndNote' (XML), 'RIS' (plain text), 'BibTeX' (plain text), 'CSLJson' (JSON), and 'Internal' (HTML). The response contains the raw citation content in the requested format, the format type, and the content type (MIME type).
593+
594+
The `datasetId` parameter can be a string, for persistent identifiers, or a number, for numeric identifiers.
595+
596+
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`.
597+
567598
#### Get Dataset Citation Text By Private URL Token
568599

569600
Returns the Dataset citation text, given an associated Private URL Token.
Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
export enum CitationFormat {
2+
Internal = 'Internal',
3+
EndNote = 'EndNote',
4+
RIS = 'RIS',
5+
BibTeX = 'BibTeX',
6+
CSLJson = 'CSL'
7+
}
Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
export type FormattedCitation = {
2+
content: string
3+
contentType: string
4+
}

src/datasets/domain/repositories/IDatasetsRepository.ts

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,8 @@ import { DatasetVersionDiff } from '../models/DatasetVersionDiff'
1010
import { DatasetDownloadCount } from '../models/DatasetDownloadCount'
1111
import { DatasetVersionSummaryInfo } from '../models/DatasetVersionSummaryInfo'
1212
import { DatasetLinkedCollection } from '../models/DatasetLinkedCollection'
13+
import { CitationFormat } from '../models/CitationFormat'
14+
import { FormattedCitation } from '../models/FormattedCitation'
1315

1416
export interface IDatasetsRepository {
1517
getDataset(
@@ -65,4 +67,10 @@ export interface IDatasetsRepository {
6567
linkDataset(datasetId: number, collectionAlias: string): Promise<void>
6668
unlinkDataset(datasetId: number, collectionAlias: string): Promise<void>
6769
getDatasetLinkedCollections(datasetId: number | string): Promise<DatasetLinkedCollection[]>
70+
getDatasetCitationInOtherFormats(
71+
datasetId: number | string,
72+
datasetVersionId: string,
73+
format: CitationFormat,
74+
includeDeaccessioned?: boolean
75+
): Promise<FormattedCitation>
6876
}
Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,36 @@
1+
import { UseCase } from '../../../core/domain/useCases/UseCase'
2+
import { IDatasetsRepository } from '../repositories/IDatasetsRepository'
3+
import { DatasetNotNumberedVersion } from '../models/DatasetNotNumberedVersion'
4+
import { FormattedCitation } from '../models/FormattedCitation'
5+
import { CitationFormat } from '../models/CitationFormat'
6+
7+
export class GetDatasetCitationInOtherFormats implements UseCase<FormattedCitation> {
8+
private datasetsRepository: IDatasetsRepository
9+
10+
constructor(datasetsRepository: IDatasetsRepository) {
11+
this.datasetsRepository = datasetsRepository
12+
}
13+
14+
/**
15+
* Returns the dataset citation in the specified format.
16+
*
17+
* @param {number | string} datasetId - The dataset identifier.
18+
* @param {string | DatasetNotNumberedVersion} [datasetVersionId=DatasetNotNumberedVersion.LATEST] - The dataset version identifier, which can be a version-specific string (e.g., '1.0') or a DatasetNotNumberedVersion enum value. Defaults to LATEST.
19+
* @param {CitationFormat} format - The citation format to return. One of: 'EndNote', 'RIS', 'BibTeX', 'CSLJson', 'Internal'.
20+
* @param {boolean} [includeDeaccessioned=false] - Whether to include deaccessioned versions in the search. Defaults to false.
21+
* @returns {Promise<FormattedCitation>} The citation content, format, and content type.
22+
*/
23+
async execute(
24+
datasetId: number | string,
25+
datasetVersionId: string | DatasetNotNumberedVersion = DatasetNotNumberedVersion.LATEST,
26+
format: CitationFormat,
27+
includeDeaccessioned = false
28+
): Promise<FormattedCitation> {
29+
return await this.datasetsRepository.getDatasetCitationInOtherFormats(
30+
datasetId,
31+
datasetVersionId,
32+
format,
33+
includeDeaccessioned
34+
)
35+
}
36+
}

src/datasets/index.ts

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,7 @@ import { DeleteDatasetDraft } from './domain/useCases/DeleteDatasetDraft'
2323
import { LinkDataset } from './domain/useCases/LinkDataset'
2424
import { UnlinkDataset } from './domain/useCases/UnlinkDataset'
2525
import { GetDatasetLinkedCollections } from './domain/useCases/GetDatasetLinkedCollections'
26+
import { GetDatasetCitationInOtherFormats } from './domain/useCases/GetDatasetCitationInOtherFormats'
2627

2728
const datasetsRepository = new DatasetsRepository()
2829

@@ -60,6 +61,7 @@ const deleteDatasetDraft = new DeleteDatasetDraft(datasetsRepository)
6061
const linkDataset = new LinkDataset(datasetsRepository)
6162
const unlinkDataset = new UnlinkDataset(datasetsRepository)
6263
const getDatasetLinkedCollections = new GetDatasetLinkedCollections(datasetsRepository)
64+
const getDatasetCitationInOtherFormats = new GetDatasetCitationInOtherFormats(datasetsRepository)
6365

6466
export {
6567
getDataset,
@@ -80,7 +82,8 @@ export {
8082
deleteDatasetDraft,
8183
linkDataset,
8284
unlinkDataset,
83-
getDatasetLinkedCollections
85+
getDatasetLinkedCollections,
86+
getDatasetCitationInOtherFormats
8487
}
8588
export { DatasetNotNumberedVersion } from './domain/models/DatasetNotNumberedVersion'
8689
export { DatasetUserPermissions } from './domain/models/DatasetUserPermissions'

src/datasets/infra/repositories/DatasetsRepository.ts

Lines changed: 30 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -21,7 +21,9 @@ import { transformDatasetVersionDiffResponseToDatasetVersionDiff } from './trans
2121
import { DatasetDownloadCount } from '../../domain/models/DatasetDownloadCount'
2222
import { DatasetVersionSummaryInfo } from '../../domain/models/DatasetVersionSummaryInfo'
2323
import { DatasetLinkedCollection } from '../../domain/models/DatasetLinkedCollection'
24+
import { CitationFormat } from '../../domain/models/CitationFormat'
2425
import { transformDatasetLinkedCollectionsResponseToDatasetLinkedCollection } from './transformers/datasetLinkedCollectionsTransformers'
26+
import { FormattedCitation } from '../../domain/models/FormattedCitation'
2527

2628
export interface GetAllDatasetPreviewsQueryParams {
2729
per_page?: number
@@ -76,7 +78,7 @@ export class DatasetsRepository extends ApiRepository implements IDatasetsReposi
7678
}
7779

7880
public async getDatasetCitation(
79-
datasetId: number,
81+
datasetId: number | string,
8082
datasetVersionId: string,
8183
includeDeaccessioned: boolean
8284
): Promise<string> {
@@ -95,6 +97,33 @@ export class DatasetsRepository extends ApiRepository implements IDatasetsReposi
9597
})
9698
}
9799

100+
public async getDatasetCitationInOtherFormats(
101+
datasetId: number | string,
102+
datasetVersionId: string | 'LATEST' = 'LATEST',
103+
format: CitationFormat,
104+
includeDeaccessioned = false
105+
): Promise<FormattedCitation> {
106+
const endpoint = this.buildApiEndpoint(
107+
this.datasetsResourceName,
108+
`versions/${datasetVersionId}/citation/${format}`,
109+
datasetId
110+
)
111+
const response = await this.doGet(endpoint, true, { includeDeaccessioned })
112+
113+
const contentType = response.headers['content-type']
114+
let content: string
115+
if (contentType && contentType.includes('application/json')) {
116+
content = JSON.stringify(response.data)
117+
} else {
118+
content = response.data
119+
}
120+
121+
return {
122+
content,
123+
contentType
124+
}
125+
}
126+
98127
public async getPrivateUrlDatasetCitation(token: string): Promise<string> {
99128
return this.doGet(
100129
this.buildApiEndpoint(this.datasetsResourceName, `privateUrlDatasetVersion/${token}/citation`)

test/integration/datasets/DatasetsRepository.test.ts

Lines changed: 108 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -51,6 +51,7 @@ import {
5151
import { FilesRepository } from '../../../src/files/infra/repositories/FilesRepository'
5252
import { DirectUploadClient } from '../../../src/files/infra/clients/DirectUploadClient'
5353
import { createTestFileUploadDestination } from '../../testHelpers/files/fileUploadDestinationHelper'
54+
import { CitationFormat } from '../../../src/datasets/domain/models/CitationFormat'
5455

5556
const TEST_DIFF_DATASET_DTO: DatasetDTO = {
5657
license: {
@@ -492,6 +493,113 @@ describe('DatasetsRepository', () => {
492493
})
493494
})
494495

496+
describe('getDatasetCitationInOtherFormats', () => {
497+
let testDatasetIds: CreatedDatasetIdentifiers
498+
499+
beforeAll(async () => {
500+
testDatasetIds = await createDataset.execute(TestConstants.TEST_NEW_DATASET_DTO)
501+
})
502+
503+
afterAll(async () => {
504+
await deletePublishedDatasetViaApi(testDatasetIds.persistentId)
505+
})
506+
507+
test('should return citation in BibTeX format', async () => {
508+
const citation = await sut.getDatasetCitationInOtherFormats(
509+
testDatasetIds.numericId,
510+
DatasetNotNumberedVersion.LATEST,
511+
CitationFormat.BibTeX
512+
)
513+
514+
expect(typeof citation.content).toBe('string')
515+
expect(citation.contentType).toMatch(/text\/plain/)
516+
})
517+
518+
test('should return citation in BibTeX format using persistent id', async () => {
519+
const citation = await sut.getDatasetCitationInOtherFormats(
520+
testDatasetIds.persistentId,
521+
DatasetNotNumberedVersion.LATEST,
522+
CitationFormat.BibTeX
523+
)
524+
525+
expect(typeof citation.content).toBe('string')
526+
expect(citation.contentType).toMatch(/text\/plain/)
527+
})
528+
529+
test('should return citation in RIS format', async () => {
530+
const citation = await sut.getDatasetCitationInOtherFormats(
531+
testDatasetIds.numericId,
532+
DatasetNotNumberedVersion.LATEST,
533+
CitationFormat.RIS
534+
)
535+
536+
expect(typeof citation.content).toBe('string')
537+
expect(citation.contentType).toMatch(/text\/plain/)
538+
})
539+
540+
test('should return citation in CSLJson format', async () => {
541+
const citation = await sut.getDatasetCitationInOtherFormats(
542+
testDatasetIds.numericId,
543+
DatasetNotNumberedVersion.LATEST,
544+
CitationFormat.CSLJson
545+
)
546+
547+
expect(typeof citation.content).toBe('string')
548+
expect(citation.contentType).toMatch(/application\/json/)
549+
})
550+
551+
test('should return citation in EndNote format', async () => {
552+
const citation = await sut.getDatasetCitationInOtherFormats(
553+
testDatasetIds.numericId,
554+
DatasetNotNumberedVersion.LATEST,
555+
CitationFormat.EndNote
556+
)
557+
558+
expect(typeof citation.content).toBe('string')
559+
expect(citation.contentType).toMatch(/text\/xml/)
560+
})
561+
562+
test('should return citation in Internal format', async () => {
563+
const citation = await sut.getDatasetCitationInOtherFormats(
564+
testDatasetIds.numericId,
565+
DatasetNotNumberedVersion.LATEST,
566+
CitationFormat.Internal
567+
)
568+
569+
expect(typeof citation.content).toBe('string')
570+
expect(citation.contentType).toMatch(/text\/html/)
571+
})
572+
573+
test('should return error when dataset does not exist', async () => {
574+
const nonExistentId = 9999999
575+
const expectedError = new ReadError(`[404] Dataset with ID ${nonExistentId} not found.`)
576+
577+
await expect(
578+
sut.getDatasetCitationInOtherFormats(
579+
nonExistentId,
580+
DatasetNotNumberedVersion.LATEST,
581+
CitationFormat.RIS
582+
)
583+
).rejects.toThrow(expectedError)
584+
})
585+
586+
test('should return citation for deaccessioned dataset when includeDeaccessioned = true', async () => {
587+
await publishDatasetViaApi(testDatasetIds.numericId)
588+
await waitForNoLocks(testDatasetIds.numericId, 10)
589+
await deaccessionDatasetViaApi(testDatasetIds.numericId, '1.0')
590+
591+
const citation = await sut.getDatasetCitationInOtherFormats(
592+
testDatasetIds.numericId,
593+
DatasetNotNumberedVersion.LATEST,
594+
CitationFormat.RIS,
595+
true
596+
)
597+
598+
expect(typeof citation.content).toBe('string')
599+
expect(citation.contentType).toMatch(/text\/plain/)
600+
})
601+
})
602+
495603
describe('getDatasetVersionDiff', () => {
496604
let testDatasetIds: CreatedDatasetIdentifiers
497605

test/testHelpers/roles/roleHelper.ts

Lines changed: 1 addition & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -42,9 +42,7 @@ export const createSuperAdminRoleArray = (): Role[] => {
4242
'ManageDatasetPermissions',
4343
'ManageFilePermissions',
4444
'PublishDataverse',
45-
'LinkDataverse',
4645
'PublishDataset',
47-
'LinkDataset',
4846
'DeleteDataverse',
4947
'DeleteDatasetDraft'
5048
],
@@ -101,11 +99,10 @@ export const createSuperAdminRoleArray = (): Role[] => {
10199
'ManageDatasetPermissions',
102100
'ManageFilePermissions',
103101
'PublishDataset',
104-
'LinkDataset',
105102
'DeleteDatasetDraft'
106103
],
107104
description:
108-
'For datasets, a person who can edit License + Terms, edit Permissions, and publish and link datasets.',
105+
'For datasets, a person who can edit License + Terms, edit Permissions, and publish datasets.',
109106
id: 7
110107
},
111108
{
Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,38 @@
1+
import { GetDatasetCitationInOtherFormats } from '../../../src/datasets/domain/useCases/GetDatasetCitationInOtherFormats'
2+
import { IDatasetsRepository } from '../../../src/datasets/domain/repositories/IDatasetsRepository'
3+
import { ReadError } from '../../../src/core/domain/repositories/ReadError'
4+
import { CitationFormat } from '../../../src/datasets/domain/models/CitationFormat'
5+
import { DatasetNotNumberedVersion } from '../../../src/datasets/domain/models/DatasetNotNumberedVersion'
6+
import { FormattedCitation } from '../../../src/datasets/domain/models/FormattedCitation'
7+
8+
describe('GetDatasetCitationInOtherFormats.execute', () => {
9+
const testDatasetId = 1
10+
const testFormat: CitationFormat = CitationFormat.BibTeX
11+
const testVersion: DatasetNotNumberedVersion = DatasetNotNumberedVersion.LATEST
12+
13+
test('should return citation response on repository success', async () => {
14+
const expectedCitation: FormattedCitation = {
15+
content: '@data{example, ...}',
16+
contentType: 'text/plain'
17+
}
18+
19+
const datasetsRepositoryStub: IDatasetsRepository = {
20+
getDatasetCitationInOtherFormats: jest.fn().mockResolvedValue(expectedCitation)
21+
} as unknown as IDatasetsRepository
22+
23+
const sut = new GetDatasetCitationInOtherFormats(datasetsRepositoryStub)
24+
25+
const actual = await sut.execute(testDatasetId, testVersion, testFormat as CitationFormat)
26+
expect(actual).toEqual(expectedCitation)
27+
})
28+
29+
test('should throw ReadError on repository failure', async () => {
30+
const datasetsRepositoryStub: IDatasetsRepository = {
31+
getDatasetCitationInOtherFormats: jest.fn().mockRejectedValue(new ReadError())
32+
} as unknown as IDatasetsRepository
33+
34+
const sut = new GetDatasetCitationInOtherFormats(datasetsRepositoryStub)
35+
36+
await expect(sut.execute(testDatasetId, testVersion, testFormat)).rejects.toThrow(ReadError)
37+
})
38+
})

0 commit comments

Comments
 (0)