Skip to content

Commit df94798

Browse files
authored
fix: filters and limits in GET/jobs to get consistent results (#1866)
* fix filters and limits in GET to get consistent results * update swaggerUI descriptions
1 parent 3acdbd9 commit df94798

File tree

3 files changed

+106
-37
lines changed

3 files changed

+106
-37
lines changed

src/common/utils.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -969,7 +969,7 @@ export const datasetsFullQueryDescriptionFields =
969969
</pre>';
970970

971971
export const jobsFullQueryExampleFields =
972-
'{"ownerGroup":["group1"], "statusCode": "jobCreated"}';
972+
'{"ownerGroup": "group1", "statusCode": "jobCreated"}';
973973

974974
export const jobsFullQueryDescriptionFields =
975975
'<pre>\n \

src/jobs/jobs.controller.ts

Lines changed: 63 additions & 35 deletions
Original file line numberDiff line numberDiff line change
@@ -37,6 +37,7 @@ import {
3737
ApiConsumes,
3838
ApiOperation,
3939
ApiQuery,
40+
ApiParam,
4041
ApiResponse,
4142
ApiTags,
4243
} from "@nestjs/swagger";
@@ -48,6 +49,7 @@ import { JWTUser } from "src/auth/interfaces/jwt-user.interface";
4849
import { AccessGroupsType } from "src/config/configuration";
4950
import { Logger } from "@nestjs/common";
5051
import { UsersService } from "src/users/users.service";
52+
import { FullFacetResponse } from "src/common/types";
5153
import {
5254
filterDescriptionSimplified,
5355
filterExampleSimplified,
@@ -710,7 +712,7 @@ export class JobsController {
710712
description: "It creates a new job.",
711713
})
712714
@ApiBody({
713-
description: "Input fields for the job to be created",
715+
description: "Input fields for the job to be created.",
714716
required: true,
715717
type: CreateJobDtoV3,
716718
})
@@ -831,6 +833,11 @@ export class JobsController {
831833
summary: "It updates an existing job.",
832834
description: "It updates an existing job.",
833835
})
836+
@ApiParam({
837+
name: "id",
838+
description: "Id of the job to be modified.",
839+
type: String,
840+
})
834841
@ApiBody({
835842
description: "Fields for the job to be updated",
836843
required: true,
@@ -866,6 +873,11 @@ export class JobsController {
866873
"It updates an existing job. Set `content-type` to `application/merge-patch+json` if you would like to update nested objects. Warning! `application/merge-patch+json` doesn’t support updating a specific item in an array — the result will always replace the entire target if it’s not an object.",
867874
})
868875
@ApiConsumes("application/json", "application/merge-patch+json")
876+
@ApiParam({
877+
name: "id",
878+
description: "Id of the job to be modified.",
879+
type: String,
880+
})
869881
@ApiBody({
870882
description: "Fields for the job to be updated",
871883
required: true,
@@ -896,7 +908,9 @@ export class JobsController {
896908
fields: JSON.parse(filters.fields ?? ("{}" as string)),
897909
limits: JSON.parse(filters.limits ?? ("{}" as string)),
898910
};
899-
const jobsFound = await this.jobsService.fullquery(parsedFilters);
911+
const jobsFound = await this.jobsService.findByFilters(
912+
parsedFilters.fields,
913+
);
900914
const jobsAccessible: JobClass[] = [];
901915

902916
// for each job run a casl JobReadOwner on a jobInstance
@@ -921,7 +935,10 @@ export class JobsController {
921935
}
922936
}
923937
}
924-
return jobsAccessible;
938+
return this.jobsService.applyFilterLimits(
939+
jobsAccessible,
940+
parsedFilters.limits,
941+
);
925942
} catch (e) {
926943
throw new HttpException(
927944
{
@@ -1029,33 +1046,11 @@ export class JobsController {
10291046
): Promise<Record<string, unknown>[]> {
10301047
try {
10311048
const fields: IJobFields = JSON.parse(filters.fields ?? ("{}" as string));
1032-
const queryFilters: IFilters<JobDocument, FilterQuery<JobDocument>> = {
1033-
fields: fields,
1034-
limits: JSON.parse("{}" as string),
1035-
};
1036-
const jobsFound = await this.jobsService.fullquery(queryFilters);
1049+
const jobsFound = await this.fullQueryJobs(request, filters);
10371050
const jobIdsAccessible: string[] = [];
1038-
1039-
// for each job run a casl JobReadOwner on a jobInstance
10401051
if (jobsFound != null) {
10411052
for (const i in jobsFound) {
1042-
const jobConfiguration = this.getJobTypeConfiguration(
1043-
jobsFound[i].type,
1044-
);
1045-
const ability = this.caslAbilityFactory.jobsInstanceAccess(
1046-
request.user as JWTUser,
1047-
jobConfiguration,
1048-
);
1049-
// check if the user can get this job
1050-
const jobInstance = await this.generateJobInstanceForPermissions(
1051-
jobsFound[i],
1052-
);
1053-
const canRead =
1054-
ability.can(Action.JobReadAny, JobClass) ||
1055-
ability.can(Action.JobReadAccess, jobInstance);
1056-
if (canRead) {
1057-
jobIdsAccessible.push(jobsFound[i]._id);
1058-
}
1053+
jobIdsAccessible.push(jobsFound[i]._id);
10591054
}
10601055
}
10611056
fields._id = { $in: jobIdsAccessible };
@@ -1092,21 +1087,25 @@ export class JobsController {
10921087
@ApiQuery({
10931088
name: "fields",
10941089
description:
1095-
"Define the filter conditions by specifying the name of values of fields requested.",
1090+
"Define the filter conditions by specifying the values of fields requested.\n" +
1091+
jobsFullQueryDescriptionFields,
10961092
required: false,
10971093
type: String,
1094+
example: jobsFullQueryExampleFields,
10981095
})
10991096
@ApiQuery({
11001097
name: "facets",
11011098
description:
11021099
"Define a list of field names, for which facet counts should be calculated.",
11031100
required: false,
11041101
type: String,
1102+
example: '["type","ownerGroup","statusCode"]',
11051103
})
11061104
@ApiResponse({
11071105
status: HttpStatus.OK,
1108-
type: [Object],
1109-
description: "Return jobs requested.",
1106+
type: FullFacetResponse,
1107+
isArray: true,
1108+
description: "Return fullfacet response for jobs requested.",
11101109
})
11111110
async fullFacetV3(
11121111
@Req() request: Request,
@@ -1132,21 +1131,25 @@ export class JobsController {
11321131
@ApiQuery({
11331132
name: "fields",
11341133
description:
1135-
"Define the filter conditions by specifying the name of values of fields requested.",
1134+
"Define the filter conditions by specifying the values of fields requested.\n" +
1135+
jobsFullQueryDescriptionFields,
11361136
required: false,
11371137
type: String,
1138+
example: jobsFullQueryExampleFields,
11381139
})
11391140
@ApiQuery({
11401141
name: "facets",
11411142
description:
11421143
"Define a list of field names, for which facet counts should be calculated.",
11431144
required: false,
11441145
type: String,
1146+
example: '["type","ownerGroup","statusCode"]',
11451147
})
11461148
@ApiResponse({
11471149
status: HttpStatus.OK,
1148-
type: [Object],
1149-
description: "Return jobs requested.",
1150+
type: FullFacetResponse,
1151+
isArray: true,
1152+
description: "Return fullfacet response for jobs requested.",
11501153
})
11511154
async fullFacetV4(
11521155
@Req() request: Request,
@@ -1202,6 +1205,11 @@ export class JobsController {
12021205
summary: "It returns the requested job.",
12031206
description: "It returns the requested job.",
12041207
})
1208+
@ApiParam({
1209+
name: "id",
1210+
description: "Id of the job to be retrieved.",
1211+
type: String,
1212+
})
12051213
@ApiResponse({
12061214
status: HttpStatus.OK,
12071215
type: OutputJobV3Dto,
@@ -1228,6 +1236,11 @@ export class JobsController {
12281236
summary: "It returns the requested job.",
12291237
description: "It returns the requested job.",
12301238
})
1239+
@ApiParam({
1240+
name: "id",
1241+
description: "Id of the job to be retrieved.",
1242+
type: String,
1243+
})
12311244
@ApiResponse({
12321245
status: HttpStatus.OK,
12331246
type: JobClass,
@@ -1260,7 +1273,9 @@ export class JobsController {
12601273
throw { message: "Invalid filter syntax." };
12611274
}
12621275
// for each job run a casl JobReadOwner on a jobInstance
1263-
const jobsFound = await this.jobsService.findAll(parsedFilter);
1276+
const jobsFound = await this.jobsService.findByFilters(
1277+
parsedFilter.where,
1278+
);
12641279
const jobsAccessible: JobClass[] = [];
12651280

12661281
for (const i in jobsFound) {
@@ -1282,7 +1297,10 @@ export class JobsController {
12821297
jobsAccessible.push(jobsFound[i]);
12831298
}
12841299
}
1285-
return jobsAccessible;
1300+
return this.jobsService.applyFilterLimits(
1301+
jobsAccessible,
1302+
parsedFilter.limits,
1303+
);
12861304
} catch (e) {
12871305
throw new HttpException(
12881306
{
@@ -1376,6 +1394,11 @@ export class JobsController {
13761394
summary: "It deletes the requested job.",
13771395
description: "It deletes the requested job.",
13781396
})
1397+
@ApiParam({
1398+
name: "id",
1399+
description: "Id of the job to be deleted.",
1400+
type: String,
1401+
})
13791402
@ApiResponse({
13801403
status: HttpStatus.OK,
13811404
type: undefined,
@@ -1412,6 +1435,11 @@ export class JobsController {
14121435
summary: "It deletes the requested job.",
14131436
description: "It deletes the requested job.",
14141437
})
1438+
@ApiParam({
1439+
name: "id",
1440+
description: "Id of the job to be deleted.",
1441+
type: String,
1442+
})
14151443
@ApiResponse({
14161444
status: HttpStatus.OK,
14171445
type: undefined,

src/jobs/jobs.service.ts

Lines changed: 42 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,11 @@ import {
1010
UpdateQuery,
1111
} from "mongoose";
1212
import { JWTUser } from "src/auth/interfaces/jwt-user.interface";
13-
import { IFacets, IFilters } from "src/common/interfaces/common.interface";
13+
import {
14+
IFacets,
15+
IFilters,
16+
ILimitsFilter,
17+
} from "src/common/interfaces/common.interface";
1418
import {
1519
addCreatedByFields,
1620
addUpdatedByField,
@@ -50,6 +54,43 @@ export class JobsService {
5054
return createdJob.save();
5155
}
5256

57+
async findByFilters(
58+
fields: FilterQuery<JobDocument> | undefined,
59+
): Promise<JobClass[]> {
60+
const filters: FilterQuery<JobDocument> =
61+
createFullqueryFilter<JobDocument>(this.jobModel, "id", fields ?? {});
62+
return this.jobModel.find(filters).exec();
63+
}
64+
65+
applyFilterLimits(
66+
jobs: JobClass[],
67+
limits: ILimitsFilter | undefined,
68+
): JobClass[] {
69+
const modifiers: QueryOptions = parseLimitFilters(limits);
70+
if (modifiers.sort) {
71+
jobs = jobs.sort((a, b) => {
72+
for (const [key, order] of Object.entries(modifiers.sort) as [
73+
keyof JobClass,
74+
1 | -1,
75+
][]) {
76+
const aValue = a[key];
77+
const bValue = b[key];
78+
if (aValue === undefined || bValue === undefined) continue;
79+
if (aValue < bValue) return order === 1 ? -1 : 1;
80+
if (aValue > bValue) return order === 1 ? 1 : -1;
81+
}
82+
return 0;
83+
});
84+
}
85+
if (modifiers.skip) {
86+
jobs = jobs.slice(modifiers.skip);
87+
}
88+
if (modifiers.limit) {
89+
jobs = jobs.slice(0, modifiers.limit);
90+
}
91+
return jobs;
92+
}
93+
5394
async findAll(
5495
filter: IFilters<JobDocument, FilterQuery<JobDocument>>,
5596
): Promise<JobClass[]> {

0 commit comments

Comments
 (0)