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:
Charles Bochet
2025-06-23 21:06:17 +02:00
committed by GitHub
parent 6aee42ab22
commit d5c974054d
145 changed files with 1485 additions and 2245 deletions

View File

@ -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: {

View File

@ -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: 'Contacts 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(),
},
},
});

View File

@ -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',

View File

@ -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,
);
}

View File

@ -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}`);

View File

@ -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(

View File

@ -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)) {

View File

@ -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,

View File

@ -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(

View File

@ -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,
);
}
}

View File

@ -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;

View File

@ -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,

View File

@ -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,
);

View File

@ -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;

View File

@ -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) {

View File

@ -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) {

View File

@ -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) {

View File

@ -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) {

View File

@ -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) {

View File

@ -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,
);

View File

@ -80,7 +80,7 @@ export class GraphqlQueryFindManyResolverService extends GraphqlQueryBaseResolve
const cursorArgFilter = computeCursorArgFilter(
cursor,
orderByWithIdCondition,
objectMetadataItemWithFieldMaps.fieldsByName,
objectMetadataItemWithFieldMaps,
isForwardPagination,
);

View File

@ -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) {

View File

@ -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) {

View File

@ -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) {

View File

@ -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) {

View File

@ -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;

View File

@ -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

View File

@ -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;

View File

@ -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;

View File

@ -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 =

View File

@ -12,7 +12,7 @@ export class WorkspaceResolverBuilderService {
constructor() {}
shouldBuildResolver(
objectMetadata: ObjectMetadataInterface,
objectMetadata: Pick<ObjectMetadataInterface, 'duplicateCriteria'>,
methodName: WorkspaceResolverBuilderMethodNames,
) {
switch (methodName) {

View File

@ -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,
);
}

View File

@ -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({

View File

@ -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({

View File

@ -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({

View File

@ -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 {

View File

@ -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({

View File

@ -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,
);

View File

@ -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,

View File

@ -31,7 +31,7 @@ export class FindDuplicatesQueryFactory {
}
edges{
node {
${objectMetadata.objectMetadataMapItem.fields
${Object.values(objectMetadata.objectMetadataMapItem.fieldsById)
.map((field) =>
mapFieldMetadataToGraphqlQuery(
objectMetadata.objectMetadataMaps,

View File

@ -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', () => {

View File

@ -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', () => {

View File

@ -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) {

View File

@ -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);

View File

@ -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);

View File

@ -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', () => {

View File

@ -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', () => {

View File

@ -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(',')

View File

@ -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;
};

View File

@ -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 = {

View File

@ -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,
);

View File

@ -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);
});
});

View File

@ -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(

View File

@ -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,
}),