diff --git a/src/origdatablocks/origdatablocks-public.v4.controller.ts b/src/origdatablocks/origdatablocks-public.v4.controller.ts index 6b5166b82..6adcdbb3f 100644 --- a/src/origdatablocks/origdatablocks-public.v4.controller.ts +++ b/src/origdatablocks/origdatablocks-public.v4.controller.ts @@ -14,7 +14,11 @@ import { IOrigDatablockFields, IOrigDatablockFiltersV4, } from "./interfaces/origdatablocks.interface"; -import { FullFacetFilters, FullFacetResponse } from "src/common/types"; +import { + FullFacetFilters, + FullFacetResponse, + CountApiResponse, +} from "src/common/types"; import { getSwaggerOrigDatablockFilterContent } from "./types/origdatablock-filter-content"; import { OrigDatablockLookupKeysEnum, @@ -237,4 +241,52 @@ export class OrigDatablocksPublicV4Controller { return origdatablock; } + + // GET /origdatablocks/public/files/count + @AllowAny() + @Get("/files/count") + @ApiOperation({ + summary: "It returns the count of public files", + description: + "It returns the total number of public files across all origdatablocks matching the provided filter.", + }) + @ApiQuery({ + name: "filter", + description: + "Database filters to apply when retrieving count of public files", + required: false, + type: String, + content: getSwaggerOrigDatablockFilterContent({ + where: true, + include: false, + fields: false, + limits: false, + }), + }) + @ApiResponse({ + status: HttpStatus.OK, + type: CountApiResponse, + description: + "Return the number of public files in the following format: { count: integer }", + }) + async countFilesPublic( + @Query( + "filter", + new FilterValidationPipe( + ALLOWED_ORIGDATABLOCK_KEYS, + ALLOWED_ORIGDATABLOCK_FILTER_KEYS, + { + where: true, + include: false, + fields: false, + limits: false, + }, + ), + ) + queryFilter?: string, + ) { + const parsedFilter = JSON.parse(queryFilter ?? "{}"); + + return this.origDatablocksService.countFiles(parsedFilter); + } } diff --git a/src/origdatablocks/origdatablocks.service.ts b/src/origdatablocks/origdatablocks.service.ts index 93fa40012..64e57305e 100644 --- a/src/origdatablocks/origdatablocks.service.ts +++ b/src/origdatablocks/origdatablocks.service.ts @@ -383,6 +383,48 @@ export class OrigDatablocksService { return { count }; } + async countFiles( + filter: FilterQuery, + ): Promise { + const whereFilter: FilterQuery = filter.where ?? {}; + const fieldsProjection: string[] = filter.fields ?? []; + + const pipeline: PipelineStage[] = [{ $match: whereFilter }]; + this.addLookupFields(pipeline, filter.include); + + if (!isEmpty(fieldsProjection)) { + const projection = parsePipelineProjection(fieldsProjection); + pipeline.push({ $project: projection }); + } + + pipeline.push({ + $lookup: { + from: "Dataset", + as: "dataset_temp", + let: { datasetId: "$datasetId" }, + pipeline: [{ $match: { $expr: { $eq: ["$pid", "$$datasetId"] } } }], + }, + }); + + pipeline.push({ + $addFields: { + datasetExist: { $gt: [{ $size: "$dataset_temp" }, 0] }, + }, + }); + + pipeline.push({ $unset: "dataset_temp" }); + + pipeline.push({ $unwind: "$dataFileList" }); + + pipeline.push({ $count: "count" }); + + const [result] = await this.origDatablockModel + .aggregate<{ count: number }>(pipeline) + .exec(); + + return { count: result?.count ?? 0 }; + } + async aggregateSizeAndFileCount( datasetId: string, ): Promise<{ size: number; numberOfFiles: number }> { diff --git a/src/origdatablocks/origdatablocks.v4.controller.ts b/src/origdatablocks/origdatablocks.v4.controller.ts index 8816c24da..3000dd795 100644 --- a/src/origdatablocks/origdatablocks.v4.controller.ts +++ b/src/origdatablocks/origdatablocks.v4.controller.ts @@ -58,6 +58,7 @@ import { IsValidResponse, FullFacetFilters, FullFacetResponse, + CountApiResponse, } from "src/common/types"; import { getSwaggerOrigDatablockFilterContent } from "./types/origdatablock-filter-content"; import { @@ -695,6 +696,60 @@ export class OrigDatablocksV4Controller { ); } + // GET /origdatablocks/files/count + @UseGuards(PoliciesGuard) + @CheckPolicies("origdatablocks", (ability: AppAbility) => + ability.can(Action.OrigdatablockRead, OrigDatablock), + ) + @Get("/files/count") + @ApiOperation({ + summary: "It returns the count of files", + description: + "It returns the total number of files across all origdatablocks matching the provided filter.", + }) + @ApiQuery({ + name: "filter", + description: "Database filters to apply when retrieving count for files", + required: false, + type: String, + content: getSwaggerOrigDatablockFilterContent({ + where: true, + include: false, + fields: false, + limits: false, + }), + }) + @ApiResponse({ + status: HttpStatus.OK, + type: CountApiResponse, + description: + "Return the number of files in the following format: { count: integer }", + }) + async countFiles( + @Req() request: Request, + @Query( + "filter", + new FilterValidationPipe( + ALLOWED_ORIGDATABLOCK_KEYS, + ALLOWED_ORIGDATABLOCK_FILTER_KEYS, + { + where: true, + include: false, + fields: false, + limits: false, + }, + ), + ) + queryFilter?: string, + ) { + const parsedFilter = JSON.parse(queryFilter ?? "{}"); + const mergedFilter = this.addAccessBasedFilters( + request.user as JWTUser, + parsedFilter, + ); + + return this.origDatablocksService.countFiles(mergedFilter); + } // GET /origdatablocks/:id @UseGuards(PoliciesGuard) @CheckPolicies("origdatablocks", (ability: AppAbility) => diff --git a/test/OrigDatablockV4.js b/test/OrigDatablockV4.js index ec634bdc0..f3a7968ea 100644 --- a/test/OrigDatablockV4.js +++ b/test/OrigDatablockV4.js @@ -909,15 +909,57 @@ describe("2800: OrigDatablock v4 endpoint tests", () => { }); }); + describe("OrigDatablocks v4 count tests", () => { + it("0600: should not be able to fetch the count of origdatablocks files if not logged in", async () => { + const filter = { + where: { + datasetId: { + $regex: datasetPid, + $options: "i", + }, + }, + }; + + return request(appUrl) + .get("/api/v4/origdatablocks/files/count") + .query({ filter: JSON.stringify(filter) }) + .expect(TestData.AccessForbiddenStatusCode) + .expect("Content-Type", /json/); + }); + + it("0601: should be able to fetch the count of origdatablocks files", async () => { + const filter = { + where: { + datasetId: { + $regex: datasetPid, + $options: "i", + }, + }, + }; + + return request(appUrl) + .get("/api/v4/origdatablocks/files/count") + .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("count"); + res.body.count.should.be.greaterThan(0); + }); + }); + }); + describe("OrigDatablocks v4 delete tests", () => { - it("0600: should not be able to delete origdatablock if not logged in", () => { + it("0700: should not be able to delete origdatablock if not logged in", () => { return request(appUrl) .delete(`/api/v4/origdatablocks/${encodeURIComponent(origDatablockPid)}`) .expect(TestData.AccessForbiddenStatusCode) .expect("Content-Type", /json/); }); - it("0601: should not be able to delete origdatablock as owner", () => { + it("0701: should not be able to delete origdatablock as owner", () => { return request(appUrl) .delete(`/api/v4/origdatablocks/${encodeURIComponent(origDatablockPid)}`) .auth(accessTokenUser1, { type: "bearer" }) @@ -925,7 +967,7 @@ describe("2800: OrigDatablock v4 endpoint tests", () => { .expect("Content-Type", /json/); }); - it("0602: should not be able to delete origdatablock as adminIngestor", () => { + it("0702: should not be able to delete origdatablock as adminIngestor", () => { return request(appUrl) .delete(`/api/v4/origdatablocks/${encodeURIComponent(origDatablockPid)}`) .auth(accessTokenAdminIngestor, { type: "bearer" }) @@ -933,7 +975,7 @@ describe("2800: OrigDatablock v4 endpoint tests", () => { .expect("Content-Type", /json/); }); - it("0603: should be able to delete origdatablock as archivemanager", () => { + it("0703: should be able to delete origdatablock as archivemanager", () => { return request(appUrl) .delete(`/api/v4/origdatablocks/${encodeURIComponent(origDatablockPid)}`) .auth(accessTokenArchiveManager, { type: "bearer" }) diff --git a/test/OrigDatablockV4Public.js b/test/OrigDatablockV4Public.js index afffeb06b..bc3d7c236 100644 --- a/test/OrigDatablockV4Public.js +++ b/test/OrigDatablockV4Public.js @@ -373,8 +373,33 @@ describe("2900: OrigDatablock v4 public endpoint tests", () => { }); }); + describe("OrigDatablocks v4 public count tests", () => { + it("0300: should be able to fetch the count of origdatablocks files providing where filter", async () => { + const filter = { + where: { + datasetId: { + $regex: datasetPid, + $options: "i", + }, + }, + }; + + return request(appUrl) + .get("/api/v4/origdatablocks/public/files/count") + .query({ filter: JSON.stringify(filter) }) + .expect(TestData.SuccessfulGetStatusCode) + .expect("Content-Type", /json/) + .then((res) => { + res.body.should.be.a("object"); + res.body.should.have.property("count"); + res.body.count.should.be.a("number"); + res.body.count.should.be.greaterThan(0); + }); + }); + }); + describe("Cleanup after the tests", () => { - it("0300: delete all origdatablocks as archivemanager", async () => { + it("0400: delete all origdatablocks as archivemanager", async () => { return await request(appUrl) .get("/api/v4/origdatablocks") .auth(accessTokenArchiveManager, { type: "bearer" }) @@ -385,7 +410,7 @@ describe("2900: OrigDatablock v4 public endpoint tests", () => { }); }); - it("0301: delete all datasets as archivemanager", async () => { + it("0401: delete all datasets as archivemanager", async () => { return await request(appUrl) .get("/api/v4/datasets") .auth(accessTokenArchiveManager, { type: "bearer" })