Skip to content

Commit 28d9400

Browse files
committed
Merge branch 'develop' of github.com:IQSS/dataverse-client-javascript into 170-file-collection-results
2 parents 07e4a4e + 42c1ce3 commit 28d9400

File tree

11 files changed

+154
-55
lines changed

11 files changed

+154
-55
lines changed

docs/useCases.md

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@ The different use cases currently available in the package are classified below,
1616
- [List All Collection Items](#list-all-collection-items)
1717
- [Collections write use cases](#collections-write-use-cases)
1818
- [Create a Collection](#create-a-collection)
19+
- [Publish a Collection](#publish-a-collection)
1920
- [Datasets](#Datasets)
2021
- [Datasets read use cases](#datasets-read-use-cases)
2122
- [Get a Dataset](#get-a-dataset)
@@ -227,6 +228,28 @@ The above example creates the new collection in the `root` collection since no c
227228

228229
The use case returns a number, which is the identifier of the created collection.
229230

231+
#### Publish a Collection
232+
233+
Publishes a Collection, given the collection identifier.
234+
235+
##### Example call:
236+
237+
```typescript
238+
import { publishCollection } from '@iqss/dataverse-client-javascript'
239+
240+
/* ... */
241+
242+
const collectionIdOrAlias = 12345
243+
244+
publishCollection.execute(collectionIdOrAlias)
245+
246+
/* ... */
247+
```
248+
249+
The `collectionIdOrAlias` is a generic collection identifier, which can be either a string (for queries by CollectionAlias), or a number (for queries by CollectionId).
250+
251+
_See [use case](../src/collections/domain/useCases/PublishCollection.ts)_ definition.
252+
230253
## Datasets
231254

232255
### Datasets Read Use Cases

src/collections/domain/repositories/ICollectionsRepository.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@ export interface ICollectionsRepository {
1111
collectionDTO: CollectionDTO,
1212
parentCollectionId: number | string
1313
): Promise<number>
14+
publishCollection(collectionIdOrAlias: number | string): Promise<void>
1415
getCollectionFacets(collectionIdOrAlias: number | string): Promise<CollectionFacet[]>
1516
getCollectionUserPermissions(
1617
collectionIdOrAlias: number | string
Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,20 @@
1+
import { UseCase } from '../../../core/domain/useCases/UseCase'
2+
import { ICollectionsRepository } from '../repositories/ICollectionsRepository' // Assuming Axios for HTTP requests
3+
4+
export class PublishCollection implements UseCase<void> {
5+
private collectionsRepository: ICollectionsRepository
6+
7+
constructor(collectionsRepository: ICollectionsRepository) {
8+
this.collectionsRepository = collectionsRepository
9+
}
10+
11+
/**
12+
* Publishes a collection, given its identifier.
13+
*
14+
* @param {number | string} [collectionIdOrAlias] - The collection identifier, which can be a string (for collection alias), or a number (for numeric identifiers).
15+
* @returns {Promise<void>} - This method does not return anything upon successful completion.
16+
*/
17+
async execute(collectionIdOrAlias: number | string): Promise<void> {
18+
return await this.collectionsRepository.publishCollection(collectionIdOrAlias)
19+
}
20+
}

src/collections/index.ts

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ import { GetCollection } from './domain/useCases/GetCollection'
33
import { GetCollectionFacets } from './domain/useCases/GetCollectionFacets'
44
import { GetCollectionUserPermissions } from './domain/useCases/GetCollectionUserPermissions'
55
import { GetCollectionItems } from './domain/useCases/GetCollectionItems'
6+
import { PublishCollection } from './domain/useCases/PublishCollection'
67

78
import { CollectionsRepository } from './infra/repositories/CollectionsRepository'
89

@@ -13,13 +14,15 @@ const createCollection = new CreateCollection(collectionsRepository)
1314
const getCollectionFacets = new GetCollectionFacets(collectionsRepository)
1415
const getCollectionUserPermissions = new GetCollectionUserPermissions(collectionsRepository)
1516
const getCollectionItems = new GetCollectionItems(collectionsRepository)
17+
const publishCollection = new PublishCollection(collectionsRepository)
1618

1719
export {
1820
getCollection,
1921
createCollection,
2022
getCollectionFacets,
2123
getCollectionUserPermissions,
22-
getCollectionItems
24+
getCollectionItems,
25+
publishCollection
2326
}
2427
export { Collection, CollectionInputLevel } from './domain/models/Collection'
2528
export { CollectionFacet } from './domain/models/CollectionFacet'

src/collections/infra/repositories/CollectionsRepository.ts

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -118,6 +118,16 @@ export class CollectionsRepository extends ApiRepository implements ICollections
118118
throw error
119119
})
120120
}
121+
public async publishCollection(collectionIdOrAlias: string | number): Promise<void> {
122+
return this.doPost(
123+
`/${this.collectionsResourceName}/${collectionIdOrAlias}/actions/:publish`,
124+
{}
125+
)
126+
.then(() => undefined)
127+
.catch((error) => {
128+
throw error
129+
})
130+
}
121131

122132
public async getCollectionUserPermissions(
123133
collectionIdOrAlias: number | string

src/files/infra/clients/DirectUploadClient.ts

Lines changed: 9 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -19,9 +19,6 @@ export class DirectUploadClient implements IDirectUploadClient {
1919
private filesRepository: IFilesRepository
2020
private maxMultipartRetries: number
2121

22-
private readonly progressAfterUrlGeneration: number = 10
23-
private readonly progressAfterFileUpload: number = 100
24-
2522
private readonly fileUploadTimeoutMs: number = 60_000
2623

2724
constructor(filesRepository: IFilesRepository, maxMultipartRetries = 5) {
@@ -43,14 +40,12 @@ export class DirectUploadClient implements IDirectUploadClient {
4340
throw new UrlGenerationError(file.name, datasetId, error.message)
4441
})
4542
}
46-
progress(this.progressAfterUrlGeneration)
4743

4844
if (destination.urls.length === 1) {
49-
await this.uploadSinglepartFile(datasetId, file, destination, abortController)
45+
await this.uploadSinglepartFile(datasetId, file, destination, progress, abortController)
5046
} else {
5147
await this.uploadMultipartFile(datasetId, file, destination, progress, abortController)
5248
}
53-
progress(this.progressAfterFileUpload)
5449

5550
return destination.storageId
5651
}
@@ -59,18 +54,20 @@ export class DirectUploadClient implements IDirectUploadClient {
5954
datasetId: number | string,
6055
file: File,
6156
destination: FileUploadDestination,
57+
progress: (now: number) => void,
6258
abortController: AbortController
6359
): Promise<void> {
6460
try {
6561
const arrayBuffer = await file.arrayBuffer()
6662
await axios.put(destination.urls[0], arrayBuffer, {
6763
headers: {
6864
'Content-Type': 'application/octet-stream',
69-
'Content-Length': file.size.toString(),
7065
'x-amz-tagging': 'dv-state=temp'
7166
},
7267
timeout: this.fileUploadTimeoutMs,
73-
signal: abortController.signal
68+
signal: abortController.signal,
69+
onUploadProgress: (progressEvent) =>
70+
progress(Math.round((progressEvent.loaded * 100) / file.size))
7471
})
7572
} catch (error) {
7673
if (axios.isCancel(error)) {
@@ -92,8 +89,6 @@ export class DirectUploadClient implements IDirectUploadClient {
9289
const maxRetries = this.maxMultipartRetries
9390
const limitConcurrency = pLimit(1)
9491

95-
const progressPartSize = 80 / destination.urls.length
96-
9792
const uploadPart = async (
9893
destinationUrl: string,
9994
index: number,
@@ -106,17 +101,17 @@ export class DirectUploadClient implements IDirectUploadClient {
106101
try {
107102
const response = await axios.put(destinationUrl, fileSlice, {
108103
headers: {
109-
'Content-Type': 'application/octet-stream',
110-
'Content-Length': fileSlice.size
104+
'Content-Type': 'application/octet-stream'
111105
},
112106
maxBodyLength: Infinity,
113107
maxContentLength: Infinity,
114108
timeout: this.fileUploadTimeoutMs,
115-
signal: abortController.signal
109+
signal: abortController.signal,
110+
onUploadProgress: (progressEvent) =>
111+
progress(Math.round(((offset + progressEvent.loaded) * 100) / file.size))
116112
})
117113
const eTag = response.headers['etag'].replace(/"/g, '')
118114
eTags[`${index + 1}`] = eTag
119-
progress(Math.round(this.progressAfterUrlGeneration + progressPartSize * (index + 1)))
120115
} catch (error) {
121116
if (axios.isCancel(error)) {
122117
await this.abortMultipartUpload(file.name, datasetId, destination.abortEndpoint)
Lines changed: 47 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,47 @@
1+
import { ApiConfig, createCollection, publishCollection, WriteError } from '../../../src'
2+
import { TestConstants } from '../../testHelpers/TestConstants'
3+
import { DataverseApiAuthMechanism } from '../../../src/core/infra/repositories/ApiConfig'
4+
import {
5+
createCollectionDTO,
6+
deleteCollectionViaApi
7+
} from '../../testHelpers/collections/collectionHelper'
8+
9+
const testNewCollection = createCollectionDTO('test-publish-collection')
10+
11+
describe('execute', () => {
12+
beforeEach(async () => {
13+
ApiConfig.init(
14+
TestConstants.TEST_API_URL,
15+
DataverseApiAuthMechanism.API_KEY,
16+
process.env.TEST_API_KEY
17+
)
18+
})
19+
20+
test('should successfully publish a collection with id', async () => {
21+
const createdCollectiontIdentifier = await createCollection.execute(testNewCollection)
22+
23+
const response = await publishCollection.execute(createdCollectiontIdentifier)
24+
25+
expect(response).toBeUndefined()
26+
await deleteCollectionViaApi(testNewCollection.alias)
27+
})
28+
test('should successfully publish a collection with alias', async () => {
29+
await createCollection.execute(testNewCollection)
30+
31+
const response = await publishCollection.execute(testNewCollection.alias)
32+
33+
expect(response).toBeUndefined()
34+
await deleteCollectionViaApi(testNewCollection.alias)
35+
})
36+
37+
test('should throw an error when trying to publish a collection that does not exist', async () => {
38+
const nonExistentTestCollectionId = 4567
39+
const expectedError = new WriteError(
40+
`[404] Can't find dataverse with identifier='${nonExistentTestCollectionId}'`
41+
)
42+
43+
await expect(publishCollection.execute(nonExistentTestCollectionId)).rejects.toThrow(
44+
expectedError
45+
)
46+
})
47+
})

test/integration/collections/CollectionsRepository.test.ts

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -107,6 +107,23 @@ describe('CollectionsRepository', () => {
107107
})
108108
})
109109

110+
describe('publishCollection', () => {
111+
const testPublishCollectionAlias = 'publishCollection-test'
112+
113+
afterAll(async () => {
114+
await deleteCollectionViaApi(testPublishCollectionAlias)
115+
})
116+
117+
test('should publish a collection', async () => {
118+
const newCollectionDTO = createCollectionDTO(testPublishCollectionAlias)
119+
const actualId = await sut.createCollection(newCollectionDTO)
120+
await sut.publishCollection(actualId)
121+
const createdCollection = await sut.getCollection(actualId)
122+
123+
expect(createdCollection.isReleased).toBe(true)
124+
expect(createdCollection.name).toBe(newCollectionDTO.name)
125+
})
126+
})
110127
describe('createCollection', () => {
111128
const testCreateCollectionAlias1 = 'createCollection-test-1'
112129
const testCreateCollectionAlias2 = 'createCollection-test-2'

test/integration/files/DirectUpload.test.ts

Lines changed: 0 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -95,10 +95,6 @@ describe('Direct Upload', () => {
9595

9696
expect(await singlepartFileExistsInBucket(singlepartFileUrl)).toBe(true)
9797

98-
expect(progressMock).toHaveBeenCalledWith(10)
99-
expect(progressMock).toHaveBeenCalledWith(100)
100-
expect(progressMock).toHaveBeenCalledTimes(2)
101-
10298
// Test FilesRepository.addUploadedFileToDataset method
10399

104100
let datasetFiles = await filesRepositorySut.getDatasetFiles(
@@ -156,12 +152,6 @@ describe('Direct Upload', () => {
156152
)
157153
expect(actualStorageId).toBe(destination.storageId)
158154

159-
expect(progressMock).toHaveBeenCalledWith(10)
160-
expect(progressMock).toHaveBeenCalledWith(50)
161-
expect(progressMock).toHaveBeenCalledWith(90)
162-
expect(progressMock).toHaveBeenCalledWith(100)
163-
expect(progressMock).toHaveBeenCalledTimes(4)
164-
165155
// Test FilesRepository.addUploadedFileToDataset method
166156

167157
let datasetFiles = await filesRepositorySut.getDatasetFiles(
@@ -221,10 +211,6 @@ describe('Direct Upload', () => {
221211
destination
222212
)
223213
).rejects.toThrow(FileUploadCancelError)
224-
225-
expect(progressMock).not.toHaveBeenCalledWith(50)
226-
expect(progressMock).not.toHaveBeenCalledWith(90)
227-
expect(progressMock).not.toHaveBeenCalledWith(100)
228214
})
229215

230216
const createTestFileUploadDestination = async (file: File, testDatasetId: number) => {
Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,23 @@
1+
import { ICollectionsRepository } from '../../../src/collections/domain/repositories/ICollectionsRepository'
2+
import { PublishCollection } from '../../../src/collections/domain/useCases/PublishCollection'
3+
import { WriteError } from '../../../src'
4+
5+
describe('execute', () => {
6+
test('should return undefined on repository success', async () => {
7+
const collectionsRepositoryStub: ICollectionsRepository = {} as ICollectionsRepository
8+
collectionsRepositoryStub.publishCollection = jest.fn().mockResolvedValue(undefined)
9+
const sut = new PublishCollection(collectionsRepositoryStub)
10+
11+
const actual = await sut.execute(1)
12+
13+
expect(actual).toEqual(undefined)
14+
})
15+
16+
test('should return error result on repository error', async () => {
17+
const collectionsRepositoryStub: ICollectionsRepository = {} as ICollectionsRepository
18+
collectionsRepositoryStub.publishCollection = jest.fn().mockRejectedValue(new WriteError())
19+
const sut = new PublishCollection(collectionsRepositoryStub)
20+
21+
await expect(sut.execute(1)).rejects.toThrow(WriteError)
22+
})
23+
})

0 commit comments

Comments
 (0)