diff --git a/src/common/utils.ts b/src/common/utils.ts
index 853aed4f9..fcefd6c26 100644
--- a/src/common/utils.ts
+++ b/src/common/utils.ts
@@ -1040,6 +1040,9 @@ export const datasetsFullQueryDescriptionFields =
export const jobsFullQueryExampleFields =
'{"ownerGroup": "group1", "statusCode": "jobCreated"}';
+export const jobsFullQueryExampleFieldsV3 =
+ '{"emailJobInitiator": "group1@email.com", "jobStatusMessage": "jobCreated"}';
+
export const jobsFullQueryDescriptionFields =
'
\n \
{\n \
@@ -1059,6 +1062,20 @@ export const jobsFullQueryDescriptionFields =
}\n \
';
+export const jobsFullQueryDescriptionFieldsV3 =
+ '\n \
+{\n \
+ "creationTime": { \n \
+ "begin": string,\n \
+ "end": string,\n \
+ },\n \
+ "type": string, \n \
+ "id": string, \n \
+ "jobStatusMessage": string, \n \
+ ... \n \
+}\n \
+ ';
+
export const proposalsFullQueryExampleFields =
'{"text": "some text", "proposalId": "proposal_id"}';
diff --git a/src/jobs/jobs.controller.ts b/src/jobs/jobs.controller.ts
index d93363fe4..16178bb92 100644
--- a/src/jobs/jobs.controller.ts
+++ b/src/jobs/jobs.controller.ts
@@ -41,8 +41,8 @@ import { FullFacetResponse } from "src/common/types";
import {
fullQueryDescriptionLimits,
fullQueryExampleLimits,
- jobsFullQueryExampleFields,
- jobsFullQueryDescriptionFields,
+ jobsFullQueryDescriptionFieldsV3,
+ jobsFullQueryExampleFieldsV3,
} from "src/common/utils";
import { CreateJobV3MappingInterceptor } from "./interceptors/create-job-v3-mapping.interceptor";
import { UpdateJobV3MappingInterceptor } from "./interceptors/update-job-v3-mapping.interceptor";
@@ -53,6 +53,12 @@ import { IncludeValidationPipe } from "./pipes/include-validation.pipe";
import { DatasetLookupKeysEnum } from "src/datasets/types/dataset-lookup";
import { PartialOutputDatasetDto } from "src/datasets/dto/output-dataset.dto";
import { ALLOWED_JOB_KEYS, ALLOWED_JOB_FILTER_KEYS } from "./types/job-lookup";
+import {
+ V3ConditionToV4Pipe,
+ V3FieldsToV4Pipe,
+ V3FilterToV4Pipe,
+ V3LimitsToV4Pipe,
+} from "./pipes/v3-filter.pipe";
@ApiBearerAuth()
@ApiTags("jobs")
@@ -155,10 +161,10 @@ export class JobsController {
name: "fields",
description:
"Filters to apply when retrieving jobs.\n" +
- jobsFullQueryDescriptionFields,
+ jobsFullQueryDescriptionFieldsV3,
required: false,
type: String,
- example: jobsFullQueryExampleFields,
+ example: jobsFullQueryExampleFieldsV3,
})
@ApiQuery({
name: "limits",
@@ -177,12 +183,13 @@ export class JobsController {
@SerializeOptions({ type: OutputJobV3Dto, excludeExtraneousValues: true })
async fullQuery(
@Req() request: Request,
- @Query() filters: { fields?: string; limits?: string },
+ @Query("fields", new V3ConditionToV4Pipe()) fields?: string,
+ @Query("limits", new V3LimitsToV4Pipe()) limits?: string,
): Promise {
- const jobs = (await this.jobsControllerUtils.fullQueryJobs(
- request,
- filters,
- )) as JobClass[] | null;
+ const jobs = (await this.jobsControllerUtils.fullQueryJobs(request, {
+ fields,
+ limits,
+ })) as JobClass[] | null;
return jobs as OutputJobV3Dto[] | null;
}
@@ -203,10 +210,10 @@ export class JobsController {
name: "fields",
description:
"Define the filter conditions by specifying the values of fields requested.\n" +
- jobsFullQueryDescriptionFields,
+ jobsFullQueryDescriptionFieldsV3,
required: false,
type: String,
- example: jobsFullQueryExampleFields,
+ example: jobsFullQueryExampleFieldsV3,
})
@ApiQuery({
name: "facets",
@@ -214,7 +221,7 @@ export class JobsController {
"Define a list of field names, for which facet counts should be calculated.",
required: false,
type: String,
- example: '["type","ownerGroup","statusCode"]',
+ example: '["type","ownerGroup","jobStatusMessage"]',
})
@ApiResponse({
status: HttpStatus.OK,
@@ -224,9 +231,10 @@ export class JobsController {
})
async fullFacet(
@Req() request: Request,
- @Query() filters: { fields?: string; facets?: string },
+ @Query("facets", new V3FieldsToV4Pipe()) facets?: string,
+ @Query("fields", new V3ConditionToV4Pipe()) fields?: string,
): Promise[]> {
- return this.jobsControllerUtils.fullFacetJobs(request, filters);
+ return this.jobsControllerUtils.fullFacetJobs(request, { facets, fields });
}
/**
@@ -440,6 +448,7 @@ export class JobsController {
@Query(
"filter",
+ new V3FilterToV4Pipe(),
new FilterValidationPipe(ALLOWED_JOB_KEYS, ALLOWED_JOB_FILTER_KEYS, {
where: false,
include: true,
diff --git a/src/jobs/pipes/v3-filter.pipe.ts b/src/jobs/pipes/v3-filter.pipe.ts
new file mode 100644
index 000000000..39f1b65f6
--- /dev/null
+++ b/src/jobs/pipes/v3-filter.pipe.ts
@@ -0,0 +1,197 @@
+import { PipeTransform, Injectable, ArgumentMetadata } from "@nestjs/common";
+import { jobV3toV4FieldMap } from "../types/jobs-filter-content";
+import _ from "lodash";
+
+type KeyMap = Record;
+
+type Func = (value: unknown) => unknown;
+
+type FuncMap = Record;
+
+interface TransformDeepOptions {
+ keyMap?: KeyMap;
+ funcMap?: FuncMap;
+ arrayFn?: Func;
+ valueFn?: Func;
+}
+
+const transformDeep = (
+ obj: unknown,
+ opts: TransformDeepOptions = {},
+): unknown => {
+ const { keyMap = {}, funcMap = {}, arrayFn, valueFn } = opts;
+
+ if (Array.isArray(obj)) {
+ return obj.map((item) =>
+ arrayFn ? arrayFn(transformDeep(item, opts)) : transformDeep(item, opts),
+ );
+ }
+
+ if (obj && typeof obj === "object") {
+ const newObj: Record = {};
+ for (const [key, value] of Object.entries(obj)) {
+ const mappedKey = keyMap[key] ?? key;
+ let transformed: unknown;
+ if (funcMap[key]) {
+ transformed = funcMap[key](value);
+ } else {
+ transformed = transformDeep(value, opts);
+ }
+ newObj[mappedKey] = valueFn ? valueFn(transformed) : transformed;
+ }
+ return newObj;
+ }
+
+ return obj;
+};
+
+class ParseJsonPipe implements PipeTransform {
+ transform(value: string): string {
+ if (!value || typeof value !== "string") return value;
+ try {
+ return JSON.parse(value);
+ } catch {
+ return value;
+ }
+ }
+}
+
+class ParseDeepJsonPipe implements PipeTransform {
+ private jsonPipe = new ParseJsonPipe();
+
+ transform(value: string): string | object {
+ const parsed = this.jsonPipe.transform(value);
+ return transformDeep(parsed, {
+ valueFn: (value) => this.jsonPipe.transform(value as string),
+ }) as object;
+ }
+}
+
+class ReplaceObjKeysPipe implements PipeTransform {
+ constructor(private keyMap: KeyMap) {}
+
+ transform(value: unknown): unknown {
+ return transformDeep(value, { keyMap: this.keyMap });
+ }
+}
+
+class TransformObjValuesPipe implements PipeTransform {
+ constructor(private funcMap: FuncMap) {}
+
+ transform(value: unknown): unknown {
+ return transformDeep(value, { funcMap: this.funcMap });
+ }
+}
+
+class TransformArrayValuesPipe implements PipeTransform {
+ constructor(private arrayFn: (item: unknown) => unknown) {}
+
+ transform(value: unknown): unknown {
+ return transformDeep(value, { arrayFn: this.arrayFn });
+ }
+}
+
+class ComposePipe implements PipeTransform {
+ private readonly pipes: PipeTransform[];
+ private readonly jsonToString = new JsonToStringPipe();
+ private readonly parseDeepJson = new ParseDeepJsonPipe();
+
+ constructor(
+ pipes: PipeTransform[],
+ private readonly jsonTransform = true,
+ ) {
+ this.pipes = [...pipes];
+ if (this.jsonTransform) {
+ this.pipes.unshift(this.parseDeepJson);
+ this.pipes.push(this.jsonToString);
+ }
+ }
+
+ transform(value: T, metadata: ArgumentMetadata = {} as ArgumentMetadata): T {
+ return this.pipes.reduce(
+ (val, pipe) => pipe.transform(val, metadata),
+ value,
+ );
+ }
+}
+
+class JsonToStringPipe implements PipeTransform