Skip to content

Commit 2642f92

Browse files
authored
Merge pull request #273 from IQSS/edit-metadata-allow-empty-values
DatasetValidator allow empty fields for conditionally required fields.
2 parents d235922 + 7b2de5f commit 2642f92

File tree

13 files changed

+303
-22
lines changed

13 files changed

+303
-22
lines changed

src/datasets/domain/models/Dataset.ts

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ export interface Dataset {
55
persistentId: string
66
versionId: number
77
versionInfo: DatasetVersionInfo
8+
internalVersionNumber: number
89
license?: DatasetLicense
910
termsOfUse: TermsOfUse
1011
alternativePersistentId?: string
@@ -165,7 +166,7 @@ interface TopicClassification extends DatasetMetadataSubField {
165166
topicClassVocabURI?: string
166167
}
167168

168-
interface Publication extends DatasetMetadataSubField {
169+
export interface Publication extends DatasetMetadataSubField {
169170
publicationCitation?: string
170171
publicationIDType?: string
171172
publicationIDNumber?: string

src/datasets/domain/repositories/IDatasetsRepository.ts

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -46,7 +46,8 @@ export interface IDatasetsRepository {
4646
updateDataset(
4747
datasetId: number | string,
4848
dataset: DatasetDTO,
49-
datasetMetadataBlocks: MetadataBlock[]
49+
datasetMetadataBlocks: MetadataBlock[],
50+
internalVersionNumber?: number
5051
): Promise<void>
5152
deaccessionDataset(
5253
datasetId: number | string,

src/datasets/domain/useCases/UpdateDataset.ts

Lines changed: 12 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -18,14 +18,24 @@ export class UpdateDataset extends DatasetWriteUseCase<void> {
1818
*
1919
* @param {number | string} [datasetId] - The dataset identifier, which can be a string (for persistent identifiers), or a number (for numeric identifiers).
2020
* @param {DatasetDTO} [updatedDataset] - DatasetDTO object including the updated dataset metadata field values for each metadata block.
21+
* @param {number} [internalVersionNumber] - The internal version number of the dataset. If another user updates the dataset version metadata before you send the update request, data inconsistencies may occur. To prevent this, you can use the optional internalVersionNumber parameter. This parameter must include the internal version number corresponding to the dataset version being updated. Note that internal version numbers increase sequentially with each version update.
2122
* @returns {Promise<void>} - This method does not return anything upon successful completion.
2223
* @throws {ResourceValidationError} - If there are validation errors related to the provided information.
2324
* @throws {ReadError} - If there are errors while reading data.
2425
* @throws {WriteError} - If there are errors while writing data.
2526
*/
26-
async execute(datasetId: number | string, updatedDataset: DatasetDTO): Promise<void> {
27+
async execute(
28+
datasetId: number | string,
29+
updatedDataset: DatasetDTO,
30+
internalVersionNumber?: number
31+
): Promise<void> {
2732
const metadataBlocks = await this.getNewDatasetMetadataBlocks(updatedDataset)
2833
this.getNewDatasetValidator().validate(updatedDataset, metadataBlocks)
29-
return this.getDatasetsRepository().updateDataset(datasetId, updatedDataset, metadataBlocks)
34+
return this.getDatasetsRepository().updateDataset(
35+
datasetId,
36+
updatedDataset,
37+
metadataBlocks,
38+
internalVersionNumber
39+
)
3040
}
3141
}

src/datasets/domain/useCases/validators/BaseMetadataFieldValidator.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@ export interface DatasetMetadataFieldAndValueInfo {
99
metadataBlockName: string
1010
metadataParentFieldKey?: string
1111
metadataFieldPosition?: number
12+
allowEmptyForConditionallyRequiredField?: boolean
1213
}
1314

1415
export abstract class BaseMetadataFieldValidator {

src/datasets/domain/useCases/validators/MetadataFieldValidator.ts

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -23,7 +23,10 @@ export class MetadataFieldValidator extends BaseMetadataFieldValidator {
2323
this.isEmptyString(metadataFieldValue) ||
2424
this.isEmptyArray(metadataFieldValue)
2525
) {
26-
if (metadataFieldInfo.isRequired) {
26+
if (
27+
metadataFieldInfo.isRequired &&
28+
!datasetMetadataFieldAndValueInfo.allowEmptyForConditionallyRequiredField
29+
) {
2730
throw new EmptyFieldError(
2831
datasetMetadataFieldAndValueInfo.metadataFieldKey,
2932
datasetMetadataFieldAndValueInfo.metadataBlockName,

src/datasets/domain/useCases/validators/SingleMetadataFieldValidator.ts

Lines changed: 57 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -136,6 +136,12 @@ export class SingleMetadataFieldValidator extends BaseMetadataFieldValidator {
136136
metadataFieldInfo.childMetadataFields as Record<string, MetadataFieldInfo>
137137
)[childMetadataFieldKey]
138138

139+
const allowEmptyForConditionallyRequiredField: boolean =
140+
this.allowEmptyValueForConditionallyRequiredField(
141+
datasetMetadataFieldAndValueInfo,
142+
childMetadataFieldKey
143+
)
144+
139145
metadataFieldValidator.validate({
140146
metadataFieldInfo: childMetadataFieldInfo,
141147
metadataFieldKey: childMetadataFieldKey,
@@ -144,8 +150,58 @@ export class SingleMetadataFieldValidator extends BaseMetadataFieldValidator {
144150
)[childMetadataFieldKey],
145151
metadataBlockName: datasetMetadataFieldAndValueInfo.metadataBlockName,
146152
metadataParentFieldKey: datasetMetadataFieldAndValueInfo.metadataFieldKey,
147-
metadataFieldPosition: datasetMetadataFieldAndValueInfo.metadataFieldPosition
153+
metadataFieldPosition: datasetMetadataFieldAndValueInfo.metadataFieldPosition,
154+
allowEmptyForConditionallyRequiredField
148155
})
149156
}
150157
}
158+
159+
/**
160+
* This method allows setting empty values for conditionally required child fields.
161+
* A child field is conditionally required if it is required and its parent field is not required.
162+
* The child field should be required only if any of its sibling fields has a value, otherwise it should be optional.
163+
*/
164+
165+
private allowEmptyValueForConditionallyRequiredField(
166+
datasetMetadataFieldAndValueInfo: DatasetMetadataFieldAndValueInfo,
167+
childMetadataFieldKey: string
168+
): boolean {
169+
let result = false
170+
const metadataFieldInfo = datasetMetadataFieldAndValueInfo.metadataFieldInfo
171+
172+
const childMetadataFieldKeys = Object.keys(
173+
metadataFieldInfo.childMetadataFields as Record<string, MetadataFieldInfo>
174+
)
175+
176+
const conditionallyRequiredChildFields: false | string[] =
177+
!datasetMetadataFieldAndValueInfo.metadataFieldInfo.isRequired &&
178+
childMetadataFieldKeys.filter(
179+
(childMetadataFieldKey) =>
180+
(metadataFieldInfo.childMetadataFields as Record<string, MetadataFieldInfo>)[
181+
childMetadataFieldKey
182+
].isRequired
183+
)
184+
const hasConditionallyRequiredChildFields = Boolean(conditionallyRequiredChildFields)
185+
186+
if (
187+
hasConditionallyRequiredChildFields &&
188+
Object.values(conditionallyRequiredChildFields as string[]).includes(childMetadataFieldKey)
189+
) {
190+
// At this point we know we are standing on a child field that is required and the parent field is not required
191+
192+
// Get the sibling fields and check if any of them has a value
193+
const { [childMetadataFieldKey as keyof Record<string, string>]: _, ...siblingFields } =
194+
datasetMetadataFieldAndValueInfo.metadataFieldValue as Record<string, string>
195+
196+
const siblingsValues = Object.values(siblingFields) as string[]
197+
198+
const isAnySiblingValuePresent = siblingsValues.some(
199+
([, value]) => value !== undefined && value !== ''
200+
)
201+
202+
result = !isAnySiblingValuePresent
203+
}
204+
205+
return result
206+
}
151207
}

src/datasets/infra/repositories/DatasetsRepository.ts

Lines changed: 8 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -206,12 +206,18 @@ export class DatasetsRepository extends ApiRepository implements IDatasetsReposi
206206
public async updateDataset(
207207
datasetId: string | number,
208208
dataset: DatasetDTO,
209-
datasetMetadataBlocks: MetadataBlock[]
209+
datasetMetadataBlocks: MetadataBlock[],
210+
internalVersionNumber?: number
210211
): Promise<void> {
211212
return this.doPut(
212213
this.buildApiEndpoint(this.datasetsResourceName, `editMetadata`, datasetId),
213214
transformDatasetModelToUpdateDatasetRequestPayload(dataset, datasetMetadataBlocks),
214-
{ replace: true }
215+
{
216+
replace: true,
217+
...(typeof internalVersionNumber === 'number' && {
218+
sourceInternalVersionNumber: internalVersionNumber
219+
})
220+
}
215221
)
216222
.then(() => undefined)
217223
.catch((error) => {

src/datasets/infra/repositories/transformers/DatasetPayload.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ export interface DatasetPayload {
55
datasetId: number
66
datasetPersistentId: string
77
id: number
8+
internalVersionNumber: number
89
versionNumber: number
910
versionMinorNumber: number
1011
versionState: string

src/datasets/infra/repositories/transformers/datasetTransformers.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -226,6 +226,7 @@ export const transformVersionPayloadToDataset = (
226226
id: versionPayload.datasetId,
227227
versionId: versionPayload.id,
228228
persistentId: versionPayload.datasetPersistentId,
229+
internalVersionNumber: versionPayload.internalVersionNumber,
229230
versionInfo: {
230231
majorNumber: versionPayload.versionNumber,
231232
minorNumber: versionPayload.versionMinorNumber,

test/integration/datasets/DatasetsRepository.test.ts

Lines changed: 139 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -28,7 +28,8 @@ import { MetadataBlocksRepository } from '../../../src/metadataBlocks/infra/repo
2828
import {
2929
Author,
3030
DatasetContact,
31-
DatasetDescription
31+
DatasetDescription,
32+
Publication
3233
} from '../../../src/datasets/domain/models/Dataset'
3334
import {
3435
createCollectionViaApi,
@@ -221,6 +222,7 @@ describe('DatasetsRepository', () => {
221222
false
222223
)
223224
expect(actual.id).toBe(testDatasetIds.numericId)
225+
expect(actual.internalVersionNumber).toBe(1)
224226
})
225227

226228
test('should return dataset when it is deaccessioned and includeDeaccessioned param is set', async () => {
@@ -803,7 +805,16 @@ describe('DatasetsRepository', () => {
803805
dsDescriptionValue: 'This is the description of the dataset.'
804806
}
805807
],
806-
subject: ['Medicine, Health and Life Sciences']
808+
subject: ['Medicine, Health and Life Sciences'],
809+
publication: [
810+
{
811+
publicationRelationType: 'Cites',
812+
publicationCitation: 'Some related publication citation',
813+
publicationIDType: 'cstr',
814+
publicationIDNumber: 'some identifier'
815+
}
816+
],
817+
notesText: 'This is a note for the dataset.'
807818
}
808819
}
809820
]
@@ -813,6 +824,7 @@ describe('DatasetsRepository', () => {
813824
const citationMetadataBlock = await metadataBlocksRepository.getMetadataBlockByName(
814825
'citation'
815826
)
827+
816828
const createdDataset = await sut.createDataset(
817829
testDataset,
818830
[citationMetadataBlock],
@@ -831,9 +843,35 @@ describe('DatasetsRepository', () => {
831843
.dsDescriptionValue
832844
).toBe('This is the description of the dataset.')
833845

846+
expect(actualCreatedDataset.metadataBlocks[0].fields.notesText as string).toBe(
847+
'This is a note for the dataset.'
848+
)
849+
expect(
850+
actualCreatedDataset.metadataBlocks[0].fields.publication as Publication[]
851+
).toStrictEqual([
852+
{
853+
publicationRelationType: 'Cites',
854+
publicationCitation: 'Some related publication citation',
855+
publicationIDType: 'cstr',
856+
publicationIDNumber: 'some identifier'
857+
}
858+
])
859+
834860
const updatedDsDescription = 'This is the updated description of the dataset.'
861+
const updatedNotesText = ''
862+
const updatedPublication = [
863+
{
864+
publicationRelationType: '',
865+
publicationCitation: 'Some updated related publication citation',
866+
publicationIDType: '',
867+
publicationIDNumber: ''
868+
}
869+
]
870+
835871
testDataset.metadataBlockValues[0].fields.dsDescription[0].dsDescriptionValue =
836872
updatedDsDescription
873+
testDataset.metadataBlockValues[0].fields.notesText = updatedNotesText
874+
testDataset.metadataBlockValues[0].fields.publication = updatedPublication
837875

838876
await sut.updateDataset(createdDataset.numericId, testDataset, [citationMetadataBlock])
839877

@@ -844,6 +882,7 @@ describe('DatasetsRepository', () => {
844882
false
845883
)
846884

885+
expect(actualUpdatedDataset.internalVersionNumber).toBe(2)
847886
expect(actualUpdatedDataset.metadataBlocks[0].fields.title).toBe(
848887
'Dataset created using the createDataset use case'
849888
)
@@ -874,6 +913,104 @@ describe('DatasetsRepository', () => {
874913
(actualUpdatedDataset.metadataBlocks[0].fields.dsDescription[0] as DatasetDescription)
875914
.dsDescriptionValue
876915
).toBe(updatedDsDescription)
916+
expect(actualUpdatedDataset.metadataBlocks[0].fields.notesText as string).toBe(undefined)
917+
expect(actualUpdatedDataset.metadataBlocks[0].fields.publication).toStrictEqual([
918+
{
919+
publicationCitation: 'Some updated related publication citation'
920+
}
921+
])
922+
})
923+
924+
test('should throw error if trying to update an outdated internal version dataset', async () => {
925+
const testDataset = {
926+
metadataBlockValues: [
927+
{
928+
name: 'citation',
929+
fields: {
930+
title: 'Dataset created using the createDataset use case',
931+
author: [
932+
{
933+
authorName: 'Admin, Dataverse',
934+
authorAffiliation: 'Dataverse.org'
935+
},
936+
{
937+
authorName: 'Owner, Dataverse',
938+
authorAffiliation: 'Dataversedemo.org'
939+
}
940+
],
941+
datasetContact: [
942+
{
943+
datasetContactEmail: '[email protected]',
944+
datasetContactName: 'Finch, Fiona'
945+
}
946+
],
947+
dsDescription: [
948+
{
949+
dsDescriptionValue: 'This is the description of the dataset.'
950+
}
951+
],
952+
subject: ['Medicine, Health and Life Sciences']
953+
}
954+
}
955+
]
956+
}
957+
958+
const metadataBlocksRepository = new MetadataBlocksRepository()
959+
const citationMetadataBlock = await metadataBlocksRepository.getMetadataBlockByName(
960+
'citation'
961+
)
962+
963+
const createdDataset = await sut.createDataset(
964+
testDataset,
965+
[citationMetadataBlock],
966+
ROOT_COLLECTION_ALIAS
967+
)
968+
969+
const actualCreatedDataset = await sut.getDataset(
970+
createdDataset.numericId,
971+
DatasetNotNumberedVersion.LATEST,
972+
false,
973+
false
974+
)
975+
const actualCreatedDatasetInternalVersionNumber = actualCreatedDataset.internalVersionNumber
976+
977+
expect(actualCreatedDataset.internalVersionNumber).toBe(1)
978+
979+
// Now update the dataset and then update again with the same internal version number
980+
const updatedDsDescription = 'This is the updated description of the dataset.'
981+
testDataset.metadataBlockValues[0].fields.dsDescription[0].dsDescriptionValue =
982+
updatedDsDescription
983+
984+
// First update sending the correct internal version number
985+
await sut.updateDataset(
986+
createdDataset.numericId,
987+
testDataset,
988+
[citationMetadataBlock],
989+
actualCreatedDatasetInternalVersionNumber
990+
)
991+
992+
const afterFirstUpdateDataset = await sut.getDataset(
993+
createdDataset.numericId,
994+
DatasetNotNumberedVersion.LATEST,
995+
false,
996+
false
997+
)
998+
999+
expect(afterFirstUpdateDataset.internalVersionNumber).toBe(2)
1000+
1001+
//Now try to update again with the previous internal version number
1002+
const expectedError = new WriteError(
1003+
`[400] Dataset internal version number ${actualCreatedDatasetInternalVersionNumber} is outdated`
1004+
)
1005+
1006+
await expect(
1007+
sut.updateDataset(
1008+
createdDataset.numericId,
1009+
testDataset,
1010+
[citationMetadataBlock],
1011+
actualCreatedDatasetInternalVersionNumber
1012+
)
1013+
).rejects.toThrow(expectedError)
8771014
})
8781015

8791016
test('should return error when dataset does not exist', async () => {

0 commit comments

Comments
 (0)