Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
22 commits
Select commit Hold shift + click to select a range
6475b3a
ensures fields required for the lookups are included
abdimo101 Sep 16, 2025
6aa1deb
changed comments and eslint fix
abdimo101 Sep 16, 2025
497573e
API test added
abdimo101 Sep 16, 2025
9c5f112
ensures fields required for the lookups are included
abdimo101 Sep 16, 2025
3d7a81b
changed comments and eslint fix
abdimo101 Sep 16, 2025
48af15a
API test added
abdimo101 Sep 16, 2025
646ac51
modified parsePipelineProjection to add include field to the projection
abdimo101 Sep 18, 2025
6e101b1
Merge branch 'fix-preserve-lookup-fields-for-relationships' of https:…
abdimo101 Sep 18, 2025
c779675
fixed failing tests
abdimo101 Sep 18, 2025
4a1e565
changes to fix failing tests + added a new api test
abdimo101 Sep 22, 2025
39790ff
Merge branch 'master' into fix-preserve-lookup-fields-for-relationships
abdimo101 Sep 22, 2025
404c4a9
eslint fix
abdimo101 Sep 22, 2025
da48ded
removed .only from api test
abdimo101 Sep 22, 2025
cf6c02b
fixed api test
abdimo101 Sep 22, 2025
1216b51
Merge branch 'master' into fix-preserve-lookup-fields-for-relationships
abdimo101 Sep 22, 2025
427685f
Merge branch 'master' into fix-preserve-lookup-fields-for-relationships
abdimo101 Sep 29, 2025
0388494
removed fields inside scope and added some requested changes
abdimo101 Sep 29, 2025
0d262ab
reverted back some changes to only fix the bug, also added error hand…
abdimo101 Oct 3, 2025
34ee21b
eslint fix
abdimo101 Oct 6, 2025
9344d17
fixed failing api tests
abdimo101 Oct 6, 2025
ff897ba
Merge branch 'master' into fix-preserve-lookup-fields-for-relationships
abdimo101 Oct 6, 2025
0b1bde8
Merge branch 'master' into fix-preserve-lookup-fields-for-relationships
minottic Oct 15, 2025
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
67 changes: 51 additions & 16 deletions src/datasets/datasets.service.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,10 @@
import { Inject, Injectable, NotFoundException, Scope } from "@nestjs/common";
import {
BadRequestException,
Inject,
Injectable,
NotFoundException,
Scope,
} from "@nestjs/common";
import { ConfigService } from "@nestjs/config";
import { REQUEST } from "@nestjs/core";
import { InjectModel } from "@nestjs/mongoose";
Expand Down Expand Up @@ -77,6 +83,7 @@ export class DatasetsService {
this.extractRelationsAndScopes(datasetLookupFields);

const scopes = relationsAndScopes.scopes;
const addedRelations: string[] = [];
for (const field of relationsAndScopes.relations) {
const fieldValue = structuredClone(DATASET_LOOKUP_FIELDS[field]);
if (!fieldValue) continue;
Expand All @@ -97,7 +104,7 @@ export class DatasetsService {
includePipeline.push({ $limit: scope.limits.limit });
if (scope?.limits?.sort) {
const sort = parsePipelineSort(scope.limits.sort);
pipeline.push({ $sort: sort });
includePipeline.push({ $sort: sort });
}

if (includePipeline.length > 0)
Expand All @@ -106,7 +113,9 @@ export class DatasetsService {
).concat(includePipeline);

pipeline.push(fieldValue);
addedRelations.push(field);
}
return addedRelations;
}

private extractRelationsAndScopes(
Expand Down Expand Up @@ -183,15 +192,21 @@ export class DatasetsService {
filter: IDatasetFiltersV4<DatasetDocument, IDatasetFields>,
): Promise<PartialOutputDatasetDto[]> {
const whereFilter: FilterQuery<DatasetDocument> = filter.where ?? {};
const fieldsProjection = (filter.fields ?? []) as string[];
let fieldsProjection = (filter.fields ?? []) as string[];
const limits = filter.limits ?? {
limit: 10,
skip: 0,
sort: { createdAt: "desc" },
};

const pipeline: PipelineStage[] = [{ $match: whereFilter }];
this.addLookupFields(pipeline, filter.include);
const addedRelations = this.addLookupFields(pipeline, filter.include);

if (Array.isArray(fieldsProjection) && fieldsProjection.length > 0) {
fieldsProjection = Array.from(
new Set([...fieldsProjection, ...addedRelations]),
);
}

if (!isEmpty(fieldsProjection)) {
const projection = parsePipelineProjection(fieldsProjection);
Expand All @@ -206,12 +221,18 @@ export class DatasetsService {
pipeline.push({ $skip: limits.skip || 0 });

pipeline.push({ $limit: limits.limit || 10 });
try {
const data = await this.datasetModel
.aggregate<PartialOutputDatasetDto>(pipeline)
.exec();

const data = await this.datasetModel
.aggregate<PartialOutputDatasetDto>(pipeline)
.exec();

return data;
return data;
} catch (error) {
if (error instanceof Error) {
throw new BadRequestException(error.message);
}
throw new BadRequestException("An unknown error occurred");
}
}

async fullquery(
Expand Down Expand Up @@ -312,13 +333,22 @@ export class DatasetsService {
filter: FilterQuery<DatasetDocument>,
): Promise<OutputDatasetDto | null> {
const whereFilter: FilterQuery<DatasetDocument> = filter.where ?? {};
const fieldsProjection: string[] = filter.fields ?? {};
let fieldsProjection: string[] = filter.fields ?? {};

const limits: QueryOptions<DatasetDocument> = filter.limits ?? {
skip: 0,
sort: { createdAt: "desc" },
};

const pipeline: PipelineStage[] = [{ $match: whereFilter }];
const addedRelations = this.addLookupFields(pipeline, filter.include);

if (Array.isArray(fieldsProjection)) {
fieldsProjection = Array.from(
new Set([...fieldsProjection, ...addedRelations]),
);
}

if (!isEmpty(fieldsProjection)) {
const projection = parsePipelineProjection(fieldsProjection);
pipeline.push({ $project: projection });
Expand All @@ -331,13 +361,18 @@ export class DatasetsService {

pipeline.push({ $skip: limits.skip || 0 });

this.addLookupFields(pipeline, filter.include);

const [data] = await this.datasetModel
.aggregate<OutputDatasetDto | undefined>(pipeline)
.exec();
try {
const [data] = await this.datasetModel
.aggregate<OutputDatasetDto | undefined>(pipeline)
.exec();

return data || null;
return data || null;
} catch (error) {
if (error instanceof Error) {
throw new BadRequestException(error.message);
}
throw new BadRequestException("An unknown error occurred");
}
}

async count(
Expand Down
5 changes: 3 additions & 2 deletions src/datasets/datasets.v4.controller.ts
Original file line number Diff line number Diff line change
Expand Up @@ -422,7 +422,8 @@ export class DatasetsV4Controller {
})
@ApiQuery({
name: "filter",
description: "Database filters to apply when retrieving datasets",
description:
"Database filters to apply when retrieving datasets <br> ⚠️ Do not include both a parent field (e.g. 'proposals') and its subfields (e.g. 'proposals.type') in the same request, as this will cause a path collision error. ",
required: false,
type: String,
content: getSwaggerDatasetFilterContent(),
Expand Down Expand Up @@ -593,7 +594,7 @@ export class DatasetsV4Controller {
@ApiOperation({
summary: "It returns the first dataset found.",
description:
"It returns the first dataset of the ones that matches the filter provided. The list returned can be modified by providing a filter.",
"It returns the first dataset of the ones that matches the filter provided. The list returned can be modified by providing a filter.<br> ⚠️ Do not include both a parent field (e.g. 'proposals') and its subfields (e.g. 'proposals.type') in the same request, as this will cause a path collision error.",
})
@ApiQuery({
name: "filter",
Expand Down
73 changes: 61 additions & 12 deletions test/DatasetV4.js
Original file line number Diff line number Diff line change
Expand Up @@ -612,7 +612,6 @@ describe("2500: Datasets v4 tests", () => {
.then((res) => {
res.body.should.be.a("array");
const [firstDataset] = res.body;

firstDataset.should.have.property("pid");
firstDataset.should.have.property("instruments");
firstDataset.should.have.property("proposals");
Expand Down Expand Up @@ -703,12 +702,12 @@ describe("2500: Datasets v4 tests", () => {
dataset.should.have.property("datasetName");
dataset.should.have.property("pid");
dataset.should.not.have.property("description");
dataset.should.not.have.property("instruments");
dataset.should.not.have.property("proposals");
dataset.should.not.have.property("datablocks");
dataset.should.not.have.property("attachments");
dataset.should.not.have.property("origdatablocks");
dataset.should.not.have.property("samples");
dataset.should.have.property("instruments");
dataset.should.have.property("proposals");
dataset.should.have.property("datablocks");
dataset.should.have.property("attachments");
dataset.should.have.property("origdatablocks");
dataset.should.have.property("samples");

dataset.datasetName.should.match(/Dataset/i);
});
Expand Down Expand Up @@ -907,7 +906,33 @@ describe("2500: Datasets v4 tests", () => {
});
});

it("0303: should fetch dataset relation fields if provided in the filter", async () => {
it("0303: should fetch specific dataset fields only if fields is provided in the filter with relationships", async () => {
const filter = {
include: ["instruments", "proposals"],
fields: ["datasetName"],
};

return request(appUrl)
.get(`/api/v4/datasets/findOne`)
.query({ filter: JSON.stringify(filter) })
.auth(accessTokenAdminIngestor, { type: "bearer" })
.expect(TestData.SuccessfulGetStatusCode)
.expect("Content-Type", /json/)
.then((res) => {
res.body.should.be.a("object");

res.body.should.have.property("datasetName");

res.body.should.not.have.property("description");
res.body.should.not.have.property("pid");

res.body.should.have.property("instruments");
res.body.should.have.property("proposals");
res.body.should.not.have.property("datablocks");
});
});

it("0304: should fetch dataset relation fields if provided in the filter", async () => {
const filter = {
include: ["instruments", "proposals"],
};
Expand All @@ -928,7 +953,7 @@ describe("2500: Datasets v4 tests", () => {
});
});

it("0304: should fetch all dataset relation fields if provided in the filter", async () => {
it("0305: should fetch all dataset relation fields if provided in the filter", async () => {
const filter = {
include: ["all"],
};
Expand All @@ -952,7 +977,7 @@ describe("2500: Datasets v4 tests", () => {
});
});

it("0305: should be able to fetch the dataset providing where filter", async () => {
it("0306: should be able to fetch the dataset providing where filter", async () => {
const filter = {
where: {
datasetName: {
Expand All @@ -974,7 +999,7 @@ describe("2500: Datasets v4 tests", () => {
});
});

it("0306: should be able to fetch a dataset providing all allowed filters together", async () => {
it("0307: should be able to fetch a dataset providing all allowed filters together", async () => {
const filter = {
where: {
datasetName: {
Expand Down Expand Up @@ -1017,7 +1042,7 @@ describe("2500: Datasets v4 tests", () => {
});
});

it("0307: should not be able to provide filters that are not allowed", async () => {
it("0308: should not be able to provide filters that are not allowed", async () => {
const filter = {
customField: { datasetName: "test" },
};
Expand All @@ -1029,6 +1054,30 @@ describe("2500: Datasets v4 tests", () => {
.expect(TestData.BadRequestStatusCode)
.expect("Content-Type", /json/);
});

it("0309: should throw BadRequest when subfields within the embedded documents are used in the fields projection", async () => {
const filter = {
include: ["all"],
fields: [
"datasetName",
"attachments.thumbnail",
"attachments.relationships.targetType",
"origdatablocks.dataFileList.path",
],
};

return request(appUrl)
.get(`/api/v4/datasets/findOne`)
.query({ filter: JSON.stringify(filter) })
.auth(accessTokenAdminIngestor, { type: "bearer" })
.expect(TestData.BadRequestStatusCode)
.expect("Content-Type", /json/)
.then((res) => {
res.body.should.be.a("object");
res.body.should.have.property("message");
res.body.message.should.match(/Invalid \$project :: caused by :: Path collision at origdatablocks/);
});
});
});

describe("Datasets v4 count tests", () => {
Expand Down
12 changes: 6 additions & 6 deletions test/DatasetV4Public.js
Original file line number Diff line number Diff line change
Expand Up @@ -269,12 +269,12 @@ describe("2600: Datasets v4 public endpoints tests", () => {
dataset.should.have.property("datasetName");
dataset.should.have.property("pid");
dataset.should.not.have.property("description");
dataset.should.not.have.property("instruments");
dataset.should.not.have.property("proposals");
dataset.should.not.have.property("datablocks");
dataset.should.not.have.property("attachments");
dataset.should.not.have.property("origdatablocks");
dataset.should.not.have.property("samples");
dataset.should.have.property("instruments");
dataset.should.have.property("proposals");
dataset.should.have.property("datablocks");
dataset.should.have.property("attachments");
dataset.should.have.property("origdatablocks");
dataset.should.have.property("samples");

dataset.datasetName.should.match(/Dataset/i);
});
Expand Down