Improve performance on metadata computation (#12785)

In this PR:

## Improve recompute metadata cache performance. We are aiming for
~100ms

Deleting relationMetadata table and FKs pointing on it
Fetching indexMetadata and indexFieldMetadata in a separate query as
typeorm is suboptimizing

## Remove caching lock

As recomputing the metadata cache is lighter, we try to stop preventing
multiple concurrent computations. This also simplifies interfaces

## Introduce self recovery mecanisms to recompute cache automatically if
corrupted

Aka getFreshObjectMetadataMaps

## custom object resolver performance improvement:  1sec to 200ms

Double check queries and indexes used while creating a custom object
Remove the queries to db to use the cached objectMetadataMap

## reduce objectMetadataMaps to 500kb
<img width="222" alt="image"
src="https://github.com/user-attachments/assets/2370dc80-49b6-4b63-8d5e-30c5ebdaa062"
/>

We used to stored 3 fieldMetadataMaps (byId, byName, byJoinColumnName).
While this is great for devXP, this is not great for performances.
Using the same mecanisme as for objectMetadataMap: we only keep byIdMap
and introduce two otherMaps to idByName, idByJoinColumnName to make the
bridge

## Add dataloader on IndexMetadata (aka indexMetadataList in the API)

## Improve field resolver performances too

## Deprecate ClientConfig
This commit is contained in:
Charles Bochet
2025-06-23 21:06:17 +02:00
committed by GitHub
parent 6aee42ab22
commit d5c974054d
145 changed files with 1485 additions and 2245 deletions

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -13,7 +13,7 @@ export interface ObjectMetadataInterface {
labelSingular: string;
labelPlural: string;
description?: string;
icon?: string;
icon: string;
targetTableName: string;
fields: FieldMetadataInterface[];
indexMetadatas: IndexMetadataInterface[];

View File

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

View File

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

View File

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

View File

@ -80,5 +80,5 @@ export class IndexMetadataEntity {
default: IndexType.BTREE,
nullable: false,
})
indexType?: IndexType;
indexType: IndexType;
}

View File

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

View File

@ -8,4 +8,6 @@ export interface IndexFieldMetadataInterface {
fieldMetadata: FieldMetadataInterface;
indexMetadata: IndexMetadataInterface;
order: number;
createdAt: Date;
updatedAt: Date;
}

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -83,7 +83,10 @@ export class ObjectMetadataRelatedRecordsService {
}
public async updateObjectViews(
updatedObjectMetadata: ObjectMetadataEntity,
updatedObjectMetadata: Pick<
ObjectMetadataEntity,
'id' | 'labelPlural' | 'icon'
>,
workspaceId: string,
) {
const viewRepository =

View File

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

View File

@ -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[] = [];

View File

@ -218,7 +218,6 @@ describe('ObjectPermissionService', () => {
).toHaveBeenCalledWith({
workspaceId,
roleIds: [roleId],
ignoreLock: true,
});
});

View File

@ -101,7 +101,6 @@ export class ObjectPermissionService {
{
workspaceId,
roleIds: [input.roleId],
ignoreLock: true,
},
);

View File

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

View File

@ -122,7 +122,6 @@ export class SettingPermissionService {
{
workspaceId,
roleIds: [input.roleId],
ignoreLock: true,
},
);
}

View File

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

View File

@ -62,7 +62,6 @@ export class UserRoleService {
await this.workspacePermissionsCacheService.recomputeUserWorkspaceRoleMapCache(
{
workspaceId,
ignoreLock: true,
},
);
}

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -64,28 +64,6 @@ export class WorkspacePermissionsCacheStorageService {
);
}
addRolesPermissionsOngoingCachingLock(workspaceId: string) {
return this.cacheStorageService.set<boolean>(
`${WorkspaceCacheKeys.MetadataPermissionsRolesPermissionsOngoingCachingLock}:${workspaceId}`,
true,
1_000 * 60, // 1 minute
);
}
removeRolesPermissionsOngoingCachingLock(workspaceId: string) {
return this.cacheStorageService.del(
`${WorkspaceCacheKeys.MetadataPermissionsRolesPermissionsOngoingCachingLock}:${workspaceId}`,
);
}
getRolesPermissionsOngoingCachingLock(
workspaceId: string,
): Promise<boolean | undefined> {
return this.cacheStorageService.get<boolean>(
`${WorkspaceCacheKeys.MetadataPermissionsRolesPermissionsOngoingCachingLock}:${workspaceId}`,
);
}
async setUserWorkspaceRoleMap(
workspaceId: string,
userWorkspaceRoleMap: UserWorkspaceRoleMap,
@ -128,31 +106,9 @@ export class WorkspacePermissionsCacheStorageService {
);
}
addUserWorkspaceRoleMapOngoingCachingLock(workspaceId: string) {
return this.cacheStorageService.set<boolean>(
`${WorkspaceCacheKeys.MetadataPermissionsUserWorkspaceRoleMapOngoingCachingLock}:${workspaceId}`,
true,
1_000 * 60, // 1 minute
);
}
removeUserWorkspaceRoleMapOngoingCachingLock(workspaceId: string) {
return this.cacheStorageService.del(
`${WorkspaceCacheKeys.MetadataPermissionsUserWorkspaceRoleMapOngoingCachingLock}:${workspaceId}`,
);
}
removeUserWorkspaceRoleMap(workspaceId: string) {
return this.cacheStorageService.del(
`${WorkspaceCacheKeys.MetadataPermissionsUserWorkspaceRoleMap}:${workspaceId}`,
);
}
getUserWorkspaceRoleMapOngoingCachingLock(
workspaceId: string,
): Promise<boolean | undefined> {
return this.cacheStorageService.get<boolean>(
`${WorkspaceCacheKeys.MetadataPermissionsUserWorkspaceRoleMapOngoingCachingLock}:${workspaceId}`,
);
}
}

View File

@ -12,7 +12,6 @@ import { ObjectMetadataEntity } from 'src/engine/metadata-modules/object-metadat
import { SettingPermissionType } from 'src/engine/metadata-modules/permissions/constants/setting-permission-type.constants';
import { RoleEntity } from 'src/engine/metadata-modules/role/role.entity';
import { UserWorkspaceRoleEntity } from 'src/engine/metadata-modules/role/user-workspace-role.entity';
import { WorkspaceFeatureFlagsMapCacheService } from 'src/engine/metadata-modules/workspace-feature-flags-map-cache/workspace-feature-flags-map-cache.service';
import { UserWorkspaceRoleMap } from 'src/engine/metadata-modules/workspace-permissions-cache/types/user-workspace-role-map.type';
import { WorkspacePermissionsCacheStorageService } from 'src/engine/metadata-modules/workspace-permissions-cache/workspace-permissions-cache-storage.service';
import { TwentyORMExceptionCode } from 'src/engine/twenty-orm/exceptions/twenty-orm.exception';
@ -39,118 +38,55 @@ export class WorkspacePermissionsCacheService {
@InjectRepository(UserWorkspaceRoleEntity, 'core')
private readonly userWorkspaceRoleRepository: Repository<UserWorkspaceRoleEntity>,
private readonly workspacePermissionsCacheStorageService: WorkspacePermissionsCacheStorageService,
private readonly workspaceFeatureFlagsMapCacheService: WorkspaceFeatureFlagsMapCacheService,
) {}
async recomputeRolesPermissionsCache({
workspaceId,
ignoreLock = false,
roleIds,
}: {
workspaceId: string;
ignoreLock?: boolean;
roleIds?: string[];
}): Promise<void> {
const isAlreadyCaching =
await this.workspacePermissionsCacheStorageService.getRolesPermissionsOngoingCachingLock(
workspaceId,
);
let currentRolesPermissions: ObjectRecordsPermissionsByRoleId | undefined;
if (isAlreadyCaching) {
if (ignoreLock) {
this.logger.warn(
`RolesPermissions data is already being cached (workspace ${workspaceId}), ignoring lock`,
);
} else {
this.logger.warn(
`RolesPermissions data is already being cached (workspace ${workspaceId}), respecting lock and returning no data`,
);
return;
}
}
await this.workspacePermissionsCacheStorageService.addRolesPermissionsOngoingCachingLock(
workspaceId,
);
try {
let currentRolesPermissions: ObjectRecordsPermissionsByRoleId | undefined;
if (roleIds) {
currentRolesPermissions =
await this.workspacePermissionsCacheStorageService.getRolesPermissions(
workspaceId,
);
}
const recomputedRolesPermissions =
await this.getObjectRecordPermissionsForRoles({
if (roleIds) {
currentRolesPermissions =
await this.workspacePermissionsCacheStorageService.getRolesPermissions(
workspaceId,
roleIds,
});
const freshObjectRecordsPermissionsByRoleId = roleIds
? { ...currentRolesPermissions, ...recomputedRolesPermissions }
: recomputedRolesPermissions;
await this.workspacePermissionsCacheStorageService.setRolesPermissions(
workspaceId,
freshObjectRecordsPermissionsByRoleId,
);
} finally {
await this.workspacePermissionsCacheStorageService.removeRolesPermissionsOngoingCachingLock(
workspaceId,
);
);
}
const recomputedRolesPermissions =
await this.getObjectRecordPermissionsForRoles({
workspaceId,
roleIds,
});
const freshObjectRecordsPermissionsByRoleId = roleIds
? { ...currentRolesPermissions, ...recomputedRolesPermissions }
: recomputedRolesPermissions;
await this.workspacePermissionsCacheStorageService.setRolesPermissions(
workspaceId,
freshObjectRecordsPermissionsByRoleId,
);
}
async recomputeUserWorkspaceRoleMapCache({
workspaceId,
ignoreLock = false,
}: {
workspaceId: string;
ignoreLock?: boolean;
}): Promise<void> {
try {
const isAlreadyCaching =
await this.workspacePermissionsCacheStorageService.getUserWorkspaceRoleMapOngoingCachingLock(
const freshUserWorkspaceRoleMap =
await this.getUserWorkspaceRoleMapFromDatabase({
workspaceId,
);
});
if (isAlreadyCaching) {
if (ignoreLock) {
this.logger.warn(
`UserWorkspaceRoleMap data is already being cached (workspace ${workspaceId}), ignoring lock`,
);
} else {
this.logger.warn(
`UserWorkspaceRoleMap data is already being cached (workspace ${workspaceId}), respecting lock and returning no data`,
);
return;
}
}
await this.workspacePermissionsCacheStorageService.addUserWorkspaceRoleMapOngoingCachingLock(
await this.workspacePermissionsCacheStorageService.setUserWorkspaceRoleMap(
workspaceId,
freshUserWorkspaceRoleMap,
);
try {
const freshUserWorkspaceRoleMap =
await this.getUserWorkspaceRoleMapFromDatabase({
workspaceId,
});
await this.workspacePermissionsCacheStorageService.setUserWorkspaceRoleMap(
workspaceId,
freshUserWorkspaceRoleMap,
);
} finally {
await this.workspacePermissionsCacheStorageService.removeUserWorkspaceRoleMapOngoingCachingLock(
workspaceId,
);
}
} catch (error) {
// Flush stale userWorkspaceRoleMap
await this.workspacePermissionsCacheStorageService.removeUserWorkspaceRoleMap(