Fix nested relations (#7158)
Co-authored-by: Charles Bochet <charles@twenty.com>
This commit is contained in:
@ -6,6 +6,7 @@ import { RecordChip } from '@/object-record/components/RecordChip';
|
|||||||
import { useFieldFocus } from '@/object-record/record-field/hooks/useFieldFocus';
|
import { useFieldFocus } from '@/object-record/record-field/hooks/useFieldFocus';
|
||||||
import { useRelationFromManyFieldDisplay } from '@/object-record/record-field/meta-types/hooks/useRelationFromManyFieldDisplay';
|
import { useRelationFromManyFieldDisplay } from '@/object-record/record-field/meta-types/hooks/useRelationFromManyFieldDisplay';
|
||||||
import { ExpandableList } from '@/ui/layout/expandable-list/components/ExpandableList';
|
import { ExpandableList } from '@/ui/layout/expandable-list/components/ExpandableList';
|
||||||
|
import { isNull } from '@sniptt/guards';
|
||||||
|
|
||||||
export const RelationFromManyFieldDisplay = () => {
|
export const RelationFromManyFieldDisplay = () => {
|
||||||
const { fieldValue, fieldDefinition } = useRelationFromManyFieldDisplay();
|
const { fieldValue, fieldDefinition } = useRelationFromManyFieldDisplay();
|
||||||
@ -47,37 +48,43 @@ export const RelationFromManyFieldDisplay = () => {
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<ExpandableList isChipCountDisplayed={isFocused}>
|
<ExpandableList isChipCountDisplayed={isFocused}>
|
||||||
{fieldValue.map((record) => (
|
{fieldValue
|
||||||
<RecordChip
|
.filter((record) => !isNull(record[relationFieldName]))
|
||||||
key={record.id}
|
.map((record) => (
|
||||||
objectNameSingular={objectNameSingular}
|
<RecordChip
|
||||||
record={record[relationFieldName]}
|
key={record.id}
|
||||||
/>
|
objectNameSingular={objectNameSingular}
|
||||||
))}
|
record={record[relationFieldName]}
|
||||||
|
/>
|
||||||
|
))}
|
||||||
</ExpandableList>
|
</ExpandableList>
|
||||||
);
|
);
|
||||||
} else if (isRelationFromActivityTargets) {
|
} else if (isRelationFromActivityTargets) {
|
||||||
return (
|
return (
|
||||||
<ExpandableList isChipCountDisplayed={isFocused}>
|
<ExpandableList isChipCountDisplayed={isFocused}>
|
||||||
{activityTargetObjectRecords.map((record) => (
|
{activityTargetObjectRecords
|
||||||
<RecordChip
|
.filter((record) => !isNull(record.targetObject))
|
||||||
key={record.targetObject.id}
|
.map((record) => (
|
||||||
objectNameSingular={record.targetObjectMetadataItem.nameSingular}
|
<RecordChip
|
||||||
record={record.targetObject}
|
key={record.targetObject.id}
|
||||||
/>
|
objectNameSingular={record.targetObjectMetadataItem.nameSingular}
|
||||||
))}
|
record={record.targetObject}
|
||||||
|
/>
|
||||||
|
))}
|
||||||
</ExpandableList>
|
</ExpandableList>
|
||||||
);
|
);
|
||||||
} else {
|
} else {
|
||||||
return (
|
return (
|
||||||
<ExpandableList isChipCountDisplayed={isFocused}>
|
<ExpandableList isChipCountDisplayed={isFocused}>
|
||||||
{fieldValue.map((record) => (
|
{fieldValue
|
||||||
<RecordChip
|
.filter((record) => !isNull(record))
|
||||||
key={record.id}
|
.map((record) => (
|
||||||
objectNameSingular={relationObjectNameSingular}
|
<RecordChip
|
||||||
record={record}
|
key={record.id}
|
||||||
/>
|
objectNameSingular={relationObjectNameSingular}
|
||||||
))}
|
record={record}
|
||||||
|
/>
|
||||||
|
))}
|
||||||
</ExpandableList>
|
</ExpandableList>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@ -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';
|
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 {
|
export class GraphqlQueryFilterConditionParser {
|
||||||
private fieldMetadataMap: FieldMetadataMap;
|
private fieldMetadataMap: FieldMetadataMap;
|
||||||
private fieldConditionParser: GraphqlQueryFilterFieldParser;
|
private queryFilterFieldParser: GraphqlQueryFilterFieldParser;
|
||||||
|
|
||||||
constructor(fieldMetadataMap: FieldMetadataMap) {
|
constructor(fieldMetadataMap: FieldMetadataMap) {
|
||||||
this.fieldMetadataMap = fieldMetadataMap;
|
this.fieldMetadataMap = fieldMetadataMap;
|
||||||
this.fieldConditionParser = new GraphqlQueryFilterFieldParser(
|
this.queryFilterFieldParser = new GraphqlQueryFilterFieldParser(
|
||||||
this.fieldMetadataMap,
|
this.fieldMetadataMap,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
public parse(
|
public parse(
|
||||||
conditions: RecordFilter,
|
queryBuilder: SelectQueryBuilder<any>,
|
||||||
isNegated = false,
|
objectNameSingular: string,
|
||||||
): FindOptionsWhere<ObjectLiteral> | FindOptionsWhere<ObjectLiteral>[] {
|
filter: RecordFilter,
|
||||||
if (Array.isArray(conditions)) {
|
): SelectQueryBuilder<any> {
|
||||||
return this.parseAndCondition(conditions, isNegated);
|
if (!filter || Object.keys(filter).length === 0) {
|
||||||
|
return queryBuilder;
|
||||||
}
|
}
|
||||||
|
|
||||||
const result: FindOptionsWhere<ObjectLiteral> = {};
|
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)) {
|
private parseKeyFilter(
|
||||||
switch (key) {
|
queryBuilder: WhereExpressionBuilder,
|
||||||
case 'and': {
|
objectNameSingular: string,
|
||||||
const andConditions = this.parseAndCondition(value, isNegated);
|
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) => ({
|
if (index === 0) {
|
||||||
...result,
|
qb.where(whereCondition);
|
||||||
...condition,
|
} else {
|
||||||
}));
|
qb.andWhere(whereCondition);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
if (isFirst) {
|
||||||
|
queryBuilder.where(andWhereCondition);
|
||||||
|
} else {
|
||||||
|
queryBuilder.andWhere(andWhereCondition);
|
||||||
}
|
}
|
||||||
case 'or': {
|
break;
|
||||||
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),
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
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<ObjectLiteral>[] {
|
|
||||||
const parsedConditions = conditions.map((condition) =>
|
|
||||||
this.parse(condition, isNegated),
|
|
||||||
);
|
|
||||||
|
|
||||||
return this.combineConditions(parsedConditions, isNegated ? 'or' : 'and');
|
|
||||||
}
|
|
||||||
|
|
||||||
private parseOrCondition(
|
|
||||||
conditions: RecordFilter[],
|
|
||||||
isNegated: boolean,
|
|
||||||
): FindOptionsWhere<ObjectLiteral>[] {
|
|
||||||
const parsedConditions = conditions.map((condition) =>
|
|
||||||
this.parse(condition, isNegated),
|
|
||||||
);
|
|
||||||
|
|
||||||
return this.combineConditions(parsedConditions, isNegated ? 'and' : 'or');
|
|
||||||
}
|
|
||||||
|
|
||||||
private combineConditions(
|
|
||||||
conditions: (
|
|
||||||
| FindOptionsWhere<ObjectLiteral>
|
|
||||||
| FindOptionsWhere<ObjectLiteral>[]
|
|
||||||
)[],
|
|
||||||
combineType: 'and' | 'or',
|
|
||||||
): FindOptionsWhere<ObjectLiteral>[] {
|
|
||||||
if (combineType === 'and') {
|
|
||||||
return conditions.reduce<FindOptionsWhere<ObjectLiteral>[]>(
|
|
||||||
(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],
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -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 { 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 { 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';
|
||||||
import { FieldMetadataMap } from 'src/engine/metadata-modules/utils/generate-object-metadata-map.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 { CompositeFieldMetadataType } from 'src/engine/metadata-modules/workspace-migration/factories/composite-column-action.factory';
|
||||||
import { capitalize } from 'src/utils/capitalize';
|
import { capitalize } from 'src/utils/capitalize';
|
||||||
import { isPlainObject } from 'src/utils/is-plain-object';
|
|
||||||
|
|
||||||
import { GraphqlQueryFilterConditionParser } from './graphql-query-filter-condition.parser';
|
type WhereConditionParts = {
|
||||||
import { GraphqlQueryFilterOperatorParser } from './graphql-query-filter-operator.parser';
|
sql: string;
|
||||||
|
params: ObjectLiteral;
|
||||||
|
};
|
||||||
|
|
||||||
export class GraphqlQueryFilterFieldParser {
|
export class GraphqlQueryFilterFieldParser {
|
||||||
private fieldMetadataMap: FieldMetadataMap;
|
private fieldMetadataMap: FieldMetadataMap;
|
||||||
private operatorParser: GraphqlQueryFilterOperatorParser;
|
|
||||||
|
|
||||||
constructor(fieldMetadataMap: FieldMetadataMap) {
|
constructor(fieldMetadataMap: FieldMetadataMap) {
|
||||||
this.fieldMetadataMap = fieldMetadataMap;
|
this.fieldMetadataMap = fieldMetadataMap;
|
||||||
this.operatorParser = new GraphqlQueryFilterOperatorParser();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public parse(
|
public parse(
|
||||||
|
queryBuilder: WhereExpressionBuilder,
|
||||||
|
objectNameSingular: string,
|
||||||
key: string,
|
key: string,
|
||||||
value: any,
|
filterValue: any,
|
||||||
isNegated: boolean,
|
isFirst = false,
|
||||||
): FindOptionsWhere<ObjectLiteral> {
|
): void {
|
||||||
const fieldMetadata = this.fieldMetadataMap[key];
|
const fieldMetadata = this.fieldMetadataMap[`${key}`];
|
||||||
|
|
||||||
if (!fieldMetadata) {
|
if (!fieldMetadata) {
|
||||||
return {
|
throw new Error(`Field metadata not found for field: ${key}`);
|
||||||
[key]: (value: RecordFilter, isNegated: boolean) => {
|
|
||||||
const conditionParser = new GraphqlQueryFilterConditionParser(
|
|
||||||
this.fieldMetadataMap,
|
|
||||||
);
|
|
||||||
|
|
||||||
return conditionParser.parse(value, isNegated);
|
|
||||||
},
|
|
||||||
};
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if (isCompositeFieldMetadataType(fieldMetadata.type)) {
|
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 { sql, params } = this.computeWhereConditionParts(
|
||||||
const parsedValue = this.operatorParser.parseOperator(value, isNegated);
|
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(
|
private parseCompositeFieldForFilter(
|
||||||
|
queryBuilder: WhereExpressionBuilder,
|
||||||
fieldMetadata: FieldMetadataInterface,
|
fieldMetadata: FieldMetadataInterface,
|
||||||
|
objectNameSingular: string,
|
||||||
fieldValue: any,
|
fieldValue: any,
|
||||||
isNegated: boolean,
|
isFirst = false,
|
||||||
): FindOptionsWhere<ObjectLiteral> {
|
): void {
|
||||||
const compositeType = compositeTypeDefinitions.get(
|
const compositeType = compositeTypeDefinitions.get(
|
||||||
fieldMetadata.type as CompositeFieldMetadataType,
|
fieldMetadata.type as CompositeFieldMetadataType,
|
||||||
);
|
);
|
||||||
@ -69,34 +158,36 @@ export class GraphqlQueryFilterFieldParser {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
return Object.entries(fieldValue).reduce(
|
Object.entries(fieldValue).map(([subFieldKey, subFieldFilter], index) => {
|
||||||
(result, [subFieldKey, subFieldValue]) => {
|
const subFieldMetadata = compositeType.properties.find(
|
||||||
const subFieldMetadata = compositeType.properties.find(
|
(property) => property.name === subFieldKey,
|
||||||
(property) => property.name === subFieldKey,
|
);
|
||||||
|
|
||||||
|
if (!subFieldMetadata) {
|
||||||
|
throw new Error(
|
||||||
|
`Sub field metadata not found for composite type: ${fieldMetadata.type}`,
|
||||||
);
|
);
|
||||||
|
}
|
||||||
|
|
||||||
if (!subFieldMetadata) {
|
const fullFieldName = `${fieldMetadata.name}${capitalize(subFieldKey)}`;
|
||||||
throw new Error(
|
|
||||||
`Sub field metadata not found for composite type: ${fieldMetadata.type}`,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
const fullFieldName = `${fieldMetadata.name}${capitalize(subFieldKey)}`;
|
const [[operator, value]] = Object.entries(
|
||||||
|
subFieldFilter as Record<string, any>,
|
||||||
|
);
|
||||||
|
|
||||||
if (isPlainObject(subFieldValue)) {
|
const { sql, params } = this.computeWhereConditionParts(
|
||||||
result[fullFieldName] = this.operatorParser.parseOperator(
|
fieldMetadata,
|
||||||
subFieldValue,
|
operator,
|
||||||
isNegated,
|
objectNameSingular,
|
||||||
);
|
fullFieldName,
|
||||||
} else {
|
value,
|
||||||
result[fullFieldName] = isNegated
|
);
|
||||||
? Not(subFieldValue)
|
|
||||||
: subFieldValue;
|
|
||||||
}
|
|
||||||
|
|
||||||
return result;
|
if (isFirst && index === 0) {
|
||||||
},
|
queryBuilder.where(sql, params);
|
||||||
{} as FindOptionsWhere<ObjectLiteral>,
|
}
|
||||||
);
|
|
||||||
|
queryBuilder.andWhere(sql, params);
|
||||||
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -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<any> };
|
|
||||||
|
|
||||||
constructor() {
|
|
||||||
this.operatorMap = {
|
|
||||||
eq: (value: any) => value,
|
|
||||||
neq: (value: any) => Not(value),
|
|
||||||
gt: (value: any) => MoreThan(value),
|
|
||||||
gte: (value: any) => MoreThanOrEqual(value),
|
|
||||||
lt: (value: any) => LessThan(value),
|
|
||||||
lte: (value: any) => LessThanOrEqual(value),
|
|
||||||
in: (value: any) => In(value),
|
|
||||||
is: (value: any) => {
|
|
||||||
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<string, any>,
|
|
||||||
isNegated: boolean,
|
|
||||||
): FindOperator<any> {
|
|
||||||
const [[operator, value]] = Object.entries(operatorObj);
|
|
||||||
|
|
||||||
if (operator in this.operatorMap) {
|
|
||||||
const operatorFunction = this.operatorMap[operator];
|
|
||||||
|
|
||||||
return isNegated ? Not(operatorFunction(value)) : operatorFunction(value);
|
|
||||||
}
|
|
||||||
|
|
||||||
throw new GraphqlQueryRunnerException(
|
|
||||||
`Operator "${operator}" is not supported`,
|
|
||||||
GraphqlQueryRunnerExceptionCode.UNSUPPORTED_OPERATOR,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@ -1,5 +1,3 @@
|
|||||||
import { FindOptionsOrderValue } from 'typeorm';
|
|
||||||
|
|
||||||
import {
|
import {
|
||||||
OrderByDirection,
|
OrderByDirection,
|
||||||
RecordOrderBy,
|
RecordOrderBy,
|
||||||
@ -24,8 +22,9 @@ export class GraphqlQueryOrderFieldParser {
|
|||||||
|
|
||||||
parse(
|
parse(
|
||||||
orderBy: RecordOrderBy,
|
orderBy: RecordOrderBy,
|
||||||
|
objectNameSingular: string,
|
||||||
isForwardPagination = true,
|
isForwardPagination = true,
|
||||||
): Record<string, FindOptionsOrderValue> {
|
): Record<string, string> {
|
||||||
return orderBy.reduce(
|
return orderBy.reduce(
|
||||||
(acc, item) => {
|
(acc, item) => {
|
||||||
Object.entries(item).forEach(([key, value]) => {
|
Object.entries(item).forEach(([key, value]) => {
|
||||||
@ -42,29 +41,29 @@ export class GraphqlQueryOrderFieldParser {
|
|||||||
const compositeOrder = this.parseCompositeFieldForOrder(
|
const compositeOrder = this.parseCompositeFieldForOrder(
|
||||||
fieldMetadata,
|
fieldMetadata,
|
||||||
value,
|
value,
|
||||||
|
objectNameSingular,
|
||||||
isForwardPagination,
|
isForwardPagination,
|
||||||
);
|
);
|
||||||
|
|
||||||
Object.assign(acc, compositeOrder);
|
Object.assign(acc, compositeOrder);
|
||||||
} else {
|
} else {
|
||||||
acc[key] = this.convertOrderByToFindOptionsOrder(
|
acc[`"${objectNameSingular}"."${key}"`] =
|
||||||
value,
|
this.convertOrderByToFindOptionsOrder(value, isForwardPagination);
|
||||||
isForwardPagination,
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
return acc;
|
return acc;
|
||||||
},
|
},
|
||||||
{} as Record<string, FindOptionsOrderValue>,
|
{} as Record<string, string>,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
private parseCompositeFieldForOrder(
|
private parseCompositeFieldForOrder(
|
||||||
fieldMetadata: FieldMetadataInterface,
|
fieldMetadata: FieldMetadataInterface,
|
||||||
value: any,
|
value: any,
|
||||||
|
objectNameSingular: string,
|
||||||
isForwardPagination = true,
|
isForwardPagination = true,
|
||||||
): Record<string, FindOptionsOrderValue> {
|
): Record<string, string> {
|
||||||
const compositeType = compositeTypeDefinitions.get(
|
const compositeType = compositeTypeDefinitions.get(
|
||||||
fieldMetadata.type as CompositeFieldMetadataType,
|
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)) {
|
if (!this.isOrderByDirection(subFieldValue)) {
|
||||||
throw new Error(
|
throw new Error(
|
||||||
@ -101,35 +100,23 @@ export class GraphqlQueryOrderFieldParser {
|
|||||||
|
|
||||||
return acc;
|
return acc;
|
||||||
},
|
},
|
||||||
{} as Record<string, FindOptionsOrderValue>,
|
{} as Record<string, string>,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
private convertOrderByToFindOptionsOrder(
|
private convertOrderByToFindOptionsOrder(
|
||||||
direction: OrderByDirection,
|
direction: OrderByDirection,
|
||||||
isForwardPagination = true,
|
isForwardPagination = true,
|
||||||
): FindOptionsOrderValue {
|
): string {
|
||||||
switch (direction) {
|
switch (direction) {
|
||||||
case OrderByDirection.AscNullsFirst:
|
case OrderByDirection.AscNullsFirst:
|
||||||
return {
|
return `${isForwardPagination ? 'ASC' : 'DESC'} NULLS FIRST`;
|
||||||
direction: isForwardPagination ? 'ASC' : 'DESC',
|
|
||||||
nulls: 'FIRST',
|
|
||||||
};
|
|
||||||
case OrderByDirection.AscNullsLast:
|
case OrderByDirection.AscNullsLast:
|
||||||
return {
|
return `${isForwardPagination ? 'ASC' : 'DESC'} NULLS LAST`;
|
||||||
direction: isForwardPagination ? 'ASC' : 'DESC',
|
|
||||||
nulls: 'LAST',
|
|
||||||
};
|
|
||||||
case OrderByDirection.DescNullsFirst:
|
case OrderByDirection.DescNullsFirst:
|
||||||
return {
|
return `${isForwardPagination ? 'DESC' : 'ASC'} NULLS FIRST`;
|
||||||
direction: isForwardPagination ? 'DESC' : 'ASC',
|
|
||||||
nulls: 'FIRST',
|
|
||||||
};
|
|
||||||
case OrderByDirection.DescNullsLast:
|
case OrderByDirection.DescNullsLast:
|
||||||
return {
|
return `${isForwardPagination ? 'DESC' : 'ASC'} NULLS LAST`;
|
||||||
direction: isForwardPagination ? 'DESC' : 'ASC',
|
|
||||||
nulls: 'LAST',
|
|
||||||
};
|
|
||||||
default:
|
default:
|
||||||
throw new GraphqlQueryRunnerException(
|
throw new GraphqlQueryRunnerException(
|
||||||
`Invalid direction: ${direction}`,
|
`Invalid direction: ${direction}`,
|
||||||
|
|||||||
@ -1,7 +1,8 @@
|
|||||||
import {
|
import {
|
||||||
FindOptionsOrderValue,
|
|
||||||
FindOptionsWhere,
|
FindOptionsWhere,
|
||||||
ObjectLiteral,
|
ObjectLiteral,
|
||||||
|
OrderByCondition,
|
||||||
|
SelectQueryBuilder,
|
||||||
} from 'typeorm';
|
} from 'typeorm';
|
||||||
|
|
||||||
import {
|
import {
|
||||||
@ -10,8 +11,8 @@ 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/graphql-query-parsers/graphql-query-filter/graphql-query-filter-condition.parser';
|
import { GraphqlQueryFilterConditionParser } 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 { 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 { GraphqlQuerySelectedFieldsParser } from 'src/engine/api/graphql/graphql-query-runner/graphql-query-parsers/graphql-query-selected-fields/graphql-selected-fields.parser';
|
||||||
import {
|
import {
|
||||||
FieldMetadataMap,
|
FieldMetadataMap,
|
||||||
@ -21,6 +22,8 @@ import {
|
|||||||
export class GraphqlQueryParser {
|
export class GraphqlQueryParser {
|
||||||
private fieldMetadataMap: FieldMetadataMap;
|
private fieldMetadataMap: FieldMetadataMap;
|
||||||
private objectMetadataMap: ObjectMetadataMap;
|
private objectMetadataMap: ObjectMetadataMap;
|
||||||
|
private filterConditionParser: GraphqlQueryFilterConditionParser;
|
||||||
|
private orderFieldParser: GraphqlQueryOrderFieldParser;
|
||||||
|
|
||||||
constructor(
|
constructor(
|
||||||
fieldMetadataMap: FieldMetadataMap,
|
fieldMetadataMap: FieldMetadataMap,
|
||||||
@ -28,33 +31,44 @@ export class GraphqlQueryParser {
|
|||||||
) {
|
) {
|
||||||
this.objectMetadataMap = objectMetadataMap;
|
this.objectMetadataMap = objectMetadataMap;
|
||||||
this.fieldMetadataMap = fieldMetadataMap;
|
this.fieldMetadataMap = fieldMetadataMap;
|
||||||
}
|
this.filterConditionParser = new GraphqlQueryFilterConditionParser(
|
||||||
|
|
||||||
parseFilter(recordFilter: RecordFilter): {
|
|
||||||
parsedFilters:
|
|
||||||
| FindOptionsWhere<ObjectLiteral>
|
|
||||||
| FindOptionsWhere<ObjectLiteral>[];
|
|
||||||
withDeleted: boolean;
|
|
||||||
} {
|
|
||||||
const graphqlQueryFilterParser = new GraphqlQueryFilterParser(
|
|
||||||
this.fieldMetadataMap,
|
this.fieldMetadataMap,
|
||||||
);
|
);
|
||||||
|
this.orderFieldParser = new GraphqlQueryOrderFieldParser(
|
||||||
|
this.fieldMetadataMap,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
const parsedFilter = graphqlQueryFilterParser.parse(recordFilter);
|
applyFilterToBuilder(
|
||||||
|
queryBuilder: SelectQueryBuilder<any>,
|
||||||
|
objectNameSingular: string,
|
||||||
|
recordFilter: RecordFilter,
|
||||||
|
): SelectQueryBuilder<any> {
|
||||||
|
return this.filterConditionParser.parse(
|
||||||
|
queryBuilder,
|
||||||
|
objectNameSingular,
|
||||||
|
recordFilter,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
const hasDeletedAtFilter = this.checkForDeletedAtFilter(parsedFilter);
|
applyDeletedAtToBuilder(
|
||||||
|
queryBuilder: SelectQueryBuilder<any>,
|
||||||
|
recordFilter: RecordFilter,
|
||||||
|
): SelectQueryBuilder<any> {
|
||||||
|
if (this.checkForDeletedAtFilter(recordFilter)) {
|
||||||
|
queryBuilder.withDeleted();
|
||||||
|
}
|
||||||
|
|
||||||
return {
|
return queryBuilder;
|
||||||
parsedFilters: parsedFilter,
|
|
||||||
withDeleted: hasDeletedAtFilter,
|
|
||||||
};
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private checkForDeletedAtFilter(
|
private checkForDeletedAtFilter(
|
||||||
filter: FindOptionsWhere<ObjectLiteral> | FindOptionsWhere<ObjectLiteral>[],
|
filter: FindOptionsWhere<ObjectLiteral> | FindOptionsWhere<ObjectLiteral>[],
|
||||||
): boolean {
|
): boolean {
|
||||||
if (Array.isArray(filter)) {
|
if (Array.isArray(filter)) {
|
||||||
return filter.some(this.checkForDeletedAtFilter);
|
return filter.some((subFilter) =>
|
||||||
|
this.checkForDeletedAtFilter(subFilter),
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
for (const [key, value] of Object.entries(filter)) {
|
for (const [key, value] of Object.entries(filter)) {
|
||||||
@ -74,15 +88,19 @@ export class GraphqlQueryParser {
|
|||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
parseOrder(
|
applyOrderToBuilder(
|
||||||
|
queryBuilder: SelectQueryBuilder<any>,
|
||||||
orderBy: RecordOrderBy,
|
orderBy: RecordOrderBy,
|
||||||
|
objectNameSingular: string,
|
||||||
isForwardPagination = true,
|
isForwardPagination = true,
|
||||||
): Record<string, FindOptionsOrderValue> {
|
): SelectQueryBuilder<any> {
|
||||||
const graphqlQueryOrderParser = new GraphqlQueryOrderParser(
|
const parsedOrderBys = this.orderFieldParser.parse(
|
||||||
this.fieldMetadataMap,
|
orderBy,
|
||||||
|
objectNameSingular,
|
||||||
|
isForwardPagination,
|
||||||
);
|
);
|
||||||
|
|
||||||
return graphqlQueryOrderParser.parse(orderBy, isForwardPagination);
|
return queryBuilder.orderBy(parsedOrderBys as OrderByCondition);
|
||||||
}
|
}
|
||||||
|
|
||||||
parseSelectedFields(
|
parseSelectedFields(
|
||||||
|
|||||||
@ -77,7 +77,7 @@ export class ProcessNestedRelationsHelper {
|
|||||||
if (Object.keys(nestedRelations).length > 0) {
|
if (Object.keys(nestedRelations).length > 0) {
|
||||||
await this.processNestedRelations(
|
await this.processNestedRelations(
|
||||||
objectMetadataMap,
|
objectMetadataMap,
|
||||||
objectMetadataMap[relationName],
|
objectMetadataMap[referenceObjectMetadataName],
|
||||||
relationResults as ObjectRecord[],
|
relationResults as ObjectRecord[],
|
||||||
nestedRelations as Record<string, FindOptionsRelations<ObjectLiteral>>,
|
nestedRelations as Record<string, FindOptionsRelations<ObjectLiteral>>,
|
||||||
limit,
|
limit,
|
||||||
@ -126,6 +126,9 @@ export class ProcessNestedRelationsHelper {
|
|||||||
const relationResults = await relationRepository.find(relationFindOptions);
|
const relationResults = await relationRepository.find(relationFindOptions);
|
||||||
|
|
||||||
parentObjectRecords.forEach((item) => {
|
parentObjectRecords.forEach((item) => {
|
||||||
|
if (relationResults.length === 0) {
|
||||||
|
(item as any)[`${relationName}Id`] = null;
|
||||||
|
}
|
||||||
(item as any)[relationName] = relationResults.filter(
|
(item as any)[relationName] = relationResults.filter(
|
||||||
(rel) => rel.id === item[`${relationName}Id`],
|
(rel) => rel.id === item[`${relationName}Id`],
|
||||||
)[0];
|
)[0];
|
||||||
@ -134,7 +137,7 @@ export class ProcessNestedRelationsHelper {
|
|||||||
if (Object.keys(nestedRelations).length > 0) {
|
if (Object.keys(nestedRelations).length > 0) {
|
||||||
await this.processNestedRelations(
|
await this.processNestedRelations(
|
||||||
objectMetadataMap,
|
objectMetadataMap,
|
||||||
objectMetadataMap[relationName],
|
objectMetadataMap[referenceObjectMetadataName],
|
||||||
relationResults as ObjectRecord[],
|
relationResults as ObjectRecord[],
|
||||||
nestedRelations as Record<string, FindOptionsRelations<ObjectLiteral>>,
|
nestedRelations as Record<string, FindOptionsRelations<ObjectLiteral>>,
|
||||||
limit,
|
limit,
|
||||||
|
|||||||
@ -1,6 +1,7 @@
|
|||||||
import { FindOptionsOrderValue } from 'typeorm';
|
import {
|
||||||
|
Record as IRecord,
|
||||||
import { Record as IRecord } from 'src/engine/api/graphql/workspace-query-builder/interfaces/record.interface';
|
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 { 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 { FieldMetadataInterface } from 'src/engine/metadata-modules/field-metadata/interfaces/field-metadata.interface';
|
||||||
|
|
||||||
@ -31,7 +32,7 @@ export class ObjectRecordsToGraphqlConnectionMapper {
|
|||||||
objectName: string,
|
objectName: string,
|
||||||
take: number,
|
take: number,
|
||||||
totalCount: number,
|
totalCount: number,
|
||||||
order: Record<string, FindOptionsOrderValue> | undefined,
|
order: RecordOrderBy | undefined,
|
||||||
hasNextPage: boolean,
|
hasNextPage: boolean,
|
||||||
hasPreviousPage: boolean,
|
hasPreviousPage: boolean,
|
||||||
depth = 0,
|
depth = 0,
|
||||||
@ -65,7 +66,7 @@ export class ObjectRecordsToGraphqlConnectionMapper {
|
|||||||
objectName: string,
|
objectName: string,
|
||||||
take: number,
|
take: number,
|
||||||
totalCount: number,
|
totalCount: number,
|
||||||
order: Record<string, FindOptionsOrderValue> | undefined = {},
|
order?: RecordOrderBy,
|
||||||
depth = 0,
|
depth = 0,
|
||||||
): T {
|
): T {
|
||||||
if (depth >= CONNECTION_MAX_DEPTH) {
|
if (depth >= CONNECTION_MAX_DEPTH) {
|
||||||
|
|||||||
@ -1,9 +1,9 @@
|
|||||||
import { isDefined } from 'class-validator';
|
import { isDefined } from 'class-validator';
|
||||||
import graphqlFields from 'graphql-fields';
|
import graphqlFields from 'graphql-fields';
|
||||||
import { FindManyOptions, ObjectLiteral } from 'typeorm';
|
|
||||||
|
|
||||||
import {
|
import {
|
||||||
Record as IRecord,
|
Record as IRecord,
|
||||||
|
OrderByDirection,
|
||||||
RecordFilter,
|
RecordFilter,
|
||||||
RecordOrderBy,
|
RecordOrderBy,
|
||||||
} from 'src/engine/api/graphql/workspace-query-builder/interfaces/record.interface';
|
} 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 { 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 { 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 { 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 { 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 { getObjectMetadataOrThrow } from 'src/engine/api/graphql/graphql-query-runner/utils/get-object-metadata-or-throw.util';
|
||||||
import {
|
import {
|
||||||
generateObjectMetadataMap,
|
|
||||||
ObjectMetadataMapItem,
|
ObjectMetadataMapItem,
|
||||||
|
generateObjectMetadataMap,
|
||||||
} from 'src/engine/metadata-modules/utils/generate-object-metadata-map.util';
|
} from 'src/engine/metadata-modules/utils/generate-object-metadata-map.util';
|
||||||
import { TwentyORMGlobalManager } from 'src/engine/twenty-orm/twenty-orm-global.manager';
|
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 {
|
export class GraphqlQueryFindManyResolverService {
|
||||||
private twentyORMGlobalManager: TwentyORMGlobalManager;
|
private twentyORMGlobalManager: TwentyORMGlobalManager;
|
||||||
@ -56,9 +57,19 @@ export class GraphqlQueryFindManyResolverService {
|
|||||||
const repository = dataSource.getRepository(
|
const repository = dataSource.getRepository(
|
||||||
objectMetadataItem.nameSingular,
|
objectMetadataItem.nameSingular,
|
||||||
);
|
);
|
||||||
|
|
||||||
|
const queryBuilder = repository.createQueryBuilder(
|
||||||
|
objectMetadataItem.nameSingular,
|
||||||
|
);
|
||||||
|
|
||||||
|
const countQueryBuilder = repository.createQueryBuilder(
|
||||||
|
objectMetadataItem.nameSingular,
|
||||||
|
);
|
||||||
|
|
||||||
const objectMetadataMap = generateObjectMetadataMap(
|
const objectMetadataMap = generateObjectMetadataMap(
|
||||||
objectMetadataCollection,
|
objectMetadataCollection,
|
||||||
);
|
);
|
||||||
|
|
||||||
const objectMetadata = getObjectMetadataOrThrow(
|
const objectMetadata = getObjectMetadataOrThrow(
|
||||||
objectMetadataMap,
|
objectMetadataMap,
|
||||||
objectMetadataItem.nameSingular,
|
objectMetadataItem.nameSingular,
|
||||||
@ -68,45 +79,82 @@ export class GraphqlQueryFindManyResolverService {
|
|||||||
objectMetadataMap,
|
objectMetadataMap,
|
||||||
);
|
);
|
||||||
|
|
||||||
|
const withFilterCountQueryBuilder = graphqlQueryParser.applyFilterToBuilder(
|
||||||
|
countQueryBuilder,
|
||||||
|
objectMetadataItem.nameSingular,
|
||||||
|
args.filter ?? ({} as Filter),
|
||||||
|
);
|
||||||
|
|
||||||
const selectedFields = graphqlFields(info);
|
const selectedFields = graphqlFields(info);
|
||||||
|
|
||||||
const { select, relations } = graphqlQueryParser.parseSelectedFields(
|
const { relations } = graphqlQueryParser.parseSelectedFields(
|
||||||
objectMetadataItem,
|
objectMetadataItem,
|
||||||
selectedFields,
|
selectedFields,
|
||||||
);
|
);
|
||||||
const isForwardPagination = !isDefined(args.before);
|
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;
|
const limit = args.first ?? args.last ?? QUERY_MAX_RECORDS;
|
||||||
|
|
||||||
this.addOrderByColumnsToSelect(order, select);
|
const withDeletedCountQueryBuilder =
|
||||||
this.addForeingKeyColumnsToSelect(relations, select, objectMetadata);
|
graphqlQueryParser.applyDeletedAtToBuilder(
|
||||||
|
withFilterCountQueryBuilder,
|
||||||
const findOptions: FindManyOptions<ObjectLiteral> = {
|
args.filter ?? ({} as Filter),
|
||||||
where,
|
);
|
||||||
order,
|
|
||||||
select,
|
|
||||||
take: limit + 1,
|
|
||||||
withDeleted,
|
|
||||||
};
|
|
||||||
|
|
||||||
const totalCount = isDefined(selectedFields.totalCount)
|
const totalCount = isDefined(selectedFields.totalCount)
|
||||||
? await repository.count({ where, withDeleted })
|
? await withDeletedCountQueryBuilder.getCount()
|
||||||
: 0;
|
: 0;
|
||||||
|
|
||||||
|
const cursor = this.getCursor(args);
|
||||||
|
|
||||||
|
let appliedFilters = args.filter ?? ({} as Filter);
|
||||||
|
|
||||||
|
const orderByWithIdCondition = [
|
||||||
|
...(args.orderBy ?? []),
|
||||||
|
{ id: OrderByDirection.AscNullsFirst },
|
||||||
|
] as OrderBy;
|
||||||
|
|
||||||
if (cursor) {
|
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(
|
const withFilterQueryBuilder = graphqlQueryParser.applyFilterToBuilder(
|
||||||
findOptions,
|
queryBuilder,
|
||||||
)) as ObjectRecord[];
|
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(
|
const { hasNextPage, hasPreviousPage } = this.getPaginationInfo(
|
||||||
objectRecords,
|
objectRecords,
|
||||||
@ -142,7 +190,7 @@ export class GraphqlQueryFindManyResolverService {
|
|||||||
objectMetadataItem.nameSingular,
|
objectMetadataItem.nameSingular,
|
||||||
limit,
|
limit,
|
||||||
totalCount,
|
totalCount,
|
||||||
order,
|
orderByWithIdCondition,
|
||||||
hasNextPage,
|
hasNextPage,
|
||||||
hasPreviousPage,
|
hasPreviousPage,
|
||||||
);
|
);
|
||||||
|
|||||||
@ -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 { 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 { generateObjectMetadataMap } from 'src/engine/metadata-modules/utils/generate-object-metadata-map.util';
|
||||||
import { TwentyORMGlobalManager } from 'src/engine/twenty-orm/twenty-orm-global.manager';
|
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 {
|
export class GraphqlQueryFindOneResolverService {
|
||||||
private twentyORMGlobalManager: TwentyORMGlobalManager;
|
private twentyORMGlobalManager: TwentyORMGlobalManager;
|
||||||
@ -35,12 +36,17 @@ export class GraphqlQueryFindOneResolverService {
|
|||||||
): Promise<ObjectRecord | undefined> {
|
): Promise<ObjectRecord | undefined> {
|
||||||
const { authContext, objectMetadataItem, info, objectMetadataCollection } =
|
const { authContext, objectMetadataItem, info, objectMetadataCollection } =
|
||||||
options;
|
options;
|
||||||
|
|
||||||
const dataSource =
|
const dataSource =
|
||||||
await this.twentyORMGlobalManager.getDataSourceForWorkspace(
|
await this.twentyORMGlobalManager.getDataSourceForWorkspace(
|
||||||
authContext.workspace.id,
|
authContext.workspace.id,
|
||||||
);
|
);
|
||||||
|
|
||||||
const repository = await dataSource.getRepository<ObjectRecord>(
|
const repository = dataSource.getRepository(
|
||||||
|
objectMetadataItem.nameSingular,
|
||||||
|
);
|
||||||
|
|
||||||
|
const queryBuilder = repository.createQueryBuilder(
|
||||||
objectMetadataItem.nameSingular,
|
objectMetadataItem.nameSingular,
|
||||||
);
|
);
|
||||||
|
|
||||||
@ -52,6 +58,7 @@ export class GraphqlQueryFindOneResolverService {
|
|||||||
objectMetadataMap,
|
objectMetadataMap,
|
||||||
objectMetadataItem.nameSingular,
|
objectMetadataItem.nameSingular,
|
||||||
);
|
);
|
||||||
|
|
||||||
const graphqlQueryParser = new GraphqlQueryParser(
|
const graphqlQueryParser = new GraphqlQueryParser(
|
||||||
objectMetadata.fields,
|
objectMetadata.fields,
|
||||||
objectMetadataMap,
|
objectMetadataMap,
|
||||||
@ -59,18 +66,29 @@ export class GraphqlQueryFindOneResolverService {
|
|||||||
|
|
||||||
const selectedFields = graphqlFields(info);
|
const selectedFields = graphqlFields(info);
|
||||||
|
|
||||||
const { select, relations } = graphqlQueryParser.parseSelectedFields(
|
const { relations } = graphqlQueryParser.parseSelectedFields(
|
||||||
objectMetadataItem,
|
objectMetadataItem,
|
||||||
selectedFields,
|
selectedFields,
|
||||||
);
|
);
|
||||||
const { parsedFilters: where, withDeleted } =
|
|
||||||
graphqlQueryParser.parseFilter(args.filter ?? ({} as Filter));
|
|
||||||
|
|
||||||
const objectRecord = (await repository.findOne({
|
const withFilterQueryBuilder = graphqlQueryParser.applyFilterToBuilder(
|
||||||
where,
|
queryBuilder,
|
||||||
select,
|
objectMetadataItem.nameSingular,
|
||||||
withDeleted,
|
args.filter ?? ({} as Filter),
|
||||||
})) as ObjectRecord;
|
);
|
||||||
|
|
||||||
|
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;
|
const limit = QUERY_MAX_RECORDS;
|
||||||
|
|
||||||
|
|||||||
@ -1,16 +0,0 @@
|
|||||||
import { FindOptionsWhere, LessThan, MoreThan, ObjectLiteral } from 'typeorm';
|
|
||||||
|
|
||||||
export const applyRangeFilter = (
|
|
||||||
where: FindOptionsWhere<ObjectLiteral>,
|
|
||||||
cursor: Record<string, any>,
|
|
||||||
isForwardPagination = true,
|
|
||||||
): FindOptionsWhere<ObjectLiteral> => {
|
|
||||||
Object.entries(cursor ?? {}).forEach(([key, value]) => {
|
|
||||||
if (key === 'id') {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
where[key] = isForwardPagination ? MoreThan(value) : LessThan(value);
|
|
||||||
});
|
|
||||||
|
|
||||||
return where;
|
|
||||||
};
|
|
||||||
@ -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<string, any>,
|
||||||
|
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;
|
||||||
|
});
|
||||||
|
};
|
||||||
@ -1,6 +1,7 @@
|
|||||||
import { FindOptionsOrderValue } from 'typeorm';
|
import {
|
||||||
|
Record as IRecord,
|
||||||
import { Record as IRecord } from 'src/engine/api/graphql/workspace-query-builder/interfaces/record.interface';
|
RecordOrderBy,
|
||||||
|
} from 'src/engine/api/graphql/workspace-query-builder/interfaces/record.interface';
|
||||||
|
|
||||||
import {
|
import {
|
||||||
GraphqlQueryRunnerException,
|
GraphqlQueryRunnerException,
|
||||||
@ -24,11 +25,15 @@ export const decodeCursor = (cursor: string): CursorData => {
|
|||||||
|
|
||||||
export const encodeCursor = <ObjectRecord extends IRecord = IRecord>(
|
export const encodeCursor = <ObjectRecord extends IRecord = IRecord>(
|
||||||
objectRecord: ObjectRecord,
|
objectRecord: ObjectRecord,
|
||||||
order: Record<string, FindOptionsOrderValue> | undefined,
|
order: RecordOrderBy | undefined,
|
||||||
): string => {
|
): string => {
|
||||||
const orderByValues: Record<string, any> = {};
|
const orderByValues: Record<string, any> = {};
|
||||||
|
|
||||||
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];
|
orderByValues[key] = objectRecord[key];
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
@ -1,5 +1,3 @@
|
|||||||
import { isPlainObject } from '@nestjs/common/utils/shared.utils';
|
|
||||||
|
|
||||||
import {
|
import {
|
||||||
DeepPartial,
|
DeepPartial,
|
||||||
DeleteResult,
|
DeleteResult,
|
||||||
@ -24,14 +22,10 @@ import { UpsertOptions } from 'typeorm/repository/UpsertOptions';
|
|||||||
|
|
||||||
import { WorkspaceInternalContext } from 'src/engine/twenty-orm/interfaces/workspace-internal-context.interface';
|
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 { ObjectMetadataMapItem } from 'src/engine/metadata-modules/utils/generate-object-metadata-map.util';
|
||||||
import { WorkspaceEntitiesStorage } from 'src/engine/twenty-orm/storage/workspace-entities.storage';
|
import { WorkspaceEntitiesStorage } from 'src/engine/twenty-orm/storage/workspace-entities.storage';
|
||||||
import { computeRelationType } from 'src/engine/twenty-orm/utils/compute-relation-type.util';
|
import { formatData } from 'src/engine/twenty-orm/utils/format-data.util';
|
||||||
import { isRelationFieldMetadataType } from 'src/engine/utils/is-relation-field-metadata-type.util';
|
import { formatResult } from 'src/engine/twenty-orm/utils/format-result.util';
|
||||||
|
|
||||||
export class WorkspaceRepository<
|
export class WorkspaceRepository<
|
||||||
Entity extends ObjectLiteral,
|
Entity extends ObjectLiteral,
|
||||||
@ -650,18 +644,6 @@ export class WorkspaceRepository<
|
|||||||
return objectMetadata;
|
return objectMetadata;
|
||||||
}
|
}
|
||||||
|
|
||||||
private async getCompositeFieldMetadataCollection(
|
|
||||||
objectMetadata: ObjectMetadataMapItem,
|
|
||||||
) {
|
|
||||||
const compositeFieldMetadataCollection = Object.values(
|
|
||||||
objectMetadata.fields,
|
|
||||||
).filter((fieldMetadata) =>
|
|
||||||
isCompositeFieldMetadataType(fieldMetadata.type),
|
|
||||||
);
|
|
||||||
|
|
||||||
return compositeFieldMetadataCollection;
|
|
||||||
}
|
|
||||||
|
|
||||||
private async transformOptions<
|
private async transformOptions<
|
||||||
T extends FindManyOptions<Entity> | FindOneOptions<Entity> | undefined,
|
T extends FindManyOptions<Entity> | FindOneOptions<Entity> | undefined,
|
||||||
>(options: T): Promise<T> {
|
>(options: T): Promise<T> {
|
||||||
@ -677,62 +659,9 @@ export class WorkspaceRepository<
|
|||||||
}
|
}
|
||||||
|
|
||||||
private async formatData<T>(data: T): Promise<T> {
|
private async formatData<T>(data: T): Promise<T> {
|
||||||
if (!data) {
|
|
||||||
return data;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (Array.isArray(data)) {
|
|
||||||
return Promise.all(
|
|
||||||
data.map((item) => this.formatData(item)),
|
|
||||||
) as Promise<T>;
|
|
||||||
}
|
|
||||||
|
|
||||||
const objectMetadata = await this.getObjectMetadataFromTarget();
|
const objectMetadata = await this.getObjectMetadataFromTarget();
|
||||||
|
|
||||||
const compositeFieldMetadataCollection =
|
return formatData(data, objectMetadata) as T;
|
||||||
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;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private async formatResult<T>(
|
private async formatResult<T>(
|
||||||
@ -741,124 +670,8 @@ export class WorkspaceRepository<
|
|||||||
): Promise<T> {
|
): Promise<T> {
|
||||||
objectMetadata ??= await this.getObjectMetadataFromTarget();
|
objectMetadata ??= await this.getObjectMetadataFromTarget();
|
||||||
|
|
||||||
if (!data) {
|
const objectMetadataMap = this.internalContext.objectMetadataMap;
|
||||||
return data;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (Array.isArray(data)) {
|
return formatResult(data, objectMetadata, objectMetadataMap) as T;
|
||||||
// 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<T>;
|
|
||||||
}
|
|
||||||
|
|
||||||
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;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -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<T>(
|
||||||
|
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;
|
||||||
|
}
|
||||||
@ -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<T>(
|
||||||
|
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;
|
||||||
|
}
|
||||||
@ -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;
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user