diff --git a/src/common/pipes/include-validation.pipe.ts b/src/common/pipes/include-validation.pipe.ts index f6a2ebc97..7a1692c89 100644 --- a/src/common/pipes/include-validation.pipe.ts +++ b/src/common/pipes/include-validation.pipe.ts @@ -29,7 +29,9 @@ export class IncludeValidationPipe implements PipeTransform< ? inValue : isJsonString(inValue) ? JSON.parse(inValue ?? "{}").include - : Array(inValue); + : inValue.includes(",") + ? IncludeValidationPipe.splitCsv(inValue) + : Array(inValue); includeValueParsed?.map((field) => { let relationField = field; @@ -46,4 +48,15 @@ export class IncludeValidationPipe implements PipeTransform< return inValue; } + + /** + * Split a comma-separated string into individual relation values. + * Handles CSV-style serialization used by some OpenAPI client generators. + */ + static splitCsv(inValue: string): string[] { + return inValue + .split(",") + .map((s) => s.trim()) + .filter(Boolean); + } } diff --git a/src/common/utils.ts b/src/common/utils.ts index 5d2af5a41..09b2e38d2 100644 --- a/src/common/utils.ts +++ b/src/common/utils.ts @@ -1340,6 +1340,13 @@ export function decodeMetadataKeyStrings(keys: string[]): string[] { return keys.map((key) => decodeURIComponentExtended(key)); } +export function filterNullFromArray( + value: (T | null | undefined)[] | undefined, +): T[] { + if (!value) return []; + return value.filter((item): item is T => item !== null && item !== undefined); +} + export function parseDate(dateString?: string): Date | undefined { if (!dateString) return undefined; const parsedDate = new Date(dateString); diff --git a/src/datasets/datasets-public.v4.controller.ts b/src/datasets/datasets-public.v4.controller.ts index c8771e0f0..afdea9849 100644 --- a/src/datasets/datasets-public.v4.controller.ts +++ b/src/datasets/datasets-public.v4.controller.ts @@ -303,6 +303,8 @@ export class DatasetsPublicV4Controller { type: String, required: false, isArray: true, + style: "form", + explode: true, }) async findByIdPublic( @Param("pid") id: string, diff --git a/src/datasets/datasets.v4.controller.ts b/src/datasets/datasets.v4.controller.ts index 6d005a78b..28be5bdd2 100644 --- a/src/datasets/datasets.v4.controller.ts +++ b/src/datasets/datasets.v4.controller.ts @@ -713,6 +713,8 @@ export class DatasetsV4Controller { type: String, required: false, isArray: true, + style: "form", + explode: true, }) async findById( @Req() request: Request, diff --git a/src/datasets/dto/output-dataset.dto.ts b/src/datasets/dto/output-dataset.dto.ts index 668c5eac1..8c5163454 100644 --- a/src/datasets/dto/output-dataset.dto.ts +++ b/src/datasets/dto/output-dataset.dto.ts @@ -1,8 +1,17 @@ -import { ApiProperty, PartialType } from "@nestjs/swagger"; +import { ApiProperty, PartialType, getSchemaPath } from "@nestjs/swagger"; import { CreateDatasetDto } from "./create-dataset.dto"; -import { IsDateString, IsString } from "class-validator"; -import { decodeScientificMetadataKeys } from "src/common/utils"; -import { Transform } from "class-transformer"; +import { IsArray, IsDateString, IsOptional, IsString } from "class-validator"; +import { + decodeScientificMetadataKeys, + filterNullFromArray, +} from "src/common/utils"; +import { Transform, Type } from "class-transformer"; +import { OutputOrigDatablockDto } from "src/origdatablocks/dto/output-origdatablock.dto"; +import { Datablock } from "src/datablocks/schemas/datablock.schema"; +import { OutputAttachmentV4Dto } from "src/attachments/dto/output-attachment.v4.dto"; +import { Instrument } from "src/instruments/schemas/instrument.schema"; +import { ProposalClass } from "src/proposals/schemas/proposal.schema"; +import { OutputSampleDto } from "src/samples/dto/output-sample.dto"; export class OutputDatasetDto extends CreateDatasetDto { @ApiProperty({ @@ -51,12 +60,13 @@ export class OutputDatasetDto extends CreateDatasetDto { @ApiProperty({ type: String, - required: true, + required: false, description: "Version of the API used when the dataset was created or last updated. API version is defined in code for each release. Managed by the system.", }) + @IsOptional() @IsString() - version: string; + version?: string; @ApiProperty({ type: Object, @@ -66,6 +76,106 @@ export class OutputDatasetDto extends CreateDatasetDto { }) @Transform(({ value }) => decodeScientificMetadataKeys(value)) declare scientificMetadata?: Record; + + @Transform(({ value }) => filterNullFromArray(value)) + declare keywords?: string[]; + + @Transform(({ value }) => filterNullFromArray(value)) + declare sharedWith?: string[]; + + @Transform(({ value }) => filterNullFromArray(value)) + declare proposalIds?: string[]; + + @Transform(({ value }) => filterNullFromArray(value)) + declare sampleIds?: string[]; + + @Transform(({ value }) => filterNullFromArray(value)) + declare instrumentIds?: string[]; + + @Transform(({ value }) => filterNullFromArray(value)) + declare inputDatasets?: string[]; + + @Transform(({ value }) => filterNullFromArray(value)) + declare usedSoftware?: string[]; + + @Transform(({ value }) => filterNullFromArray(value)) + declare principalInvestigators?: string[]; + + // --------------------------------------------------------------------------- + // Includable relation fields — populated via ?include query parameter + // --------------------------------------------------------------------------- + + @ApiProperty({ + type: "array", + items: { $ref: getSchemaPath(OutputOrigDatablockDto) }, + required: false, + description: + "Containers that list all files and their attributes which make up a dataset. Included when ?include=origdatablocks is used.", + }) + @IsOptional() + @IsArray() + @Type(() => OutputOrigDatablockDto) + origdatablocks?: OutputOrigDatablockDto[]; + + @ApiProperty({ + type: "array", + items: { $ref: getSchemaPath(Datablock) }, + required: false, + description: + "Archived file blocks with checksums. Included when ?include=datablocks is used.", + }) + @IsOptional() + @IsArray() + @Type(() => Datablock) + datablocks?: Datablock[]; + + @ApiProperty({ + type: "array", + items: { $ref: getSchemaPath(OutputAttachmentV4Dto) }, + required: false, + description: + "Small attachments such as preview images. Included when ?include=attachments is used.", + }) + @IsOptional() + @IsArray() + @Type(() => OutputAttachmentV4Dto) + attachments?: OutputAttachmentV4Dto[]; + + @ApiProperty({ + type: "array", + items: { $ref: getSchemaPath(Instrument) }, + required: false, + description: + "Instruments associated with the dataset. Included when ?include=instruments is used.", + }) + @IsOptional() + @IsArray() + @Type(() => Instrument) + instruments?: Instrument[]; + + @ApiProperty({ + type: "array", + items: { $ref: getSchemaPath(ProposalClass) }, + required: false, + description: + "Proposals associated with the dataset. Included when ?include=proposals is used.", + }) + @IsOptional() + @IsArray() + @Type(() => ProposalClass) + proposals?: ProposalClass[]; + + @ApiProperty({ + type: "array", + items: { $ref: getSchemaPath(OutputSampleDto) }, + required: false, + description: + "Samples associated with the dataset. Included when ?include=samples is used.", + }) + @IsOptional() + @IsArray() + @Type(() => OutputSampleDto) + samples?: OutputSampleDto[]; } export class PartialOutputDatasetDto extends PartialType(OutputDatasetDto) {} diff --git a/src/datasets/schemas/dataset.schema.ts b/src/datasets/schemas/dataset.schema.ts index 9a8af6c39..fa7bdfab1 100644 --- a/src/datasets/schemas/dataset.schema.ts +++ b/src/datasets/schemas/dataset.schema.ts @@ -215,8 +215,8 @@ export class DatasetClass extends OwnableClass { description: "Array of tags associated with the meaning or contents of this dataset. Values should ideally come from defined vocabularies, taxonomies, ontologies or knowledge graphs.", }) - @Prop({ type: [String], required: false }) - keywords: string[]; + @Prop({ type: [String], required: false, default: [] }) + keywords: string[] = []; @ApiProperty({ type: String, @@ -366,7 +366,7 @@ export class DatasetClass extends OwnableClass { description: "First and last name of principal investigator(s). Multiple PIs can be provided as separate strings in the array. This field is required if the dataset is a Raw dataset.", }) - @Prop({ type: [String], required: false }) + @Prop({ type: [String], required: false, default: [] }) principalInvestigators?: string[]; @ApiProperty({ @@ -420,7 +420,7 @@ export class DatasetClass extends OwnableClass { description: "The ID of the proposal to which the dataset belongs to and it has been acquired under.", }) - @Prop({ type: [String], ref: "Proposal", required: false }) + @Prop({ type: [String], ref: "Proposal", required: false, default: [] }) proposalIds?: string[]; @ApiProperty({ @@ -429,7 +429,7 @@ export class DatasetClass extends OwnableClass { description: "Single ID or array of IDS of the samples used when collecting the data.", }) - @Prop({ type: [String], ref: "Sample", required: false }) + @Prop({ type: [String], ref: "Sample", required: false, default: [] }) sampleIds?: string[]; @ApiProperty({ @@ -438,7 +438,7 @@ export class DatasetClass extends OwnableClass { description: "Id of the instrument or array of IDS of the instruments where the data contained in this dataset was created/acquired.", }) - @Prop({ type: [String], ref: "Instrument", required: false }) + @Prop({ type: [String], ref: "Instrument", required: false, default: [] }) instrumentIds?: string[]; @ApiProperty({ @@ -447,7 +447,7 @@ export class DatasetClass extends OwnableClass { description: "Array of input dataset identifiers used in producing the derived dataset. Ideally these are the global identifier to existing datasets inside this or federated data catalogs.", }) - @Prop({ type: [String], required: false }) + @Prop({ type: [String], required: false, default: [] }) inputDatasets?: string[]; @ApiProperty({ @@ -456,7 +456,7 @@ export class DatasetClass extends OwnableClass { description: "A list of links to software repositories which uniquely identifies the pieces of software, including versions, used for yielding the derived data.", }) - @Prop({ type: [String], required: false }) + @Prop({ type: [String], required: false, default: [] }) usedSoftware?: string[]; @ApiProperty({ diff --git a/src/datasets/types/dataset-lookup.ts b/src/datasets/types/dataset-lookup.ts index 758aa8fa8..f73c02579 100644 --- a/src/datasets/types/dataset-lookup.ts +++ b/src/datasets/types/dataset-lookup.ts @@ -70,7 +70,7 @@ export const DATASET_LOOKUP_FIELDS: Record< $expr: { $anyElementTrue: { $map: { - input: "$relationships", + input: { $ifNull: ["$relationships", []] }, as: "relationship", in: { $and: [ diff --git a/src/origdatablocks/dto/output-origdatablock.dto.ts b/src/origdatablocks/dto/output-origdatablock.dto.ts index 42be473ad..c4663a1d9 100644 --- a/src/origdatablocks/dto/output-origdatablock.dto.ts +++ b/src/origdatablocks/dto/output-origdatablock.dto.ts @@ -1,6 +1,6 @@ import { ApiProperty, PartialType } from "@nestjs/swagger"; import { CreateOrigDatablockDto } from "./create-origdatablock.dto"; -import { IsDateString, IsString } from "class-validator"; +import { IsDateString, IsOptional, IsString } from "class-validator"; export class OutputOrigDatablockDto extends CreateOrigDatablockDto { @ApiProperty({ @@ -41,12 +41,13 @@ export class OutputOrigDatablockDto extends CreateOrigDatablockDto { @ApiProperty({ type: String, - required: true, + required: false, description: "Version of the API used when the origdatablock was created or last updated. API version is defined in code for each release. Managed by the system.", }) + @IsOptional() @IsString() - version: string; + version?: string; } export class PartialOutputOrigDatablockDto extends PartialType( diff --git a/src/origdatablocks/origdatablocks.v4.controller.ts b/src/origdatablocks/origdatablocks.v4.controller.ts index 66b874787..644a52192 100644 --- a/src/origdatablocks/origdatablocks.v4.controller.ts +++ b/src/origdatablocks/origdatablocks.v4.controller.ts @@ -748,6 +748,8 @@ export class OrigDatablocksV4Controller { type: String, required: false, isArray: true, + style: "form", + explode: true, }) async findById( @Req() request: Request, diff --git a/src/samples/dto/output-sample.dto.ts b/src/samples/dto/output-sample.dto.ts index e92f9deb1..f5688cdf1 100644 --- a/src/samples/dto/output-sample.dto.ts +++ b/src/samples/dto/output-sample.dto.ts @@ -1,5 +1,5 @@ import { ApiProperty } from "@nestjs/swagger"; -import { IsDateString, IsString } from "class-validator"; +import { IsDateString, IsOptional, IsString } from "class-validator"; import { CreateSampleDto } from "./create-sample.dto"; import { Transform } from "class-transformer"; import { decodeScientificMetadataKeys } from "src/common/utils"; @@ -47,6 +47,7 @@ export class OutputSampleDto extends CreateSampleDto { description: "Version of the API used when the dataset was created or last updated. API version is defined in code for each release. Managed by the system.", }) + @IsOptional() @IsString() version?: string;