diff --git a/packages/twenty-front/src/modules/object-record/record-field/meta-types/display/components/RelationFromManyFieldDisplay.tsx b/packages/twenty-front/src/modules/object-record/record-field/meta-types/display/components/RelationFromManyFieldDisplay.tsx
index 086fe6b33..48e83e11c 100644
--- a/packages/twenty-front/src/modules/object-record/record-field/meta-types/display/components/RelationFromManyFieldDisplay.tsx
+++ b/packages/twenty-front/src/modules/object-record/record-field/meta-types/display/components/RelationFromManyFieldDisplay.tsx
@@ -6,6 +6,7 @@ import { RecordChip } from '@/object-record/components/RecordChip';
import { useFieldFocus } from '@/object-record/record-field/hooks/useFieldFocus';
import { useRelationFromManyFieldDisplay } from '@/object-record/record-field/meta-types/hooks/useRelationFromManyFieldDisplay';
import { ExpandableList } from '@/ui/layout/expandable-list/components/ExpandableList';
+import { isNull } from '@sniptt/guards';
export const RelationFromManyFieldDisplay = () => {
const { fieldValue, fieldDefinition } = useRelationFromManyFieldDisplay();
@@ -47,37 +48,43 @@ export const RelationFromManyFieldDisplay = () => {
return (
- {fieldValue.map((record) => (
-
- ))}
+ {fieldValue
+ .filter((record) => !isNull(record[relationFieldName]))
+ .map((record) => (
+
+ ))}
);
} else if (isRelationFromActivityTargets) {
return (
- {activityTargetObjectRecords.map((record) => (
-
- ))}
+ {activityTargetObjectRecords
+ .filter((record) => !isNull(record.targetObject))
+ .map((record) => (
+
+ ))}
);
} else {
return (
- {fieldValue.map((record) => (
-
- ))}
+ {fieldValue
+ .filter((record) => !isNull(record))
+ .map((record) => (
+
+ ))}
);
}
diff --git a/packages/twenty-server/src/engine/api/graphql/graphql-query-runner/graphql-query-parsers/graphql-query-filter/graphql-query-filter-condition.parser.ts b/packages/twenty-server/src/engine/api/graphql/graphql-query-runner/graphql-query-parsers/graphql-query-filter/graphql-query-filter-condition.parser.ts
index d27996c02..093364db8 100644
--- a/packages/twenty-server/src/engine/api/graphql/graphql-query-runner/graphql-query-parsers/graphql-query-filter/graphql-query-filter-condition.parser.ts
+++ b/packages/twenty-server/src/engine/api/graphql/graphql-query-runner/graphql-query-parsers/graphql-query-filter/graphql-query-filter-condition.parser.ts
@@ -1,4 +1,9 @@
-import { FindOptionsWhere, ObjectLiteral } from 'typeorm';
+import {
+ Brackets,
+ NotBrackets,
+ SelectQueryBuilder,
+ WhereExpressionBuilder,
+} from 'typeorm';
import { RecordFilter } from 'src/engine/api/graphql/workspace-query-builder/interfaces/record.interface';
@@ -8,106 +13,138 @@ import { GraphqlQueryFilterFieldParser } from './graphql-query-filter-field.pars
export class GraphqlQueryFilterConditionParser {
private fieldMetadataMap: FieldMetadataMap;
- private fieldConditionParser: GraphqlQueryFilterFieldParser;
+ private queryFilterFieldParser: GraphqlQueryFilterFieldParser;
constructor(fieldMetadataMap: FieldMetadataMap) {
this.fieldMetadataMap = fieldMetadataMap;
- this.fieldConditionParser = new GraphqlQueryFilterFieldParser(
+ this.queryFilterFieldParser = new GraphqlQueryFilterFieldParser(
this.fieldMetadataMap,
);
}
public parse(
- conditions: RecordFilter,
- isNegated = false,
- ): FindOptionsWhere | FindOptionsWhere[] {
- if (Array.isArray(conditions)) {
- return this.parseAndCondition(conditions, isNegated);
+ queryBuilder: SelectQueryBuilder,
+ objectNameSingular: string,
+ filter: RecordFilter,
+ ): SelectQueryBuilder {
+ if (!filter || Object.keys(filter).length === 0) {
+ return queryBuilder;
}
- const result: FindOptionsWhere = {};
+ return queryBuilder.where(
+ new Brackets((qb) => {
+ Object.entries(filter).forEach(([key, value], index) => {
+ this.parseKeyFilter(qb, objectNameSingular, key, value, index === 0);
+ });
+ }),
+ );
+ }
- for (const [key, value] of Object.entries(conditions)) {
- switch (key) {
- case 'and': {
- const andConditions = this.parseAndCondition(value, isNegated);
+ private parseKeyFilter(
+ queryBuilder: WhereExpressionBuilder,
+ objectNameSingular: string,
+ key: string,
+ value: any,
+ isFirst = false,
+ ): void {
+ switch (key) {
+ case 'and': {
+ const andWhereCondition = new Brackets((qb) => {
+ value.forEach((filter: RecordFilter, index: number) => {
+ const whereCondition = new Brackets((qb2) => {
+ Object.entries(filter).forEach(
+ ([subFilterkey, subFilterValue], index) => {
+ this.parseKeyFilter(
+ qb2,
+ objectNameSingular,
+ subFilterkey,
+ subFilterValue,
+ index === 0,
+ );
+ },
+ );
+ });
- return andConditions.map((condition) => ({
- ...result,
- ...condition,
- }));
+ if (index === 0) {
+ qb.where(whereCondition);
+ } else {
+ qb.andWhere(whereCondition);
+ }
+ });
+ });
+
+ if (isFirst) {
+ queryBuilder.where(andWhereCondition);
+ } else {
+ queryBuilder.andWhere(andWhereCondition);
}
- case 'or': {
- const orConditions = this.parseOrCondition(value, isNegated);
-
- return orConditions.map((condition) => ({ ...result, ...condition }));
- }
- case 'not':
- Object.assign(result, this.parse(value, !isNegated));
- break;
- default:
- Object.assign(
- result,
- this.fieldConditionParser.parse(key, value, isNegated),
- );
+ break;
}
+ case 'or': {
+ const orWhereCondition = new Brackets((qb) => {
+ value.forEach((filter: RecordFilter, index: number) => {
+ const whereCondition = new Brackets((qb2) => {
+ Object.entries(filter).forEach(
+ ([subFilterkey, subFilterValue], index) => {
+ this.parseKeyFilter(
+ qb2,
+ objectNameSingular,
+ subFilterkey,
+ subFilterValue,
+ index === 0,
+ );
+ },
+ );
+ });
+
+ if (index === 0) {
+ qb.where(whereCondition);
+ } else {
+ qb.orWhere(whereCondition);
+ }
+ });
+ });
+
+ if (isFirst) {
+ queryBuilder.where(orWhereCondition);
+ } else {
+ queryBuilder.andWhere(orWhereCondition);
+ }
+
+ break;
+ }
+ case 'not': {
+ const notWhereCondition = new NotBrackets((qb) => {
+ Object.entries(value).forEach(
+ ([subFilterkey, subFilterValue], index) => {
+ this.parseKeyFilter(
+ qb,
+ objectNameSingular,
+ subFilterkey,
+ subFilterValue,
+ index === 0,
+ );
+ },
+ );
+ });
+
+ if (isFirst) {
+ queryBuilder.where(notWhereCondition);
+ } else {
+ queryBuilder.andWhere(notWhereCondition);
+ }
+
+ break;
+ }
+ default:
+ this.queryFilterFieldParser.parse(
+ queryBuilder,
+ objectNameSingular,
+ key,
+ value,
+ isFirst,
+ );
+ break;
}
-
- return result;
- }
-
- private parseAndCondition(
- conditions: RecordFilter[],
- isNegated: boolean,
- ): FindOptionsWhere[] {
- const parsedConditions = conditions.map((condition) =>
- this.parse(condition, isNegated),
- );
-
- return this.combineConditions(parsedConditions, isNegated ? 'or' : 'and');
- }
-
- private parseOrCondition(
- conditions: RecordFilter[],
- isNegated: boolean,
- ): FindOptionsWhere[] {
- const parsedConditions = conditions.map((condition) =>
- this.parse(condition, isNegated),
- );
-
- return this.combineConditions(parsedConditions, isNegated ? 'and' : 'or');
- }
-
- private combineConditions(
- conditions: (
- | FindOptionsWhere
- | FindOptionsWhere[]
- )[],
- combineType: 'and' | 'or',
- ): FindOptionsWhere[] {
- if (combineType === 'and') {
- return conditions.reduce[]>(
- (acc, condition) => {
- if (Array.isArray(condition)) {
- return acc.flatMap((accCondition) =>
- condition.map((subCondition) => ({
- ...accCondition,
- ...subCondition,
- })),
- );
- }
-
- return acc.map((accCondition) => ({
- ...accCondition,
- ...condition,
- }));
- },
- [{}],
- );
- }
-
- return conditions.flatMap((condition) =>
- Array.isArray(condition) ? condition : [condition],
- );
}
}
diff --git a/packages/twenty-server/src/engine/api/graphql/graphql-query-runner/graphql-query-parsers/graphql-query-filter/graphql-query-filter-field.parser.ts b/packages/twenty-server/src/engine/api/graphql/graphql-query-runner/graphql-query-parsers/graphql-query-filter/graphql-query-filter-field.parser.ts
index 9df7ab803..9918a88af 100644
--- a/packages/twenty-server/src/engine/api/graphql/graphql-query-runner/graphql-query-parsers/graphql-query-filter/graphql-query-filter-field.parser.ts
+++ b/packages/twenty-server/src/engine/api/graphql/graphql-query-runner/graphql-query-parsers/graphql-query-filter/graphql-query-filter-field.parser.ts
@@ -1,64 +1,153 @@
-import { FindOptionsWhere, Not, ObjectLiteral } from 'typeorm';
+import { ObjectLiteral, WhereExpressionBuilder } 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 {
+ GraphqlQueryRunnerException,
+ GraphqlQueryRunnerExceptionCode,
+} from 'src/engine/api/graphql/graphql-query-runner/errors/graphql-query-runner.exception';
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 { FieldMetadataMap } from 'src/engine/metadata-modules/utils/generate-object-metadata-map.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';
+type WhereConditionParts = {
+ sql: string;
+ params: ObjectLiteral;
+};
export class GraphqlQueryFilterFieldParser {
private fieldMetadataMap: FieldMetadataMap;
- private operatorParser: GraphqlQueryFilterOperatorParser;
constructor(fieldMetadataMap: FieldMetadataMap) {
this.fieldMetadataMap = fieldMetadataMap;
- this.operatorParser = new GraphqlQueryFilterOperatorParser();
}
public parse(
+ queryBuilder: WhereExpressionBuilder,
+ objectNameSingular: string,
key: string,
- value: any,
- isNegated: boolean,
- ): FindOptionsWhere {
- const fieldMetadata = this.fieldMetadataMap[key];
+ filterValue: any,
+ isFirst = false,
+ ): void {
+ const fieldMetadata = this.fieldMetadataMap[`${key}`];
if (!fieldMetadata) {
- return {
- [key]: (value: RecordFilter, isNegated: boolean) => {
- const conditionParser = new GraphqlQueryFilterConditionParser(
- this.fieldMetadataMap,
- );
-
- return conditionParser.parse(value, isNegated);
- },
- };
+ throw new Error(`Field metadata not found for field: ${key}`);
}
if (isCompositeFieldMetadataType(fieldMetadata.type)) {
- return this.parseCompositeFieldForFilter(fieldMetadata, value, isNegated);
+ return this.parseCompositeFieldForFilter(
+ queryBuilder,
+ fieldMetadata,
+ objectNameSingular,
+ filterValue,
+ isFirst,
+ );
}
+ const [[operator, value]] = Object.entries(filterValue);
- if (isPlainObject(value)) {
- const parsedValue = this.operatorParser.parseOperator(value, isNegated);
+ const { sql, params } = this.computeWhereConditionParts(
+ fieldMetadata,
+ operator,
+ objectNameSingular,
+ key,
+ value,
+ );
- return { [key]: parsedValue };
+ if (isFirst) {
+ queryBuilder.where(sql, params);
+ } else {
+ queryBuilder.andWhere(sql, params);
}
+ }
- return { [key]: isNegated ? Not(value) : value };
+ private computeWhereConditionParts(
+ fieldMetadata: FieldMetadataInterface,
+ operator: string,
+ objectNameSingular: string,
+ key: string,
+ value: any,
+ ): WhereConditionParts {
+ const uuid = Math.random().toString(36).slice(2, 7);
+
+ switch (operator) {
+ case 'eq':
+ return {
+ sql: `${objectNameSingular}.${key} = :${key}${uuid}`,
+ params: { [`${key}${uuid}`]: value },
+ };
+ case 'neq':
+ return {
+ sql: `${objectNameSingular}.${key} != :${key}${uuid}`,
+ params: { [`${key}${uuid}`]: value },
+ };
+ case 'gt':
+ return {
+ sql: `${objectNameSingular}.${key} > :${key}${uuid}`,
+ params: { [`${key}${uuid}`]: value },
+ };
+ case 'gte':
+ return {
+ sql: `${objectNameSingular}.${key} >= :${key}${uuid}`,
+ params: { [`${key}${uuid}`]: value },
+ };
+ case 'lt':
+ return {
+ sql: `${objectNameSingular}.${key} < :${key}${uuid}`,
+ params: { [`${key}${uuid}`]: value },
+ };
+ case 'lte':
+ return {
+ sql: `${objectNameSingular}.${key} <= :${key}${uuid}`,
+ params: { [`${key}${uuid}`]: value },
+ };
+ case 'in':
+ return {
+ sql: `${objectNameSingular}.${key} IN (:...${key}${uuid})`,
+ params: { [`${key}${uuid}`]: value },
+ };
+ case 'is':
+ return {
+ sql: `${objectNameSingular}.${key} IS ${value === 'NULL' ? 'NULL' : 'NOT NULL'}`,
+ params: {},
+ };
+ case 'like':
+ return {
+ sql: `${objectNameSingular}.${key} LIKE :${key}${uuid}`,
+ params: { [`${key}${uuid}`]: `${value}` },
+ };
+ case 'ilike':
+ return {
+ sql: `${objectNameSingular}.${key} ILIKE :${key}${uuid}`,
+ params: { [`${key}${uuid}`]: `${value}` },
+ };
+ case 'startsWith':
+ return {
+ sql: `${objectNameSingular}.${key} LIKE :${key}${uuid}`,
+ params: { [`${key}${uuid}`]: `${value}` },
+ };
+ case 'endsWith':
+ return {
+ sql: `${objectNameSingular}.${key} LIKE :${key}${uuid}`,
+ params: { [`${key}${uuid}`]: `${value}` },
+ };
+ default:
+ throw new GraphqlQueryRunnerException(
+ `Operator "${operator}" is not supported`,
+ GraphqlQueryRunnerExceptionCode.UNSUPPORTED_OPERATOR,
+ );
+ }
}
private parseCompositeFieldForFilter(
+ queryBuilder: WhereExpressionBuilder,
fieldMetadata: FieldMetadataInterface,
+ objectNameSingular: string,
fieldValue: any,
- isNegated: boolean,
- ): FindOptionsWhere {
+ isFirst = false,
+ ): void {
const compositeType = compositeTypeDefinitions.get(
fieldMetadata.type as CompositeFieldMetadataType,
);
@@ -69,34 +158,36 @@ export class GraphqlQueryFilterFieldParser {
);
}
- return Object.entries(fieldValue).reduce(
- (result, [subFieldKey, subFieldValue]) => {
- const subFieldMetadata = compositeType.properties.find(
- (property) => property.name === subFieldKey,
+ Object.entries(fieldValue).map(([subFieldKey, subFieldFilter], index) => {
+ const subFieldMetadata = compositeType.properties.find(
+ (property) => property.name === subFieldKey,
+ );
+
+ if (!subFieldMetadata) {
+ throw new Error(
+ `Sub field metadata not found for composite type: ${fieldMetadata.type}`,
);
+ }
- if (!subFieldMetadata) {
- throw new Error(
- `Sub field metadata not found for composite type: ${fieldMetadata.type}`,
- );
- }
+ const fullFieldName = `${fieldMetadata.name}${capitalize(subFieldKey)}`;
- const fullFieldName = `${fieldMetadata.name}${capitalize(subFieldKey)}`;
+ const [[operator, value]] = Object.entries(
+ subFieldFilter as Record,
+ );
- if (isPlainObject(subFieldValue)) {
- result[fullFieldName] = this.operatorParser.parseOperator(
- subFieldValue,
- isNegated,
- );
- } else {
- result[fullFieldName] = isNegated
- ? Not(subFieldValue)
- : subFieldValue;
- }
+ const { sql, params } = this.computeWhereConditionParts(
+ fieldMetadata,
+ operator,
+ objectNameSingular,
+ fullFieldName,
+ value,
+ );
- return result;
- },
- {} as FindOptionsWhere,
- );
+ if (isFirst && index === 0) {
+ queryBuilder.where(sql, params);
+ }
+
+ queryBuilder.andWhere(sql, params);
+ });
}
}
diff --git a/packages/twenty-server/src/engine/api/graphql/graphql-query-runner/graphql-query-parsers/graphql-query-filter/graphql-query-filter-operator.parser.ts b/packages/twenty-server/src/engine/api/graphql/graphql-query-runner/graphql-query-parsers/graphql-query-filter/graphql-query-filter-operator.parser.ts
deleted file mode 100644
index 8deb04664..000000000
--- a/packages/twenty-server/src/engine/api/graphql/graphql-query-runner/graphql-query-parsers/graphql-query-filter/graphql-query-filter-operator.parser.ts
+++ /dev/null
@@ -1,64 +0,0 @@
-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 };
-
- 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) => {
- if (value === 'NULL') {
- return IsNull();
- } else if (value === 'NOT_NULL') {
- return Not(IsNull());
- } else {
- return 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,
- isNegated: boolean,
- ): FindOperator {
- 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,
- );
- }
-}
diff --git a/packages/twenty-server/src/engine/api/graphql/graphql-query-runner/graphql-query-parsers/graphql-query-order/graphql-query-order.parser.ts b/packages/twenty-server/src/engine/api/graphql/graphql-query-runner/graphql-query-parsers/graphql-query-order/graphql-query-order.parser.ts
index 914c2f4d4..aaa242d80 100644
--- a/packages/twenty-server/src/engine/api/graphql/graphql-query-runner/graphql-query-parsers/graphql-query-order/graphql-query-order.parser.ts
+++ b/packages/twenty-server/src/engine/api/graphql/graphql-query-runner/graphql-query-parsers/graphql-query-order/graphql-query-order.parser.ts
@@ -1,5 +1,3 @@
-import { FindOptionsOrderValue } from 'typeorm';
-
import {
OrderByDirection,
RecordOrderBy,
@@ -24,8 +22,9 @@ export class GraphqlQueryOrderFieldParser {
parse(
orderBy: RecordOrderBy,
+ objectNameSingular: string,
isForwardPagination = true,
- ): Record {
+ ): Record {
return orderBy.reduce(
(acc, item) => {
Object.entries(item).forEach(([key, value]) => {
@@ -42,29 +41,29 @@ export class GraphqlQueryOrderFieldParser {
const compositeOrder = this.parseCompositeFieldForOrder(
fieldMetadata,
value,
+ objectNameSingular,
isForwardPagination,
);
Object.assign(acc, compositeOrder);
} else {
- acc[key] = this.convertOrderByToFindOptionsOrder(
- value,
- isForwardPagination,
- );
+ acc[`"${objectNameSingular}"."${key}"`] =
+ this.convertOrderByToFindOptionsOrder(value, isForwardPagination);
}
});
return acc;
},
- {} as Record,
+ {} as Record,
);
}
private parseCompositeFieldForOrder(
fieldMetadata: FieldMetadataInterface,
value: any,
+ objectNameSingular: string,
isForwardPagination = true,
- ): Record {
+ ): Record {
const compositeType = compositeTypeDefinitions.get(
fieldMetadata.type as CompositeFieldMetadataType,
);
@@ -87,7 +86,7 @@ export class GraphqlQueryOrderFieldParser {
);
}
- const fullFieldName = `${fieldMetadata.name}${capitalize(subFieldKey)}`;
+ const fullFieldName = `"${objectNameSingular}"."${fieldMetadata.name}${capitalize(subFieldKey)}"`;
if (!this.isOrderByDirection(subFieldValue)) {
throw new Error(
@@ -101,35 +100,23 @@ export class GraphqlQueryOrderFieldParser {
return acc;
},
- {} as Record,
+ {} as Record,
);
}
private convertOrderByToFindOptionsOrder(
direction: OrderByDirection,
isForwardPagination = true,
- ): FindOptionsOrderValue {
+ ): string {
switch (direction) {
case OrderByDirection.AscNullsFirst:
- return {
- direction: isForwardPagination ? 'ASC' : 'DESC',
- nulls: 'FIRST',
- };
+ return `${isForwardPagination ? 'ASC' : 'DESC'} NULLS FIRST`;
case OrderByDirection.AscNullsLast:
- return {
- direction: isForwardPagination ? 'ASC' : 'DESC',
- nulls: 'LAST',
- };
+ return `${isForwardPagination ? 'ASC' : 'DESC'} NULLS LAST`;
case OrderByDirection.DescNullsFirst:
- return {
- direction: isForwardPagination ? 'DESC' : 'ASC',
- nulls: 'FIRST',
- };
+ return `${isForwardPagination ? 'DESC' : 'ASC'} NULLS FIRST`;
case OrderByDirection.DescNullsLast:
- return {
- direction: isForwardPagination ? 'DESC' : 'ASC',
- nulls: 'LAST',
- };
+ return `${isForwardPagination ? 'DESC' : 'ASC'} NULLS LAST`;
default:
throw new GraphqlQueryRunnerException(
`Invalid direction: ${direction}`,
diff --git a/packages/twenty-server/src/engine/api/graphql/graphql-query-runner/graphql-query-parsers/graphql-query.parser.ts b/packages/twenty-server/src/engine/api/graphql/graphql-query-runner/graphql-query-parsers/graphql-query.parser.ts
index fc1be7e98..157187fd0 100644
--- a/packages/twenty-server/src/engine/api/graphql/graphql-query-runner/graphql-query-parsers/graphql-query.parser.ts
+++ b/packages/twenty-server/src/engine/api/graphql/graphql-query-runner/graphql-query-parsers/graphql-query.parser.ts
@@ -1,7 +1,8 @@
import {
- FindOptionsOrderValue,
FindOptionsWhere,
ObjectLiteral,
+ OrderByCondition,
+ SelectQueryBuilder,
} from 'typeorm';
import {
@@ -10,8 +11,8 @@ import {
} 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 { GraphqlQueryFilterConditionParser } from 'src/engine/api/graphql/graphql-query-runner/graphql-query-parsers/graphql-query-filter/graphql-query-filter-condition.parser';
+import { GraphqlQueryOrderFieldParser } 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,
@@ -21,6 +22,8 @@ import {
export class GraphqlQueryParser {
private fieldMetadataMap: FieldMetadataMap;
private objectMetadataMap: ObjectMetadataMap;
+ private filterConditionParser: GraphqlQueryFilterConditionParser;
+ private orderFieldParser: GraphqlQueryOrderFieldParser;
constructor(
fieldMetadataMap: FieldMetadataMap,
@@ -28,33 +31,44 @@ export class GraphqlQueryParser {
) {
this.objectMetadataMap = objectMetadataMap;
this.fieldMetadataMap = fieldMetadataMap;
- }
-
- parseFilter(recordFilter: RecordFilter): {
- parsedFilters:
- | FindOptionsWhere
- | FindOptionsWhere[];
- withDeleted: boolean;
- } {
- const graphqlQueryFilterParser = new GraphqlQueryFilterParser(
+ this.filterConditionParser = new GraphqlQueryFilterConditionParser(
this.fieldMetadataMap,
);
+ this.orderFieldParser = new GraphqlQueryOrderFieldParser(
+ this.fieldMetadataMap,
+ );
+ }
- const parsedFilter = graphqlQueryFilterParser.parse(recordFilter);
+ applyFilterToBuilder(
+ queryBuilder: SelectQueryBuilder,
+ objectNameSingular: string,
+ recordFilter: RecordFilter,
+ ): SelectQueryBuilder {
+ return this.filterConditionParser.parse(
+ queryBuilder,
+ objectNameSingular,
+ recordFilter,
+ );
+ }
- const hasDeletedAtFilter = this.checkForDeletedAtFilter(parsedFilter);
+ applyDeletedAtToBuilder(
+ queryBuilder: SelectQueryBuilder,
+ recordFilter: RecordFilter,
+ ): SelectQueryBuilder {
+ if (this.checkForDeletedAtFilter(recordFilter)) {
+ queryBuilder.withDeleted();
+ }
- return {
- parsedFilters: parsedFilter,
- withDeleted: hasDeletedAtFilter,
- };
+ return queryBuilder;
}
private checkForDeletedAtFilter(
filter: FindOptionsWhere | FindOptionsWhere[],
): boolean {
if (Array.isArray(filter)) {
- return filter.some(this.checkForDeletedAtFilter);
+ return filter.some((subFilter) =>
+ this.checkForDeletedAtFilter(subFilter),
+ );
}
for (const [key, value] of Object.entries(filter)) {
@@ -74,15 +88,19 @@ export class GraphqlQueryParser {
return false;
}
- parseOrder(
+ applyOrderToBuilder(
+ queryBuilder: SelectQueryBuilder,
orderBy: RecordOrderBy,
+ objectNameSingular: string,
isForwardPagination = true,
- ): Record {
- const graphqlQueryOrderParser = new GraphqlQueryOrderParser(
- this.fieldMetadataMap,
+ ): SelectQueryBuilder {
+ const parsedOrderBys = this.orderFieldParser.parse(
+ orderBy,
+ objectNameSingular,
+ isForwardPagination,
);
- return graphqlQueryOrderParser.parse(orderBy, isForwardPagination);
+ return queryBuilder.orderBy(parsedOrderBys as OrderByCondition);
}
parseSelectedFields(
diff --git a/packages/twenty-server/src/engine/api/graphql/graphql-query-runner/helpers/process-nested-relations.helper.ts b/packages/twenty-server/src/engine/api/graphql/graphql-query-runner/helpers/process-nested-relations.helper.ts
index 3284faaf3..f19c7cf06 100644
--- a/packages/twenty-server/src/engine/api/graphql/graphql-query-runner/helpers/process-nested-relations.helper.ts
+++ b/packages/twenty-server/src/engine/api/graphql/graphql-query-runner/helpers/process-nested-relations.helper.ts
@@ -77,7 +77,7 @@ export class ProcessNestedRelationsHelper {
if (Object.keys(nestedRelations).length > 0) {
await this.processNestedRelations(
objectMetadataMap,
- objectMetadataMap[relationName],
+ objectMetadataMap[referenceObjectMetadataName],
relationResults as ObjectRecord[],
nestedRelations as Record>,
limit,
@@ -126,6 +126,9 @@ export class ProcessNestedRelationsHelper {
const relationResults = await relationRepository.find(relationFindOptions);
parentObjectRecords.forEach((item) => {
+ if (relationResults.length === 0) {
+ (item as any)[`${relationName}Id`] = null;
+ }
(item as any)[relationName] = relationResults.filter(
(rel) => rel.id === item[`${relationName}Id`],
)[0];
@@ -134,7 +137,7 @@ export class ProcessNestedRelationsHelper {
if (Object.keys(nestedRelations).length > 0) {
await this.processNestedRelations(
objectMetadataMap,
- objectMetadataMap[relationName],
+ objectMetadataMap[referenceObjectMetadataName],
relationResults as ObjectRecord[],
nestedRelations as Record>,
limit,
diff --git a/packages/twenty-server/src/engine/api/graphql/graphql-query-runner/orm-mappers/object-records-to-graphql-connection.mapper.ts b/packages/twenty-server/src/engine/api/graphql/graphql-query-runner/orm-mappers/object-records-to-graphql-connection.mapper.ts
index 88d140932..b9a81ef24 100644
--- a/packages/twenty-server/src/engine/api/graphql/graphql-query-runner/orm-mappers/object-records-to-graphql-connection.mapper.ts
+++ b/packages/twenty-server/src/engine/api/graphql/graphql-query-runner/orm-mappers/object-records-to-graphql-connection.mapper.ts
@@ -1,6 +1,7 @@
-import { FindOptionsOrderValue } from 'typeorm';
-
-import { Record as IRecord } from 'src/engine/api/graphql/workspace-query-builder/interfaces/record.interface';
+import {
+ Record as IRecord,
+ RecordOrderBy,
+} 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';
@@ -31,7 +32,7 @@ export class ObjectRecordsToGraphqlConnectionMapper {
objectName: string,
take: number,
totalCount: number,
- order: Record | undefined,
+ order: RecordOrderBy | undefined,
hasNextPage: boolean,
hasPreviousPage: boolean,
depth = 0,
@@ -65,7 +66,7 @@ export class ObjectRecordsToGraphqlConnectionMapper {
objectName: string,
take: number,
totalCount: number,
- order: Record | undefined = {},
+ order?: RecordOrderBy,
depth = 0,
): T {
if (depth >= CONNECTION_MAX_DEPTH) {
diff --git a/packages/twenty-server/src/engine/api/graphql/graphql-query-runner/resolvers/graphql-query-find-many-resolver.service.ts b/packages/twenty-server/src/engine/api/graphql/graphql-query-runner/resolvers/graphql-query-find-many-resolver.service.ts
index 5608d5ed5..396b5ffdd 100644
--- a/packages/twenty-server/src/engine/api/graphql/graphql-query-runner/resolvers/graphql-query-find-many-resolver.service.ts
+++ b/packages/twenty-server/src/engine/api/graphql/graphql-query-runner/resolvers/graphql-query-find-many-resolver.service.ts
@@ -1,9 +1,9 @@
import { isDefined } from 'class-validator';
import graphqlFields from 'graphql-fields';
-import { FindManyOptions, ObjectLiteral } from 'typeorm';
import {
Record as IRecord,
+ OrderByDirection,
RecordFilter,
RecordOrderBy,
} from 'src/engine/api/graphql/workspace-query-builder/interfaces/record.interface';
@@ -19,14 +19,15 @@ import {
import { GraphqlQueryParser } from 'src/engine/api/graphql/graphql-query-runner/graphql-query-parsers/graphql-query.parser';
import { ProcessNestedRelationsHelper } from 'src/engine/api/graphql/graphql-query-runner/helpers/process-nested-relations.helper';
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 { computeCursorArgFilter } from 'src/engine/api/graphql/graphql-query-runner/utils/compute-cursor-arg-filter';
import { decodeCursor } from 'src/engine/api/graphql/graphql-query-runner/utils/cursors.util';
import { getObjectMetadataOrThrow } from 'src/engine/api/graphql/graphql-query-runner/utils/get-object-metadata-or-throw.util';
import {
- generateObjectMetadataMap,
ObjectMetadataMapItem,
+ generateObjectMetadataMap,
} from 'src/engine/metadata-modules/utils/generate-object-metadata-map.util';
import { TwentyORMGlobalManager } from 'src/engine/twenty-orm/twenty-orm-global.manager';
+import { formatResult } from 'src/engine/twenty-orm/utils/format-result.util';
export class GraphqlQueryFindManyResolverService {
private twentyORMGlobalManager: TwentyORMGlobalManager;
@@ -56,9 +57,19 @@ export class GraphqlQueryFindManyResolverService {
const repository = dataSource.getRepository(
objectMetadataItem.nameSingular,
);
+
+ const queryBuilder = repository.createQueryBuilder(
+ objectMetadataItem.nameSingular,
+ );
+
+ const countQueryBuilder = repository.createQueryBuilder(
+ objectMetadataItem.nameSingular,
+ );
+
const objectMetadataMap = generateObjectMetadataMap(
objectMetadataCollection,
);
+
const objectMetadata = getObjectMetadataOrThrow(
objectMetadataMap,
objectMetadataItem.nameSingular,
@@ -68,45 +79,82 @@ export class GraphqlQueryFindManyResolverService {
objectMetadataMap,
);
+ const withFilterCountQueryBuilder = graphqlQueryParser.applyFilterToBuilder(
+ countQueryBuilder,
+ objectMetadataItem.nameSingular,
+ args.filter ?? ({} as Filter),
+ );
+
const selectedFields = graphqlFields(info);
- const { select, relations } = graphqlQueryParser.parseSelectedFields(
+ const { relations } = graphqlQueryParser.parseSelectedFields(
objectMetadataItem,
selectedFields,
);
const isForwardPagination = !isDefined(args.before);
- const order = graphqlQueryParser.parseOrder(
- args.orderBy ?? [],
- isForwardPagination,
- );
- const { parsedFilters: where, withDeleted } =
- graphqlQueryParser.parseFilter(args.filter ?? ({} as Filter));
- const cursor = this.getCursor(args);
const limit = args.first ?? args.last ?? QUERY_MAX_RECORDS;
- this.addOrderByColumnsToSelect(order, select);
- this.addForeingKeyColumnsToSelect(relations, select, objectMetadata);
-
- const findOptions: FindManyOptions = {
- where,
- order,
- select,
- take: limit + 1,
- withDeleted,
- };
+ const withDeletedCountQueryBuilder =
+ graphqlQueryParser.applyDeletedAtToBuilder(
+ withFilterCountQueryBuilder,
+ args.filter ?? ({} as Filter),
+ );
const totalCount = isDefined(selectedFields.totalCount)
- ? await repository.count({ where, withDeleted })
+ ? await withDeletedCountQueryBuilder.getCount()
: 0;
+ const cursor = this.getCursor(args);
+
+ let appliedFilters = args.filter ?? ({} as Filter);
+
+ const orderByWithIdCondition = [
+ ...(args.orderBy ?? []),
+ { id: OrderByDirection.AscNullsFirst },
+ ] as OrderBy;
+
if (cursor) {
- applyRangeFilter(where, cursor, isForwardPagination);
+ const cursorArgFilter = computeCursorArgFilter(
+ cursor,
+ orderByWithIdCondition,
+ isForwardPagination,
+ );
+
+ appliedFilters = (args.filter
+ ? {
+ and: [args.filter, { or: cursorArgFilter }],
+ }
+ : { or: cursorArgFilter }) as unknown as Filter;
}
- const objectRecords = (await repository.find(
- findOptions,
- )) as ObjectRecord[];
+ const withFilterQueryBuilder = graphqlQueryParser.applyFilterToBuilder(
+ queryBuilder,
+ objectMetadataItem.nameSingular,
+ appliedFilters,
+ );
+
+ const withOrderByQueryBuilder = graphqlQueryParser.applyOrderToBuilder(
+ withFilterQueryBuilder,
+ orderByWithIdCondition,
+ objectMetadataItem.nameSingular,
+ isForwardPagination,
+ );
+
+ const withDeletedQueryBuilder = graphqlQueryParser.applyDeletedAtToBuilder(
+ withOrderByQueryBuilder,
+ args.filter ?? ({} as Filter),
+ );
+
+ const nonFormattedObjectRecords = await withDeletedQueryBuilder
+ .take(limit + 1)
+ .getMany();
+
+ const objectRecords = formatResult(
+ nonFormattedObjectRecords,
+ objectMetadata,
+ objectMetadataMap,
+ );
const { hasNextPage, hasPreviousPage } = this.getPaginationInfo(
objectRecords,
@@ -142,7 +190,7 @@ export class GraphqlQueryFindManyResolverService {
objectMetadataItem.nameSingular,
limit,
totalCount,
- order,
+ orderByWithIdCondition,
hasNextPage,
hasPreviousPage,
);
diff --git a/packages/twenty-server/src/engine/api/graphql/graphql-query-runner/resolvers/graphql-query-find-one-resolver.service.ts b/packages/twenty-server/src/engine/api/graphql/graphql-query-runner/resolvers/graphql-query-find-one-resolver.service.ts
index c1609e0c0..b9e86e420 100644
--- a/packages/twenty-server/src/engine/api/graphql/graphql-query-runner/resolvers/graphql-query-find-one-resolver.service.ts
+++ b/packages/twenty-server/src/engine/api/graphql/graphql-query-runner/resolvers/graphql-query-find-one-resolver.service.ts
@@ -18,6 +18,7 @@ import { ObjectRecordsToGraphqlConnectionMapper } from 'src/engine/api/graphql/g
import { getObjectMetadataOrThrow } from 'src/engine/api/graphql/graphql-query-runner/utils/get-object-metadata-or-throw.util';
import { generateObjectMetadataMap } from 'src/engine/metadata-modules/utils/generate-object-metadata-map.util';
import { TwentyORMGlobalManager } from 'src/engine/twenty-orm/twenty-orm-global.manager';
+import { formatResult } from 'src/engine/twenty-orm/utils/format-result.util';
export class GraphqlQueryFindOneResolverService {
private twentyORMGlobalManager: TwentyORMGlobalManager;
@@ -35,12 +36,17 @@ export class GraphqlQueryFindOneResolverService {
): Promise {
const { authContext, objectMetadataItem, info, objectMetadataCollection } =
options;
+
const dataSource =
await this.twentyORMGlobalManager.getDataSourceForWorkspace(
authContext.workspace.id,
);
- const repository = await dataSource.getRepository(
+ const repository = dataSource.getRepository(
+ objectMetadataItem.nameSingular,
+ );
+
+ const queryBuilder = repository.createQueryBuilder(
objectMetadataItem.nameSingular,
);
@@ -52,6 +58,7 @@ export class GraphqlQueryFindOneResolverService {
objectMetadataMap,
objectMetadataItem.nameSingular,
);
+
const graphqlQueryParser = new GraphqlQueryParser(
objectMetadata.fields,
objectMetadataMap,
@@ -59,18 +66,29 @@ export class GraphqlQueryFindOneResolverService {
const selectedFields = graphqlFields(info);
- const { select, relations } = graphqlQueryParser.parseSelectedFields(
+ const { relations } = graphqlQueryParser.parseSelectedFields(
objectMetadataItem,
selectedFields,
);
- const { parsedFilters: where, withDeleted } =
- graphqlQueryParser.parseFilter(args.filter ?? ({} as Filter));
- const objectRecord = (await repository.findOne({
- where,
- select,
- withDeleted,
- })) as ObjectRecord;
+ const withFilterQueryBuilder = graphqlQueryParser.applyFilterToBuilder(
+ queryBuilder,
+ objectMetadataItem.nameSingular,
+ args.filter ?? ({} as Filter),
+ );
+
+ const withDeletedQueryBuilder = graphqlQueryParser.applyDeletedAtToBuilder(
+ withFilterQueryBuilder,
+ args.filter ?? ({} as Filter),
+ );
+
+ const nonFormattedObjectRecord = await withDeletedQueryBuilder.getOne();
+
+ const objectRecord = formatResult(
+ nonFormattedObjectRecord,
+ objectMetadata,
+ objectMetadataMap,
+ );
const limit = QUERY_MAX_RECORDS;
diff --git a/packages/twenty-server/src/engine/api/graphql/graphql-query-runner/utils/apply-range-filter.util.ts b/packages/twenty-server/src/engine/api/graphql/graphql-query-runner/utils/apply-range-filter.util.ts
deleted file mode 100644
index 3a536c68c..000000000
--- a/packages/twenty-server/src/engine/api/graphql/graphql-query-runner/utils/apply-range-filter.util.ts
+++ /dev/null
@@ -1,16 +0,0 @@
-import { FindOptionsWhere, LessThan, MoreThan, ObjectLiteral } from 'typeorm';
-
-export const applyRangeFilter = (
- where: FindOptionsWhere,
- cursor: Record,
- isForwardPagination = true,
-): FindOptionsWhere => {
- Object.entries(cursor ?? {}).forEach(([key, value]) => {
- if (key === 'id') {
- return;
- }
- where[key] = isForwardPagination ? MoreThan(value) : LessThan(value);
- });
-
- return where;
-};
diff --git a/packages/twenty-server/src/engine/api/graphql/graphql-query-runner/utils/compute-cursor-arg-filter.ts b/packages/twenty-server/src/engine/api/graphql/graphql-query-runner/utils/compute-cursor-arg-filter.ts
new file mode 100644
index 000000000..ed55af29e
--- /dev/null
+++ b/packages/twenty-server/src/engine/api/graphql/graphql-query-runner/utils/compute-cursor-arg-filter.ts
@@ -0,0 +1,66 @@
+import {
+ OrderByDirection,
+ RecordFilter,
+ RecordOrderBy,
+} 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 computeCursorArgFilter = (
+ cursor: Record,
+ orderBy: RecordOrderBy,
+ isForwardPagination = true,
+): RecordFilter[] => {
+ const cursorKeys = Object.keys(cursor ?? {});
+ const cursorValues = Object.values(cursor ?? {});
+
+ if (cursorKeys.length === 0) {
+ return [];
+ }
+
+ return Object.entries(cursor ?? {}).map(([key, value], index) => {
+ let whereCondition = {};
+
+ for (
+ let subConditionIndex = 0;
+ subConditionIndex < index;
+ subConditionIndex++
+ ) {
+ whereCondition = {
+ ...whereCondition,
+ [cursorKeys[subConditionIndex]]: {
+ eq: cursorValues[subConditionIndex],
+ },
+ };
+ }
+
+ const keyOrderBy = orderBy.find((order) => key in order);
+
+ if (!keyOrderBy) {
+ throw new GraphqlQueryRunnerException(
+ 'Invalid cursor',
+ GraphqlQueryRunnerExceptionCode.INVALID_CURSOR,
+ );
+ }
+
+ const isAscending =
+ keyOrderBy[key] === OrderByDirection.AscNullsFirst ||
+ keyOrderBy[key] === OrderByDirection.AscNullsLast;
+
+ const operator = isAscending
+ ? isForwardPagination
+ ? 'gt'
+ : 'lt'
+ : isForwardPagination
+ ? 'lt'
+ : 'gt';
+
+ return {
+ ...whereCondition,
+ ...{ [key]: { [operator]: value } },
+ } as RecordFilter;
+ });
+};
diff --git a/packages/twenty-server/src/engine/api/graphql/graphql-query-runner/utils/cursors.util.ts b/packages/twenty-server/src/engine/api/graphql/graphql-query-runner/utils/cursors.util.ts
index 5a556850f..bf8eb52d0 100644
--- a/packages/twenty-server/src/engine/api/graphql/graphql-query-runner/utils/cursors.util.ts
+++ b/packages/twenty-server/src/engine/api/graphql/graphql-query-runner/utils/cursors.util.ts
@@ -1,6 +1,7 @@
-import { FindOptionsOrderValue } from 'typeorm';
-
-import { Record as IRecord } from 'src/engine/api/graphql/workspace-query-builder/interfaces/record.interface';
+import {
+ Record as IRecord,
+ RecordOrderBy,
+} from 'src/engine/api/graphql/workspace-query-builder/interfaces/record.interface';
import {
GraphqlQueryRunnerException,
@@ -24,11 +25,15 @@ export const decodeCursor = (cursor: string): CursorData => {
export const encodeCursor = (
objectRecord: ObjectRecord,
- order: Record | undefined,
+ order: RecordOrderBy | undefined,
): string => {
const orderByValues: Record = {};
- Object.keys(order ?? {}).forEach((key) => {
+ const orderBy = order?.reduce((acc, orderBy) => ({ ...acc, ...orderBy }), {});
+
+ const orderByKeys = Object.keys(orderBy ?? {});
+
+ orderByKeys?.forEach((key) => {
orderByValues[key] = objectRecord[key];
});
diff --git a/packages/twenty-server/src/engine/twenty-orm/repository/workspace.repository.ts b/packages/twenty-server/src/engine/twenty-orm/repository/workspace.repository.ts
index a264b44bf..ea3c3f9e8 100644
--- a/packages/twenty-server/src/engine/twenty-orm/repository/workspace.repository.ts
+++ b/packages/twenty-server/src/engine/twenty-orm/repository/workspace.repository.ts
@@ -1,5 +1,3 @@
-import { isPlainObject } from '@nestjs/common/utils/shared.utils';
-
import {
DeepPartial,
DeleteResult,
@@ -24,14 +22,10 @@ import { UpsertOptions } from 'typeorm/repository/UpsertOptions';
import { WorkspaceInternalContext } from 'src/engine/twenty-orm/interfaces/workspace-internal-context.interface';
-import { compositeTypeDefinitions } from 'src/engine/metadata-modules/field-metadata/composite-types';
-import { computeCompositeColumnName } from 'src/engine/metadata-modules/field-metadata/utils/compute-column-name.util';
-import { isCompositeFieldMetadataType } from 'src/engine/metadata-modules/field-metadata/utils/is-composite-field-metadata-type.util';
-import { RelationMetadataEntity } from 'src/engine/metadata-modules/relation-metadata/relation-metadata.entity';
import { ObjectMetadataMapItem } from 'src/engine/metadata-modules/utils/generate-object-metadata-map.util';
import { WorkspaceEntitiesStorage } from 'src/engine/twenty-orm/storage/workspace-entities.storage';
-import { computeRelationType } from 'src/engine/twenty-orm/utils/compute-relation-type.util';
-import { isRelationFieldMetadataType } from 'src/engine/utils/is-relation-field-metadata-type.util';
+import { formatData } from 'src/engine/twenty-orm/utils/format-data.util';
+import { formatResult } from 'src/engine/twenty-orm/utils/format-result.util';
export class WorkspaceRepository<
Entity extends ObjectLiteral,
@@ -650,18 +644,6 @@ export class WorkspaceRepository<
return objectMetadata;
}
- private async getCompositeFieldMetadataCollection(
- objectMetadata: ObjectMetadataMapItem,
- ) {
- const compositeFieldMetadataCollection = Object.values(
- objectMetadata.fields,
- ).filter((fieldMetadata) =>
- isCompositeFieldMetadataType(fieldMetadata.type),
- );
-
- return compositeFieldMetadataCollection;
- }
-
private async transformOptions<
T extends FindManyOptions | FindOneOptions | undefined,
>(options: T): Promise {
@@ -677,62 +659,9 @@ export class WorkspaceRepository<
}
private async formatData(data: T): Promise {
- if (!data) {
- return data;
- }
-
- if (Array.isArray(data)) {
- return Promise.all(
- data.map((item) => this.formatData(item)),
- ) as Promise;
- }
-
const objectMetadata = await this.getObjectMetadataFromTarget();
- const compositeFieldMetadataCollection =
- await this.getCompositeFieldMetadataCollection(objectMetadata);
- const compositeFieldMetadataMap = new Map(
- compositeFieldMetadataCollection.map((fieldMetadata) => [
- fieldMetadata.name,
- fieldMetadata,
- ]),
- );
- const newData: object = {};
-
- for (const [key, value] of Object.entries(data)) {
- const fieldMetadata = compositeFieldMetadataMap.get(key);
-
- if (!fieldMetadata) {
- if (isPlainObject(value)) {
- newData[key] = await this.formatData(value);
- } else {
- newData[key] = value;
- }
- continue;
- }
-
- const compositeType = compositeTypeDefinitions.get(fieldMetadata.type);
-
- if (!compositeType) {
- continue;
- }
-
- for (const compositeProperty of compositeType.properties) {
- const compositeKey = computeCompositeColumnName(
- fieldMetadata.name,
- compositeProperty,
- );
- const value = data?.[key]?.[compositeProperty.name];
-
- if (value === undefined || value === null) {
- continue;
- }
-
- newData[compositeKey] = data[key][compositeProperty.name];
- }
- }
-
- return newData as T;
+ return formatData(data, objectMetadata) as T;
}
private async formatResult(
@@ -741,124 +670,8 @@ export class WorkspaceRepository<
): Promise {
objectMetadata ??= await this.getObjectMetadataFromTarget();
- if (!data) {
- return data;
- }
+ const objectMetadataMap = this.internalContext.objectMetadataMap;
- if (Array.isArray(data)) {
- // If the data is an array, map each item in the array, format result is a promise
- return Promise.all(
- data.map((item) => this.formatResult(item, objectMetadata)),
- ) as Promise;
- }
-
- if (!isPlainObject(data)) {
- return data;
- }
-
- if (!objectMetadata) {
- throw new Error('Object metadata is missing');
- }
-
- const compositeFieldMetadataCollection =
- await this.getCompositeFieldMetadataCollection(objectMetadata);
-
- const compositeFieldMetadataMap = new Map(
- compositeFieldMetadataCollection.flatMap((fieldMetadata) => {
- const compositeType = compositeTypeDefinitions.get(fieldMetadata.type);
-
- if (!compositeType) return [];
-
- // Map each composite property to a [key, value] pair
- return compositeType.properties.map((compositeProperty) => [
- computeCompositeColumnName(fieldMetadata.name, compositeProperty),
- {
- parentField: fieldMetadata.name,
- ...compositeProperty,
- },
- ]);
- }),
- );
-
- const relationMetadataMap = new Map(
- Object.values(objectMetadata.fields)
- .filter(({ type }) => isRelationFieldMetadataType(type))
- .map((fieldMetadata) => [
- fieldMetadata.name,
- {
- relationMetadata:
- fieldMetadata.fromRelationMetadata ??
- fieldMetadata.toRelationMetadata,
- relationType: computeRelationType(
- fieldMetadata,
- fieldMetadata.fromRelationMetadata ??
- (fieldMetadata.toRelationMetadata as RelationMetadataEntity),
- ),
- },
- ]),
- );
- const newData: object = {};
-
- for (const [key, value] of Object.entries(data)) {
- const compositePropertyArgs = compositeFieldMetadataMap.get(key);
- const { relationMetadata, relationType } =
- relationMetadataMap.get(key) ?? {};
-
- if (!compositePropertyArgs && !relationMetadata) {
- if (isPlainObject(value)) {
- newData[key] = await this.formatResult(value);
- } else {
- newData[key] = value;
- }
- continue;
- }
-
- if (relationMetadata) {
- const toObjectMetadata =
- this.internalContext.objectMetadataMap[
- relationMetadata.toObjectMetadataId
- ];
-
- const fromObjectMetadata =
- this.internalContext.objectMetadataMap[
- relationMetadata.fromObjectMetadataId
- ];
-
- if (!toObjectMetadata) {
- throw new Error(
- `Object metadata for object metadataId "${relationMetadata.toObjectMetadataId}" is missing`,
- );
- }
-
- if (!fromObjectMetadata) {
- throw new Error(
- `Object metadata for object metadataId "${relationMetadata.fromObjectMetadataId}" is missing`,
- );
- }
-
- newData[key] = await this.formatResult(
- value,
-
- relationType === 'one-to-many'
- ? toObjectMetadata
- : fromObjectMetadata,
- );
- continue;
- }
-
- if (!compositePropertyArgs) {
- continue;
- }
-
- const { parentField, ...compositeProperty } = compositePropertyArgs;
-
- if (!newData[parentField]) {
- newData[parentField] = {};
- }
-
- newData[parentField][compositeProperty.name] = value;
- }
-
- return newData as T;
+ return formatResult(data, objectMetadata, objectMetadataMap) as T;
}
}
diff --git a/packages/twenty-server/src/engine/twenty-orm/utils/format-data.util.ts b/packages/twenty-server/src/engine/twenty-orm/utils/format-data.util.ts
new file mode 100644
index 000000000..f6f31f32c
--- /dev/null
+++ b/packages/twenty-server/src/engine/twenty-orm/utils/format-data.util.ts
@@ -0,0 +1,65 @@
+import { isPlainObject } from '@nestjs/common/utils/shared.utils';
+
+import { compositeTypeDefinitions } from 'src/engine/metadata-modules/field-metadata/composite-types';
+import { computeCompositeColumnName } from 'src/engine/metadata-modules/field-metadata/utils/compute-column-name.util';
+import { ObjectMetadataMapItem } from 'src/engine/metadata-modules/utils/generate-object-metadata-map.util';
+import { getCompositeFieldMetadataCollection } from 'src/engine/twenty-orm/utils/get-composite-field-metadata-collection';
+
+export function formatData(
+ data: T,
+ objectMetadata: ObjectMetadataMapItem,
+): T {
+ if (!data) {
+ return data;
+ }
+
+ if (Array.isArray(data)) {
+ return data.map((item) => formatData(item, objectMetadata)) as T;
+ }
+
+ const compositeFieldMetadataCollection =
+ getCompositeFieldMetadataCollection(objectMetadata);
+
+ const compositeFieldMetadataMap = new Map(
+ compositeFieldMetadataCollection.map((fieldMetadata) => [
+ fieldMetadata.name,
+ fieldMetadata,
+ ]),
+ );
+ const newData: object = {};
+
+ for (const [key, value] of Object.entries(data)) {
+ const fieldMetadata = compositeFieldMetadataMap.get(key);
+
+ if (!fieldMetadata) {
+ if (isPlainObject(value)) {
+ newData[key] = formatData(value, objectMetadata);
+ } else {
+ newData[key] = value;
+ }
+ continue;
+ }
+
+ const compositeType = compositeTypeDefinitions.get(fieldMetadata.type);
+
+ if (!compositeType) {
+ continue;
+ }
+
+ for (const compositeProperty of compositeType.properties) {
+ const compositeKey = computeCompositeColumnName(
+ fieldMetadata.name,
+ compositeProperty,
+ );
+ const value = data?.[key]?.[compositeProperty.name];
+
+ if (value === undefined || value === null) {
+ continue;
+ }
+
+ newData[compositeKey] = data[key][compositeProperty.name];
+ }
+ }
+
+ return newData as T;
+}
diff --git a/packages/twenty-server/src/engine/twenty-orm/utils/format-result.util.ts b/packages/twenty-server/src/engine/twenty-orm/utils/format-result.util.ts
new file mode 100644
index 000000000..81949e074
--- /dev/null
+++ b/packages/twenty-server/src/engine/twenty-orm/utils/format-result.util.ts
@@ -0,0 +1,131 @@
+import { isPlainObject } from '@nestjs/common/utils/shared.utils';
+
+import { compositeTypeDefinitions } from 'src/engine/metadata-modules/field-metadata/composite-types';
+import { computeCompositeColumnName } from 'src/engine/metadata-modules/field-metadata/utils/compute-column-name.util';
+import { RelationMetadataEntity } from 'src/engine/metadata-modules/relation-metadata/relation-metadata.entity';
+import {
+ ObjectMetadataMap,
+ ObjectMetadataMapItem,
+} from 'src/engine/metadata-modules/utils/generate-object-metadata-map.util';
+import { computeRelationType } from 'src/engine/twenty-orm/utils/compute-relation-type.util';
+import { getCompositeFieldMetadataCollection } from 'src/engine/twenty-orm/utils/get-composite-field-metadata-collection';
+import { isRelationFieldMetadataType } from 'src/engine/utils/is-relation-field-metadata-type.util';
+
+export function formatResult(
+ data: T,
+ objectMetadata: ObjectMetadataMapItem,
+ objectMetadataMap: ObjectMetadataMap,
+): T {
+ if (!data) {
+ return data;
+ }
+
+ if (Array.isArray(data)) {
+ return data.map((item) =>
+ formatResult(item, objectMetadata, objectMetadataMap),
+ ) as T;
+ }
+
+ if (!isPlainObject(data)) {
+ return data;
+ }
+
+ if (!objectMetadata) {
+ throw new Error('Object metadata is missing');
+ }
+
+ const compositeFieldMetadataCollection =
+ getCompositeFieldMetadataCollection(objectMetadata);
+
+ const compositeFieldMetadataMap = new Map(
+ compositeFieldMetadataCollection.flatMap((fieldMetadata) => {
+ const compositeType = compositeTypeDefinitions.get(fieldMetadata.type);
+
+ if (!compositeType) return [];
+
+ // Map each composite property to a [key, value] pair
+ return compositeType.properties.map((compositeProperty) => [
+ computeCompositeColumnName(fieldMetadata.name, compositeProperty),
+ {
+ parentField: fieldMetadata.name,
+ ...compositeProperty,
+ },
+ ]);
+ }),
+ );
+
+ const relationMetadataMap = new Map(
+ Object.values(objectMetadata.fields)
+ .filter(({ type }) => isRelationFieldMetadataType(type))
+ .map((fieldMetadata) => [
+ fieldMetadata.name,
+ {
+ relationMetadata:
+ fieldMetadata.fromRelationMetadata ??
+ fieldMetadata.toRelationMetadata,
+ relationType: computeRelationType(
+ fieldMetadata,
+ fieldMetadata.fromRelationMetadata ??
+ (fieldMetadata.toRelationMetadata as RelationMetadataEntity),
+ ),
+ },
+ ]),
+ );
+ const newData: object = {};
+
+ for (const [key, value] of Object.entries(data)) {
+ const compositePropertyArgs = compositeFieldMetadataMap.get(key);
+ const { relationMetadata, relationType } =
+ relationMetadataMap.get(key) ?? {};
+
+ if (!compositePropertyArgs && !relationMetadata) {
+ if (isPlainObject(value)) {
+ newData[key] = formatResult(value, objectMetadata, objectMetadataMap);
+ } else {
+ newData[key] = value;
+ }
+ continue;
+ }
+
+ if (relationMetadata) {
+ const toObjectMetadata =
+ objectMetadataMap[relationMetadata.toObjectMetadataId];
+
+ const fromObjectMetadata =
+ objectMetadataMap[relationMetadata.fromObjectMetadataId];
+
+ if (!toObjectMetadata) {
+ throw new Error(
+ `Object metadata for object metadataId "${relationMetadata.toObjectMetadataId}" is missing`,
+ );
+ }
+
+ if (!fromObjectMetadata) {
+ throw new Error(
+ `Object metadata for object metadataId "${relationMetadata.fromObjectMetadataId}" is missing`,
+ );
+ }
+
+ newData[key] = formatResult(
+ value,
+ relationType === 'one-to-many' ? toObjectMetadata : fromObjectMetadata,
+ objectMetadataMap,
+ );
+ continue;
+ }
+
+ if (!compositePropertyArgs) {
+ continue;
+ }
+
+ const { parentField, ...compositeProperty } = compositePropertyArgs;
+
+ if (!newData[parentField]) {
+ newData[parentField] = {};
+ }
+
+ newData[parentField][compositeProperty.name] = value;
+ }
+
+ return newData as T;
+}
diff --git a/packages/twenty-server/src/engine/twenty-orm/utils/get-composite-field-metadata-collection.ts b/packages/twenty-server/src/engine/twenty-orm/utils/get-composite-field-metadata-collection.ts
new file mode 100644
index 000000000..88ac2820c
--- /dev/null
+++ b/packages/twenty-server/src/engine/twenty-orm/utils/get-composite-field-metadata-collection.ts
@@ -0,0 +1,12 @@
+import { isCompositeFieldMetadataType } from 'src/engine/metadata-modules/field-metadata/utils/is-composite-field-metadata-type.util';
+import { ObjectMetadataMapItem } from 'src/engine/metadata-modules/utils/generate-object-metadata-map.util';
+
+export function getCompositeFieldMetadataCollection(
+ objectMetadata: ObjectMetadataMapItem,
+) {
+ const compositeFieldMetadataCollection = Object.values(
+ objectMetadata.fields,
+ ).filter((fieldMetadata) => isCompositeFieldMetadataType(fieldMetadata.type));
+
+ return compositeFieldMetadataCollection;
+}