[POC] add graphql query runner (#6747)

## Context
The goal is to replace pg_graphql with our own ORM wrapper (TwentyORM).
This PR tries to add some parsing logic to convert graphql requests to
send to the ORM to replace pg_graphql implementation.

---------

Co-authored-by: Charles Bochet <charles@twenty.com>
This commit is contained in:
Weiko
2024-08-27 17:06:39 +02:00
committed by GitHub
parent ef4f2e43b0
commit f6fd92adcb
51 changed files with 1397 additions and 249 deletions

View File

@ -0,0 +1,29 @@
import {
FindOptionsOrderValue,
FindOptionsWhere,
LessThan,
MoreThan,
ObjectLiteral,
} from 'typeorm';
export const applyRangeFilter = (
where: FindOptionsWhere<ObjectLiteral>,
order: Record<string, FindOptionsOrderValue> | undefined,
cursor: Record<string, any>,
): FindOptionsWhere<ObjectLiteral> => {
if (!order) return where;
const orderEntries = Object.entries(order);
orderEntries.forEach(([column, order], index) => {
if (typeof order !== 'object' || !('direction' in order)) {
return;
}
where[column] =
order.direction === 'ASC'
? MoreThan(cursor[index])
: LessThan(cursor[index]);
});
return where;
};

View File

@ -0,0 +1,114 @@
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');
};

View File

@ -0,0 +1,35 @@
import { FieldMetadataInterface } from 'src/engine/metadata-modules/field-metadata/interfaces/field-metadata.interface';
import { ObjectMetadataInterface } from 'src/engine/metadata-modules/field-metadata/interfaces/object-metadata.interface';
export type FieldMetadataMap = Record<string, FieldMetadataInterface>;
export type ObjectMetadataMapItem = Omit<ObjectMetadataInterface, 'fields'> & {
fields: FieldMetadataMap;
};
export type ObjectMetadataMap = Record<string, ObjectMetadataMapItem>;
export const convertObjectMetadataToMap = (
objectMetadataCollection: ObjectMetadataInterface[],
): ObjectMetadataMap => {
const objectMetadataMap: ObjectMetadataMap = {};
for (const objectMetadata of objectMetadataCollection) {
const fieldsMap: FieldMetadataMap = {};
for (const fieldMetadata of objectMetadata.fields) {
fieldsMap[fieldMetadata.name] = fieldMetadata;
}
const processedObjectMetadata: ObjectMetadataMapItem = {
...objectMetadata,
fields: fieldsMap,
};
objectMetadataMap[objectMetadata.id] = processedObjectMetadata;
objectMetadataMap[objectMetadata.nameSingular] = processedObjectMetadata;
objectMetadataMap[objectMetadata.namePlural] = processedObjectMetadata;
}
return objectMetadataMap;
};