refactor graphql query runner connection mapper (#6771)

This commit is contained in:
Weiko
2024-08-28 19:02:45 +02:00
committed by GitHub
parent 7c894fe870
commit c87ccfa3c7
20 changed files with 326 additions and 216 deletions
@@ -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%%'),
});
});
});
@@ -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');
});
});
});
@@ -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();
}
}
@@ -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>,
);
}
}
@@ -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,
);
}
}
@@ -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' },
});
});
});
});
@@ -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);
}
}
@@ -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;
}
}
@@ -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>,
);
}
}
@@ -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);
}
}