Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,9 @@ node_modules
# unit tests
coverage

# macOS
.DS_Store

# ignore npm lock
package-json.lock
.npmrc
8 changes: 8 additions & 0 deletions src/collections/domain/models/CollectionLinks.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
import { CollectionSummary } from './CollectionSummary'
import { DatasetSummary } from '../../../datasets/domain/models/DatasetSummary'

export interface CollectionLinks {
linkedCollections: CollectionSummary[]
collectionsLinkingToThis: CollectionSummary[]
linkedDatasets: DatasetSummary[]
}
5 changes: 5 additions & 0 deletions src/collections/domain/models/CollectionSummary.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
export interface CollectionSummary {
id: number
alias: string
displayName: string
}
10 changes: 10 additions & 0 deletions src/collections/domain/repositories/ICollectionsRepository.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ import { CollectionSearchCriteria } from '../models/CollectionSearchCriteria'
import { CollectionUserPermissions } from '../models/CollectionUserPermissions'
import { PublicationStatus } from '../../../core/domain/models/PublicationStatus'
import { CollectionItemType } from '../../../collections/domain/models/CollectionItemType'
import { CollectionLinks } from '../models/CollectionLinks'

export interface ICollectionsRepository {
getCollection(collectionIdOrAlias: number | string): Promise<Collection>
Expand Down Expand Up @@ -50,4 +51,13 @@ export interface ICollectionsRepository {
): Promise<FeaturedItem[]>
deleteCollectionFeaturedItems(collectionIdOrAlias: number | string): Promise<void>
deleteCollectionFeaturedItem(featuredItemId: number): Promise<void>
linkCollection(
linkedCollectionIdOrAlias: number | string,
linkingCollectionIdOrAlias: number | string
): Promise<void>
unlinkCollection(
linkedCollectionIdOrAlias: number | string,
linkingCollectionIdOrAlias: number | string
): Promise<void>
getCollectionLinks(collectionIdOrAlias: number | string): Promise<CollectionLinks>
}
22 changes: 22 additions & 0 deletions src/collections/domain/useCases/GetCollectionLinks.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
import { UseCase } from '../../../core/domain/useCases/UseCase'
import { ICollectionsRepository } from '../repositories/ICollectionsRepository'
import { CollectionLinks } from '../models/CollectionLinks'

export class GetCollectionLinks implements UseCase<CollectionLinks> {
private collectionsRepository: ICollectionsRepository

constructor(collectionsRepository: ICollectionsRepository) {
this.collectionsRepository = collectionsRepository
}

/**
* Returns a CollectionLinks object containing other collections this collection is linked to, the other collections linking to this collection, and datasets linked to this collection, given the collection identifier or alias.
*
* @param {number | string} [collectionIdOrAlias] - A generic collection identifier, which can be either a string (for queries by CollectionAlias), or a number (for queries by CollectionId)
* If this parameter is not set, the default value is: ':root'
* @returns {Promise<CollectionLinks>}
*/
async execute(collectionId: number | string): Promise<CollectionLinks> {
return await this.collectionsRepository.getCollectionLinks(collectionId)
}
}
27 changes: 27 additions & 0 deletions src/collections/domain/useCases/LinkCollection.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
import { UseCase } from '../../../core/domain/useCases/UseCase'
import { ICollectionsRepository } from '../repositories/ICollectionsRepository'

export class LinkCollection implements UseCase<void> {
private collectionsRepository: ICollectionsRepository

constructor(collectionsRepository: ICollectionsRepository) {
this.collectionsRepository = collectionsRepository
}

/**
* Creates a link between two collections. The linked collection will be linked to the linking collection.:
*
* @param {number| string} [linkedCollectionIdOrAlias] - The collection to be linked. Can be either a string (collection alias), or a number (collection id)
* @param { number | string} [linkingCollectionIdOrAlias] - The collection that will be linking to the linked collection. Can be either a string (collection alias), or a number (collection id)
* @returns {Promise<void>} -This method does not return anything upon successful completion.
*/
async execute(
linkedCollectionIdOrAlias: number | string,
linkingCollectionIdOrAlias: number | string
): Promise<void> {
return await this.collectionsRepository.linkCollection(
linkedCollectionIdOrAlias,
linkingCollectionIdOrAlias
)
}
}
27 changes: 27 additions & 0 deletions src/collections/domain/useCases/UnlinkCollection.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
import { UseCase } from '../../../core/domain/useCases/UseCase'
import { ICollectionsRepository } from '../repositories/ICollectionsRepository'

export class UnlinkCollection implements UseCase<void> {
private collectionsRepository: ICollectionsRepository

constructor(collectionsRepository: ICollectionsRepository) {
this.collectionsRepository = collectionsRepository
}

/**
* Unlinks a collection from the collection that links to it
*
* @param {number| string} [linkedCollectionIdOrAlias] - The collection that is linked. Can be either a string (collection alias), or a number (collection id)
* @param { number | string} [linkingCollectionIdOrAlias] - The collection that links to the linked collection. Can be either a string (collection alias), or a number (collection id)
* @returns {Promise<void>} -This method does not return anything upon successful completion.
*/
async execute(
linkedCollectionIdOrAlias: number | string,
linkingCollectionIdOrAlias: number | string
): Promise<void> {
return await this.collectionsRepository.unlinkCollection(
linkedCollectionIdOrAlias,
linkingCollectionIdOrAlias
)
}
}
11 changes: 10 additions & 1 deletion src/collections/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,9 @@ import { DeleteCollectionFeaturedItems } from './domain/useCases/DeleteCollectio
import { DeleteCollection } from './domain/useCases/DeleteCollection'
import { GetMyDataCollectionItems } from './domain/useCases/GetMyDataCollectionItems'
import { DeleteCollectionFeaturedItem } from './domain/useCases/DeleteCollectionFeaturedItem'
import { LinkCollection } from './domain/useCases/LinkCollection'
import { UnlinkCollection } from './domain/useCases/UnlinkCollection'
import { GetCollectionLinks } from './domain/useCases/GetCollectionLinks'

const collectionsRepository = new CollectionsRepository()

Expand All @@ -28,6 +31,9 @@ const updateCollectionFeaturedItems = new UpdateCollectionFeaturedItems(collecti
const deleteCollectionFeaturedItems = new DeleteCollectionFeaturedItems(collectionsRepository)
const deleteCollection = new DeleteCollection(collectionsRepository)
const deleteCollectionFeaturedItem = new DeleteCollectionFeaturedItem(collectionsRepository)
const linkCollection = new LinkCollection(collectionsRepository)
const unlinkCollection = new UnlinkCollection(collectionsRepository)
const getCollectionLinks = new GetCollectionLinks(collectionsRepository)

export {
getCollection,
Expand All @@ -42,7 +48,10 @@ export {
updateCollectionFeaturedItems,
deleteCollectionFeaturedItems,
deleteCollection,
deleteCollectionFeaturedItem
deleteCollectionFeaturedItem,
linkCollection,
unlinkCollection,
getCollectionLinks
}
export { Collection, CollectionInputLevel } from './domain/models/Collection'
export { CollectionFacet } from './domain/models/CollectionFacet'
Expand Down
36 changes: 36 additions & 0 deletions src/collections/infra/repositories/CollectionsRepository.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ import { ICollectionsRepository } from '../../domain/repositories/ICollectionsRe
import {
transformCollectionFacetsResponseToCollectionFacets,
transformCollectionItemsResponseToCollectionItemSubset,
transformCollectionLinksResponseToCollectionLinks,
transformCollectionResponseToCollection,
transformMyDataResponseToCollectionItemSubset
} from './transformers/collectionTransformers'
Expand Down Expand Up @@ -36,6 +37,7 @@ import {
import { ApiConstants } from '../../../core/infra/repositories/ApiConstants'
import { PublicationStatus } from '../../../core/domain/models/PublicationStatus'
import { ReadError } from '../../../core/domain/repositories/ReadError'
import { CollectionLinks } from '../../domain/models/CollectionLinks'

export interface NewCollectionRequestPayload {
alias: string
Expand Down Expand Up @@ -446,4 +448,38 @@ export class CollectionsRepository extends ApiRepository implements ICollections
throw error
})
}
public async linkCollection(
linkedCollectionIdOrAlias: number | string,
linkingCollectionIdOrAlias: number | string
): Promise<void> {
return this.doPut(
`/dataverses/${linkedCollectionIdOrAlias}/link/${linkingCollectionIdOrAlias}`,
{} // No data is needed for this operation
)
.then(() => undefined)
.catch((error) => {
throw error
})
}
public async unlinkCollection(
linkedCollectionIdOrAlias: number | string,
linkingCollectionIdOrAlias: number | string
): Promise<void> {
return this.doDelete(
`/dataverses/${linkedCollectionIdOrAlias}/deleteLink/${linkingCollectionIdOrAlias}`
)
.then(() => undefined)
.catch((error) => {
throw error
})
}
public async getCollectionLinks(collectionIdOrAlias: number | string): Promise<CollectionLinks> {
return this.doGet(`/${this.collectionsResourceName}/${collectionIdOrAlias}/links`, true)
.then((response) => {
return transformCollectionLinksResponseToCollectionLinks(response)
})
.catch((error) => {
throw error
})
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,7 @@ import {
PublicationStatusCount
} from '../../../domain/models/MyDataCollectionItemSubset'
import { PublicationStatus } from '../../../../core/domain/models/PublicationStatus'
import { CollectionLinks } from '../../../domain/models/CollectionLinks'

export const transformCollectionResponseToCollection = (response: AxiosResponse): Collection => {
const collectionPayload = response.data.data
Expand Down Expand Up @@ -152,7 +153,19 @@ export const transformCollectionItemsResponseToCollectionItemSubset = (
...(countPerObjectType && { countPerObjectType })
}
}

export const transformCollectionLinksResponseToCollectionLinks = (
response: AxiosResponse
): CollectionLinks => {
const responseDataPayload = response.data.data
const linkedCollections = responseDataPayload.linkedDataverses
const collectionsLinkingToThis = responseDataPayload.dataversesLinkingToThis
const linkedDatasets = responseDataPayload.linkedDatasets
return {
linkedCollections,
collectionsLinkingToThis,
linkedDatasets
}
}
export const transformMyDataResponseToCollectionItemSubset = (
response: AxiosResponse
): MyDataCollectionItemSubset => {
Expand Down
4 changes: 4 additions & 0 deletions src/datasets/domain/models/DatasetSummary.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
export interface DatasetSummary {
persistentId: string
title: string
}
71 changes: 71 additions & 0 deletions test/functional/collections/LinkCollection.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,71 @@
import {
ApiConfig,
WriteError,
createCollection,
getCollection,
linkCollection,
deleteCollection,
getCollectionItems
} from '../../../src'
import { TestConstants } from '../../testHelpers/TestConstants'
import { DataverseApiAuthMechanism } from '../../../src/core/infra/repositories/ApiConfig'
import { createCollectionDTO } from '../../testHelpers/collections/collectionHelper'

describe('execute', () => {
const firstCollectionAlias = 'linkCollection-functional-test-first'
const secondCollectionAlias = 'linkCollection-functional-test-second'
let firstCollectionId: number
let secondCollectionId: number
beforeEach(async () => {
ApiConfig.init(
TestConstants.TEST_API_URL,
DataverseApiAuthMechanism.API_KEY,
process.env.TEST_API_KEY
)
const firstCollection = createCollectionDTO(firstCollectionAlias)
const secondCollection = createCollectionDTO(secondCollectionAlias)
firstCollectionId = await createCollection.execute(firstCollection)
secondCollectionId = await createCollection.execute(secondCollection)
})

afterEach(async () => {
await Promise.all([
deleteCollection.execute(firstCollectionId),
deleteCollection.execute(secondCollectionId)
])
})

test('should successfully link two collections', async () => {
expect.assertions(1)
try {
await linkCollection.execute(secondCollectionAlias, firstCollectionAlias)
} catch (error) {
throw new Error('Collections should be linked successfully')
} finally {
// Wait for the linking to be processed by Solr
await new Promise((resolve) => setTimeout(resolve, 5000))
const collectionItemSubset = await getCollectionItems.execute(firstCollectionAlias)

expect(collectionItemSubset.items.length).toBe(1)
}
})

test('should throw an error when linking a non-existent collection', async () => {
const invalidCollectionId = 99999
const firstCollection = await getCollection.execute(firstCollectionAlias)

expect.assertions(2)
let writeError: WriteError | undefined = undefined
try {
await linkCollection.execute(invalidCollectionId, firstCollection.id)
throw new Error('Use case should throw an error')
} catch (error) {
writeError = error as WriteError
} finally {
expect(writeError).toBeInstanceOf(WriteError)
expect(writeError?.message).toEqual(
`There was an error when writing the resource. Reason was: [404] Can't find dataverse with identifier='${invalidCollectionId}'`
)
}
})
})
71 changes: 71 additions & 0 deletions test/functional/collections/UnlinkCollection.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,71 @@
import {
ApiConfig,
WriteError,
createCollection,
linkCollection,
deleteCollection,
getCollectionItems,
unlinkCollection
} from '../../../src'
import { TestConstants } from '../../testHelpers/TestConstants'
import { DataverseApiAuthMechanism } from '../../../src/core/infra/repositories/ApiConfig'
import { createCollectionDTO } from '../../testHelpers/collections/collectionHelper'

describe('execute', () => {
const firstCollectionAlias = 'unlinkCollection-functional-test-first'
const secondCollectionAlias = 'unlinkCollection-functional-test-second'

let firstCollectionId: number
let secondCollectionId: number
beforeEach(async () => {
ApiConfig.init(
TestConstants.TEST_API_URL,
DataverseApiAuthMechanism.API_KEY,
process.env.TEST_API_KEY
)
const firstCollectionDTO = createCollectionDTO(firstCollectionAlias)
const secondCollectionDTO = createCollectionDTO(secondCollectionAlias)
firstCollectionId = await createCollection.execute(firstCollectionDTO)
secondCollectionId = await createCollection.execute(secondCollectionDTO)
await linkCollection.execute(secondCollectionAlias, firstCollectionAlias)
// Give enough time to Solr for indexing
await new Promise((resolve) => setTimeout(resolve, 5000))
})

afterEach(async () => {
await Promise.all([
deleteCollection.execute(firstCollectionId),
deleteCollection.execute(secondCollectionId)
])
})

test('should successfully unlink two collections', async () => {
// Verify that the collections are linked
const collectionItemSubset = await getCollectionItems.execute(firstCollectionAlias)
expect(collectionItemSubset.items.length).toBe(1)

await unlinkCollection.execute(secondCollectionAlias, firstCollectionAlias)
// Wait for the unlinking to be processed by Solr
await new Promise((resolve) => setTimeout(resolve, 5000))
const collectionItemSubset2 = await getCollectionItems.execute(firstCollectionAlias)
expect(collectionItemSubset2.items.length).toBe(0)
})

test('should throw an error when unlinking a non-existent collection', async () => {
const invalidCollectionId = 99999

expect.assertions(2)
let writeError: WriteError | undefined = undefined
try {
await unlinkCollection.execute(invalidCollectionId, firstCollectionId)
throw new Error('Use case should throw an error')
} catch (error) {
writeError = error as WriteError
} finally {
expect(writeError).toBeInstanceOf(WriteError)
expect(writeError?.message).toEqual(
`There was an error when writing the resource. Reason was: [404] Can't find dataverse with identifier='${invalidCollectionId}'`
)
}
})
})
Loading
Loading