refactor graphql query runner connection mapper (#6771)
This commit is contained in:
@ -14,4 +14,5 @@ export enum GraphqlQueryRunnerExceptionCode {
|
|||||||
UNSUPPORTED_OPERATOR = 'UNSUPPORTED_OPERATOR',
|
UNSUPPORTED_OPERATOR = 'UNSUPPORTED_OPERATOR',
|
||||||
ARGS_CONFLICT = 'ARGS_CONFLICT',
|
ARGS_CONFLICT = 'ARGS_CONFLICT',
|
||||||
FIELD_NOT_FOUND = 'FIELD_NOT_FOUND',
|
FIELD_NOT_FOUND = 'FIELD_NOT_FOUND',
|
||||||
|
OBJECT_METADATA_NOT_FOUND = 'OBJECT_METADATA_NOT_FOUND',
|
||||||
}
|
}
|
||||||
|
|||||||
@ -2,7 +2,7 @@ import { FindOperator, Not } from 'typeorm';
|
|||||||
|
|
||||||
import { FieldMetadataInterface } from 'src/engine/metadata-modules/field-metadata/interfaces/field-metadata.interface';
|
import { FieldMetadataInterface } from 'src/engine/metadata-modules/field-metadata/interfaces/field-metadata.interface';
|
||||||
|
|
||||||
import { GraphqlQueryFilterFieldParser } from 'src/engine/api/graphql/graphql-query-runner/parsers/graphql-query-filter/graphql-query-filter-field.parser';
|
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';
|
import { FieldMetadataType } from 'src/engine/metadata-modules/field-metadata/field-metadata.entity';
|
||||||
|
|
||||||
describe('GraphqlQueryFilterFieldParser', () => {
|
describe('GraphqlQueryFilterFieldParser', () => {
|
||||||
@ -12,7 +12,7 @@ import {
|
|||||||
} from 'typeorm';
|
} from 'typeorm';
|
||||||
|
|
||||||
import { GraphqlQueryRunnerException } from 'src/engine/api/graphql/graphql-query-runner/errors/graphql-query-runner.exception';
|
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/parsers/graphql-query-filter/graphql-query-filter-operator.parser';
|
import { GraphqlQueryFilterOperatorParser } from 'src/engine/api/graphql/graphql-query-runner/graphql-query-parsers/graphql-query-filter/graphql-query-filter-operator.parser';
|
||||||
|
|
||||||
describe('GraphqlQueryFilterOperatorParser', () => {
|
describe('GraphqlQueryFilterOperatorParser', () => {
|
||||||
let parser: GraphqlQueryFilterOperatorParser;
|
let parser: GraphqlQueryFilterOperatorParser;
|
||||||
@ -1,6 +1,6 @@
|
|||||||
import { OrderByDirection } from 'src/engine/api/graphql/workspace-query-builder/interfaces/record.interface';
|
import { OrderByDirection } from 'src/engine/api/graphql/workspace-query-builder/interfaces/record.interface';
|
||||||
|
|
||||||
import { GraphqlQueryOrderFieldParser } from 'src/engine/api/graphql/graphql-query-runner/parsers/graphql-query-order/graphql-query-order.parser';
|
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 { 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';
|
import { FieldMetadataType } from 'src/engine/metadata-modules/field-metadata/field-metadata.entity';
|
||||||
|
|
||||||
@ -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;
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -4,7 +4,7 @@ import {
|
|||||||
GraphqlQueryRunnerException,
|
GraphqlQueryRunnerException,
|
||||||
GraphqlQueryRunnerExceptionCode,
|
GraphqlQueryRunnerExceptionCode,
|
||||||
} from 'src/engine/api/graphql/graphql-query-runner/errors/graphql-query-runner.exception';
|
} from 'src/engine/api/graphql/graphql-query-runner/errors/graphql-query-runner.exception';
|
||||||
import { GraphqlSelectedFieldsRelationParser } from 'src/engine/api/graphql/graphql-query-runner/parsers/graphql-selected-fields/graphql-selected-fields-relation.parser';
|
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 { 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 { 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 { isCompositeFieldMetadataType } from 'src/engine/metadata-modules/field-metadata/utils/is-composite-field-metadata-type.util';
|
||||||
@ -13,12 +13,12 @@ import { isRelationFieldMetadataType } from 'src/engine/utils/is-relation-field-
|
|||||||
import { capitalize } from 'src/utils/capitalize';
|
import { capitalize } from 'src/utils/capitalize';
|
||||||
import { isPlainObject } from 'src/utils/is-plain-object';
|
import { isPlainObject } from 'src/utils/is-plain-object';
|
||||||
|
|
||||||
export class GraphQLSelectedFieldsParser {
|
export class GraphqlQuerySelectedFieldsParser {
|
||||||
private graphqlSelectedFieldsRelationParser: GraphqlSelectedFieldsRelationParser;
|
private graphqlQuerySelectedFieldsRelationParser: GraphqlQuerySelectedFieldsRelationParser;
|
||||||
|
|
||||||
constructor(objectMetadataMap: ObjectMetadataMap) {
|
constructor(objectMetadataMap: ObjectMetadataMap) {
|
||||||
this.graphqlSelectedFieldsRelationParser =
|
this.graphqlQuerySelectedFieldsRelationParser =
|
||||||
new GraphqlSelectedFieldsRelationParser(objectMetadataMap);
|
new GraphqlQuerySelectedFieldsRelationParser(objectMetadataMap);
|
||||||
}
|
}
|
||||||
|
|
||||||
parse(
|
parse(
|
||||||
@ -57,7 +57,7 @@ export class GraphQLSelectedFieldsParser {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (isRelationFieldMetadataType(fieldMetadata.type)) {
|
if (isRelationFieldMetadataType(fieldMetadata.type)) {
|
||||||
this.graphqlSelectedFieldsRelationParser.parseRelationField(
|
this.graphqlQuerySelectedFieldsRelationParser.parseRelationField(
|
||||||
fieldMetadata,
|
fieldMetadata,
|
||||||
fieldKey,
|
fieldKey,
|
||||||
fieldValue,
|
fieldValue,
|
||||||
@ -10,9 +10,9 @@ import {
|
|||||||
} from 'src/engine/api/graphql/workspace-query-builder/interfaces/record.interface';
|
} 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 { ObjectMetadataInterface } from 'src/engine/metadata-modules/field-metadata/interfaces/object-metadata.interface';
|
||||||
|
|
||||||
import { GraphqlQueryFilterConditionParser as GraphqlQueryFilterParser } from 'src/engine/api/graphql/graphql-query-runner/parsers/graphql-query-filter/graphql-query-filter-condition.parser';
|
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/parsers/graphql-query-order/graphql-query-order.parser';
|
import { GraphqlQueryOrderFieldParser as GraphqlQueryOrderParser } from 'src/engine/api/graphql/graphql-query-runner/graphql-query-parsers/graphql-query-order/graphql-query-order.parser';
|
||||||
import { GraphQLSelectedFieldsParser } from 'src/engine/api/graphql/graphql-query-runner/parsers/graphql-selected-fields/graphql-selected-fields.parser';
|
import { GraphqlQuerySelectedFieldsParser } from 'src/engine/api/graphql/graphql-query-runner/graphql-query-parsers/graphql-query-selected-fields/graphql-selected-fields.parser';
|
||||||
import {
|
import {
|
||||||
FieldMetadataMap,
|
FieldMetadataMap,
|
||||||
ObjectMetadataMap,
|
ObjectMetadataMap,
|
||||||
@ -61,7 +61,7 @@ export class GraphqlQueryParser {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
const selectedFieldsParser = new GraphQLSelectedFieldsParser(
|
const selectedFieldsParser = new GraphqlQuerySelectedFieldsParser(
|
||||||
this.objectMetadataMap,
|
this.objectMetadataMap,
|
||||||
);
|
);
|
||||||
|
|
||||||
@ -17,13 +17,11 @@ import {
|
|||||||
GraphqlQueryRunnerException,
|
GraphqlQueryRunnerException,
|
||||||
GraphqlQueryRunnerExceptionCode,
|
GraphqlQueryRunnerExceptionCode,
|
||||||
} from 'src/engine/api/graphql/graphql-query-runner/errors/graphql-query-runner.exception';
|
} from 'src/engine/api/graphql/graphql-query-runner/errors/graphql-query-runner.exception';
|
||||||
import { GraphqlQueryParser } from 'src/engine/api/graphql/graphql-query-runner/parsers/graphql-query.parser';
|
import { GraphqlQueryParser } from 'src/engine/api/graphql/graphql-query-runner/graphql-query-parsers/graphql-query.parser';
|
||||||
|
import { ObjectRecordsToGraphqlConnectionMapper } from 'src/engine/api/graphql/graphql-query-runner/orm-mappers/object-records-to-graphql-connection.mapper';
|
||||||
import { applyRangeFilter } from 'src/engine/api/graphql/graphql-query-runner/utils/apply-range-filter.util';
|
import { applyRangeFilter } from 'src/engine/api/graphql/graphql-query-runner/utils/apply-range-filter.util';
|
||||||
import {
|
|
||||||
createConnection,
|
|
||||||
decodeCursor,
|
|
||||||
} from 'src/engine/api/graphql/graphql-query-runner/utils/connection.util';
|
|
||||||
import { convertObjectMetadataToMap } from 'src/engine/api/graphql/graphql-query-runner/utils/convert-object-metadata-to-map.util';
|
import { convertObjectMetadataToMap } from 'src/engine/api/graphql/graphql-query-runner/utils/convert-object-metadata-to-map.util';
|
||||||
|
import { decodeCursor } from 'src/engine/api/graphql/graphql-query-runner/utils/cursors.util';
|
||||||
import { LogExecutionTime } from 'src/engine/decorators/observability/log-execution-time.decorator';
|
import { LogExecutionTime } from 'src/engine/decorators/observability/log-execution-time.decorator';
|
||||||
import { TwentyORMGlobalManager } from 'src/engine/twenty-orm/twenty-orm-global.manager';
|
import { TwentyORMGlobalManager } from 'src/engine/twenty-orm/twenty-orm-global.manager';
|
||||||
|
|
||||||
@ -120,11 +118,15 @@ export class GraphqlQueryRunnerService {
|
|||||||
|
|
||||||
const objectRecords = await repository.find(findOptions);
|
const objectRecords = await repository.find(findOptions);
|
||||||
|
|
||||||
return createConnection(
|
const typeORMObjectRecordsParser =
|
||||||
|
new ObjectRecordsToGraphqlConnectionMapper(objectMetadataMap);
|
||||||
|
|
||||||
|
return typeORMObjectRecordsParser.createConnection(
|
||||||
(objectRecords as ObjectRecord[]) ?? [],
|
(objectRecords as ObjectRecord[]) ?? [],
|
||||||
take,
|
take,
|
||||||
totalCount,
|
totalCount,
|
||||||
order,
|
order,
|
||||||
|
objectMetadataItem.nameSingular,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -0,0 +1,183 @@
|
|||||||
|
import { FindOptionsOrderValue } from 'typeorm';
|
||||||
|
|
||||||
|
import { Record as IRecord } from 'src/engine/api/graphql/workspace-query-builder/interfaces/record.interface';
|
||||||
|
import { IConnection } from 'src/engine/api/graphql/workspace-query-runner/interfaces/connection.interface';
|
||||||
|
import { FieldMetadataInterface } from 'src/engine/metadata-modules/field-metadata/interfaces/field-metadata.interface';
|
||||||
|
|
||||||
|
import { CONNECTION_MAX_DEPTH } from 'src/engine/api/graphql/graphql-query-runner/constants/connection-max-depth.constant';
|
||||||
|
import {
|
||||||
|
GraphqlQueryRunnerException,
|
||||||
|
GraphqlQueryRunnerExceptionCode,
|
||||||
|
} from 'src/engine/api/graphql/graphql-query-runner/errors/graphql-query-runner.exception';
|
||||||
|
import { ObjectMetadataMap } from 'src/engine/api/graphql/graphql-query-runner/utils/convert-object-metadata-to-map.util';
|
||||||
|
import { encodeCursor } from 'src/engine/api/graphql/graphql-query-runner/utils/cursors.util';
|
||||||
|
import { getRelationObjectMetadata } from 'src/engine/api/graphql/graphql-query-runner/utils/get-relation-object-metadata.util';
|
||||||
|
import { compositeTypeDefinitions } from 'src/engine/metadata-modules/field-metadata/composite-types';
|
||||||
|
import { FieldMetadataType } from 'src/engine/metadata-modules/field-metadata/field-metadata.entity';
|
||||||
|
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 { isPlainObject } from 'src/utils/is-plain-object';
|
||||||
|
|
||||||
|
export class ObjectRecordsToGraphqlConnectionMapper {
|
||||||
|
private objectMetadataMap: ObjectMetadataMap;
|
||||||
|
|
||||||
|
constructor(objectMetadataMap: ObjectMetadataMap) {
|
||||||
|
this.objectMetadataMap = objectMetadataMap;
|
||||||
|
}
|
||||||
|
|
||||||
|
public createConnection<ObjectRecord extends IRecord = IRecord>(
|
||||||
|
objectRecords: ObjectRecord[],
|
||||||
|
take: number,
|
||||||
|
totalCount: number,
|
||||||
|
order: Record<string, FindOptionsOrderValue> | undefined,
|
||||||
|
objectName: string,
|
||||||
|
depth = 0,
|
||||||
|
): IConnection<ObjectRecord> {
|
||||||
|
const edges = (objectRecords ?? []).map((objectRecord) => ({
|
||||||
|
node: this.processRecord(
|
||||||
|
objectRecord,
|
||||||
|
take,
|
||||||
|
totalCount,
|
||||||
|
order,
|
||||||
|
objectName,
|
||||||
|
depth,
|
||||||
|
),
|
||||||
|
cursor: encodeCursor(objectRecord, order),
|
||||||
|
}));
|
||||||
|
|
||||||
|
return {
|
||||||
|
edges,
|
||||||
|
pageInfo: {
|
||||||
|
hasNextPage: objectRecords.length === take && totalCount > take,
|
||||||
|
hasPreviousPage: false,
|
||||||
|
startCursor: edges[0]?.cursor,
|
||||||
|
endCursor: edges[edges.length - 1]?.cursor,
|
||||||
|
},
|
||||||
|
totalCount: totalCount,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
private processRecord<T extends Record<string, any>>(
|
||||||
|
objectRecord: T,
|
||||||
|
take: number,
|
||||||
|
totalCount: number,
|
||||||
|
order: Record<string, FindOptionsOrderValue> | undefined,
|
||||||
|
objectName: string,
|
||||||
|
depth = 0,
|
||||||
|
): T {
|
||||||
|
if (depth >= CONNECTION_MAX_DEPTH) {
|
||||||
|
throw new GraphqlQueryRunnerException(
|
||||||
|
`Maximum depth of ${CONNECTION_MAX_DEPTH} reached`,
|
||||||
|
GraphqlQueryRunnerExceptionCode.MAX_DEPTH_REACHED,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
const objectMetadata = this.objectMetadataMap[objectName];
|
||||||
|
|
||||||
|
if (!objectMetadata) {
|
||||||
|
throw new GraphqlQueryRunnerException(
|
||||||
|
`Object metadata not found for ${objectName}`,
|
||||||
|
GraphqlQueryRunnerExceptionCode.OBJECT_METADATA_NOT_FOUND,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
const processedObjectRecord: Record<string, any> = {};
|
||||||
|
|
||||||
|
for (const [key, value] of Object.entries(objectRecord)) {
|
||||||
|
const fieldMetadata = objectMetadata.fields[key];
|
||||||
|
|
||||||
|
if (!fieldMetadata) {
|
||||||
|
processedObjectRecord[key] = value;
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (isRelationFieldMetadataType(fieldMetadata.type)) {
|
||||||
|
if (Array.isArray(value)) {
|
||||||
|
processedObjectRecord[key] = this.createConnection(
|
||||||
|
value,
|
||||||
|
take,
|
||||||
|
value.length,
|
||||||
|
order,
|
||||||
|
getRelationObjectMetadata(fieldMetadata, this.objectMetadataMap)
|
||||||
|
.nameSingular,
|
||||||
|
depth + 1,
|
||||||
|
);
|
||||||
|
} else if (isPlainObject(value)) {
|
||||||
|
processedObjectRecord[key] = this.processRecord(
|
||||||
|
value,
|
||||||
|
take,
|
||||||
|
totalCount,
|
||||||
|
order,
|
||||||
|
getRelationObjectMetadata(fieldMetadata, this.objectMetadataMap)
|
||||||
|
.nameSingular,
|
||||||
|
depth + 1,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
} else if (isCompositeFieldMetadataType(fieldMetadata.type)) {
|
||||||
|
processedObjectRecord[key] = this.processCompositeField(
|
||||||
|
fieldMetadata,
|
||||||
|
value,
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
processedObjectRecord[key] = this.formatFieldValue(
|
||||||
|
value,
|
||||||
|
fieldMetadata.type,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return processedObjectRecord as T;
|
||||||
|
}
|
||||||
|
|
||||||
|
private processCompositeField(
|
||||||
|
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.entries(fieldValue).reduce(
|
||||||
|
(acc, [subFieldKey, subFieldValue]) => {
|
||||||
|
if (subFieldKey === '__typename') return acc;
|
||||||
|
|
||||||
|
const subFieldMetadata = compositeType.properties.find(
|
||||||
|
(property) => property.name === subFieldKey,
|
||||||
|
);
|
||||||
|
|
||||||
|
if (!subFieldMetadata) {
|
||||||
|
throw new Error(
|
||||||
|
`Sub field metadata not found for composite type: ${fieldMetadata.type}`,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
acc[subFieldKey] = this.formatFieldValue(
|
||||||
|
subFieldValue,
|
||||||
|
subFieldMetadata.type,
|
||||||
|
);
|
||||||
|
|
||||||
|
return acc;
|
||||||
|
},
|
||||||
|
{} as Record<string, any>,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
private formatFieldValue(value: any, fieldType: FieldMetadataType) {
|
||||||
|
switch (fieldType) {
|
||||||
|
case FieldMetadataType.RAW_JSON:
|
||||||
|
return value ? JSON.stringify(value) : value;
|
||||||
|
case FieldMetadataType.DATE:
|
||||||
|
case FieldMetadataType.DATE_TIME:
|
||||||
|
return value instanceof Date ? value.toISOString() : value;
|
||||||
|
default:
|
||||||
|
return value;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -1,79 +0,0 @@
|
|||||||
import { FieldMetadataInterface } from 'src/engine/metadata-modules/field-metadata/interfaces/field-metadata.interface';
|
|
||||||
|
|
||||||
import { GraphQLSelectedFieldsParser } from 'src/engine/api/graphql/graphql-query-runner/parsers/graphql-selected-fields/graphql-selected-fields.parser';
|
|
||||||
import {
|
|
||||||
ObjectMetadataMap,
|
|
||||||
ObjectMetadataMapItem,
|
|
||||||
} from 'src/engine/api/graphql/graphql-query-runner/utils/convert-object-metadata-to-map.util';
|
|
||||||
import { RelationMetadataEntity } from 'src/engine/metadata-modules/relation-metadata/relation-metadata.entity';
|
|
||||||
import {
|
|
||||||
deduceRelationDirection,
|
|
||||||
RelationDirection,
|
|
||||||
} from 'src/engine/utils/deduce-relation-direction.util';
|
|
||||||
|
|
||||||
export class GraphqlSelectedFieldsRelationParser {
|
|
||||||
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 relationMetadata =
|
|
||||||
fieldMetadata.fromRelationMetadata ?? fieldMetadata.toRelationMetadata;
|
|
||||||
|
|
||||||
if (!relationMetadata) {
|
|
||||||
throw new Error(
|
|
||||||
`Relation metadata not found for field ${fieldMetadata.name}`,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
const relationDirection = deduceRelationDirection(
|
|
||||||
fieldMetadata,
|
|
||||||
relationMetadata,
|
|
||||||
);
|
|
||||||
const referencedObjectMetadata = this.getReferencedObjectMetadata(
|
|
||||||
relationMetadata,
|
|
||||||
relationDirection,
|
|
||||||
);
|
|
||||||
|
|
||||||
const relationFields = referencedObjectMetadata.fields;
|
|
||||||
const fieldParser = new GraphQLSelectedFieldsParser(this.objectMetadataMap);
|
|
||||||
const subResult = fieldParser.parse(fieldValue, relationFields);
|
|
||||||
|
|
||||||
result.select[fieldKey] = {
|
|
||||||
id: true,
|
|
||||||
...subResult.select,
|
|
||||||
};
|
|
||||||
result.relations[fieldKey] = subResult.relations;
|
|
||||||
}
|
|
||||||
|
|
||||||
private getReferencedObjectMetadata(
|
|
||||||
relationMetadata: RelationMetadataEntity,
|
|
||||||
relationDirection: RelationDirection,
|
|
||||||
): ObjectMetadataMapItem {
|
|
||||||
const referencedObjectMetadata =
|
|
||||||
relationDirection === RelationDirection.TO
|
|
||||||
? this.objectMetadataMap[relationMetadata.fromObjectMetadataId]
|
|
||||||
: this.objectMetadataMap[relationMetadata.toObjectMetadataId];
|
|
||||||
|
|
||||||
if (!referencedObjectMetadata) {
|
|
||||||
throw new Error(
|
|
||||||
`Referenced object metadata not found for relation ${relationMetadata.id}`,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
return referencedObjectMetadata;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@ -1,114 +0,0 @@
|
|||||||
import { FindOptionsOrderValue } from 'typeorm';
|
|
||||||
|
|
||||||
import { Record as IRecord } from 'src/engine/api/graphql/workspace-query-builder/interfaces/record.interface';
|
|
||||||
import { IConnection } from 'src/engine/api/graphql/workspace-query-runner/interfaces/connection.interface';
|
|
||||||
|
|
||||||
import { CONNECTION_MAX_DEPTH } from 'src/engine/api/graphql/graphql-query-runner/constants/connection-max-depth.constant';
|
|
||||||
import {
|
|
||||||
GraphqlQueryRunnerException,
|
|
||||||
GraphqlQueryRunnerExceptionCode,
|
|
||||||
} from 'src/engine/api/graphql/graphql-query-runner/errors/graphql-query-runner.exception';
|
|
||||||
import { isPlainObject } from 'src/utils/is-plain-object';
|
|
||||||
|
|
||||||
export const createConnection = <ObjectRecord extends IRecord = IRecord>(
|
|
||||||
objectRecords: ObjectRecord[],
|
|
||||||
take: number,
|
|
||||||
totalCount: number,
|
|
||||||
order: Record<string, FindOptionsOrderValue> | undefined,
|
|
||||||
depth = 0,
|
|
||||||
): IConnection<ObjectRecord> => {
|
|
||||||
const edges = (objectRecords ?? []).map((objectRecord) => ({
|
|
||||||
node: processNestedConnections(
|
|
||||||
objectRecord,
|
|
||||||
take,
|
|
||||||
totalCount,
|
|
||||||
order,
|
|
||||||
depth,
|
|
||||||
),
|
|
||||||
cursor: encodeCursor(objectRecord, order),
|
|
||||||
}));
|
|
||||||
|
|
||||||
return {
|
|
||||||
edges,
|
|
||||||
pageInfo: {
|
|
||||||
hasNextPage: objectRecords.length === take && totalCount > take,
|
|
||||||
hasPreviousPage: false,
|
|
||||||
startCursor: edges[0]?.cursor,
|
|
||||||
endCursor: edges[edges.length - 1]?.cursor,
|
|
||||||
},
|
|
||||||
totalCount: totalCount,
|
|
||||||
};
|
|
||||||
};
|
|
||||||
|
|
||||||
const processNestedConnections = <T extends Record<string, any>>(
|
|
||||||
objectRecord: T,
|
|
||||||
take: number,
|
|
||||||
totalCount: number,
|
|
||||||
order: Record<string, FindOptionsOrderValue> | undefined,
|
|
||||||
depth = 0,
|
|
||||||
): T => {
|
|
||||||
if (depth >= CONNECTION_MAX_DEPTH) {
|
|
||||||
throw new GraphqlQueryRunnerException(
|
|
||||||
`Maximum depth of ${CONNECTION_MAX_DEPTH} reached`,
|
|
||||||
GraphqlQueryRunnerExceptionCode.MAX_DEPTH_REACHED,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
const processedObjectRecords: Record<string, any> = { ...objectRecord };
|
|
||||||
|
|
||||||
for (const [key, value] of Object.entries(objectRecord)) {
|
|
||||||
if (Array.isArray(value)) {
|
|
||||||
if (value.length > 0 && typeof value[0] !== 'object') {
|
|
||||||
processedObjectRecords[key] = value;
|
|
||||||
} else {
|
|
||||||
processedObjectRecords[key] = createConnection(
|
|
||||||
value,
|
|
||||||
take,
|
|
||||||
value.length,
|
|
||||||
order,
|
|
||||||
depth + 1,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
} else if (value instanceof Date) {
|
|
||||||
processedObjectRecords[key] = value.toISOString();
|
|
||||||
} else if (isPlainObject(value)) {
|
|
||||||
processedObjectRecords[key] = processNestedConnections(
|
|
||||||
value,
|
|
||||||
take,
|
|
||||||
totalCount,
|
|
||||||
order,
|
|
||||||
depth + 1,
|
|
||||||
);
|
|
||||||
} else {
|
|
||||||
processedObjectRecords[key] = value;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return processedObjectRecords as T;
|
|
||||||
};
|
|
||||||
|
|
||||||
export const decodeCursor = (cursor: string): Record<string, any> => {
|
|
||||||
try {
|
|
||||||
return JSON.parse(Buffer.from(cursor, 'base64').toString());
|
|
||||||
} catch (err) {
|
|
||||||
throw new GraphqlQueryRunnerException(
|
|
||||||
`Invalid cursor: ${cursor}`,
|
|
||||||
GraphqlQueryRunnerExceptionCode.INVALID_CURSOR,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
export const encodeCursor = <ObjectRecord extends IRecord = IRecord>(
|
|
||||||
objectRecord: ObjectRecord,
|
|
||||||
order: Record<string, FindOptionsOrderValue> | undefined,
|
|
||||||
): string => {
|
|
||||||
const cursor = {};
|
|
||||||
|
|
||||||
Object.keys(order ?? []).forEach((key) => {
|
|
||||||
cursor[key] = objectRecord[key];
|
|
||||||
});
|
|
||||||
|
|
||||||
cursor['id'] = objectRecord.id;
|
|
||||||
|
|
||||||
return Buffer.from(JSON.stringify(Object.values(cursor))).toString('base64');
|
|
||||||
};
|
|
||||||
@ -0,0 +1,34 @@
|
|||||||
|
import { FindOptionsOrderValue } from 'typeorm';
|
||||||
|
|
||||||
|
import { Record as IRecord } from 'src/engine/api/graphql/workspace-query-builder/interfaces/record.interface';
|
||||||
|
|
||||||
|
import {
|
||||||
|
GraphqlQueryRunnerException,
|
||||||
|
GraphqlQueryRunnerExceptionCode,
|
||||||
|
} from 'src/engine/api/graphql/graphql-query-runner/errors/graphql-query-runner.exception';
|
||||||
|
|
||||||
|
export const decodeCursor = (cursor: string): Record<string, any> => {
|
||||||
|
try {
|
||||||
|
return JSON.parse(Buffer.from(cursor, 'base64').toString());
|
||||||
|
} catch (err) {
|
||||||
|
throw new GraphqlQueryRunnerException(
|
||||||
|
`Invalid cursor: ${cursor}`,
|
||||||
|
GraphqlQueryRunnerExceptionCode.INVALID_CURSOR,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
export const encodeCursor = <ObjectRecord extends IRecord = IRecord>(
|
||||||
|
objectRecord: ObjectRecord,
|
||||||
|
order: Record<string, FindOptionsOrderValue> | undefined,
|
||||||
|
): string => {
|
||||||
|
const cursor = {};
|
||||||
|
|
||||||
|
Object.keys(order ?? []).forEach((key) => {
|
||||||
|
cursor[key] = objectRecord[key];
|
||||||
|
});
|
||||||
|
|
||||||
|
cursor['id'] = objectRecord.id;
|
||||||
|
|
||||||
|
return Buffer.from(JSON.stringify(Object.values(cursor))).toString('base64');
|
||||||
|
};
|
||||||
@ -0,0 +1,39 @@
|
|||||||
|
import { FieldMetadataInterface } from 'src/engine/metadata-modules/field-metadata/interfaces/field-metadata.interface';
|
||||||
|
|
||||||
|
import { ObjectMetadataMap } from 'src/engine/api/graphql/graphql-query-runner/utils/convert-object-metadata-to-map.util';
|
||||||
|
import {
|
||||||
|
deduceRelationDirection,
|
||||||
|
RelationDirection,
|
||||||
|
} from 'src/engine/utils/deduce-relation-direction.util';
|
||||||
|
|
||||||
|
export const getRelationObjectMetadata = (
|
||||||
|
fieldMetadata: FieldMetadataInterface,
|
||||||
|
objectMetadataMap: ObjectMetadataMap,
|
||||||
|
) => {
|
||||||
|
const relationMetadata =
|
||||||
|
fieldMetadata.fromRelationMetadata ?? fieldMetadata.toRelationMetadata;
|
||||||
|
|
||||||
|
if (!relationMetadata) {
|
||||||
|
throw new Error(
|
||||||
|
`Relation metadata not found for field ${fieldMetadata.name}`,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
const relationDirection = deduceRelationDirection(
|
||||||
|
fieldMetadata,
|
||||||
|
relationMetadata,
|
||||||
|
);
|
||||||
|
|
||||||
|
const referencedObjectMetadata =
|
||||||
|
relationDirection === RelationDirection.TO
|
||||||
|
? objectMetadataMap[relationMetadata.fromObjectMetadataId]
|
||||||
|
: objectMetadataMap[relationMetadata.toObjectMetadataId];
|
||||||
|
|
||||||
|
if (!referencedObjectMetadata) {
|
||||||
|
throw new Error(
|
||||||
|
`Referenced object metadata not found for relation ${relationMetadata.id}`,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return referencedObjectMetadata;
|
||||||
|
};
|
||||||
@ -5,11 +5,11 @@ import { GraphQLInputFieldConfigMap, GraphQLInputObjectType } from 'graphql';
|
|||||||
import { WorkspaceBuildSchemaOptions } from 'src/engine/api/graphql/workspace-schema-builder/interfaces/workspace-build-schema-optionts.interface';
|
import { WorkspaceBuildSchemaOptions } from 'src/engine/api/graphql/workspace-schema-builder/interfaces/workspace-build-schema-optionts.interface';
|
||||||
import { ObjectMetadataInterface } from 'src/engine/metadata-modules/field-metadata/interfaces/object-metadata.interface';
|
import { ObjectMetadataInterface } from 'src/engine/metadata-modules/field-metadata/interfaces/object-metadata.interface';
|
||||||
|
|
||||||
import { pascalCase } from 'src/utils/pascal-case';
|
import { TypeMapperService } from 'src/engine/api/graphql/workspace-schema-builder/services/type-mapper.service';
|
||||||
import { isRelationFieldMetadataType } from 'src/engine/utils/is-relation-field-metadata-type.util';
|
|
||||||
import { FieldMetadataType } from 'src/engine/metadata-modules/field-metadata/field-metadata.entity';
|
import { FieldMetadataType } from 'src/engine/metadata-modules/field-metadata/field-metadata.entity';
|
||||||
import { isCompositeFieldMetadataType } from 'src/engine/metadata-modules/field-metadata/utils/is-composite-field-metadata-type.util';
|
import { isCompositeFieldMetadataType } from 'src/engine/metadata-modules/field-metadata/utils/is-composite-field-metadata-type.util';
|
||||||
import { TypeMapperService } from 'src/engine/api/graphql/workspace-schema-builder/services/type-mapper.service';
|
import { isRelationFieldMetadataType } from 'src/engine/utils/is-relation-field-metadata-type.util';
|
||||||
|
import { pascalCase } from 'src/utils/pascal-case';
|
||||||
|
|
||||||
import { InputTypeFactory } from './input-type.factory';
|
import { InputTypeFactory } from './input-type.factory';
|
||||||
|
|
||||||
|
|||||||
@ -4,13 +4,13 @@ import { GraphQLInputObjectType, GraphQLInputType, GraphQLList } from 'graphql';
|
|||||||
|
|
||||||
import { WorkspaceBuildSchemaOptions } from 'src/engine/api/graphql/workspace-schema-builder/interfaces/workspace-build-schema-optionts.interface';
|
import { WorkspaceBuildSchemaOptions } from 'src/engine/api/graphql/workspace-schema-builder/interfaces/workspace-build-schema-optionts.interface';
|
||||||
|
|
||||||
|
import { FilterIs } from 'src/engine/api/graphql/workspace-schema-builder/graphql-types/input/filter-is.input-type';
|
||||||
import {
|
import {
|
||||||
TypeMapperService,
|
TypeMapperService,
|
||||||
TypeOptions,
|
TypeOptions,
|
||||||
} from 'src/engine/api/graphql/workspace-schema-builder/services/type-mapper.service';
|
} from 'src/engine/api/graphql/workspace-schema-builder/services/type-mapper.service';
|
||||||
import { TypeDefinitionsStorage } from 'src/engine/api/graphql/workspace-schema-builder/storages/type-definitions.storage';
|
import { TypeDefinitionsStorage } from 'src/engine/api/graphql/workspace-schema-builder/storages/type-definitions.storage';
|
||||||
import { FieldMetadataType } from 'src/engine/metadata-modules/field-metadata/field-metadata.entity';
|
import { FieldMetadataType } from 'src/engine/metadata-modules/field-metadata/field-metadata.entity';
|
||||||
import { FilterIs } from 'src/engine/api/graphql/workspace-schema-builder/graphql-types/input/filter-is.input-type';
|
|
||||||
import { isEnumFieldMetadataType } from 'src/engine/metadata-modules/field-metadata/utils/is-enum-field-metadata-type.util';
|
import { isEnumFieldMetadataType } from 'src/engine/metadata-modules/field-metadata/utils/is-enum-field-metadata-type.util';
|
||||||
|
|
||||||
import { InputTypeDefinitionKind } from './input-type-definition.factory';
|
import { InputTypeDefinitionKind } from './input-type-definition.factory';
|
||||||
|
|||||||
@ -209,6 +209,7 @@ export class ObjectMetadataService extends TypeOrmQueryService<ObjectMetadataEnt
|
|||||||
isCustom: isCustom,
|
isCustom: isCustom,
|
||||||
isSystem: false,
|
isSystem: false,
|
||||||
isRemote: objectMetadataInput.isRemote,
|
isRemote: objectMetadataInput.isRemote,
|
||||||
|
isSoftDeletable: true,
|
||||||
fields: isCustom
|
fields: isCustom
|
||||||
? // Creating default fields.
|
? // Creating default fields.
|
||||||
// No need to create a custom migration for this though as the default columns are already
|
// No need to create a custom migration for this though as the default columns are already
|
||||||
|
|||||||
Reference in New Issue
Block a user