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
2 changes: 2 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,8 @@ This changelog follows the principles of [Keep a Changelog](https://keepachangel

### Added

- Files: Added `getFileCitationByFormat` use case, repository method, and `FileCitationFormat` enum to support Dataverse file citation exports in `EndNote`, `RIS`, `BibTeX`, `CSL`, and `Internal` formats.

### Changed

### Fixed
Expand Down
27 changes: 27 additions & 0 deletions docs/useCases.md
Original file line number Diff line number Diff line change
Expand Up @@ -71,6 +71,7 @@ The different use cases currently available in the package are classified below,
- [Get a File](#get-a-file)
- [Get a File and its Dataset](#get-a-file-and-its-dataset)
- [Get File Citation Text](#get-file-citation-text)
- [Get File Citation By Format](#get-file-citation-by-format)
- [Get File Counts in a Dataset](#get-file-counts-in-a-dataset)
- [Get File Data Tables](#get-file-data-tables)
- [Get File Download Count](#get-file-download-count)
Expand Down Expand Up @@ -1710,6 +1711,32 @@ The `fileId` parameter can be a string, for persistent identifiers, or a number,

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

#### Get File Citation By Format

Returns the File citation in the requested citation export format.

##### Example call:

```typescript
import { FileCitationFormat, getFileCitationByFormat } from '@iqss/dataverse-client-javascript'

/* ... */

const fileId = 3

getFileCitationByFormat.execute(fileId, FileCitationFormat.BIBTEX).then((citationText: string) => {
/* ... */
})

/* ... */
```

_See [use case](../src/files/domain/useCases/GetFileCitationByFormat.ts) implementation_.

The `fileId` parameter can be a string, for persistent identifiers, or a number, for numeric identifiers.

The `format` parameter must be one of the available [FileCitationFormat](../src/files/domain/models/FileCitationFormat.ts) enum values: `EndNote`, `RIS`, `BibTeX`, `CSL`, or `Internal`.
Copy link

Copilot AI Apr 28, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The docs list the allowed format values as raw strings (EndNote, RIS, etc.). Since the public API expects a FileCitationFormat enum, it would be clearer/less error-prone to document the enum members (FileCitationFormat.ENDNOTE, ...RIS, ...BIBTEX, ...CSL, ...INTERNAL) rather than (or in addition to) the underlying string values.

Suggested change
The `format` parameter must be one of the available [FileCitationFormat](../src/files/domain/models/FileCitationFormat.ts) enum values: `EndNote`, `RIS`, `BibTeX`, `CSL`, or `Internal`.
The `format` parameter must be one of the available [FileCitationFormat](../src/files/domain/models/FileCitationFormat.ts) enum values: `FileCitationFormat.ENDNOTE`, `FileCitationFormat.RIS`, `FileCitationFormat.BIBTEX`, `FileCitationFormat.CSL`, or `FileCitationFormat.INTERNAL`.

Copilot uses AI. Check for mistakes.

#### Get File Counts in a Dataset

Returns an instance of [FileCounts](../src/files/domain/models/FileCounts.ts), containing the requested Dataset total file count, as well as file counts for the following file properties:
Expand Down
7 changes: 7 additions & 0 deletions src/files/domain/models/FileCitationFormat.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
export enum FileCitationFormat {
ENDNOTE = 'EndNote',
RIS = 'RIS',
BIBTEX = 'BibTeX',
CSL = 'CSL',
INTERNAL = 'Internal'
}
3 changes: 3 additions & 0 deletions src/files/domain/repositories/IFilesRepository.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ import { UploadedFileDTO } from '../dtos/UploadedFileDTO'
import { UpdateFileMetadataDTO } from '../dtos/UpdateFileMetadataDTO'
import { RestrictFileDTO } from '../dtos/RestrictFileDTO'
import { FileVersionSummarySubset } from '../models/FileVersionSummaryInfo'
import { FileCitationFormat } from '../models/FileCitationFormat'

export interface IFilesRepository {
getDatasetFiles(
Expand Down Expand Up @@ -57,6 +58,8 @@ export interface IFilesRepository {
includeDeaccessioned: boolean
): Promise<string>

getFileCitationByFormat(fileId: number | string, format: FileCitationFormat): Promise<string>

getFileUploadDestination(datasetId: number | string, file: File): Promise<FileUploadDestination>

addUploadedFilesToDataset(
Expand Down
22 changes: 22 additions & 0 deletions src/files/domain/useCases/GetFileCitationByFormat.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
import { UseCase } from '../../../core/domain/useCases/UseCase'
import { IFilesRepository } from '../repositories/IFilesRepository'
import { FileCitationFormat } from '../models/FileCitationFormat'

export class GetFileCitationByFormat implements UseCase<string> {
private filesRepository: IFilesRepository

constructor(filesRepository: IFilesRepository) {
this.filesRepository = filesRepository
}

/**
* Returns the File citation in the requested format (EndNote XML, RIS, BibTeX, CSL JSON, or Internal HTML).
*
* @param {number | string} [fileId] - The File identifier, which can be a string (for persistent identifiers), or a number (for numeric identifiers).
* @param {FileCitationFormat} [format] - The citation format to return.
* @returns {Promise<string>}
*/
async execute(fileId: number | string, format: FileCitationFormat): Promise<string> {
return await this.filesRepository.getFileCitationByFormat(fileId, format)
}
}
4 changes: 4 additions & 0 deletions src/files/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ import { GetFileDataTables } from './domain/useCases/GetFileDataTables'
import { GetDatasetFilesTotalDownloadSize } from './domain/useCases/GetDatasetFilesTotalDownloadSize'
import { GetFile } from './domain/useCases/GetFile'
import { GetFileCitation } from './domain/useCases/GetFileCitation'
import { GetFileCitationByFormat } from './domain/useCases/GetFileCitationByFormat'
import { GetFileAndDataset } from './domain/useCases/GetFileAndDataset'
import { UploadFile } from './domain/useCases/UploadFile'
import { DirectUploadClient } from './infra/clients/DirectUploadClient'
Expand All @@ -32,6 +33,7 @@ const getDatasetFilesTotalDownloadSize = new GetDatasetFilesTotalDownloadSize(fi
const getFile = new GetFile(filesRepository)
const getFileAndDataset = new GetFileAndDataset(filesRepository)
const getFileCitation = new GetFileCitation(filesRepository)
const getFileCitationByFormat = new GetFileCitationByFormat(filesRepository)
const uploadFile = new UploadFile(directUploadClient)
const addUploadedFilesToDataset = new AddUploadedFilesToDataset(filesRepository)
const deleteFile = new DeleteFile(filesRepository)
Expand All @@ -53,6 +55,7 @@ export {
getFile,
getFileAndDataset,
getFileCitation,
getFileCitationByFormat,
uploadFile,
addUploadedFilesToDataset,
deleteFile,
Expand Down Expand Up @@ -89,6 +92,7 @@ export {
FileDataVariableFormatType
} from './domain/models/FileDataTable'
export { FileDownloadSizeMode } from './domain/models/FileDownloadSizeMode'
export { FileCitationFormat } from './domain/models/FileCitationFormat'
export { FilesSubset } from './domain/models/FilesSubset'
export { FilePreview, FilePreviewChecksum } from './domain/models/FilePreview'
export { UploadedFileDTO } from './domain/dtos/UploadedFileDTO'
Expand Down
17 changes: 17 additions & 0 deletions src/files/infra/repositories/FilesRepository.ts
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@ import { ApiConstants } from '../../../core/infra/repositories/ApiConstants'
import { RestrictFileDTO } from '../../domain/dtos/RestrictFileDTO'
import { FileVersionSummarySubset } from '../../domain/models/FileVersionSummaryInfo'
import { transformFileVersionSummaryInfoResponseToFileVersionSummaryInfo } from './transformers/fileVersionSummaryInfoTransformers'
import { FileCitationFormat } from '../../domain/models/FileCitationFormat'

export interface GetFilesQueryParams {
includeDeaccessioned: boolean
Expand Down Expand Up @@ -234,6 +235,22 @@ export class FilesRepository extends ApiRepository implements IFilesRepository {
})
}

public async getFileCitationByFormat(
fileId: number | string,
format: FileCitationFormat
): Promise<string> {
return this.doGet(
this.buildApiEndpoint(this.accessResourceName, `citation/${format}`, fileId),
true
)
.then((response) =>
typeof response.data === 'string' ? response.data : JSON.stringify(response.data)
)
.catch((error) => {
throw error
})
}

public async getFileUploadDestination(
datasetId: number | string,
file: File
Expand Down
4 changes: 2 additions & 2 deletions test/environment/.env
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
POSTGRES_VERSION=17
DATAVERSE_DB_USER=dataverse
SOLR_VERSION=9.8.0
DATAVERSE_IMAGE_REGISTRY=docker.io
DATAVERSE_IMAGE_TAG=unstable
DATAVERSE_IMAGE_REGISTRY=ghcr.io
DATAVERSE_IMAGE_TAG=11733-api-get-file-citation-format
Comment on lines +4 to +5
Copy link

Copilot AI Apr 28, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

test/environment/.env is now pinned to a Dataverse PR/branch image tag (11733-api-get-file-citation-format) on GHCR. This makes integration/functional tests depend on a potentially ephemeral image and can break CI or future local runs once that tag is removed. Consider reverting to the default (docker.io + unstable) and documenting/overriding the image tag via local env/CI when testing against a Dataverse PR image.

Suggested change
DATAVERSE_IMAGE_REGISTRY=ghcr.io
DATAVERSE_IMAGE_TAG=11733-api-get-file-citation-format
DATAVERSE_IMAGE_REGISTRY=docker.io
DATAVERSE_IMAGE_TAG=unstable

Copilot uses AI. Check for mistakes.
DATAVERSE_BOOTSTRAP_TIMEOUT=5m
123 changes: 123 additions & 0 deletions test/functional/files/GetFileCitationByFormat.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,123 @@
import {
ApiConfig,
createDataset,
CreatedDatasetIdentifiers,
FileCitationFormat,
getDatasetFiles,
getFileCitationByFormat,
ReadError
} from '../../../src'
import { DataverseApiAuthMechanism } from '../../../src/core/infra/repositories/ApiConfig'
import {
createCollectionViaApi,
deleteCollectionViaApi
} from '../../testHelpers/collections/collectionHelper'
import { deleteUnpublishedDatasetViaApi } from '../../testHelpers/datasets/datasetHelper'
import { uploadFileViaApi } from '../../testHelpers/files/filesHelper'
import { TestConstants } from '../../testHelpers/TestConstants'

describe('execute', () => {
const testCollectionAlias = 'getFileCitationByFormatFunctionalTest'
const testTextFile1Name = 'test-file-1.txt'
let testDatasetIds: CreatedDatasetIdentifiers

beforeAll(async () => {
ApiConfig.init(
TestConstants.TEST_API_URL,
DataverseApiAuthMechanism.API_KEY,
process.env.TEST_API_KEY
)
await createCollectionViaApi(testCollectionAlias)

try {
testDatasetIds = await createDataset.execute(
TestConstants.TEST_NEW_DATASET_DTO,
testCollectionAlias
)
} catch (error) {
throw new Error('Tests beforeAll(): Error while creating test dataset')
}

await uploadFileViaApi(testDatasetIds.numericId, testTextFile1Name).catch(() => {
throw new Error(`Tests beforeAll(): Error while uploading file ${testTextFile1Name}`)
})
})

afterAll(async () => {
try {
await deleteUnpublishedDatasetViaApi(testDatasetIds.numericId)
} catch (error) {
throw new Error('Tests afterAll(): Error while deleting test dataset')
}

try {
await deleteCollectionViaApi(testCollectionAlias)
} catch (error) {
throw new Error('Tests afterAll(): Error while deleting test collection')
}
})

const getTestFileId = async (): Promise<number> => {
const datasetFiles = await getDatasetFiles.execute(testDatasetIds.numericId)
return datasetFiles.files[0].id
}

test('should successfully get file citation in EndNote (XML) format', async () => {
const fileId = await getTestFileId()

const citation = await getFileCitationByFormat.execute(fileId, FileCitationFormat.ENDNOTE)

expect(typeof citation).toBe('string')
expect(citation.trimStart()).toMatch(/^<\?xml/)
})

test('should successfully get file citation in RIS (plain text) format', async () => {
const fileId = await getTestFileId()

const citation = await getFileCitationByFormat.execute(fileId, FileCitationFormat.RIS)

expect(typeof citation).toBe('string')
// RIS records use TY (type) and ER (end of record) tags
expect(citation).toMatch(/TY\s+-/)
expect(citation).toMatch(/ER\s+-/)
})

test('should successfully get file citation in BibTeX (plain text) format', async () => {
const fileId = await getTestFileId()

const citation = await getFileCitationByFormat.execute(fileId, FileCitationFormat.BIBTEX)

expect(typeof citation).toBe('string')
// BibTeX entries start with @<entry-type>{
expect(citation.trimStart()).toMatch(/^@\w+\{/)
})

test('should successfully get file citation in CSL (JSON) format', async () => {
const fileId = await getTestFileId()

const citation = await getFileCitationByFormat.execute(fileId, FileCitationFormat.CSL)

expect(typeof citation).toBe('string')
const parsed = JSON.parse(citation)
expect(typeof parsed).toBe('object')
expect(parsed).not.toBeNull()
})

test('should successfully get file citation in Internal (HTML) format', async () => {
const fileId = await getTestFileId()

const citation = await getFileCitationByFormat.execute(fileId, FileCitationFormat.INTERNAL)

expect(typeof citation).toBe('string')
// Internal HTML format includes anchor tags linking to the dataset
expect(citation).toMatch(/<a\s+href=/i)
})

test('should throw an error when the file id does not exist', async () => {
const nonExistentFileId = 5

await expect(
getFileCitationByFormat.execute(nonExistentFileId, FileCitationFormat.BIBTEX)
).rejects.toThrow(ReadError)
})
})
50 changes: 50 additions & 0 deletions test/integration/files/FilesRepository.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,7 @@ import {
} from '../../../src/datasets'
import { FileModel } from '../../../src/files/domain/models/FileModel'
import { FileCounts } from '../../../src/files/domain/models/FileCounts'
import { FileCitationFormat } from '../../../src/files/domain/models/FileCitationFormat'
import { FileDownloadSizeMode, WriteError } from '../../../src'
import {
deaccessionDatasetViaApi,
Expand Down Expand Up @@ -656,6 +657,55 @@ describe('FilesRepository', () => {
})
})

describe('getFileCitationByFormat', () => {
test('should return EndNote citation as XML', async () => {
const citation = await sut.getFileCitationByFormat(testFileId, FileCitationFormat.ENDNOTE)

expect(typeof citation).toBe('string')
expect(citation.trimStart()).toMatch(/^<\?xml/)
})

test('should return RIS citation as plain text', async () => {
const citation = await sut.getFileCitationByFormat(testFileId, FileCitationFormat.RIS)

expect(typeof citation).toBe('string')
// RIS records use TY (type) and ER (end of record) tags
expect(citation).toMatch(/TY\s+-/)
expect(citation).toMatch(/ER\s+-/)
})

test('should return BibTeX citation as plain text', async () => {
const citation = await sut.getFileCitationByFormat(testFileId, FileCitationFormat.BIBTEX)

expect(typeof citation).toBe('string')
// BibTeX entries start with @<entry-type>{
expect(citation.trimStart()).toMatch(/^@\w+\{/)
})

test('should return CSL citation as JSON', async () => {
const citation = await sut.getFileCitationByFormat(testFileId, FileCitationFormat.CSL)

expect(typeof citation).toBe('string')
const parsed = JSON.parse(citation)
expect(typeof parsed).toBe('object')
expect(parsed).not.toBeNull()
})

test('should return Internal citation as HTML', async () => {
const citation = await sut.getFileCitationByFormat(testFileId, FileCitationFormat.INTERNAL)

expect(typeof citation).toBe('string')
// Internal HTML format includes anchor tags linking to the dataset
expect(citation).toMatch(/<a\s+href=/i)
})

test('should return error when file does not exist', async () => {
await expect(
sut.getFileCitationByFormat(nonExistentFiledId, FileCitationFormat.BIBTEX)
).rejects.toThrow(ReadError)
})
})

describe('getFileUploadDestination', () => {
const testCollectionAlias = 'getFileUploadDestinationsTestCollection'
let testDataset2Ids: CreatedDatasetIdentifiers
Expand Down
Loading
Loading