Skip to content

Commit 66fe1f0

Browse files
committed
resolve merge conflicts
2 parents 14eb3ac + 5d88a09 commit 66fe1f0

13 files changed

Lines changed: 410 additions & 67 deletions

File tree

CHANGELOG.md

Lines changed: 23 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -8,29 +8,32 @@ This changelog follows the principles of [Keep a Changelog](https://keepachangel
88

99
### Added
1010

11-
- New Use Case: [Update Dataset License](./docs/useCases.md#update-dataset-license) under Datasets.
12-
- New Use Case: [Get Dataset Storage Driver](./docs/useCases.md#get-dataset-storage-driver) under Datasets.
13-
- New Use Case: [Get Dataset Upload Limits](./docs/useCases.md#get-dataset-upload-limits) under Datasets.
14-
- New Use Case: [Update Terms of Access](./docs/useCases.md#update-terms-of-access) under Datasets.
11+
### Changed
12+
13+
### Fixed
14+
15+
### Removed
16+
17+
## [v2.2.0] -- 2026-04-24
18+
19+
### Added
20+
21+
- Datasets: Added `updateDatasetLicense` use case and repository method to support Dataverse endpoint `PUT /datasets/{id}/license`, for updating dataset license or custom terms.
22+
- Datasets: Added `getDatasetStorageDriver` use case and repository method to support Dataverse endpoint `GET /datasets/{identifier}/storageDriver`, for retrieving dataset storage driver configuration with properties: name, type, label, directUpload, directDownload, and uploadOutOfBand.
23+
- Datasets: Added `getDatasetUploadLimits` use case and repository method to support Dataverse endpoint `GET /datasets/{id}/uploadlimits`, for retrieving remaining storage upload quotas, if present.
24+
- New Use Case: [Get Collections For Linking Use Case](./docs/useCases.md#get-collections-for-linking).
1525
- New Use Case: [Create a Template](./docs/useCases.md#create-a-template) under Templates.
1626
- New Use Case: [Get a Template](./docs/useCases.md#get-a-template) under Templates.
1727
- New Use Case: [Delete a Template](./docs/useCases.md#delete-a-template) under Templates.
18-
- New Use Case: [Set Template as Default](./docs/useCases.md#set-template-as-default) under Templates.
19-
- New Use Case: [Unset Template as Default](./docs/useCases.md#unset-template-as-default) under Templates.
20-
- New Use Case: [Create Guestbook](./docs/useCases.md#create-guestbook) under Guestbooks.
21-
- New Use Case: [Get Guestbooks](./docs/useCases.md#get-guestbooks) under Guestbooks.
22-
- New Use Case: [Enable Guestbook](./docs/useCases.md#enable-guestbook) under Guestbooks.
23-
- New Use Case: [Disable Guestbook](./docs/useCases.md#disable-guestbook) under Guestbooks.
24-
- New Use Case: [Assign Dataset Guestbook](./docs/useCases.md#assign-dataset-guestbook) under Guestbooks.
25-
- New Use Case: [Remove Dataset Guestbook](./docs/useCases.md#remove-dataset-guestbook) under Guestbooks.
28+
- Templates: Added `setTemplateAsDefault` use case and repository method to support Dataverse endpoint `POST /dataverses/{id}/template/default/{templateId}`.
29+
- Templates: Added `unsetTemplateAsDefault` use case and repository method to support Dataverse endpoint `DELETE /dataverses/{id}/template/default`.
30+
- New Use Case: [Update Terms of Access](./docs/useCases.md#update-terms-of-access).
31+
- Guestbooks: Added use cases and repository support for guestbook creation, listing, and enabling/disabling.
32+
- Guestbooks: Added dataset-level guestbook assignment and removal support via `assignDatasetGuestbook` (`PUT /api/datasets/{identifier}/guestbook`) and `removeDatasetGuestbook` (`DELETE /api/datasets/{identifier}/guestbook`).
33+
- Datasets/Guestbooks: Added `guestbookId` in `getDataset` responses.
34+
- Access: Added`access` module for guestbook-at-request and download terms/guestbook submission endpoints.
2635
- New Use Case: [Get Publish Dataset Disclaimer Text](./docs/useCases.md#get-publish-dataset-disclaimer-text).
2736
- New Use Case: [Get Dataset Publish Popup Custom Text](./docs/useCases.md#get-dataset-publish-popup-custom-text).
28-
- New Use Case: [Get Allowed Collection Storage Drivers](./docs/useCases.md#get-allowed-collection-storage-drivers) under Collections.
29-
- New Use Case: [Get Collection Storage Driver](./docs/useCases.md#get-collection-storage-driver) under Collections.
30-
- New Use Case: [Set Collection Storage Driver](./docs/useCases.md#set-collection-storage-driver) under Collections.
31-
- New Use Case: [Delete Collection Storage Driver](./docs/useCases.md#delete-collection-storage-driver) under Collections.
32-
- New Use Case: [Get Collections For Linking](./docs/useCases.md#get-collections-for-linking) under Collections.
33-
- Access: Added `access` module for guestbook-at-request and download terms/guestbook submission endpoints.
3437
- DatasetType: Updated datasetType data model. Added two more fields: description and displayName.
3538

3639
### Changed
@@ -39,8 +42,7 @@ This changelog follows the principles of [Keep a Changelog](https://keepachangel
3942
- Templates: Rename `CreateDatasetTemplateDTO` to `CreateTemplateDTO`.
4043
- Templates: Rename `createDatasetTemplate` repository method to `createTemplate`.
4144
- Templates: Rename `getDatasetTemplates` repository method to `getTemplatesByCollectionId`.
42-
- Datasets/Guestbooks: Added `guestbookId` in `getDataset` responses.
43-
- DatasetType: Updated datasetType data model. Added two more fields: description and displayName.
45+
- Collections: `updateCollection` now supports partial updates by accepting `Partial<CollectionDTO>`. Only explicitly provided fields are sent in update requests, aligning with Dataverse API semantics. Metadata blocks handling was adjusted to respect inheritance flags and avoid invalid field combinations.
4446

4547
### Fixed
4648

@@ -51,7 +53,7 @@ This changelog follows the principles of [Keep a Changelog](https://keepachangel
5153

5254
- Removed date fields validations in create and update dataset use cases, since validation is already handled in the backend and SPA frontend (other clients should perform client side validation also). This avoids duplicated logic and keeps the package focused on its core responsibility.
5355

54-
[Unreleased]: https://github.com/IQSS/dataverse-client-javascript/compare/v2.1.0...develop
56+
[Unreleased]: https://github.com/IQSS/dataverse-client-javascript/compare/v2.2.0...develop
5557

5658
---
5759

package-lock.json

Lines changed: 2 additions & 2 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
{
22
"name": "@iqss/dataverse-client-javascript",
3-
"version": "2.1.0",
3+
"version": "2.2.0",
44
"description": "Dataverse API wrapper package for JavaScript/TypeScript-based applications",
55
"main": "./dist/index.js",
66
"types": "./dist/index.d.ts",
Lines changed: 117 additions & 32 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,11 @@
1+
import { ApiConfig, DataverseApiAuthMechanism } from '../../../core/infra/repositories/ApiConfig'
2+
import { WriteError } from '../../../core/domain/repositories/WriteError'
3+
import { ApiConstants } from '../../../core/infra/repositories/ApiConstants'
14
import { ApiRepository } from '../../../core/infra/repositories/ApiRepository'
5+
import {
6+
buildRequestConfig,
7+
buildRequestUrl
8+
} from '../../../core/infra/repositories/apiConfigBuilders'
29
import { GuestbookResponseDTO } from '../../domain/dtos/GuestbookResponseDTO'
310
import { IAccessRepository } from '../../domain/repositories/IAccessRepository'
411

@@ -13,14 +20,7 @@ export class AccessRepository extends ApiRepository implements IAccessRepository
1320
const endpoint = this.buildApiEndpoint(`${this.accessResourceName}/datafile`, undefined, fileId)
1421
const queryParams = format ? { signed: true, format } : { signed: true }
1522

16-
return this.doPost(endpoint, guestbookResponse, queryParams)
17-
.then((response) => {
18-
const signedUrl = response.data.data.signedUrl
19-
return signedUrl
20-
})
21-
.catch((error) => {
22-
throw error
23-
})
23+
return await this.submitGuestbookDownload(endpoint, guestbookResponse, queryParams)
2424
}
2525

2626
public async submitGuestbookForDatafilesDownload(
@@ -30,21 +30,14 @@ export class AccessRepository extends ApiRepository implements IAccessRepository
3030
): Promise<string> {
3131
const queryParams = format ? { signed: true, format } : { signed: true }
3232

33-
return this.doPost(
33+
return await this.submitGuestbookDownload(
3434
this.buildApiEndpoint(
3535
this.accessResourceName,
3636
`datafiles/${Array.isArray(fileIds) ? fileIds.join(',') : fileIds}`
3737
),
3838
guestbookResponse,
3939
queryParams
4040
)
41-
.then((response) => {
42-
const signedUrl = response.data.data.signedUrl
43-
return signedUrl
44-
})
45-
.catch((error) => {
46-
throw error
47-
})
4841
}
4942

5043
public async submitGuestbookForDatasetDownload(
@@ -59,14 +52,7 @@ export class AccessRepository extends ApiRepository implements IAccessRepository
5952
)
6053
const queryParams = format ? { signed: true, format } : { signed: true }
6154

62-
return this.doPost(endpoint, guestbookResponse, queryParams)
63-
.then((response) => {
64-
const signedUrl = response.data.data.signedUrl
65-
return signedUrl
66-
})
67-
.catch((error) => {
68-
throw error
69-
})
55+
return await this.submitGuestbookDownload(endpoint, guestbookResponse, queryParams)
7056
}
7157

7258
public async submitGuestbookForDatasetVersionDownload(
@@ -82,13 +68,112 @@ export class AccessRepository extends ApiRepository implements IAccessRepository
8268
)
8369
const queryParams = format ? { signed: true, format } : { signed: true }
8470

85-
return this.doPost(endpoint, guestbookResponse, queryParams)
86-
.then((response) => {
87-
const signedUrl = response.data.data.signedUrl
88-
return signedUrl
89-
})
90-
.catch((error) => {
91-
throw error
92-
})
71+
return await this.submitGuestbookDownload(endpoint, guestbookResponse, queryParams)
72+
}
73+
74+
private async submitGuestbookDownload(
75+
apiEndpoint: string,
76+
guestbookResponse: GuestbookResponseDTO,
77+
queryParams: object
78+
): Promise<string> {
79+
const requestConfig = buildRequestConfig(
80+
true,
81+
queryParams,
82+
ApiConstants.CONTENT_TYPE_APPLICATION_JSON
83+
)
84+
const response = await fetch(
85+
this.buildUrlWithQueryParams(buildRequestUrl(apiEndpoint), queryParams),
86+
{
87+
method: 'POST',
88+
headers: this.buildFetchHeaders(requestConfig.headers),
89+
credentials: this.getFetchCredentials(requestConfig.withCredentials),
90+
body: JSON.stringify(guestbookResponse)
91+
}
92+
).catch((error) => {
93+
throw new WriteError(error instanceof Error ? error.message : String(error))
94+
})
95+
96+
const responseData = await this.parseResponseBody(response)
97+
98+
if (!response.ok) {
99+
throw new WriteError(this.buildFetchErrorMessage(response.status, responseData))
100+
}
101+
102+
return this.getSignedUrlOrThrow(responseData)
103+
}
104+
105+
private getFetchCredentials(withCredentials?: boolean): RequestCredentials | undefined {
106+
if (ApiConfig.dataverseApiAuthMechanism === DataverseApiAuthMechanism.BEARER_TOKEN) {
107+
return 'omit'
108+
}
109+
110+
if (withCredentials) {
111+
return 'include'
112+
}
113+
114+
return undefined
115+
}
116+
117+
private buildUrlWithQueryParams(requestUrl: string, queryParams: object): string {
118+
const url = new URL(requestUrl)
119+
120+
Object.entries(queryParams).forEach(([key, value]) => {
121+
if (value !== undefined && value !== null) {
122+
url.searchParams.append(key, String(value))
123+
}
124+
})
125+
126+
return url.toString()
127+
}
128+
129+
private buildFetchHeaders(headers?: Record<string, unknown>): Record<string, string> {
130+
const fetchHeaders: Record<string, string> = {}
131+
132+
if (!headers) {
133+
return fetchHeaders
134+
}
135+
136+
Object.entries(headers).forEach(([key, value]) => {
137+
if (value !== undefined) {
138+
fetchHeaders[key] = String(value)
139+
}
140+
})
141+
142+
return fetchHeaders
143+
}
144+
145+
private async parseResponseBody(response: Response): Promise<any> {
146+
const contentType = response.headers.get('content-type') ?? ''
147+
148+
if (contentType.includes('application/json')) {
149+
return await response.json()
150+
}
151+
152+
const responseText = await response.text()
153+
154+
try {
155+
return JSON.parse(responseText)
156+
} catch {
157+
return responseText
158+
}
159+
}
160+
161+
private buildFetchErrorMessage(status: number, responseData: any): string {
162+
const message =
163+
typeof responseData === 'string'
164+
? responseData
165+
: responseData?.message || responseData?.data?.message || 'unknown error'
166+
167+
return `[${status}] ${message}`
168+
}
169+
170+
private getSignedUrlOrThrow(responseData: any): string {
171+
const signedUrl = responseData?.data?.signedUrl
172+
173+
if (typeof signedUrl !== 'string' || signedUrl.length === 0) {
174+
throw new WriteError('Missing signedUrl in access download response.')
175+
}
176+
177+
return signedUrl
93178
}
94179
}

src/collections/domain/repositories/ICollectionsRepository.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -58,7 +58,7 @@ export interface ICollectionsRepository {
5858
): Promise<MyDataCollectionItemSubset>
5959
updateCollection(
6060
collectionIdOrAlias: number | string,
61-
updatedCollection: CollectionDTO
61+
updatedCollection: Partial<CollectionDTO>
6262
): Promise<void>
6363
getCollectionFeaturedItems(collectionIdOrAlias: number | string): Promise<FeaturedItem[]>
6464
updateCollectionFeaturedItems(

src/collections/domain/useCases/UpdateCollection.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -19,7 +19,7 @@ export class UpdateCollection implements UseCase<void> {
1919
*/
2020
async execute(
2121
collectionIdOrAlias: number | string,
22-
updatedCollection: CollectionDTO
22+
updatedCollection: Partial<CollectionDTO>
2323
): Promise<void> {
2424
return await this.collectionsRepository.updateCollection(collectionIdOrAlias, updatedCollection)
2525
}

src/collections/infra/repositories/CollectionsRepository.ts

Lines changed: 82 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -273,9 +273,9 @@ export class CollectionsRepository extends ApiRepository implements ICollections
273273

274274
public async updateCollection(
275275
collectionIdOrAlias: string | number,
276-
updatedCollection: CollectionDTO
276+
updatedCollection: Partial<CollectionDTO>
277277
): Promise<void> {
278-
const requestBody = this.createCreateOrUpdateRequestBody(updatedCollection)
278+
const requestBody = this.createUpdateRequestBody(updatedCollection)
279279

280280
return this.doPut(`/${this.collectionsResourceName}/${collectionIdOrAlias}`, requestBody)
281281
.then(() => undefined)
@@ -390,6 +390,86 @@ export class CollectionsRepository extends ApiRepository implements ICollections
390390
}
391391
}
392392

393+
private createUpdateRequestBody(
394+
collectionDTO: Partial<CollectionDTO>
395+
): Partial<NewCollectionRequestPayload> {
396+
const dataverseContacts: NewCollectionContactRequestPayload[] | undefined =
397+
collectionDTO.contacts?.map((contact) => ({
398+
contactEmail: contact
399+
}))
400+
const inputLevelsRequestBody: NewCollectionInputLevelRequestPayload[] | undefined =
401+
collectionDTO.inputLevels?.map((inputLevel) => ({
402+
datasetFieldTypeName: inputLevel.datasetFieldName,
403+
include: inputLevel.include,
404+
required: inputLevel.required
405+
}))
406+
let metadataBlocksRequestBody: Partial<NewCollectionMetadataBlocksRequestPayload> | undefined
407+
408+
const hasMetadataBlocksData =
409+
collectionDTO.metadataBlockNames !== undefined ||
410+
collectionDTO.facetIds !== undefined ||
411+
collectionDTO.inputLevels !== undefined ||
412+
collectionDTO.inheritMetadataBlocksFromParent !== undefined ||
413+
collectionDTO.inheritFacetsFromParent !== undefined
414+
415+
if (hasMetadataBlocksData) {
416+
metadataBlocksRequestBody = {}
417+
if (collectionDTO.inheritMetadataBlocksFromParent !== true) {
418+
if (collectionDTO.metadataBlockNames !== undefined) {
419+
metadataBlocksRequestBody.metadataBlockNames = collectionDTO.metadataBlockNames
420+
}
421+
if (inputLevelsRequestBody !== undefined) {
422+
metadataBlocksRequestBody.inputLevels = inputLevelsRequestBody
423+
}
424+
}
425+
if (collectionDTO.inheritFacetsFromParent !== true) {
426+
if (collectionDTO.facetIds !== undefined) {
427+
metadataBlocksRequestBody.facetIds = collectionDTO.facetIds
428+
}
429+
}
430+
if (collectionDTO.inheritMetadataBlocksFromParent !== undefined) {
431+
metadataBlocksRequestBody.inheritMetadataBlocksFromParent =
432+
collectionDTO.inheritMetadataBlocksFromParent
433+
}
434+
if (collectionDTO.inheritFacetsFromParent !== undefined) {
435+
metadataBlocksRequestBody.inheritFacetsFromParent = collectionDTO.inheritFacetsFromParent
436+
}
437+
}
438+
439+
// Build the final request body, only including defined fields
440+
const requestBody: Partial<NewCollectionRequestPayload> = {}
441+
442+
if (collectionDTO.alias !== undefined) {
443+
requestBody.alias = collectionDTO.alias
444+
}
445+
446+
if (collectionDTO.name !== undefined) {
447+
requestBody.name = collectionDTO.name
448+
}
449+
450+
if (dataverseContacts !== undefined) {
451+
requestBody.dataverseContacts = dataverseContacts
452+
}
453+
454+
if (collectionDTO.type !== undefined) {
455+
requestBody.dataverseType = collectionDTO.type
456+
}
457+
458+
if (collectionDTO.description !== undefined) {
459+
requestBody.description = collectionDTO.description
460+
}
461+
462+
if (collectionDTO.affiliation !== undefined) {
463+
requestBody.affiliation = collectionDTO.affiliation
464+
}
465+
466+
if (metadataBlocksRequestBody !== undefined) {
467+
requestBody.metadataBlocks = metadataBlocksRequestBody
468+
}
469+
470+
return requestBody
471+
}
472+
393473
private applyCollectionSearchCriteriaToQueryParams(
394474
queryParams: URLSearchParams,
395475
collectionSearchCriteria: CollectionSearchCriteria

0 commit comments

Comments
 (0)