Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Get collection metadata blocks #141

Merged
merged 10 commits into from
Apr 15, 2024
27 changes: 27 additions & 0 deletions docs/useCases.md
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,7 @@ The different use cases currently available in the package are classified below,
- [Metadata Blocks](#metadata-blocks)
- [Metadata Blocks read use cases](#metadata-blocks-read-use-cases)
- [Get Metadata Block By Name](#get-metadata-block-by-name)
- [Get Collection Metadata Blocks](#get-collection-metadata-blocks)
- [Users](#Users)
- [Users read use cases](#users-read-use-cases)
- [Get Current Authenticated User](#get-current-authenticated-user)
Expand Down Expand Up @@ -765,6 +766,32 @@ getMetadataBlockByName.execute(name).then((metadataBlock: MetadataBlock) => {

_See [use case](../src/metadataBlocks/domain/useCases/GetMetadataBlockByName.ts) implementation_.

#### Get Collection Metadata Blocks

Returns a [MetadataBlock](../src/metadataBlocks/domain/models/MetadataBlock.ts) array containing the metadata blocks from the requested collection.

##### Example call:

```typescript
import { getCollectionMetadataBlocks } from '@iqss/dataverse-client-javascript'

/* ... */

const collectionIdOrAlias = 'citation'

getCollectionMetadataBlocks.execute(collectionAlias).then((metadataBlocks: MetadataBlock[]) => {
/* ... */
})

/* ... */
```

_See [use case](../src/metadataBlocks/domain/useCases/GetCollectionMetadataBlocks.ts) implementation_.

The `collectionIdOrAlias` is a generic collection identifier, which can be either a string (for queries by CollectionAlias), or a number (for queries by CollectionId).

There is a second optional parameter called `onlyDisplayedOnCreate` which indicates whether or not to return only the metadata blocks that are displayed on dataset creation. The default value is false.

## Users

### Users read use cases
Expand Down
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import { Collection } from '../models/Collection'

export interface ICollectionsRepository {
getCollection(collectionIdOrAlias: number | string): Promise<Collection>
}
14 changes: 4 additions & 10 deletions src/collections/infra/repositories/CollectionsRepository.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,22 +2,16 @@ import { ApiRepository } from '../../../core/infra/repositories/ApiRepository'
import { ICollectionsRepository } from '../../domain/repositories/ICollectionsRepository'
import { transformCollectionResponseToCollection } from './transformers/collectionTransformers'
import { Collection, ROOT_COLLECTION_ALIAS } from '../../domain/models/Collection'

export class CollectionsRepository extends ApiRepository implements ICollectionsRepository {
private readonly collectionsResourceName: string = 'dataverses'
private readonly collectionsDefaultOperationType: string = 'get'

public async getCollection(
collectionIdOrAlias: number | string = ROOT_COLLECTION_ALIAS
): Promise<Collection> {
return this.doGet(
this.buildApiEndpoint(
this.collectionsResourceName,
this.collectionsDefaultOperationType,
collectionIdOrAlias
),
true,
{ returnOwners: true }
)
return this.doGet(`/${this.collectionsResourceName}/${collectionIdOrAlias}`, true, {
returnOwners: true
})
.then((response) => transformCollectionResponseToCollection(response))
.catch((error) => {
throw error
Expand Down
4 changes: 0 additions & 4 deletions src/core/infra/repositories/ApiRepository.ts
Original file line number Diff line number Diff line change
Expand Up @@ -39,10 +39,6 @@ export abstract class ApiRepository {
operation: string,
resourceId: number | string = undefined
) {
if (resourceName === 'dataverses') {
return `/${resourceName}/${resourceId}`
}

return typeof resourceId === 'number'
? `/${resourceName}/${resourceId}/${operation}`
: typeof resourceId === 'string'
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import {
} from './BaseMetadataFieldValidator'
import { NewDatasetMetadataFieldValueDTO } from '../../dtos/NewDatasetDTO'
import { SingleMetadataFieldValidator } from './SingleMetadataFieldValidator'
import { MetadataFieldType } from '../../../../metadataBlocks/domain/models/MetadataBlock'

export class MultipleMetadataFieldValidator extends BaseMetadataFieldValidator {
constructor(private singleMetadataFieldValidator: SingleMetadataFieldValidator) {
Expand All @@ -19,14 +20,17 @@ export class MultipleMetadataFieldValidator extends BaseMetadataFieldValidator {
'Expecting an array of values.'
)
}
if (this.isValidArrayType(metadataFieldValue, 'string') && metadataFieldInfo.type === 'NONE') {
if (
this.isValidArrayType(metadataFieldValue, 'string') &&
metadataFieldInfo.type === MetadataFieldType.None
) {
throw this.createGeneralValidationError(
newDatasetMetadataFieldAndValueInfo,
'Expecting an array of child fields, not strings.'
)
} else if (
this.isValidArrayType(metadataFieldValue, 'object') &&
metadataFieldInfo.type !== 'NONE'
metadataFieldInfo.type !== MetadataFieldType.None
) {
throw this.createGeneralValidationError(
newDatasetMetadataFieldAndValueInfo,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ import { DateFormatFieldError } from './errors/DateFormatFieldError'
import { MetadataFieldValidator } from './MetadataFieldValidator'
import { NewDatasetMetadataChildFieldValueDTO } from '../../dtos/NewDatasetDTO'
import { MultipleMetadataFieldValidator } from './MultipleMetadataFieldValidator'
import { MetadataFieldType } from '../../../../metadataBlocks/domain/models/MetadataBlock'

export class SingleMetadataFieldValidator extends BaseMetadataFieldValidator {
validate(newDatasetMetadataFieldAndValueInfo: NewDatasetMetadataFieldAndValueInfo): void {
Expand All @@ -18,13 +19,19 @@ export class SingleMetadataFieldValidator extends BaseMetadataFieldValidator {
'Expecting a single field, not an array.'
)
}
if (typeof metadataFieldValue === 'object' && metadataFieldInfo.type !== 'NONE') {
if (
typeof metadataFieldValue === 'object' &&
metadataFieldInfo.type !== MetadataFieldType.None
) {
throw this.createGeneralValidationError(
newDatasetMetadataFieldAndValueInfo,
'Expecting a string, not child fields.'
)
}
if (typeof metadataFieldValue === 'string' && metadataFieldInfo.type === 'NONE') {
if (
typeof metadataFieldValue === 'string' &&
metadataFieldInfo.type === MetadataFieldType.None
) {
throw this.createGeneralValidationError(
newDatasetMetadataFieldAndValueInfo,
'Expecting child fields, not a string.'
Expand All @@ -41,7 +48,7 @@ export class SingleMetadataFieldValidator extends BaseMetadataFieldValidator {
this.validateControlledVocabularyFieldValue(newDatasetMetadataFieldAndValueInfo)
}

if (metadataFieldInfo.type == 'DATE') {
if (metadataFieldInfo.type == MetadataFieldType.Date) {
this.validateDateFieldValue(newDatasetMetadataFieldAndValueInfo)
}

Expand Down
40 changes: 37 additions & 3 deletions src/metadataBlocks/domain/models/MetadataBlock.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,15 +3,16 @@ export interface MetadataBlock {
name: string
displayName: string
metadataFields: Record<string, MetadataFieldInfo>
displayOnCreate: boolean
}

export interface MetadataFieldInfo {
name: string
displayName: string
title: string
type: string
typeClass: string
watermark: string
type: MetadataFieldType
typeClass: MetadataFieldTypeClass
watermark: MetadataFieldWatermark
description: string
multiple: boolean
isControlledVocabulary: boolean
Expand All @@ -20,4 +21,37 @@ export interface MetadataFieldInfo {
childMetadataFields?: Record<string, MetadataFieldInfo>
isRequired: boolean
displayOrder: number
displayOnCreate: boolean
}

export enum MetadataFieldType {
Date = 'DATE',
Email = 'EMAIL',
Float = 'FLOAT',
Int = 'INT',
None = 'NONE',
Text = 'TEXT',
Textbox = 'TEXTBOX',
URL = 'URL'
}

export enum MetadataFieldTypeClass {
Compound = 'compound',
ControlledVocabulary = 'controlledVocabulary',
Primitive = 'primitive'
}

export enum MetadataFieldWatermark {
Empty = '',
EnterAFloatingPointNumber = 'Enter a floating-point number.',
EnterAnInteger = 'Enter an integer.',
FamilyNameGivenNameOrOrganization = 'FamilyName, GivenName or Organization',
HTTPS = 'https://',
NameEmailXyz = 'name@email.xyz',
OrganizationXYZ = 'Organization XYZ',
The1FamilyNameGivenNameOr2Organization = '1) FamilyName, GivenName or 2) Organization',
The1FamilyNameGivenNameOr2OrganizationXYZ = '1) Family Name, Given Name or 2) Organization XYZ',
WatermarkEnterAnInteger = 'Enter an integer...',
YYYYOrYYYYMMOrYYYYMMDD = 'YYYY or YYYY-MM or YYYY-MM-DD',
YyyyMmDD = 'YYYY-MM-DD'
}
Original file line number Diff line number Diff line change
Expand Up @@ -2,4 +2,9 @@ import { MetadataBlock } from '../models/MetadataBlock'

export interface IMetadataBlocksRepository {
getMetadataBlockByName(metadataBlockName: string): Promise<MetadataBlock>

getCollectionMetadataBlocks(
collectionIdOrAlias: number | string,
onlyDisplayedOnCreate: boolean
): Promise<MetadataBlock[]>
}
30 changes: 30 additions & 0 deletions src/metadataBlocks/domain/useCases/GetCollectionMetadataBlocks.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
import { UseCase } from '../../../core/domain/useCases/UseCase'
import { MetadataBlock } from '../..'
import { ROOT_COLLECTION_ALIAS } from '../../../collections/domain/models/Collection'
import { IMetadataBlocksRepository } from '../repositories/IMetadataBlocksRepository'

export class GetCollectionMetadataBlocks implements UseCase<MetadataBlock[]> {
Comment on lines +4 to +6
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

To adhere to our naming conventions, should we consider renaming the method to GetMetadataBlocksByCollection? Given that we are in the MetadataBlocks domain, this name explicitly reflects that we're retrieving metadata blocks.

Additionally, this naming strategy enhances usability in IDEs. When developers start typing getMetadataBlocks, the IDE can autocomplete with all related fetching methods like byName, byCollection, etc. This could streamline finding the right method compared to starting with getCollection, which might not immediately suggest that it pertains to metadata blocks.

Alternatively, we could consider moving this use case to the collection domain. In that context, naming the method getCollectionMetadataBlocks would be more semantically appropriate, as it clearly indicates that the method retrieves metadata blocks specific to collections.

QA police 🚨 😆

Copy link
Contributor Author

@GPortas GPortas Apr 11, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I like the naming convention you describe, and makes sense to me.

In any case, the naming convention that we have mostly followed so far in the package is the one I have used. If you notice, it is used in almost all use cases that return a property "by a parameter".

For example, see: GetDatasetFiles, GetFileCitation, GetFileDataTables, etc.

To be consistent with the naming convention, we must rename all or none. We can create a separate issue for general renaming and make all use cases follow the same naming strategy (And mention this requirement in the dev guidelines). Let me know what you think. @MellyGray

Copy link
Contributor

@MellyGray MellyGray Apr 15, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It's fine for me to create a separate issue.

However, based on the use cases you describe, I don't see the same problem with GetFileCitation and GetFileDataTables. They are in the file domain, and as they are attributes of the file, they sound fine, I wouldn't change those.

In the case of GetDatasetFiles, it might be more similar to the metadatablock case, because you are in the files domain and you want some files. However, you have to start typing getDataset, which might make more sense as getFilesByDatasetId or simply getFiles, as the necessary datasetId could be specified through the method parameters.

In any case, I think it makes sense to choose a naming convention that aligns well with IDE autocomplete and to create a separate issue for that

private metadataBlocksRepository: IMetadataBlocksRepository

constructor(metadataBlocksRepository: IMetadataBlocksRepository) {
this.metadataBlocksRepository = metadataBlocksRepository
}

/**
* Returns a MetadataBlock array containing the metadata blocks from the requested collection.
*
* @param {number | string} [collectionIdOrAlias = 'root'] - 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'
* @param {boolean} [onlyDisplayedOnCreate=false] - Indicates whether or not to return only the metadata blocks that are displayed on dataset creation. The default value is false.
* @returns {Promise<MetadataBlock[]>}
*/
async execute(
collectionIdOrAlias: number | string = ROOT_COLLECTION_ALIAS,
onlyDisplayedOnCreate = false
): Promise<MetadataBlock[]> {
return await this.metadataBlocksRepository.getCollectionMetadataBlocks(
collectionIdOrAlias,
onlyDisplayedOnCreate
)
}
}
11 changes: 9 additions & 2 deletions src/metadataBlocks/index.ts
Original file line number Diff line number Diff line change
@@ -1,9 +1,16 @@
import { GetMetadataBlockByName } from './domain/useCases/GetMetadataBlockByName'
import { MetadataBlocksRepository } from './infra/repositories/MetadataBlocksRepository'
import { GetCollectionMetadataBlocks } from './domain/useCases/GetCollectionMetadataBlocks'

const metadataBlocksRepository = new MetadataBlocksRepository()

const getMetadataBlockByName = new GetMetadataBlockByName(metadataBlocksRepository)
const getCollectionMetadataBlocks = new GetCollectionMetadataBlocks(metadataBlocksRepository)

export { getMetadataBlockByName }
export { MetadataBlock, MetadataFieldInfo } from './domain/models/MetadataBlock'
export { getMetadataBlockByName, getCollectionMetadataBlocks }
export {
MetadataBlock,
MetadataFieldInfo,
MetadataFieldType,
MetadataFieldTypeClass
} from './domain/models/MetadataBlock'
Original file line number Diff line number Diff line change
@@ -1,7 +1,10 @@
import { ApiRepository } from '../../../core/infra/repositories/ApiRepository'
import { IMetadataBlocksRepository } from '../../domain/repositories/IMetadataBlocksRepository'
import { MetadataBlock } from '../../domain/models/MetadataBlock'
import { transformMetadataBlockResponseToMetadataBlock } from './transformers/metadataBlockTransformers'
import {
transformMetadataBlockResponseToMetadataBlock,
transformMetadataBlocksResponseToMetadataBlocks
} from './transformers/metadataBlockTransformers'

export class MetadataBlocksRepository extends ApiRepository implements IMetadataBlocksRepository {
public async getMetadataBlockByName(metadataBlockName: string): Promise<MetadataBlock> {
Expand All @@ -11,4 +14,18 @@ export class MetadataBlocksRepository extends ApiRepository implements IMetadata
throw error
})
}

public async getCollectionMetadataBlocks(
collectionIdOrAlias: string | number,
onlyDisplayedOnCreate: boolean
): Promise<MetadataBlock[]> {
return this.doGet(`/dataverses/${collectionIdOrAlias}/metadatablocks`, true, {
onlyDisplayedOnCreate: onlyDisplayedOnCreate,
returnDatasetFieldTypes: true
})
.then((response) => transformMetadataBlocksResponseToMetadataBlocks(response))
.catch((error) => {
throw error
})
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
export interface MetadataBlockPayload {
id: number
name: string
displayName: string
displayOnCreate: boolean
fields: Record<string, unknown>
}
Original file line number Diff line number Diff line change
@@ -1,10 +1,34 @@
import { AxiosResponse } from 'axios'
import { MetadataBlock, MetadataFieldInfo } from '../../../domain/models/MetadataBlock'
import {
MetadataBlock,
MetadataFieldInfo,
MetadataFieldTypeClass,
MetadataFieldType,
MetadataFieldWatermark
} from '../../../domain/models/MetadataBlock'
import { MetadataBlockPayload } from './MetadataBlockPayload'

export const transformMetadataBlocksResponseToMetadataBlocks = (
response: AxiosResponse
): MetadataBlock[] => {
const metadataBlocksPayload = response.data.data
const metadataBlocks: MetadataBlock[] = []
metadataBlocksPayload.forEach(function (metadataBlockPayload: MetadataBlockPayload) {
metadataBlocks.push(transformMetadataBlockPayloadToMetadataBlock(metadataBlockPayload))
})
return metadataBlocks
}

export const transformMetadataBlockResponseToMetadataBlock = (
response: AxiosResponse
): MetadataBlock => {
const metadataBlockPayload = response.data.data
return transformMetadataBlockPayloadToMetadataBlock(metadataBlockPayload)
}

const transformMetadataBlockPayloadToMetadataBlock = (
metadataBlockPayload: MetadataBlockPayload
): MetadataBlock => {
const metadataFields: Record<string, MetadataFieldInfo> = {}
const metadataBlockFieldsPayload = metadataBlockPayload.fields
const childFieldKeys = getChildFieldKeys(metadataBlockFieldsPayload)
Expand All @@ -19,6 +43,7 @@ export const transformMetadataBlockResponseToMetadataBlock = (
id: metadataBlockPayload.id,
name: metadataBlockPayload.name,
displayName: metadataBlockPayload.displayName,
displayOnCreate: metadataBlockPayload.displayOnCreate,
metadataFields: metadataFields
}
}
Expand Down Expand Up @@ -46,8 +71,8 @@ const transformPayloadMetadataFieldInfo = (
name: metadataFieldInfoPayload.name,
displayName: metadataFieldInfoPayload.displayName,
title: metadataFieldInfoPayload.title,
type: metadataFieldInfoPayload.type,
watermark: metadataFieldInfoPayload.watermark,
type: metadataFieldInfoPayload.type as MetadataFieldType,
watermark: metadataFieldInfoPayload.watermark as MetadataFieldWatermark,
description: metadataFieldInfoPayload.description,
multiple: metadataFieldInfoPayload.multiple,
isControlledVocabulary: metadataFieldInfoPayload.isControlledVocabulary,
Expand All @@ -57,7 +82,8 @@ const transformPayloadMetadataFieldInfo = (
displayFormat: metadataFieldInfoPayload.displayFormat,
isRequired: metadataFieldInfoPayload.isRequired,
displayOrder: metadataFieldInfoPayload.displayOrder,
typeClass: metadataFieldInfoPayload.typeClass
typeClass: metadataFieldInfoPayload.typeClass as MetadataFieldTypeClass,
displayOnCreate: metadataFieldInfoPayload.displayOnCreate
}
if (!isChild && 'childFields' in metadataFieldInfoPayload) {
const childMetadataFieldsPayload = metadataFieldInfoPayload.childFields
Expand Down
Loading
Loading