refactor graphql query runner connection mapper (#6771)
This commit is contained in:
+43
@@ -0,0 +1,43 @@
|
||||
import { FindOperator, Not } from 'typeorm';
|
||||
|
||||
import { FieldMetadataInterface } from 'src/engine/metadata-modules/field-metadata/interfaces/field-metadata.interface';
|
||||
|
||||
import { GraphqlQueryFilterFieldParser } from 'src/engine/api/graphql/graphql-query-runner/graphql-query-parsers/graphql-query-filter/graphql-query-filter-field.parser';
|
||||
import { FieldMetadataType } from 'src/engine/metadata-modules/field-metadata/field-metadata.entity';
|
||||
|
||||
describe('GraphqlQueryFilterFieldParser', () => {
|
||||
let parser: GraphqlQueryFilterFieldParser;
|
||||
let mockFieldMetadataMap: Record<string, FieldMetadataInterface>;
|
||||
|
||||
beforeEach(() => {
|
||||
mockFieldMetadataMap = {
|
||||
simpleField: {
|
||||
id: '1',
|
||||
name: 'simpleField',
|
||||
type: FieldMetadataType.TEXT,
|
||||
label: 'Simple Field',
|
||||
objectMetadataId: 'obj1',
|
||||
},
|
||||
};
|
||||
parser = new GraphqlQueryFilterFieldParser(mockFieldMetadataMap);
|
||||
});
|
||||
it('should parse simple field correctly', () => {
|
||||
const result = parser.parse('simpleField', 'value', false);
|
||||
|
||||
expect(result).toEqual({ simpleField: 'value' });
|
||||
});
|
||||
|
||||
it('should negate simple field correctly', () => {
|
||||
const result = parser.parse('simpleField', 'value', true);
|
||||
|
||||
expect(result).toEqual({ simpleField: Not('value') });
|
||||
});
|
||||
|
||||
it('should parse object value using operator parser', () => {
|
||||
const result = parser.parse('simpleField', { like: '%value%' }, false);
|
||||
|
||||
expect(result).toEqual({
|
||||
simpleField: new FindOperator('like', '%%value%%'),
|
||||
});
|
||||
});
|
||||
});
|
||||
+130
@@ -0,0 +1,130 @@
|
||||
import {
|
||||
FindOperator,
|
||||
ILike,
|
||||
In,
|
||||
IsNull,
|
||||
LessThan,
|
||||
LessThanOrEqual,
|
||||
Like,
|
||||
MoreThan,
|
||||
MoreThanOrEqual,
|
||||
Not,
|
||||
} from 'typeorm';
|
||||
|
||||
import { GraphqlQueryRunnerException } from 'src/engine/api/graphql/graphql-query-runner/errors/graphql-query-runner.exception';
|
||||
import { GraphqlQueryFilterOperatorParser } from 'src/engine/api/graphql/graphql-query-runner/graphql-query-parsers/graphql-query-filter/graphql-query-filter-operator.parser';
|
||||
|
||||
describe('GraphqlQueryFilterOperatorParser', () => {
|
||||
let parser: GraphqlQueryFilterOperatorParser;
|
||||
|
||||
beforeEach(() => {
|
||||
parser = new GraphqlQueryFilterOperatorParser();
|
||||
});
|
||||
|
||||
describe('parseOperator', () => {
|
||||
it('should parse eq operator correctly', () => {
|
||||
const result = parser.parseOperator({ eq: 'value' }, false);
|
||||
|
||||
expect(result).toBe('value');
|
||||
});
|
||||
|
||||
it('should parse neq operator correctly', () => {
|
||||
const result = parser.parseOperator({ neq: 'value' }, false);
|
||||
|
||||
expect(result).toBeInstanceOf(FindOperator);
|
||||
expect(result).toEqual(Not('value'));
|
||||
});
|
||||
|
||||
it('should parse gt operator correctly', () => {
|
||||
const result = parser.parseOperator({ gt: 5 }, false);
|
||||
|
||||
expect(result).toBeInstanceOf(FindOperator);
|
||||
expect(result).toEqual(MoreThan(5));
|
||||
});
|
||||
|
||||
it('should parse gte operator correctly', () => {
|
||||
const result = parser.parseOperator({ gte: 5 }, false);
|
||||
|
||||
expect(result).toBeInstanceOf(FindOperator);
|
||||
expect(result).toEqual(MoreThanOrEqual(5));
|
||||
});
|
||||
|
||||
it('should parse lt operator correctly', () => {
|
||||
const result = parser.parseOperator({ lt: 5 }, false);
|
||||
|
||||
expect(result).toBeInstanceOf(FindOperator);
|
||||
expect(result).toEqual(LessThan(5));
|
||||
});
|
||||
|
||||
it('should parse lte operator correctly', () => {
|
||||
const result = parser.parseOperator({ lte: 5 }, false);
|
||||
|
||||
expect(result).toBeInstanceOf(FindOperator);
|
||||
expect(result).toEqual(LessThanOrEqual(5));
|
||||
});
|
||||
|
||||
it('should parse in operator correctly', () => {
|
||||
const result = parser.parseOperator({ in: [1, 2, 3] }, false);
|
||||
|
||||
expect(result).toBeInstanceOf(FindOperator);
|
||||
expect(result).toEqual(In([1, 2, 3]));
|
||||
});
|
||||
|
||||
it('should parse is operator with NULL correctly', () => {
|
||||
const result = parser.parseOperator({ is: 'NULL' }, false);
|
||||
|
||||
expect(result).toBeInstanceOf(FindOperator);
|
||||
expect(result).toEqual(IsNull());
|
||||
});
|
||||
|
||||
it('should parse is operator with non-NULL value correctly', () => {
|
||||
const result = parser.parseOperator({ is: 'NOT_NULL' }, false);
|
||||
|
||||
expect(result).toBe('NOT_NULL');
|
||||
});
|
||||
|
||||
it('should parse like operator correctly', () => {
|
||||
const result = parser.parseOperator({ like: 'test' }, false);
|
||||
|
||||
expect(result).toBeInstanceOf(FindOperator);
|
||||
expect(result).toEqual(Like('%test%'));
|
||||
});
|
||||
|
||||
it('should parse ilike operator correctly', () => {
|
||||
const result = parser.parseOperator({ ilike: 'test' }, false);
|
||||
|
||||
expect(result).toBeInstanceOf(FindOperator);
|
||||
expect(result).toEqual(ILike('%test%'));
|
||||
});
|
||||
|
||||
it('should parse startsWith operator correctly', () => {
|
||||
const result = parser.parseOperator({ startsWith: 'test' }, false);
|
||||
|
||||
expect(result).toBeInstanceOf(FindOperator);
|
||||
expect(result).toEqual(ILike('test%'));
|
||||
});
|
||||
|
||||
it('should parse endsWith operator correctly', () => {
|
||||
const result = parser.parseOperator({ endsWith: 'test' }, false);
|
||||
|
||||
expect(result).toBeInstanceOf(FindOperator);
|
||||
expect(result).toEqual(ILike('%test'));
|
||||
});
|
||||
|
||||
it('should negate the operator when isNegated is true', () => {
|
||||
const result = parser.parseOperator({ eq: 'value' }, true);
|
||||
|
||||
expect(result).toBeInstanceOf(FindOperator);
|
||||
expect(result).toEqual(Not('value'));
|
||||
});
|
||||
|
||||
it('should throw an exception for unsupported operator', () => {
|
||||
expect(() =>
|
||||
parser.parseOperator({ unsupported: 'value' }, false),
|
||||
).toThrow(GraphqlQueryRunnerException);
|
||||
expect(() =>
|
||||
parser.parseOperator({ unsupported: 'value' }, false),
|
||||
).toThrow('Operator "unsupported" is not supported');
|
||||
});
|
||||
});
|
||||
});
|
||||
+107
@@ -0,0 +1,107 @@
|
||||
import { FindOptionsWhere, ObjectLiteral } from 'typeorm';
|
||||
|
||||
import { RecordFilter } from 'src/engine/api/graphql/workspace-query-builder/interfaces/record.interface';
|
||||
|
||||
import { FieldMetadataMap } from 'src/engine/api/graphql/graphql-query-runner/utils/convert-object-metadata-to-map.util';
|
||||
|
||||
import { GraphqlQueryFilterFieldParser } from './graphql-query-filter-field.parser';
|
||||
|
||||
export class GraphqlQueryFilterConditionParser {
|
||||
private fieldMetadataMap: FieldMetadataMap;
|
||||
private fieldConditionParser: GraphqlQueryFilterFieldParser;
|
||||
|
||||
constructor(fieldMetadataMap: FieldMetadataMap) {
|
||||
this.fieldMetadataMap = fieldMetadataMap;
|
||||
this.fieldConditionParser = new GraphqlQueryFilterFieldParser(
|
||||
this.fieldMetadataMap,
|
||||
);
|
||||
}
|
||||
|
||||
public parse(
|
||||
conditions: RecordFilter,
|
||||
isNegated = false,
|
||||
): FindOptionsWhere<ObjectLiteral> | FindOptionsWhere<ObjectLiteral>[] {
|
||||
if (Array.isArray(conditions)) {
|
||||
return this.parseAndCondition(conditions, isNegated);
|
||||
}
|
||||
|
||||
const result: FindOptionsWhere<ObjectLiteral> = {};
|
||||
|
||||
for (const [key, value] of Object.entries(conditions)) {
|
||||
switch (key) {
|
||||
case 'and':
|
||||
return this.parseAndCondition(value, isNegated);
|
||||
case 'or':
|
||||
return this.parseOrCondition(value, isNegated);
|
||||
case 'not':
|
||||
return this.parse(value, !isNegated);
|
||||
default:
|
||||
Object.assign(
|
||||
result,
|
||||
this.fieldConditionParser.parse(key, value, isNegated),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
private parseAndCondition(
|
||||
conditions: RecordFilter[],
|
||||
isNegated: boolean,
|
||||
): FindOptionsWhere<ObjectLiteral>[] {
|
||||
const parsedConditions = conditions.map((condition) =>
|
||||
this.parse(condition, isNegated),
|
||||
);
|
||||
|
||||
return this.combineConditions(parsedConditions, isNegated ? 'or' : 'and');
|
||||
}
|
||||
|
||||
private parseOrCondition(
|
||||
conditions: RecordFilter[],
|
||||
isNegated: boolean,
|
||||
): FindOptionsWhere<ObjectLiteral>[] {
|
||||
const parsedConditions = conditions.map((condition) =>
|
||||
this.parse(condition, isNegated),
|
||||
);
|
||||
|
||||
return this.combineConditions(parsedConditions, isNegated ? 'and' : 'or');
|
||||
}
|
||||
|
||||
private combineConditions(
|
||||
conditions: (
|
||||
| FindOptionsWhere<ObjectLiteral>
|
||||
| FindOptionsWhere<ObjectLiteral>[]
|
||||
)[],
|
||||
combineType: 'and' | 'or',
|
||||
): FindOptionsWhere<ObjectLiteral>[] {
|
||||
if (combineType === 'and') {
|
||||
let result: FindOptionsWhere<ObjectLiteral>[] = [{}];
|
||||
|
||||
for (const condition of conditions) {
|
||||
if (Array.isArray(condition)) {
|
||||
const newResult: FindOptionsWhere<ObjectLiteral>[] = [];
|
||||
|
||||
for (const existingCondition of result) {
|
||||
for (const orCondition of condition) {
|
||||
newResult.push({
|
||||
...existingCondition,
|
||||
...orCondition,
|
||||
});
|
||||
}
|
||||
}
|
||||
result = newResult;
|
||||
} else {
|
||||
result = result.map((existingCondition) => ({
|
||||
...existingCondition,
|
||||
...condition,
|
||||
}));
|
||||
}
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
return conditions.flat();
|
||||
}
|
||||
}
|
||||
+102
@@ -0,0 +1,102 @@
|
||||
import { FindOptionsWhere, Not, ObjectLiteral } from 'typeorm';
|
||||
|
||||
import { RecordFilter } from 'src/engine/api/graphql/workspace-query-builder/interfaces/record.interface';
|
||||
import { FieldMetadataInterface } from 'src/engine/metadata-modules/field-metadata/interfaces/field-metadata.interface';
|
||||
|
||||
import { FieldMetadataMap } from 'src/engine/api/graphql/graphql-query-runner/utils/convert-object-metadata-to-map.util';
|
||||
import { compositeTypeDefinitions } from 'src/engine/metadata-modules/field-metadata/composite-types';
|
||||
import { isCompositeFieldMetadataType } from 'src/engine/metadata-modules/field-metadata/utils/is-composite-field-metadata-type.util';
|
||||
import { CompositeFieldMetadataType } from 'src/engine/metadata-modules/workspace-migration/factories/composite-column-action.factory';
|
||||
import { capitalize } from 'src/utils/capitalize';
|
||||
import { isPlainObject } from 'src/utils/is-plain-object';
|
||||
|
||||
import { GraphqlQueryFilterConditionParser } from './graphql-query-filter-condition.parser';
|
||||
import { GraphqlQueryFilterOperatorParser } from './graphql-query-filter-operator.parser';
|
||||
|
||||
export class GraphqlQueryFilterFieldParser {
|
||||
private fieldMetadataMap: FieldMetadataMap;
|
||||
private operatorParser: GraphqlQueryFilterOperatorParser;
|
||||
|
||||
constructor(fieldMetadataMap: FieldMetadataMap) {
|
||||
this.fieldMetadataMap = fieldMetadataMap;
|
||||
this.operatorParser = new GraphqlQueryFilterOperatorParser();
|
||||
}
|
||||
|
||||
public parse(
|
||||
key: string,
|
||||
value: any,
|
||||
isNegated: boolean,
|
||||
): FindOptionsWhere<ObjectLiteral> {
|
||||
const fieldMetadata = this.fieldMetadataMap[key];
|
||||
|
||||
if (!fieldMetadata) {
|
||||
return {
|
||||
[key]: (value: RecordFilter, isNegated: boolean) => {
|
||||
const conditionParser = new GraphqlQueryFilterConditionParser(
|
||||
this.fieldMetadataMap,
|
||||
);
|
||||
|
||||
return conditionParser.parse(value, isNegated);
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
if (isCompositeFieldMetadataType(fieldMetadata.type)) {
|
||||
return this.parseCompositeFieldForFilter(fieldMetadata, value, isNegated);
|
||||
}
|
||||
|
||||
if (isPlainObject(value)) {
|
||||
const parsedValue = this.operatorParser.parseOperator(value, isNegated);
|
||||
|
||||
return { [key]: parsedValue };
|
||||
}
|
||||
|
||||
return { [key]: isNegated ? Not(value) : value };
|
||||
}
|
||||
|
||||
private parseCompositeFieldForFilter(
|
||||
fieldMetadata: FieldMetadataInterface,
|
||||
fieldValue: any,
|
||||
isNegated: boolean,
|
||||
): FindOptionsWhere<ObjectLiteral> {
|
||||
const compositeType = compositeTypeDefinitions.get(
|
||||
fieldMetadata.type as CompositeFieldMetadataType,
|
||||
);
|
||||
|
||||
if (!compositeType) {
|
||||
throw new Error(
|
||||
`Composite type definition not found for type: ${fieldMetadata.type}`,
|
||||
);
|
||||
}
|
||||
|
||||
return Object.entries(fieldValue).reduce(
|
||||
(result, [subFieldKey, subFieldValue]) => {
|
||||
const subFieldMetadata = compositeType.properties.find(
|
||||
(property) => property.name === subFieldKey,
|
||||
);
|
||||
|
||||
if (!subFieldMetadata) {
|
||||
throw new Error(
|
||||
`Sub field metadata not found for composite type: ${fieldMetadata.type}`,
|
||||
);
|
||||
}
|
||||
|
||||
const fullFieldName = `${fieldMetadata.name}${capitalize(subFieldKey)}`;
|
||||
|
||||
if (isPlainObject(subFieldValue)) {
|
||||
result[fullFieldName] = this.operatorParser.parseOperator(
|
||||
subFieldValue,
|
||||
isNegated,
|
||||
);
|
||||
} else {
|
||||
result[fullFieldName] = isNegated
|
||||
? Not(subFieldValue)
|
||||
: subFieldValue;
|
||||
}
|
||||
|
||||
return result;
|
||||
},
|
||||
{} as FindOptionsWhere<ObjectLiteral>,
|
||||
);
|
||||
}
|
||||
}
|
||||
+56
@@ -0,0 +1,56 @@
|
||||
import {
|
||||
FindOperator,
|
||||
ILike,
|
||||
In,
|
||||
IsNull,
|
||||
LessThan,
|
||||
LessThanOrEqual,
|
||||
Like,
|
||||
MoreThan,
|
||||
MoreThanOrEqual,
|
||||
Not,
|
||||
} from 'typeorm';
|
||||
|
||||
import {
|
||||
GraphqlQueryRunnerException,
|
||||
GraphqlQueryRunnerExceptionCode,
|
||||
} from 'src/engine/api/graphql/graphql-query-runner/errors/graphql-query-runner.exception';
|
||||
|
||||
export class GraphqlQueryFilterOperatorParser {
|
||||
private operatorMap: { [key: string]: (value: any) => FindOperator<any> };
|
||||
|
||||
constructor() {
|
||||
this.operatorMap = {
|
||||
eq: (value: any) => value,
|
||||
neq: (value: any) => Not(value),
|
||||
gt: (value: any) => MoreThan(value),
|
||||
gte: (value: any) => MoreThanOrEqual(value),
|
||||
lt: (value: any) => LessThan(value),
|
||||
lte: (value: any) => LessThanOrEqual(value),
|
||||
in: (value: any) => In(value),
|
||||
is: (value: any) => (value === 'NULL' ? IsNull() : value),
|
||||
like: (value: string) => Like(`%${value}%`),
|
||||
ilike: (value: string) => ILike(`%${value}%`),
|
||||
startsWith: (value: string) => ILike(`${value}%`),
|
||||
endsWith: (value: string) => ILike(`%${value}`),
|
||||
};
|
||||
}
|
||||
|
||||
public parseOperator(
|
||||
operatorObj: Record<string, any>,
|
||||
isNegated: boolean,
|
||||
): FindOperator<any> {
|
||||
const [[operator, value]] = Object.entries(operatorObj);
|
||||
|
||||
if (operator in this.operatorMap) {
|
||||
const operatorFunction = this.operatorMap[operator];
|
||||
|
||||
return isNegated ? Not(operatorFunction(value)) : operatorFunction(value);
|
||||
}
|
||||
|
||||
throw new GraphqlQueryRunnerException(
|
||||
`Operator "${operator}" is not supported`,
|
||||
GraphqlQueryRunnerExceptionCode.UNSUPPORTED_OPERATOR,
|
||||
);
|
||||
}
|
||||
}
|
||||
+50
@@ -0,0 +1,50 @@
|
||||
import { OrderByDirection } from 'src/engine/api/graphql/workspace-query-builder/interfaces/record.interface';
|
||||
|
||||
import { GraphqlQueryOrderFieldParser } from 'src/engine/api/graphql/graphql-query-runner/graphql-query-parsers/graphql-query-order/graphql-query-order.parser';
|
||||
import { FieldMetadataMap } from 'src/engine/api/graphql/graphql-query-runner/utils/convert-object-metadata-to-map.util';
|
||||
import { FieldMetadataType } from 'src/engine/metadata-modules/field-metadata/field-metadata.entity';
|
||||
|
||||
describe('GraphqlQueryOrderFieldParser', () => {
|
||||
let parser: GraphqlQueryOrderFieldParser;
|
||||
const fieldMetadataMap: FieldMetadataMap = {};
|
||||
|
||||
beforeEach(() => {
|
||||
fieldMetadataMap['name'] = {
|
||||
id: 'name-id',
|
||||
name: 'name',
|
||||
type: FieldMetadataType.TEXT,
|
||||
label: 'Name',
|
||||
objectMetadataId: 'object-id',
|
||||
};
|
||||
fieldMetadataMap['age'] = {
|
||||
id: 'age-id',
|
||||
name: 'age',
|
||||
type: FieldMetadataType.NUMBER,
|
||||
label: 'Age',
|
||||
objectMetadataId: 'object-id',
|
||||
};
|
||||
fieldMetadataMap['address'] = {
|
||||
id: 'address-id',
|
||||
name: 'address',
|
||||
type: FieldMetadataType.ADDRESS,
|
||||
label: 'Address',
|
||||
objectMetadataId: 'object-id',
|
||||
};
|
||||
|
||||
parser = new GraphqlQueryOrderFieldParser(fieldMetadataMap);
|
||||
});
|
||||
describe('parse', () => {
|
||||
it('should parse simple order by fields', () => {
|
||||
const orderBy = [
|
||||
{ name: OrderByDirection.AscNullsFirst },
|
||||
{ age: OrderByDirection.DescNullsLast },
|
||||
];
|
||||
const result = parser.parse(orderBy);
|
||||
|
||||
expect(result).toEqual({
|
||||
name: { direction: 'ASC', nulls: 'FIRST' },
|
||||
age: { direction: 'DESC', nulls: 'LAST' },
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
+122
@@ -0,0 +1,122 @@
|
||||
import { FindOptionsOrderValue } from 'typeorm';
|
||||
|
||||
import {
|
||||
OrderByDirection,
|
||||
RecordOrderBy,
|
||||
} from 'src/engine/api/graphql/workspace-query-builder/interfaces/record.interface';
|
||||
import { FieldMetadataInterface } from 'src/engine/metadata-modules/field-metadata/interfaces/field-metadata.interface';
|
||||
|
||||
import {
|
||||
GraphqlQueryRunnerException,
|
||||
GraphqlQueryRunnerExceptionCode,
|
||||
} from 'src/engine/api/graphql/graphql-query-runner/errors/graphql-query-runner.exception';
|
||||
import { FieldMetadataMap } from 'src/engine/api/graphql/graphql-query-runner/utils/convert-object-metadata-to-map.util';
|
||||
import { compositeTypeDefinitions } from 'src/engine/metadata-modules/field-metadata/composite-types';
|
||||
import { isCompositeFieldMetadataType } from 'src/engine/metadata-modules/field-metadata/utils/is-composite-field-metadata-type.util';
|
||||
import { CompositeFieldMetadataType } from 'src/engine/metadata-modules/workspace-migration/factories/composite-column-action.factory';
|
||||
import { capitalize } from 'src/utils/capitalize';
|
||||
|
||||
export class GraphqlQueryOrderFieldParser {
|
||||
private fieldMetadataMap: FieldMetadataMap;
|
||||
|
||||
constructor(fieldMetadataMap: FieldMetadataMap) {
|
||||
this.fieldMetadataMap = fieldMetadataMap;
|
||||
}
|
||||
|
||||
parse(orderBy: RecordOrderBy): Record<string, FindOptionsOrderValue> {
|
||||
return orderBy.reduce(
|
||||
(acc, item) => {
|
||||
Object.entries(item).forEach(([key, value]) => {
|
||||
const fieldMetadata = this.fieldMetadataMap[key];
|
||||
|
||||
if (!fieldMetadata || value === undefined) {
|
||||
throw new GraphqlQueryRunnerException(
|
||||
`Field "${key}" does not exist or is not sortable`,
|
||||
GraphqlQueryRunnerExceptionCode.FIELD_NOT_FOUND,
|
||||
);
|
||||
}
|
||||
|
||||
if (isCompositeFieldMetadataType(fieldMetadata.type)) {
|
||||
const compositeOrder = this.parseCompositeFieldForOrder(
|
||||
fieldMetadata,
|
||||
value,
|
||||
);
|
||||
|
||||
Object.assign(acc, compositeOrder);
|
||||
} else {
|
||||
acc[key] = this.convertOrderByToFindOptionsOrder(value);
|
||||
}
|
||||
});
|
||||
|
||||
return acc;
|
||||
},
|
||||
{} as Record<string, FindOptionsOrderValue>,
|
||||
);
|
||||
}
|
||||
|
||||
private parseCompositeFieldForOrder(
|
||||
fieldMetadata: FieldMetadataInterface,
|
||||
value: any,
|
||||
): Record<string, FindOptionsOrderValue> {
|
||||
const compositeType = compositeTypeDefinitions.get(
|
||||
fieldMetadata.type as CompositeFieldMetadataType,
|
||||
);
|
||||
|
||||
if (!compositeType) {
|
||||
throw new Error(
|
||||
`Composite type definition not found for type: ${fieldMetadata.type}`,
|
||||
);
|
||||
}
|
||||
|
||||
return Object.entries(value).reduce(
|
||||
(acc, [subFieldKey, subFieldValue]) => {
|
||||
const subFieldMetadata = compositeType.properties.find(
|
||||
(property) => property.name === subFieldKey,
|
||||
);
|
||||
|
||||
if (!subFieldMetadata) {
|
||||
throw new Error(
|
||||
`Sub field metadata not found for composite type: ${fieldMetadata.type}`,
|
||||
);
|
||||
}
|
||||
|
||||
const fullFieldName = `${fieldMetadata.name}${capitalize(subFieldKey)}`;
|
||||
|
||||
if (!this.isOrderByDirection(subFieldValue)) {
|
||||
throw new Error(
|
||||
`Sub field order by value must be of type OrderByDirection, but got: ${subFieldValue}`,
|
||||
);
|
||||
}
|
||||
acc[fullFieldName] =
|
||||
this.convertOrderByToFindOptionsOrder(subFieldValue);
|
||||
|
||||
return acc;
|
||||
},
|
||||
{} as Record<string, FindOptionsOrderValue>,
|
||||
);
|
||||
}
|
||||
|
||||
private convertOrderByToFindOptionsOrder(
|
||||
direction: OrderByDirection,
|
||||
): FindOptionsOrderValue {
|
||||
switch (direction) {
|
||||
case OrderByDirection.AscNullsFirst:
|
||||
return { direction: 'ASC', nulls: 'FIRST' };
|
||||
case OrderByDirection.AscNullsLast:
|
||||
return { direction: 'ASC', nulls: 'LAST' };
|
||||
case OrderByDirection.DescNullsFirst:
|
||||
return { direction: 'DESC', nulls: 'FIRST' };
|
||||
case OrderByDirection.DescNullsLast:
|
||||
return { direction: 'DESC', nulls: 'LAST' };
|
||||
default:
|
||||
throw new GraphqlQueryRunnerException(
|
||||
`Invalid direction: ${direction}`,
|
||||
GraphqlQueryRunnerExceptionCode.INVALID_DIRECTION,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
private isOrderByDirection(value: unknown): value is OrderByDirection {
|
||||
return Object.values(OrderByDirection).includes(value as OrderByDirection);
|
||||
}
|
||||
}
|
||||
+43
@@ -0,0 +1,43 @@
|
||||
import { FieldMetadataInterface } from 'src/engine/metadata-modules/field-metadata/interfaces/field-metadata.interface';
|
||||
|
||||
import { GraphqlQuerySelectedFieldsParser } from 'src/engine/api/graphql/graphql-query-runner/graphql-query-parsers/graphql-query-selected-fields/graphql-selected-fields.parser';
|
||||
import { ObjectMetadataMap } from 'src/engine/api/graphql/graphql-query-runner/utils/convert-object-metadata-to-map.util';
|
||||
import { getRelationObjectMetadata } from 'src/engine/api/graphql/graphql-query-runner/utils/get-relation-object-metadata.util';
|
||||
|
||||
export class GraphqlQuerySelectedFieldsRelationParser {
|
||||
private objectMetadataMap: ObjectMetadataMap;
|
||||
|
||||
constructor(objectMetadataMap: ObjectMetadataMap) {
|
||||
this.objectMetadataMap = objectMetadataMap;
|
||||
}
|
||||
|
||||
parseRelationField(
|
||||
fieldMetadata: FieldMetadataInterface,
|
||||
fieldKey: string,
|
||||
fieldValue: any,
|
||||
result: { select: Record<string, any>; relations: Record<string, any> },
|
||||
): void {
|
||||
result.relations[fieldKey] = true;
|
||||
|
||||
if (!fieldValue || typeof fieldValue !== 'object') {
|
||||
return;
|
||||
}
|
||||
|
||||
const referencedObjectMetadata = getRelationObjectMetadata(
|
||||
fieldMetadata,
|
||||
this.objectMetadataMap,
|
||||
);
|
||||
|
||||
const relationFields = referencedObjectMetadata.fields;
|
||||
const fieldParser = new GraphqlQuerySelectedFieldsParser(
|
||||
this.objectMetadataMap,
|
||||
);
|
||||
const subResult = fieldParser.parse(fieldValue, relationFields);
|
||||
|
||||
result.select[fieldKey] = {
|
||||
id: true,
|
||||
...subResult.select,
|
||||
};
|
||||
result.relations[fieldKey] = subResult.relations;
|
||||
}
|
||||
}
|
||||
+128
@@ -0,0 +1,128 @@
|
||||
import { FieldMetadataInterface } from 'src/engine/metadata-modules/field-metadata/interfaces/field-metadata.interface';
|
||||
|
||||
import {
|
||||
GraphqlQueryRunnerException,
|
||||
GraphqlQueryRunnerExceptionCode,
|
||||
} from 'src/engine/api/graphql/graphql-query-runner/errors/graphql-query-runner.exception';
|
||||
import { GraphqlQuerySelectedFieldsRelationParser } from 'src/engine/api/graphql/graphql-query-runner/graphql-query-parsers/graphql-query-selected-fields/graphql-selected-fields-relation.parser';
|
||||
import { ObjectMetadataMap } from 'src/engine/api/graphql/graphql-query-runner/utils/convert-object-metadata-to-map.util';
|
||||
import { compositeTypeDefinitions } from 'src/engine/metadata-modules/field-metadata/composite-types';
|
||||
import { isCompositeFieldMetadataType } from 'src/engine/metadata-modules/field-metadata/utils/is-composite-field-metadata-type.util';
|
||||
import { CompositeFieldMetadataType } from 'src/engine/metadata-modules/workspace-migration/factories/composite-column-action.factory';
|
||||
import { isRelationFieldMetadataType } from 'src/engine/utils/is-relation-field-metadata-type.util';
|
||||
import { capitalize } from 'src/utils/capitalize';
|
||||
import { isPlainObject } from 'src/utils/is-plain-object';
|
||||
|
||||
export class GraphqlQuerySelectedFieldsParser {
|
||||
private graphqlQuerySelectedFieldsRelationParser: GraphqlQuerySelectedFieldsRelationParser;
|
||||
|
||||
constructor(objectMetadataMap: ObjectMetadataMap) {
|
||||
this.graphqlQuerySelectedFieldsRelationParser =
|
||||
new GraphqlQuerySelectedFieldsRelationParser(objectMetadataMap);
|
||||
}
|
||||
|
||||
parse(
|
||||
graphqlSelectedFields: Partial<Record<string, any>>,
|
||||
fieldMetadataMap: Record<string, FieldMetadataInterface>,
|
||||
): { select: Record<string, any>; relations: Record<string, any> } {
|
||||
const result: {
|
||||
select: Record<string, any>;
|
||||
relations: Record<string, any>;
|
||||
} = {
|
||||
select: {},
|
||||
relations: {},
|
||||
};
|
||||
|
||||
for (const [fieldKey, fieldValue] of Object.entries(
|
||||
graphqlSelectedFields,
|
||||
)) {
|
||||
if (this.shouldNotParseField(fieldKey)) {
|
||||
continue;
|
||||
}
|
||||
if (this.isConnectionField(fieldKey, fieldValue)) {
|
||||
const subResult = this.parse(fieldValue, fieldMetadataMap);
|
||||
|
||||
Object.assign(result.select, subResult.select);
|
||||
Object.assign(result.relations, subResult.relations);
|
||||
continue;
|
||||
}
|
||||
|
||||
const fieldMetadata = fieldMetadataMap[fieldKey];
|
||||
|
||||
if (!fieldMetadata) {
|
||||
throw new GraphqlQueryRunnerException(
|
||||
`Field "${fieldKey}" does not exist or is not selectable`,
|
||||
GraphqlQueryRunnerExceptionCode.FIELD_NOT_FOUND,
|
||||
);
|
||||
}
|
||||
|
||||
if (isRelationFieldMetadataType(fieldMetadata.type)) {
|
||||
this.graphqlQuerySelectedFieldsRelationParser.parseRelationField(
|
||||
fieldMetadata,
|
||||
fieldKey,
|
||||
fieldValue,
|
||||
result,
|
||||
);
|
||||
} else if (isCompositeFieldMetadataType(fieldMetadata.type)) {
|
||||
const compositeResult = this.parseCompositeField(
|
||||
fieldMetadata,
|
||||
fieldValue,
|
||||
);
|
||||
|
||||
Object.assign(result.select, compositeResult);
|
||||
} else {
|
||||
result.select[fieldKey] = true;
|
||||
}
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
private isConnectionField(fieldKey: string, fieldValue: any): boolean {
|
||||
return ['edges', 'node'].includes(fieldKey) && isPlainObject(fieldValue);
|
||||
}
|
||||
|
||||
private shouldNotParseField(fieldKey: string): boolean {
|
||||
return ['__typename', 'totalCount', 'pageInfo', 'cursor'].includes(
|
||||
fieldKey,
|
||||
);
|
||||
}
|
||||
|
||||
private parseCompositeField(
|
||||
fieldMetadata: FieldMetadataInterface,
|
||||
fieldValue: any,
|
||||
): Record<string, any> {
|
||||
const compositeType = compositeTypeDefinitions.get(
|
||||
fieldMetadata.type as CompositeFieldMetadataType,
|
||||
);
|
||||
|
||||
if (!compositeType) {
|
||||
throw new Error(
|
||||
`Composite type definition not found for type: ${fieldMetadata.type}`,
|
||||
);
|
||||
}
|
||||
|
||||
return Object.keys(fieldValue)
|
||||
.filter((subFieldKey) => subFieldKey !== '__typename')
|
||||
.reduce(
|
||||
(acc, subFieldKey) => {
|
||||
const subFieldMetadata = compositeType.properties.find(
|
||||
(property) => property.name === subFieldKey,
|
||||
);
|
||||
|
||||
if (!subFieldMetadata) {
|
||||
throw new Error(
|
||||
`Sub field metadata not found for composite type: ${fieldMetadata.type}`,
|
||||
);
|
||||
}
|
||||
|
||||
const fullFieldName = `${fieldMetadata.name}${capitalize(subFieldKey)}`;
|
||||
|
||||
acc[fullFieldName] = true;
|
||||
|
||||
return acc;
|
||||
},
|
||||
{} as Record<string, any>,
|
||||
);
|
||||
}
|
||||
}
|
||||
+70
@@ -0,0 +1,70 @@
|
||||
import {
|
||||
FindOptionsOrderValue,
|
||||
FindOptionsWhere,
|
||||
ObjectLiteral,
|
||||
} from 'typeorm';
|
||||
|
||||
import {
|
||||
RecordFilter,
|
||||
RecordOrderBy,
|
||||
} from 'src/engine/api/graphql/workspace-query-builder/interfaces/record.interface';
|
||||
import { ObjectMetadataInterface } from 'src/engine/metadata-modules/field-metadata/interfaces/object-metadata.interface';
|
||||
|
||||
import { GraphqlQueryFilterConditionParser as GraphqlQueryFilterParser } from 'src/engine/api/graphql/graphql-query-runner/graphql-query-parsers/graphql-query-filter/graphql-query-filter-condition.parser';
|
||||
import { GraphqlQueryOrderFieldParser as GraphqlQueryOrderParser } from 'src/engine/api/graphql/graphql-query-runner/graphql-query-parsers/graphql-query-order/graphql-query-order.parser';
|
||||
import { GraphqlQuerySelectedFieldsParser } from 'src/engine/api/graphql/graphql-query-runner/graphql-query-parsers/graphql-query-selected-fields/graphql-selected-fields.parser';
|
||||
import {
|
||||
FieldMetadataMap,
|
||||
ObjectMetadataMap,
|
||||
} from 'src/engine/api/graphql/graphql-query-runner/utils/convert-object-metadata-to-map.util';
|
||||
|
||||
export class GraphqlQueryParser {
|
||||
private fieldMetadataMap: FieldMetadataMap;
|
||||
private objectMetadataMap: ObjectMetadataMap;
|
||||
|
||||
constructor(
|
||||
fieldMetadataMap: FieldMetadataMap,
|
||||
objectMetadataMap: ObjectMetadataMap,
|
||||
) {
|
||||
this.objectMetadataMap = objectMetadataMap;
|
||||
this.fieldMetadataMap = fieldMetadataMap;
|
||||
}
|
||||
|
||||
parseFilter(
|
||||
recordFilter: RecordFilter,
|
||||
): FindOptionsWhere<ObjectLiteral> | FindOptionsWhere<ObjectLiteral>[] {
|
||||
const graphqlQueryFilterParser = new GraphqlQueryFilterParser(
|
||||
this.fieldMetadataMap,
|
||||
);
|
||||
|
||||
return graphqlQueryFilterParser.parse(recordFilter);
|
||||
}
|
||||
|
||||
parseOrder(orderBy: RecordOrderBy): Record<string, FindOptionsOrderValue> {
|
||||
const graphqlQueryOrderParser = new GraphqlQueryOrderParser(
|
||||
this.fieldMetadataMap,
|
||||
);
|
||||
|
||||
return graphqlQueryOrderParser.parse(orderBy);
|
||||
}
|
||||
|
||||
parseSelectedFields(
|
||||
parentObjectMetadata: ObjectMetadataInterface,
|
||||
graphqlSelectedFields: Partial<Record<string, any>>,
|
||||
): { select: Record<string, any>; relations: Record<string, any> } {
|
||||
const parentFields =
|
||||
this.objectMetadataMap[parentObjectMetadata.nameSingular]?.fields;
|
||||
|
||||
if (!parentFields) {
|
||||
throw new Error(
|
||||
`Could not find object metadata for ${parentObjectMetadata.nameSingular}`,
|
||||
);
|
||||
}
|
||||
|
||||
const selectedFieldsParser = new GraphqlQuerySelectedFieldsParser(
|
||||
this.objectMetadataMap,
|
||||
);
|
||||
|
||||
return selectedFieldsParser.parse(graphqlSelectedFields, parentFields);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user