Improve performance on metadata computation (#12785)
In this PR: ## Improve recompute metadata cache performance. We are aiming for ~100ms Deleting relationMetadata table and FKs pointing on it Fetching indexMetadata and indexFieldMetadata in a separate query as typeorm is suboptimizing ## Remove caching lock As recomputing the metadata cache is lighter, we try to stop preventing multiple concurrent computations. This also simplifies interfaces ## Introduce self recovery mecanisms to recompute cache automatically if corrupted Aka getFreshObjectMetadataMaps ## custom object resolver performance improvement: 1sec to 200ms Double check queries and indexes used while creating a custom object Remove the queries to db to use the cached objectMetadataMap ## reduce objectMetadataMaps to 500kb <img width="222" alt="image" src="https://github.com/user-attachments/assets/2370dc80-49b6-4b63-8d5e-30c5ebdaa062" /> We used to stored 3 fieldMetadataMaps (byId, byName, byJoinColumnName). While this is great for devXP, this is not great for performances. Using the same mecanisme as for objectMetadataMap: we only keep byIdMap and introduce two otherMaps to idByName, idByJoinColumnName to make the bridge ## Add dataloader on IndexMetadata (aka indexMetadataList in the API) ## Improve field resolver performances too ## Deprecate ClientConfig
This commit is contained in:
@ -307,22 +307,34 @@ export const objectMetadataItemMock = {
|
||||
|
||||
export const objectMetadataMapItemMock = {
|
||||
id: 'mockObjectId',
|
||||
icon: 'Icon123',
|
||||
nameSingular: 'objectName',
|
||||
namePlural: 'objectsName',
|
||||
fields,
|
||||
fieldsById: fields.reduce((acc, field) => {
|
||||
// @ts-expect-error legacy noImplicitAny
|
||||
acc[field.id] = field;
|
||||
|
||||
return acc;
|
||||
}, {}),
|
||||
fieldsByName: fields.reduce((acc, field) => {
|
||||
fieldIdByName: fields.reduce((acc, field) => {
|
||||
// @ts-expect-error legacy noImplicitAny
|
||||
acc[field.name] = field;
|
||||
|
||||
return acc;
|
||||
}, {}),
|
||||
} as ObjectMetadataItemWithFieldMaps;
|
||||
fieldIdByJoinColumnName: {},
|
||||
labelSingular: 'Object',
|
||||
labelPlural: 'Objects',
|
||||
workspaceId: 'mockWorkspaceId',
|
||||
isCustom: false,
|
||||
isSystem: false,
|
||||
targetTableName: '',
|
||||
indexMetadatas: [],
|
||||
isActive: true,
|
||||
isRemote: false,
|
||||
isAuditLogged: false,
|
||||
isSearchable: false,
|
||||
} satisfies ObjectMetadataItemWithFieldMaps;
|
||||
|
||||
export const objectMetadataMapsMock = {
|
||||
byId: {
|
||||
|
||||
@ -3,10 +3,11 @@ import { FieldMetadataType } from 'twenty-shared/types';
|
||||
import { WorkspaceEntityDuplicateCriteria } from 'src/engine/api/graphql/workspace-query-builder/types/workspace-entity-duplicate-criteria.type';
|
||||
import { ObjectMetadataItemWithFieldMaps } from 'src/engine/metadata-modules/types/object-metadata-item-with-field-maps';
|
||||
|
||||
export const mockPersonObjectMetadata = (
|
||||
export const mockPersonObjectMetadataWithFieldMaps = (
|
||||
duplicateCriteria: WorkspaceEntityDuplicateCriteria[],
|
||||
): ObjectMetadataItemWithFieldMaps => ({
|
||||
id: '',
|
||||
icon: 'Icon123',
|
||||
standardId: '',
|
||||
nameSingular: 'person',
|
||||
namePlural: 'people',
|
||||
@ -24,12 +25,16 @@ export const mockPersonObjectMetadata = (
|
||||
labelIdentifierFieldMetadataId: '',
|
||||
imageIdentifierFieldMetadataId: '',
|
||||
workspaceId: '',
|
||||
fields: [],
|
||||
indexMetadatas: [],
|
||||
fieldsById: {},
|
||||
fieldsByJoinColumnName: {},
|
||||
fieldsByName: {
|
||||
name: {
|
||||
fieldIdByName: {
|
||||
name: 'name-id',
|
||||
emails: 'emails-id',
|
||||
linkedinLink: 'linkedinLink-id',
|
||||
jobTitle: 'jobTitle-id',
|
||||
},
|
||||
fieldIdByJoinColumnName: {},
|
||||
fieldsById: {
|
||||
'name-id': {
|
||||
id: '',
|
||||
objectMetadataId: '',
|
||||
type: FieldMetadataType.FULL_NAME,
|
||||
@ -44,8 +49,11 @@ export const mockPersonObjectMetadata = (
|
||||
isNullable: true,
|
||||
isUnique: false,
|
||||
workspaceId: '',
|
||||
isLabelSyncedWithName: true,
|
||||
createdAt: new Date(),
|
||||
updatedAt: new Date(),
|
||||
},
|
||||
emails: {
|
||||
'emails-id': {
|
||||
id: '',
|
||||
objectMetadataId: '',
|
||||
type: FieldMetadataType.EMAILS,
|
||||
@ -57,9 +65,13 @@ export const mockPersonObjectMetadata = (
|
||||
},
|
||||
description: 'Contact’s Emails',
|
||||
isCustom: false,
|
||||
isNullable: true,
|
||||
workspaceId: '',
|
||||
isLabelSyncedWithName: true,
|
||||
createdAt: new Date(),
|
||||
updatedAt: new Date(),
|
||||
},
|
||||
linkedinLink: {
|
||||
'linkedinLink-id': {
|
||||
id: '',
|
||||
objectMetadataId: '',
|
||||
type: FieldMetadataType.LINKS,
|
||||
@ -75,8 +87,11 @@ export const mockPersonObjectMetadata = (
|
||||
isNullable: true,
|
||||
isUnique: false,
|
||||
workspaceId: '',
|
||||
isLabelSyncedWithName: true,
|
||||
createdAt: new Date(),
|
||||
updatedAt: new Date(),
|
||||
},
|
||||
jobTitle: {
|
||||
'jobTitle-id': {
|
||||
id: '',
|
||||
objectMetadataId: '',
|
||||
type: FieldMetadataType.TEXT,
|
||||
@ -88,6 +103,9 @@ export const mockPersonObjectMetadata = (
|
||||
isNullable: false,
|
||||
isUnique: false,
|
||||
workspaceId: '20202020-1c25-4d02-bf25-6aeccf7ea419',
|
||||
isLabelSyncedWithName: true,
|
||||
createdAt: new Date(),
|
||||
updatedAt: new Date(),
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
@ -15,6 +15,7 @@ export enum GraphqlQueryRunnerExceptionCode {
|
||||
UNSUPPORTED_OPERATOR = 'UNSUPPORTED_OPERATOR',
|
||||
ARGS_CONFLICT = 'ARGS_CONFLICT',
|
||||
FIELD_NOT_FOUND = 'FIELD_NOT_FOUND',
|
||||
MISSING_SYSTEM_FIELD = 'MISSING_SYSTEM_FIELD',
|
||||
OBJECT_METADATA_NOT_FOUND = 'OBJECT_METADATA_NOT_FOUND',
|
||||
RECORD_NOT_FOUND = 'RECORD_NOT_FOUND',
|
||||
INVALID_ARGS_FIRST = 'INVALID_ARGS_FIRST',
|
||||
|
||||
@ -2,25 +2,19 @@ import { Brackets, NotBrackets, WhereExpressionBuilder } from 'typeorm';
|
||||
|
||||
import { ObjectRecordFilter } from 'src/engine/api/graphql/workspace-query-builder/interfaces/object-record.interface';
|
||||
|
||||
import { FieldMetadataMap } from 'src/engine/metadata-modules/types/field-metadata-map';
|
||||
import { ObjectMetadataItemWithFieldMaps } from 'src/engine/metadata-modules/types/object-metadata-item-with-field-maps';
|
||||
import { WorkspaceSelectQueryBuilder } from 'src/engine/twenty-orm/repository/workspace-select-query-builder';
|
||||
|
||||
import { GraphqlQueryFilterFieldParser } from './graphql-query-filter-field.parser';
|
||||
|
||||
export class GraphqlQueryFilterConditionParser {
|
||||
private fieldMetadataMapByName: FieldMetadataMap;
|
||||
private fieldMetadataMapByJoinColumnName: FieldMetadataMap;
|
||||
private objectMetadataMapItem: ObjectMetadataItemWithFieldMaps;
|
||||
private queryFilterFieldParser: GraphqlQueryFilterFieldParser;
|
||||
|
||||
constructor(
|
||||
fieldMetadataMapByName: FieldMetadataMap,
|
||||
fieldMetadataMapByJoinColumnName: FieldMetadataMap,
|
||||
) {
|
||||
this.fieldMetadataMapByName = fieldMetadataMapByName;
|
||||
this.fieldMetadataMapByJoinColumnName = fieldMetadataMapByJoinColumnName;
|
||||
constructor(objectMetadataMapItem: ObjectMetadataItemWithFieldMaps) {
|
||||
this.objectMetadataMapItem = objectMetadataMapItem;
|
||||
this.queryFilterFieldParser = new GraphqlQueryFilterFieldParser(
|
||||
this.fieldMetadataMapByName,
|
||||
this.fieldMetadataMapByJoinColumnName,
|
||||
this.objectMetadataMapItem,
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
@ -10,21 +10,16 @@ import {
|
||||
import { computeWhereConditionParts } from 'src/engine/api/graphql/graphql-query-runner/utils/compute-where-condition-parts';
|
||||
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/types/field-metadata-map';
|
||||
import { ObjectMetadataItemWithFieldMaps } from 'src/engine/metadata-modules/types/object-metadata-item-with-field-maps';
|
||||
import { CompositeFieldMetadataType } from 'src/engine/metadata-modules/workspace-migration/factories/composite-column-action.factory';
|
||||
|
||||
const ARRAY_OPERATORS = ['in', 'contains', 'notContains'];
|
||||
|
||||
export class GraphqlQueryFilterFieldParser {
|
||||
private fieldMetadataMapByName: FieldMetadataMap;
|
||||
private fieldMetadataMapByJoinColumnName: FieldMetadataMap;
|
||||
private objectMetadataMapItem: ObjectMetadataItemWithFieldMaps;
|
||||
|
||||
constructor(
|
||||
fieldMetadataMapByName: FieldMetadataMap,
|
||||
fieldMetadataMapByJoinColumnName: FieldMetadataMap,
|
||||
) {
|
||||
this.fieldMetadataMapByName = fieldMetadataMapByName;
|
||||
this.fieldMetadataMapByJoinColumnName = fieldMetadataMapByJoinColumnName;
|
||||
constructor(objectMetadataMapItem: ObjectMetadataItemWithFieldMaps) {
|
||||
this.objectMetadataMapItem = objectMetadataMapItem;
|
||||
}
|
||||
|
||||
public parse(
|
||||
@ -35,9 +30,12 @@ export class GraphqlQueryFilterFieldParser {
|
||||
filterValue: any,
|
||||
isFirst = false,
|
||||
): void {
|
||||
const fieldMetadataId =
|
||||
this.objectMetadataMapItem.fieldIdByName[`${key}`] ||
|
||||
this.objectMetadataMapItem.fieldIdByJoinColumnName[`${key}`];
|
||||
|
||||
const fieldMetadata =
|
||||
this.fieldMetadataMapByName[`${key}`] ||
|
||||
this.fieldMetadataMapByJoinColumnName[`${key}`];
|
||||
this.objectMetadataMapItem.fieldsById[fieldMetadataId];
|
||||
|
||||
if (!fieldMetadata) {
|
||||
throw new Error(`Field metadata not found for field: ${key}`);
|
||||
|
||||
@ -12,14 +12,14 @@ import {
|
||||
} 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/types/field-metadata-map';
|
||||
import { ObjectMetadataItemWithFieldMaps } from 'src/engine/metadata-modules/types/object-metadata-item-with-field-maps';
|
||||
import { CompositeFieldMetadataType } from 'src/engine/metadata-modules/workspace-migration/factories/composite-column-action.factory';
|
||||
|
||||
export class GraphqlQueryOrderFieldParser {
|
||||
private fieldMetadataMapByName: FieldMetadataMap;
|
||||
private objectMetadataMapItem: ObjectMetadataItemWithFieldMaps;
|
||||
|
||||
constructor(fieldMetadataMapByName: FieldMetadataMap) {
|
||||
this.fieldMetadataMapByName = fieldMetadataMapByName;
|
||||
constructor(objectMetadataMapItem: ObjectMetadataItemWithFieldMaps) {
|
||||
this.objectMetadataMapItem = objectMetadataMapItem;
|
||||
}
|
||||
|
||||
parse(
|
||||
@ -30,7 +30,9 @@ export class GraphqlQueryOrderFieldParser {
|
||||
return orderBy.reduce(
|
||||
(acc, item) => {
|
||||
Object.entries(item).forEach(([key, value]) => {
|
||||
const fieldMetadata = this.fieldMetadataMapByName[key];
|
||||
const fieldMetadataId = this.objectMetadataMapItem.fieldIdByName[key];
|
||||
const fieldMetadata =
|
||||
this.objectMetadataMapItem.fieldsById[fieldMetadataId];
|
||||
|
||||
if (!fieldMetadata || value === undefined) {
|
||||
throw new GraphqlQueryRunnerException(
|
||||
|
||||
@ -1,21 +1,20 @@
|
||||
import { FieldMetadataInterface } from 'src/engine/metadata-modules/field-metadata/interfaces/field-metadata.interface';
|
||||
|
||||
import { GraphqlQuerySelectedFieldsResult } from 'src/engine/api/graphql/graphql-query-runner/graphql-query-parsers/graphql-query-selected-fields/graphql-selected-fields.parser';
|
||||
import {
|
||||
AggregationField,
|
||||
getAvailableAggregationsFromObjectFields,
|
||||
} from 'src/engine/api/graphql/workspace-schema-builder/utils/get-available-aggregations-from-object-fields.util';
|
||||
import { ObjectMetadataItemWithFieldMaps } from 'src/engine/metadata-modules/types/object-metadata-item-with-field-maps';
|
||||
|
||||
export class GraphqlQuerySelectedFieldsAggregateParser {
|
||||
parse(
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
graphqlSelectedFields: Partial<Record<string, any>>,
|
||||
fieldMetadataMapByName: Record<string, FieldMetadataInterface>,
|
||||
objectMetadataMapItem: ObjectMetadataItemWithFieldMaps,
|
||||
accumulator: GraphqlQuerySelectedFieldsResult,
|
||||
): void {
|
||||
const availableAggregations: Record<string, AggregationField> =
|
||||
getAvailableAggregationsFromObjectFields(
|
||||
Object.values(fieldMetadataMapByName),
|
||||
Object.values(objectMetadataMapItem.fieldsById),
|
||||
);
|
||||
|
||||
for (const selectedField of Object.keys(graphqlSelectedFields)) {
|
||||
|
||||
@ -32,11 +32,13 @@ export class GraphqlQuerySelectedFieldsRelationParser {
|
||||
this.objectMetadataMaps,
|
||||
);
|
||||
|
||||
const targetFields = targetObjectMetadata.fieldsByName;
|
||||
const fieldParser = new GraphqlQuerySelectedFieldsParser(
|
||||
this.objectMetadataMaps,
|
||||
);
|
||||
const relationAccumulator = fieldParser.parse(fieldValue, targetFields);
|
||||
const relationAccumulator = fieldParser.parse(
|
||||
fieldValue,
|
||||
targetObjectMetadata,
|
||||
);
|
||||
|
||||
accumulator.select[fieldKey] = {
|
||||
id: true,
|
||||
|
||||
@ -6,6 +6,7 @@ import { GraphqlQuerySelectedFieldsAggregateParser } from 'src/engine/api/graphq
|
||||
import { GraphqlQuerySelectedFieldsRelationParser } from 'src/engine/api/graphql/graphql-query-runner/graphql-query-parsers/graphql-query-selected-fields/graphql-selected-fields-relation.parser';
|
||||
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 { ObjectMetadataItemWithFieldMaps } from 'src/engine/metadata-modules/types/object-metadata-item-with-field-maps';
|
||||
import { ObjectMetadataMaps } from 'src/engine/metadata-modules/types/object-metadata-maps';
|
||||
import { CompositeFieldMetadataType } from 'src/engine/metadata-modules/workspace-migration/factories/composite-column-action.factory';
|
||||
import { isRelationFieldMetadataType } from 'src/engine/utils/is-relation-field-metadata-type.util';
|
||||
@ -32,7 +33,7 @@ export class GraphqlQuerySelectedFieldsParser {
|
||||
parse(
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
graphqlSelectedFields: Partial<Record<string, any>>,
|
||||
fieldMetadataMapByName: Record<string, FieldMetadataInterface>,
|
||||
objectMetadataMapItem: ObjectMetadataItemWithFieldMaps,
|
||||
): GraphqlQuerySelectedFieldsResult {
|
||||
const accumulator: GraphqlQuerySelectedFieldsResult = {
|
||||
select: {},
|
||||
@ -43,7 +44,7 @@ export class GraphqlQuerySelectedFieldsParser {
|
||||
if (this.isRootConnection(graphqlSelectedFields)) {
|
||||
this.parseConnectionField(
|
||||
graphqlSelectedFields,
|
||||
fieldMetadataMapByName,
|
||||
objectMetadataMapItem,
|
||||
accumulator,
|
||||
);
|
||||
|
||||
@ -52,13 +53,13 @@ export class GraphqlQuerySelectedFieldsParser {
|
||||
|
||||
this.aggregateParser.parse(
|
||||
graphqlSelectedFields,
|
||||
fieldMetadataMapByName,
|
||||
objectMetadataMapItem,
|
||||
accumulator,
|
||||
);
|
||||
|
||||
this.parseRecordField(
|
||||
graphqlSelectedFields,
|
||||
fieldMetadataMapByName,
|
||||
objectMetadataMapItem,
|
||||
accumulator,
|
||||
);
|
||||
|
||||
@ -68,13 +69,16 @@ export class GraphqlQuerySelectedFieldsParser {
|
||||
private parseRecordField(
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
graphqlSelectedFields: Partial<Record<string, any>>,
|
||||
fieldMetadataMapByName: Record<string, FieldMetadataInterface>,
|
||||
objectMetadataMapItem: ObjectMetadataItemWithFieldMaps,
|
||||
accumulator: GraphqlQuerySelectedFieldsResult,
|
||||
): void {
|
||||
for (const [fieldKey, fieldValue] of Object.entries(
|
||||
graphqlSelectedFields,
|
||||
)) {
|
||||
const fieldMetadata = fieldMetadataMapByName[fieldKey];
|
||||
const fieldMetadata =
|
||||
objectMetadataMapItem.fieldsById[
|
||||
objectMetadataMapItem.fieldIdByName[fieldKey]
|
||||
];
|
||||
|
||||
if (!fieldMetadata) {
|
||||
continue;
|
||||
@ -103,18 +107,18 @@ export class GraphqlQuerySelectedFieldsParser {
|
||||
private parseConnectionField(
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
graphqlSelectedFields: Partial<Record<string, any>>,
|
||||
fieldMetadataMapByName: Record<string, FieldMetadataInterface>,
|
||||
objectMetadataMapItem: ObjectMetadataItemWithFieldMaps,
|
||||
accumulator: GraphqlQuerySelectedFieldsResult,
|
||||
): void {
|
||||
this.aggregateParser.parse(
|
||||
graphqlSelectedFields,
|
||||
fieldMetadataMapByName,
|
||||
objectMetadataMapItem,
|
||||
accumulator,
|
||||
);
|
||||
|
||||
const node = graphqlSelectedFields.edges.node;
|
||||
|
||||
this.parseRecordField(node, fieldMetadataMapByName, accumulator);
|
||||
this.parseRecordField(node, objectMetadataMapItem, accumulator);
|
||||
}
|
||||
|
||||
private isRootConnection(
|
||||
|
||||
@ -15,33 +15,29 @@ import {
|
||||
GraphqlQuerySelectedFieldsParser,
|
||||
GraphqlQuerySelectedFieldsResult,
|
||||
} from 'src/engine/api/graphql/graphql-query-runner/graphql-query-parsers/graphql-query-selected-fields/graphql-selected-fields.parser';
|
||||
import { FieldMetadataMap } from 'src/engine/metadata-modules/types/field-metadata-map';
|
||||
import { ObjectMetadataItemWithFieldMaps } from 'src/engine/metadata-modules/types/object-metadata-item-with-field-maps';
|
||||
import { ObjectMetadataMaps } from 'src/engine/metadata-modules/types/object-metadata-maps';
|
||||
import { getObjectMetadataMapItemByNameSingular } from 'src/engine/metadata-modules/utils/get-object-metadata-map-item-by-name-singular.util';
|
||||
import { WorkspaceSelectQueryBuilder } from 'src/engine/twenty-orm/repository/workspace-select-query-builder';
|
||||
|
||||
export class GraphqlQueryParser {
|
||||
private fieldMetadataMapByName: FieldMetadataMap;
|
||||
private fieldMetadataMapByJoinColumnName: FieldMetadataMap;
|
||||
private objectMetadataMapItem: ObjectMetadataItemWithFieldMaps;
|
||||
private objectMetadataMaps: ObjectMetadataMaps;
|
||||
private filterConditionParser: GraphqlQueryFilterConditionParser;
|
||||
private orderFieldParser: GraphqlQueryOrderFieldParser;
|
||||
|
||||
constructor(
|
||||
fieldMetadataMapByName: FieldMetadataMap,
|
||||
fieldMetadataMapByJoinColumnName: FieldMetadataMap,
|
||||
objectMetadataMapItem: ObjectMetadataItemWithFieldMaps,
|
||||
objectMetadataMaps: ObjectMetadataMaps,
|
||||
) {
|
||||
this.objectMetadataMapItem = objectMetadataMapItem;
|
||||
this.objectMetadataMaps = objectMetadataMaps;
|
||||
this.fieldMetadataMapByName = fieldMetadataMapByName;
|
||||
this.fieldMetadataMapByJoinColumnName = fieldMetadataMapByJoinColumnName;
|
||||
|
||||
this.filterConditionParser = new GraphqlQueryFilterConditionParser(
|
||||
this.fieldMetadataMapByName,
|
||||
this.fieldMetadataMapByJoinColumnName,
|
||||
this.objectMetadataMapItem,
|
||||
);
|
||||
this.orderFieldParser = new GraphqlQueryOrderFieldParser(
|
||||
this.fieldMetadataMapByName,
|
||||
this.objectMetadataMapItem,
|
||||
);
|
||||
}
|
||||
|
||||
@ -120,12 +116,12 @@ export class GraphqlQueryParser {
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
graphqlSelectedFields: Partial<Record<string, any>>,
|
||||
): GraphqlQuerySelectedFieldsResult {
|
||||
const parentFields = getObjectMetadataMapItemByNameSingular(
|
||||
const objectMetadataMapItem = getObjectMetadataMapItemByNameSingular(
|
||||
this.objectMetadataMaps,
|
||||
parentObjectMetadata.nameSingular,
|
||||
)?.fieldsByName;
|
||||
);
|
||||
|
||||
if (!parentFields) {
|
||||
if (!objectMetadataMapItem) {
|
||||
throw new GraphqlQueryRunnerException(
|
||||
`Could not find object metadata for ${parentObjectMetadata.nameSingular}`,
|
||||
GraphqlQueryRunnerExceptionCode.OBJECT_METADATA_NOT_FOUND,
|
||||
@ -136,6 +132,9 @@ export class GraphqlQueryParser {
|
||||
this.objectMetadataMaps,
|
||||
);
|
||||
|
||||
return selectedFieldsParser.parse(graphqlSelectedFields, parentFields);
|
||||
return selectedFieldsParser.parse(
|
||||
graphqlSelectedFields,
|
||||
objectMetadataMapItem,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@ -168,7 +168,8 @@ export class ObjectRecordsToGraphqlConnectionHelper {
|
||||
const processedObjectRecord: Record<string, any> = {};
|
||||
|
||||
for (const [key, value] of Object.entries(objectRecord)) {
|
||||
const fieldMetadata = objectMetadata.fieldsByName[key];
|
||||
const fieldMetadataId = objectMetadata.fieldIdByName[key];
|
||||
const fieldMetadata = objectMetadata.fieldsById[fieldMetadataId];
|
||||
|
||||
if (!fieldMetadata) {
|
||||
processedObjectRecord[key] = value;
|
||||
|
||||
@ -103,8 +103,10 @@ export class ProcessNestedRelationsV2Helper {
|
||||
shouldBypassPermissionChecks: boolean;
|
||||
roleId?: string;
|
||||
}): Promise<void> {
|
||||
const sourceFieldMetadataId =
|
||||
parentObjectMetadataItem.fieldIdByName[sourceFieldName];
|
||||
const sourceFieldMetadata =
|
||||
parentObjectMetadataItem.fieldsByName[sourceFieldName];
|
||||
parentObjectMetadataItem.fieldsById[sourceFieldMetadataId];
|
||||
|
||||
if (
|
||||
!isFieldMetadataInterfaceOfType(
|
||||
@ -219,8 +221,11 @@ export class ProcessNestedRelationsV2Helper {
|
||||
parentObjectMetadataItem: ObjectMetadataItemWithFieldMaps;
|
||||
sourceFieldName: string;
|
||||
}) {
|
||||
const targetFieldMetadataId =
|
||||
parentObjectMetadataItem.fieldIdByName[sourceFieldName];
|
||||
const targetFieldMetadata =
|
||||
parentObjectMetadataItem.fieldsByName[sourceFieldName];
|
||||
parentObjectMetadataItem.fieldsById[targetFieldMetadataId];
|
||||
|
||||
const targetObjectMetadata = getTargetObjectMetadataOrThrow(
|
||||
targetFieldMetadata,
|
||||
objectMetadataMaps,
|
||||
|
||||
@ -93,7 +93,6 @@ export abstract class GraphqlQueryBaseResolverService<
|
||||
const workspaceDataSource =
|
||||
await this.twentyORMGlobalManager.getDataSourceForWorkspace({
|
||||
workspaceId: workspace.id,
|
||||
shouldFailIfMetadataNotFound: false,
|
||||
});
|
||||
|
||||
const featureFlagsMap = workspaceDataSource.featureFlagMap;
|
||||
@ -132,8 +131,7 @@ export abstract class GraphqlQueryBaseResolverService<
|
||||
);
|
||||
|
||||
const graphqlQueryParser = new GraphqlQueryParser(
|
||||
objectMetadataItemWithFieldMaps.fieldsByName,
|
||||
objectMetadataItemWithFieldMaps.fieldsByJoinColumnName,
|
||||
objectMetadataItemWithFieldMaps,
|
||||
options.objectMetadataMaps,
|
||||
);
|
||||
|
||||
|
||||
@ -12,6 +12,10 @@ import { ObjectRecord } from 'src/engine/api/graphql/workspace-query-builder/int
|
||||
import { WorkspaceQueryRunnerOptions } from 'src/engine/api/graphql/workspace-query-runner/interfaces/query-runner-option.interface';
|
||||
import { CreateManyResolverArgs } from 'src/engine/api/graphql/workspace-resolver-builder/interfaces/workspace-resolvers-builder.interface';
|
||||
|
||||
import {
|
||||
GraphqlQueryRunnerException,
|
||||
GraphqlQueryRunnerExceptionCode,
|
||||
} from 'src/engine/api/graphql/graphql-query-runner/errors/graphql-query-runner.exception';
|
||||
import { ObjectRecordsToGraphqlConnectionHelper } from 'src/engine/api/graphql/graphql-query-runner/helpers/object-records-to-graphql-connection.helper';
|
||||
import { assertIsValidUuid } from 'src/engine/api/graphql/workspace-query-runner/utils/assert-is-valid-uuid.util';
|
||||
import { AuthContext } from 'src/engine/core-modules/auth/types/auth-context.type';
|
||||
@ -19,6 +23,7 @@ import { compositeTypeDefinitions } from 'src/engine/metadata-modules/field-meta
|
||||
import { assertMutationNotOnRemoteObject } from 'src/engine/metadata-modules/object-metadata/utils/assert-mutation-not-on-remote-object.util';
|
||||
import { ObjectMetadataItemWithFieldMaps } from 'src/engine/metadata-modules/types/object-metadata-item-with-field-maps';
|
||||
import { ObjectMetadataMaps } from 'src/engine/metadata-modules/types/object-metadata-maps';
|
||||
import { getObjectMetadataFromObjectMetadataItemWithFieldMaps } from 'src/engine/metadata-modules/utils/get-object-metadata-from-object-metadata-Item-with-field-maps';
|
||||
import { WorkspaceRepository } from 'src/engine/twenty-orm/repository/workspace.repository';
|
||||
import { formatData } from 'src/engine/twenty-orm/utils/format-data.util';
|
||||
import { formatResult } from 'src/engine/twenty-orm/utils/format-result.util';
|
||||
@ -128,7 +133,7 @@ export class GraphqlQueryCreateManyResolverService extends GraphqlQueryBaseResol
|
||||
fullPath: string;
|
||||
column: string;
|
||||
}[] {
|
||||
return objectMetadataItemWithFieldMaps.fields
|
||||
return Object.values(objectMetadataItemWithFieldMaps.fieldsById)
|
||||
.filter((field) => field.isUnique || field.name === 'id')
|
||||
.flatMap((field) => {
|
||||
const compositeType = compositeTypeDefinitions.get(field.type);
|
||||
@ -330,7 +335,10 @@ export class GraphqlQueryCreateManyResolverService extends GraphqlQueryBaseResol
|
||||
records: structuredClone([record]),
|
||||
updatedFields: Object.keys(formattedPartialRecordToUpdate),
|
||||
authContext,
|
||||
objectMetadataItem: objectMetadataItemWithFieldMaps,
|
||||
objectMetadataItem:
|
||||
getObjectMetadataFromObjectMetadataItemWithFieldMaps(
|
||||
objectMetadataItemWithFieldMaps,
|
||||
),
|
||||
});
|
||||
}
|
||||
}
|
||||
@ -373,7 +381,9 @@ export class GraphqlQueryCreateManyResolverService extends GraphqlQueryBaseResol
|
||||
this.apiEventEmitterService.emitCreateEvents({
|
||||
records: structuredClone(formattedInsertedRecords),
|
||||
authContext,
|
||||
objectMetadataItem: objectMetadataItemWithFieldMaps,
|
||||
objectMetadataItem: getObjectMetadataFromObjectMetadataItemWithFieldMaps(
|
||||
objectMetadataItemWithFieldMaps,
|
||||
),
|
||||
});
|
||||
}
|
||||
|
||||
@ -450,11 +460,19 @@ export class GraphqlQueryCreateManyResolverService extends GraphqlQueryBaseResol
|
||||
) {
|
||||
let recordWithoutCreatedByUpdate = record;
|
||||
|
||||
if (
|
||||
'createdBy' in record &&
|
||||
objectMetadataItemWithFieldMaps.fieldsByName['createdBy']?.isCustom ===
|
||||
false
|
||||
) {
|
||||
const createdByFieldMetadataId =
|
||||
objectMetadataItemWithFieldMaps.fieldIdByName['createdBy'];
|
||||
const createdByFieldMetadata =
|
||||
objectMetadataItemWithFieldMaps.fieldsById[createdByFieldMetadataId];
|
||||
|
||||
if (!isDefined(createdByFieldMetadata)) {
|
||||
throw new GraphqlQueryRunnerException(
|
||||
`Missing createdBy field metadata for object ${objectMetadataItemWithFieldMaps.nameSingular}`,
|
||||
GraphqlQueryRunnerExceptionCode.MISSING_SYSTEM_FIELD,
|
||||
);
|
||||
}
|
||||
|
||||
if ('createdBy' in record && createdByFieldMetadata.isCustom === false) {
|
||||
const { createdBy: _createdBy, ...recordWithoutCreatedBy } = record;
|
||||
|
||||
recordWithoutCreatedByUpdate = recordWithoutCreatedBy;
|
||||
|
||||
@ -14,6 +14,7 @@ import { CreateOneResolverArgs } from 'src/engine/api/graphql/workspace-resolver
|
||||
import { ObjectRecordsToGraphqlConnectionHelper } from 'src/engine/api/graphql/graphql-query-runner/helpers/object-records-to-graphql-connection.helper';
|
||||
import { assertIsValidUuid } from 'src/engine/api/graphql/workspace-query-runner/utils/assert-is-valid-uuid.util';
|
||||
import { assertMutationNotOnRemoteObject } from 'src/engine/metadata-modules/object-metadata/utils/assert-mutation-not-on-remote-object.util';
|
||||
import { getObjectMetadataFromObjectMetadataItemWithFieldMaps } from 'src/engine/metadata-modules/utils/get-object-metadata-from-object-metadata-Item-with-field-maps';
|
||||
import { formatResult } from 'src/engine/twenty-orm/utils/format-result.util';
|
||||
|
||||
@Injectable()
|
||||
@ -56,7 +57,9 @@ export class GraphqlQueryCreateOneResolverService extends GraphqlQueryBaseResolv
|
||||
this.apiEventEmitterService.emitCreateEvents({
|
||||
records: structuredClone(upsertedRecords),
|
||||
authContext,
|
||||
objectMetadataItem: objectMetadataItemWithFieldMaps,
|
||||
objectMetadataItem: getObjectMetadataFromObjectMetadataItemWithFieldMaps(
|
||||
objectMetadataItemWithFieldMaps,
|
||||
),
|
||||
});
|
||||
|
||||
if (executionArgs.graphqlQuerySelectedFieldsResult.relations) {
|
||||
|
||||
@ -13,6 +13,7 @@ import { DeleteManyResolverArgs } from 'src/engine/api/graphql/workspace-resolve
|
||||
import { ObjectRecordsToGraphqlConnectionHelper } from 'src/engine/api/graphql/graphql-query-runner/helpers/object-records-to-graphql-connection.helper';
|
||||
import { assertIsValidUuid } from 'src/engine/api/graphql/workspace-query-runner/utils/assert-is-valid-uuid.util';
|
||||
import { assertMutationNotOnRemoteObject } from 'src/engine/metadata-modules/object-metadata/utils/assert-mutation-not-on-remote-object.util';
|
||||
import { getObjectMetadataFromObjectMetadataItemWithFieldMaps } from 'src/engine/metadata-modules/utils/get-object-metadata-from-object-metadata-Item-with-field-maps';
|
||||
import { formatResult } from 'src/engine/twenty-orm/utils/format-result.util';
|
||||
import { computeTableName } from 'src/engine/utils/compute-table-name.util';
|
||||
|
||||
@ -58,7 +59,9 @@ export class GraphqlQueryDeleteManyResolverService extends GraphqlQueryBaseResol
|
||||
this.apiEventEmitterService.emitDeletedEvents({
|
||||
records: structuredClone(formattedDeletedRecords),
|
||||
authContext,
|
||||
objectMetadataItem: objectMetadataItemWithFieldMaps,
|
||||
objectMetadataItem: getObjectMetadataFromObjectMetadataItemWithFieldMaps(
|
||||
objectMetadataItemWithFieldMaps,
|
||||
),
|
||||
});
|
||||
|
||||
if (executionArgs.graphqlQuerySelectedFieldsResult.relations) {
|
||||
|
||||
@ -18,6 +18,7 @@ import { ObjectRecordsToGraphqlConnectionHelper } from 'src/engine/api/graphql/g
|
||||
import { assertIsValidUuid } from 'src/engine/api/graphql/workspace-query-runner/utils/assert-is-valid-uuid.util';
|
||||
import { assertMutationNotOnRemoteObject } from 'src/engine/metadata-modules/object-metadata/utils/assert-mutation-not-on-remote-object.util';
|
||||
import { formatResult } from 'src/engine/twenty-orm/utils/format-result.util';
|
||||
import { getObjectMetadataFromObjectMetadataItemWithFieldMaps } from 'src/engine/metadata-modules/utils/get-object-metadata-from-object-metadata-Item-with-field-maps';
|
||||
|
||||
@Injectable()
|
||||
export class GraphqlQueryDeleteOneResolverService extends GraphqlQueryBaseResolverService<
|
||||
@ -60,7 +61,9 @@ export class GraphqlQueryDeleteOneResolverService extends GraphqlQueryBaseResolv
|
||||
this.apiEventEmitterService.emitDeletedEvents({
|
||||
records: structuredClone(formattedDeletedRecords),
|
||||
authContext,
|
||||
objectMetadataItem: objectMetadataItemWithFieldMaps,
|
||||
objectMetadataItem: getObjectMetadataFromObjectMetadataItemWithFieldMaps(
|
||||
objectMetadataItemWithFieldMaps,
|
||||
),
|
||||
});
|
||||
|
||||
if (executionArgs.graphqlQuerySelectedFieldsResult.relations) {
|
||||
|
||||
@ -11,6 +11,7 @@ import { WorkspaceQueryRunnerOptions } from 'src/engine/api/graphql/workspace-qu
|
||||
import { DestroyManyResolverArgs } from 'src/engine/api/graphql/workspace-resolver-builder/interfaces/workspace-resolvers-builder.interface';
|
||||
|
||||
import { ObjectRecordsToGraphqlConnectionHelper } from 'src/engine/api/graphql/graphql-query-runner/helpers/object-records-to-graphql-connection.helper';
|
||||
import { getObjectMetadataFromObjectMetadataItemWithFieldMaps } from 'src/engine/metadata-modules/utils/get-object-metadata-from-object-metadata-Item-with-field-maps';
|
||||
import { formatResult } from 'src/engine/twenty-orm/utils/format-result.util';
|
||||
import { computeTableName } from 'src/engine/utils/compute-table-name.util';
|
||||
|
||||
@ -56,7 +57,9 @@ export class GraphqlQueryDestroyManyResolverService extends GraphqlQueryBaseReso
|
||||
this.apiEventEmitterService.emitDestroyEvents({
|
||||
records: structuredClone(deletedRecords),
|
||||
authContext,
|
||||
objectMetadataItem: objectMetadataItemWithFieldMaps,
|
||||
objectMetadataItem: getObjectMetadataFromObjectMetadataItemWithFieldMaps(
|
||||
objectMetadataItemWithFieldMaps,
|
||||
),
|
||||
});
|
||||
|
||||
if (executionArgs.graphqlQuerySelectedFieldsResult.relations) {
|
||||
|
||||
@ -15,6 +15,7 @@ import {
|
||||
GraphqlQueryRunnerExceptionCode,
|
||||
} from 'src/engine/api/graphql/graphql-query-runner/errors/graphql-query-runner.exception';
|
||||
import { ObjectRecordsToGraphqlConnectionHelper } from 'src/engine/api/graphql/graphql-query-runner/helpers/object-records-to-graphql-connection.helper';
|
||||
import { getObjectMetadataFromObjectMetadataItemWithFieldMaps } from 'src/engine/metadata-modules/utils/get-object-metadata-from-object-metadata-Item-with-field-maps';
|
||||
import { formatResult } from 'src/engine/twenty-orm/utils/format-result.util';
|
||||
|
||||
@Injectable()
|
||||
@ -56,7 +57,9 @@ export class GraphqlQueryDestroyOneResolverService extends GraphqlQueryBaseResol
|
||||
this.apiEventEmitterService.emitDestroyEvents({
|
||||
records: structuredClone(deletedRecords),
|
||||
authContext,
|
||||
objectMetadataItem: objectMetadataItemWithFieldMaps,
|
||||
objectMetadataItem: getObjectMetadataFromObjectMetadataItemWithFieldMaps(
|
||||
objectMetadataItemWithFieldMaps,
|
||||
),
|
||||
});
|
||||
|
||||
if (executionArgs.graphqlQuerySelectedFieldsResult.relations) {
|
||||
|
||||
@ -21,10 +21,10 @@ import {
|
||||
} from 'src/engine/api/graphql/graphql-query-runner/errors/graphql-query-runner.exception';
|
||||
import { GraphqlQueryParser } from 'src/engine/api/graphql/graphql-query-runner/graphql-query-parsers/graphql-query.parser';
|
||||
import { ObjectRecordsToGraphqlConnectionHelper } from 'src/engine/api/graphql/graphql-query-runner/helpers/object-records-to-graphql-connection.helper';
|
||||
import { buildDuplicateConditions } from 'src/engine/api/utils/build-duplicate-conditions.utils';
|
||||
import { getObjectMetadataMapItemByNameSingular } from 'src/engine/metadata-modules/utils/get-object-metadata-map-item-by-name-singular.util';
|
||||
import { formatData } from 'src/engine/twenty-orm/utils/format-data.util';
|
||||
import { formatResult } from 'src/engine/twenty-orm/utils/format-result.util';
|
||||
import { buildDuplicateConditions } from 'src/engine/api/utils/build-duplicate-conditions.utils';
|
||||
|
||||
@Injectable()
|
||||
export class GraphqlQueryFindDuplicatesResolverService extends GraphqlQueryBaseResolverService<
|
||||
@ -56,8 +56,7 @@ export class GraphqlQueryFindDuplicatesResolverService extends GraphqlQueryBaseR
|
||||
}
|
||||
|
||||
const graphqlQueryParser = new GraphqlQueryParser(
|
||||
objectMetadataItemWithFieldsMaps?.fieldsByName,
|
||||
objectMetadataItemWithFieldsMaps?.fieldsByJoinColumnName,
|
||||
objectMetadataItemWithFieldMaps,
|
||||
objectMetadataMaps,
|
||||
);
|
||||
|
||||
|
||||
@ -80,7 +80,7 @@ export class GraphqlQueryFindManyResolverService extends GraphqlQueryBaseResolve
|
||||
const cursorArgFilter = computeCursorArgFilter(
|
||||
cursor,
|
||||
orderByWithIdCondition,
|
||||
objectMetadataItemWithFieldMaps.fieldsByName,
|
||||
objectMetadataItemWithFieldMaps,
|
||||
isForwardPagination,
|
||||
);
|
||||
|
||||
|
||||
@ -15,6 +15,7 @@ import { assertIsValidUuid } from 'src/engine/api/graphql/workspace-query-runner
|
||||
import { assertMutationNotOnRemoteObject } from 'src/engine/metadata-modules/object-metadata/utils/assert-mutation-not-on-remote-object.util';
|
||||
import { formatResult } from 'src/engine/twenty-orm/utils/format-result.util';
|
||||
import { computeTableName } from 'src/engine/utils/compute-table-name.util';
|
||||
import { getObjectMetadataFromObjectMetadataItemWithFieldMaps } from 'src/engine/metadata-modules/utils/get-object-metadata-from-object-metadata-Item-with-field-maps';
|
||||
|
||||
@Injectable()
|
||||
export class GraphqlQueryRestoreManyResolverService extends GraphqlQueryBaseResolverService<
|
||||
@ -58,7 +59,9 @@ export class GraphqlQueryRestoreManyResolverService extends GraphqlQueryBaseReso
|
||||
this.apiEventEmitterService.emitRestoreEvents({
|
||||
records: structuredClone(formattedRestoredRecords),
|
||||
authContext,
|
||||
objectMetadataItem: objectMetadataItemWithFieldMaps,
|
||||
objectMetadataItem: getObjectMetadataFromObjectMetadataItemWithFieldMaps(
|
||||
objectMetadataItemWithFieldMaps,
|
||||
),
|
||||
});
|
||||
|
||||
if (executionArgs.graphqlQuerySelectedFieldsResult.relations) {
|
||||
|
||||
@ -17,6 +17,7 @@ import {
|
||||
import { ObjectRecordsToGraphqlConnectionHelper } from 'src/engine/api/graphql/graphql-query-runner/helpers/object-records-to-graphql-connection.helper';
|
||||
import { assertIsValidUuid } from 'src/engine/api/graphql/workspace-query-runner/utils/assert-is-valid-uuid.util';
|
||||
import { assertMutationNotOnRemoteObject } from 'src/engine/metadata-modules/object-metadata/utils/assert-mutation-not-on-remote-object.util';
|
||||
import { getObjectMetadataFromObjectMetadataItemWithFieldMaps } from 'src/engine/metadata-modules/utils/get-object-metadata-from-object-metadata-Item-with-field-maps';
|
||||
import { formatResult } from 'src/engine/twenty-orm/utils/format-result.util';
|
||||
|
||||
@Injectable()
|
||||
@ -60,7 +61,9 @@ export class GraphqlQueryRestoreOneResolverService extends GraphqlQueryBaseResol
|
||||
this.apiEventEmitterService.emitRestoreEvents({
|
||||
records: structuredClone(formattedRestoredRecords),
|
||||
authContext,
|
||||
objectMetadataItem: objectMetadataItemWithFieldMaps,
|
||||
objectMetadataItem: getObjectMetadataFromObjectMetadataItemWithFieldMaps(
|
||||
objectMetadataItemWithFieldMaps,
|
||||
),
|
||||
});
|
||||
|
||||
if (executionArgs.graphqlQuerySelectedFieldsResult.relations) {
|
||||
|
||||
@ -21,6 +21,7 @@ import { assertMutationNotOnRemoteObject } from 'src/engine/metadata-modules/obj
|
||||
import { formatData } from 'src/engine/twenty-orm/utils/format-data.util';
|
||||
import { formatResult } from 'src/engine/twenty-orm/utils/format-result.util';
|
||||
import { computeTableName } from 'src/engine/utils/compute-table-name.util';
|
||||
import { getObjectMetadataFromObjectMetadataItemWithFieldMaps } from 'src/engine/metadata-modules/utils/get-object-metadata-from-object-metadata-Item-with-field-maps';
|
||||
|
||||
@Injectable()
|
||||
export class GraphqlQueryUpdateManyResolverService extends GraphqlQueryBaseResolverService<
|
||||
@ -94,7 +95,9 @@ export class GraphqlQueryUpdateManyResolverService extends GraphqlQueryBaseResol
|
||||
records: structuredClone(formattedUpdatedRecords),
|
||||
updatedFields: Object.keys(executionArgs.args.data),
|
||||
authContext,
|
||||
objectMetadataItem: objectMetadataItemWithFieldMaps,
|
||||
objectMetadataItem: getObjectMetadataFromObjectMetadataItemWithFieldMaps(
|
||||
objectMetadataItemWithFieldMaps,
|
||||
),
|
||||
});
|
||||
|
||||
if (executionArgs.graphqlQuerySelectedFieldsResult.relations) {
|
||||
|
||||
@ -18,6 +18,7 @@ import {
|
||||
import { ObjectRecordsToGraphqlConnectionHelper } from 'src/engine/api/graphql/graphql-query-runner/helpers/object-records-to-graphql-connection.helper';
|
||||
import { assertIsValidUuid } from 'src/engine/api/graphql/workspace-query-runner/utils/assert-is-valid-uuid.util';
|
||||
import { assertMutationNotOnRemoteObject } from 'src/engine/metadata-modules/object-metadata/utils/assert-mutation-not-on-remote-object.util';
|
||||
import { getObjectMetadataFromObjectMetadataItemWithFieldMaps } from 'src/engine/metadata-modules/utils/get-object-metadata-from-object-metadata-Item-with-field-maps';
|
||||
import { formatData } from 'src/engine/twenty-orm/utils/format-data.util';
|
||||
import { formatResult } from 'src/engine/twenty-orm/utils/format-result.util';
|
||||
|
||||
@ -89,7 +90,9 @@ export class GraphqlQueryUpdateOneResolverService extends GraphqlQueryBaseResolv
|
||||
records: structuredClone(formattedUpdatedRecords),
|
||||
updatedFields: Object.keys(executionArgs.args.data),
|
||||
authContext,
|
||||
objectMetadataItem: objectMetadataItemWithFieldMaps,
|
||||
objectMetadataItem: getObjectMetadataFromObjectMetadataItemWithFieldMaps(
|
||||
objectMetadataItemWithFieldMaps,
|
||||
),
|
||||
});
|
||||
|
||||
if (executionArgs.graphqlQuerySelectedFieldsResult.relations) {
|
||||
|
||||
@ -23,40 +23,29 @@ describe('QueryRunnerArgsFactory', () => {
|
||||
objectMetadataItemWithFieldMaps: {
|
||||
isCustom: true,
|
||||
nameSingular: 'testNumber',
|
||||
fields: [
|
||||
{
|
||||
fieldsById: {
|
||||
'position-id': {
|
||||
type: FieldMetadataType.POSITION,
|
||||
isCustom: true,
|
||||
name: 'position',
|
||||
},
|
||||
{
|
||||
'testNumber-id': {
|
||||
type: FieldMetadataType.NUMBER,
|
||||
isCustom: true,
|
||||
name: 'testNumber',
|
||||
},
|
||||
{
|
||||
type: FieldMetadataType.TEXT,
|
||||
isCustom: true,
|
||||
name: 'otherField',
|
||||
},
|
||||
],
|
||||
fieldsByName: {
|
||||
position: {
|
||||
type: FieldMetadataType.POSITION,
|
||||
isCustom: true,
|
||||
name: 'position',
|
||||
},
|
||||
testNumber: {
|
||||
type: FieldMetadataType.NUMBER,
|
||||
isCustom: true,
|
||||
name: 'testNumber',
|
||||
},
|
||||
otherField: {
|
||||
'otherField-id': {
|
||||
type: FieldMetadataType.TEXT,
|
||||
isCustom: true,
|
||||
name: 'otherField',
|
||||
},
|
||||
} as unknown as FieldMetadataMap,
|
||||
fieldIdByName: {
|
||||
position: 'position-id',
|
||||
testNumber: 'testNumber-id',
|
||||
otherField: 'otherField-id',
|
||||
},
|
||||
fieldIdByJoinColumnName: {},
|
||||
},
|
||||
} as unknown as WorkspaceQueryRunnerOptions;
|
||||
|
||||
|
||||
@ -8,7 +8,6 @@ import { QueryResultFieldValue } from 'src/engine/api/graphql/workspace-query-ru
|
||||
import { QueryResultGetterHandlerInterface } from 'src/engine/api/graphql/workspace-query-runner/factories/query-result-getters/interfaces/query-result-getter-handler.interface';
|
||||
import { IConnection } from 'src/engine/api/graphql/workspace-query-runner/interfaces/connection.interface';
|
||||
import { IEdge } from 'src/engine/api/graphql/workspace-query-runner/interfaces/edge.interface';
|
||||
import { ObjectMetadataInterface } from 'src/engine/metadata-modules/field-metadata/interfaces/object-metadata.interface';
|
||||
|
||||
import { isQueryResultFieldValueAConnection } from 'src/engine/api/graphql/workspace-query-runner/factories/query-result-getters/guards/is-query-result-field-value-a-connection.guard';
|
||||
import { isQueryResultFieldValueANestedRecordArray } from 'src/engine/api/graphql/workspace-query-runner/factories/query-result-getters/guards/is-query-result-field-value-a-nested-record-array.guard';
|
||||
@ -22,6 +21,7 @@ import { CompositeInputTypeDefinitionFactory } from 'src/engine/api/graphql/work
|
||||
import { FileService } from 'src/engine/core-modules/file/services/file.service';
|
||||
import { ObjectMetadataMaps } from 'src/engine/metadata-modules/types/object-metadata-maps';
|
||||
import { isFieldMetadataInterfaceOfType } from 'src/engine/utils/is-field-metadata-of-type.util';
|
||||
import { ObjectMetadataItemWithFieldMaps } from 'src/engine/metadata-modules/types/object-metadata-item-with-field-maps';
|
||||
|
||||
// TODO: find a way to prevent conflict between handlers executing logic on object relations
|
||||
// And this factory that is also executing logic on object relations
|
||||
@ -126,7 +126,9 @@ export class QueryResultGettersFactory {
|
||||
const relationFields = Object.keys(record)
|
||||
.map(
|
||||
(recordFieldName) =>
|
||||
objectMetadataMapItem.fieldsByName[recordFieldName],
|
||||
objectMetadataMapItem.fieldsById[
|
||||
objectMetadataMapItem.fieldIdByName[recordFieldName]
|
||||
],
|
||||
)
|
||||
.filter(isDefined)
|
||||
.filter((fieldMetadata) =>
|
||||
@ -214,7 +216,7 @@ export class QueryResultGettersFactory {
|
||||
|
||||
async create(
|
||||
result: QueryResultFieldValue,
|
||||
objectMetadataItem: ObjectMetadataInterface,
|
||||
objectMetadataItem: ObjectMetadataItemWithFieldMaps,
|
||||
workspaceId: string,
|
||||
objectMetadataMaps: ObjectMetadataMaps,
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
|
||||
@ -19,12 +19,12 @@ import {
|
||||
UpdateManyResolverArgs,
|
||||
UpdateOneResolverArgs,
|
||||
} from 'src/engine/api/graphql/workspace-resolver-builder/interfaces/workspace-resolvers-builder.interface';
|
||||
import { FieldMetadataInterface } from 'src/engine/metadata-modules/field-metadata/interfaces/field-metadata.interface';
|
||||
|
||||
import { RecordPositionService } from 'src/engine/core-modules/record-position/services/record-position.service';
|
||||
import { RecordInputTransformerService } from 'src/engine/core-modules/record-transformer/services/record-input-transformer.service';
|
||||
import { workspaceValidator } from 'src/engine/core-modules/workspace/workspace.validate';
|
||||
import { FieldMetadataMap } from 'src/engine/metadata-modules/types/field-metadata-map';
|
||||
import { ObjectMetadataItemWithFieldMaps } from 'src/engine/metadata-modules/types/object-metadata-item-with-field-maps';
|
||||
|
||||
type ArgPositionBackfillInput = {
|
||||
argIndex?: number;
|
||||
@ -44,14 +44,14 @@ export class QueryRunnerArgsFactory {
|
||||
resolverArgsType: ResolverArgsType,
|
||||
) {
|
||||
const fieldMetadataMapByNameByName =
|
||||
options.objectMetadataItemWithFieldMaps.fieldsByName;
|
||||
options.objectMetadataItemWithFieldMaps.fieldsById;
|
||||
|
||||
const shouldBackfillPosition =
|
||||
options.objectMetadataItemWithFieldMaps.fields.some(
|
||||
(field) =>
|
||||
field.type === FieldMetadataType.POSITION &&
|
||||
field.name === 'position',
|
||||
);
|
||||
const shouldBackfillPosition = Object.values(
|
||||
options.objectMetadataItemWithFieldMaps.fieldsById,
|
||||
).some(
|
||||
(field) =>
|
||||
field.type === FieldMetadataType.POSITION && field.name === 'position',
|
||||
);
|
||||
|
||||
switch (resolverArgsType) {
|
||||
case ResolverArgsType.CreateOne:
|
||||
@ -60,7 +60,6 @@ export class QueryRunnerArgsFactory {
|
||||
data: await this.overrideDataByFieldMetadata(
|
||||
(args as CreateOneResolverArgs).data,
|
||||
options,
|
||||
fieldMetadataMapByNameByName,
|
||||
{
|
||||
argIndex: 0,
|
||||
shouldBackfillPosition,
|
||||
@ -72,15 +71,10 @@ export class QueryRunnerArgsFactory {
|
||||
...args,
|
||||
data: await Promise.all(
|
||||
(args as CreateManyResolverArgs).data?.map((arg, index) =>
|
||||
this.overrideDataByFieldMetadata(
|
||||
arg,
|
||||
options,
|
||||
fieldMetadataMapByNameByName,
|
||||
{
|
||||
argIndex: index,
|
||||
shouldBackfillPosition,
|
||||
},
|
||||
),
|
||||
this.overrideDataByFieldMetadata(arg, options, {
|
||||
argIndex: index,
|
||||
shouldBackfillPosition,
|
||||
}),
|
||||
) ?? [],
|
||||
),
|
||||
} satisfies CreateManyResolverArgs;
|
||||
@ -91,7 +85,6 @@ export class QueryRunnerArgsFactory {
|
||||
data: await this.overrideDataByFieldMetadata(
|
||||
(args as UpdateOneResolverArgs).data,
|
||||
options,
|
||||
fieldMetadataMapByNameByName,
|
||||
{
|
||||
argIndex: 0,
|
||||
shouldBackfillPosition: false,
|
||||
@ -103,12 +96,11 @@ export class QueryRunnerArgsFactory {
|
||||
...args,
|
||||
filter: this.overrideFilterByFieldMetadata(
|
||||
(args as UpdateManyResolverArgs).filter,
|
||||
fieldMetadataMapByNameByName,
|
||||
options.objectMetadataItemWithFieldMaps,
|
||||
),
|
||||
data: await this.overrideDataByFieldMetadata(
|
||||
(args as UpdateManyResolverArgs).data,
|
||||
options,
|
||||
fieldMetadataMapByNameByName,
|
||||
{
|
||||
argIndex: 0,
|
||||
shouldBackfillPosition: false,
|
||||
@ -120,7 +112,7 @@ export class QueryRunnerArgsFactory {
|
||||
...args,
|
||||
filter: this.overrideFilterByFieldMetadata(
|
||||
(args as FindOneResolverArgs).filter,
|
||||
fieldMetadataMapByNameByName,
|
||||
options.objectMetadataItemWithFieldMaps,
|
||||
),
|
||||
};
|
||||
case ResolverArgsType.FindMany:
|
||||
@ -128,7 +120,7 @@ export class QueryRunnerArgsFactory {
|
||||
...args,
|
||||
filter: this.overrideFilterByFieldMetadata(
|
||||
(args as FindManyResolverArgs).filter,
|
||||
fieldMetadataMapByNameByName,
|
||||
options.objectMetadataItemWithFieldMaps,
|
||||
),
|
||||
};
|
||||
|
||||
@ -146,15 +138,10 @@ export class QueryRunnerArgsFactory {
|
||||
)) as string[],
|
||||
data: await Promise.all(
|
||||
(args as FindDuplicatesResolverArgs).data?.map((arg, index) =>
|
||||
this.overrideDataByFieldMetadata(
|
||||
arg,
|
||||
options,
|
||||
fieldMetadataMapByNameByName,
|
||||
{
|
||||
argIndex: index,
|
||||
shouldBackfillPosition,
|
||||
},
|
||||
),
|
||||
this.overrideDataByFieldMetadata(arg, options, {
|
||||
argIndex: index,
|
||||
shouldBackfillPosition,
|
||||
}),
|
||||
) ?? [],
|
||||
),
|
||||
} satisfies FindDuplicatesResolverArgs;
|
||||
@ -166,7 +153,6 @@ export class QueryRunnerArgsFactory {
|
||||
private async overrideDataByFieldMetadata(
|
||||
data: Partial<ObjectRecord> | undefined,
|
||||
options: WorkspaceQueryRunnerOptions,
|
||||
fieldMetadataMapByNameByName: Record<string, FieldMetadataInterface>,
|
||||
argPositionBackfillInput: ArgPositionBackfillInput,
|
||||
): Promise<Partial<ObjectRecord>> {
|
||||
if (!isDefined(data)) {
|
||||
@ -184,7 +170,10 @@ export class QueryRunnerArgsFactory {
|
||||
data,
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
).map(async ([key, value]): Promise<[string, any]> => {
|
||||
const fieldMetadata = fieldMetadataMapByNameByName[key];
|
||||
const fieldMetadataId =
|
||||
options.objectMetadataItemWithFieldMaps.fieldIdByName[key];
|
||||
const fieldMetadata =
|
||||
options.objectMetadataItemWithFieldMaps.fieldsById[fieldMetadataId];
|
||||
|
||||
if (!fieldMetadata) {
|
||||
return [key, value];
|
||||
@ -257,7 +246,7 @@ export class QueryRunnerArgsFactory {
|
||||
|
||||
private overrideFilterByFieldMetadata(
|
||||
filter: ObjectRecordFilter | undefined,
|
||||
fieldMetadataMapByName: Record<string, FieldMetadataInterface>,
|
||||
objectMetadataItemWithFieldMaps: ObjectMetadataItemWithFieldMaps,
|
||||
) {
|
||||
if (!filter) {
|
||||
return;
|
||||
@ -278,7 +267,7 @@ export class QueryRunnerArgsFactory {
|
||||
acc[key] = this.transformFilterValueByType(
|
||||
key,
|
||||
value,
|
||||
fieldMetadataMapByName,
|
||||
objectMetadataItemWithFieldMaps,
|
||||
);
|
||||
}
|
||||
|
||||
@ -293,9 +282,11 @@ export class QueryRunnerArgsFactory {
|
||||
key: string,
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
value: any,
|
||||
fieldMetadataMapByName: FieldMetadataMap,
|
||||
objectMetadataItemWithFieldMaps: ObjectMetadataItemWithFieldMaps,
|
||||
) {
|
||||
const fieldMetadata = fieldMetadataMapByName[key];
|
||||
const fieldMetadataId = objectMetadataItemWithFieldMaps.fieldIdByName[key];
|
||||
const fieldMetadata =
|
||||
objectMetadataItemWithFieldMaps.fieldsById[fieldMetadataId];
|
||||
|
||||
if (!fieldMetadata) {
|
||||
return value;
|
||||
|
||||
@ -28,6 +28,7 @@ export const graphqlQueryRunnerExceptionHandler = (
|
||||
case GraphqlQueryRunnerExceptionCode.RELATION_SETTINGS_NOT_FOUND:
|
||||
case GraphqlQueryRunnerExceptionCode.RELATION_TARGET_OBJECT_METADATA_NOT_FOUND:
|
||||
case GraphqlQueryRunnerExceptionCode.INVALID_POST_HOOK_PAYLOAD:
|
||||
case GraphqlQueryRunnerExceptionCode.MISSING_SYSTEM_FIELD:
|
||||
throw error;
|
||||
default: {
|
||||
const _exhaustiveCheck: never = error.code;
|
||||
|
||||
@ -1,5 +1,5 @@
|
||||
import { QueryFailedError } from 'typeorm';
|
||||
import { isDefined } from 'twenty-shared/utils';
|
||||
import { QueryFailedError } from 'typeorm';
|
||||
|
||||
import { WorkspaceQueryRunnerOptions } from 'src/engine/api/graphql/workspace-query-runner/interfaces/query-runner-option.interface';
|
||||
|
||||
@ -14,14 +14,14 @@ export const handleDuplicateKeyError = (
|
||||
if (indexNameMatch) {
|
||||
const indexName = indexNameMatch[1];
|
||||
|
||||
const deletedAtFieldMetadata =
|
||||
context.objectMetadataItemWithFieldMaps.fieldsByName['deletedAt'];
|
||||
const deletedAtFieldMetadataId =
|
||||
context.objectMetadataItemWithFieldMaps.fieldIdByName['deletedAt'];
|
||||
|
||||
const affectedColumns =
|
||||
context.objectMetadataItemWithFieldMaps.indexMetadatas
|
||||
.find((index) => index.name === indexName)
|
||||
?.indexFieldMetadatas?.filter(
|
||||
(field) => field.fieldMetadataId !== deletedAtFieldMetadata?.id,
|
||||
(field) => field.fieldMetadataId !== deletedAtFieldMetadataId,
|
||||
)
|
||||
.map((indexField) => {
|
||||
const fieldMetadata =
|
||||
|
||||
@ -12,7 +12,7 @@ export class WorkspaceResolverBuilderService {
|
||||
constructor() {}
|
||||
|
||||
shouldBuildResolver(
|
||||
objectMetadata: ObjectMetadataInterface,
|
||||
objectMetadata: Pick<ObjectMetadataInterface, 'duplicateCriteria'>,
|
||||
methodName: WorkspaceResolverBuilderMethodNames,
|
||||
) {
|
||||
switch (methodName) {
|
||||
|
||||
@ -3,26 +3,18 @@ import { Injectable } from '@nestjs/common';
|
||||
import { makeExecutableSchema } from '@graphql-tools/schema';
|
||||
import { GraphQLSchema, printSchema } from 'graphql';
|
||||
import { gql } from 'graphql-tag';
|
||||
import { isDefined } from 'twenty-shared/utils';
|
||||
|
||||
import { ScalarsExplorerService } from 'src/engine/api/graphql/services/scalars-explorer.service';
|
||||
import { workspaceResolverBuilderMethodNames } from 'src/engine/api/graphql/workspace-resolver-builder/factories/factories';
|
||||
import { WorkspaceResolverFactory } from 'src/engine/api/graphql/workspace-resolver-builder/workspace-resolver.factory';
|
||||
import { WorkspaceGraphQLSchemaFactory } from 'src/engine/api/graphql/workspace-schema-builder/workspace-graphql-schema.factory';
|
||||
import { AuthContext } from 'src/engine/core-modules/auth/types/auth-context.type';
|
||||
import { FeatureFlagService } from 'src/engine/core-modules/feature-flag/services/feature-flag.service';
|
||||
import { TwentyConfigService } from 'src/engine/core-modules/twenty-config/twenty-config.service';
|
||||
import { DataSourceService } from 'src/engine/metadata-modules/data-source/data-source.service';
|
||||
import { ObjectMetadataMaps } from 'src/engine/metadata-modules/types/object-metadata-maps';
|
||||
import {
|
||||
WorkspaceMetadataCacheException,
|
||||
WorkspaceMetadataCacheExceptionCode,
|
||||
} from 'src/engine/metadata-modules/workspace-metadata-cache/exceptions/workspace-metadata-cache.exception';
|
||||
import { WorkspaceMetadataCacheService } from 'src/engine/metadata-modules/workspace-metadata-cache/services/workspace-metadata-cache.service';
|
||||
import {
|
||||
WorkspaceMetadataVersionException,
|
||||
WorkspaceMetadataVersionExceptionCode,
|
||||
} from 'src/engine/metadata-modules/workspace-metadata-version/exceptions/workspace-metadata-version.exception';
|
||||
import { WorkspaceCacheStorageService } from 'src/engine/workspace-cache-storage/workspace-cache-storage.service';
|
||||
|
||||
@Injectable()
|
||||
@ -34,8 +26,6 @@ export class WorkspaceSchemaFactory {
|
||||
private readonly workspaceResolverFactory: WorkspaceResolverFactory,
|
||||
private readonly workspaceCacheStorageService: WorkspaceCacheStorageService,
|
||||
private readonly workspaceMetadataCacheService: WorkspaceMetadataCacheService,
|
||||
private readonly featureFlagService: FeatureFlagService,
|
||||
private readonly twentyConfigService: TwentyConfigService,
|
||||
) {}
|
||||
|
||||
async createGraphQLSchema(authContext: AuthContext): Promise<GraphQLSchema> {
|
||||
@ -52,38 +42,12 @@ export class WorkspaceSchemaFactory {
|
||||
return new GraphQLSchema({});
|
||||
}
|
||||
|
||||
let currentCacheVersion =
|
||||
await this.workspaceCacheStorageService.getMetadataVersion(
|
||||
authContext.workspace.id,
|
||||
);
|
||||
|
||||
let objectMetadataMaps: ObjectMetadataMaps | undefined;
|
||||
|
||||
if (currentCacheVersion === undefined) {
|
||||
const recomputed =
|
||||
await this.workspaceMetadataCacheService.recomputeMetadataCache({
|
||||
const { objectMetadataMaps, metadataVersion } =
|
||||
await this.workspaceMetadataCacheService.getExistingOrRecomputeMetadataMaps(
|
||||
{
|
||||
workspaceId: authContext.workspace.id,
|
||||
});
|
||||
|
||||
objectMetadataMaps = recomputed?.recomputedObjectMetadataMaps;
|
||||
currentCacheVersion = recomputed?.recomputedMetadataVersion;
|
||||
} else {
|
||||
objectMetadataMaps =
|
||||
await this.workspaceCacheStorageService.getObjectMetadataMaps(
|
||||
authContext.workspace.id,
|
||||
currentCacheVersion,
|
||||
);
|
||||
|
||||
if (!isDefined(objectMetadataMaps)) {
|
||||
const recomputed =
|
||||
await this.workspaceMetadataCacheService.recomputeMetadataCache({
|
||||
workspaceId: authContext.workspace.id,
|
||||
});
|
||||
|
||||
objectMetadataMaps = recomputed?.recomputedObjectMetadataMaps;
|
||||
currentCacheVersion = recomputed?.recomputedMetadataVersion;
|
||||
}
|
||||
}
|
||||
},
|
||||
);
|
||||
|
||||
if (!objectMetadataMaps) {
|
||||
throw new WorkspaceMetadataCacheException(
|
||||
@ -92,17 +56,10 @@ export class WorkspaceSchemaFactory {
|
||||
);
|
||||
}
|
||||
|
||||
if (!currentCacheVersion) {
|
||||
throw new WorkspaceMetadataVersionException(
|
||||
'Metadata cache version not found',
|
||||
WorkspaceMetadataVersionExceptionCode.METADATA_VERSION_NOT_FOUND,
|
||||
);
|
||||
}
|
||||
|
||||
const objectMetadataCollection = Object.values(objectMetadataMaps.byId).map(
|
||||
(objectMetadataItem) => ({
|
||||
...objectMetadataItem,
|
||||
fields: objectMetadataItem.fields,
|
||||
fields: Object.values(objectMetadataItem.fieldsById),
|
||||
indexes: objectMetadataItem.indexMetadatas,
|
||||
}),
|
||||
);
|
||||
@ -110,12 +67,12 @@ export class WorkspaceSchemaFactory {
|
||||
// Get typeDefs from cache
|
||||
let typeDefs = await this.workspaceCacheStorageService.getGraphQLTypeDefs(
|
||||
authContext.workspace.id,
|
||||
currentCacheVersion,
|
||||
metadataVersion,
|
||||
);
|
||||
let usedScalarNames =
|
||||
await this.workspaceCacheStorageService.getGraphQLUsedScalarNames(
|
||||
authContext.workspace.id,
|
||||
currentCacheVersion,
|
||||
metadataVersion,
|
||||
);
|
||||
|
||||
// If typeDefs are not cached, generate them
|
||||
@ -133,12 +90,12 @@ export class WorkspaceSchemaFactory {
|
||||
|
||||
await this.workspaceCacheStorageService.setGraphQLTypeDefs(
|
||||
authContext.workspace.id,
|
||||
currentCacheVersion,
|
||||
metadataVersion,
|
||||
typeDefs,
|
||||
);
|
||||
await this.workspaceCacheStorageService.setGraphQLUsedScalarNames(
|
||||
authContext.workspace.id,
|
||||
currentCacheVersion,
|
||||
metadataVersion,
|
||||
usedScalarNames,
|
||||
);
|
||||
}
|
||||
|
||||
@ -5,6 +5,8 @@ import { isDefined } from 'twenty-shared/utils';
|
||||
|
||||
import { RestApiBaseHandler } from 'src/engine/api/rest/core/interfaces/rest-api-base.handler';
|
||||
|
||||
import { getObjectMetadataFromObjectMetadataItemWithFieldMaps } from 'src/engine/metadata-modules/utils/get-object-metadata-from-object-metadata-Item-with-field-maps';
|
||||
|
||||
@Injectable()
|
||||
export class RestApiCreateManyHandler extends RestApiBaseHandler {
|
||||
async handle(request: Request) {
|
||||
@ -57,7 +59,9 @@ export class RestApiCreateManyHandler extends RestApiBaseHandler {
|
||||
this.apiEventEmitterService.emitCreateEvents({
|
||||
records: createdRecords,
|
||||
authContext: this.getAuthContextFromRequest(request),
|
||||
objectMetadataItem: objectMetadata.objectMetadataMapItem,
|
||||
objectMetadataItem: getObjectMetadataFromObjectMetadataItemWithFieldMaps(
|
||||
objectMetadata.objectMetadataMapItem,
|
||||
),
|
||||
});
|
||||
|
||||
const records = await this.getRecord({
|
||||
|
||||
@ -5,6 +5,8 @@ import { isDefined } from 'twenty-shared/utils';
|
||||
|
||||
import { RestApiBaseHandler } from 'src/engine/api/rest/core/interfaces/rest-api-base.handler';
|
||||
|
||||
import { getObjectMetadataFromObjectMetadataItemWithFieldMaps } from 'src/engine/metadata-modules/utils/get-object-metadata-from-object-metadata-Item-with-field-maps';
|
||||
|
||||
@Injectable()
|
||||
export class RestApiCreateOneHandler extends RestApiBaseHandler {
|
||||
async handle(request: Request) {
|
||||
@ -40,7 +42,9 @@ export class RestApiCreateOneHandler extends RestApiBaseHandler {
|
||||
this.apiEventEmitterService.emitCreateEvents({
|
||||
records: [createdRecord],
|
||||
authContext: this.getAuthContextFromRequest(request),
|
||||
objectMetadataItem: objectMetadata.objectMetadataMapItem,
|
||||
objectMetadataItem: getObjectMetadataFromObjectMetadataItemWithFieldMaps(
|
||||
objectMetadata.objectMetadataMapItem,
|
||||
),
|
||||
});
|
||||
|
||||
const records = await this.getRecord({
|
||||
|
||||
@ -5,6 +5,7 @@ import { Request } from 'express';
|
||||
import { RestApiBaseHandler } from 'src/engine/api/rest/core/interfaces/rest-api-base.handler';
|
||||
|
||||
import { parseCorePath } from 'src/engine/api/rest/core/query-builder/utils/path-parsers/parse-core-path.utils';
|
||||
import { getObjectMetadataFromObjectMetadataItemWithFieldMaps } from 'src/engine/metadata-modules/utils/get-object-metadata-from-object-metadata-Item-with-field-maps';
|
||||
|
||||
@Injectable()
|
||||
export class RestApiDeleteOneHandler extends RestApiBaseHandler {
|
||||
@ -26,7 +27,9 @@ export class RestApiDeleteOneHandler extends RestApiBaseHandler {
|
||||
this.apiEventEmitterService.emitDestroyEvents({
|
||||
records: [recordToDelete],
|
||||
authContext: this.getAuthContextFromRequest(request),
|
||||
objectMetadataItem: objectMetadata.objectMetadataMapItem,
|
||||
objectMetadataItem: getObjectMetadataFromObjectMetadataItemWithFieldMaps(
|
||||
objectMetadata.objectMetadataMapItem,
|
||||
),
|
||||
});
|
||||
|
||||
return this.formatResult({
|
||||
|
||||
@ -4,14 +4,14 @@ import { Request } from 'express';
|
||||
import isEmpty from 'lodash.isempty';
|
||||
import { In } from 'typeorm';
|
||||
|
||||
import { ObjectRecord } from 'src/engine/api/graphql/workspace-query-builder/interfaces/object-record.interface';
|
||||
import {
|
||||
FormatResult,
|
||||
RestApiBaseHandler,
|
||||
} from 'src/engine/api/rest/core/interfaces/rest-api-base.handler';
|
||||
import { ObjectRecord } from 'src/engine/api/graphql/workspace-query-builder/interfaces/object-record.interface';
|
||||
|
||||
import { formatResult } from 'src/engine/twenty-orm/utils/format-result.util';
|
||||
import { buildDuplicateConditions } from 'src/engine/api/utils/build-duplicate-conditions.utils';
|
||||
import { formatResult } from 'src/engine/twenty-orm/utils/format-result.util';
|
||||
|
||||
@Injectable()
|
||||
export class RestApiFindDuplicatesHandler extends RestApiBaseHandler {
|
||||
|
||||
@ -6,6 +6,7 @@ import { isDefined } from 'twenty-shared/utils';
|
||||
import { RestApiBaseHandler } from 'src/engine/api/rest/core/interfaces/rest-api-base.handler';
|
||||
|
||||
import { parseCorePath } from 'src/engine/api/rest/core/query-builder/utils/path-parsers/parse-core-path.utils';
|
||||
import { getObjectMetadataFromObjectMetadataItemWithFieldMaps } from 'src/engine/metadata-modules/utils/get-object-metadata-from-object-metadata-Item-with-field-maps';
|
||||
|
||||
@Injectable()
|
||||
export class RestApiUpdateOneHandler extends RestApiBaseHandler {
|
||||
@ -38,7 +39,9 @@ export class RestApiUpdateOneHandler extends RestApiBaseHandler {
|
||||
records: [updatedRecord],
|
||||
updatedFields: Object.keys(request.body),
|
||||
authContext: this.getAuthContextFromRequest(request),
|
||||
objectMetadataItem: objectMetadata.objectMetadataMapItem,
|
||||
objectMetadataItem: getObjectMetadataFromObjectMetadataItemWithFieldMaps(
|
||||
objectMetadata.objectMetadataMapItem,
|
||||
),
|
||||
});
|
||||
|
||||
const records = await this.getRecord({
|
||||
|
||||
@ -160,32 +160,34 @@ export abstract class RestApiBaseHandler {
|
||||
|
||||
const relations: string[] = [];
|
||||
|
||||
objectMetadata.objectMetadataMapItem.fields.forEach((field) => {
|
||||
if (field.type === FieldMetadataType.RELATION) {
|
||||
if (
|
||||
depth === MAX_DEPTH &&
|
||||
isDefined(field.relationTargetObjectMetadataId)
|
||||
) {
|
||||
const relationTargetObjectMetadata =
|
||||
objectMetadata.objectMetadataMaps.byId[
|
||||
field.relationTargetObjectMetadataId
|
||||
];
|
||||
const depth2Relations = this.getRelations({
|
||||
objectMetadata: {
|
||||
objectMetadataMaps: objectMetadata.objectMetadataMaps,
|
||||
objectMetadataMapItem: relationTargetObjectMetadata,
|
||||
},
|
||||
depth: 1,
|
||||
});
|
||||
Object.values(objectMetadata.objectMetadataMapItem.fieldsById).forEach(
|
||||
(field) => {
|
||||
if (field.type === FieldMetadataType.RELATION) {
|
||||
if (
|
||||
depth === MAX_DEPTH &&
|
||||
isDefined(field.relationTargetObjectMetadataId)
|
||||
) {
|
||||
const relationTargetObjectMetadata =
|
||||
objectMetadata.objectMetadataMaps.byId[
|
||||
field.relationTargetObjectMetadataId
|
||||
];
|
||||
const depth2Relations = this.getRelations({
|
||||
objectMetadata: {
|
||||
objectMetadataMaps: objectMetadata.objectMetadataMaps,
|
||||
objectMetadataMapItem: relationTargetObjectMetadata,
|
||||
},
|
||||
depth: 1,
|
||||
});
|
||||
|
||||
depth2Relations.forEach((depth2Relation) => {
|
||||
relations.push(`${field.name}.${depth2Relation}`);
|
||||
});
|
||||
} else {
|
||||
relations.push(`${field.name}`);
|
||||
depth2Relations.forEach((depth2Relation) => {
|
||||
relations.push(`${field.name}.${depth2Relation}`);
|
||||
});
|
||||
} else {
|
||||
relations.push(`${field.name}`);
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
},
|
||||
);
|
||||
|
||||
return relations;
|
||||
}
|
||||
@ -305,9 +307,7 @@ export abstract class RestApiBaseHandler {
|
||||
objectMetadataMaps: ObjectMetadataMaps;
|
||||
objectMetadataMapItem: ObjectMetadataItemWithFieldMaps;
|
||||
};
|
||||
objectMetadataItemWithFieldsMaps:
|
||||
| ObjectMetadataItemWithFieldMaps
|
||||
| undefined;
|
||||
objectMetadataItemWithFieldsMaps: ObjectMetadataItemWithFieldMaps;
|
||||
extraFilters?: Partial<ObjectRecordFilter>;
|
||||
}) {
|
||||
const objectMetadataNameSingular =
|
||||
@ -321,17 +321,10 @@ export abstract class RestApiBaseHandler {
|
||||
objectMetadata,
|
||||
);
|
||||
|
||||
const fieldMetadataMapByName =
|
||||
objectMetadataItemWithFieldsMaps?.fieldsByName || {};
|
||||
|
||||
const fieldMetadataMapByJoinColumnName =
|
||||
objectMetadataItemWithFieldsMaps?.fieldsByJoinColumnName || {};
|
||||
|
||||
const isForwardPagination = !inputs.endingBefore;
|
||||
|
||||
const graphqlQueryParser = new GraphqlQueryParser(
|
||||
fieldMetadataMapByName,
|
||||
fieldMetadataMapByJoinColumnName,
|
||||
objectMetadataItemWithFieldsMaps,
|
||||
objectMetadata.objectMetadataMaps,
|
||||
);
|
||||
|
||||
@ -442,7 +435,7 @@ export abstract class RestApiBaseHandler {
|
||||
const cursorArgFilter = computeCursorArgFilter(
|
||||
this.parseCursor(cursor),
|
||||
inputs.orderBy || [],
|
||||
objectMetadata.objectMetadataMapItem.fieldsByName,
|
||||
objectMetadata.objectMetadataMapItem,
|
||||
isForwardPagination,
|
||||
);
|
||||
|
||||
|
||||
@ -26,7 +26,7 @@ export class CreateManyQueryFactory {
|
||||
mutation Create${objectNamePlural}($data: [${objectNameSingular}CreateInput!]) {
|
||||
create${objectNamePlural}(data: $data) {
|
||||
id
|
||||
${objectMetadata.objectMetadataMapItem.fields
|
||||
${Object.values(objectMetadata.objectMetadataMapItem.fieldsById)
|
||||
.map((field) =>
|
||||
mapFieldMetadataToGraphqlQuery(
|
||||
objectMetadata.objectMetadataMaps,
|
||||
|
||||
@ -31,7 +31,7 @@ export class FindDuplicatesQueryFactory {
|
||||
}
|
||||
edges{
|
||||
node {
|
||||
${objectMetadata.objectMetadataMapItem.fields
|
||||
${Object.values(objectMetadata.objectMetadataMapItem.fieldsById)
|
||||
.map((field) =>
|
||||
mapFieldMetadataToGraphqlQuery(
|
||||
objectMetadata.objectMetadataMaps,
|
||||
|
||||
@ -17,21 +17,23 @@ describe('checkFields', () => {
|
||||
objectMetadataId: 'object-metadata-id',
|
||||
isNullable: fieldNumberMock.isNullable,
|
||||
defaultValue: fieldNumberMock.defaultValue,
|
||||
isLabelSyncedWithName: true,
|
||||
createdAt: new Date(),
|
||||
updatedAt: new Date(),
|
||||
};
|
||||
|
||||
const fieldsById: FieldMetadataMap = {
|
||||
'field-number-id': completeFieldNumberMock,
|
||||
};
|
||||
|
||||
const fieldsByName: FieldMetadataMap = {
|
||||
[completeFieldNumberMock.name]: completeFieldNumberMock,
|
||||
};
|
||||
|
||||
const mockObjectMetadataWithFieldMaps = {
|
||||
...objectMetadataItemMock,
|
||||
fieldsById,
|
||||
fieldsByName,
|
||||
fieldsByJoinColumnName: {},
|
||||
fieldIdByName: {
|
||||
[completeFieldNumberMock.name]: completeFieldNumberMock.id,
|
||||
},
|
||||
fieldIdByJoinColumnName: {},
|
||||
indexMetadatas: [],
|
||||
};
|
||||
|
||||
it('should check field types', () => {
|
||||
|
||||
@ -18,21 +18,23 @@ describe('getFieldType', () => {
|
||||
objectMetadataId: 'object-metadata-id',
|
||||
isNullable: fieldNumberMock.isNullable,
|
||||
defaultValue: fieldNumberMock.defaultValue,
|
||||
isLabelSyncedWithName: true,
|
||||
createdAt: new Date(),
|
||||
updatedAt: new Date(),
|
||||
};
|
||||
|
||||
const fieldsById: FieldMetadataMap = {
|
||||
'field-number-id': completeFieldNumberMock,
|
||||
};
|
||||
|
||||
const fieldsByName: FieldMetadataMap = {
|
||||
[completeFieldNumberMock.name]: completeFieldNumberMock,
|
||||
};
|
||||
|
||||
const mockObjectMetadataWithFieldMaps = {
|
||||
...objectMetadataItemMock,
|
||||
fieldsById,
|
||||
fieldsByName,
|
||||
fieldsByJoinColumnName: {},
|
||||
fieldIdByName: {
|
||||
[completeFieldNumberMock.name]: completeFieldNumberMock.id,
|
||||
},
|
||||
fieldIdByJoinColumnName: {},
|
||||
indexMetadatas: [],
|
||||
};
|
||||
|
||||
it('should get field type', () => {
|
||||
|
||||
@ -24,6 +24,9 @@ describe('mapFieldMetadataToGraphqlQuery', () => {
|
||||
objectMetadataId: 'object-metadata-id',
|
||||
isNullable: fieldNumberMock.isNullable,
|
||||
defaultValue: fieldNumberMock.defaultValue,
|
||||
isLabelSyncedWithName: true,
|
||||
createdAt: new Date(),
|
||||
updatedAt: new Date(),
|
||||
};
|
||||
|
||||
const typedFieldTextMock: FieldMetadataInterface = {
|
||||
@ -34,6 +37,9 @@ describe('mapFieldMetadataToGraphqlQuery', () => {
|
||||
objectMetadataId: 'object-metadata-id',
|
||||
isNullable: fieldTextMock.isNullable,
|
||||
defaultValue: fieldTextMock.defaultValue,
|
||||
isLabelSyncedWithName: true,
|
||||
createdAt: new Date(),
|
||||
updatedAt: new Date(),
|
||||
};
|
||||
|
||||
const typedFieldCurrencyMock: FieldMetadataInterface = {
|
||||
@ -44,6 +50,9 @@ describe('mapFieldMetadataToGraphqlQuery', () => {
|
||||
objectMetadataId: 'object-metadata-id',
|
||||
isNullable: fieldCurrencyMock.isNullable,
|
||||
defaultValue: fieldCurrencyMock.defaultValue,
|
||||
isLabelSyncedWithName: true,
|
||||
createdAt: new Date(),
|
||||
updatedAt: new Date(),
|
||||
};
|
||||
|
||||
const fieldsById: FieldMetadataMap = {
|
||||
@ -52,17 +61,16 @@ describe('mapFieldMetadataToGraphqlQuery', () => {
|
||||
'field-currency-id': typedFieldCurrencyMock,
|
||||
};
|
||||
|
||||
const fieldsByName: FieldMetadataMap = {
|
||||
[typedFieldNumberMock.name]: typedFieldNumberMock,
|
||||
[typedFieldTextMock.name]: typedFieldTextMock,
|
||||
[typedFieldCurrencyMock.name]: typedFieldCurrencyMock,
|
||||
};
|
||||
|
||||
const typedObjectMetadataItem: ObjectMetadataItemWithFieldMaps = {
|
||||
...objectMetadataItemMock,
|
||||
fieldsById,
|
||||
fieldsByName,
|
||||
fieldsByJoinColumnName: {},
|
||||
fieldIdByName: {
|
||||
[typedFieldNumberMock.name]: typedFieldNumberMock.id,
|
||||
[typedFieldTextMock.name]: typedFieldTextMock.id,
|
||||
[typedFieldCurrencyMock.name]: typedFieldCurrencyMock.id,
|
||||
},
|
||||
fieldIdByJoinColumnName: {},
|
||||
indexMetadatas: [],
|
||||
};
|
||||
|
||||
const objectMetadataMapsMock: ObjectMetadataMaps = {
|
||||
@ -110,6 +118,10 @@ describe('mapFieldMetadataToGraphqlQuery', () => {
|
||||
name: 'toObjectMetadataName',
|
||||
label: 'Test Field',
|
||||
objectMetadataId: 'object-metadata-id',
|
||||
isNullable: true,
|
||||
isLabelSyncedWithName: true,
|
||||
createdAt: new Date(),
|
||||
updatedAt: new Date(),
|
||||
};
|
||||
|
||||
if (fieldMetadataType === FieldMetadataType.RELATION) {
|
||||
|
||||
@ -9,7 +9,7 @@ export const checkFields = (
|
||||
objectMetadataItem: ObjectMetadataItemWithFieldMaps,
|
||||
fieldNames: string[],
|
||||
): void => {
|
||||
const fieldMetadataNames = objectMetadataItem.fields
|
||||
const fieldMetadataNames = Object.values(objectMetadataItem.fieldsById)
|
||||
.map((field) => {
|
||||
if (isCompositeFieldMetadataType(field.type)) {
|
||||
const compositeType = compositeTypeDefinitions.get(field.type);
|
||||
|
||||
@ -11,7 +11,7 @@ export const checkArrayFields = (
|
||||
objectMetadataItem: ObjectMetadataItemWithFieldMaps,
|
||||
fields: Array<Partial<ObjectRecord>>,
|
||||
): void => {
|
||||
const fieldMetadataNames = objectMetadataItem.fields
|
||||
const fieldMetadataNames = Object.values(objectMetadataItem.fieldsById)
|
||||
.map((field) => {
|
||||
if (isCompositeFieldMetadataType(field.type)) {
|
||||
const compositeType = compositeTypeDefinitions.get(field.type);
|
||||
|
||||
@ -19,21 +19,23 @@ describe('checkFilterEnumValues', () => {
|
||||
isNullable: fieldSelectMock.isNullable,
|
||||
defaultValue: fieldSelectMock.defaultValue,
|
||||
options: fieldSelectMock.options,
|
||||
isLabelSyncedWithName: true,
|
||||
createdAt: new Date(),
|
||||
updatedAt: new Date(),
|
||||
};
|
||||
|
||||
const fieldsById: FieldMetadataMap = {
|
||||
'field-select-id': completeFieldSelectMock,
|
||||
};
|
||||
|
||||
const fieldsByName: FieldMetadataMap = {
|
||||
[completeFieldSelectMock.name]: completeFieldSelectMock,
|
||||
};
|
||||
|
||||
const mockObjectMetadataWithFieldMaps = {
|
||||
...objectMetadataItemMock,
|
||||
fieldsById,
|
||||
fieldsByName,
|
||||
fieldsByJoinColumnName: {},
|
||||
fieldIdByName: {
|
||||
[completeFieldSelectMock.name]: completeFieldSelectMock.id,
|
||||
},
|
||||
fieldIdByJoinColumnName: {},
|
||||
indexMetadatas: [],
|
||||
};
|
||||
|
||||
it('should check properly', () => {
|
||||
|
||||
@ -7,6 +7,7 @@ import {
|
||||
} from 'src/engine/api/__mocks__/object-metadata-item.mock';
|
||||
import { parseFilter } from 'src/engine/api/rest/core/query-builder/utils/filter-utils/parse-filter.utils';
|
||||
import { FieldMetadataMap } from 'src/engine/metadata-modules/types/field-metadata-map';
|
||||
import { ObjectMetadataItemWithFieldMaps } from 'src/engine/metadata-modules/types/object-metadata-item-with-field-maps';
|
||||
|
||||
describe('parseFilter', () => {
|
||||
const completeFieldNumberMock: FieldMetadataInterface = {
|
||||
@ -17,6 +18,9 @@ describe('parseFilter', () => {
|
||||
objectMetadataId: 'object-metadata-id',
|
||||
isNullable: fieldNumberMock.isNullable,
|
||||
defaultValue: fieldNumberMock.defaultValue,
|
||||
isLabelSyncedWithName: true,
|
||||
createdAt: new Date(),
|
||||
updatedAt: new Date(),
|
||||
};
|
||||
|
||||
const completeFieldTextMock: FieldMetadataInterface = {
|
||||
@ -27,6 +31,9 @@ describe('parseFilter', () => {
|
||||
objectMetadataId: 'object-metadata-id',
|
||||
isNullable: fieldTextMock.isNullable,
|
||||
defaultValue: fieldTextMock.defaultValue,
|
||||
isLabelSyncedWithName: true,
|
||||
createdAt: new Date(),
|
||||
updatedAt: new Date(),
|
||||
};
|
||||
|
||||
const fieldsById: FieldMetadataMap = {
|
||||
@ -34,16 +41,15 @@ describe('parseFilter', () => {
|
||||
'field-text-id': completeFieldTextMock,
|
||||
};
|
||||
|
||||
const fieldsByName: FieldMetadataMap = {
|
||||
[completeFieldNumberMock.name]: completeFieldNumberMock,
|
||||
[completeFieldTextMock.name]: completeFieldTextMock,
|
||||
};
|
||||
|
||||
const mockObjectMetadataWithFieldMaps = {
|
||||
const mockObjectMetadataWithFieldMaps: ObjectMetadataItemWithFieldMaps = {
|
||||
...objectMetadataItemMock,
|
||||
fieldsById,
|
||||
fieldsByName,
|
||||
fieldsByJoinColumnName: {},
|
||||
fieldIdByName: {
|
||||
[completeFieldNumberMock.name]: completeFieldNumberMock.id,
|
||||
[completeFieldTextMock.name]: completeFieldTextMock.id,
|
||||
},
|
||||
fieldIdByJoinColumnName: {},
|
||||
indexMetadatas: [],
|
||||
};
|
||||
|
||||
it('should parse string filter test 1', () => {
|
||||
|
||||
@ -18,7 +18,8 @@ export const checkFilterEnumValues = (
|
||||
) {
|
||||
return;
|
||||
}
|
||||
const field = objectMetadataItem.fieldsByName[fieldName];
|
||||
const fieldMetadataId = objectMetadataItem.fieldIdByName[fieldName];
|
||||
const field = objectMetadataItem.fieldsById[fieldMetadataId];
|
||||
|
||||
const values = /^\[.*\]$/.test(value)
|
||||
? value.slice(1, -1).split(',')
|
||||
|
||||
@ -6,5 +6,8 @@ export const getFieldType = (
|
||||
objectMetadataItem: ObjectMetadataItemWithFieldMaps,
|
||||
fieldName: string,
|
||||
): FieldMetadataType | undefined => {
|
||||
return objectMetadataItem.fieldsByName[fieldName]?.type;
|
||||
const fieldMetadataId = objectMetadataItem.fieldIdByName[fieldName];
|
||||
const field = objectMetadataItem.fieldsById[fieldMetadataId];
|
||||
|
||||
return field?.type;
|
||||
};
|
||||
|
||||
@ -10,6 +10,7 @@ import {
|
||||
} from 'src/engine/api/__mocks__/object-metadata-item.mock';
|
||||
import { FilterInputFactory } from 'src/engine/api/rest/input-factories/filter-input.factory';
|
||||
import { FieldMetadataMap } from 'src/engine/metadata-modules/types/field-metadata-map';
|
||||
import { ObjectMetadataItemWithFieldMaps } from 'src/engine/metadata-modules/types/object-metadata-item-with-field-maps';
|
||||
|
||||
describe('FilterInputFactory', () => {
|
||||
const completeFieldNumberMock: FieldMetadataInterface = {
|
||||
@ -20,6 +21,9 @@ describe('FilterInputFactory', () => {
|
||||
objectMetadataId: 'object-metadata-id',
|
||||
isNullable: fieldNumberMock.isNullable,
|
||||
defaultValue: fieldNumberMock.defaultValue,
|
||||
isLabelSyncedWithName: true,
|
||||
createdAt: new Date(),
|
||||
updatedAt: new Date(),
|
||||
};
|
||||
|
||||
const completeFieldTextMock: FieldMetadataInterface = {
|
||||
@ -30,6 +34,9 @@ describe('FilterInputFactory', () => {
|
||||
objectMetadataId: 'object-metadata-id',
|
||||
isNullable: fieldTextMock.isNullable,
|
||||
defaultValue: fieldTextMock.defaultValue,
|
||||
isLabelSyncedWithName: true,
|
||||
createdAt: new Date(),
|
||||
updatedAt: new Date(),
|
||||
};
|
||||
|
||||
const completeFieldCurrencyMock: FieldMetadataInterface = {
|
||||
@ -40,6 +47,9 @@ describe('FilterInputFactory', () => {
|
||||
objectMetadataId: 'object-metadata-id',
|
||||
isNullable: fieldCurrencyMock.isNullable,
|
||||
defaultValue: fieldCurrencyMock.defaultValue,
|
||||
isLabelSyncedWithName: true,
|
||||
createdAt: new Date(),
|
||||
updatedAt: new Date(),
|
||||
};
|
||||
|
||||
const fieldsById: FieldMetadataMap = {
|
||||
@ -48,16 +58,15 @@ describe('FilterInputFactory', () => {
|
||||
'field-currency-id': completeFieldCurrencyMock,
|
||||
};
|
||||
|
||||
const fieldsByName: FieldMetadataMap = {
|
||||
[completeFieldNumberMock.name]: completeFieldNumberMock,
|
||||
[completeFieldTextMock.name]: completeFieldTextMock,
|
||||
[completeFieldCurrencyMock.name]: completeFieldCurrencyMock,
|
||||
};
|
||||
|
||||
const objectMetadataMapItem = {
|
||||
const objectMetadataMapItem: ObjectMetadataItemWithFieldMaps = {
|
||||
...objectMetadataMapItemMock,
|
||||
fieldsById,
|
||||
fieldsByName,
|
||||
fieldIdByName: {
|
||||
[completeFieldNumberMock.name]: completeFieldNumberMock.id,
|
||||
[completeFieldTextMock.name]: completeFieldTextMock.id,
|
||||
[completeFieldCurrencyMock.name]: completeFieldCurrencyMock.id,
|
||||
},
|
||||
fieldIdByJoinColumnName: {},
|
||||
};
|
||||
|
||||
const objectMetadataMaps = {
|
||||
|
||||
@ -1,11 +1,11 @@
|
||||
import { mockPersonObjectMetadata } from 'src/engine/api/graphql/graphql-query-runner/__mocks__/mockPersonObjectMetadata';
|
||||
import { buildDuplicateConditions } from 'src/engine/api/utils/build-duplicate-conditions.utils';
|
||||
import { mockPersonObjectMetadataWithFieldMaps } from 'src/engine/api/graphql/graphql-query-runner/__mocks__/mockPersonObjectMetadata';
|
||||
import { mockPersonRecords } from 'src/engine/api/graphql/graphql-query-runner/__mocks__/mockPersonRecords';
|
||||
import { buildDuplicateConditions } from 'src/engine/api/utils/build-duplicate-conditions.utils';
|
||||
|
||||
describe('buildDuplicateConditions', () => {
|
||||
it('should build conditions based on duplicate criteria from composite field', () => {
|
||||
const duplicateConditons = buildDuplicateConditions(
|
||||
mockPersonObjectMetadata([['emailsPrimaryEmail']]),
|
||||
mockPersonObjectMetadataWithFieldMaps([['emailsPrimaryEmail']]),
|
||||
mockPersonRecords,
|
||||
'recordId',
|
||||
);
|
||||
@ -13,8 +13,10 @@ describe('buildDuplicateConditions', () => {
|
||||
expect(duplicateConditons).toEqual({
|
||||
or: [
|
||||
{
|
||||
emailsPrimaryEmail: {
|
||||
eq: 'test@test.fr',
|
||||
emails: {
|
||||
primaryEmail: {
|
||||
eq: 'test@test.fr',
|
||||
},
|
||||
},
|
||||
},
|
||||
],
|
||||
@ -26,7 +28,7 @@ describe('buildDuplicateConditions', () => {
|
||||
|
||||
it('should build conditions based on duplicate criteria from basic field', () => {
|
||||
const duplicateConditons = buildDuplicateConditions(
|
||||
mockPersonObjectMetadata([['jobTitle']]),
|
||||
mockPersonObjectMetadataWithFieldMaps([['jobTitle']]),
|
||||
mockPersonRecords,
|
||||
'recordId',
|
||||
);
|
||||
@ -47,7 +49,7 @@ describe('buildDuplicateConditions', () => {
|
||||
|
||||
it('should not build conditions based on duplicate criteria if record value is null or too small', () => {
|
||||
const duplicateConditons = buildDuplicateConditions(
|
||||
mockPersonObjectMetadata([['linkedinLinkPrimaryLinkUrl']]),
|
||||
mockPersonObjectMetadataWithFieldMaps([['linkedinLinkPrimaryLinkUrl']]),
|
||||
mockPersonRecords,
|
||||
'recordId',
|
||||
);
|
||||
@ -57,7 +59,7 @@ describe('buildDuplicateConditions', () => {
|
||||
|
||||
it('should build conditions based on duplicate criteria and without recordId filter', () => {
|
||||
const duplicateConditons = buildDuplicateConditions(
|
||||
mockPersonObjectMetadata([['jobTitle']]),
|
||||
mockPersonObjectMetadataWithFieldMaps([['jobTitle']]),
|
||||
mockPersonRecords,
|
||||
);
|
||||
|
||||
|
||||
@ -4,35 +4,76 @@ import { OrderByDirection } from 'src/engine/api/graphql/workspace-query-builder
|
||||
|
||||
import { GraphqlQueryRunnerException } from 'src/engine/api/graphql/graphql-query-runner/errors/graphql-query-runner.exception';
|
||||
import { computeCursorArgFilter } from 'src/engine/api/utils/compute-cursor-arg-filter.utils';
|
||||
import { ObjectMetadataItemWithFieldMaps } from 'src/engine/metadata-modules/types/object-metadata-item-with-field-maps';
|
||||
|
||||
describe('computeCursorArgFilter', () => {
|
||||
const mockFieldMetadataMap = {
|
||||
name: {
|
||||
type: FieldMetadataType.TEXT,
|
||||
id: 'name-id',
|
||||
name: 'name',
|
||||
label: 'Name',
|
||||
objectMetadataId: 'object-id',
|
||||
const objectMetadataItemWithFieldMaps = {
|
||||
id: 'object-id',
|
||||
workspaceId: 'workspace-id',
|
||||
nameSingular: 'person',
|
||||
namePlural: 'people',
|
||||
isCustom: false,
|
||||
isRemote: false,
|
||||
labelSingular: 'Person',
|
||||
labelPlural: 'People',
|
||||
targetTableName: 'person',
|
||||
indexMetadatas: [],
|
||||
isSystem: false,
|
||||
isActive: true,
|
||||
isAuditLogged: false,
|
||||
isSearchable: false,
|
||||
fieldIdByJoinColumnName: {},
|
||||
icon: 'Icon123',
|
||||
fieldIdByName: {
|
||||
name: 'name-id',
|
||||
age: 'age-id',
|
||||
fullName: 'fullname-id',
|
||||
},
|
||||
age: {
|
||||
type: FieldMetadataType.NUMBER,
|
||||
id: 'age-id',
|
||||
name: 'age',
|
||||
label: 'Age',
|
||||
objectMetadataId: 'object-id',
|
||||
fieldsById: {
|
||||
'name-id': {
|
||||
type: FieldMetadataType.TEXT,
|
||||
id: 'name-id',
|
||||
name: 'name',
|
||||
label: 'Name',
|
||||
objectMetadataId: 'object-id',
|
||||
isLabelSyncedWithName: true,
|
||||
isNullable: true,
|
||||
createdAt: new Date(),
|
||||
updatedAt: new Date(),
|
||||
},
|
||||
'age-id': {
|
||||
type: FieldMetadataType.NUMBER,
|
||||
id: 'age-id',
|
||||
name: 'age',
|
||||
label: 'Age',
|
||||
objectMetadataId: 'object-id',
|
||||
isLabelSyncedWithName: true,
|
||||
isNullable: true,
|
||||
createdAt: new Date(),
|
||||
updatedAt: new Date(),
|
||||
},
|
||||
'fullname-id': {
|
||||
type: FieldMetadataType.FULL_NAME,
|
||||
id: 'fullname-id',
|
||||
name: 'fullName',
|
||||
label: 'Full Name',
|
||||
objectMetadataId: 'object-id',
|
||||
isLabelSyncedWithName: true,
|
||||
isNullable: true,
|
||||
createdAt: new Date(),
|
||||
updatedAt: new Date(),
|
||||
},
|
||||
},
|
||||
fullName: {
|
||||
type: FieldMetadataType.FULL_NAME,
|
||||
id: 'fullname-id',
|
||||
name: 'fullName',
|
||||
label: 'Full Name',
|
||||
objectMetadataId: 'object-id',
|
||||
},
|
||||
};
|
||||
} satisfies ObjectMetadataItemWithFieldMaps;
|
||||
|
||||
describe('basic cursor filtering', () => {
|
||||
it('should return empty array when cursor is empty', () => {
|
||||
const result = computeCursorArgFilter({}, [], mockFieldMetadataMap, true);
|
||||
const result = computeCursorArgFilter(
|
||||
{},
|
||||
[],
|
||||
objectMetadataItemWithFieldMaps,
|
||||
true,
|
||||
);
|
||||
|
||||
expect(result).toEqual([]);
|
||||
});
|
||||
@ -44,7 +85,7 @@ describe('computeCursorArgFilter', () => {
|
||||
const result = computeCursorArgFilter(
|
||||
cursor,
|
||||
orderBy,
|
||||
mockFieldMetadataMap,
|
||||
objectMetadataItemWithFieldMaps,
|
||||
true,
|
||||
);
|
||||
|
||||
@ -58,7 +99,7 @@ describe('computeCursorArgFilter', () => {
|
||||
const result = computeCursorArgFilter(
|
||||
cursor,
|
||||
orderBy,
|
||||
mockFieldMetadataMap,
|
||||
objectMetadataItemWithFieldMaps,
|
||||
false,
|
||||
);
|
||||
|
||||
@ -77,7 +118,7 @@ describe('computeCursorArgFilter', () => {
|
||||
const result = computeCursorArgFilter(
|
||||
cursor,
|
||||
orderBy,
|
||||
mockFieldMetadataMap,
|
||||
objectMetadataItemWithFieldMaps,
|
||||
true,
|
||||
);
|
||||
|
||||
@ -105,7 +146,7 @@ describe('computeCursorArgFilter', () => {
|
||||
const result = computeCursorArgFilter(
|
||||
cursor,
|
||||
orderBy,
|
||||
mockFieldMetadataMap,
|
||||
objectMetadataItemWithFieldMaps,
|
||||
true,
|
||||
);
|
||||
|
||||
@ -151,7 +192,7 @@ describe('computeCursorArgFilter', () => {
|
||||
const result = computeCursorArgFilter(
|
||||
cursor,
|
||||
orderBy,
|
||||
mockFieldMetadataMap,
|
||||
objectMetadataItemWithFieldMaps,
|
||||
true,
|
||||
);
|
||||
|
||||
@ -180,7 +221,7 @@ describe('computeCursorArgFilter', () => {
|
||||
const result = computeCursorArgFilter(
|
||||
cursor,
|
||||
orderBy,
|
||||
mockFieldMetadataMap,
|
||||
objectMetadataItemWithFieldMaps,
|
||||
false,
|
||||
);
|
||||
|
||||
@ -218,7 +259,12 @@ describe('computeCursorArgFilter', () => {
|
||||
const orderBy = [{ invalidField: OrderByDirection.AscNullsLast }];
|
||||
|
||||
expect(() =>
|
||||
computeCursorArgFilter(cursor, orderBy, mockFieldMetadataMap, true),
|
||||
computeCursorArgFilter(
|
||||
cursor,
|
||||
orderBy,
|
||||
objectMetadataItemWithFieldMaps,
|
||||
true,
|
||||
),
|
||||
).toThrow(GraphqlQueryRunnerException);
|
||||
});
|
||||
|
||||
@ -227,7 +273,12 @@ describe('computeCursorArgFilter', () => {
|
||||
const orderBy = [{ age: OrderByDirection.AscNullsLast }];
|
||||
|
||||
expect(() =>
|
||||
computeCursorArgFilter(cursor, orderBy, mockFieldMetadataMap, true),
|
||||
computeCursorArgFilter(
|
||||
cursor,
|
||||
orderBy,
|
||||
objectMetadataItemWithFieldMaps,
|
||||
true,
|
||||
),
|
||||
).toThrow(GraphqlQueryRunnerException);
|
||||
});
|
||||
});
|
||||
|
||||
@ -11,19 +11,19 @@ import {
|
||||
GraphqlQueryRunnerException,
|
||||
GraphqlQueryRunnerExceptionCode,
|
||||
} from 'src/engine/api/graphql/graphql-query-runner/errors/graphql-query-runner.exception';
|
||||
import { buildCursorCompositeFieldWhereCondition } from 'src/engine/api/utils/build-cursor-composite-field-where-condition.utils';
|
||||
import { computeOperator } from 'src/engine/api/utils/compute-operator.utils';
|
||||
import { isAscendingOrder } from 'src/engine/api/utils/is-ascending-order.utils';
|
||||
import { validateAndGetOrderByForScalarField } from 'src/engine/api/utils/validate-and-get-order-by.utils';
|
||||
import { isCompositeFieldMetadataType } from 'src/engine/metadata-modules/field-metadata/utils/is-composite-field-metadata-type.util';
|
||||
import { FieldMetadataMap } from 'src/engine/metadata-modules/types/field-metadata-map';
|
||||
import { buildCursorCompositeFieldWhereCondition } from 'src/engine/api/utils/build-cursor-composite-field-where-condition.utils';
|
||||
import { ObjectMetadataItemWithFieldMaps } from 'src/engine/metadata-modules/types/object-metadata-item-with-field-maps';
|
||||
|
||||
type BuildCursorWhereConditionParams = {
|
||||
cursorKey: keyof ObjectRecord;
|
||||
cursorValue:
|
||||
| ObjectRecordCursorLeafScalarValue
|
||||
| ObjectRecordCursorLeafCompositeValue;
|
||||
fieldMetadataMapByName: FieldMetadataMap;
|
||||
objectMetadataItemWithFieldMaps: ObjectMetadataItemWithFieldMaps;
|
||||
orderBy: ObjectRecordOrderBy;
|
||||
isForwardPagination: boolean;
|
||||
isEqualityCondition?: boolean;
|
||||
@ -32,12 +32,15 @@ type BuildCursorWhereConditionParams = {
|
||||
export const buildCursorWhereCondition = ({
|
||||
cursorKey,
|
||||
cursorValue,
|
||||
fieldMetadataMapByName,
|
||||
objectMetadataItemWithFieldMaps,
|
||||
orderBy,
|
||||
isForwardPagination,
|
||||
isEqualityCondition = false,
|
||||
}: BuildCursorWhereConditionParams): Record<string, unknown> => {
|
||||
const fieldMetadata = fieldMetadataMapByName[cursorKey];
|
||||
const fieldMetadataId =
|
||||
objectMetadataItemWithFieldMaps.fieldIdByName[cursorKey];
|
||||
const fieldMetadata =
|
||||
objectMetadataItemWithFieldMaps.fieldsById[fieldMetadataId];
|
||||
|
||||
if (!fieldMetadata) {
|
||||
throw new GraphqlQueryRunnerException(
|
||||
|
||||
@ -9,13 +9,13 @@ import {
|
||||
} from 'src/engine/api/graphql/workspace-query-builder/interfaces/object-record.interface';
|
||||
|
||||
import { buildCursorCumulativeWhereCondition } from 'src/engine/api/utils/build-cursor-cumulative-where-conditions.utils';
|
||||
import { FieldMetadataMap } from 'src/engine/metadata-modules/types/field-metadata-map';
|
||||
import { buildCursorWhereCondition } from 'src/engine/api/utils/build-cursor-where-condition.utils';
|
||||
import { ObjectMetadataItemWithFieldMaps } from 'src/engine/metadata-modules/types/object-metadata-item-with-field-maps';
|
||||
|
||||
export const computeCursorArgFilter = (
|
||||
cursor: ObjectRecordCursor,
|
||||
orderBy: ObjectRecordOrderBy,
|
||||
fieldMetadataMapByName: FieldMetadataMap,
|
||||
objectMetadataItemWithFieldMaps: ObjectMetadataItemWithFieldMaps,
|
||||
isForwardPagination = true,
|
||||
): ObjectRecordFilter[] => {
|
||||
const cursorEntries = Object.entries(cursor)
|
||||
@ -42,7 +42,7 @@ export const computeCursorArgFilter = (
|
||||
buildCursorWhereCondition({
|
||||
cursorKey,
|
||||
cursorValue,
|
||||
fieldMetadataMapByName,
|
||||
objectMetadataItemWithFieldMaps,
|
||||
orderBy,
|
||||
isForwardPagination: true,
|
||||
isEqualityCondition: true,
|
||||
@ -51,7 +51,7 @@ export const computeCursorArgFilter = (
|
||||
buildCursorWhereCondition({
|
||||
cursorKey,
|
||||
cursorValue,
|
||||
fieldMetadataMapByName,
|
||||
objectMetadataItemWithFieldMaps,
|
||||
orderBy,
|
||||
isForwardPagination,
|
||||
}),
|
||||
|
||||
Reference in New Issue
Block a user