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,
|
||||
}),
|
||||
|
||||
@ -23,25 +23,6 @@ export const mockObjectMetadataItemsWithFieldMaps: ObjectMetadataItemWithFieldMa
|
||||
labelIdentifierFieldMetadataId: 'nameFieldMetadataId',
|
||||
imageIdentifierFieldMetadataId: '',
|
||||
workspaceId: '',
|
||||
fields: [
|
||||
{
|
||||
id: 'nameFieldMetadataId',
|
||||
objectMetadataId: '',
|
||||
type: FieldMetadataType.FULL_NAME,
|
||||
icon: 'test-field-icon',
|
||||
name: 'name',
|
||||
label: 'Name',
|
||||
defaultValue: {
|
||||
lastName: "''",
|
||||
firstName: "''",
|
||||
},
|
||||
description: 'Contact’s name',
|
||||
isCustom: false,
|
||||
isNullable: true,
|
||||
isUnique: false,
|
||||
workspaceId: '',
|
||||
},
|
||||
],
|
||||
indexMetadatas: [],
|
||||
fieldsById: {
|
||||
nameFieldMetadataId: {
|
||||
@ -60,28 +41,15 @@ export const mockObjectMetadataItemsWithFieldMaps: ObjectMetadataItemWithFieldMa
|
||||
isNullable: true,
|
||||
isUnique: false,
|
||||
workspaceId: '',
|
||||
isLabelSyncedWithName: true,
|
||||
createdAt: new Date(),
|
||||
updatedAt: new Date(),
|
||||
},
|
||||
},
|
||||
fieldsByName: {
|
||||
name: {
|
||||
id: 'nameFieldMetadataId',
|
||||
objectMetadataId: '',
|
||||
type: FieldMetadataType.FULL_NAME,
|
||||
icon: 'test-field-icon',
|
||||
name: 'name',
|
||||
label: 'Name',
|
||||
defaultValue: {
|
||||
lastName: "''",
|
||||
firstName: "''",
|
||||
},
|
||||
description: 'Contact’s name',
|
||||
isCustom: false,
|
||||
isNullable: true,
|
||||
isUnique: false,
|
||||
workspaceId: '',
|
||||
},
|
||||
fieldIdByName: {
|
||||
name: 'nameFieldMetadataId',
|
||||
},
|
||||
fieldsByJoinColumnName: {},
|
||||
fieldIdByJoinColumnName: {},
|
||||
},
|
||||
{
|
||||
id: '',
|
||||
@ -102,34 +70,6 @@ export const mockObjectMetadataItemsWithFieldMaps: ObjectMetadataItemWithFieldMa
|
||||
labelIdentifierFieldMetadataId: 'nameFieldMetadataId',
|
||||
imageIdentifierFieldMetadataId: '',
|
||||
workspaceId: '',
|
||||
fields: [
|
||||
{
|
||||
id: 'nameFieldMetadataId',
|
||||
objectMetadataId: '',
|
||||
type: FieldMetadataType.TEXT,
|
||||
icon: 'test-field-icon',
|
||||
name: 'name',
|
||||
label: 'Name',
|
||||
defaultValue: '',
|
||||
isCustom: false,
|
||||
isNullable: true,
|
||||
isUnique: false,
|
||||
workspaceId: '',
|
||||
},
|
||||
{
|
||||
id: 'domainNameFieldMetadataId',
|
||||
objectMetadataId: '',
|
||||
type: FieldMetadataType.LINKS,
|
||||
icon: 'test-field-icon',
|
||||
name: 'domainName',
|
||||
label: 'Domain Name',
|
||||
defaultValue: '',
|
||||
isCustom: false,
|
||||
isNullable: true,
|
||||
isUnique: false,
|
||||
workspaceId: '',
|
||||
},
|
||||
],
|
||||
indexMetadatas: [],
|
||||
fieldsById: {
|
||||
nameFieldMetadataId: {
|
||||
@ -144,6 +84,9 @@ export const mockObjectMetadataItemsWithFieldMaps: ObjectMetadataItemWithFieldMa
|
||||
isNullable: true,
|
||||
isUnique: false,
|
||||
workspaceId: '',
|
||||
isLabelSyncedWithName: true,
|
||||
createdAt: new Date(),
|
||||
updatedAt: new Date(),
|
||||
},
|
||||
domainNameFieldMetadataId: {
|
||||
id: 'domainNameFieldMetadataId',
|
||||
@ -157,40 +100,16 @@ export const mockObjectMetadataItemsWithFieldMaps: ObjectMetadataItemWithFieldMa
|
||||
isNullable: true,
|
||||
isUnique: false,
|
||||
workspaceId: '',
|
||||
isLabelSyncedWithName: true,
|
||||
createdAt: new Date(),
|
||||
updatedAt: new Date(),
|
||||
},
|
||||
},
|
||||
fieldsByName: {
|
||||
name: {
|
||||
id: 'nameFieldMetadataId',
|
||||
objectMetadataId: '',
|
||||
type: FieldMetadataType.TEXT,
|
||||
icon: 'test-field-icon',
|
||||
name: 'name',
|
||||
label: 'Name',
|
||||
defaultValue: {
|
||||
lastName: "''",
|
||||
firstName: "''",
|
||||
},
|
||||
isCustom: false,
|
||||
isNullable: true,
|
||||
isUnique: false,
|
||||
workspaceId: '',
|
||||
},
|
||||
domainName: {
|
||||
id: 'domainNameFieldMetadataId',
|
||||
objectMetadataId: '',
|
||||
type: FieldMetadataType.LINKS,
|
||||
icon: 'test-field-icon',
|
||||
name: 'domainName',
|
||||
label: 'Domain Name',
|
||||
defaultValue: '',
|
||||
isCustom: false,
|
||||
isNullable: true,
|
||||
isUnique: false,
|
||||
workspaceId: '',
|
||||
},
|
||||
fieldIdByName: {
|
||||
name: 'nameFieldMetadataId',
|
||||
domainName: 'domainNameFieldMetadataId',
|
||||
},
|
||||
fieldsByJoinColumnName: {},
|
||||
fieldIdByJoinColumnName: {},
|
||||
},
|
||||
{
|
||||
id: '',
|
||||
@ -211,34 +130,6 @@ export const mockObjectMetadataItemsWithFieldMaps: ObjectMetadataItemWithFieldMa
|
||||
labelIdentifierFieldMetadataId: 'nameFieldMetadataId',
|
||||
imageIdentifierFieldMetadataId: 'imageIdentifierFieldMetadataId',
|
||||
workspaceId: '',
|
||||
fields: [
|
||||
{
|
||||
id: 'nameFieldMetadataId',
|
||||
objectMetadataId: '',
|
||||
type: FieldMetadataType.TEXT,
|
||||
icon: 'test-field-icon',
|
||||
name: 'name',
|
||||
label: 'Name',
|
||||
defaultValue: '',
|
||||
isCustom: false,
|
||||
isNullable: true,
|
||||
isUnique: false,
|
||||
workspaceId: '',
|
||||
},
|
||||
{
|
||||
id: 'imageIdentifierFieldMetadataId',
|
||||
objectMetadataId: '',
|
||||
type: FieldMetadataType.TEXT,
|
||||
icon: 'test-field-icon',
|
||||
name: 'imageIdentifierFieldName',
|
||||
label: 'Image Identifier Field Name',
|
||||
defaultValue: '',
|
||||
isCustom: false,
|
||||
isNullable: true,
|
||||
isUnique: false,
|
||||
workspaceId: '',
|
||||
},
|
||||
],
|
||||
indexMetadatas: [],
|
||||
fieldsById: {
|
||||
nameFieldMetadataId: {
|
||||
@ -253,6 +144,9 @@ export const mockObjectMetadataItemsWithFieldMaps: ObjectMetadataItemWithFieldMa
|
||||
isNullable: true,
|
||||
isUnique: false,
|
||||
workspaceId: '',
|
||||
isLabelSyncedWithName: true,
|
||||
createdAt: new Date(),
|
||||
updatedAt: new Date(),
|
||||
},
|
||||
imageIdentifierFieldMetadataId: {
|
||||
id: 'imageIdentifierFieldMetadataId',
|
||||
@ -266,40 +160,16 @@ export const mockObjectMetadataItemsWithFieldMaps: ObjectMetadataItemWithFieldMa
|
||||
isNullable: true,
|
||||
isUnique: false,
|
||||
workspaceId: '',
|
||||
isLabelSyncedWithName: true,
|
||||
createdAt: new Date(),
|
||||
updatedAt: new Date(),
|
||||
},
|
||||
},
|
||||
fieldsByName: {
|
||||
name: {
|
||||
id: 'nameFieldMetadataId',
|
||||
objectMetadataId: '',
|
||||
type: FieldMetadataType.TEXT,
|
||||
icon: 'test-field-icon',
|
||||
name: 'name',
|
||||
label: 'Name',
|
||||
defaultValue: {
|
||||
lastName: "''",
|
||||
firstName: "''",
|
||||
},
|
||||
isCustom: false,
|
||||
isNullable: true,
|
||||
isUnique: false,
|
||||
workspaceId: '',
|
||||
},
|
||||
imageIdentifierFieldName: {
|
||||
id: 'imageIdentifierFieldMetadataId',
|
||||
objectMetadataId: '',
|
||||
type: FieldMetadataType.TEXT,
|
||||
icon: 'test-field-icon',
|
||||
name: 'imageIdentifierFieldName',
|
||||
label: 'Image Identifier Field Name',
|
||||
defaultValue: '',
|
||||
isCustom: false,
|
||||
isNullable: true,
|
||||
isUnique: false,
|
||||
workspaceId: '',
|
||||
},
|
||||
fieldIdByName: {
|
||||
name: 'nameFieldMetadataId',
|
||||
imageIdentifierFieldName: 'imageIdentifierFieldMetadataId',
|
||||
},
|
||||
fieldsByJoinColumnName: {},
|
||||
fieldIdByJoinColumnName: {},
|
||||
},
|
||||
{
|
||||
id: '',
|
||||
@ -320,10 +190,9 @@ export const mockObjectMetadataItemsWithFieldMaps: ObjectMetadataItemWithFieldMa
|
||||
labelIdentifierFieldMetadataId: '',
|
||||
imageIdentifierFieldMetadataId: '',
|
||||
workspaceId: '',
|
||||
fields: [],
|
||||
indexMetadatas: [],
|
||||
fieldsById: {},
|
||||
fieldsByName: {},
|
||||
fieldsByJoinColumnName: {},
|
||||
fieldIdByName: {},
|
||||
fieldIdByJoinColumnName: {},
|
||||
},
|
||||
];
|
||||
|
||||
@ -21,13 +21,13 @@ import {
|
||||
import { JwtWrapperService } from 'src/engine/core-modules/jwt/services/jwt-wrapper.service';
|
||||
import { TwentyConfigService } from 'src/engine/core-modules/twenty-config/twenty-config.service';
|
||||
import { UserWorkspace } from 'src/engine/core-modules/user-workspace/user-workspace.entity';
|
||||
import { userWorkspaceValidator } from 'src/engine/core-modules/user-workspace/user-workspace.validate';
|
||||
import { User } from 'src/engine/core-modules/user/user.entity';
|
||||
import { userValidator } from 'src/engine/core-modules/user/user.validate';
|
||||
import { Workspace } from 'src/engine/core-modules/workspace/workspace.entity';
|
||||
import { workspaceValidator } from 'src/engine/core-modules/workspace/workspace.validate';
|
||||
import { TwentyORMGlobalManager } from 'src/engine/twenty-orm/twenty-orm-global.manager';
|
||||
import { WorkspaceMemberWorkspaceEntity } from 'src/modules/workspace-member/standard-objects/workspace-member.workspace-entity';
|
||||
import { userWorkspaceValidator } from 'src/engine/core-modules/user-workspace/user-workspace.validate';
|
||||
|
||||
@Injectable()
|
||||
export class AccessTokenService {
|
||||
@ -78,9 +78,6 @@ export class AccessTokenService {
|
||||
await this.twentyORMGlobalManager.getRepositoryForWorkspace<WorkspaceMemberWorkspaceEntity>(
|
||||
workspaceId,
|
||||
'workspaceMember',
|
||||
{
|
||||
shouldFailIfMetadataNotFound: false,
|
||||
},
|
||||
);
|
||||
|
||||
const workspaceMember = await workspaceMemberRepository.findOne({
|
||||
|
||||
@ -13,6 +13,6 @@ export class ObjectRecordBaseEvent<T = object> {
|
||||
recordId: string;
|
||||
userId?: string;
|
||||
workspaceMemberId?: string;
|
||||
objectMetadata: ObjectMetadataInterface;
|
||||
objectMetadata: Omit<ObjectMetadataInterface, 'indexMetadatas'>;
|
||||
properties: Properties<T>;
|
||||
}
|
||||
|
||||
@ -4,6 +4,7 @@ import { objectRecordChangedValues } from 'src/engine/core-modules/event-emitter
|
||||
|
||||
const mockObjectMetadata: ObjectMetadataInterface = {
|
||||
id: '1',
|
||||
icon: 'Icon123',
|
||||
nameSingular: 'Object',
|
||||
namePlural: 'Objects',
|
||||
labelSingular: 'Object',
|
||||
|
||||
@ -74,10 +74,7 @@ export class FeatureFlagService {
|
||||
);
|
||||
|
||||
await this.workspaceFeatureFlagsMapCacheService.recomputeFeatureFlagsMapCache(
|
||||
{
|
||||
workspaceId,
|
||||
ignoreLock: true,
|
||||
},
|
||||
{ workspaceId },
|
||||
);
|
||||
}
|
||||
}
|
||||
@ -132,10 +129,7 @@ export class FeatureFlagService {
|
||||
const result = await this.featureFlagRepository.save(featureFlagToSave);
|
||||
|
||||
await this.workspaceFeatureFlagsMapCacheService.recomputeFeatureFlagsMapCache(
|
||||
{
|
||||
workspaceId,
|
||||
ignoreLock: true,
|
||||
},
|
||||
{ workspaceId },
|
||||
);
|
||||
|
||||
return result;
|
||||
|
||||
@ -3,8 +3,6 @@ import { Injectable } from '@nestjs/common';
|
||||
import { FieldMetadataType } from 'twenty-shared/types';
|
||||
import { isDefined } from 'twenty-shared/utils';
|
||||
|
||||
import { FieldMetadataInterface } from 'src/engine/metadata-modules/field-metadata/interfaces/field-metadata.interface';
|
||||
|
||||
import { transformLinksValue } from 'src/engine/core-modules/record-transformer/utils/transform-links-value.util';
|
||||
import { transformPhonesValue } from 'src/engine/core-modules/record-transformer/utils/transform-phones-value.util';
|
||||
import { compositeTypeDefinitions } from 'src/engine/metadata-modules/field-metadata/composite-types';
|
||||
@ -29,19 +27,11 @@ export class RecordInputTransformerService {
|
||||
return recordInput;
|
||||
}
|
||||
|
||||
const fieldMetadataByFieldName = objectMetadataMapItem.fields.reduce(
|
||||
(acc, field) => {
|
||||
acc[field.name] = field;
|
||||
|
||||
return acc;
|
||||
},
|
||||
{} as Record<string, FieldMetadataInterface>,
|
||||
);
|
||||
|
||||
let transformedEntries = {};
|
||||
|
||||
for (const [key, value] of Object.entries(recordInput)) {
|
||||
const fieldMetadata = fieldMetadataByFieldName[key];
|
||||
const fieldMetadataId = objectMetadataMapItem.fieldIdByName[key];
|
||||
const fieldMetadata = objectMetadataMapItem.fieldsById[fieldMetadataId];
|
||||
|
||||
if (!fieldMetadata) {
|
||||
transformedEntries = { ...transformedEntries, [key]: value };
|
||||
|
||||
@ -163,9 +163,13 @@ export class SearchService {
|
||||
const queryBuilder = entityManager.createQueryBuilder();
|
||||
|
||||
const queryParser = new GraphqlQueryParser(
|
||||
objectMetadataItem.fieldsByName,
|
||||
objectMetadataItem.fieldsByJoinColumnName,
|
||||
generateObjectMetadataMaps([objectMetadataItem]),
|
||||
objectMetadataItem,
|
||||
generateObjectMetadataMaps([
|
||||
{
|
||||
...objectMetadataItem,
|
||||
fields: Object.values(objectMetadataItem.fieldsById),
|
||||
},
|
||||
]),
|
||||
);
|
||||
|
||||
queryParser.applyFilterToBuilder(
|
||||
|
||||
@ -2,9 +2,12 @@ import DataLoader from 'dataloader';
|
||||
|
||||
import {
|
||||
FieldMetadataLoaderPayload,
|
||||
IndexMetadataLoaderPayload,
|
||||
RelationLoaderPayload,
|
||||
} from 'src/engine/dataloaders/dataloader.service';
|
||||
import { FieldMetadataDTO } from 'src/engine/metadata-modules/field-metadata/dtos/field-metadata.dto';
|
||||
import { FieldMetadataEntity } from 'src/engine/metadata-modules/field-metadata/field-metadata.entity';
|
||||
import { IndexMetadataDTO } from 'src/engine/metadata-modules/index-metadata/dtos/index-metadata.dto';
|
||||
import { ObjectMetadataEntity } from 'src/engine/metadata-modules/object-metadata/object-metadata.entity';
|
||||
|
||||
export interface IDataloaders {
|
||||
@ -20,6 +23,11 @@ export interface IDataloaders {
|
||||
|
||||
fieldMetadataLoader: DataLoader<
|
||||
FieldMetadataLoaderPayload,
|
||||
FieldMetadataEntity[]
|
||||
FieldMetadataDTO[]
|
||||
>;
|
||||
|
||||
indexMetadataLoader: DataLoader<
|
||||
IndexMetadataLoaderPayload,
|
||||
IndexMetadataDTO[]
|
||||
>;
|
||||
}
|
||||
|
||||
@ -2,9 +2,10 @@ import { Module } from '@nestjs/common';
|
||||
|
||||
import { DataloaderService } from 'src/engine/dataloaders/dataloader.service';
|
||||
import { FieldMetadataModule } from 'src/engine/metadata-modules/field-metadata/field-metadata.module';
|
||||
import { WorkspaceMetadataCacheModule } from 'src/engine/metadata-modules/workspace-metadata-cache/workspace-metadata-cache.module';
|
||||
|
||||
@Module({
|
||||
imports: [FieldMetadataModule],
|
||||
imports: [FieldMetadataModule, WorkspaceMetadataCacheModule],
|
||||
providers: [DataloaderService],
|
||||
exports: [DataloaderService],
|
||||
})
|
||||
|
||||
@ -6,10 +6,13 @@ import { FieldMetadataInterface } from 'src/engine/metadata-modules/field-metada
|
||||
import { ObjectMetadataInterface } from 'src/engine/metadata-modules/field-metadata/interfaces/object-metadata.interface';
|
||||
|
||||
import { IDataloaders } from 'src/engine/dataloaders/dataloader.interface';
|
||||
import { FieldMetadataDTO } from 'src/engine/metadata-modules/field-metadata/dtos/field-metadata.dto';
|
||||
import { FieldMetadataEntity } from 'src/engine/metadata-modules/field-metadata/field-metadata.entity';
|
||||
import { FieldMetadataService } from 'src/engine/metadata-modules/field-metadata/field-metadata.service';
|
||||
import { FieldMetadataRelationService } from 'src/engine/metadata-modules/field-metadata/relation/field-metadata-relation.service';
|
||||
import { IndexMetadataDTO } from 'src/engine/metadata-modules/index-metadata/dtos/index-metadata.dto';
|
||||
import { ObjectMetadataEntity } from 'src/engine/metadata-modules/object-metadata/object-metadata.entity';
|
||||
import { WorkspaceMetadataCacheService } from 'src/engine/metadata-modules/workspace-metadata-cache/services/workspace-metadata-cache.service';
|
||||
|
||||
export type RelationMetadataLoaderPayload = {
|
||||
workspaceId: string;
|
||||
@ -36,20 +39,28 @@ export type FieldMetadataLoaderPayload = {
|
||||
objectMetadata: Pick<ObjectMetadataInterface, 'id'>;
|
||||
};
|
||||
|
||||
export type IndexMetadataLoaderPayload = {
|
||||
workspaceId: string;
|
||||
objectMetadata: Pick<ObjectMetadataInterface, 'id'>;
|
||||
};
|
||||
|
||||
@Injectable()
|
||||
export class DataloaderService {
|
||||
constructor(
|
||||
private readonly fieldMetadataRelationService: FieldMetadataRelationService,
|
||||
private readonly fieldMetadataService: FieldMetadataService,
|
||||
private readonly workspaceMetadataCacheService: WorkspaceMetadataCacheService,
|
||||
) {}
|
||||
|
||||
createLoaders(): IDataloaders {
|
||||
const relationLoader = this.createRelationLoader();
|
||||
const fieldMetadataLoader = this.createFieldMetadataLoader();
|
||||
const indexMetadataLoader = this.createIndexMetadataLoader();
|
||||
|
||||
return {
|
||||
relationLoader,
|
||||
fieldMetadataLoader,
|
||||
indexMetadataLoader,
|
||||
};
|
||||
}
|
||||
|
||||
@ -78,20 +89,67 @@ export class DataloaderService {
|
||||
});
|
||||
}
|
||||
|
||||
private createFieldMetadataLoader() {
|
||||
return new DataLoader<FieldMetadataLoaderPayload, FieldMetadataEntity[]>(
|
||||
async (dataLoaderParams: FieldMetadataLoaderPayload[]) => {
|
||||
private createIndexMetadataLoader() {
|
||||
return new DataLoader<IndexMetadataLoaderPayload, IndexMetadataDTO[]>(
|
||||
async (dataLoaderParams: IndexMetadataLoaderPayload[]) => {
|
||||
const workspaceId = dataLoaderParams[0].workspaceId;
|
||||
const objectMetadataItems = dataLoaderParams.map(
|
||||
(dataLoaderParam) => dataLoaderParam.objectMetadata,
|
||||
const objectMetadataIds = dataLoaderParams.map(
|
||||
(dataLoaderParam) => dataLoaderParam.objectMetadata.id,
|
||||
);
|
||||
|
||||
const fieldMetadataCollection =
|
||||
await this.fieldMetadataService.getFieldMetadataItemsByBatch(
|
||||
objectMetadataItems.map((item) => item.id),
|
||||
workspaceId,
|
||||
const { objectMetadataMaps } =
|
||||
await this.workspaceMetadataCacheService.getExistingOrRecomputeMetadataMaps(
|
||||
{ workspaceId },
|
||||
);
|
||||
|
||||
const indexMetadataCollection = objectMetadataIds.map((id) =>
|
||||
Object.values(objectMetadataMaps.byId[id].indexMetadatas).map(
|
||||
(indexMetadata) => {
|
||||
return {
|
||||
...indexMetadata,
|
||||
createdAt: new Date(indexMetadata.createdAt),
|
||||
updatedAt: new Date(indexMetadata.updatedAt),
|
||||
id: indexMetadata.id,
|
||||
indexWhereClause: indexMetadata.indexWhereClause ?? undefined,
|
||||
objectMetadataId: id,
|
||||
workspaceId: workspaceId,
|
||||
};
|
||||
},
|
||||
),
|
||||
);
|
||||
|
||||
return indexMetadataCollection;
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
private createFieldMetadataLoader() {
|
||||
return new DataLoader<FieldMetadataLoaderPayload, FieldMetadataDTO[]>(
|
||||
async (dataLoaderParams: FieldMetadataLoaderPayload[]) => {
|
||||
const workspaceId = dataLoaderParams[0].workspaceId;
|
||||
const objectMetadataIds = dataLoaderParams.map(
|
||||
(dataLoaderParam) => dataLoaderParam.objectMetadata.id,
|
||||
);
|
||||
|
||||
const { objectMetadataMaps } =
|
||||
await this.workspaceMetadataCacheService.getExistingOrRecomputeMetadataMaps(
|
||||
{ workspaceId },
|
||||
);
|
||||
|
||||
const fieldMetadataCollection = objectMetadataIds.map((id) =>
|
||||
Object.values(objectMetadataMaps.byId[id].fieldsById).map(
|
||||
// TODO: fix this as we should merge FieldMetadataEntity and FieldMetadataInterface
|
||||
(fieldMetadata) => {
|
||||
return {
|
||||
...fieldMetadata,
|
||||
createdAt: new Date(fieldMetadata.createdAt),
|
||||
updatedAt: new Date(fieldMetadata.updatedAt),
|
||||
workspaceId: workspaceId,
|
||||
};
|
||||
},
|
||||
),
|
||||
);
|
||||
|
||||
return fieldMetadataCollection;
|
||||
},
|
||||
);
|
||||
|
||||
@ -1,11 +1,12 @@
|
||||
import {
|
||||
Column,
|
||||
CreateDateColumn,
|
||||
DataSourceOptions,
|
||||
Entity,
|
||||
Index,
|
||||
OneToMany,
|
||||
PrimaryGeneratedColumn,
|
||||
UpdateDateColumn,
|
||||
DataSourceOptions,
|
||||
OneToMany,
|
||||
} from 'typeorm';
|
||||
|
||||
import { ObjectMetadataEntity } from 'src/engine/metadata-modules/object-metadata/object-metadata.entity';
|
||||
@ -13,6 +14,7 @@ import { ObjectMetadataEntity } from 'src/engine/metadata-modules/object-metadat
|
||||
export type DataSourceType = DataSourceOptions['type'];
|
||||
|
||||
@Entity('dataSource')
|
||||
@Index('IDX_DATA_SOURCE_WORKSPACE_ID_CREATED_AT', ['workspaceId', 'createdAt'])
|
||||
export class DataSourceEntity {
|
||||
@PrimaryGeneratedColumn('uuid')
|
||||
id: string;
|
||||
|
||||
@ -17,7 +17,6 @@ import {
|
||||
import { FieldMetadataDefaultValue } from 'src/engine/metadata-modules/field-metadata/interfaces/field-metadata-default-value.interface';
|
||||
import { FieldMetadataOptions } from 'src/engine/metadata-modules/field-metadata/interfaces/field-metadata-options.interface';
|
||||
import { FieldMetadataSettings } from 'src/engine/metadata-modules/field-metadata/interfaces/field-metadata-settings.interface';
|
||||
import { FieldMetadataInterface } from 'src/engine/metadata-modules/field-metadata/interfaces/field-metadata.interface';
|
||||
|
||||
import { FieldStandardOverridesDTO } from 'src/engine/metadata-modules/field-metadata/dtos/field-standard-overrides.dto';
|
||||
import { IndexFieldMetadataEntity } from 'src/engine/metadata-modules/index-metadata/index-field-metadata.entity';
|
||||
@ -41,8 +40,7 @@ import { ObjectMetadataEntity } from 'src/engine/metadata-modules/object-metadat
|
||||
])
|
||||
export class FieldMetadataEntity<
|
||||
T extends FieldMetadataType = FieldMetadataType,
|
||||
> implements FieldMetadataInterface<T>
|
||||
{
|
||||
> {
|
||||
@PrimaryGeneratedColumn('uuid')
|
||||
id: string;
|
||||
|
||||
|
||||
@ -24,6 +24,7 @@ import { IsFieldMetadataOptions } from 'src/engine/metadata-modules/field-metada
|
||||
import { ObjectMetadataEntity } from 'src/engine/metadata-modules/object-metadata/object-metadata.entity';
|
||||
import { ObjectMetadataModule } from 'src/engine/metadata-modules/object-metadata/object-metadata.module';
|
||||
import { PermissionsModule } from 'src/engine/metadata-modules/permissions/permissions.module';
|
||||
import { WorkspaceMetadataCacheModule } from 'src/engine/metadata-modules/workspace-metadata-cache/workspace-metadata-cache.module';
|
||||
import { WorkspaceMetadataVersionModule } from 'src/engine/metadata-modules/workspace-metadata-version/workspace-metadata-version.module';
|
||||
import { WorkspaceMigrationModule } from 'src/engine/metadata-modules/workspace-migration/workspace-migration.module';
|
||||
import { WorkspaceCacheStorageModule } from 'src/engine/workspace-cache-storage/workspace-cache-storage.module';
|
||||
@ -54,6 +55,7 @@ import { UpdateFieldInput } from './dtos/update-field.input';
|
||||
ActorModule,
|
||||
ViewModule,
|
||||
PermissionsModule,
|
||||
WorkspaceMetadataCacheModule,
|
||||
],
|
||||
services: [
|
||||
IsFieldMetadataDefaultValue,
|
||||
|
||||
@ -10,6 +10,7 @@ import { isDefined } from 'twenty-shared/utils';
|
||||
import { DataSource, FindOneOptions, In, Repository } from 'typeorm';
|
||||
import { v4 as uuidV4, v4 } from 'uuid';
|
||||
|
||||
import { FieldMetadataInterface } from 'src/engine/metadata-modules/field-metadata/interfaces/field-metadata.interface';
|
||||
import { RelationType } from 'src/engine/metadata-modules/field-metadata/interfaces/relation-type.interface';
|
||||
|
||||
import { settings } from 'src/engine/constants/settings';
|
||||
@ -40,9 +41,9 @@ import { generateNullable } from 'src/engine/metadata-modules/field-metadata/uti
|
||||
import { isCompositeFieldMetadataType } from 'src/engine/metadata-modules/field-metadata/utils/is-composite-field-metadata-type.util';
|
||||
import { isEnumFieldMetadataType } from 'src/engine/metadata-modules/field-metadata/utils/is-enum-field-metadata-type.util';
|
||||
import { isSelectOrMultiSelectFieldMetadata } from 'src/engine/metadata-modules/field-metadata/utils/is-select-or-multi-select-field-metadata.util';
|
||||
import { ObjectMetadataEntity } from 'src/engine/metadata-modules/object-metadata/object-metadata.entity';
|
||||
import { assertMutationNotOnRemoteObject } from 'src/engine/metadata-modules/object-metadata/utils/assert-mutation-not-on-remote-object.util';
|
||||
import { RelationOnDeleteAction } from 'src/engine/metadata-modules/relation-metadata/relation-on-delete-action.type';
|
||||
import { ObjectMetadataItemWithFieldMaps } from 'src/engine/metadata-modules/types/object-metadata-item-with-field-maps';
|
||||
import { InvalidMetadataException } from 'src/engine/metadata-modules/utils/exceptions/invalid-metadata.exception';
|
||||
import { validateFieldNameAvailabilityOrThrow } from 'src/engine/metadata-modules/utils/validate-field-name-availability.utils';
|
||||
import { validateMetadataNameOrThrow } from 'src/engine/metadata-modules/utils/validate-metadata-name.utils';
|
||||
@ -50,6 +51,7 @@ import {
|
||||
computeMetadataNameFromLabel,
|
||||
validateNameAndLabelAreSyncOrThrow,
|
||||
} from 'src/engine/metadata-modules/utils/validate-name-and-label-are-sync-or-throw.util';
|
||||
import { WorkspaceMetadataCacheService } from 'src/engine/metadata-modules/workspace-metadata-cache/services/workspace-metadata-cache.service';
|
||||
import { WorkspaceMetadataVersionService } from 'src/engine/metadata-modules/workspace-metadata-version/services/workspace-metadata-version.service';
|
||||
import { generateMigrationName } from 'src/engine/metadata-modules/workspace-migration/utils/generate-migration-name.util';
|
||||
import {
|
||||
@ -78,8 +80,8 @@ type ValidateFieldMetadataArgs<T extends UpdateFieldInput | CreateFieldInput> =
|
||||
{
|
||||
fieldMetadataType: FieldMetadataType;
|
||||
fieldMetadataInput: T;
|
||||
objectMetadata: ObjectMetadataEntity;
|
||||
existingFieldMetadata?: FieldMetadataEntity;
|
||||
objectMetadata: ObjectMetadataItemWithFieldMaps;
|
||||
existingFieldMetadata?: FieldMetadataInterface;
|
||||
};
|
||||
|
||||
@Injectable()
|
||||
@ -89,8 +91,6 @@ export class FieldMetadataService extends TypeOrmQueryService<FieldMetadataEntit
|
||||
private readonly coreDataSource: DataSource,
|
||||
@InjectRepository(FieldMetadataEntity, 'core')
|
||||
private readonly fieldMetadataRepository: Repository<FieldMetadataEntity>,
|
||||
@InjectRepository(ObjectMetadataEntity, 'core')
|
||||
private readonly objectMetadataRepository: Repository<ObjectMetadataEntity>,
|
||||
private readonly workspaceMigrationFactory: WorkspaceMigrationFactory,
|
||||
private readonly workspaceMigrationService: WorkspaceMigrationService,
|
||||
private readonly workspaceMigrationRunnerService: WorkspaceMigrationRunnerService,
|
||||
@ -100,6 +100,7 @@ export class FieldMetadataService extends TypeOrmQueryService<FieldMetadataEntit
|
||||
private readonly fieldMetadataValidationService: FieldMetadataValidationService,
|
||||
private readonly fieldMetadataRelatedRecordsService: FieldMetadataRelatedRecordsService,
|
||||
private readonly viewService: ViewService,
|
||||
private readonly workspaceMetadataCacheService: WorkspaceMetadataCacheService,
|
||||
) {
|
||||
super(fieldMetadataRepository);
|
||||
}
|
||||
@ -123,54 +124,49 @@ export class FieldMetadataService extends TypeOrmQueryService<FieldMetadataEntit
|
||||
id: string,
|
||||
fieldMetadataInput: UpdateFieldInput,
|
||||
): Promise<FieldMetadataEntity> {
|
||||
const { objectMetadataMaps } =
|
||||
await this.workspaceMetadataCacheService.getExistingOrRecomputeMetadataMaps(
|
||||
{ workspaceId: fieldMetadataInput.workspaceId },
|
||||
);
|
||||
|
||||
let existingFieldMetadata: FieldMetadataInterface | undefined;
|
||||
|
||||
for (const objectMetadataItem of Object.values(objectMetadataMaps.byId)) {
|
||||
const fieldMetadata = objectMetadataItem.fieldsById[id];
|
||||
|
||||
if (fieldMetadata) {
|
||||
existingFieldMetadata = fieldMetadata;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if (!isDefined(existingFieldMetadata)) {
|
||||
throw new FieldMetadataException(
|
||||
'Field does not exist',
|
||||
FieldMetadataExceptionCode.FIELD_METADATA_NOT_FOUND,
|
||||
);
|
||||
}
|
||||
|
||||
const objectMetadataItemWithFieldMaps =
|
||||
objectMetadataMaps.byId[existingFieldMetadata.objectMetadataId];
|
||||
|
||||
const queryRunner = this.coreDataSource.createQueryRunner();
|
||||
|
||||
await queryRunner.connect();
|
||||
await queryRunner.startTransaction();
|
||||
|
||||
try {
|
||||
const fieldMetadataRepository =
|
||||
queryRunner.manager.getRepository<FieldMetadataEntity>(
|
||||
FieldMetadataEntity,
|
||||
);
|
||||
|
||||
const [existingFieldMetadata] = await fieldMetadataRepository.find({
|
||||
where: {
|
||||
id,
|
||||
workspaceId: fieldMetadataInput.workspaceId,
|
||||
},
|
||||
});
|
||||
|
||||
if (!isDefined(existingFieldMetadata)) {
|
||||
throw new FieldMetadataException(
|
||||
'Field does not exist',
|
||||
FieldMetadataExceptionCode.FIELD_METADATA_NOT_FOUND,
|
||||
);
|
||||
}
|
||||
|
||||
const [objectMetadata] = await this.objectMetadataRepository.find({
|
||||
where: {
|
||||
id: existingFieldMetadata.objectMetadataId,
|
||||
workspaceId: fieldMetadataInput.workspaceId,
|
||||
},
|
||||
relations: ['fields'],
|
||||
order: {},
|
||||
});
|
||||
|
||||
if (!isDefined(objectMetadata)) {
|
||||
throw new FieldMetadataException(
|
||||
'Object metadata does not exist',
|
||||
FieldMetadataExceptionCode.OBJECT_METADATA_NOT_FOUND,
|
||||
);
|
||||
}
|
||||
|
||||
if (!isDefined(objectMetadata.labelIdentifierFieldMetadataId)) {
|
||||
if (
|
||||
!isDefined(
|
||||
objectMetadataItemWithFieldMaps.labelIdentifierFieldMetadataId,
|
||||
)
|
||||
) {
|
||||
throw new FieldMetadataException(
|
||||
'Label identifier field metadata id does not exist',
|
||||
FieldMetadataExceptionCode.LABEL_IDENTIFIER_FIELD_METADATA_ID_NOT_FOUND,
|
||||
);
|
||||
}
|
||||
assertMutationNotOnRemoteObject(objectMetadata);
|
||||
assertMutationNotOnRemoteObject(objectMetadataItemWithFieldMaps);
|
||||
|
||||
assertDoesNotNullifyDefaultValueForNonNullableField({
|
||||
isNullable: existingFieldMetadata.isNullable,
|
||||
@ -180,19 +176,9 @@ export class FieldMetadataService extends TypeOrmQueryService<FieldMetadataEntit
|
||||
if (fieldMetadataInput.isActive === false) {
|
||||
checkCanDeactivateFieldOrThrow({
|
||||
labelIdentifierFieldMetadataId:
|
||||
objectMetadata.labelIdentifierFieldMetadataId,
|
||||
objectMetadataItemWithFieldMaps.labelIdentifierFieldMetadataId,
|
||||
existingFieldMetadata,
|
||||
});
|
||||
|
||||
const viewsRepository =
|
||||
await this.twentyORMGlobalManager.getRepositoryForWorkspace(
|
||||
fieldMetadataInput.workspaceId,
|
||||
'view',
|
||||
);
|
||||
|
||||
await viewsRepository.delete({
|
||||
kanbanFieldMetadataId: id,
|
||||
});
|
||||
}
|
||||
|
||||
const updatableFieldInput =
|
||||
@ -221,7 +207,7 @@ export class FieldMetadataService extends TypeOrmQueryService<FieldMetadataEntit
|
||||
fieldMetadataType: existingFieldMetadata.type,
|
||||
existingFieldMetadata,
|
||||
fieldMetadataInput: fieldMetadataForUpdate,
|
||||
objectMetadata,
|
||||
objectMetadata: objectMetadataItemWithFieldMaps,
|
||||
});
|
||||
|
||||
const isLabelSyncedWithName =
|
||||
@ -236,9 +222,9 @@ export class FieldMetadataService extends TypeOrmQueryService<FieldMetadataEntit
|
||||
}
|
||||
|
||||
// We're running field update under a transaction, so we can rollback if migration fails
|
||||
await fieldMetadataRepository.update(id, fieldMetadataForUpdate);
|
||||
await this.fieldMetadataRepository.update(id, fieldMetadataForUpdate);
|
||||
|
||||
const [updatedFieldMetadata] = await fieldMetadataRepository.find({
|
||||
const [updatedFieldMetadata] = await this.fieldMetadataRepository.find({
|
||||
where: { id },
|
||||
});
|
||||
|
||||
@ -249,6 +235,18 @@ export class FieldMetadataService extends TypeOrmQueryService<FieldMetadataEntit
|
||||
);
|
||||
}
|
||||
|
||||
if (fieldMetadataInput.isActive === false) {
|
||||
const viewsRepository =
|
||||
await this.twentyORMGlobalManager.getRepositoryForWorkspace(
|
||||
fieldMetadataInput.workspaceId,
|
||||
'view',
|
||||
);
|
||||
|
||||
await viewsRepository.delete({
|
||||
kanbanFieldMetadataId: id,
|
||||
});
|
||||
}
|
||||
|
||||
if (
|
||||
updatedFieldMetadata.isActive &&
|
||||
isSelectOrMultiSelectFieldMetadata(updatedFieldMetadata) &&
|
||||
@ -272,10 +270,10 @@ export class FieldMetadataService extends TypeOrmQueryService<FieldMetadataEntit
|
||||
) {
|
||||
await this.workspaceMigrationService.createCustomMigration(
|
||||
generateMigrationName(`update-${updatedFieldMetadata.name}`),
|
||||
existingFieldMetadata.workspaceId,
|
||||
fieldMetadataInput.workspaceId,
|
||||
[
|
||||
{
|
||||
name: computeObjectTargetTable(objectMetadata),
|
||||
name: computeObjectTargetTable(objectMetadataItemWithFieldMaps),
|
||||
action: WorkspaceMigrationTableActionType.ALTER,
|
||||
columns: this.workspaceMigrationFactory.createColumnActions(
|
||||
WorkspaceMigrationColumnActionType.ALTER,
|
||||
@ -299,6 +297,7 @@ export class FieldMetadataService extends TypeOrmQueryService<FieldMetadataEntit
|
||||
throw error;
|
||||
} finally {
|
||||
await queryRunner.release();
|
||||
|
||||
await this.workspaceMetadataVersionService.incrementMetadataVersion(
|
||||
fieldMetadataInput.workspaceId,
|
||||
);
|
||||
@ -470,28 +469,6 @@ export class FieldMetadataService extends TypeOrmQueryService<FieldMetadataEntit
|
||||
}
|
||||
}
|
||||
|
||||
public async findOneOrFail(
|
||||
id: string,
|
||||
options?: FindOneOptions<FieldMetadataEntity>,
|
||||
) {
|
||||
const [fieldMetadata] = await this.fieldMetadataRepository.find({
|
||||
...options,
|
||||
where: {
|
||||
...options?.where,
|
||||
id,
|
||||
},
|
||||
});
|
||||
|
||||
if (!fieldMetadata) {
|
||||
throw new FieldMetadataException(
|
||||
'Field does not exist',
|
||||
FieldMetadataExceptionCode.FIELD_METADATA_NOT_FOUND,
|
||||
);
|
||||
}
|
||||
|
||||
return fieldMetadata;
|
||||
}
|
||||
|
||||
public async findOneWithinWorkspace(
|
||||
workspaceId: string,
|
||||
options: FindOneOptions<FieldMetadataEntity>,
|
||||
@ -509,7 +486,10 @@ export class FieldMetadataService extends TypeOrmQueryService<FieldMetadataEntit
|
||||
|
||||
private buildUpdatableStandardFieldInput(
|
||||
fieldMetadataInput: UpdateFieldInput,
|
||||
existingFieldMetadata: FieldMetadataEntity,
|
||||
existingFieldMetadata: Pick<
|
||||
FieldMetadataInterface,
|
||||
'type' | 'isNullable' | 'defaultValue' | 'options'
|
||||
>,
|
||||
) {
|
||||
const updatableStandardFieldInput: UpdateFieldInput & {
|
||||
standardOverrides?: FieldStandardOverridesDTO;
|
||||
@ -754,7 +734,7 @@ export class FieldMetadataService extends TypeOrmQueryService<FieldMetadataEntit
|
||||
|
||||
private async validateAndCreateFieldMetadataItems(
|
||||
fieldMetadataInput: CreateFieldInput,
|
||||
objectMetadata: ObjectMetadataEntity,
|
||||
objectMetadata: ObjectMetadataItemWithFieldMaps,
|
||||
fieldMetadataRepository: Repository<FieldMetadataEntity>,
|
||||
): Promise<FieldMetadataEntity[]> {
|
||||
if (!fieldMetadataInput.isRemoteCreation) {
|
||||
@ -841,7 +821,7 @@ export class FieldMetadataService extends TypeOrmQueryService<FieldMetadataEntit
|
||||
isRemoteCreation,
|
||||
}: {
|
||||
createdFieldMetadataItems: FieldMetadataEntity[];
|
||||
objectMetadataMap: Record<string, ObjectMetadataEntity>;
|
||||
objectMetadataMap: Record<string, ObjectMetadataItemWithFieldMaps>;
|
||||
isRemoteCreation: boolean;
|
||||
}): Promise<WorkspaceMigrationTableAction[]> {
|
||||
if (isRemoteCreation) {
|
||||
@ -886,6 +866,11 @@ export class FieldMetadataService extends TypeOrmQueryService<FieldMetadataEntit
|
||||
return [];
|
||||
}
|
||||
|
||||
const { objectMetadataMaps } =
|
||||
await this.workspaceMetadataCacheService.getExistingOrRecomputeMetadataMaps(
|
||||
{ workspaceId: fieldMetadataInputs[0].workspaceId },
|
||||
);
|
||||
|
||||
const workspaceId = fieldMetadataInputs[0].workspaceId;
|
||||
const queryRunner = this.coreDataSource.createQueryRunner();
|
||||
|
||||
@ -902,23 +887,11 @@ export class FieldMetadataService extends TypeOrmQueryService<FieldMetadataEntit
|
||||
this.groupFieldInputsByObjectId(fieldMetadataInputs);
|
||||
const objectMetadataIds = Object.keys(inputsByObjectId);
|
||||
|
||||
const objectMetadatas = await this.objectMetadataRepository.find({
|
||||
where: {
|
||||
workspaceId,
|
||||
},
|
||||
relations: ['fields'],
|
||||
});
|
||||
|
||||
const objectMetadataMap = objectMetadatas.reduce(
|
||||
(acc, obj) => ({ ...acc, [obj.id]: obj }),
|
||||
{} as Record<string, ObjectMetadataEntity>,
|
||||
);
|
||||
|
||||
const createdFieldMetadatas: FieldMetadataEntity[] = [];
|
||||
const migrationActions: WorkspaceMigrationTableAction[] = [];
|
||||
|
||||
for (const objectMetadataId of objectMetadataIds) {
|
||||
const objectMetadata = objectMetadataMap[objectMetadataId];
|
||||
const objectMetadata = objectMetadataMaps.byId[objectMetadataId];
|
||||
|
||||
if (!isDefined(objectMetadata)) {
|
||||
throw new FieldMetadataException(
|
||||
@ -941,7 +914,7 @@ export class FieldMetadataService extends TypeOrmQueryService<FieldMetadataEntit
|
||||
|
||||
const fieldMigrationActions = await this.createMigrationActions({
|
||||
createdFieldMetadataItems,
|
||||
objectMetadataMap,
|
||||
objectMetadataMap: objectMetadataMaps.byId,
|
||||
isRemoteCreation: fieldMetadataInput.isRemoteCreation ?? false,
|
||||
});
|
||||
|
||||
|
||||
@ -19,7 +19,7 @@ export interface FieldMetadataInterface<
|
||||
workspaceId?: string;
|
||||
description?: string;
|
||||
icon?: string;
|
||||
isNullable?: boolean;
|
||||
isNullable: boolean;
|
||||
isUnique?: boolean;
|
||||
relationTargetFieldMetadataId?: string;
|
||||
relationTargetFieldMetadata?: FieldMetadataInterface;
|
||||
@ -30,4 +30,7 @@ export interface FieldMetadataInterface<
|
||||
isActive?: boolean;
|
||||
generatedType?: 'STORED' | 'VIRTUAL';
|
||||
asExpression?: string;
|
||||
isLabelSyncedWithName: boolean;
|
||||
createdAt: Date;
|
||||
updatedAt: Date;
|
||||
}
|
||||
|
||||
@ -13,7 +13,7 @@ export interface ObjectMetadataInterface {
|
||||
labelSingular: string;
|
||||
labelPlural: string;
|
||||
description?: string;
|
||||
icon?: string;
|
||||
icon: string;
|
||||
targetTableName: string;
|
||||
fields: FieldMetadataInterface[];
|
||||
indexMetadatas: IndexMetadataInterface[];
|
||||
|
||||
@ -8,7 +8,7 @@ import {
|
||||
FieldMetadataExceptionCode,
|
||||
} from 'src/engine/metadata-modules/field-metadata/field-metadata.exception';
|
||||
import { ObjectMetadataEntity } from 'src/engine/metadata-modules/object-metadata/object-metadata.entity';
|
||||
import { removeFieldMapsFromObjectMetadata } from 'src/engine/metadata-modules/utils/remove-field-maps-from-object-metadata.util';
|
||||
import { getObjectMetadataFromObjectMetadataItemWithFieldMaps } from 'src/engine/metadata-modules/utils/get-object-metadata-from-object-metadata-Item-with-field-maps';
|
||||
import { WorkspaceCacheStorageService } from 'src/engine/workspace-cache-storage/workspace-cache-storage.service';
|
||||
|
||||
@Injectable()
|
||||
@ -77,13 +77,15 @@ export class FieldMetadataRelationService {
|
||||
}
|
||||
|
||||
return {
|
||||
sourceObjectMetadata: removeFieldMapsFromObjectMetadata(
|
||||
sourceObjectMetadata,
|
||||
) as ObjectMetadataEntity,
|
||||
sourceObjectMetadata:
|
||||
getObjectMetadataFromObjectMetadataItemWithFieldMaps(
|
||||
sourceObjectMetadata,
|
||||
) as ObjectMetadataEntity,
|
||||
sourceFieldMetadata: sourceFieldMetadata as FieldMetadataEntity,
|
||||
targetObjectMetadata: removeFieldMapsFromObjectMetadata(
|
||||
targetObjectMetadata,
|
||||
) as ObjectMetadataEntity,
|
||||
targetObjectMetadata:
|
||||
getObjectMetadataFromObjectMetadataItemWithFieldMaps(
|
||||
targetObjectMetadata,
|
||||
) as ObjectMetadataEntity,
|
||||
targetFieldMetadata: targetFieldMetadata as FieldMetadataEntity,
|
||||
};
|
||||
});
|
||||
|
||||
@ -6,6 +6,7 @@ import { assertUnreachable, isDefined } from 'twenty-shared/utils';
|
||||
import { z } from 'zod';
|
||||
|
||||
import { FieldMetadataOptions } from 'src/engine/metadata-modules/field-metadata/interfaces/field-metadata-options.interface';
|
||||
import { FieldMetadataInterface } from 'src/engine/metadata-modules/field-metadata/interfaces/field-metadata.interface';
|
||||
|
||||
import { CreateFieldInput } from 'src/engine/metadata-modules/field-metadata/dtos/create-field.input';
|
||||
import {
|
||||
@ -13,7 +14,6 @@ import {
|
||||
FieldMetadataDefaultOption,
|
||||
} from 'src/engine/metadata-modules/field-metadata/dtos/options.input';
|
||||
import { UpdateFieldInput } from 'src/engine/metadata-modules/field-metadata/dtos/update-field.input';
|
||||
import { FieldMetadataEntity } from 'src/engine/metadata-modules/field-metadata/field-metadata.entity';
|
||||
import {
|
||||
FieldMetadataException,
|
||||
FieldMetadataExceptionCode,
|
||||
@ -31,7 +31,10 @@ type Validator<T> = { validator: (str: T) => boolean; message: string };
|
||||
type FieldMetadataUpdateCreateInput = CreateFieldInput | UpdateFieldInput;
|
||||
|
||||
type ValidateEnumFieldMetadataArgs = {
|
||||
existingFieldMetadata?: FieldMetadataEntity;
|
||||
existingFieldMetadata?: Pick<
|
||||
FieldMetadataInterface,
|
||||
'type' | 'isNullable' | 'defaultValue' | 'options'
|
||||
>;
|
||||
fieldMetadataInput: FieldMetadataUpdateCreateInput;
|
||||
fieldMetadataType: EnumFieldMetadataUnionType;
|
||||
};
|
||||
|
||||
@ -1,17 +1,15 @@
|
||||
import { FieldMetadataType } from 'twenty-shared/types';
|
||||
|
||||
import { FieldMetadataInterface } from 'src/engine/metadata-modules/field-metadata/interfaces/field-metadata.interface';
|
||||
|
||||
import { FieldMetadataEntity } from 'src/engine/metadata-modules/field-metadata/field-metadata.entity';
|
||||
|
||||
export type SelectOrMultiSelectFieldMetadataEntity = FieldMetadataEntity<
|
||||
FieldMetadataType.SELECT | FieldMetadataType.MULTI_SELECT
|
||||
>;
|
||||
export const isSelectOrMultiSelectFieldMetadata = (
|
||||
fieldMetadata: unknown,
|
||||
fieldMetadata: FieldMetadataInterface,
|
||||
): fieldMetadata is SelectOrMultiSelectFieldMetadataEntity => {
|
||||
if (!(fieldMetadata instanceof FieldMetadataEntity)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return [FieldMetadataType.SELECT, FieldMetadataType.MULTI_SELECT].includes(
|
||||
fieldMetadata.type,
|
||||
);
|
||||
|
||||
@ -80,5 +80,5 @@ export class IndexMetadataEntity {
|
||||
default: IndexType.BTREE,
|
||||
nullable: false,
|
||||
})
|
||||
indexType?: IndexType;
|
||||
indexType: IndexType;
|
||||
}
|
||||
|
||||
@ -106,7 +106,10 @@ export class IndexMetadataService {
|
||||
|
||||
async recomputeIndexMetadataForObject(
|
||||
workspaceId: string,
|
||||
updatedObjectMetadata: ObjectMetadataEntity,
|
||||
updatedObjectMetadata: Pick<
|
||||
ObjectMetadataEntity,
|
||||
'nameSingular' | 'isCustom' | 'id'
|
||||
>,
|
||||
) {
|
||||
const indexesToRecompute = await this.indexMetadataRepository.find({
|
||||
where: {
|
||||
@ -232,7 +235,10 @@ export class IndexMetadataService {
|
||||
|
||||
async createIndexRecomputeMigrations(
|
||||
workspaceId: string,
|
||||
objectMetadata: ObjectMetadataEntity,
|
||||
objectMetadata: Pick<
|
||||
ObjectMetadataEntity,
|
||||
'nameSingular' | 'isCustom' | 'id'
|
||||
>,
|
||||
recomputedIndexes: {
|
||||
indexMetadata: IndexMetadataEntity;
|
||||
previousName: string;
|
||||
|
||||
@ -8,4 +8,6 @@ export interface IndexFieldMetadataInterface {
|
||||
fieldMetadata: FieldMetadataInterface;
|
||||
indexMetadata: IndexMetadataInterface;
|
||||
order: number;
|
||||
createdAt: Date;
|
||||
updatedAt: Date;
|
||||
}
|
||||
|
||||
@ -1,7 +1,14 @@
|
||||
import { IndexFieldMetadataInterface } from 'src/engine/metadata-modules/index-metadata/interfaces/index-field-metadata.interface';
|
||||
|
||||
import { IndexType } from 'src/engine/metadata-modules/index-metadata/index-metadata.entity';
|
||||
|
||||
export interface IndexMetadataInterface {
|
||||
id: string;
|
||||
name: string;
|
||||
isUnique: boolean;
|
||||
indexFieldMetadatas: IndexFieldMetadataInterface[];
|
||||
createdAt: Date;
|
||||
updatedAt: Date;
|
||||
indexWhereClause: string | null;
|
||||
indexType: IndexType;
|
||||
}
|
||||
|
||||
@ -26,6 +26,7 @@ import { PermissionsModule } from 'src/engine/metadata-modules/permissions/permi
|
||||
import { PermissionsGraphqlApiExceptionFilter } from 'src/engine/metadata-modules/permissions/utils/permissions-graphql-api-exception.filter';
|
||||
import { RemoteTableRelationsModule } from 'src/engine/metadata-modules/remote-server/remote-table/remote-table-relations/remote-table-relations.module';
|
||||
import { SearchVectorModule } from 'src/engine/metadata-modules/search-vector/search-vector.module';
|
||||
import { WorkspaceMetadataCacheModule } from 'src/engine/metadata-modules/workspace-metadata-cache/workspace-metadata-cache.module';
|
||||
import { WorkspaceMetadataVersionModule } from 'src/engine/metadata-modules/workspace-metadata-version/workspace-metadata-version.module';
|
||||
import { WorkspaceMigrationModule } from 'src/engine/metadata-modules/workspace-migration/workspace-migration.module';
|
||||
import { WorkspacePermissionsCacheModule } from 'src/engine/metadata-modules/workspace-permissions-cache/workspace-permissions-cache.module';
|
||||
@ -59,6 +60,7 @@ import { UpdateObjectPayload } from './dtos/update-object.input';
|
||||
PermissionsModule,
|
||||
WorkspacePermissionsCacheModule,
|
||||
WorkspaceCacheStorageModule,
|
||||
WorkspaceMetadataCacheModule,
|
||||
],
|
||||
services: [
|
||||
ObjectMetadataService,
|
||||
|
||||
@ -17,6 +17,7 @@ import { AuthWorkspace } from 'src/engine/decorators/auth/auth-workspace.decorat
|
||||
import { SettingsPermissionsGuard } from 'src/engine/guards/settings-permissions.guard';
|
||||
import { WorkspaceAuthGuard } from 'src/engine/guards/workspace-auth.guard';
|
||||
import { FieldMetadataDTO } from 'src/engine/metadata-modules/field-metadata/dtos/field-metadata.dto';
|
||||
import { IndexMetadataDTO } from 'src/engine/metadata-modules/index-metadata/dtos/index-metadata.dto';
|
||||
import { DeleteOneObjectInput } from 'src/engine/metadata-modules/object-metadata/dtos/delete-object.input';
|
||||
import { ObjectMetadataDTO } from 'src/engine/metadata-modules/object-metadata/dtos/object-metadata.dto';
|
||||
import {
|
||||
@ -150,4 +151,26 @@ export class ObjectMetadataResolver {
|
||||
return [];
|
||||
}
|
||||
}
|
||||
|
||||
@ResolveField(() => [IndexMetadataDTO], { nullable: false })
|
||||
async indexMetadataList(
|
||||
@AuthWorkspace() workspace: Workspace,
|
||||
@Parent() objectMetadata: ObjectMetadataDTO,
|
||||
@Context() context: { loaders: IDataloaders },
|
||||
): Promise<IndexMetadataDTO[]> {
|
||||
try {
|
||||
const indexMetadataItems = await context.loaders.indexMetadataLoader.load(
|
||||
{
|
||||
objectMetadata,
|
||||
workspaceId: workspace.id,
|
||||
},
|
||||
);
|
||||
|
||||
return indexMetadataItems;
|
||||
} catch (error) {
|
||||
objectMetadataGraphqlApiExceptionHandler(error);
|
||||
|
||||
return [];
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -4,11 +4,9 @@ import { InjectRepository } from '@nestjs/typeorm';
|
||||
import { i18n } from '@lingui/core';
|
||||
import { Query, QueryOptions } from '@ptc-org/nestjs-query-core';
|
||||
import { TypeOrmQueryService } from '@ptc-org/nestjs-query-typeorm';
|
||||
import { APP_LOCALES, SOURCE_LOCALE } from 'twenty-shared/translations';
|
||||
import { APP_LOCALES } from 'twenty-shared/translations';
|
||||
import { capitalize, isDefined } from 'twenty-shared/utils';
|
||||
import { FindManyOptions, FindOneOptions, In, Not, Repository } from 'typeorm';
|
||||
|
||||
import { ObjectMetadataStandardIdToIdMap } from 'src/engine/metadata-modules/object-metadata/interfaces/object-metadata-standard-id-to-id-map';
|
||||
import { FindManyOptions, FindOneOptions, In, Repository } from 'typeorm';
|
||||
|
||||
import { generateMessageId } from 'src/engine/core-modules/i18n/utils/generateMessageId';
|
||||
import { DataSourceService } from 'src/engine/metadata-modules/data-source/data-source.service';
|
||||
@ -35,7 +33,10 @@ import {
|
||||
} from 'src/engine/metadata-modules/object-metadata/utils/validate-object-metadata-input.util';
|
||||
import { RemoteTableRelationsService } from 'src/engine/metadata-modules/remote-server/remote-table/remote-table-relations/remote-table-relations.service';
|
||||
import { SearchVectorService } from 'src/engine/metadata-modules/search-vector/search-vector.service';
|
||||
import { ObjectMetadataItemWithFieldMaps } from 'src/engine/metadata-modules/types/object-metadata-item-with-field-maps';
|
||||
import { validateNameAndLabelAreSyncOrThrow } from 'src/engine/metadata-modules/utils/validate-name-and-label-are-sync-or-throw.util';
|
||||
import { validatesNoOtherObjectWithSameNameExistsOrThrows } from 'src/engine/metadata-modules/utils/validate-no-other-object-with-same-name-exists-or-throw.util';
|
||||
import { WorkspaceMetadataCacheService } from 'src/engine/metadata-modules/workspace-metadata-cache/services/workspace-metadata-cache.service';
|
||||
import { WorkspaceMetadataVersionService } from 'src/engine/metadata-modules/workspace-metadata-version/services/workspace-metadata-version.service';
|
||||
import { WorkspacePermissionsCacheService } from 'src/engine/metadata-modules/workspace-permissions-cache/workspace-permissions-cache.service';
|
||||
import { computeObjectTargetTable } from 'src/engine/utils/compute-object-target-table.util';
|
||||
@ -58,6 +59,7 @@ export class ObjectMetadataService extends TypeOrmQueryService<ObjectMetadataEnt
|
||||
|
||||
private readonly remoteTableRelationsService: RemoteTableRelationsService,
|
||||
private readonly dataSourceService: DataSourceService,
|
||||
private readonly workspaceMetadataCacheService: WorkspaceMetadataCacheService,
|
||||
private readonly workspaceMigrationRunnerService: WorkspaceMigrationRunnerService,
|
||||
private readonly workspaceMetadataVersionService: WorkspaceMetadataVersionService,
|
||||
private readonly searchVectorService: SearchVectorService,
|
||||
@ -89,6 +91,13 @@ export class ObjectMetadataService extends TypeOrmQueryService<ObjectMetadataEnt
|
||||
override async createOne(
|
||||
objectMetadataInput: CreateObjectInput,
|
||||
): Promise<ObjectMetadataEntity> {
|
||||
const { objectMetadataMaps } =
|
||||
await this.workspaceMetadataCacheService.getExistingOrRecomputeMetadataMaps(
|
||||
{
|
||||
workspaceId: objectMetadataInput.workspaceId,
|
||||
},
|
||||
);
|
||||
|
||||
const lastDataSourceMetadata =
|
||||
await this.dataSourceService.getLastDataSourceMetadataFromWorkspaceIdOrFail(
|
||||
objectMetadataInput.workspaceId,
|
||||
@ -131,13 +140,28 @@ export class ObjectMetadataService extends TypeOrmQueryService<ObjectMetadataEnt
|
||||
);
|
||||
}
|
||||
|
||||
await this.validatesNoOtherObjectWithSameNameExistsOrThrows({
|
||||
validatesNoOtherObjectWithSameNameExistsOrThrows({
|
||||
objectMetadataNamePlural: objectMetadataInput.namePlural,
|
||||
objectMetadataNameSingular: objectMetadataInput.nameSingular,
|
||||
workspaceId: objectMetadataInput.workspaceId,
|
||||
objectMetadataMaps,
|
||||
});
|
||||
|
||||
const createdObjectMetadata = await super.createOne({
|
||||
const baseCustomFields = buildDefaultFieldsForCustomObject(
|
||||
objectMetadataInput.workspaceId,
|
||||
);
|
||||
|
||||
const labelIdentifierFieldMetadataId = baseCustomFields.find(
|
||||
(field) => field.standardId === CUSTOM_OBJECT_STANDARD_FIELD_IDS.name,
|
||||
)?.id;
|
||||
|
||||
if (!labelIdentifierFieldMetadataId) {
|
||||
throw new ObjectMetadataException(
|
||||
'Label identifier field metadata not created properly',
|
||||
ObjectMetadataExceptionCode.MISSING_CUSTOM_OBJECT_DEFAULT_LABEL_IDENTIFIER_FIELD,
|
||||
);
|
||||
}
|
||||
|
||||
const createdObjectMetadata = await this.objectMetadataRepository.save({
|
||||
...objectMetadataInput,
|
||||
dataSourceId: lastDataSourceMetadata.id,
|
||||
targetTableName: 'DEPRECATED',
|
||||
@ -146,24 +170,8 @@ export class ObjectMetadataService extends TypeOrmQueryService<ObjectMetadataEnt
|
||||
isSystem: false,
|
||||
isRemote: objectMetadataInput.isRemote,
|
||||
isSearchable: !objectMetadataInput.isRemote,
|
||||
fields: objectMetadataInput.isRemote
|
||||
? []
|
||||
: buildDefaultFieldsForCustomObject(objectMetadataInput.workspaceId),
|
||||
});
|
||||
|
||||
const labelIdentifierFieldMetadata = createdObjectMetadata.fields.find(
|
||||
(field) => field.standardId === CUSTOM_OBJECT_STANDARD_FIELD_IDS.name,
|
||||
);
|
||||
|
||||
if (!labelIdentifierFieldMetadata) {
|
||||
throw new ObjectMetadataException(
|
||||
'Label identifier field metadata not created properly',
|
||||
ObjectMetadataExceptionCode.MISSING_CUSTOM_OBJECT_DEFAULT_LABEL_IDENTIFIER_FIELD,
|
||||
);
|
||||
}
|
||||
|
||||
await this.objectMetadataRepository.update(createdObjectMetadata.id, {
|
||||
labelIdentifierFieldMetadataId: labelIdentifierFieldMetadata.id,
|
||||
fields: objectMetadataInput.isRemote ? [] : baseCustomFields,
|
||||
labelIdentifierFieldMetadataId,
|
||||
});
|
||||
|
||||
if (objectMetadataInput.isRemote) {
|
||||
@ -174,6 +182,13 @@ export class ObjectMetadataService extends TypeOrmQueryService<ObjectMetadataEnt
|
||||
objectMetadataInput.primaryKeyColumnType,
|
||||
);
|
||||
} else {
|
||||
const createdRelatedObjectMetadataCollection =
|
||||
await this.objectMetadataFieldRelationService.createRelationsAndForeignKeysMetadata(
|
||||
objectMetadataInput.workspaceId,
|
||||
createdObjectMetadata,
|
||||
objectMetadataMaps,
|
||||
);
|
||||
|
||||
await this.objectMetadataMigrationService.createTableMigration(
|
||||
createdObjectMetadata,
|
||||
);
|
||||
@ -183,12 +198,6 @@ export class ObjectMetadataService extends TypeOrmQueryService<ObjectMetadataEnt
|
||||
createdObjectMetadata.fields,
|
||||
);
|
||||
|
||||
const createdRelatedObjectMetadataCollection =
|
||||
await this.objectMetadataFieldRelationService.createRelationsAndForeignKeysMetadata(
|
||||
objectMetadataInput.workspaceId,
|
||||
createdObjectMetadata,
|
||||
);
|
||||
|
||||
await this.objectMetadataMigrationService.createRelationMigrations(
|
||||
createdObjectMetadata,
|
||||
createdRelatedObjectMetadataCollection,
|
||||
@ -214,7 +223,6 @@ export class ObjectMetadataService extends TypeOrmQueryService<ObjectMetadataEnt
|
||||
|
||||
await this.workspacePermissionsCacheService.recomputeRolesPermissionsCache({
|
||||
workspaceId: objectMetadataInput.workspaceId,
|
||||
ignoreLock: true,
|
||||
});
|
||||
|
||||
return createdObjectMetadata;
|
||||
@ -224,6 +232,11 @@ export class ObjectMetadataService extends TypeOrmQueryService<ObjectMetadataEnt
|
||||
input: UpdateOneObjectInput,
|
||||
workspaceId: string,
|
||||
): Promise<ObjectMetadataEntity> {
|
||||
const { objectMetadataMaps } =
|
||||
await this.workspaceMetadataCacheService.getExistingOrRecomputeMetadataMaps(
|
||||
{ workspaceId },
|
||||
);
|
||||
|
||||
const inputId = input.id;
|
||||
|
||||
const inputPayload = {
|
||||
@ -238,9 +251,7 @@ export class ObjectMetadataService extends TypeOrmQueryService<ObjectMetadataEnt
|
||||
|
||||
validateObjectMetadataInputNamesOrThrow(inputPayload);
|
||||
|
||||
const existingObjectMetadata = await this.objectMetadataRepository.findOne({
|
||||
where: { id: inputId, workspaceId: workspaceId },
|
||||
});
|
||||
const existingObjectMetadata = objectMetadataMaps.byId[inputId];
|
||||
|
||||
if (!existingObjectMetadata) {
|
||||
throw new ObjectMetadataException(
|
||||
@ -254,14 +265,14 @@ export class ObjectMetadataService extends TypeOrmQueryService<ObjectMetadataEnt
|
||||
...inputPayload,
|
||||
};
|
||||
|
||||
await this.validatesNoOtherObjectWithSameNameExistsOrThrows({
|
||||
validatesNoOtherObjectWithSameNameExistsOrThrows({
|
||||
objectMetadataNameSingular:
|
||||
existingObjectMetadataCombinedWithUpdateInput.nameSingular,
|
||||
objectMetadataNamePlural:
|
||||
existingObjectMetadataCombinedWithUpdateInput.namePlural,
|
||||
workspaceId: workspaceId,
|
||||
existingObjectMetadataId:
|
||||
existingObjectMetadataCombinedWithUpdateInput.id,
|
||||
objectMetadataMaps,
|
||||
});
|
||||
|
||||
if (existingObjectMetadataCombinedWithUpdateInput.isLabelSyncedWithName) {
|
||||
@ -395,7 +406,6 @@ export class ObjectMetadataService extends TypeOrmQueryService<ObjectMetadataEnt
|
||||
|
||||
await this.workspacePermissionsCacheService.recomputeRolesPermissionsCache({
|
||||
workspaceId,
|
||||
ignoreLock: true,
|
||||
});
|
||||
|
||||
return objectMetadata;
|
||||
@ -436,44 +446,26 @@ export class ObjectMetadataService extends TypeOrmQueryService<ObjectMetadataEnt
|
||||
});
|
||||
}
|
||||
|
||||
public async findMany(options?: FindManyOptions<ObjectMetadataEntity>) {
|
||||
return this.objectMetadataRepository.find({
|
||||
relations: ['fields'],
|
||||
...options,
|
||||
where: {
|
||||
...options?.where,
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
public async deleteObjectsMetadata(workspaceId: string) {
|
||||
await this.objectMetadataRepository.delete({ workspaceId });
|
||||
}
|
||||
|
||||
public async getObjectMetadataStandardIdToIdMap(workspaceId: string) {
|
||||
const objectMetadata = await this.findManyWithinWorkspace(workspaceId);
|
||||
|
||||
const objectMetadataStandardIdToIdMap =
|
||||
objectMetadata.reduce<ObjectMetadataStandardIdToIdMap>((acc, object) => {
|
||||
acc[object.standardId ?? ''] = {
|
||||
id: object.id,
|
||||
fields: object.fields.reduce((acc, field) => {
|
||||
// @ts-expect-error legacy noImplicitAny
|
||||
acc[field.standardId ?? ''] = field.id;
|
||||
|
||||
return acc;
|
||||
}, {}),
|
||||
};
|
||||
|
||||
return acc;
|
||||
}, {});
|
||||
|
||||
return { objectMetadataStandardIdToIdMap };
|
||||
}
|
||||
|
||||
private async handleObjectNameAndLabelUpdates(
|
||||
existingObjectMetadata: ObjectMetadataEntity,
|
||||
objectMetadataForUpdate: ObjectMetadataEntity,
|
||||
existingObjectMetadata: Pick<
|
||||
ObjectMetadataItemWithFieldMaps,
|
||||
'nameSingular' | 'isCustom' | 'id' | 'labelPlural' | 'icon' | 'fieldsById'
|
||||
>,
|
||||
objectMetadataForUpdate: Pick<
|
||||
ObjectMetadataItemWithFieldMaps,
|
||||
| 'nameSingular'
|
||||
| 'isCustom'
|
||||
| 'workspaceId'
|
||||
| 'id'
|
||||
| 'labelSingular'
|
||||
| 'labelPlural'
|
||||
| 'icon'
|
||||
| 'fieldsById'
|
||||
>,
|
||||
inputPayload: UpdateObjectPayload,
|
||||
) {
|
||||
const newTargetTableName = computeObjectTargetTable(
|
||||
@ -533,45 +525,6 @@ export class ObjectMetadataService extends TypeOrmQueryService<ObjectMetadataEnt
|
||||
}
|
||||
}
|
||||
|
||||
private validatesNoOtherObjectWithSameNameExistsOrThrows = async ({
|
||||
objectMetadataNameSingular,
|
||||
objectMetadataNamePlural,
|
||||
workspaceId,
|
||||
existingObjectMetadataId,
|
||||
}: {
|
||||
objectMetadataNameSingular: string;
|
||||
objectMetadataNamePlural: string;
|
||||
workspaceId: string;
|
||||
existingObjectMetadataId?: string;
|
||||
}): Promise<void> => {
|
||||
const baseWhereConditions = [
|
||||
{ nameSingular: objectMetadataNameSingular, workspaceId },
|
||||
{ nameSingular: objectMetadataNamePlural, workspaceId },
|
||||
{ namePlural: objectMetadataNameSingular, workspaceId },
|
||||
{ namePlural: objectMetadataNamePlural, workspaceId },
|
||||
];
|
||||
|
||||
const whereConditions = baseWhereConditions.map((condition) => {
|
||||
return {
|
||||
...condition,
|
||||
...(isDefined(existingObjectMetadataId)
|
||||
? { id: Not(In([existingObjectMetadataId])) }
|
||||
: {}),
|
||||
};
|
||||
});
|
||||
|
||||
const objectAlreadyExists = await this.objectMetadataRepository.findOne({
|
||||
where: whereConditions,
|
||||
});
|
||||
|
||||
if (objectAlreadyExists) {
|
||||
throw new ObjectMetadataException(
|
||||
'Object already exists',
|
||||
ObjectMetadataExceptionCode.OBJECT_ALREADY_EXISTS,
|
||||
);
|
||||
}
|
||||
};
|
||||
|
||||
async resolveOverridableString(
|
||||
objectMetadata: ObjectMetadataDTO,
|
||||
labelKey: 'labelPlural' | 'labelSingular' | 'description' | 'icon',
|
||||
@ -581,17 +534,6 @@ export class ObjectMetadataService extends TypeOrmQueryService<ObjectMetadataEnt
|
||||
return objectMetadata[labelKey];
|
||||
}
|
||||
|
||||
if (!locale || locale === SOURCE_LOCALE) {
|
||||
if (
|
||||
objectMetadata.standardOverrides &&
|
||||
isDefined(objectMetadata.standardOverrides[labelKey])
|
||||
) {
|
||||
return objectMetadata.standardOverrides[labelKey] as string;
|
||||
}
|
||||
|
||||
return objectMetadata[labelKey];
|
||||
}
|
||||
|
||||
const translationValue =
|
||||
// @ts-expect-error legacy noImplicitAny
|
||||
objectMetadata.standardOverrides?.translations?.[locale]?.[labelKey];
|
||||
|
||||
@ -14,6 +14,8 @@ import { ObjectMetadataEntity } from 'src/engine/metadata-modules/object-metadat
|
||||
import { buildDescriptionForRelationFieldMetadataOnFromField } from 'src/engine/metadata-modules/object-metadata/utils/build-description-for-relation-field-on-from-field.util';
|
||||
import { buildDescriptionForRelationFieldMetadataOnToField } from 'src/engine/metadata-modules/object-metadata/utils/build-description-for-relation-field-on-to-field.util';
|
||||
import { RelationOnDeleteAction } from 'src/engine/metadata-modules/relation-metadata/relation-on-delete-action.type';
|
||||
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 {
|
||||
CUSTOM_OBJECT_STANDARD_FIELD_IDS,
|
||||
STANDARD_OBJECT_FIELD_IDS,
|
||||
@ -41,33 +43,51 @@ export class ObjectMetadataFieldRelationService {
|
||||
|
||||
public async createRelationsAndForeignKeysMetadata(
|
||||
workspaceId: string,
|
||||
sourceObjectMetadata: ObjectMetadataEntity,
|
||||
sourceObjectMetadata: Pick<
|
||||
ObjectMetadataItemWithFieldMaps,
|
||||
'id' | 'nameSingular' | 'labelSingular'
|
||||
>,
|
||||
objectMetadataMaps: ObjectMetadataMaps,
|
||||
) {
|
||||
const relatedObjectMetadataCollection = await Promise.all(
|
||||
DEFAULT_RELATIONS_OBJECTS_STANDARD_IDS.map(
|
||||
async (relationObjectMetadataStandardId) =>
|
||||
this.createRelationAndForeignKeyMetadata(
|
||||
this.createRelationAndForeignKeyMetadata({
|
||||
workspaceId,
|
||||
sourceObjectMetadata,
|
||||
relationObjectMetadataStandardId,
|
||||
),
|
||||
objectMetadataMaps,
|
||||
}),
|
||||
),
|
||||
);
|
||||
|
||||
return relatedObjectMetadataCollection;
|
||||
}
|
||||
|
||||
private async createRelationAndForeignKeyMetadata(
|
||||
workspaceId: string,
|
||||
sourceObjectMetadata: ObjectMetadataEntity,
|
||||
relationObjectMetadataStandardId: string,
|
||||
) {
|
||||
const targetObjectMetadata =
|
||||
await this.objectMetadataRepository.findOneByOrFail({
|
||||
standardId: relationObjectMetadataStandardId,
|
||||
workspaceId: workspaceId,
|
||||
isCustom: false,
|
||||
});
|
||||
private async createRelationAndForeignKeyMetadata({
|
||||
workspaceId,
|
||||
sourceObjectMetadata,
|
||||
relationObjectMetadataStandardId,
|
||||
objectMetadataMaps,
|
||||
}: {
|
||||
workspaceId: string;
|
||||
sourceObjectMetadata: Pick<
|
||||
ObjectMetadataItemWithFieldMaps,
|
||||
'id' | 'nameSingular' | 'labelSingular'
|
||||
>;
|
||||
objectMetadataMaps: ObjectMetadataMaps;
|
||||
relationObjectMetadataStandardId: string;
|
||||
}) {
|
||||
const targetObjectMetadata = Object.values(objectMetadataMaps.byId).find(
|
||||
(objectMetadata) =>
|
||||
objectMetadata.standardId === relationObjectMetadataStandardId,
|
||||
);
|
||||
|
||||
if (!targetObjectMetadata) {
|
||||
throw new Error(
|
||||
`Target object metadata not found for standard ID: ${relationObjectMetadataStandardId}`,
|
||||
);
|
||||
}
|
||||
|
||||
await this.createFieldMetadataRelation(
|
||||
workspaceId,
|
||||
@ -80,8 +100,11 @@ export class ObjectMetadataFieldRelationService {
|
||||
|
||||
private async createFieldMetadataRelation(
|
||||
workspaceId: string,
|
||||
sourceObjectMetadata: ObjectMetadataEntity,
|
||||
targetObjectMetadata: ObjectMetadataEntity,
|
||||
sourceObjectMetadata: Pick<
|
||||
ObjectMetadataItemWithFieldMaps,
|
||||
'id' | 'nameSingular' | 'labelSingular'
|
||||
>,
|
||||
targetObjectMetadata: ObjectMetadataItemWithFieldMaps,
|
||||
): Promise<FieldMetadataEntity<FieldMetadataType.RELATION>[]> {
|
||||
const sourceFieldMetadata = this.createSourceFieldMetadata(
|
||||
workspaceId,
|
||||
@ -119,7 +142,10 @@ export class ObjectMetadataFieldRelationService {
|
||||
|
||||
public async updateRelationsAndForeignKeysMetadata(
|
||||
workspaceId: string,
|
||||
updatedObjectMetadata: ObjectMetadataEntity,
|
||||
updatedObjectMetadata: Pick<
|
||||
ObjectMetadataEntity,
|
||||
'nameSingular' | 'isCustom' | 'id' | 'labelSingular'
|
||||
>,
|
||||
): Promise<
|
||||
{
|
||||
targetObjectMetadata: ObjectMetadataEntity;
|
||||
@ -141,7 +167,10 @@ export class ObjectMetadataFieldRelationService {
|
||||
|
||||
private async updateRelationAndForeignKeyMetadata(
|
||||
workspaceId: string,
|
||||
sourceObjectMetadata: ObjectMetadataEntity,
|
||||
sourceObjectMetadata: Pick<
|
||||
ObjectMetadataEntity,
|
||||
'nameSingular' | 'id' | 'isCustom' | 'labelSingular'
|
||||
>,
|
||||
targetObjectMetadataStandardId: string,
|
||||
) {
|
||||
const targetObjectMetadata =
|
||||
@ -226,8 +255,14 @@ export class ObjectMetadataFieldRelationService {
|
||||
|
||||
private createSourceFieldMetadata(
|
||||
workspaceId: string,
|
||||
sourceObjectMetadata: ObjectMetadataEntity,
|
||||
targetObjectMetadata: ObjectMetadataEntity,
|
||||
sourceObjectMetadata: Pick<
|
||||
ObjectMetadataItemWithFieldMaps,
|
||||
'labelSingular' | 'id'
|
||||
>,
|
||||
targetObjectMetadata: Pick<
|
||||
ObjectMetadataItemWithFieldMaps,
|
||||
'namePlural' | 'labelSingular'
|
||||
>,
|
||||
): Partial<FieldMetadataEntity<FieldMetadataType.RELATION>> {
|
||||
const relationObjectMetadataNamePlural = targetObjectMetadata.namePlural;
|
||||
|
||||
@ -261,8 +296,8 @@ export class ObjectMetadataFieldRelationService {
|
||||
}
|
||||
|
||||
private updateSourceFieldMetadata(
|
||||
sourceObjectMetadata: ObjectMetadataEntity,
|
||||
targetObjectMetadata: ObjectMetadataEntity,
|
||||
sourceObjectMetadata: Pick<ObjectMetadataEntity, 'labelSingular'>,
|
||||
targetObjectMetadata: Pick<ObjectMetadataEntity, 'namePlural'>,
|
||||
) {
|
||||
const relationObjectMetadataNamePlural = targetObjectMetadata.namePlural;
|
||||
|
||||
@ -280,8 +315,14 @@ export class ObjectMetadataFieldRelationService {
|
||||
|
||||
private createTargetFieldMetadata(
|
||||
workspaceId: string,
|
||||
sourceObjectMetadata: ObjectMetadataEntity,
|
||||
targetObjectMetadata: ObjectMetadataEntity,
|
||||
sourceObjectMetadata: Pick<
|
||||
ObjectMetadataItemWithFieldMaps,
|
||||
'labelSingular' | 'id' | 'nameSingular'
|
||||
>,
|
||||
targetObjectMetadata: Pick<
|
||||
ObjectMetadataItemWithFieldMaps,
|
||||
'namePlural' | 'labelSingular' | 'id' | 'nameSingular'
|
||||
>,
|
||||
): Partial<FieldMetadataEntity<FieldMetadataType.RELATION>> {
|
||||
const customStandardFieldId =
|
||||
// @ts-expect-error legacy noImplicitAny
|
||||
@ -319,8 +360,14 @@ export class ObjectMetadataFieldRelationService {
|
||||
}
|
||||
|
||||
private updateTargetFieldMetadata(
|
||||
sourceObjectMetadata: ObjectMetadataEntity,
|
||||
targetObjectMetadata: ObjectMetadataEntity,
|
||||
sourceObjectMetadata: Pick<
|
||||
ObjectMetadataEntity,
|
||||
'nameSingular' | 'labelSingular'
|
||||
>,
|
||||
targetObjectMetadata: Pick<
|
||||
ObjectMetadataEntity,
|
||||
'nameSingular' | 'namePlural'
|
||||
>,
|
||||
) {
|
||||
const customStandardFieldId =
|
||||
// @ts-expect-error legacy noImplicitAny
|
||||
|
||||
@ -2,13 +2,14 @@ import { Injectable } from '@nestjs/common';
|
||||
import { InjectRepository } from '@nestjs/typeorm';
|
||||
|
||||
import { FieldMetadataType } from 'twenty-shared/types';
|
||||
import { In, Repository } from 'typeorm';
|
||||
import { Repository } from 'typeorm';
|
||||
|
||||
import { RelationType } from 'src/engine/metadata-modules/field-metadata/interfaces/relation-type.interface';
|
||||
|
||||
import { FieldMetadataEntity } from 'src/engine/metadata-modules/field-metadata/field-metadata.entity';
|
||||
import { ObjectMetadataEntity } from 'src/engine/metadata-modules/object-metadata/object-metadata.entity';
|
||||
import { buildMigrationsForCustomObjectRelations } from 'src/engine/metadata-modules/object-metadata/utils/build-migrations-for-custom-object-relations.util';
|
||||
import { ObjectMetadataItemWithFieldMaps } from 'src/engine/metadata-modules/types/object-metadata-item-with-field-maps';
|
||||
import { fieldMetadataTypeToColumnType } from 'src/engine/metadata-modules/workspace-migration/utils/field-metadata-type-to-column-type.util';
|
||||
import { generateMigrationName } from 'src/engine/metadata-modules/workspace-migration/utils/generate-migration-name.util';
|
||||
import {
|
||||
@ -73,8 +74,14 @@ export class ObjectMetadataMigrationService {
|
||||
}
|
||||
|
||||
public async createRelationMigrations(
|
||||
createdObjectMetadata: ObjectMetadataEntity,
|
||||
relatedObjectMetadataCollection: ObjectMetadataEntity[],
|
||||
createdObjectMetadata: Pick<
|
||||
ObjectMetadataItemWithFieldMaps,
|
||||
'nameSingular' | 'workspaceId' | 'isCustom'
|
||||
>,
|
||||
relatedObjectMetadataCollection: Pick<
|
||||
ObjectMetadataItemWithFieldMaps,
|
||||
'nameSingular' | 'isCustom'
|
||||
>[],
|
||||
) {
|
||||
await this.workspaceMigrationService.createCustomMigration(
|
||||
generateMigrationName(
|
||||
@ -89,8 +96,14 @@ export class ObjectMetadataMigrationService {
|
||||
}
|
||||
|
||||
public async createRenameTableMigration(
|
||||
existingObjectMetadata: ObjectMetadataEntity,
|
||||
objectMetadataForUpdate: ObjectMetadataEntity,
|
||||
existingObjectMetadata: Pick<
|
||||
ObjectMetadataEntity,
|
||||
'nameSingular' | 'isCustom'
|
||||
>,
|
||||
objectMetadataForUpdate: Pick<
|
||||
ObjectMetadataEntity,
|
||||
'nameSingular' | 'isCustom'
|
||||
>,
|
||||
workspaceId: string,
|
||||
) {
|
||||
const newTargetTableName = computeObjectTargetTable(
|
||||
@ -114,8 +127,8 @@ export class ObjectMetadataMigrationService {
|
||||
}
|
||||
|
||||
public async updateRelationMigrations(
|
||||
currentObjectMetadata: ObjectMetadataEntity,
|
||||
alteredObjectMetadata: ObjectMetadataEntity,
|
||||
currentObjectMetadata: Pick<ObjectMetadataEntity, 'nameSingular'>,
|
||||
alteredObjectMetadata: Pick<ObjectMetadataEntity, 'nameSingular'>,
|
||||
relationMetadataCollection: {
|
||||
targetObjectMetadata: ObjectMetadataEntity;
|
||||
targetFieldMetadata: FieldMetadataEntity;
|
||||
@ -282,21 +295,22 @@ export class ObjectMetadataMigrationService {
|
||||
}
|
||||
|
||||
public async recomputeEnumNames(
|
||||
updatedObjectMetadata: ObjectMetadataEntity,
|
||||
updatedObjectMetadata: Pick<
|
||||
ObjectMetadataItemWithFieldMaps,
|
||||
'nameSingular' | 'isCustom' | 'id' | 'fieldsById'
|
||||
>,
|
||||
workspaceId: string,
|
||||
) {
|
||||
const fieldMetadataToUpdate = await this.fieldMetadataRepository.find({
|
||||
where: {
|
||||
objectMetadataId: updatedObjectMetadata.id,
|
||||
workspaceId,
|
||||
type: In([
|
||||
FieldMetadataType.SELECT,
|
||||
FieldMetadataType.MULTI_SELECT,
|
||||
FieldMetadataType.RATING,
|
||||
FieldMetadataType.ACTOR,
|
||||
]),
|
||||
},
|
||||
});
|
||||
const enumFieldMetadataTypes = [
|
||||
FieldMetadataType.SELECT,
|
||||
FieldMetadataType.MULTI_SELECT,
|
||||
FieldMetadataType.RATING,
|
||||
FieldMetadataType.ACTOR,
|
||||
];
|
||||
|
||||
const fieldMetadataToUpdate = Object.values(
|
||||
updatedObjectMetadata.fieldsById,
|
||||
).filter((field) => enumFieldMetadataTypes.includes(field.type));
|
||||
|
||||
for (const fieldMetadata of fieldMetadataToUpdate) {
|
||||
await this.workspaceMigrationService.createCustomMigration(
|
||||
|
||||
@ -83,7 +83,10 @@ export class ObjectMetadataRelatedRecordsService {
|
||||
}
|
||||
|
||||
public async updateObjectViews(
|
||||
updatedObjectMetadata: ObjectMetadataEntity,
|
||||
updatedObjectMetadata: Pick<
|
||||
ObjectMetadataEntity,
|
||||
'id' | 'labelPlural' | 'icon'
|
||||
>,
|
||||
workspaceId: string,
|
||||
) {
|
||||
const viewRepository =
|
||||
|
||||
@ -1,4 +1,5 @@
|
||||
import { FieldMetadataType } from 'twenty-shared/types';
|
||||
import { v4 } from 'uuid';
|
||||
|
||||
import { FieldMetadataEntity } from 'src/engine/metadata-modules/field-metadata/field-metadata.entity';
|
||||
import {
|
||||
@ -10,6 +11,7 @@ export const buildDefaultFieldsForCustomObject = (
|
||||
workspaceId: string,
|
||||
): Partial<FieldMetadataEntity>[] => [
|
||||
{
|
||||
id: v4(),
|
||||
standardId: BASE_OBJECT_STANDARD_FIELD_IDS.id,
|
||||
type: FieldMetadataType.UUID,
|
||||
name: 'id',
|
||||
@ -24,6 +26,7 @@ export const buildDefaultFieldsForCustomObject = (
|
||||
defaultValue: 'uuid',
|
||||
},
|
||||
{
|
||||
id: v4(),
|
||||
standardId: CUSTOM_OBJECT_STANDARD_FIELD_IDS.name,
|
||||
type: FieldMetadataType.TEXT,
|
||||
name: 'name',
|
||||
@ -37,6 +40,7 @@ export const buildDefaultFieldsForCustomObject = (
|
||||
defaultValue: "'Untitled'",
|
||||
},
|
||||
{
|
||||
id: v4(),
|
||||
standardId: BASE_OBJECT_STANDARD_FIELD_IDS.createdAt,
|
||||
type: FieldMetadataType.DATE_TIME,
|
||||
name: 'createdAt',
|
||||
@ -50,6 +54,7 @@ export const buildDefaultFieldsForCustomObject = (
|
||||
defaultValue: 'now',
|
||||
},
|
||||
{
|
||||
id: v4(),
|
||||
standardId: BASE_OBJECT_STANDARD_FIELD_IDS.updatedAt,
|
||||
type: FieldMetadataType.DATE_TIME,
|
||||
name: 'updatedAt',
|
||||
@ -64,6 +69,7 @@ export const buildDefaultFieldsForCustomObject = (
|
||||
defaultValue: 'now',
|
||||
},
|
||||
{
|
||||
id: v4(),
|
||||
standardId: BASE_OBJECT_STANDARD_FIELD_IDS.deletedAt,
|
||||
type: FieldMetadataType.DATE_TIME,
|
||||
name: 'deletedAt',
|
||||
@ -78,6 +84,7 @@ export const buildDefaultFieldsForCustomObject = (
|
||||
defaultValue: null,
|
||||
},
|
||||
{
|
||||
id: v4(),
|
||||
standardId: CUSTOM_OBJECT_STANDARD_FIELD_IDS.createdBy,
|
||||
type: FieldMetadataType.ACTOR,
|
||||
name: 'createdBy',
|
||||
@ -92,6 +99,7 @@ export const buildDefaultFieldsForCustomObject = (
|
||||
defaultValue: { name: "''", source: "'MANUAL'" },
|
||||
},
|
||||
{
|
||||
id: v4(),
|
||||
standardId: CUSTOM_OBJECT_STANDARD_FIELD_IDS.position,
|
||||
type: FieldMetadataType.POSITION,
|
||||
name: 'position',
|
||||
|
||||
@ -1,6 +1,6 @@
|
||||
import { computeColumnName } from 'src/engine/metadata-modules/field-metadata/utils/compute-column-name.util';
|
||||
import { ObjectMetadataEntity } from 'src/engine/metadata-modules/object-metadata/object-metadata.entity';
|
||||
import { RelationOnDeleteAction } from 'src/engine/metadata-modules/relation-metadata/relation-on-delete-action.type';
|
||||
import { ObjectMetadataItemWithFieldMaps } from 'src/engine/metadata-modules/types/object-metadata-item-with-field-maps';
|
||||
import {
|
||||
WorkspaceMigrationColumnActionType,
|
||||
WorkspaceMigrationColumnCreate,
|
||||
@ -10,8 +10,14 @@ import {
|
||||
import { computeObjectTargetTable } from 'src/engine/utils/compute-object-target-table.util';
|
||||
|
||||
export const buildMigrationsForCustomObjectRelations = (
|
||||
createdObjectMetadata: ObjectMetadataEntity,
|
||||
relatedObjectMetadataCollection: ObjectMetadataEntity[],
|
||||
createdObjectMetadata: Pick<
|
||||
ObjectMetadataItemWithFieldMaps,
|
||||
'nameSingular' | 'isCustom'
|
||||
>,
|
||||
relatedObjectMetadataCollection: Pick<
|
||||
ObjectMetadataItemWithFieldMaps,
|
||||
'nameSingular' | 'isCustom'
|
||||
>[],
|
||||
): WorkspaceMigrationTableAction[] => {
|
||||
const migrations: WorkspaceMigrationTableAction[] = [];
|
||||
|
||||
|
||||
@ -218,7 +218,6 @@ describe('ObjectPermissionService', () => {
|
||||
).toHaveBeenCalledWith({
|
||||
workspaceId,
|
||||
roleIds: [roleId],
|
||||
ignoreLock: true,
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
@ -101,7 +101,6 @@ export class ObjectPermissionService {
|
||||
{
|
||||
workspaceId,
|
||||
roleIds: [input.roleId],
|
||||
ignoreLock: true,
|
||||
},
|
||||
);
|
||||
|
||||
|
||||
@ -82,7 +82,6 @@ export class RoleService {
|
||||
await this.workspacePermissionsCacheService.recomputeRolesPermissionsCache({
|
||||
workspaceId,
|
||||
roleIds: [role.id],
|
||||
ignoreLock: true,
|
||||
});
|
||||
|
||||
return role;
|
||||
@ -128,7 +127,6 @@ export class RoleService {
|
||||
await this.workspacePermissionsCacheService.recomputeRolesPermissionsCache({
|
||||
workspaceId,
|
||||
roleIds: [input.id],
|
||||
ignoreLock: true,
|
||||
});
|
||||
|
||||
return { ...existingRole, ...updatedRole };
|
||||
@ -195,7 +193,6 @@ export class RoleService {
|
||||
|
||||
await this.workspacePermissionsCacheService.recomputeRolesPermissionsCache({
|
||||
workspaceId,
|
||||
ignoreLock: true,
|
||||
});
|
||||
|
||||
return roleId;
|
||||
|
||||
@ -122,7 +122,6 @@ export class SettingPermissionService {
|
||||
{
|
||||
workspaceId,
|
||||
roleIds: [input.roleId],
|
||||
ignoreLock: true,
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
@ -1,9 +1,14 @@
|
||||
import { ObjectMetadataInterface } from 'src/engine/metadata-modules/field-metadata/interfaces/object-metadata.interface';
|
||||
import { IndexMetadataInterface } from 'src/engine/metadata-modules/index-metadata/interfaces/index-metadata.interface';
|
||||
|
||||
import { FieldMetadataMap } from 'src/engine/metadata-modules/types/field-metadata-map';
|
||||
|
||||
export type ObjectMetadataItemWithFieldMaps = ObjectMetadataInterface & {
|
||||
export type ObjectMetadataItemWithFieldMaps = Omit<
|
||||
ObjectMetadataInterface,
|
||||
'fields'
|
||||
> & {
|
||||
fieldsById: FieldMetadataMap;
|
||||
fieldsByName: FieldMetadataMap;
|
||||
fieldsByJoinColumnName: FieldMetadataMap;
|
||||
fieldIdByJoinColumnName: Record<string, string>;
|
||||
fieldIdByName: Record<string, string>;
|
||||
indexMetadatas: IndexMetadataInterface[];
|
||||
};
|
||||
|
||||
@ -62,7 +62,6 @@ export class UserRoleService {
|
||||
await this.workspacePermissionsCacheService.recomputeUserWorkspaceRoleMapCache(
|
||||
{
|
||||
workspaceId,
|
||||
ignoreLock: true,
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
@ -6,7 +6,7 @@ import {
|
||||
FIELD_CURRENCY_MOCK_NAME,
|
||||
FIELD_FULL_NAME_MOCK_NAME,
|
||||
FIELD_LINKS_MOCK_NAME,
|
||||
objectMetadataItemMock,
|
||||
objectMetadataMapItemMock,
|
||||
} from 'src/engine/api/__mocks__/object-metadata-item.mock';
|
||||
import { validateFieldNameAvailabilityOrThrow } from 'src/engine/metadata-modules/utils/validate-field-name-availability.utils';
|
||||
|
||||
@ -57,18 +57,22 @@ const validateFieldNameAvailabilityTestCases: ValidateFieldNameAvailabilityTestC
|
||||
];
|
||||
|
||||
describe('validateFieldNameAvailabilityOrThrow', () => {
|
||||
const objectMetadata = objectMetadataItemMock;
|
||||
|
||||
it.each(validateFieldNameAvailabilityTestCases)(
|
||||
'$title',
|
||||
({ context: { input, shouldNotThrow } }) => {
|
||||
if (shouldNotThrow) {
|
||||
expect(() =>
|
||||
validateFieldNameAvailabilityOrThrow(input, objectMetadata),
|
||||
validateFieldNameAvailabilityOrThrow(
|
||||
input,
|
||||
objectMetadataMapItemMock,
|
||||
),
|
||||
).not.toThrow();
|
||||
} else {
|
||||
expect(() =>
|
||||
validateFieldNameAvailabilityOrThrow(input, objectMetadata),
|
||||
validateFieldNameAvailabilityOrThrow(
|
||||
input,
|
||||
objectMetadataMapItemMock,
|
||||
),
|
||||
).toThrowErrorMatchingSnapshot();
|
||||
}
|
||||
},
|
||||
|
||||
@ -1,3 +1,4 @@
|
||||
import omit from 'lodash.omit';
|
||||
import { FieldMetadataType } from 'twenty-shared/types';
|
||||
|
||||
import { ObjectMetadataInterface } from 'src/engine/metadata-modules/field-metadata/interfaces/object-metadata.interface';
|
||||
@ -16,9 +17,7 @@ export const generateObjectMetadataMaps = (
|
||||
};
|
||||
|
||||
for (const objectMetadata of objectMetadataCollection) {
|
||||
const fieldsByIdMap: FieldMetadataMap = {};
|
||||
const fieldsByNameMap: FieldMetadataMap = {};
|
||||
const fieldsByJoinColumnNameMap: FieldMetadataMap = {};
|
||||
const fieldIdByJoinColumnNameMap: Record<string, string> = {};
|
||||
|
||||
for (const fieldMetadata of objectMetadata.fields) {
|
||||
if (
|
||||
@ -28,20 +27,25 @@ export const generateObjectMetadataMaps = (
|
||||
)
|
||||
) {
|
||||
if (fieldMetadata.settings?.joinColumnName) {
|
||||
fieldsByJoinColumnNameMap[fieldMetadata.settings.joinColumnName] =
|
||||
fieldMetadata;
|
||||
fieldIdByJoinColumnNameMap[fieldMetadata.settings.joinColumnName] =
|
||||
fieldMetadata.id;
|
||||
}
|
||||
}
|
||||
|
||||
fieldsByNameMap[fieldMetadata.name] = fieldMetadata;
|
||||
fieldsByIdMap[fieldMetadata.id] = fieldMetadata;
|
||||
}
|
||||
|
||||
const fieldsByIdMap = objectMetadata.fields.reduce((acc, field) => {
|
||||
acc[field.id] = field;
|
||||
|
||||
return acc;
|
||||
}, {} as FieldMetadataMap);
|
||||
|
||||
const processedObjectMetadata: ObjectMetadataItemWithFieldMaps = {
|
||||
...objectMetadata,
|
||||
...omit(objectMetadata, 'fields'),
|
||||
fieldsById: fieldsByIdMap,
|
||||
fieldsByName: fieldsByNameMap,
|
||||
fieldsByJoinColumnName: fieldsByJoinColumnNameMap,
|
||||
fieldIdByName: Object.fromEntries(
|
||||
Object.entries(fieldsByIdMap).map(([id, field]) => [field.name, id]),
|
||||
),
|
||||
fieldIdByJoinColumnName: fieldIdByJoinColumnNameMap,
|
||||
};
|
||||
|
||||
objectMetadataMaps.byId[objectMetadata.id] = processedObjectMetadata;
|
||||
|
||||
@ -0,0 +1,18 @@
|
||||
import omit from 'lodash.omit';
|
||||
|
||||
import { ObjectMetadataInterface } from 'src/engine/metadata-modules/field-metadata/interfaces/object-metadata.interface';
|
||||
|
||||
import { ObjectMetadataItemWithFieldMaps } from 'src/engine/metadata-modules/types/object-metadata-item-with-field-maps';
|
||||
|
||||
export const getObjectMetadataFromObjectMetadataItemWithFieldMaps = (
|
||||
objectMetadataMapItem: ObjectMetadataItemWithFieldMaps,
|
||||
): ObjectMetadataInterface => {
|
||||
return {
|
||||
...omit(objectMetadataMapItem, [
|
||||
'fieldsById',
|
||||
'fieldIdByName',
|
||||
'fieldIdByJoinColumnName',
|
||||
]),
|
||||
fields: Object.values(objectMetadataMapItem.fieldsById),
|
||||
};
|
||||
};
|
||||
@ -1,14 +0,0 @@
|
||||
import omit from 'lodash.omit';
|
||||
|
||||
import { ObjectMetadataInterface } from 'src/engine/metadata-modules/field-metadata/interfaces/object-metadata.interface';
|
||||
|
||||
import { ObjectMetadataItemWithFieldMaps } from 'src/engine/metadata-modules/types/object-metadata-item-with-field-maps';
|
||||
|
||||
export const removeFieldMapsFromObjectMetadata = (
|
||||
objectMetadata: ObjectMetadataItemWithFieldMaps,
|
||||
): ObjectMetadataInterface =>
|
||||
omit(objectMetadata, [
|
||||
'fieldsById',
|
||||
'fieldsByName',
|
||||
'fieldsByJoinColumnName',
|
||||
]);
|
||||
@ -1,18 +1,18 @@
|
||||
import { compositeTypeDefinitions } from 'src/engine/metadata-modules/field-metadata/composite-types';
|
||||
import { computeCompositeColumnName } from 'src/engine/metadata-modules/field-metadata/utils/compute-column-name.util';
|
||||
import { isCompositeFieldMetadataType } from 'src/engine/metadata-modules/field-metadata/utils/is-composite-field-metadata-type.util';
|
||||
import { ObjectMetadataEntity } from 'src/engine/metadata-modules/object-metadata/object-metadata.entity';
|
||||
import { ObjectMetadataItemWithFieldMaps } from 'src/engine/metadata-modules/types/object-metadata-item-with-field-maps';
|
||||
import {
|
||||
InvalidMetadataException,
|
||||
InvalidMetadataExceptionCode,
|
||||
} from 'src/engine/metadata-modules/utils/exceptions/invalid-metadata.exception';
|
||||
|
||||
const getReservedCompositeFieldNames = (
|
||||
objectMetadata: ObjectMetadataEntity,
|
||||
objectMetadata: ObjectMetadataItemWithFieldMaps,
|
||||
) => {
|
||||
const reservedCompositeFieldsNames: string[] = [];
|
||||
|
||||
for (const field of objectMetadata.fields) {
|
||||
for (const field of Object.values(objectMetadata.fieldsById)) {
|
||||
if (isCompositeFieldMetadataType(field.type)) {
|
||||
const base = field.name;
|
||||
const compositeType = compositeTypeDefinitions.get(field.type);
|
||||
@ -30,12 +30,16 @@ const getReservedCompositeFieldNames = (
|
||||
|
||||
export const validateFieldNameAvailabilityOrThrow = (
|
||||
name: string,
|
||||
objectMetadata: ObjectMetadataEntity,
|
||||
objectMetadata: ObjectMetadataItemWithFieldMaps,
|
||||
) => {
|
||||
const reservedCompositeFieldsNames =
|
||||
getReservedCompositeFieldNames(objectMetadata);
|
||||
|
||||
if (objectMetadata.fields.some((field) => field.name === name)) {
|
||||
if (
|
||||
Object.values(objectMetadata.fieldsById).some(
|
||||
(field) => field.name === name,
|
||||
)
|
||||
) {
|
||||
throw new InvalidMetadataException(
|
||||
`Name "${name}" is not available`,
|
||||
InvalidMetadataExceptionCode.NOT_AVAILABLE,
|
||||
|
||||
@ -0,0 +1,35 @@
|
||||
import {
|
||||
ObjectMetadataException,
|
||||
ObjectMetadataExceptionCode,
|
||||
} from 'src/engine/metadata-modules/object-metadata/object-metadata.exception';
|
||||
import { ObjectMetadataMaps } from 'src/engine/metadata-modules/types/object-metadata-maps';
|
||||
|
||||
type ValidateNoOtherObjectWithSameNameExistsOrThrowsParams = {
|
||||
objectMetadataNameSingular: string;
|
||||
objectMetadataNamePlural: string;
|
||||
existingObjectMetadataId?: string;
|
||||
objectMetadataMaps: ObjectMetadataMaps;
|
||||
};
|
||||
|
||||
export const validatesNoOtherObjectWithSameNameExistsOrThrows = ({
|
||||
objectMetadataNameSingular,
|
||||
objectMetadataNamePlural,
|
||||
existingObjectMetadataId,
|
||||
objectMetadataMaps,
|
||||
}: ValidateNoOtherObjectWithSameNameExistsOrThrowsParams) => {
|
||||
const objectAlreadyExists = Object.values(objectMetadataMaps.byId).find(
|
||||
(objectMetadata) =>
|
||||
(objectMetadata.nameSingular === objectMetadataNameSingular ||
|
||||
objectMetadata.namePlural === objectMetadataNamePlural ||
|
||||
objectMetadata.nameSingular === objectMetadataNamePlural ||
|
||||
objectMetadata.namePlural === objectMetadataNameSingular) &&
|
||||
objectMetadata.id !== existingObjectMetadataId,
|
||||
);
|
||||
|
||||
if (objectAlreadyExists) {
|
||||
throw new ObjectMetadataException(
|
||||
'Object already exists',
|
||||
ObjectMetadataExceptionCode.OBJECT_ALREADY_EXISTS,
|
||||
);
|
||||
}
|
||||
};
|
||||
@ -55,34 +55,9 @@ export class WorkspaceFeatureFlagsMapCacheService {
|
||||
|
||||
async recomputeFeatureFlagsMapCache({
|
||||
workspaceId,
|
||||
ignoreLock = false,
|
||||
}: {
|
||||
workspaceId: string;
|
||||
ignoreLock?: boolean;
|
||||
}): Promise<void> {
|
||||
const isAlreadyCaching =
|
||||
await this.workspaceCacheStorageService.getFeatureFlagsMapOngoingCachingLock(
|
||||
workspaceId,
|
||||
);
|
||||
|
||||
if (isAlreadyCaching) {
|
||||
if (ignoreLock) {
|
||||
this.logger.warn(
|
||||
`Feature flags map cache is already being cached (workspace ${workspaceId}), respecting lock and returning no data`,
|
||||
);
|
||||
|
||||
return;
|
||||
} else {
|
||||
this.logger.warn(
|
||||
`Feature flags map cache is already being cached (workspace ${workspaceId}), ignoring lock`,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
await this.workspaceCacheStorageService.addFeatureFlagMapOngoingCachingLock(
|
||||
workspaceId,
|
||||
);
|
||||
|
||||
const freshFeatureFlagMap =
|
||||
await this.getFeatureFlagsMapFromDatabase(workspaceId);
|
||||
|
||||
@ -90,10 +65,6 @@ export class WorkspaceFeatureFlagsMapCacheService {
|
||||
workspaceId,
|
||||
freshFeatureFlagMap,
|
||||
);
|
||||
|
||||
await this.workspaceCacheStorageService.removeFeatureFlagsMapOngoingCachingLock(
|
||||
workspaceId,
|
||||
);
|
||||
}
|
||||
|
||||
private async getFeatureFlagsMapFromDatabase(workspaceId: string) {
|
||||
|
||||
@ -2,9 +2,10 @@ import { Injectable, Logger } from '@nestjs/common';
|
||||
import { InjectRepository } from '@nestjs/typeorm';
|
||||
|
||||
import { isDefined } from 'twenty-shared/utils';
|
||||
import { Repository } from 'typeorm';
|
||||
import { In, Repository } from 'typeorm';
|
||||
|
||||
import { Workspace } from 'src/engine/core-modules/workspace/workspace.entity';
|
||||
import { IndexMetadataEntity } from 'src/engine/metadata-modules/index-metadata/index-metadata.entity';
|
||||
import { ObjectMetadataEntity } from 'src/engine/metadata-modules/object-metadata/object-metadata.entity';
|
||||
import { ObjectMetadataMaps } from 'src/engine/metadata-modules/types/object-metadata-maps';
|
||||
import { generateObjectMetadataMaps } from 'src/engine/metadata-modules/utils/generate-object-metadata-maps.util';
|
||||
@ -14,6 +15,11 @@ import {
|
||||
} 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';
|
||||
|
||||
type getExistingOrRecomputeMetadataMapsResult = {
|
||||
objectMetadataMaps: ObjectMetadataMaps;
|
||||
metadataVersion: number;
|
||||
};
|
||||
|
||||
@Injectable()
|
||||
export class WorkspaceMetadataCacheService {
|
||||
logger = new Logger(WorkspaceMetadataCacheService.name);
|
||||
@ -24,21 +30,15 @@ export class WorkspaceMetadataCacheService {
|
||||
private readonly workspaceCacheStorageService: WorkspaceCacheStorageService,
|
||||
@InjectRepository(ObjectMetadataEntity, 'core')
|
||||
private readonly objectMetadataRepository: Repository<ObjectMetadataEntity>,
|
||||
@InjectRepository(IndexMetadataEntity, 'core')
|
||||
private readonly indexMetadataRepository: Repository<IndexMetadataEntity>,
|
||||
) {}
|
||||
|
||||
async recomputeMetadataCache({
|
||||
async getExistingOrRecomputeMetadataMaps({
|
||||
workspaceId,
|
||||
ignoreLock = false,
|
||||
}: {
|
||||
workspaceId: string;
|
||||
ignoreLock?: boolean;
|
||||
}): Promise<
|
||||
| {
|
||||
recomputedObjectMetadataMaps: ObjectMetadataMaps;
|
||||
recomputedMetadataVersion: number;
|
||||
}
|
||||
| undefined
|
||||
> {
|
||||
}): Promise<getExistingOrRecomputeMetadataMapsResult> {
|
||||
const currentCacheVersion =
|
||||
await this.getMetadataVersionFromCache(workspaceId);
|
||||
|
||||
@ -52,68 +52,94 @@ export class WorkspaceMetadataCacheService {
|
||||
);
|
||||
}
|
||||
|
||||
if (currentDatabaseVersion === currentCacheVersion) {
|
||||
return;
|
||||
}
|
||||
const shouldRecompute =
|
||||
!isDefined(currentCacheVersion) ||
|
||||
currentCacheVersion !== currentDatabaseVersion;
|
||||
|
||||
if (!ignoreLock) {
|
||||
const isAlreadyCaching =
|
||||
await this.workspaceCacheStorageService.getObjectMetadataOngoingCachingLock(
|
||||
workspaceId,
|
||||
currentDatabaseVersion,
|
||||
);
|
||||
|
||||
if (isAlreadyCaching) {
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
if (currentCacheVersion !== undefined) {
|
||||
this.workspaceCacheStorageService.flushVersionedMetadata(
|
||||
workspaceId,
|
||||
currentCacheVersion,
|
||||
);
|
||||
}
|
||||
|
||||
try {
|
||||
await this.workspaceCacheStorageService.addObjectMetadataCollectionOngoingCachingLock(
|
||||
const existingObjectMetadataMaps =
|
||||
await this.workspaceCacheStorageService.getObjectMetadataMaps(
|
||||
workspaceId,
|
||||
currentDatabaseVersion,
|
||||
);
|
||||
|
||||
const objectMetadataItems = await this.objectMetadataRepository.find({
|
||||
where: { workspaceId },
|
||||
relations: [
|
||||
'fields',
|
||||
'indexMetadatas',
|
||||
'indexMetadatas.indexFieldMetadatas',
|
||||
],
|
||||
if (isDefined(existingObjectMetadataMaps) && !shouldRecompute) {
|
||||
return {
|
||||
objectMetadataMaps: existingObjectMetadataMaps,
|
||||
metadataVersion: currentDatabaseVersion,
|
||||
};
|
||||
}
|
||||
|
||||
const { objectMetadataMaps, metadataVersion } =
|
||||
await this.recomputeMetadataCache({
|
||||
workspaceId,
|
||||
});
|
||||
|
||||
const freshObjectMetadataMaps =
|
||||
generateObjectMetadataMaps(objectMetadataItems);
|
||||
return {
|
||||
objectMetadataMaps,
|
||||
metadataVersion,
|
||||
};
|
||||
}
|
||||
|
||||
await this.workspaceCacheStorageService.setObjectMetadataMaps(
|
||||
workspaceId,
|
||||
currentDatabaseVersion,
|
||||
freshObjectMetadataMaps,
|
||||
);
|
||||
async recomputeMetadataCache({
|
||||
workspaceId,
|
||||
}: {
|
||||
workspaceId: string;
|
||||
}): Promise<getExistingOrRecomputeMetadataMapsResult> {
|
||||
const currentDatabaseVersion =
|
||||
await this.getMetadataVersionFromDatabase(workspaceId);
|
||||
|
||||
await this.workspaceCacheStorageService.setMetadataVersion(
|
||||
workspaceId,
|
||||
currentDatabaseVersion,
|
||||
);
|
||||
|
||||
return {
|
||||
recomputedObjectMetadataMaps: freshObjectMetadataMaps,
|
||||
recomputedMetadataVersion: currentDatabaseVersion,
|
||||
};
|
||||
} finally {
|
||||
await this.workspaceCacheStorageService.removeObjectMetadataOngoingCachingLock(
|
||||
workspaceId,
|
||||
currentDatabaseVersion,
|
||||
if (!isDefined(currentDatabaseVersion)) {
|
||||
throw new WorkspaceMetadataVersionException(
|
||||
'Metadata version not found in the database',
|
||||
WorkspaceMetadataVersionExceptionCode.METADATA_VERSION_NOT_FOUND,
|
||||
);
|
||||
}
|
||||
|
||||
await this.workspaceCacheStorageService.flushVersionedMetadata(workspaceId);
|
||||
|
||||
const objectMetadataItems = await this.objectMetadataRepository.find({
|
||||
where: { workspaceId },
|
||||
relations: ['fields'],
|
||||
});
|
||||
|
||||
const objectMetadataItemsIds = objectMetadataItems.map(
|
||||
(objectMetadataItem) => objectMetadataItem.id,
|
||||
);
|
||||
|
||||
const indexMetadataItems = await this.indexMetadataRepository.find({
|
||||
where: { objectMetadataId: In(objectMetadataItemsIds) },
|
||||
relations: ['indexFieldMetadatas'],
|
||||
});
|
||||
|
||||
const objectMetadataItemsWithIndexMetadatas = objectMetadataItems.map(
|
||||
(objectMetadataItem) => ({
|
||||
...objectMetadataItem,
|
||||
indexMetadatas: indexMetadataItems.filter(
|
||||
(indexMetadataItem) =>
|
||||
indexMetadataItem.objectMetadataId === objectMetadataItem.id,
|
||||
),
|
||||
}),
|
||||
);
|
||||
|
||||
const freshObjectMetadataMaps = generateObjectMetadataMaps(
|
||||
objectMetadataItemsWithIndexMetadatas,
|
||||
);
|
||||
|
||||
await this.workspaceCacheStorageService.setObjectMetadataMaps(
|
||||
workspaceId,
|
||||
currentDatabaseVersion,
|
||||
freshObjectMetadataMaps,
|
||||
);
|
||||
|
||||
await this.workspaceCacheStorageService.setMetadataVersion(
|
||||
workspaceId,
|
||||
currentDatabaseVersion,
|
||||
);
|
||||
|
||||
return {
|
||||
objectMetadataMaps: freshObjectMetadataMaps,
|
||||
metadataVersion: currentDatabaseVersion,
|
||||
};
|
||||
}
|
||||
|
||||
private async getMetadataVersionFromDatabase(
|
||||
|
||||
@ -2,14 +2,17 @@ import { Module } from '@nestjs/common';
|
||||
import { TypeOrmModule } from '@nestjs/typeorm';
|
||||
|
||||
import { Workspace } from 'src/engine/core-modules/workspace/workspace.entity';
|
||||
import { IndexMetadataEntity } from 'src/engine/metadata-modules/index-metadata/index-metadata.entity';
|
||||
import { ObjectMetadataEntity } from 'src/engine/metadata-modules/object-metadata/object-metadata.entity';
|
||||
import { WorkspaceMetadataCacheService } from 'src/engine/metadata-modules/workspace-metadata-cache/services/workspace-metadata-cache.service';
|
||||
import { WorkspaceCacheStorageModule } from 'src/engine/workspace-cache-storage/workspace-cache-storage.module';
|
||||
|
||||
@Module({
|
||||
imports: [
|
||||
TypeOrmModule.forFeature([Workspace], 'core'),
|
||||
TypeOrmModule.forFeature([ObjectMetadataEntity], 'core'),
|
||||
TypeOrmModule.forFeature(
|
||||
[Workspace, ObjectMetadataEntity, IndexMetadataEntity],
|
||||
'core',
|
||||
),
|
||||
WorkspaceCacheStorageModule,
|
||||
],
|
||||
exports: [WorkspaceMetadataCacheService],
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user