From c7f5bafe75949373fd6a90bd859cc4fb45ad441b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Germ=C3=A1n=20Saracca?= Date: Fri, 26 Sep 2025 11:32:44 -0300 Subject: [PATCH 1/8] feat: update repo --- package-lock.json | 8 ++++---- package.json | 2 +- src/dataset/domain/repositories/DatasetRepository.ts | 2 +- src/dataset/domain/useCases/updateDatasetMetadata.ts | 4 ++-- .../repositories/DatasetJSDataverseRepository.ts | 4 ++-- 5 files changed, 10 insertions(+), 10 deletions(-) diff --git a/package-lock.json b/package-lock.json index 0b8b14475..37f4c8d6a 100644 --- a/package-lock.json +++ b/package-lock.json @@ -13,7 +13,7 @@ "@dnd-kit/sortable": "8.0.0", "@dnd-kit/utilities": "3.2.2", "@faker-js/faker": "7.6.0", - "@iqss/dataverse-client-javascript": "2.0.0-alpha.62", + "@iqss/dataverse-client-javascript": "2.0.0-pr379.a1cef4b", "@iqss/dataverse-design-system": "*", "@istanbuljs/nyc-config-typescript": "1.0.2", "@tanstack/react-table": "8.9.2", @@ -1954,9 +1954,9 @@ }, "node_modules/@iqss/dataverse-client-javascript": { "name": "@IQSS/dataverse-client-javascript", - "version": "2.0.0-alpha.62", - "resolved": "https://npm.pkg.github.com/download/@IQSS/dataverse-client-javascript/2.0.0-alpha.62/61ac45ca90983c86af8adb443bc013ea929db0ad", - "integrity": "sha512-BtblnMfg6a0m6E8bbcwkZ7aEOBHeLzRbkaJVcKFybxWz33h76Xjr1TIOKcd9cwGNoaAFxReigGGp8EWmWwqehA==", + "version": "2.0.0-pr379.a1cef4b", + "resolved": "https://npm.pkg.github.com/download/@IQSS/dataverse-client-javascript/2.0.0-pr379.a1cef4b/91389b1ec69e74f5776700fc34142ae794c441f7", + "integrity": "sha512-lxsc8XM6K193oq9mZXu/7vfbgpOE/XK+b8eLY9Wir4WegazEikf4npfpor/FNFt4VpFWwD+oXd2ov5b2fX+QOA==", "license": "MIT", "dependencies": { "@types/node": "^18.15.11", diff --git a/package.json b/package.json index 7a4dafa43..27a7d7a1d 100644 --- a/package.json +++ b/package.json @@ -18,7 +18,7 @@ "@dnd-kit/sortable": "8.0.0", "@dnd-kit/utilities": "3.2.2", "@faker-js/faker": "7.6.0", - "@iqss/dataverse-client-javascript": "2.0.0-alpha.62", + "@iqss/dataverse-client-javascript": "2.0.0-pr379.a1cef4b", "@iqss/dataverse-design-system": "*", "@istanbuljs/nyc-config-typescript": "1.0.2", "@tanstack/react-table": "8.9.2", diff --git a/src/dataset/domain/repositories/DatasetRepository.ts b/src/dataset/domain/repositories/DatasetRepository.ts index 17e4d5670..6eeeb08a1 100644 --- a/src/dataset/domain/repositories/DatasetRepository.ts +++ b/src/dataset/domain/repositories/DatasetRepository.ts @@ -29,7 +29,7 @@ export interface DatasetRepository { updateMetadata: ( datasetId: string | number, datasetDTO: DatasetDTO, - internalVersionNumber: number + sourceLastUpdateTime?: string ) => Promise deaccession: ( datasetId: string | number, diff --git a/src/dataset/domain/useCases/updateDatasetMetadata.ts b/src/dataset/domain/useCases/updateDatasetMetadata.ts index d3a6c43c2..ec7e5cadd 100644 --- a/src/dataset/domain/useCases/updateDatasetMetadata.ts +++ b/src/dataset/domain/useCases/updateDatasetMetadata.ts @@ -5,10 +5,10 @@ export function updateDatasetMetadata( datasetRepository: DatasetRepository, datasetId: string | number, updatedDataset: DatasetDTO, - internalVersionNumber: number + sourceLastUpdateTime?: string ): Promise { return datasetRepository - .updateMetadata(datasetId, updatedDataset, internalVersionNumber) + .updateMetadata(datasetId, updatedDataset, sourceLastUpdateTime) .catch((error: Error) => { throw new Error(error.message) }) diff --git a/src/dataset/infrastructure/repositories/DatasetJSDataverseRepository.ts b/src/dataset/infrastructure/repositories/DatasetJSDataverseRepository.ts index c1519b352..9e08a0b66 100644 --- a/src/dataset/infrastructure/repositories/DatasetJSDataverseRepository.ts +++ b/src/dataset/infrastructure/repositories/DatasetJSDataverseRepository.ts @@ -348,10 +348,10 @@ export class DatasetJSDataverseRepository implements DatasetRepository { updateMetadata( datasetId: string | number, updatedDataset: DatasetDTO, - internalVersionNumber: number + sourceLastUpdateTime?: string ): Promise { return updateDataset - .execute(datasetId, DatasetDTOMapper.toJSDatasetDTO(updatedDataset), internalVersionNumber) + .execute(datasetId, DatasetDTOMapper.toJSDatasetDTO(updatedDataset), sourceLastUpdateTime) .catch((error: WriteError) => { throw new Error(error.message) }) From 5133aa150de07f73c8b7b52b1ee1b1fd9a9f6c4f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Germ=C3=A1n=20Saracca?= Date: Fri, 26 Sep 2025 11:56:08 -0300 Subject: [PATCH 2/8] feat: add lastUpdateTime to DatasetVersion and related mappers --- src/dataset/domain/models/Dataset.ts | 3 +++ src/dataset/infrastructure/mappers/JSDatasetMapper.ts | 1 + src/dataset/infrastructure/mappers/JSDatasetPreviewMapper.ts | 3 ++- src/dataset/infrastructure/mappers/JSDatasetVersionMapper.ts | 2 ++ src/files/infrastructure/mappers/JSFileMapper.ts | 1 + tests/component/dataset/domain/models/DatasetMother.ts | 3 ++- .../dataset/infrastructure/mappers/JSDatasetMapper.spec.ts | 2 +- 7 files changed, 12 insertions(+), 3 deletions(-) diff --git a/src/dataset/domain/models/Dataset.ts b/src/dataset/domain/models/Dataset.ts index 1edb088fc..0afd20955 100644 --- a/src/dataset/domain/models/Dataset.ts +++ b/src/dataset/domain/models/Dataset.ts @@ -255,6 +255,7 @@ export class DatasetVersion { public readonly isInReview: boolean, public readonly latestVersionPublishingStatus: DatasetPublishingStatus, public readonly someDatasetVersionHasBeenReleased: boolean, + public readonly lastUpdateTime: string, public readonly termsOfAccess?: TermsOfAccess, public readonly deaccessionNote?: string ) {} @@ -271,6 +272,7 @@ export class DatasetVersion { public readonly isInReview: boolean, public readonly latestVersionPublishingStatus: DatasetPublishingStatus, public readonly someDatasetVersionHasBeenReleased: boolean, + public readonly lastUpdateTime: string, public readonly termsOfAccess?: TermsOfAccess, public readonly deaccessionNote?: string ) { @@ -339,6 +341,7 @@ export class DatasetVersion { this.isInReview, this.latestVersionPublishingStatus, this.someDatasetVersionHasBeenReleased, + this.lastUpdateTime, this.termsOfAccess, this.deaccessionNote ) diff --git a/src/dataset/infrastructure/mappers/JSDatasetMapper.ts b/src/dataset/infrastructure/mappers/JSDatasetMapper.ts index edb9d5a70..a147285e5 100644 --- a/src/dataset/infrastructure/mappers/JSDatasetMapper.ts +++ b/src/dataset/infrastructure/mappers/JSDatasetMapper.ts @@ -56,6 +56,7 @@ export class JSDatasetMapper { jsDataset.versionInfo, JSDatasetMapper.toDatasetTitle(jsDataset.metadataBlocks), jsDatasetCitation, + jsDataset.versionInfo.lastUpdateTime, jsDataset.publicationDate, jsDataset.termsOfUse?.termsOfAccess, jsDataset.versionInfo.deaccessionNote as string diff --git a/src/dataset/infrastructure/mappers/JSDatasetPreviewMapper.ts b/src/dataset/infrastructure/mappers/JSDatasetPreviewMapper.ts index f738592d4..a9bf65df4 100644 --- a/src/dataset/infrastructure/mappers/JSDatasetPreviewMapper.ts +++ b/src/dataset/infrastructure/mappers/JSDatasetPreviewMapper.ts @@ -12,7 +12,8 @@ export class JSDatasetPreviewMapper { jsDatasetPreview.versionId, jsDatasetPreview.versionInfo, jsDatasetPreview.title, - jsDatasetPreview.citation + jsDatasetPreview.citation, + jsDatasetPreview.versionInfo.lastUpdateTime ), releaseOrCreateDate: JSDatasetPreviewMapper.toPreviewDate(jsDatasetPreview.versionInfo), description: jsDatasetPreview.description, diff --git a/src/dataset/infrastructure/mappers/JSDatasetVersionMapper.ts b/src/dataset/infrastructure/mappers/JSDatasetVersionMapper.ts index 63b3fc8a3..ab12ee12f 100644 --- a/src/dataset/infrastructure/mappers/JSDatasetVersionMapper.ts +++ b/src/dataset/infrastructure/mappers/JSDatasetVersionMapper.ts @@ -15,6 +15,7 @@ export class JSDatasetVersionMapper { jsDatasetVersionInfo: JSDatasetVersionInfo, jsDatasetTitle: string, jsDatasetCitation: string, + jsDatasetLastUpdateTime: string, jsDatasetPublicationDate?: string, jsDatasettermsOfAccess?: TermsOfAccess, jsDatasetDeaccessionedNote?: string @@ -29,6 +30,7 @@ export class JSDatasetVersionMapper { false, // TODO Connect with dataset version isInReview this.toStatus(jsDatasetVersionInfo.state), this.toSomeDatasetVersionHasBeenReleased(jsDatasetVersionInfo, jsDatasetPublicationDate), + jsDatasetLastUpdateTime, jsDatasettermsOfAccess, jsDatasetDeaccessionedNote ) diff --git a/src/files/infrastructure/mappers/JSFileMapper.ts b/src/files/infrastructure/mappers/JSFileMapper.ts index d2ecb3704..7b1610f17 100644 --- a/src/files/infrastructure/mappers/JSFileMapper.ts +++ b/src/files/infrastructure/mappers/JSFileMapper.ts @@ -55,6 +55,7 @@ export class JSFileMapper { jsDataset.versionInfo, JSDatasetMapper.toDatasetTitle(jsDataset.metadataBlocks), datasetCitation, + jsDataset.versionInfo.lastUpdateTime, jsDataset.publicationDate, jsDataset.termsOfUse?.termsOfAccess ) diff --git a/tests/component/dataset/domain/models/DatasetMother.ts b/tests/component/dataset/domain/models/DatasetMother.ts index 661a2e6e9..24f1d697f 100644 --- a/tests/component/dataset/domain/models/DatasetMother.ts +++ b/tests/component/dataset/domain/models/DatasetMother.ts @@ -41,7 +41,8 @@ export class DatasetVersionMother { props?.isLatest ?? false, props?.isInReview ?? false, props?.latestVersionPublishingStatus ?? DatasetPublishingStatus.RELEASED, - props?.someDatasetVersionHasBeenReleased ?? faker.datatype.boolean() + props?.someDatasetVersionHasBeenReleased ?? faker.datatype.boolean(), + props?.lastUpdateTime ?? faker.date.recent().toISOString() ) } diff --git a/tests/component/dataset/infrastructure/mappers/JSDatasetMapper.spec.ts b/tests/component/dataset/infrastructure/mappers/JSDatasetMapper.spec.ts index ef11e886c..390079cf5 100644 --- a/tests/component/dataset/infrastructure/mappers/JSDatasetMapper.spec.ts +++ b/tests/component/dataset/infrastructure/mappers/JSDatasetMapper.spec.ts @@ -44,7 +44,7 @@ const jsDataset = { majorNumber: 0, minorNumber: 0, createTime: new Date('2023-09-07T13:40:04.000Z'), - lastUpdateTime: new Date('2023-09-07T13:40:04.000Z'), + lastUpdateTime: '2023-09-07T13:40:04.000Z', releaseTime: undefined, deaccessionNote: undefined }, From b1258a903992e6e834eb110209e657efc81ea74a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Germ=C3=A1n=20Saracca?= Date: Fri, 26 Sep 2025 12:06:40 -0300 Subject: [PATCH 3/8] feat: switch to use dataset last update time as optimistic concurrency --- src/dataset/domain/models/Dataset.ts | 5 +++++ .../edit-dataset-metadata/EditDatasetMetadata.tsx | 2 +- .../form/DatasetMetadataForm/MetadataForm/index.tsx | 6 +++--- src/sections/shared/form/DatasetMetadataForm/index.tsx | 8 ++++---- .../shared/form/DatasetMetadataForm/useSubmitDataset.ts | 4 ++-- 5 files changed, 15 insertions(+), 10 deletions(-) diff --git a/src/dataset/domain/models/Dataset.ts b/src/dataset/domain/models/Dataset.ts index 0afd20955..74228cd51 100644 --- a/src/dataset/domain/models/Dataset.ts +++ b/src/dataset/domain/models/Dataset.ts @@ -255,6 +255,11 @@ export class DatasetVersion { public readonly isInReview: boolean, public readonly latestVersionPublishingStatus: DatasetPublishingStatus, public readonly someDatasetVersionHasBeenReleased: boolean, + /** + * The timestamp of the last update to this dataset version. + * Format: ISO 8601 string (e.g., "2023-06-01T12:34:56Z"). + * Used for optimistic concurrency control to detect concurrent updates. + */ public readonly lastUpdateTime: string, public readonly termsOfAccess?: TermsOfAccess, public readonly deaccessionNote?: string diff --git a/src/sections/edit-dataset-metadata/EditDatasetMetadata.tsx b/src/sections/edit-dataset-metadata/EditDatasetMetadata.tsx index 6b926d2fd..8b2e07b08 100644 --- a/src/sections/edit-dataset-metadata/EditDatasetMetadata.tsx +++ b/src/sections/edit-dataset-metadata/EditDatasetMetadata.tsx @@ -70,7 +70,7 @@ export const EditDatasetMetadata = ({ metadataBlockInfoRepository={metadataBlockInfoRepository} datasetPersistentID={dataset.persistentId} datasetMetadaBlocksCurrentValues={dataset.metadataBlocks} - datasetInternalVersionNumber={dataset.internalVersionNumber} + datasetLastUpdateTime={dataset.version.lastUpdateTime} /> diff --git a/src/sections/shared/form/DatasetMetadataForm/MetadataForm/index.tsx b/src/sections/shared/form/DatasetMetadataForm/MetadataForm/index.tsx index 17d3dfc71..d94ff801c 100644 --- a/src/sections/shared/form/DatasetMetadataForm/MetadataForm/index.tsx +++ b/src/sections/shared/form/DatasetMetadataForm/MetadataForm/index.tsx @@ -24,7 +24,7 @@ interface FormProps { errorLoadingMetadataBlocksInfo: string | null datasetRepository: DatasetRepository datasetPersistentID?: string - datasetInternalVersionNumber?: number + datasetLastUpdateTime?: string } export const MetadataForm = ({ @@ -35,7 +35,7 @@ export const MetadataForm = ({ errorLoadingMetadataBlocksInfo, datasetRepository, datasetPersistentID, - datasetInternalVersionNumber + datasetLastUpdateTime }: FormProps) => { const { user } = useSession() const navigate = useNavigate() @@ -57,7 +57,7 @@ export const MetadataForm = ({ datasetRepository, onSubmitDatasetError, datasetPersistentID, - datasetInternalVersionNumber + datasetLastUpdateTime ) useEffect(() => { diff --git a/src/sections/shared/form/DatasetMetadataForm/index.tsx b/src/sections/shared/form/DatasetMetadataForm/index.tsx index 4ebd7aaab..4bb07a30d 100644 --- a/src/sections/shared/form/DatasetMetadataForm/index.tsx +++ b/src/sections/shared/form/DatasetMetadataForm/index.tsx @@ -17,7 +17,7 @@ type DatasetMetadataFormProps = datasetPersistentID?: never metadataBlockInfoRepository: MetadataBlockInfoRepository datasetMetadaBlocksCurrentValues?: never - datasetInternalVersionNumber?: never + datasetLastUpdateTime?: never } | { mode: 'edit' @@ -26,7 +26,7 @@ type DatasetMetadataFormProps = datasetPersistentID: string metadataBlockInfoRepository: MetadataBlockInfoRepository datasetMetadaBlocksCurrentValues: DatasetMetadataBlocks - datasetInternalVersionNumber: number + datasetLastUpdateTime?: string } export type DatasetMetadataFormMode = 'create' | 'edit' @@ -38,7 +38,7 @@ export const DatasetMetadataForm = ({ datasetPersistentID, metadataBlockInfoRepository, datasetMetadaBlocksCurrentValues, - datasetInternalVersionNumber + datasetLastUpdateTime }: DatasetMetadataFormProps) => { const { setIsLoading } = useLoading() const onEditMode = mode === 'edit' @@ -115,7 +115,7 @@ export const DatasetMetadataForm = ({ errorLoadingMetadataBlocksInfo={errorLoadingMetadataBlocksInfo} datasetRepository={datasetRepository} datasetPersistentID={datasetPersistentID} - datasetInternalVersionNumber={datasetInternalVersionNumber} + datasetLastUpdateTime={datasetLastUpdateTime} /> ) } diff --git a/src/sections/shared/form/DatasetMetadataForm/useSubmitDataset.ts b/src/sections/shared/form/DatasetMetadataForm/useSubmitDataset.ts index c10f6eafb..7a6afbe1d 100644 --- a/src/sections/shared/form/DatasetMetadataForm/useSubmitDataset.ts +++ b/src/sections/shared/form/DatasetMetadataForm/useSubmitDataset.ts @@ -39,7 +39,7 @@ export function useSubmitDataset( datasetRepository: DatasetRepository, onSubmitErrorCallback: () => void, datasetPersistentID?: string, - datasetInternalVersionNumber?: number + datasetLastUpdateTime?: string ): UseSubmitDatasetReturnType { const navigate = useNavigate() const { t } = useTranslation('shared', { keyPrefix: 'datasetMetadataForm' }) @@ -89,7 +89,7 @@ export function useSubmitDataset( datasetRepository, currentEditedDatasetPersistentID, formattedFormValues, - datasetInternalVersionNumber as number + datasetLastUpdateTime as string ) .then(() => { setSubmitError(null) From 75e2d7d84901a18270d0e459b88613a5fdb56cea Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Germ=C3=A1n=20Saracca?= Date: Fri, 26 Sep 2025 12:22:28 -0300 Subject: [PATCH 4/8] fix: update test and stories due to updated prop --- .../DatasetMetadataForm.stories.tsx | 2 +- .../DatasetMetadataForm.spec.tsx | 20 +++++++++---------- 2 files changed, 11 insertions(+), 11 deletions(-) diff --git a/src/stories/shared/dataset-metadata-form/DatasetMetadataForm.stories.tsx b/src/stories/shared/dataset-metadata-form/DatasetMetadataForm.stories.tsx index 367e0bf2c..efd1242e7 100644 --- a/src/stories/shared/dataset-metadata-form/DatasetMetadataForm.stories.tsx +++ b/src/stories/shared/dataset-metadata-form/DatasetMetadataForm.stories.tsx @@ -40,7 +40,7 @@ export const EditMode: Story = { metadataBlockInfoRepository={new MetadataBlockInfoMockRepository()} datasetPersistentID={datasetToEditMock.persistentId} datasetMetadaBlocksCurrentValues={datasetToEditMock.metadataBlocks} - datasetInternalVersionNumber={1} + datasetLastUpdateTime="2023-06-01T12:34:56Z" /> ) } diff --git a/tests/component/sections/shared/dataset-metadata-form/DatasetMetadataForm.spec.tsx b/tests/component/sections/shared/dataset-metadata-form/DatasetMetadataForm.spec.tsx index 9315bb0e7..374190121 100644 --- a/tests/component/sections/shared/dataset-metadata-form/DatasetMetadataForm.spec.tsx +++ b/tests/component/sections/shared/dataset-metadata-form/DatasetMetadataForm.spec.tsx @@ -233,7 +233,7 @@ describe('DatasetMetadataForm', () => { metadataBlockInfoRepository={metadataBlockInfoRepository} datasetPersistentID={dataset.persistentId} datasetMetadaBlocksCurrentValues={dataset.metadataBlocks} - datasetInternalVersionNumber={dataset.internalVersionNumber} + datasetLastUpdateTime={dataset.version.lastUpdateTime} /> ) @@ -493,7 +493,7 @@ describe('DatasetMetadataForm', () => { metadataBlockInfoRepository={metadataBlockInfoRepository} datasetPersistentID={dataset.persistentId} datasetMetadaBlocksCurrentValues={dataset.metadataBlocks} - datasetInternalVersionNumber={dataset.internalVersionNumber} + datasetLastUpdateTime={dataset.version.lastUpdateTime} /> ) }) @@ -1247,7 +1247,7 @@ describe('DatasetMetadataForm', () => { metadataBlockInfoRepository={metadataBlockInfoRepository} datasetPersistentID={dataset.persistentId} datasetMetadaBlocksCurrentValues={dataset.metadataBlocks} - datasetInternalVersionNumber={dataset.internalVersionNumber} + datasetLastUpdateTime={dataset.version.lastUpdateTime} /> ) // Clear title field to undisable the Save button and unfill a required field that is already filled as it is in edit mode @@ -1299,7 +1299,7 @@ describe('DatasetMetadataForm', () => { metadataBlockInfoRepository={metadataBlockInfoRepository} datasetPersistentID={dataset.persistentId} datasetMetadaBlocksCurrentValues={dataset.metadataBlocks} - datasetInternalVersionNumber={dataset.internalVersionNumber} + datasetLastUpdateTime={dataset.version.lastUpdateTime} /> ) cy.findByLabelText(/^Title/i) @@ -1650,7 +1650,7 @@ describe('DatasetMetadataForm', () => { metadataBlockInfoRepository={metadataBlockInfoRepository} datasetPersistentID={dataset.persistentId} datasetMetadaBlocksCurrentValues={dataset.metadataBlocks} - datasetInternalVersionNumber={dataset.internalVersionNumber} + datasetLastUpdateTime={dataset.version.lastUpdateTime} /> ) @@ -1682,7 +1682,7 @@ describe('DatasetMetadataForm', () => { metadataBlockInfoRepository={metadataBlockInfoRepository} datasetPersistentID={dataset.persistentId} datasetMetadaBlocksCurrentValues={dataset.metadataBlocks} - datasetInternalVersionNumber={dataset.internalVersionNumber} + datasetLastUpdateTime={dataset.version.lastUpdateTime} /> ) @@ -1711,7 +1711,7 @@ describe('DatasetMetadataForm', () => { metadataBlockInfoRepository={metadataBlockInfoRepository} datasetPersistentID={dataset.persistentId} datasetMetadaBlocksCurrentValues={dataset.metadataBlocks} - datasetInternalVersionNumber={dataset.internalVersionNumber} + datasetLastUpdateTime={dataset.version.lastUpdateTime} /> ) @@ -1768,7 +1768,7 @@ describe('DatasetMetadataForm', () => { metadataBlockInfoRepository={metadataBlockInfoRepository} datasetPersistentID={dataset.persistentId} datasetMetadaBlocksCurrentValues={dataset.metadataBlocks} - datasetInternalVersionNumber={dataset.internalVersionNumber} + datasetLastUpdateTime={dataset.version.lastUpdateTime} /> ) @@ -1832,7 +1832,7 @@ describe('DatasetMetadataForm', () => { metadataBlockInfoRepository={metadataBlockInfoRepository} datasetPersistentID={dataset.persistentId} datasetMetadaBlocksCurrentValues={dataset.metadataBlocks} - datasetInternalVersionNumber={dataset.internalVersionNumber} + datasetLastUpdateTime={dataset.version.lastUpdateTime} /> ) @@ -1872,7 +1872,7 @@ describe('DatasetMetadataForm', () => { metadataBlockInfoRepository={metadataBlockInfoRepository} datasetPersistentID={dataset.persistentId} datasetMetadaBlocksCurrentValues={dataset.metadataBlocks} - datasetInternalVersionNumber={dataset.internalVersionNumber} + datasetLastUpdateTime={dataset.version.lastUpdateTime} /> ) From a13789f9b707cc8619132371b6839df31b818d9c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Germ=C3=A1n=20Saracca?= Date: Fri, 26 Sep 2025 13:18:51 -0300 Subject: [PATCH 5/8] test: fix due to changes in mappers --- .../mappers/JSDatasetMapper.spec.ts | 4 ++ .../DatasetJSDataverseRepository.spec.ts | 41 +++++++++++++++---- 2 files changed, 38 insertions(+), 7 deletions(-) diff --git a/tests/component/dataset/infrastructure/mappers/JSDatasetMapper.spec.ts b/tests/component/dataset/infrastructure/mappers/JSDatasetMapper.spec.ts index 390079cf5..c4a38a696 100644 --- a/tests/component/dataset/infrastructure/mappers/JSDatasetMapper.spec.ts +++ b/tests/component/dataset/infrastructure/mappers/JSDatasetMapper.spec.ts @@ -231,6 +231,7 @@ const expectedDataset = { majorNumber: 0 }, someDatasetVersionHasBeenReleased: false, + lastUpdateTime: '2023-09-07T13:40:04.000Z', termsOfAccess: termsOfAccess, deaccessionNote: undefined, citation: @@ -349,6 +350,7 @@ const expectedDatasetWithPublicationDate = { majorNumber: 0 }, someDatasetVersionHasBeenReleased: true, + lastUpdateTime: '2023-09-07T13:40:04.000Z', citation: 'Finch, Fiona, 2023, "Darwin\'s Finches", https://doi.org/10.5072/FK2/B4B2MJ, Root, DRAFT VERSION' }, @@ -464,6 +466,7 @@ const expectedDatasetWithNextVersionNumbers = { majorNumber: 0 }, someDatasetVersionHasBeenReleased: true, + lastUpdateTime: '2023-09-07T13:40:04.000Z', termsOfAccess: termsOfAccess, deaccessionNote: undefined, citation: @@ -585,6 +588,7 @@ const expectedDatasetAlternateVersion = { majorNumber: 0 }, someDatasetVersionHasBeenReleased: false, + lastUpdateTime: '2023-09-07T13:40:04.000Z', termsOfAccess: termsOfAccess, deaccessionNote: undefined, citation: diff --git a/tests/e2e-integration/integration/datasets/DatasetJSDataverseRepository.spec.ts b/tests/e2e-integration/integration/datasets/DatasetJSDataverseRepository.spec.ts index 7443de61a..9fd227a54 100644 --- a/tests/e2e-integration/integration/datasets/DatasetJSDataverseRepository.spec.ts +++ b/tests/e2e-integration/integration/datasets/DatasetJSDataverseRepository.spec.ts @@ -58,7 +58,7 @@ const termsOfUse = { termsOfAccessForRestrictedFiles: undefined } -const datasetData = (persistentId: string, versionId: number) => { +const datasetData = (persistentId: string, versionId: number, lastUpdateTime: string) => { return { license: { name: 'CC0 1.0', @@ -138,6 +138,7 @@ const datasetData = (persistentId: string, versionId: number) => { } ], someDatasetVersionHasBeenReleased: false, + lastUpdateTime, termsOfAccess: termsOfUse, deaccessionNote: undefined }, @@ -181,7 +182,11 @@ describe('Dataset JSDataverse Repository', () => { if (!dataset) { throw new Error('Dataset not found') } - const datasetExpected = datasetData(dataset.persistentId, dataset.version.id) + const datasetExpected = datasetData( + dataset.persistentId, + dataset.version.id, + dataset.version.lastUpdateTime + ) expect(dataset.license).to.deep.equal(datasetExpected.license) expect(dataset.metadataBlocks).to.deep.equal(datasetExpected.metadataBlocks) expect(dataset.summaryFields).to.deep.equal(datasetExpected.summaryFields) @@ -222,6 +227,7 @@ describe('Dataset JSDataverse Repository', () => { false, DatasetPublishingStatus.RELEASED, true, + dataset.version.lastUpdateTime, termsOfUse, undefined ) @@ -254,7 +260,11 @@ describe('Dataset JSDataverse Repository', () => { if (!dataset) { throw new Error('Dataset not found') } - const datasetExpected = datasetData(dataset.persistentId, dataset.version.id) + const datasetExpected = datasetData( + dataset.persistentId, + dataset.version.id, + dataset.version.lastUpdateTime + ) const newVersion = new DatasetVersion( dataset.version.id, "Darwin's Finches", @@ -266,6 +276,7 @@ describe('Dataset JSDataverse Repository', () => { false, DatasetPublishingStatus.RELEASED, true, + dataset.version.lastUpdateTime, termsOfUse, undefined ) @@ -289,7 +300,11 @@ describe('Dataset JSDataverse Repository', () => { if (!dataset) { throw new Error('Dataset not found') } - const datasetExpected = datasetData(dataset.persistentId, dataset.version.id) + const datasetExpected = datasetData( + dataset.persistentId, + dataset.version.id, + dataset.version.lastUpdateTime + ) expect(dataset.version.title).to.deep.equal(datasetExpected.title) expect(dataset.version).to.deep.equal(datasetExpected.version) @@ -304,7 +319,11 @@ describe('Dataset JSDataverse Repository', () => { if (!dataset) { throw new Error('Dataset not found') } - const datasetExpected = datasetData(dataset.persistentId, dataset.version.id) + const datasetExpected = datasetData( + dataset.persistentId, + dataset.version.id, + dataset.version.lastUpdateTime + ) expect(dataset.version.title).to.deep.equal(datasetExpected.title) expect(dataset.version).to.deep.equal(datasetExpected.version) @@ -366,7 +385,11 @@ describe('Dataset JSDataverse Repository', () => { if (!dataset) { throw new Error('Dataset not found') } - const datasetExpected = datasetData(dataset.persistentId, dataset.version.id) + const datasetExpected = datasetData( + dataset.persistentId, + dataset.version.id, + dataset.version.lastUpdateTime + ) expect(dataset.version.title).to.deep.equal(datasetExpected.title) expect(dataset.version.publishingStatus).to.equal(DatasetPublishingStatus.DEACCESSIONED) @@ -383,7 +406,11 @@ describe('Dataset JSDataverse Repository', () => { if (!dataset) { throw new Error('Dataset not found') } - const datasetExpected = datasetData(dataset.persistentId, dataset.version.id) + const datasetExpected = datasetData( + dataset.persistentId, + dataset.version.id, + dataset.version.lastUpdateTime + ) expect(dataset.version.title).to.deep.equal(datasetExpected.title) From 215162cf1d83d8297b9cfa96c71be086dc2e1148 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Germ=C3=A1n=20Saracca?= Date: Fri, 26 Sep 2025 14:15:22 -0300 Subject: [PATCH 6/8] feat: implement optimistic concurrency control using sourceLastUpdateTime in file metadata updates --- CHANGELOG.md | 1 + src/files/domain/repositories/FileRepository.ts | 6 +++++- src/files/domain/useCases/editFileMetadata.ts | 5 +++-- src/files/infrastructure/FileJSDataverseRepository.ts | 8 ++++++-- src/sections/edit-file-metadata/EditFileMetadata.tsx | 1 + src/sections/edit-file-metadata/EditFilesList.tsx | 7 +++++-- src/sections/edit-file-metadata/useSubmitFileMetadata.ts | 5 +++-- 7 files changed, 24 insertions(+), 9 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 59a59ad18..e602963be 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -11,6 +11,7 @@ This changelog follows the principles of [Keep a Changelog](https://keepachangel ### Changed - Standardize Node.js version to 22 across all environments (docker dev environment, CI, production). +- Use of the new `sourceLastUpdateTime` query parameter from update dataset and file metadata endpoints to support optimistic concurrency control during editing operations. See [Edit Dataset Metadata](https://guides.dataverse.org/en/6.8/api/native-api.html#edit-dataset-metadata) and [Updating File Metadata](https://guides.dataverse.org/en/6.8/api/native-api.html#updating-file-metadata) guides for more details. ### Fixed diff --git a/src/files/domain/repositories/FileRepository.ts b/src/files/domain/repositories/FileRepository.ts index 3f3dc32f3..587e8a1be 100644 --- a/src/files/domain/repositories/FileRepository.ts +++ b/src/files/domain/repositories/FileRepository.ts @@ -54,7 +54,11 @@ export interface FileRepository { delete: (fileId: number | string) => Promise replace: (fileId: number | string, uploadedFileDTO: UploadedFileDTO) => Promise getFixityAlgorithm: () => Promise - updateMetadata: (fileId: number | string, fileMetadata: FileMetadataDTO) => Promise + updateMetadata: ( + fileId: number | string, + fileMetadata: FileMetadataDTO, + sourceLastUpdateTime?: string + ) => Promise restrict: (fileId: number | string, restrictFileDTO: RestrictFileDTO) => Promise updateTabularTags: ( fileId: number | string, diff --git a/src/files/domain/useCases/editFileMetadata.ts b/src/files/domain/useCases/editFileMetadata.ts index 1a8f17c28..552d9f8fd 100644 --- a/src/files/domain/useCases/editFileMetadata.ts +++ b/src/files/domain/useCases/editFileMetadata.ts @@ -4,7 +4,8 @@ import { FileMetadataDTO } from '@/files/domain/useCases/DTOs/FileMetadataDTO' export function editFileMetadata( fileRepository: FileRepository, fileId: number | string, - fileMetadata: FileMetadataDTO + fileMetadata: FileMetadataDTO, + sourceLastUpdateTime?: string ): Promise { - return fileRepository.updateMetadata(fileId, fileMetadata) + return fileRepository.updateMetadata(fileId, fileMetadata, sourceLastUpdateTime) } diff --git a/src/files/infrastructure/FileJSDataverseRepository.ts b/src/files/infrastructure/FileJSDataverseRepository.ts index 91b50dcdd..29c287ce7 100644 --- a/src/files/infrastructure/FileJSDataverseRepository.ts +++ b/src/files/infrastructure/FileJSDataverseRepository.ts @@ -356,8 +356,12 @@ export class FileJSDataverseRepository implements FileRepository { .then((newFileIdentifier) => newFileIdentifier) } - updateMetadata(fileId: number | string, fileMetadata: FileMetadataDTO): Promise { - return updateFileMetadata.execute(fileId, fileMetadata) + updateMetadata( + fileId: number | string, + fileMetadata: FileMetadataDTO, + sourceLastUpdateTime?: string + ): Promise { + return updateFileMetadata.execute(fileId, fileMetadata, sourceLastUpdateTime) } // TODO - Not a priority but could be nice to implement this use case in js-dataverse when having time getFixityAlgorithm(): Promise { diff --git a/src/sections/edit-file-metadata/EditFileMetadata.tsx b/src/sections/edit-file-metadata/EditFileMetadata.tsx index fb8696490..50651da6d 100644 --- a/src/sections/edit-file-metadata/EditFileMetadata.tsx +++ b/src/sections/edit-file-metadata/EditFileMetadata.tsx @@ -73,6 +73,7 @@ export const EditFileMetadata = ({ fileId, fileRepository, referrer }: EditFileM editFileMetadataFormData={createEditFileMetadataFormData(file)} referrer={referrer} datasetPersistentId={file.hierarchy.parent?.persistentId} + datasetLastUpdateTime={file.datasetVersion.lastUpdateTime} /> diff --git a/src/sections/edit-file-metadata/EditFilesList.tsx b/src/sections/edit-file-metadata/EditFilesList.tsx index f6eba5868..3dc6311e6 100644 --- a/src/sections/edit-file-metadata/EditFilesList.tsx +++ b/src/sections/edit-file-metadata/EditFilesList.tsx @@ -31,13 +31,15 @@ type EditFilesListProps = { editFileMetadataFormData: EditFileMetadataFormData referrer: EditFileMetadataReferrer datasetPersistentId?: string + datasetLastUpdateTime: string } export const EditFilesList = ({ fileRepository, editFileMetadataFormData, referrer, - datasetPersistentId + datasetPersistentId, + datasetLastUpdateTime }: EditFilesListProps) => { const { t } = useTranslation('shared') const navigate = useNavigate() @@ -56,7 +58,8 @@ export const EditFilesList = ({ } const { submitForm, submissionStatus, submitError } = useSubmitFileMetadata( fileRepository, - onSubmitSucceed + onSubmitSucceed, + datasetLastUpdateTime ) const isSaving = submissionStatus === SubmissionStatus.IsSubmitting const handleCancel = () => navigate(-1) diff --git a/src/sections/edit-file-metadata/useSubmitFileMetadata.ts b/src/sections/edit-file-metadata/useSubmitFileMetadata.ts index 4704d21b8..d4142fe2c 100644 --- a/src/sections/edit-file-metadata/useSubmitFileMetadata.ts +++ b/src/sections/edit-file-metadata/useSubmitFileMetadata.ts @@ -27,7 +27,8 @@ type UseSubmitFileMetadataReturnType = export const useSubmitFileMetadata = ( fileRepository: FileRepository, - onSubmitSucceed: () => void + onSubmitSucceed: () => void, + datasetLastUpdateTime: string ) => { const [submissionStatus, setSubmissionStatus] = useState( SubmissionStatus.NotSubmitted @@ -45,7 +46,7 @@ export const useSubmitFileMetadata = ( directoryLabel: file.fileDir, label: file.fileName } - await editFileMetadata(fileRepository, file.id, fileMetadataDTO) + await editFileMetadata(fileRepository, file.id, fileMetadataDTO, datasetLastUpdateTime) } setSubmitError(null) From ad46d51ff542933d8efabbf24ab053a2be346be4 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Germ=C3=A1n=20Saracca?= Date: Fri, 26 Sep 2025 14:49:02 -0300 Subject: [PATCH 7/8] test: assert lastUpdateTime being sent --- .../edit-file-metadata/EditFilesList.spec.tsx | 62 ++++++++++++++----- 1 file changed, 46 insertions(+), 16 deletions(-) diff --git a/tests/component/sections/edit-file-metadata/EditFilesList.spec.tsx b/tests/component/sections/edit-file-metadata/EditFilesList.spec.tsx index 12b64a79a..730ea5de7 100644 --- a/tests/component/sections/edit-file-metadata/EditFilesList.spec.tsx +++ b/tests/component/sections/edit-file-metadata/EditFilesList.spec.tsx @@ -1,10 +1,16 @@ -import { EditFilesList } from '@/sections/edit-file-metadata/EditFilesList' -import { FileMockRepository } from '@/stories/file/FileMockRepository' +import { + EditFileMetadataFormData, + EditFilesList +} from '@/sections/edit-file-metadata/EditFilesList' import { EditFileMetadataReferrer } from '@/sections/edit-file-metadata/EditFileMetadata' +import { FileMetadataDTO } from '@/files/domain/useCases/DTOs/FileMetadataDTO' +import { FileRepository } from '@/files/domain/repositories/FileRepository' + +const datasetLastUpdateTime = '2023-06-01T12:34:56Z' describe('EditFilesList Component', () => { - const fileRepository = new FileMockRepository() - const editFileMetadataFormData = { + const fileRepository: FileRepository = {} as FileRepository + const editFileMetadataFormData: EditFileMetadataFormData = { files: [ { id: 1, @@ -44,6 +50,7 @@ describe('EditFilesList Component', () => { } beforeEach(() => { cy.viewport(1200, 800) + fileRepository.updateMetadata = cy.stub().as('editFileMetadataStub').resolves() }) it('renders the form with file metadata', () => { cy.customMount( @@ -51,6 +58,7 @@ describe('EditFilesList Component', () => { fileRepository={fileRepository} editFileMetadataFormData={editFileMetadataFormData} referrer={EditFileMetadataReferrer.FILE} + datasetLastUpdateTime={datasetLastUpdateTime} /> ) @@ -71,6 +79,7 @@ describe('EditFilesList Component', () => { editFileMetadataFormData={multipleFilesFormData} referrer={EditFileMetadataReferrer.DATASET} datasetPersistentId="dataset-persistent-id" + datasetLastUpdateTime={datasetLastUpdateTime} /> ) @@ -97,6 +106,7 @@ describe('EditFilesList Component', () => { fileRepository={fileRepository} editFileMetadataFormData={editFileMetadataFormData} referrer={EditFileMetadataReferrer.FILE} + datasetLastUpdateTime={datasetLastUpdateTime} /> ) @@ -105,76 +115,96 @@ describe('EditFilesList Component', () => { }) it('submits the form', () => { - const editFileMetadataStub = cy.stub(fileRepository, 'updateMetadata').resolves() + fileRepository.updateMetadata = cy.stub().as('editFileMetadataStub').resolves() cy.customMount( ) + cy.findByLabelText(/File Name/) + .clear() + .type('newname.txt') + + cy.findByLabelText(/File Path/) + .clear() + .type('/newdir') + + cy.findByLabelText('Description').clear().type('New description') + cy.findByTestId('edit-file-metadata-form').submit() - cy.wrap(editFileMetadataStub).should('have.been.called') + + cy.get('@editFileMetadataStub').should((spy) => { + const editFileMetadataSpy = spy as unknown as Cypress.Agent + const fileMetadataDTO = editFileMetadataSpy.getCall(0).args[1] as FileMetadataDTO + const sourceLastUpdateTimeArg = editFileMetadataSpy.getCall(0).args[2] as string + + expect(fileMetadataDTO.label).to.equal('newname.txt') + expect(fileMetadataDTO.description).to.equal('New description') + expect(fileMetadataDTO.directoryLabel).to.equal('/newdir') + expect(sourceLastUpdateTimeArg).to.equal(datasetLastUpdateTime) + }) }) it('submits the form when referrer is Dataset', () => { - const editFileMetadataStub = cy.stub(fileRepository, 'updateMetadata').resolves() cy.customMount( ) cy.findByTestId('edit-file-metadata-form').submit() - cy.wrap(editFileMetadataStub).should('have.been.called') + cy.get('@editFileMetadataStub').should('have.been.called') }) it('handles error when submitting the form', () => { - const editFileMetadataStub = cy - .stub(fileRepository, 'updateMetadata') - .rejects(new Error('Error')) + fileRepository.updateMetadata = cy.stub().as('editFileMetadataStub').rejects(new Error('Error')) cy.customMount( ) cy.findByTestId('edit-file-metadata-form').submit() - cy.wrap(editFileMetadataStub).should('have.been.called') + cy.get('@editFileMetadataStub').should('have.been.called') cy.findByText('Error').should('exist') }) it('does not submit the form when pressing enter in the description textarea', () => { - const editFileMetadataStub = cy.stub(fileRepository, 'updateMetadata').resolves() cy.customMount( ) cy.findByLabelText('Description').type('{enter}') - cy.wrap(editFileMetadataStub).should('not.have.been.called') + cy.get('@editFileMetadataStub').should('not.have.been.called') }) it('does submit the form when pressing enter in the Save Changes button', () => { - const editFileMetadataStub = cy.stub(fileRepository, 'updateMetadata').resolves() cy.customMount( ) cy.findByRole('button', { name: 'Save Changes' }).type('{enter}') - cy.wrap(editFileMetadataStub).should('have.been.called') + cy.get('@editFileMetadataStub').should('have.been.called') }) }) From fe04622ef5da1e4be4c287835fab7bdb45626cda Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Germ=C3=A1n=20Saracca?= Date: Fri, 3 Oct 2025 12:51:16 -0300 Subject: [PATCH 8/8] docs: use alpha version of js-dv --- package-lock.json | 14 +++++++------- package.json | 2 +- 2 files changed, 8 insertions(+), 8 deletions(-) diff --git a/package-lock.json b/package-lock.json index 543455944..de9fd9fe7 100644 --- a/package-lock.json +++ b/package-lock.json @@ -13,7 +13,7 @@ "@dnd-kit/sortable": "8.0.0", "@dnd-kit/utilities": "3.2.2", "@faker-js/faker": "7.6.0", - "@iqss/dataverse-client-javascript": "2.0.0-alpha.67", + "@iqss/dataverse-client-javascript": "2.0.0-alpha.76", "@iqss/dataverse-design-system": "*", "@istanbuljs/nyc-config-typescript": "1.0.2", "@tanstack/react-table": "8.9.2", @@ -81,7 +81,7 @@ "babel-plugin-named-exports-order": "0.0.2", "chai": "4.3.7", "chai-as-promised": "7.1.1", - "chromatic": "^13.3.0", + "chromatic": "13.3.0", "concurrently": "8.0.1", "cypress": "15.2.0", "cypress-vite": "1.4.0", @@ -1954,14 +1954,14 @@ }, "node_modules/@iqss/dataverse-client-javascript": { "name": "@IQSS/dataverse-client-javascript", - "version": "2.0.0-alpha.67", - "resolved": "https://npm.pkg.github.com/download/@IQSS/dataverse-client-javascript/2.0.0-alpha.67/0ee4f1c8e03eb3ef688d6b034e84003c9e155d3b", - "integrity": "sha512-uEAGtwXz7LYkBfWCBRktgb5d8oba6yPH9YWnVFhI40UqgdB1sQ/WWCDhZTn5LFsaQY+1XBzOoTjwrQ2HGz94og==", + "version": "2.0.0-alpha.76", + "resolved": "https://npm.pkg.github.com/download/@IQSS/dataverse-client-javascript/2.0.0-alpha.76/0bad3d2706d09bbc527bb622fc7abd6d72af7b72", + "integrity": "sha512-pOX+Ql53unePr/eClw8MZQcKPk+xVOR5Ag4Wi0Xdb9Z0GG+rYdOmVLwipkH70Dj0R0fC3BG3xAausT5B56yKhA==", "license": "MIT", "dependencies": { "@types/node": "^18.15.11", "@types/turndown": "^5.0.1", - "axios": "^1.7.2", + "axios": "^1.12.2", "turndown": "^7.1.2", "typescript": "^4.9.5" } @@ -30401,7 +30401,7 @@ "@testing-library/cypress": "10.1.0", "@vitejs/plugin-react": "4.3.1", "axe-playwright": "1.2.3", - "chromatic": "^13.3.0", + "chromatic": "13.3.0", "cypress": "15.2.0", "react": "18.2.0", "vite": "5.4.20", diff --git a/package.json b/package.json index 34008bc0b..5fd00c778 100644 --- a/package.json +++ b/package.json @@ -18,7 +18,7 @@ "@dnd-kit/sortable": "8.0.0", "@dnd-kit/utilities": "3.2.2", "@faker-js/faker": "7.6.0", - "@iqss/dataverse-client-javascript": "2.0.0-alpha.67", + "@iqss/dataverse-client-javascript": "2.0.0-alpha.76", "@iqss/dataverse-design-system": "*", "@istanbuljs/nyc-config-typescript": "1.0.2", "@tanstack/react-table": "8.9.2",