[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:
@ -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;
|
||||
};
|
||||
@ -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');
|
||||
};
|
||||
@ -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;
|
||||
};
|
||||
Reference in New Issue
Block a user