From 96f14087e02fc90f202ee3f59725de5616cab44d Mon Sep 17 00:00:00 2001 From: Ellen Kraffmiller Date: Thu, 14 Nov 2024 09:21:02 -0500 Subject: [PATCH 1/9] feat: implement GetDatasetVersionDiff use case --- .../domain/models/DatasetVersionDiff.ts | 50 +++++ .../repositories/IDatasetsRepository.ts | 6 + .../domain/useCases/GetDatasetVersionDiff.ts | 31 +++ src/datasets/index.ts | 3 + .../infra/repositories/DatasetsRepository.ts | 20 ++ .../transformers/DatasetVersionDiffPayload.ts | 178 ++++++++++++++++++ .../datasetVersionDiffTransformers.ts | 29 +++ .../datasets/DatasetsRepository.test.ts | 98 +++++++++- .../integration/files/FilesRepository.test.ts | 10 +- .../datasets/datasetVersionDiffHelper.ts | 77 ++++++++ test/testHelpers/files/filesHelper.ts | 7 +- .../datasets/GetDatasetVersionDiff.test.ts | 30 +++ 12 files changed, 532 insertions(+), 7 deletions(-) create mode 100644 src/datasets/domain/models/DatasetVersionDiff.ts create mode 100644 src/datasets/domain/useCases/GetDatasetVersionDiff.ts create mode 100644 src/datasets/infra/repositories/transformers/DatasetVersionDiffPayload.ts create mode 100644 src/datasets/infra/repositories/transformers/datasetVersionDiffTransformers.ts create mode 100644 test/testHelpers/datasets/datasetVersionDiffHelper.ts create mode 100644 test/unit/datasets/GetDatasetVersionDiff.test.ts diff --git a/src/datasets/domain/models/DatasetVersionDiff.ts b/src/datasets/domain/models/DatasetVersionDiff.ts new file mode 100644 index 00000000..bda416a2 --- /dev/null +++ b/src/datasets/domain/models/DatasetVersionDiff.ts @@ -0,0 +1,50 @@ +// Details API + +export interface DatasetVersionDiff { + oldVersion: VersionSummary + newVersion: VersionSummary + metadataChanges?: MetadataBlockDiff[] + filesAdded?: FileSummary[] + filesRemoved?: FileSummary[] + fileChanges?: FileDiff[] + filesReplaced?: FileReplacement[] + termsOfAccess?: FieldDiff[] +} + +export interface FileSummary { + fileName: string + MD5: string + type: string + fileId: number + filePath: string + description: string + isRestricted: boolean + tags: string[] + categories: string[] +} + +export interface VersionSummary { + versionNumber: string + lastUpdatedDate: string +} +export interface MetadataBlockDiff { + blockName: string + changed: FieldDiff[] +} + +export interface FileDiff { + fileName: string + md5: string + fileId: number + changed: FieldDiff[] +} + +export interface FileReplacement { + oldFile: FileSummary + newFile: FileSummary +} +export interface FieldDiff { + fieldName: string + oldValue: string + newValue: string +} diff --git a/src/datasets/domain/repositories/IDatasetsRepository.ts b/src/datasets/domain/repositories/IDatasetsRepository.ts index 6d5abf8e..c5cd44d4 100644 --- a/src/datasets/domain/repositories/IDatasetsRepository.ts +++ b/src/datasets/domain/repositories/IDatasetsRepository.ts @@ -5,6 +5,7 @@ import { DatasetUserPermissions } from '../models/DatasetUserPermissions' import { CreatedDatasetIdentifiers } from '../models/CreatedDatasetIdentifiers' import { DatasetDTO } from '../dtos/DatasetDTO' import { MetadataBlock } from '../../../metadataBlocks' +import { DatasetVersionDiff } from '../models/DatasetVersionDiff' export interface IDatasetsRepository { getDataset( @@ -28,6 +29,11 @@ export interface IDatasetsRepository { getDatasetSummaryFieldNames(): Promise getPrivateUrlDatasetCitation(token: string): Promise getDatasetUserPermissions(datasetId: number | string): Promise + getDatasetVersionDiff( + datasetId: number | string, + newVersionId: string, + oldVersionId: string + ): Promise createDataset( newDataset: DatasetDTO, datasetMetadataBlocks: MetadataBlock[], diff --git a/src/datasets/domain/useCases/GetDatasetVersionDiff.ts b/src/datasets/domain/useCases/GetDatasetVersionDiff.ts new file mode 100644 index 00000000..156ec19a --- /dev/null +++ b/src/datasets/domain/useCases/GetDatasetVersionDiff.ts @@ -0,0 +1,31 @@ +import { UseCase } from '../../../core/domain/useCases/UseCase' +import { IDatasetsRepository } from '../repositories/IDatasetsRepository' +import { DatasetVersionDiff } from '../models/DatasetVersionDiff' + +export class GetDatasetVersionDiff implements UseCase { + private datasetsRepository: IDatasetsRepository + + constructor(datasetsRepository: IDatasetsRepository) { + this.datasetsRepository = datasetsRepository + } + + /** + * Returns a Dataset instance, given the search parameters to identify it. + * TODO: should we allow DatasetNotNumberedVersion enum values for newVersionId and oldVersionId? + * @param {number | string} [datasetId] - The dataset identifier, which can be a string (for persistent identifiers), or a number (for numeric identifiers). + * @param {string } [newVersionId] - The dataset version identifier, which can be a version-specific numeric string (for example, 1.0) or a DatasetNotNumberedVersion enum value. If this parameter is not set, the default value is: DatasetNotNumberedVersion.LATEST + * @param {string } [oldVersionId] - The dataset version identifier, which can be a version-specific numeric string (for example, 1.0) or a DatasetNotNumberedVersion enum value. If this parameter is not set, the default value is: DatasetNotNumberedVersion.LATEST * @param {boolean} [includeDeaccessioned=false] - Indicates whether to consider deaccessioned versions in the dataset search or not. The default value is false + * @returns {Promise} + */ + async execute( + datasetId: number | string, + newVersionId: string, + oldVersionId: string + ): Promise { + return await this.datasetsRepository.getDatasetVersionDiff( + datasetId, + newVersionId, + oldVersionId + ) + } +} diff --git a/src/datasets/index.ts b/src/datasets/index.ts index 71c340cd..83572caa 100644 --- a/src/datasets/index.ts +++ b/src/datasets/index.ts @@ -15,6 +15,7 @@ import { SingleMetadataFieldValidator } from './domain/useCases/validators/Singl import { MultipleMetadataFieldValidator } from './domain/useCases/validators/MultipleMetadataFieldValidator' import { PublishDataset } from './domain/useCases/PublishDataset' import { UpdateDataset } from './domain/useCases/UpdateDataset' +import { GetDatasetVersionDiff } from './domain/useCases/GetDatasetVersionDiff' const datasetsRepository = new DatasetsRepository() @@ -26,6 +27,7 @@ const getAllDatasetPreviews = new GetAllDatasetPreviews(datasetsRepository) const getDatasetUserPermissions = new GetDatasetUserPermissions(datasetsRepository) const getDatasetSummaryFieldNames = new GetDatasetSummaryFieldNames(datasetsRepository) const getPrivateUrlDatasetCitation = new GetPrivateUrlDatasetCitation(datasetsRepository) +const getDatasetVersionDiff = new GetDatasetVersionDiff(datasetsRepository) const singleMetadataFieldValidator = new SingleMetadataFieldValidator() const metadataFieldValidator = new MetadataFieldValidator( new SingleMetadataFieldValidator(), @@ -54,6 +56,7 @@ export { getDatasetUserPermissions, getDatasetSummaryFieldNames, getPrivateUrlDatasetCitation, + getDatasetVersionDiff, publishDataset, createDataset, updateDataset diff --git a/src/datasets/infra/repositories/DatasetsRepository.ts b/src/datasets/infra/repositories/DatasetsRepository.ts index 212d7e5c..2ade4976 100644 --- a/src/datasets/infra/repositories/DatasetsRepository.ts +++ b/src/datasets/infra/repositories/DatasetsRepository.ts @@ -15,6 +15,8 @@ import { MetadataBlock } from '../../../metadataBlocks' import { transformDatasetModelToNewDatasetRequestPayload } from './transformers/datasetTransformers' import { transformDatasetLocksResponseToDatasetLocks } from './transformers/datasetLocksTransformers' import { transformDatasetPreviewsResponseToDatasetPreviewSubset } from './transformers/datasetPreviewsTransformers' +import { DatasetVersionDiff } from '../../domain/models/DatasetVersionDiff' +import { transformDatasetVersionDiffResponseToDatasetVersionDiff } from './transformers/datasetVersionDiffTransformers' export interface GetAllDatasetPreviewsQueryParams { per_page?: number @@ -141,6 +143,24 @@ export class DatasetsRepository extends ApiRepository implements IDatasetsReposi }) } + public async getDatasetVersionDiff( + datasetId: string | number, + newVersionId: string, + oldVersionId: string + ): Promise { + return this.doGet( + this.buildApiEndpoint( + this.datasetsResourceName, + `versions/${oldVersionId}/compare/${newVersionId}`, + datasetId + ), + true + ) + .then((response) => transformDatasetVersionDiffResponseToDatasetVersionDiff(response)) + .catch((error) => { + throw error + }) + } public async createDataset( newDataset: DatasetDTO, datasetMetadataBlocks: MetadataBlock[], diff --git a/src/datasets/infra/repositories/transformers/DatasetVersionDiffPayload.ts b/src/datasets/infra/repositories/transformers/DatasetVersionDiffPayload.ts new file mode 100644 index 00000000..baef6be2 --- /dev/null +++ b/src/datasets/infra/repositories/transformers/DatasetVersionDiffPayload.ts @@ -0,0 +1,178 @@ +/* +{ + "status": "OK", + "data": { + "oldVersion": { + "versionNumber": "1.0", + "lastUpdatedDate": "2024-10-24T15:17:11Z" + }, + "newVersion": { + "versionNumber": "DRAFT", + "lastUpdatedDate": "2024-10-24T15:17:16Z" + }, + "metadataChanges": [ + { + "blockName": "Citation Metadata", + "changed": [ + { + "fieldName": "Author", + "oldValue": "Finch, Fiona; (Birds Inc.)", + "newValue": "Finch, Fiona; (Birds Inc.); Poe, Edgar Allen; (Baltimore Poets); Mulligan, Hercules; (Sons of Liberty)" + }, + { + "fieldName": "Subject", + "oldValue": "Medicine, Health and Life Sciences", + "newValue": "Medicine, Health and Life Sciences; Astronomy and Astrophysics; Other" + }, + { + "fieldName": "Producer", + "oldValue": "", + "newValue": "Allen, Irwin; (MGM); Spielberg, Stephen; (ILM)" + } + ] + }, + { + "blockName": "Life Sciences Metadata", + "changed": [ + { + "fieldName": "Design Type", + "oldValue": "", + "newValue": "Parallel Group Design; Nested Case Control Design" + } + ] + } + ], + "filesAdded": [ + { + "fileName": "test.tab", + "filePath": "data/subdir1", + "MD5": "77c7f03a7d7772907b43f0b322cef723", + "type": "text/tab-separated-values", + "fileId": 42, + "description": "my description", + "isRestricted": false, + "categories": [ + "Data" + ], + "tags": [ + "Survey" + ] + } + ], + "filesRemoved": [ + { + "fileName": "dataverseproject_logo.jpg", + "filePath": "data/subdir1", + "MD5": "c1edbefa86a55c5037873370ae7fd7b6", + "type": "image/jpeg", + "fileId": 40, + "description": "my description", + "isRestricted": false, + "categories": [ + "Data" + ] + } + ], + "filesReplaced": [ + { + "oldFile": { + "fileName": "favicon-16x16.png", + "filePath": "data/subdir1", + "MD5": "d3c852e7ecb92fd105ba4018116a9be8", + "type": "image/png", + "fileId": 41, + "description": "my description", + "isRestricted": false, + "categories": [ + "Data" + ] + }, + "newFile": { + "fileName": "favicon-32x32.png", + "filePath": "data/subdir1", + "MD5": "c931f7add8b6a1f9a691046b77c231fa", + "type": "image/png", + "fileId": 43, + "description": "my description", + "isRestricted": false, + "categories": [ + "Data" + ] + } + } + ], + "fileChanges": [ + { + "fileName": "dataverse-icon-1200.png", + "MD5": "a23eb44803d9127bc6e055f77b869816", + "fileId": 39, + "changed": [ + { + "fieldName": "isRestricted", + "oldValue": "false", + "newValue": "true" + } + ] + } + ], + "TermsOfAccess": { + "changed": [ + { + "fieldName": "Data Access Place", + "oldValue": "", + "newValue": "Somewhere" + } + ] + } + } +} + */ + +export interface DatasetVersionDiffPayload { + oldVersion: VersionSummaryPayload + newVersion: VersionSummaryPayload + metadataChanges: MetadataBlockDiffPayload[] + filesAdded: FileSummaryPayload[] + filesRemoved: FileSummaryPayload[] + fileChanges: FileDiffPayload[] + filesReplaced: FileReplacementPayload[] + TermsOfAccess: FieldDiffPayload[] +} + +export interface FileSummaryPayload { + fileName: string + MD5: string + type: string + fileId: number + filePath: string + description: string + isRestricted: boolean + tags: string[] + categories: string[] +} + +export interface VersionSummaryPayload { + versionNumber: string + lastUpdatedDate: string +} +export interface MetadataBlockDiffPayload { + blockName: string + changed: FieldDiffPayload[] +} + +export interface FileDiffPayload { + fileName: string + md5: string + fileId: number + changed: FieldDiffPayload[] +} +export interface FieldDiffPayload { + fieldName: string + oldValue: string + newValue: string +} + +export interface FileReplacementPayload { + oldFile: FileSummaryPayload + newFile: FileSummaryPayload +} diff --git a/src/datasets/infra/repositories/transformers/datasetVersionDiffTransformers.ts b/src/datasets/infra/repositories/transformers/datasetVersionDiffTransformers.ts new file mode 100644 index 00000000..a9385794 --- /dev/null +++ b/src/datasets/infra/repositories/transformers/datasetVersionDiffTransformers.ts @@ -0,0 +1,29 @@ +import { AxiosResponse } from 'axios' +import { DatasetVersionDiff } from '../../../domain/models/DatasetVersionDiff' + +/* + oldVersion: VersionSummaryPayload + newVersion: VersionSummaryPayload + metadataChanges: MetadataBlockDiffPayload[] + filesAdded: FileSummaryPayload[] + filesRemoved: FileSummaryPayload[] + fileChanges: FileDiffPayload[] + filesReplaced: FileReplacementPayload[] + TermsOfAccess: FieldDiffPayload[] + */ +export const transformDatasetVersionDiffResponseToDatasetVersionDiff = ( + response: AxiosResponse +): DatasetVersionDiff => { + const datasetVersionDiffPayload = response.data.data + const retValue = { + oldVersion: datasetVersionDiffPayload.oldVersion, + newVersion: datasetVersionDiffPayload.newVersion, + metadataChanges: datasetVersionDiffPayload.metadataChanges, + filesAdded: datasetVersionDiffPayload.filesAdded, + filesRemoved: datasetVersionDiffPayload.filesRemoved, + fileChanges: datasetVersionDiffPayload.fileChanges, + filesReplaced: datasetVersionDiffPayload.filesReplaced, + termsOfAccess: datasetVersionDiffPayload.TermsOfAccess + } + return retValue +} diff --git a/test/integration/datasets/DatasetsRepository.test.ts b/test/integration/datasets/DatasetsRepository.test.ts index db561395..4273d2bf 100644 --- a/test/integration/datasets/DatasetsRepository.test.ts +++ b/test/integration/datasets/DatasetsRepository.test.ts @@ -16,7 +16,8 @@ import { DatasetPreviewSubset, VersionUpdateType, createDataset, - CreatedDatasetIdentifiers + CreatedDatasetIdentifiers, + DatasetDTO } from '../../../src/datasets' import { ApiConfig, WriteError } from '../../../src' import { DataverseApiAuthMechanism } from '../../../src/core/infra/repositories/ApiConfig' @@ -31,6 +32,45 @@ import { deleteCollectionViaApi, ROOT_COLLECTION_ALIAS } from '../../testHelpers/collections/collectionHelper' +import { testTextFile1Name, uploadFileViaApi } from '../../testHelpers/files/filesHelper' + +const TEST_DIFF_DATASET_DTO: DatasetDTO = { + license: { + name: 'CC0 1.0', + uri: 'http://creativecommons.org/publicdomain/zero/1.0', + iconUri: 'https://licensebuttons.net/p/zero/1.0/88x31.png' + }, + metadataBlockValues: [ + { + name: 'citation', + fields: { + title: 'Updated Dataset Title', + author: [ + { + authorName: 'Smith, John', + authorAffiliation: 'Dataverse.org' + }, + { + authorName: 'Owner, Dataverse', + authorAffiliation: 'Dataversedemo.org' + } + ], + datasetContact: [ + { + datasetContactEmail: 'bird@mailinator.com', + datasetContactName: 'Bird, Fiona' + } + ], + dsDescription: [ + { + dsDescriptionValue: 'This is the updated description of the dataset.' + } + ], + subject: ['Medicine, Health and Life Sciences'] + } + } + ] +} describe('DatasetsRepository', () => { const testCollectionAlias = 'datasetsRepositoryTestCollection' @@ -429,6 +469,62 @@ describe('DatasetsRepository', () => { expect(typeof actualDatasetCitation).toBe('string') }) }) + describe('getDatasetVersionDiff', () => { + let testDatasetIds: CreatedDatasetIdentifiers + + beforeAll(async () => { + testDatasetIds = await createDataset.execute(TestConstants.TEST_NEW_DATASET_DTO) + // Dataset is in draft, so we need to publish it first + await sut.publishDataset(testDatasetIds.numericId, VersionUpdateType.MAJOR) + await waitForNoLocks(testDatasetIds.numericId, 10) + }) + test('should return dataset metadata diff between two dataset versions', async () => { + // Update dataset + const metadataBlocksRepository = new MetadataBlocksRepository() + const citationMetadataBlock = await metadataBlocksRepository.getMetadataBlockByName( + 'citation' + ) + + await sut.updateDataset(testDatasetIds.numericId, TEST_DIFF_DATASET_DTO, [ + citationMetadataBlock + ]) + const actual = await sut.getDatasetVersionDiff( + testDatasetIds.numericId, + DatasetNotNumberedVersion.DRAFT, + '1.0' + ) + expect(actual.metadataChanges[0].blockName).toEqual('Citation Metadata') + }) + test('should return added file diff between two dataset versions', async () => { + const fileMetadata = { + description: 'test description', + directoryLabel: 'directoryLabel', + categories: ['category1', 'category2'] + } + const expectedFilesAdded = [ + { + fileName: 'test-file-1.txt', + type: 'text/plain', + isRestricted: false, + description: fileMetadata.description, + filePath: fileMetadata.directoryLabel, + categories: fileMetadata.categories, + MD5: '68b22040025784da775f55cfcb6dee2e', + fileId: 50 + } + ] + await uploadFileViaApi(testDatasetIds.numericId, testTextFile1Name, fileMetadata) + const actual = await sut.getDatasetVersionDiff( + testDatasetIds.numericId, + DatasetNotNumberedVersion.DRAFT, + '1.0' + ) + expect(actual.filesAdded).toEqual(expectedFilesAdded) + }) + afterAll(async () => { + await deletePublishedDatasetViaApi(testDatasetIds.persistentId) + }) + }) describe('createDataset', () => { test('should create a dataset with the provided dataset citation fields', async () => { diff --git a/test/integration/files/FilesRepository.test.ts b/test/integration/files/FilesRepository.test.ts index 073c4307..5d8b6821 100644 --- a/test/integration/files/FilesRepository.test.ts +++ b/test/integration/files/FilesRepository.test.ts @@ -8,7 +8,11 @@ import { createMultipartFileBlob, createSinglepartFileBlob, registerFileViaApi, - uploadFileViaApi + uploadFileViaApi, + testTextFile1Name, + testTextFile2Name, + testTextFile3Name, + testTabFile4Name } from '../../testHelpers/files/filesHelper' import { ReadError } from '../../../src/core/domain/repositories/ReadError' import { @@ -43,10 +47,6 @@ describe('FilesRepository', () => { let testDatasetIds: CreatedDatasetIdentifiers - const testTextFile1Name = 'test-file-1.txt' - const testTextFile2Name = 'test-file-2.txt' - const testTextFile3Name = 'test-file-3.txt' - const testTabFile4Name = 'test-file-4.tab' const testCategoryName = 'testCategory' const nonExistentFiledId = 200 diff --git a/test/testHelpers/datasets/datasetVersionDiffHelper.ts b/test/testHelpers/datasets/datasetVersionDiffHelper.ts new file mode 100644 index 00000000..34119142 --- /dev/null +++ b/test/testHelpers/datasets/datasetVersionDiffHelper.ts @@ -0,0 +1,77 @@ +import { + DatasetVersionDiff, + VersionSummary, + MetadataBlockDiff, + FileSummary, + FileDiff, + FileReplacement, + FieldDiff +} from '../../../src/datasets/domain/models/DatasetVersionDiff' + +export const createDatasetVersionDiff = (): DatasetVersionDiff => { + const versionSummary: VersionSummary = { + versionNumber: '1.0', + lastUpdatedDate: '2023-05-15T08:21:03Z' + } + + const metadataBlockDiff: MetadataBlockDiff = { + blockName: 'citation', + changed: [ + { + fieldName: 'title', + oldValue: 'Old Title', + newValue: 'New Title' + } + ] + } + + const fileSummary: FileSummary = { + fileName: 'file1.txt', + MD5: 'd41d8cd98f00b204e9800998ecf8427e', + type: 'text/plain', + fileId: 1, + filePath: '/path/to/file1.txt', + description: 'Test file', + isRestricted: false, + tags: ['tag1'], + categories: ['category1'] + } + + const fileDiff: FileDiff = { + fileName: 'file1.txt', + md5: 'd41d8cd98f00b204e9800998ecf8427e', + fileId: 1, + changed: [ + { + fieldName: 'description', + oldValue: 'Old description', + newValue: 'New description' + } + ] + } + + const fileReplacement: FileReplacement = { + oldFile: fileSummary, + newFile: { + ...fileSummary, + fileName: 'file2.txt' + } + } + + const fieldDiff: FieldDiff = { + fieldName: 'termsOfAccess', + oldValue: 'Old terms', + newValue: 'New terms' + } + + return { + oldVersion: versionSummary, + newVersion: versionSummary, + metadataChanges: [metadataBlockDiff], + filesAdded: [fileSummary], + filesRemoved: [fileSummary], + fileChanges: [fileDiff], + filesReplaced: [fileReplacement], + termsOfAccess: [fieldDiff] + } +} diff --git a/test/testHelpers/files/filesHelper.ts b/test/testHelpers/files/filesHelper.ts index 3d4c1f52..abe6bc0b 100644 --- a/test/testHelpers/files/filesHelper.ts +++ b/test/testHelpers/files/filesHelper.ts @@ -9,8 +9,13 @@ import { FilePayload } from '../../../src/files/infra/repositories/transformers/ interface FileMetadata { categories?: string[] + description?: string + directoryLabel?: string } - +export const testTextFile1Name = 'test-file-1.txt' +export const testTextFile2Name = 'test-file-2.txt' +export const testTextFile3Name = 'test-file-3.txt' +export const testTabFile4Name = 'test-file-4.tab' export const createFileModel = (): FileModel => { return { id: 1, diff --git a/test/unit/datasets/GetDatasetVersionDiff.test.ts b/test/unit/datasets/GetDatasetVersionDiff.test.ts new file mode 100644 index 00000000..0faaf83a --- /dev/null +++ b/test/unit/datasets/GetDatasetVersionDiff.test.ts @@ -0,0 +1,30 @@ +import { ReadError } from '../../../src/core/domain/repositories/ReadError' +import { IDatasetsRepository } from '../../../src/datasets/domain/repositories/IDatasetsRepository' + +import { createDatasetVersionDiff } from '../../testHelpers/datasets/datasetVersionDiffHelper' +import { GetDatasetVersionDiff } from '../../../src/datasets/domain/useCases/GetDatasetVersionDiff' + +describe('execute', () => { + const testDatasetId = 1 + + test('should return dataset version diff on repository success', async () => { + const testDatasetVersionDiff = [createDatasetVersionDiff()] + const datasetsRepositoryStub: IDatasetsRepository = {} as IDatasetsRepository + datasetsRepositoryStub.getDatasetVersionDiff = jest + .fn() + .mockResolvedValue(testDatasetVersionDiff) + const sut = new GetDatasetVersionDiff(datasetsRepositoryStub) + + const actual = await sut.execute(testDatasetId, '1.0', '2.0') + + expect(actual).toEqual(testDatasetVersionDiff) + }) + + test('should return error result on repository error', async () => { + const datasetsRepositoryStub: IDatasetsRepository = {} as IDatasetsRepository + datasetsRepositoryStub.getDatasetVersionDiff = jest.fn().mockRejectedValue(new ReadError()) + const sut = new GetDatasetVersionDiff(datasetsRepositoryStub) + + await expect(sut.execute(testDatasetId, '1.0', '2.0')).rejects.toThrow(ReadError) + }) +}) From a8fa2cb6e07c75b1844ea71a3502ab51bf0b6743 Mon Sep 17 00:00:00 2001 From: Ellen Kraffmiller Date: Thu, 14 Nov 2024 09:25:56 -0500 Subject: [PATCH 2/9] fix: function comments --- src/datasets/domain/useCases/GetDatasetVersionDiff.ts | 8 +++----- 1 file changed, 3 insertions(+), 5 deletions(-) diff --git a/src/datasets/domain/useCases/GetDatasetVersionDiff.ts b/src/datasets/domain/useCases/GetDatasetVersionDiff.ts index 156ec19a..9340ea5c 100644 --- a/src/datasets/domain/useCases/GetDatasetVersionDiff.ts +++ b/src/datasets/domain/useCases/GetDatasetVersionDiff.ts @@ -10,12 +10,10 @@ export class GetDatasetVersionDiff implements UseCase { } /** - * Returns a Dataset instance, given the search parameters to identify it. - * TODO: should we allow DatasetNotNumberedVersion enum values for newVersionId and oldVersionId? + * Returns a DatasetVersionDiff instance, which contains the differences between the two given versions. * @param {number | string} [datasetId] - The dataset identifier, which can be a string (for persistent identifiers), or a number (for numeric identifiers). - * @param {string } [newVersionId] - The dataset version identifier, which can be a version-specific numeric string (for example, 1.0) or a DatasetNotNumberedVersion enum value. If this parameter is not set, the default value is: DatasetNotNumberedVersion.LATEST - * @param {string } [oldVersionId] - The dataset version identifier, which can be a version-specific numeric string (for example, 1.0) or a DatasetNotNumberedVersion enum value. If this parameter is not set, the default value is: DatasetNotNumberedVersion.LATEST * @param {boolean} [includeDeaccessioned=false] - Indicates whether to consider deaccessioned versions in the dataset search or not. The default value is false - * @returns {Promise} + * @param {string } [newVersionId] - The dataset version identifier, which can be a version-specific numeric string (for example, 1.0) or a DatasetNotNumberedVersion enum value. + * @param {string } [oldVersionId] - The dataset version identifier, which can be a version-specific numeric string (for example, 1.0) or a DatasetNotNumberedVersion enum value. */ async execute( datasetId: number | string, From 97bc0820972a13beb75a6daf2957f40d71de0ac5 Mon Sep 17 00:00:00 2001 From: Ellen Kraffmiller Date: Thu, 14 Nov 2024 09:56:51 -0500 Subject: [PATCH 3/9] fix: order of version parameters --- docs/useCases.md | 30 +++++++++++++++++++ .../domain/useCases/GetDatasetVersionDiff.ts | 10 +++---- .../infra/repositories/DatasetsRepository.ts | 4 +-- .../datasets/DatasetsRepository.test.ts | 8 ++--- 4 files changed, 41 insertions(+), 11 deletions(-) diff --git a/docs/useCases.md b/docs/useCases.md index bfe762bf..a155f10d 100644 --- a/docs/useCases.md +++ b/docs/useCases.md @@ -26,6 +26,7 @@ The different use cases currently available in the package are classified below, - [Get Dataset Locks](#get-dataset-locks) - [Get Dataset Summary Field Names](#get-dataset-summary-field-names) - [Get User Permissions on a Dataset](#get-user-permissions-on-a-dataset) + - [Get Differences between Two Dataset Versions](#get-differences-between-two-dataset-versions) - [List All Datasets](#list-all-datasets) - [Datasets write use cases](#datasets-write-use-cases) - [Create a Dataset](#create-a-dataset) @@ -428,6 +429,35 @@ _See [use case](../src/datasets/domain/useCases/GetDatasetUserPermissions.ts) im The `datasetId` parameter can be a string, for persistent identifiers, or a number, for numeric identifiers. +#### Get Differences between Two Dataset Versions + +Returns an instance of [DatasetVersionDiff](../src/datasets/domain/models/DatasetVersionDiff.ts) that contains the differences between two Dataset Versions. + +##### Example call: + +```typescript +import { getDatasetVersionDiff } from '@iqss/dataverse-client-javascript' + +/* ... */ + +const datasetId = 'doi:10.77777/FK2/AAAAAA' +const oldVersion = '1.0' +const newVersion = '2.0' + +getDatasetVersionDiff.execute(datasetId, oldVersion, newVersion).then((versionDiff: DatasetVersionDiff) => { + /* ... */ +}) + +/* ... */ +``` + +_See [use case](../src/datasets/domain/useCases/GetDatasetVersionDiff.ts) implementation_. + +The `datasetId` parameter can be a string, for persistent identifiers, or a number, for numeric identifiers. + +The `oldVersion` and `newVersion` parameters specify the versions of the dataset to compare. + + #### List All Datasets Returns an instance of [DatasetPreviewSubset](../src/datasets/domain/models/DatasetPreviewSubset.ts) that contains reduced information for each dataset that the calling user can access in the installation. diff --git a/src/datasets/domain/useCases/GetDatasetVersionDiff.ts b/src/datasets/domain/useCases/GetDatasetVersionDiff.ts index 9340ea5c..d1499d7a 100644 --- a/src/datasets/domain/useCases/GetDatasetVersionDiff.ts +++ b/src/datasets/domain/useCases/GetDatasetVersionDiff.ts @@ -12,18 +12,18 @@ export class GetDatasetVersionDiff implements UseCase { /** * Returns a DatasetVersionDiff instance, which contains the differences between the two given versions. * @param {number | string} [datasetId] - The dataset identifier, which can be a string (for persistent identifiers), or a number (for numeric identifiers). - * @param {string } [newVersionId] - The dataset version identifier, which can be a version-specific numeric string (for example, 1.0) or a DatasetNotNumberedVersion enum value. * @param {string } [oldVersionId] - The dataset version identifier, which can be a version-specific numeric string (for example, 1.0) or a DatasetNotNumberedVersion enum value. + * @param {string } [newVersionId] - The dataset version identifier, which can be a version-specific numeric string (for example, 1.0) or a DatasetNotNumberedVersion enum value. */ async execute( datasetId: number | string, - newVersionId: string, - oldVersionId: string + oldVersionId: string, + newVersionId: string ): Promise { return await this.datasetsRepository.getDatasetVersionDiff( datasetId, - newVersionId, - oldVersionId + oldVersionId, + newVersionId ) } } diff --git a/src/datasets/infra/repositories/DatasetsRepository.ts b/src/datasets/infra/repositories/DatasetsRepository.ts index 2ade4976..a4491291 100644 --- a/src/datasets/infra/repositories/DatasetsRepository.ts +++ b/src/datasets/infra/repositories/DatasetsRepository.ts @@ -145,8 +145,8 @@ export class DatasetsRepository extends ApiRepository implements IDatasetsReposi public async getDatasetVersionDiff( datasetId: string | number, - newVersionId: string, - oldVersionId: string + oldVersionId: string, + newVersionId: string ): Promise { return this.doGet( this.buildApiEndpoint( diff --git a/test/integration/datasets/DatasetsRepository.test.ts b/test/integration/datasets/DatasetsRepository.test.ts index 4273d2bf..113e2c98 100644 --- a/test/integration/datasets/DatasetsRepository.test.ts +++ b/test/integration/datasets/DatasetsRepository.test.ts @@ -490,8 +490,8 @@ describe('DatasetsRepository', () => { ]) const actual = await sut.getDatasetVersionDiff( testDatasetIds.numericId, - DatasetNotNumberedVersion.DRAFT, - '1.0' + '1.0', + DatasetNotNumberedVersion.DRAFT ) expect(actual.metadataChanges[0].blockName).toEqual('Citation Metadata') }) @@ -516,8 +516,8 @@ describe('DatasetsRepository', () => { await uploadFileViaApi(testDatasetIds.numericId, testTextFile1Name, fileMetadata) const actual = await sut.getDatasetVersionDiff( testDatasetIds.numericId, - DatasetNotNumberedVersion.DRAFT, - '1.0' + '1.0', + DatasetNotNumberedVersion.DRAFT ) expect(actual.filesAdded).toEqual(expectedFilesAdded) }) From f0d3fb6840f4223419c251bd9d97341c1649a2c3 Mon Sep 17 00:00:00 2001 From: Ellen Kraffmiller Date: Thu, 14 Nov 2024 10:09:08 -0500 Subject: [PATCH 4/9] fix: useCases.md formatting --- docs/useCases.md | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/docs/useCases.md b/docs/useCases.md index a155f10d..a506a93e 100644 --- a/docs/useCases.md +++ b/docs/useCases.md @@ -444,9 +444,11 @@ const datasetId = 'doi:10.77777/FK2/AAAAAA' const oldVersion = '1.0' const newVersion = '2.0' -getDatasetVersionDiff.execute(datasetId, oldVersion, newVersion).then((versionDiff: DatasetVersionDiff) => { - /* ... */ -}) +lgetDatasetVersionDiff + .execute(datasetId, oldVersion, newVersion) + .then((versionDiff: DatasetVersionDiff) => { + /* ... */ + }) /* ... */ ``` @@ -457,7 +459,6 @@ The `datasetId` parameter can be a string, for persistent identifiers, or a numb The `oldVersion` and `newVersion` parameters specify the versions of the dataset to compare. - #### List All Datasets Returns an instance of [DatasetPreviewSubset](../src/datasets/domain/models/DatasetPreviewSubset.ts) that contains reduced information for each dataset that the calling user can access in the installation. From 68c9f95e5418a9d31ee56f7f80ed7e1146a49f8c Mon Sep 17 00:00:00 2001 From: Ellen Kraffmiller Date: Thu, 14 Nov 2024 10:10:03 -0500 Subject: [PATCH 5/9] set test environment to dataverse PR image --- test/environment/.env | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/test/environment/.env b/test/environment/.env index 80e9a14e..c74bacd1 100644 --- a/test/environment/.env +++ b/test/environment/.env @@ -1,6 +1,6 @@ POSTGRES_VERSION=13 DATAVERSE_DB_USER=dataverse SOLR_VERSION=9.3.0 -DATAVERSE_IMAGE_REGISTRY=docker.io -DATAVERSE_IMAGE_TAG=unstable +DATAVERSE_IMAGE_REGISTRY=ghcr.io +DATAVERSE_IMAGE_TAG=10888-add-api-for-comparing-dataset-versions DATAVERSE_BOOTSTRAP_TIMEOUT=5m From 5174e64fe2482db3c0fbdcac6dea8511292411b9 Mon Sep 17 00:00:00 2001 From: Ellen Kraffmiller Date: Thu, 14 Nov 2024 14:59:33 -0500 Subject: [PATCH 6/9] add DatasetVersionDiff to exports --- src/datasets/index.ts | 1 + 1 file changed, 1 insertion(+) diff --git a/src/datasets/index.ts b/src/datasets/index.ts index 83572caa..2eaaed5d 100644 --- a/src/datasets/index.ts +++ b/src/datasets/index.ts @@ -76,6 +76,7 @@ export { DatasetMetadataFieldValue } from './domain/models/Dataset' export { DatasetPreview } from './domain/models/DatasetPreview' +export { DatasetVersionDiff } from './domain/models/DatasetVersionDiff' export { DatasetPreviewSubset } from './domain/models/DatasetPreviewSubset' export { DatasetDTO, From ee8a024074c7ada6779dc08ab68674cd2a2ed9e1 Mon Sep 17 00:00:00 2001 From: Ellen Kraffmiller Date: Thu, 14 Nov 2024 15:19:04 -0500 Subject: [PATCH 7/9] add integration test --- .../datasets/DatasetsRepository.test.ts | 37 ++++++++++++++++++- 1 file changed, 35 insertions(+), 2 deletions(-) diff --git a/test/integration/datasets/DatasetsRepository.test.ts b/test/integration/datasets/DatasetsRepository.test.ts index 91a0dadc..141c5bf5 100644 --- a/test/integration/datasets/DatasetsRepository.test.ts +++ b/test/integration/datasets/DatasetsRepository.test.ts @@ -472,7 +472,7 @@ describe('DatasetsRepository', () => { describe('getDatasetVersionDiff', () => { let testDatasetIds: CreatedDatasetIdentifiers - beforeAll(async () => { + beforeEach(async () => { testDatasetIds = await createDataset.execute(TestConstants.TEST_NEW_DATASET_DTO) // Dataset is in draft, so we need to publish it first await sut.publishDataset(testDatasetIds.numericId, VersionUpdateType.MAJOR) @@ -528,7 +528,40 @@ describe('DatasetsRepository', () => { ) expect(actual.filesAdded).toEqual(expectedFilesAdded) }) - afterAll(async () => { + test('should return diff between :latestPublished and :draft', async () => { + const fileMetadata = { + description: 'test description', + directoryLabel: 'directoryLabel', + categories: ['category1', 'category2'] + } + + const uploadResponse = await uploadFileViaApi( + testDatasetIds.numericId, + testTextFile1Name, + fileMetadata + ) + + const fileId = uploadResponse.data.data.files[0].dataFile.id + const expectedFilesAdded = [ + { + fileName: 'test-file-1.txt', + type: 'text/plain', + isRestricted: false, + description: fileMetadata.description, + filePath: fileMetadata.directoryLabel, + categories: fileMetadata.categories, + MD5: '68b22040025784da775f55cfcb6dee2e', + fileId: fileId + } + ] + const actual = await sut.getDatasetVersionDiff( + testDatasetIds.numericId, + DatasetNotNumberedVersion.LATEST_PUBLISHED, + DatasetNotNumberedVersion.DRAFT + ) + expect(actual.filesAdded).toEqual(expectedFilesAdded) + }) + afterEach(async () => { await deletePublishedDatasetViaApi(testDatasetIds.persistentId) }) }) From 97ffd51ff33c894541dfcbffedd3e713eeec09eb Mon Sep 17 00:00:00 2001 From: Ellen Kraffmiller Date: Mon, 25 Nov 2024 09:11:56 -0500 Subject: [PATCH 8/9] fixed spacing, removed comments, added test for undefined --- src/datasets/domain/models/DatasetVersionDiff.ts | 2 -- .../transformers/datasetVersionDiffTransformers.ts | 10 ---------- test/integration/datasets/DatasetsRepository.test.ts | 5 +++++ test/unit/datasets/GetDatasetVersionDiff.test.ts | 1 - 4 files changed, 5 insertions(+), 13 deletions(-) diff --git a/src/datasets/domain/models/DatasetVersionDiff.ts b/src/datasets/domain/models/DatasetVersionDiff.ts index bda416a2..e0f94fcf 100644 --- a/src/datasets/domain/models/DatasetVersionDiff.ts +++ b/src/datasets/domain/models/DatasetVersionDiff.ts @@ -1,5 +1,3 @@ -// Details API - export interface DatasetVersionDiff { oldVersion: VersionSummary newVersion: VersionSummary diff --git a/src/datasets/infra/repositories/transformers/datasetVersionDiffTransformers.ts b/src/datasets/infra/repositories/transformers/datasetVersionDiffTransformers.ts index a9385794..9072c6bc 100644 --- a/src/datasets/infra/repositories/transformers/datasetVersionDiffTransformers.ts +++ b/src/datasets/infra/repositories/transformers/datasetVersionDiffTransformers.ts @@ -1,16 +1,6 @@ import { AxiosResponse } from 'axios' import { DatasetVersionDiff } from '../../../domain/models/DatasetVersionDiff' -/* - oldVersion: VersionSummaryPayload - newVersion: VersionSummaryPayload - metadataChanges: MetadataBlockDiffPayload[] - filesAdded: FileSummaryPayload[] - filesRemoved: FileSummaryPayload[] - fileChanges: FileDiffPayload[] - filesReplaced: FileReplacementPayload[] - TermsOfAccess: FieldDiffPayload[] - */ export const transformDatasetVersionDiffResponseToDatasetVersionDiff = ( response: AxiosResponse ): DatasetVersionDiff => { diff --git a/test/integration/datasets/DatasetsRepository.test.ts b/test/integration/datasets/DatasetsRepository.test.ts index 141c5bf5..f946812f 100644 --- a/test/integration/datasets/DatasetsRepository.test.ts +++ b/test/integration/datasets/DatasetsRepository.test.ts @@ -478,6 +478,7 @@ describe('DatasetsRepository', () => { await sut.publishDataset(testDatasetIds.numericId, VersionUpdateType.MAJOR) await waitForNoLocks(testDatasetIds.numericId, 10) }) + test('should return dataset metadata diff between two dataset versions', async () => { // Update dataset const metadataBlocksRepository = new MetadataBlocksRepository() @@ -493,8 +494,10 @@ describe('DatasetsRepository', () => { '1.0', DatasetNotNumberedVersion.DRAFT ) + expect(actual.metadataChanges[0]).not.toBeUndefined() expect(actual.metadataChanges[0].blockName).toEqual('Citation Metadata') }) + test('should return added file diff between two dataset versions', async () => { const fileMetadata = { description: 'test description', @@ -528,6 +531,7 @@ describe('DatasetsRepository', () => { ) expect(actual.filesAdded).toEqual(expectedFilesAdded) }) + test('should return diff between :latestPublished and :draft', async () => { const fileMetadata = { description: 'test description', @@ -561,6 +565,7 @@ describe('DatasetsRepository', () => { ) expect(actual.filesAdded).toEqual(expectedFilesAdded) }) + afterEach(async () => { await deletePublishedDatasetViaApi(testDatasetIds.persistentId) }) diff --git a/test/unit/datasets/GetDatasetVersionDiff.test.ts b/test/unit/datasets/GetDatasetVersionDiff.test.ts index 0faaf83a..8c6cb5ee 100644 --- a/test/unit/datasets/GetDatasetVersionDiff.test.ts +++ b/test/unit/datasets/GetDatasetVersionDiff.test.ts @@ -1,6 +1,5 @@ import { ReadError } from '../../../src/core/domain/repositories/ReadError' import { IDatasetsRepository } from '../../../src/datasets/domain/repositories/IDatasetsRepository' - import { createDatasetVersionDiff } from '../../testHelpers/datasets/datasetVersionDiffHelper' import { GetDatasetVersionDiff } from '../../../src/datasets/domain/useCases/GetDatasetVersionDiff' From 2faf06bc67ced71dcdecd48a38c806523480f6a0 Mon Sep 17 00:00:00 2001 From: Cheng Shi Date: Mon, 25 Nov 2024 15:15:05 -0500 Subject: [PATCH 9/9] chore: remove some comments --- .../transformers/DatasetVersionDiffPayload.ts | 130 ------------------ 1 file changed, 130 deletions(-) diff --git a/src/datasets/infra/repositories/transformers/DatasetVersionDiffPayload.ts b/src/datasets/infra/repositories/transformers/DatasetVersionDiffPayload.ts index baef6be2..b9016732 100644 --- a/src/datasets/infra/repositories/transformers/DatasetVersionDiffPayload.ts +++ b/src/datasets/infra/repositories/transformers/DatasetVersionDiffPayload.ts @@ -1,133 +1,3 @@ -/* -{ - "status": "OK", - "data": { - "oldVersion": { - "versionNumber": "1.0", - "lastUpdatedDate": "2024-10-24T15:17:11Z" - }, - "newVersion": { - "versionNumber": "DRAFT", - "lastUpdatedDate": "2024-10-24T15:17:16Z" - }, - "metadataChanges": [ - { - "blockName": "Citation Metadata", - "changed": [ - { - "fieldName": "Author", - "oldValue": "Finch, Fiona; (Birds Inc.)", - "newValue": "Finch, Fiona; (Birds Inc.); Poe, Edgar Allen; (Baltimore Poets); Mulligan, Hercules; (Sons of Liberty)" - }, - { - "fieldName": "Subject", - "oldValue": "Medicine, Health and Life Sciences", - "newValue": "Medicine, Health and Life Sciences; Astronomy and Astrophysics; Other" - }, - { - "fieldName": "Producer", - "oldValue": "", - "newValue": "Allen, Irwin; (MGM); Spielberg, Stephen; (ILM)" - } - ] - }, - { - "blockName": "Life Sciences Metadata", - "changed": [ - { - "fieldName": "Design Type", - "oldValue": "", - "newValue": "Parallel Group Design; Nested Case Control Design" - } - ] - } - ], - "filesAdded": [ - { - "fileName": "test.tab", - "filePath": "data/subdir1", - "MD5": "77c7f03a7d7772907b43f0b322cef723", - "type": "text/tab-separated-values", - "fileId": 42, - "description": "my description", - "isRestricted": false, - "categories": [ - "Data" - ], - "tags": [ - "Survey" - ] - } - ], - "filesRemoved": [ - { - "fileName": "dataverseproject_logo.jpg", - "filePath": "data/subdir1", - "MD5": "c1edbefa86a55c5037873370ae7fd7b6", - "type": "image/jpeg", - "fileId": 40, - "description": "my description", - "isRestricted": false, - "categories": [ - "Data" - ] - } - ], - "filesReplaced": [ - { - "oldFile": { - "fileName": "favicon-16x16.png", - "filePath": "data/subdir1", - "MD5": "d3c852e7ecb92fd105ba4018116a9be8", - "type": "image/png", - "fileId": 41, - "description": "my description", - "isRestricted": false, - "categories": [ - "Data" - ] - }, - "newFile": { - "fileName": "favicon-32x32.png", - "filePath": "data/subdir1", - "MD5": "c931f7add8b6a1f9a691046b77c231fa", - "type": "image/png", - "fileId": 43, - "description": "my description", - "isRestricted": false, - "categories": [ - "Data" - ] - } - } - ], - "fileChanges": [ - { - "fileName": "dataverse-icon-1200.png", - "MD5": "a23eb44803d9127bc6e055f77b869816", - "fileId": 39, - "changed": [ - { - "fieldName": "isRestricted", - "oldValue": "false", - "newValue": "true" - } - ] - } - ], - "TermsOfAccess": { - "changed": [ - { - "fieldName": "Data Access Place", - "oldValue": "", - "newValue": "Somewhere" - } - ] - } - } -} - */ - export interface DatasetVersionDiffPayload { oldVersion: VersionSummaryPayload newVersion: VersionSummaryPayload