Refactor migration runner within transaction (#12941)
Modifying the data-model can sometimes fail in the middle of your operation, due to the way we handle both metadata update and schema migration separately, a field can be created while the associated column creation failed (same for object/table and such). This is also an issue because WorkspaceMigrations are then stored as FAILED can never really recovered by themselves so the schema is broken and we can't update the models anymore. This PR adds a executeMigrationFromPendingMigrationsWithinTransaction method where we can (and must) pass a queryRunner executing a transaction, which should come from the metadata services so that if anything during metadata update OR schema update fails, it rolls back everything (this also mean a workspaceMigration should never stay in a failed state now). This also fixes some issues with migration not running in the correct order due to having the same timestamp and having to do some weird logic to fix that. This is a first step and fix before working on a much more reliable solution in the upcoming weeks where we will refactor the way we interact with the data model. --------- Co-authored-by: Charles Bochet <charlesBochet@users.noreply.github.com>
This commit is contained in:
@ -2,55 +2,23 @@ import { Injectable, OnModuleDestroy, OnModuleInit } from '@nestjs/common';
|
|||||||
|
|
||||||
import { DataSource } from 'typeorm';
|
import { DataSource } from 'typeorm';
|
||||||
|
|
||||||
import { AppToken } from 'src/engine/core-modules/app-token/app-token.entity';
|
|
||||||
import { ApprovedAccessDomain } from 'src/engine/core-modules/approved-access-domain/approved-access-domain.entity';
|
|
||||||
import { BillingCustomer } from 'src/engine/core-modules/billing/entities/billing-customer.entity';
|
|
||||||
import { BillingEntitlement } from 'src/engine/core-modules/billing/entities/billing-entitlement.entity';
|
|
||||||
import { BillingMeter } from 'src/engine/core-modules/billing/entities/billing-meter.entity';
|
|
||||||
import { BillingPrice } from 'src/engine/core-modules/billing/entities/billing-price.entity';
|
|
||||||
import { BillingProduct } from 'src/engine/core-modules/billing/entities/billing-product.entity';
|
|
||||||
import { BillingSubscriptionItem } from 'src/engine/core-modules/billing/entities/billing-subscription-item.entity';
|
|
||||||
import { BillingSubscription } from 'src/engine/core-modules/billing/entities/billing-subscription.entity';
|
|
||||||
import { FeatureFlag } from 'src/engine/core-modules/feature-flag/feature-flag.entity';
|
|
||||||
import { KeyValuePair } from 'src/engine/core-modules/key-value-pair/key-value-pair.entity';
|
|
||||||
import { PostgresCredentials } from 'src/engine/core-modules/postgres-credentials/postgres-credentials.entity';
|
|
||||||
import { WorkspaceSSOIdentityProvider } from 'src/engine/core-modules/sso/workspace-sso-identity-provider.entity';
|
|
||||||
import { TwentyConfigService } from 'src/engine/core-modules/twenty-config/twenty-config.service';
|
import { TwentyConfigService } from 'src/engine/core-modules/twenty-config/twenty-config.service';
|
||||||
import { TwoFactorMethod } from 'src/engine/core-modules/two-factor-method/two-factor-method.entity';
|
|
||||||
import { UserWorkspace } from 'src/engine/core-modules/user-workspace/user-workspace.entity';
|
|
||||||
import { User } from 'src/engine/core-modules/user/user.entity';
|
|
||||||
import { Workspace } from 'src/engine/core-modules/workspace/workspace.entity';
|
|
||||||
import { AgentEntity } from 'src/engine/metadata-modules/agent/agent.entity';
|
|
||||||
|
|
||||||
@Injectable()
|
@Injectable()
|
||||||
export class TypeORMService implements OnModuleInit, OnModuleDestroy {
|
export class TypeORMService implements OnModuleInit, OnModuleDestroy {
|
||||||
private mainDataSource: DataSource;
|
private mainDataSource: DataSource;
|
||||||
|
|
||||||
constructor(private readonly twentyConfigService: TwentyConfigService) {
|
constructor(private readonly twentyConfigService: TwentyConfigService) {
|
||||||
|
const isJest = process.argv.some((arg) => arg.includes('jest'));
|
||||||
|
|
||||||
this.mainDataSource = new DataSource({
|
this.mainDataSource = new DataSource({
|
||||||
url: twentyConfigService.get('PG_DATABASE_URL'),
|
url: twentyConfigService.get('PG_DATABASE_URL'),
|
||||||
type: 'postgres',
|
type: 'postgres',
|
||||||
logging: twentyConfigService.getLoggingConfig(),
|
logging: twentyConfigService.getLoggingConfig(),
|
||||||
schema: 'core',
|
schema: 'core',
|
||||||
entities: [
|
entities: [
|
||||||
User,
|
`${isJest ? '' : 'dist/'}src/engine/core-modules/**/*.entity{.ts,.js}`,
|
||||||
Workspace,
|
`${isJest ? '' : 'dist/'}src/engine/metadata-modules/**/*.entity{.ts,.js}`,
|
||||||
UserWorkspace,
|
|
||||||
AppToken,
|
|
||||||
KeyValuePair,
|
|
||||||
FeatureFlag,
|
|
||||||
BillingSubscription,
|
|
||||||
BillingSubscriptionItem,
|
|
||||||
BillingMeter,
|
|
||||||
BillingCustomer,
|
|
||||||
BillingProduct,
|
|
||||||
BillingPrice,
|
|
||||||
BillingEntitlement,
|
|
||||||
PostgresCredentials,
|
|
||||||
WorkspaceSSOIdentityProvider,
|
|
||||||
ApprovedAccessDomain,
|
|
||||||
TwoFactorMethod,
|
|
||||||
AgentEntity,
|
|
||||||
],
|
],
|
||||||
metadataTableName: '_typeorm_generated_columns_and_materialized_views',
|
metadataTableName: '_typeorm_generated_columns_and_materialized_views',
|
||||||
ssl: twentyConfigService.get('PG_SSL_ALLOW_SELF_SIGNED')
|
ssl: twentyConfigService.get('PG_SSL_ALLOW_SELF_SIGNED')
|
||||||
|
|||||||
@ -158,6 +158,11 @@ export class FieldMetadataService extends TypeOrmQueryService<FieldMetadataEntit
|
|||||||
await queryRunner.startTransaction();
|
await queryRunner.startTransaction();
|
||||||
|
|
||||||
try {
|
try {
|
||||||
|
const fieldMetadataRepository =
|
||||||
|
queryRunner.manager.getRepository<FieldMetadataEntity>(
|
||||||
|
FieldMetadataEntity,
|
||||||
|
);
|
||||||
|
|
||||||
if (
|
if (
|
||||||
!isDefined(
|
!isDefined(
|
||||||
objectMetadataItemWithFieldMaps.labelIdentifierFieldMetadataId,
|
objectMetadataItemWithFieldMaps.labelIdentifierFieldMetadataId,
|
||||||
@ -224,10 +229,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 this.fieldMetadataRepository.find({
|
const [updatedFieldMetadata] = await fieldMetadataRepository.find({
|
||||||
where: { id },
|
where: { id },
|
||||||
});
|
});
|
||||||
|
|
||||||
@ -238,6 +242,36 @@ export class FieldMetadataService extends TypeOrmQueryService<FieldMetadataEntit
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (
|
||||||
|
isDefined(fieldMetadataInput.name) ||
|
||||||
|
isDefined(updatableFieldInput.options) ||
|
||||||
|
isDefined(updatableFieldInput.defaultValue)
|
||||||
|
) {
|
||||||
|
await this.workspaceMigrationService.createCustomMigration(
|
||||||
|
generateMigrationName(`update-${updatedFieldMetadata.name}`),
|
||||||
|
fieldMetadataInput.workspaceId,
|
||||||
|
[
|
||||||
|
{
|
||||||
|
name: computeObjectTargetTable(objectMetadataItemWithFieldMaps),
|
||||||
|
action: WorkspaceMigrationTableActionType.ALTER,
|
||||||
|
columns: this.workspaceMigrationFactory.createColumnActions(
|
||||||
|
WorkspaceMigrationColumnActionType.ALTER,
|
||||||
|
existingFieldMetadata,
|
||||||
|
updatedFieldMetadata,
|
||||||
|
),
|
||||||
|
} satisfies WorkspaceMigrationTableAction,
|
||||||
|
],
|
||||||
|
queryRunner,
|
||||||
|
);
|
||||||
|
|
||||||
|
await this.workspaceMigrationRunnerService.executeMigrationFromPendingMigrationsWithinTransaction(
|
||||||
|
updatedFieldMetadata.workspaceId,
|
||||||
|
queryRunner,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
await queryRunner.commitTransaction();
|
||||||
|
|
||||||
if (fieldMetadataInput.isActive === false) {
|
if (fieldMetadataInput.isActive === false) {
|
||||||
const viewsRepository =
|
const viewsRepository =
|
||||||
await this.twentyORMGlobalManager.getRepositoryForWorkspace(
|
await this.twentyORMGlobalManager.getRepositoryForWorkspace(
|
||||||
@ -266,44 +300,19 @@ export class FieldMetadataService extends TypeOrmQueryService<FieldMetadataEntit
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (
|
|
||||||
isDefined(fieldMetadataInput.name) ||
|
|
||||||
isDefined(updatableFieldInput.options) ||
|
|
||||||
isDefined(updatableFieldInput.defaultValue)
|
|
||||||
) {
|
|
||||||
await this.workspaceMigrationService.createCustomMigration(
|
|
||||||
generateMigrationName(`update-${updatedFieldMetadata.name}`),
|
|
||||||
fieldMetadataInput.workspaceId,
|
|
||||||
[
|
|
||||||
{
|
|
||||||
name: computeObjectTargetTable(objectMetadataItemWithFieldMaps),
|
|
||||||
action: WorkspaceMigrationTableActionType.ALTER,
|
|
||||||
columns: this.workspaceMigrationFactory.createColumnActions(
|
|
||||||
WorkspaceMigrationColumnActionType.ALTER,
|
|
||||||
existingFieldMetadata,
|
|
||||||
updatedFieldMetadata,
|
|
||||||
),
|
|
||||||
} satisfies WorkspaceMigrationTableAction,
|
|
||||||
],
|
|
||||||
);
|
|
||||||
|
|
||||||
await this.workspaceMigrationRunnerService.executeMigrationFromPendingMigrations(
|
|
||||||
updatedFieldMetadata.workspaceId,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
await queryRunner.commitTransaction();
|
|
||||||
|
|
||||||
return updatedFieldMetadata;
|
|
||||||
} catch (error) {
|
|
||||||
await queryRunner.rollbackTransaction();
|
|
||||||
throw error;
|
|
||||||
} finally {
|
|
||||||
await queryRunner.release();
|
|
||||||
|
|
||||||
await this.workspaceMetadataVersionService.incrementMetadataVersion(
|
await this.workspaceMetadataVersionService.incrementMetadataVersion(
|
||||||
fieldMetadataInput.workspaceId,
|
fieldMetadataInput.workspaceId,
|
||||||
);
|
);
|
||||||
|
|
||||||
|
return updatedFieldMetadata;
|
||||||
|
} catch (error) {
|
||||||
|
if (queryRunner.isTransactionActive) {
|
||||||
|
await queryRunner.rollbackTransaction();
|
||||||
|
}
|
||||||
|
|
||||||
|
throw error;
|
||||||
|
} finally {
|
||||||
|
await queryRunner.release();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -314,7 +323,7 @@ export class FieldMetadataService extends TypeOrmQueryService<FieldMetadataEntit
|
|||||||
const queryRunner = this.coreDataSource.createQueryRunner();
|
const queryRunner = this.coreDataSource.createQueryRunner();
|
||||||
|
|
||||||
await queryRunner.connect();
|
await queryRunner.connect();
|
||||||
await queryRunner.startTransaction(); // transaction not safe as a different queryRunner is used within workspaceMigrationRunnerService
|
await queryRunner.startTransaction();
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const fieldMetadataRepository =
|
const fieldMetadataRepository =
|
||||||
@ -357,11 +366,6 @@ export class FieldMetadataService extends TypeOrmQueryService<FieldMetadataEntit
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
await this.viewService.resetKanbanAggregateOperationByFieldMetadataId({
|
|
||||||
workspaceId,
|
|
||||||
fieldMetadataId: fieldMetadata.id,
|
|
||||||
});
|
|
||||||
|
|
||||||
if (fieldMetadata.type === FieldMetadataType.RELATION) {
|
if (fieldMetadata.type === FieldMetadataType.RELATION) {
|
||||||
const isManyToOneRelation =
|
const isManyToOneRelation =
|
||||||
(fieldMetadata as FieldMetadataEntity<FieldMetadataType.RELATION>)
|
(fieldMetadata as FieldMetadataEntity<FieldMetadataType.RELATION>)
|
||||||
@ -402,6 +406,7 @@ export class FieldMetadataService extends TypeOrmQueryService<FieldMetadataEntit
|
|||||||
],
|
],
|
||||||
} satisfies WorkspaceMigrationTableAction,
|
} satisfies WorkspaceMigrationTableAction,
|
||||||
],
|
],
|
||||||
|
queryRunner,
|
||||||
);
|
);
|
||||||
} else if (isCompositeFieldMetadataType(fieldMetadata.type)) {
|
} else if (isCompositeFieldMetadataType(fieldMetadata.type)) {
|
||||||
await fieldMetadataRepository.delete(fieldMetadata.id);
|
await fieldMetadataRepository.delete(fieldMetadata.id);
|
||||||
@ -433,6 +438,7 @@ export class FieldMetadataService extends TypeOrmQueryService<FieldMetadataEntit
|
|||||||
}),
|
}),
|
||||||
} satisfies WorkspaceMigrationTableAction,
|
} satisfies WorkspaceMigrationTableAction,
|
||||||
],
|
],
|
||||||
|
queryRunner,
|
||||||
);
|
);
|
||||||
} else {
|
} else {
|
||||||
await fieldMetadataRepository.delete(fieldMetadata.id);
|
await fieldMetadataRepository.delete(fieldMetadata.id);
|
||||||
@ -451,24 +457,35 @@ export class FieldMetadataService extends TypeOrmQueryService<FieldMetadataEntit
|
|||||||
],
|
],
|
||||||
} satisfies WorkspaceMigrationTableAction,
|
} satisfies WorkspaceMigrationTableAction,
|
||||||
],
|
],
|
||||||
|
queryRunner,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
await this.workspaceMigrationRunnerService.executeMigrationFromPendingMigrations(
|
await this.workspaceMigrationRunnerService.executeMigrationFromPendingMigrationsWithinTransaction(
|
||||||
workspaceId,
|
workspaceId,
|
||||||
|
queryRunner,
|
||||||
);
|
);
|
||||||
|
|
||||||
await queryRunner.commitTransaction();
|
await queryRunner.commitTransaction();
|
||||||
|
|
||||||
return fieldMetadata;
|
await this.viewService.resetKanbanAggregateOperationByFieldMetadataId({
|
||||||
} catch (error) {
|
workspaceId,
|
||||||
await queryRunner.rollbackTransaction();
|
fieldMetadataId: fieldMetadata.id,
|
||||||
throw error;
|
});
|
||||||
} finally {
|
|
||||||
await queryRunner.release();
|
|
||||||
await this.workspaceMetadataVersionService.incrementMetadataVersion(
|
await this.workspaceMetadataVersionService.incrementMetadataVersion(
|
||||||
workspaceId,
|
workspaceId,
|
||||||
);
|
);
|
||||||
|
|
||||||
|
return fieldMetadata;
|
||||||
|
} catch (error) {
|
||||||
|
if (queryRunner.isTransactionActive) {
|
||||||
|
await queryRunner.rollbackTransaction();
|
||||||
|
}
|
||||||
|
|
||||||
|
throw error;
|
||||||
|
} finally {
|
||||||
|
await queryRunner.release();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -943,26 +960,32 @@ export class FieldMetadataService extends TypeOrmQueryService<FieldMetadataEntit
|
|||||||
generateMigrationName(`create-multiple-fields`),
|
generateMigrationName(`create-multiple-fields`),
|
||||||
workspaceId,
|
workspaceId,
|
||||||
migrationActions,
|
migrationActions,
|
||||||
|
queryRunner,
|
||||||
);
|
);
|
||||||
|
|
||||||
await this.workspaceMigrationRunnerService.executeMigrationFromPendingMigrations(
|
await this.workspaceMigrationRunnerService.executeMigrationFromPendingMigrationsWithinTransaction(
|
||||||
workspaceId,
|
workspaceId,
|
||||||
|
queryRunner,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
await this.createViewAndViewFields(createdFieldMetadatas, workspaceId);
|
|
||||||
|
|
||||||
await queryRunner.commitTransaction();
|
await queryRunner.commitTransaction();
|
||||||
|
|
||||||
return createdFieldMetadatas;
|
await this.createViewAndViewFields(createdFieldMetadatas, workspaceId);
|
||||||
} catch (error) {
|
|
||||||
await queryRunner.rollbackTransaction();
|
|
||||||
throw error;
|
|
||||||
} finally {
|
|
||||||
await queryRunner.release();
|
|
||||||
await this.workspaceMetadataVersionService.incrementMetadataVersion(
|
await this.workspaceMetadataVersionService.incrementMetadataVersion(
|
||||||
workspaceId,
|
workspaceId,
|
||||||
);
|
);
|
||||||
|
|
||||||
|
return createdFieldMetadatas;
|
||||||
|
} catch (error) {
|
||||||
|
if (queryRunner.isTransactionActive) {
|
||||||
|
await queryRunner.rollbackTransaction();
|
||||||
|
}
|
||||||
|
|
||||||
|
throw error;
|
||||||
|
} finally {
|
||||||
|
await queryRunner.release();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -3,7 +3,7 @@ import { InjectRepository } from '@nestjs/typeorm';
|
|||||||
|
|
||||||
import isEmpty from 'lodash.isempty';
|
import isEmpty from 'lodash.isempty';
|
||||||
import { isDefined } from 'twenty-shared/utils';
|
import { isDefined } from 'twenty-shared/utils';
|
||||||
import { Repository } from 'typeorm';
|
import { QueryRunner, Repository } from 'typeorm';
|
||||||
|
|
||||||
import { FieldMetadataEntity } from 'src/engine/metadata-modules/field-metadata/field-metadata.entity';
|
import { FieldMetadataEntity } from 'src/engine/metadata-modules/field-metadata/field-metadata.entity';
|
||||||
import {
|
import {
|
||||||
@ -30,15 +30,25 @@ export class IndexMetadataService {
|
|||||||
private readonly workspaceMigrationService: WorkspaceMigrationService,
|
private readonly workspaceMigrationService: WorkspaceMigrationService,
|
||||||
) {}
|
) {}
|
||||||
|
|
||||||
async createIndexMetadata(
|
async createIndexMetadata({
|
||||||
workspaceId: string,
|
workspaceId,
|
||||||
objectMetadata: ObjectMetadataEntity,
|
objectMetadata,
|
||||||
fieldMetadataToIndex: Partial<FieldMetadataEntity>[],
|
fieldMetadataToIndex,
|
||||||
isUnique: boolean,
|
isUnique,
|
||||||
isCustom: boolean,
|
isCustom,
|
||||||
indexType?: IndexType,
|
indexType,
|
||||||
indexWhereClause?: string,
|
indexWhereClause,
|
||||||
) {
|
queryRunner,
|
||||||
|
}: {
|
||||||
|
workspaceId: string;
|
||||||
|
objectMetadata: ObjectMetadataEntity;
|
||||||
|
fieldMetadataToIndex: Partial<FieldMetadataEntity>[];
|
||||||
|
isUnique: boolean;
|
||||||
|
isCustom: boolean;
|
||||||
|
indexType?: IndexType;
|
||||||
|
indexWhereClause?: string;
|
||||||
|
queryRunner?: QueryRunner;
|
||||||
|
}) {
|
||||||
const tableName = computeObjectTargetTable(objectMetadata);
|
const tableName = computeObjectTargetTable(objectMetadata);
|
||||||
|
|
||||||
const columnNames: string[] = fieldMetadataToIndex.map(
|
const columnNames: string[] = fieldMetadataToIndex.map(
|
||||||
@ -53,7 +63,11 @@ export class IndexMetadataService {
|
|||||||
|
|
||||||
let result: IndexMetadataEntity;
|
let result: IndexMetadataEntity;
|
||||||
|
|
||||||
const existingIndex = await this.indexMetadataRepository.findOne({
|
const indexMetadataRepository = queryRunner
|
||||||
|
? queryRunner.manager.getRepository(IndexMetadataEntity)
|
||||||
|
: this.indexMetadataRepository;
|
||||||
|
|
||||||
|
const existingIndex = await indexMetadataRepository.findOne({
|
||||||
where: {
|
where: {
|
||||||
name: indexName,
|
name: indexName,
|
||||||
workspaceId,
|
workspaceId,
|
||||||
@ -68,7 +82,7 @@ export class IndexMetadataService {
|
|||||||
}
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
result = await this.indexMetadataRepository.save({
|
result = await indexMetadataRepository.save({
|
||||||
name: indexName,
|
name: indexName,
|
||||||
indexFieldMetadatas: fieldMetadataToIndex.map(
|
indexFieldMetadatas: fieldMetadataToIndex.map(
|
||||||
(fieldMetadata, index) => ({
|
(fieldMetadata, index) => ({
|
||||||
@ -93,15 +107,15 @@ export class IndexMetadataService {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
await this.createIndexCreationMigration(
|
await this.createIndexCreationMigration({
|
||||||
workspaceId,
|
workspaceId,
|
||||||
objectMetadata,
|
objectMetadata,
|
||||||
fieldMetadataToIndex,
|
fieldMetadataToIndex,
|
||||||
isUnique,
|
isUnique,
|
||||||
isCustom,
|
|
||||||
indexType,
|
indexType,
|
||||||
indexWhereClause,
|
indexWhereClause,
|
||||||
);
|
queryRunner,
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
async recomputeIndexMetadataForObject(
|
async recomputeIndexMetadataForObject(
|
||||||
@ -110,8 +124,13 @@ export class IndexMetadataService {
|
|||||||
ObjectMetadataEntity,
|
ObjectMetadataEntity,
|
||||||
'nameSingular' | 'isCustom' | 'id'
|
'nameSingular' | 'isCustom' | 'id'
|
||||||
>,
|
>,
|
||||||
|
queryRunner?: QueryRunner,
|
||||||
) {
|
) {
|
||||||
const indexesToRecompute = await this.indexMetadataRepository.find({
|
const indexMetadataRepository = queryRunner
|
||||||
|
? queryRunner.manager.getRepository(IndexMetadataEntity)
|
||||||
|
: this.indexMetadataRepository;
|
||||||
|
|
||||||
|
const indexesToRecompute = await indexMetadataRepository.find({
|
||||||
where: {
|
where: {
|
||||||
objectMetadataId: updatedObjectMetadata.id,
|
objectMetadataId: updatedObjectMetadata.id,
|
||||||
workspaceId,
|
workspaceId,
|
||||||
@ -142,7 +161,7 @@ export class IndexMetadataService {
|
|||||||
...columnNames,
|
...columnNames,
|
||||||
])}`;
|
])}`;
|
||||||
|
|
||||||
await this.indexMetadataRepository.update(index.id, {
|
await indexMetadataRepository.update(index.id, {
|
||||||
name: newIndexName,
|
name: newIndexName,
|
||||||
});
|
});
|
||||||
|
|
||||||
@ -160,6 +179,7 @@ export class IndexMetadataService {
|
|||||||
workspaceId: string,
|
workspaceId: string,
|
||||||
objectMetadata: ObjectMetadataEntity,
|
objectMetadata: ObjectMetadataEntity,
|
||||||
fieldMetadataToIndex: Partial<FieldMetadataEntity>[],
|
fieldMetadataToIndex: Partial<FieldMetadataEntity>[],
|
||||||
|
queryRunner?: QueryRunner,
|
||||||
) {
|
) {
|
||||||
const tableName = computeObjectTargetTable(objectMetadata);
|
const tableName = computeObjectTargetTable(objectMetadata);
|
||||||
|
|
||||||
@ -173,7 +193,11 @@ export class IndexMetadataService {
|
|||||||
|
|
||||||
const indexName = `IDX_${generateDeterministicIndexName([tableName, ...columnNames])}`;
|
const indexName = `IDX_${generateDeterministicIndexName([tableName, ...columnNames])}`;
|
||||||
|
|
||||||
const indexMetadata = await this.indexMetadataRepository.findOne({
|
const indexMetadataRepository = queryRunner
|
||||||
|
? queryRunner.manager.getRepository(IndexMetadataEntity)
|
||||||
|
: this.indexMetadataRepository;
|
||||||
|
|
||||||
|
const indexMetadata = await indexMetadataRepository.findOne({
|
||||||
where: {
|
where: {
|
||||||
name: indexName,
|
name: indexName,
|
||||||
objectMetadataId: objectMetadata.id,
|
objectMetadataId: objectMetadata.id,
|
||||||
@ -186,7 +210,7 @@ export class IndexMetadataService {
|
|||||||
}
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
await this.indexMetadataRepository.delete(indexMetadata.id);
|
await indexMetadataRepository.delete(indexMetadata.id);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
throw new Error(
|
throw new Error(
|
||||||
`Failed to delete index metadata with name ${indexName} (error: ${error.message})`,
|
`Failed to delete index metadata with name ${indexName} (error: ${error.message})`,
|
||||||
@ -194,15 +218,23 @@ export class IndexMetadataService {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
async createIndexCreationMigration(
|
async createIndexCreationMigration({
|
||||||
workspaceId: string,
|
workspaceId,
|
||||||
objectMetadata: ObjectMetadataEntity,
|
objectMetadata,
|
||||||
fieldMetadataToIndex: Partial<FieldMetadataEntity>[],
|
fieldMetadataToIndex,
|
||||||
isUnique: boolean,
|
isUnique,
|
||||||
isCustom: boolean,
|
indexType,
|
||||||
indexType?: IndexType,
|
indexWhereClause,
|
||||||
indexWhereClause?: string,
|
queryRunner,
|
||||||
) {
|
}: {
|
||||||
|
workspaceId: string;
|
||||||
|
objectMetadata: ObjectMetadataEntity;
|
||||||
|
fieldMetadataToIndex: Partial<FieldMetadataEntity>[];
|
||||||
|
isUnique: boolean;
|
||||||
|
indexType?: IndexType;
|
||||||
|
indexWhereClause?: string;
|
||||||
|
queryRunner?: QueryRunner;
|
||||||
|
}) {
|
||||||
const tableName = computeObjectTargetTable(objectMetadata);
|
const tableName = computeObjectTargetTable(objectMetadata);
|
||||||
|
|
||||||
const columnNames: string[] = fieldMetadataToIndex.map(
|
const columnNames: string[] = fieldMetadataToIndex.map(
|
||||||
@ -230,6 +262,7 @@ export class IndexMetadataService {
|
|||||||
generateMigrationName(`create-${objectMetadata.nameSingular}-index`),
|
generateMigrationName(`create-${objectMetadata.nameSingular}-index`),
|
||||||
workspaceId,
|
workspaceId,
|
||||||
[migration],
|
[migration],
|
||||||
|
queryRunner,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -244,6 +277,7 @@ export class IndexMetadataService {
|
|||||||
previousName: string;
|
previousName: string;
|
||||||
newName: string;
|
newName: string;
|
||||||
}[],
|
}[],
|
||||||
|
queryRunner?: QueryRunner,
|
||||||
) {
|
) {
|
||||||
for (const recomputedIndex of recomputedIndexes) {
|
for (const recomputedIndex of recomputedIndexes) {
|
||||||
const { previousName, newName, indexMetadata } = recomputedIndex;
|
const { previousName, newName, indexMetadata } = recomputedIndex;
|
||||||
@ -283,6 +317,7 @@ export class IndexMetadataService {
|
|||||||
generateMigrationName(`update-${objectMetadata.nameSingular}-index`),
|
generateMigrationName(`update-${objectMetadata.nameSingular}-index`),
|
||||||
workspaceId,
|
workspaceId,
|
||||||
[migration],
|
[migration],
|
||||||
|
queryRunner,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -31,6 +31,7 @@ import { WorkspaceMetadataVersionModule } from 'src/engine/metadata-modules/work
|
|||||||
import { WorkspaceMigrationModule } from 'src/engine/metadata-modules/workspace-migration/workspace-migration.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';
|
import { WorkspacePermissionsCacheModule } from 'src/engine/metadata-modules/workspace-permissions-cache/workspace-permissions-cache.module';
|
||||||
import { WorkspaceCacheStorageModule } from 'src/engine/workspace-cache-storage/workspace-cache-storage.module';
|
import { WorkspaceCacheStorageModule } from 'src/engine/workspace-cache-storage/workspace-cache-storage.module';
|
||||||
|
import { WorkspaceDataSourceModule } from 'src/engine/workspace-datasource/workspace-datasource.module';
|
||||||
import { WorkspaceMigrationRunnerModule } from 'src/engine/workspace-manager/workspace-migration-runner/workspace-migration-runner.module';
|
import { WorkspaceMigrationRunnerModule } from 'src/engine/workspace-manager/workspace-migration-runner/workspace-migration-runner.module';
|
||||||
|
|
||||||
import { ObjectMetadataEntity } from './object-metadata.entity';
|
import { ObjectMetadataEntity } from './object-metadata.entity';
|
||||||
@ -61,6 +62,7 @@ import { UpdateObjectPayload } from './dtos/update-object.input';
|
|||||||
WorkspacePermissionsCacheModule,
|
WorkspacePermissionsCacheModule,
|
||||||
WorkspaceCacheStorageModule,
|
WorkspaceCacheStorageModule,
|
||||||
WorkspaceMetadataCacheModule,
|
WorkspaceMetadataCacheModule,
|
||||||
|
WorkspaceDataSourceModule,
|
||||||
],
|
],
|
||||||
services: [
|
services: [
|
||||||
ObjectMetadataService,
|
ObjectMetadataService,
|
||||||
|
|||||||
@ -6,7 +6,13 @@ import { Query, QueryOptions } from '@ptc-org/nestjs-query-core';
|
|||||||
import { TypeOrmQueryService } from '@ptc-org/nestjs-query-typeorm';
|
import { TypeOrmQueryService } from '@ptc-org/nestjs-query-typeorm';
|
||||||
import { APP_LOCALES } from 'twenty-shared/translations';
|
import { APP_LOCALES } from 'twenty-shared/translations';
|
||||||
import { capitalize, isDefined } from 'twenty-shared/utils';
|
import { capitalize, isDefined } from 'twenty-shared/utils';
|
||||||
import { FindManyOptions, FindOneOptions, In, Repository } from 'typeorm';
|
import {
|
||||||
|
FindManyOptions,
|
||||||
|
FindOneOptions,
|
||||||
|
In,
|
||||||
|
QueryRunner,
|
||||||
|
Repository,
|
||||||
|
} from 'typeorm';
|
||||||
|
|
||||||
import { generateMessageId } from 'src/engine/core-modules/i18n/utils/generateMessageId';
|
import { generateMessageId } from 'src/engine/core-modules/i18n/utils/generateMessageId';
|
||||||
import { DataSourceService } from 'src/engine/metadata-modules/data-source/data-source.service';
|
import { DataSourceService } from 'src/engine/metadata-modules/data-source/data-source.service';
|
||||||
@ -31,7 +37,6 @@ import {
|
|||||||
validateObjectMetadataInputLabelsOrThrow,
|
validateObjectMetadataInputLabelsOrThrow,
|
||||||
validateObjectMetadataInputNamesOrThrow,
|
validateObjectMetadataInputNamesOrThrow,
|
||||||
} from 'src/engine/metadata-modules/object-metadata/utils/validate-object-metadata-input.util';
|
} 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 { 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 { ObjectMetadataItemWithFieldMaps } from 'src/engine/metadata-modules/types/object-metadata-item-with-field-maps';
|
||||||
import { validateMetadataIdentifierFieldMetadataIds } from 'src/engine/metadata-modules/utils/validate-metadata-identifier-field-metadata-id.utils';
|
import { validateMetadataIdentifierFieldMetadataIds } from 'src/engine/metadata-modules/utils/validate-metadata-identifier-field-metadata-id.utils';
|
||||||
@ -41,6 +46,7 @@ import { WorkspaceMetadataCacheService } from 'src/engine/metadata-modules/works
|
|||||||
import { WorkspaceMetadataVersionService } from 'src/engine/metadata-modules/workspace-metadata-version/services/workspace-metadata-version.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 { WorkspacePermissionsCacheService } from 'src/engine/metadata-modules/workspace-permissions-cache/workspace-permissions-cache.service';
|
||||||
import { computeObjectTargetTable } from 'src/engine/utils/compute-object-target-table.util';
|
import { computeObjectTargetTable } from 'src/engine/utils/compute-object-target-table.util';
|
||||||
|
import { WorkspaceDataSourceService } from 'src/engine/workspace-datasource/workspace-datasource.service';
|
||||||
import { WorkspaceMigrationRunnerService } from 'src/engine/workspace-manager/workspace-migration-runner/workspace-migration-runner.service';
|
import { WorkspaceMigrationRunnerService } from 'src/engine/workspace-manager/workspace-migration-runner/workspace-migration-runner.service';
|
||||||
import { CUSTOM_OBJECT_STANDARD_FIELD_IDS } from 'src/engine/workspace-manager/workspace-sync-metadata/constants/standard-field-ids';
|
import { CUSTOM_OBJECT_STANDARD_FIELD_IDS } from 'src/engine/workspace-manager/workspace-sync-metadata/constants/standard-field-ids';
|
||||||
import { isSearchableFieldType } from 'src/engine/workspace-manager/workspace-sync-metadata/utils/is-searchable-field.util';
|
import { isSearchableFieldType } from 'src/engine/workspace-manager/workspace-sync-metadata/utils/is-searchable-field.util';
|
||||||
@ -55,10 +61,6 @@ export class ObjectMetadataService extends TypeOrmQueryService<ObjectMetadataEnt
|
|||||||
@InjectRepository(ObjectMetadataEntity, 'core')
|
@InjectRepository(ObjectMetadataEntity, 'core')
|
||||||
private readonly objectMetadataRepository: Repository<ObjectMetadataEntity>,
|
private readonly objectMetadataRepository: Repository<ObjectMetadataEntity>,
|
||||||
|
|
||||||
@InjectRepository(FieldMetadataEntity, 'core')
|
|
||||||
private readonly fieldMetadataRepository: Repository<FieldMetadataEntity>,
|
|
||||||
|
|
||||||
private readonly remoteTableRelationsService: RemoteTableRelationsService,
|
|
||||||
private readonly dataSourceService: DataSourceService,
|
private readonly dataSourceService: DataSourceService,
|
||||||
private readonly workspaceMetadataCacheService: WorkspaceMetadataCacheService,
|
private readonly workspaceMetadataCacheService: WorkspaceMetadataCacheService,
|
||||||
private readonly workspaceMigrationRunnerService: WorkspaceMigrationRunnerService,
|
private readonly workspaceMigrationRunnerService: WorkspaceMigrationRunnerService,
|
||||||
@ -69,6 +71,7 @@ export class ObjectMetadataService extends TypeOrmQueryService<ObjectMetadataEnt
|
|||||||
private readonly objectMetadataRelatedRecordsService: ObjectMetadataRelatedRecordsService,
|
private readonly objectMetadataRelatedRecordsService: ObjectMetadataRelatedRecordsService,
|
||||||
private readonly indexMetadataService: IndexMetadataService,
|
private readonly indexMetadataService: IndexMetadataService,
|
||||||
private readonly workspacePermissionsCacheService: WorkspacePermissionsCacheService,
|
private readonly workspacePermissionsCacheService: WorkspacePermissionsCacheService,
|
||||||
|
private readonly workspaceDataSourceService: WorkspaceDataSourceService,
|
||||||
) {
|
) {
|
||||||
super(objectMetadataRepository);
|
super(objectMetadataRepository);
|
||||||
}
|
}
|
||||||
@ -92,332 +95,422 @@ export class ObjectMetadataService extends TypeOrmQueryService<ObjectMetadataEnt
|
|||||||
override async createOne(
|
override async createOne(
|
||||||
objectMetadataInput: CreateObjectInput,
|
objectMetadataInput: CreateObjectInput,
|
||||||
): Promise<ObjectMetadataEntity> {
|
): Promise<ObjectMetadataEntity> {
|
||||||
const { objectMetadataMaps } =
|
const mainDataSource =
|
||||||
await this.workspaceMetadataCacheService.getExistingOrRecomputeMetadataMaps(
|
await this.workspaceDataSourceService.connectToMainDataSource();
|
||||||
|
const queryRunner = mainDataSource.createQueryRunner();
|
||||||
|
|
||||||
|
await queryRunner.connect();
|
||||||
|
await queryRunner.startTransaction();
|
||||||
|
|
||||||
|
try {
|
||||||
|
const objectMetadataRepository =
|
||||||
|
queryRunner.manager.getRepository(ObjectMetadataEntity);
|
||||||
|
|
||||||
|
const { objectMetadataMaps } =
|
||||||
|
await this.workspaceMetadataCacheService.getExistingOrRecomputeMetadataMaps(
|
||||||
|
{
|
||||||
|
workspaceId: objectMetadataInput.workspaceId,
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
const lastDataSourceMetadata =
|
||||||
|
await this.dataSourceService.getLastDataSourceMetadataFromWorkspaceIdOrFail(
|
||||||
|
objectMetadataInput.workspaceId,
|
||||||
|
);
|
||||||
|
|
||||||
|
objectMetadataInput.labelSingular = capitalize(
|
||||||
|
objectMetadataInput.labelSingular,
|
||||||
|
);
|
||||||
|
objectMetadataInput.labelPlural = capitalize(
|
||||||
|
objectMetadataInput.labelPlural,
|
||||||
|
);
|
||||||
|
|
||||||
|
validateObjectMetadataInputNamesOrThrow(objectMetadataInput);
|
||||||
|
validateObjectMetadataInputLabelsOrThrow(objectMetadataInput);
|
||||||
|
|
||||||
|
validateLowerCasedAndTrimmedStringsAreDifferentOrThrow({
|
||||||
|
inputs: [
|
||||||
|
objectMetadataInput.nameSingular,
|
||||||
|
objectMetadataInput.namePlural,
|
||||||
|
],
|
||||||
|
message:
|
||||||
|
'The singular and plural names cannot be the same for an object',
|
||||||
|
});
|
||||||
|
validateLowerCasedAndTrimmedStringsAreDifferentOrThrow({
|
||||||
|
inputs: [
|
||||||
|
objectMetadataInput.labelPlural,
|
||||||
|
objectMetadataInput.labelSingular,
|
||||||
|
],
|
||||||
|
message:
|
||||||
|
'The singular and plural labels cannot be the same for an object',
|
||||||
|
});
|
||||||
|
|
||||||
|
if (objectMetadataInput.isLabelSyncedWithName === true) {
|
||||||
|
validateNameAndLabelAreSyncOrThrow(
|
||||||
|
objectMetadataInput.labelSingular,
|
||||||
|
objectMetadataInput.nameSingular,
|
||||||
|
);
|
||||||
|
validateNameAndLabelAreSyncOrThrow(
|
||||||
|
objectMetadataInput.labelPlural,
|
||||||
|
objectMetadataInput.namePlural,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
validatesNoOtherObjectWithSameNameExistsOrThrows({
|
||||||
|
objectMetadataNamePlural: objectMetadataInput.namePlural,
|
||||||
|
objectMetadataNameSingular: objectMetadataInput.nameSingular,
|
||||||
|
objectMetadataMaps,
|
||||||
|
});
|
||||||
|
|
||||||
|
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 objectMetadataRepository.save({
|
||||||
|
...objectMetadataInput,
|
||||||
|
dataSourceId: lastDataSourceMetadata.id,
|
||||||
|
targetTableName: 'DEPRECATED',
|
||||||
|
isActive: true,
|
||||||
|
isCustom: !objectMetadataInput.isRemote,
|
||||||
|
isSystem: false,
|
||||||
|
isRemote: objectMetadataInput.isRemote,
|
||||||
|
isSearchable: !objectMetadataInput.isRemote,
|
||||||
|
fields: objectMetadataInput.isRemote ? [] : baseCustomFields,
|
||||||
|
labelIdentifierFieldMetadataId,
|
||||||
|
});
|
||||||
|
|
||||||
|
if (objectMetadataInput.isRemote) {
|
||||||
|
throw new Error('Remote objects are not supported yet');
|
||||||
|
} else {
|
||||||
|
const createdRelatedObjectMetadataCollection =
|
||||||
|
await this.objectMetadataFieldRelationService.createRelationsAndForeignKeysMetadata(
|
||||||
|
objectMetadataInput.workspaceId,
|
||||||
|
createdObjectMetadata,
|
||||||
|
objectMetadataMaps,
|
||||||
|
queryRunner,
|
||||||
|
);
|
||||||
|
|
||||||
|
await this.objectMetadataMigrationService.createTableMigration(
|
||||||
|
createdObjectMetadata,
|
||||||
|
queryRunner,
|
||||||
|
);
|
||||||
|
|
||||||
|
await this.objectMetadataMigrationService.createColumnsMigrations(
|
||||||
|
createdObjectMetadata,
|
||||||
|
createdObjectMetadata.fields,
|
||||||
|
queryRunner,
|
||||||
|
);
|
||||||
|
|
||||||
|
await this.objectMetadataMigrationService.createRelationMigrations(
|
||||||
|
createdObjectMetadata,
|
||||||
|
createdRelatedObjectMetadataCollection,
|
||||||
|
queryRunner,
|
||||||
|
);
|
||||||
|
|
||||||
|
await this.searchVectorService.createSearchVectorFieldForObject(
|
||||||
|
objectMetadataInput,
|
||||||
|
createdObjectMetadata,
|
||||||
|
queryRunner,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
await this.workspaceMigrationRunnerService.executeMigrationFromPendingMigrationsWithinTransaction(
|
||||||
|
createdObjectMetadata.workspaceId,
|
||||||
|
queryRunner,
|
||||||
|
);
|
||||||
|
|
||||||
|
await queryRunner.commitTransaction();
|
||||||
|
|
||||||
|
// After commit, do non-transactional work
|
||||||
|
await this.workspacePermissionsCacheService.recomputeRolesPermissionsCache(
|
||||||
{
|
{
|
||||||
workspaceId: objectMetadataInput.workspaceId,
|
workspaceId: objectMetadataInput.workspaceId,
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
|
await this.objectMetadataRelatedRecordsService.createObjectRelatedRecords(
|
||||||
|
createdObjectMetadata,
|
||||||
|
);
|
||||||
|
|
||||||
const lastDataSourceMetadata =
|
await this.workspaceMetadataVersionService.incrementMetadataVersion(
|
||||||
await this.dataSourceService.getLastDataSourceMetadataFromWorkspaceIdOrFail(
|
|
||||||
objectMetadataInput.workspaceId,
|
objectMetadataInput.workspaceId,
|
||||||
);
|
);
|
||||||
|
|
||||||
objectMetadataInput.labelSingular = capitalize(
|
return createdObjectMetadata;
|
||||||
objectMetadataInput.labelSingular,
|
} catch (error) {
|
||||||
);
|
if (queryRunner.isTransactionActive) {
|
||||||
objectMetadataInput.labelPlural = capitalize(
|
await queryRunner.rollbackTransaction();
|
||||||
objectMetadataInput.labelPlural,
|
}
|
||||||
);
|
throw error;
|
||||||
|
} finally {
|
||||||
validateObjectMetadataInputNamesOrThrow(objectMetadataInput);
|
await queryRunner.release();
|
||||||
validateObjectMetadataInputLabelsOrThrow(objectMetadataInput);
|
|
||||||
|
|
||||||
validateLowerCasedAndTrimmedStringsAreDifferentOrThrow({
|
|
||||||
inputs: [
|
|
||||||
objectMetadataInput.nameSingular,
|
|
||||||
objectMetadataInput.namePlural,
|
|
||||||
],
|
|
||||||
message: 'The singular and plural names cannot be the same for an object',
|
|
||||||
});
|
|
||||||
validateLowerCasedAndTrimmedStringsAreDifferentOrThrow({
|
|
||||||
inputs: [
|
|
||||||
objectMetadataInput.labelPlural,
|
|
||||||
objectMetadataInput.labelSingular,
|
|
||||||
],
|
|
||||||
message:
|
|
||||||
'The singular and plural labels cannot be the same for an object',
|
|
||||||
});
|
|
||||||
|
|
||||||
if (objectMetadataInput.isLabelSyncedWithName === true) {
|
|
||||||
validateNameAndLabelAreSyncOrThrow(
|
|
||||||
objectMetadataInput.labelSingular,
|
|
||||||
objectMetadataInput.nameSingular,
|
|
||||||
);
|
|
||||||
validateNameAndLabelAreSyncOrThrow(
|
|
||||||
objectMetadataInput.labelPlural,
|
|
||||||
objectMetadataInput.namePlural,
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
validatesNoOtherObjectWithSameNameExistsOrThrows({
|
|
||||||
objectMetadataNamePlural: objectMetadataInput.namePlural,
|
|
||||||
objectMetadataNameSingular: objectMetadataInput.nameSingular,
|
|
||||||
objectMetadataMaps,
|
|
||||||
});
|
|
||||||
|
|
||||||
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',
|
|
||||||
isActive: true,
|
|
||||||
isCustom: !objectMetadataInput.isRemote,
|
|
||||||
isSystem: false,
|
|
||||||
isRemote: objectMetadataInput.isRemote,
|
|
||||||
isSearchable: !objectMetadataInput.isRemote,
|
|
||||||
fields: objectMetadataInput.isRemote ? [] : baseCustomFields,
|
|
||||||
labelIdentifierFieldMetadataId,
|
|
||||||
});
|
|
||||||
|
|
||||||
if (objectMetadataInput.isRemote) {
|
|
||||||
await this.remoteTableRelationsService.createForeignKeysMetadataAndMigrations(
|
|
||||||
objectMetadataInput.workspaceId,
|
|
||||||
createdObjectMetadata,
|
|
||||||
objectMetadataInput.primaryKeyFieldMetadataSettings,
|
|
||||||
objectMetadataInput.primaryKeyColumnType,
|
|
||||||
);
|
|
||||||
} else {
|
|
||||||
const createdRelatedObjectMetadataCollection =
|
|
||||||
await this.objectMetadataFieldRelationService.createRelationsAndForeignKeysMetadata(
|
|
||||||
objectMetadataInput.workspaceId,
|
|
||||||
createdObjectMetadata,
|
|
||||||
objectMetadataMaps,
|
|
||||||
);
|
|
||||||
|
|
||||||
await this.objectMetadataMigrationService.createTableMigration(
|
|
||||||
createdObjectMetadata,
|
|
||||||
);
|
|
||||||
|
|
||||||
await this.objectMetadataMigrationService.createColumnsMigrations(
|
|
||||||
createdObjectMetadata,
|
|
||||||
createdObjectMetadata.fields,
|
|
||||||
);
|
|
||||||
|
|
||||||
await this.objectMetadataMigrationService.createRelationMigrations(
|
|
||||||
createdObjectMetadata,
|
|
||||||
createdRelatedObjectMetadataCollection,
|
|
||||||
);
|
|
||||||
|
|
||||||
await this.searchVectorService.createSearchVectorFieldForObject(
|
|
||||||
objectMetadataInput,
|
|
||||||
createdObjectMetadata,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
await this.workspaceMigrationRunnerService.executeMigrationFromPendingMigrations(
|
|
||||||
createdObjectMetadata.workspaceId,
|
|
||||||
);
|
|
||||||
|
|
||||||
await this.objectMetadataRelatedRecordsService.createObjectRelatedRecords(
|
|
||||||
createdObjectMetadata,
|
|
||||||
);
|
|
||||||
|
|
||||||
await this.workspaceMetadataVersionService.incrementMetadataVersion(
|
|
||||||
objectMetadataInput.workspaceId,
|
|
||||||
);
|
|
||||||
|
|
||||||
await this.workspacePermissionsCacheService.recomputeRolesPermissionsCache({
|
|
||||||
workspaceId: objectMetadataInput.workspaceId,
|
|
||||||
});
|
|
||||||
|
|
||||||
return createdObjectMetadata;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public async updateOneObject(
|
public async updateOneObject(
|
||||||
input: UpdateOneObjectInput,
|
input: UpdateOneObjectInput,
|
||||||
workspaceId: string,
|
workspaceId: string,
|
||||||
): Promise<ObjectMetadataEntity> {
|
): Promise<ObjectMetadataEntity> {
|
||||||
const { objectMetadataMaps } =
|
const mainDataSource =
|
||||||
await this.workspaceMetadataCacheService.getExistingOrRecomputeMetadataMaps(
|
await this.workspaceDataSourceService.connectToMainDataSource();
|
||||||
{ workspaceId },
|
const queryRunner = mainDataSource.createQueryRunner();
|
||||||
);
|
|
||||||
|
|
||||||
const inputId = input.id;
|
await queryRunner.connect();
|
||||||
|
await queryRunner.startTransaction();
|
||||||
|
|
||||||
const inputPayload = {
|
try {
|
||||||
...input.update,
|
const objectMetadataRepository =
|
||||||
...(isDefined(input.update.labelSingular)
|
queryRunner.manager.getRepository(ObjectMetadataEntity);
|
||||||
? { labelSingular: capitalize(input.update.labelSingular) }
|
|
||||||
: {}),
|
|
||||||
...(isDefined(input.update.labelPlural)
|
|
||||||
? { labelPlural: capitalize(input.update.labelPlural) }
|
|
||||||
: {}),
|
|
||||||
};
|
|
||||||
|
|
||||||
validateObjectMetadataInputNamesOrThrow(inputPayload);
|
const { objectMetadataMaps } =
|
||||||
|
await this.workspaceMetadataCacheService.getExistingOrRecomputeMetadataMaps(
|
||||||
|
{ workspaceId },
|
||||||
|
);
|
||||||
|
const inputId = input.id;
|
||||||
|
const inputPayload = {
|
||||||
|
...input.update,
|
||||||
|
...(isDefined(input.update.labelSingular)
|
||||||
|
? { labelSingular: capitalize(input.update.labelSingular) }
|
||||||
|
: {}),
|
||||||
|
...(isDefined(input.update.labelPlural)
|
||||||
|
? { labelPlural: capitalize(input.update.labelPlural) }
|
||||||
|
: {}),
|
||||||
|
};
|
||||||
|
|
||||||
const existingObjectMetadata = objectMetadataMaps.byId[inputId];
|
validateObjectMetadataInputNamesOrThrow(inputPayload);
|
||||||
|
const existingObjectMetadata = objectMetadataMaps.byId[inputId];
|
||||||
|
|
||||||
if (!existingObjectMetadata) {
|
if (!existingObjectMetadata) {
|
||||||
throw new ObjectMetadataException(
|
throw new ObjectMetadataException(
|
||||||
'Object does not exist',
|
'Object does not exist',
|
||||||
ObjectMetadataExceptionCode.OBJECT_METADATA_NOT_FOUND,
|
ObjectMetadataExceptionCode.OBJECT_METADATA_NOT_FOUND,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
const existingObjectMetadataCombinedWithUpdateInput = {
|
||||||
|
...existingObjectMetadata,
|
||||||
|
...inputPayload,
|
||||||
|
};
|
||||||
|
|
||||||
const existingObjectMetadataCombinedWithUpdateInput = {
|
validatesNoOtherObjectWithSameNameExistsOrThrows({
|
||||||
...existingObjectMetadata,
|
objectMetadataNameSingular:
|
||||||
...inputPayload,
|
|
||||||
};
|
|
||||||
|
|
||||||
validatesNoOtherObjectWithSameNameExistsOrThrows({
|
|
||||||
objectMetadataNameSingular:
|
|
||||||
existingObjectMetadataCombinedWithUpdateInput.nameSingular,
|
|
||||||
objectMetadataNamePlural:
|
|
||||||
existingObjectMetadataCombinedWithUpdateInput.namePlural,
|
|
||||||
existingObjectMetadataId:
|
|
||||||
existingObjectMetadataCombinedWithUpdateInput.id,
|
|
||||||
objectMetadataMaps,
|
|
||||||
});
|
|
||||||
|
|
||||||
if (existingObjectMetadataCombinedWithUpdateInput.isLabelSyncedWithName) {
|
|
||||||
validateNameAndLabelAreSyncOrThrow(
|
|
||||||
existingObjectMetadataCombinedWithUpdateInput.labelSingular,
|
|
||||||
existingObjectMetadataCombinedWithUpdateInput.nameSingular,
|
|
||||||
);
|
|
||||||
validateNameAndLabelAreSyncOrThrow(
|
|
||||||
existingObjectMetadataCombinedWithUpdateInput.labelPlural,
|
|
||||||
existingObjectMetadataCombinedWithUpdateInput.namePlural,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (
|
|
||||||
isDefined(inputPayload.nameSingular) ||
|
|
||||||
isDefined(inputPayload.namePlural)
|
|
||||||
) {
|
|
||||||
validateLowerCasedAndTrimmedStringsAreDifferentOrThrow({
|
|
||||||
inputs: [
|
|
||||||
existingObjectMetadataCombinedWithUpdateInput.nameSingular,
|
existingObjectMetadataCombinedWithUpdateInput.nameSingular,
|
||||||
|
objectMetadataNamePlural:
|
||||||
existingObjectMetadataCombinedWithUpdateInput.namePlural,
|
existingObjectMetadataCombinedWithUpdateInput.namePlural,
|
||||||
],
|
existingObjectMetadataId:
|
||||||
message:
|
existingObjectMetadataCombinedWithUpdateInput.id,
|
||||||
'The singular and plural names cannot be the same for an object',
|
objectMetadataMaps,
|
||||||
});
|
});
|
||||||
}
|
if (existingObjectMetadataCombinedWithUpdateInput.isLabelSyncedWithName) {
|
||||||
|
validateNameAndLabelAreSyncOrThrow(
|
||||||
validateMetadataIdentifierFieldMetadataIds({
|
existingObjectMetadataCombinedWithUpdateInput.labelSingular,
|
||||||
fieldMetadataItems: Object.values(existingObjectMetadata.fieldsById),
|
existingObjectMetadataCombinedWithUpdateInput.nameSingular,
|
||||||
labelIdentifierFieldMetadataId:
|
);
|
||||||
inputPayload.labelIdentifierFieldMetadataId,
|
validateNameAndLabelAreSyncOrThrow(
|
||||||
imageIdentifierFieldMetadataId:
|
existingObjectMetadataCombinedWithUpdateInput.labelPlural,
|
||||||
inputPayload.imageIdentifierFieldMetadataId,
|
existingObjectMetadataCombinedWithUpdateInput.namePlural,
|
||||||
});
|
);
|
||||||
|
}
|
||||||
const updatedObject = await super.updateOne(inputId, inputPayload);
|
if (
|
||||||
|
isDefined(inputPayload.nameSingular) ||
|
||||||
await this.handleObjectNameAndLabelUpdates(
|
isDefined(inputPayload.namePlural)
|
||||||
existingObjectMetadata,
|
) {
|
||||||
existingObjectMetadataCombinedWithUpdateInput,
|
validateLowerCasedAndTrimmedStringsAreDifferentOrThrow({
|
||||||
inputPayload,
|
inputs: [
|
||||||
);
|
existingObjectMetadataCombinedWithUpdateInput.nameSingular,
|
||||||
|
existingObjectMetadataCombinedWithUpdateInput.namePlural,
|
||||||
await this.workspaceMigrationRunnerService.executeMigrationFromPendingMigrations(
|
|
||||||
workspaceId,
|
|
||||||
);
|
|
||||||
if (inputPayload.labelIdentifierFieldMetadataId) {
|
|
||||||
const labelIdentifierFieldMetadata =
|
|
||||||
await this.fieldMetadataRepository.findOneByOrFail({
|
|
||||||
id: inputPayload.labelIdentifierFieldMetadataId,
|
|
||||||
objectMetadataId: inputId,
|
|
||||||
workspaceId: workspaceId,
|
|
||||||
});
|
|
||||||
|
|
||||||
if (isSearchableFieldType(labelIdentifierFieldMetadata.type)) {
|
|
||||||
await this.searchVectorService.updateSearchVector(
|
|
||||||
inputId,
|
|
||||||
[
|
|
||||||
{
|
|
||||||
name: labelIdentifierFieldMetadata.name,
|
|
||||||
type: labelIdentifierFieldMetadata.type,
|
|
||||||
},
|
|
||||||
],
|
],
|
||||||
|
message:
|
||||||
|
'The singular and plural names cannot be the same for an object',
|
||||||
|
});
|
||||||
|
}
|
||||||
|
validateMetadataIdentifierFieldMetadataIds({
|
||||||
|
fieldMetadataItems: Object.values(existingObjectMetadata.fieldsById),
|
||||||
|
labelIdentifierFieldMetadataId:
|
||||||
|
inputPayload.labelIdentifierFieldMetadataId,
|
||||||
|
imageIdentifierFieldMetadataId:
|
||||||
|
inputPayload.imageIdentifierFieldMetadataId,
|
||||||
|
});
|
||||||
|
const updatedObject = await objectMetadataRepository.save({
|
||||||
|
...existingObjectMetadata,
|
||||||
|
...inputPayload,
|
||||||
|
});
|
||||||
|
|
||||||
|
const { didUpdateLabelOrIcon } =
|
||||||
|
await this.handleObjectNameAndLabelUpdates(
|
||||||
|
existingObjectMetadata,
|
||||||
|
existingObjectMetadataCombinedWithUpdateInput,
|
||||||
|
inputPayload,
|
||||||
|
queryRunner,
|
||||||
|
);
|
||||||
|
|
||||||
|
await this.workspaceMigrationRunnerService.executeMigrationFromPendingMigrationsWithinTransaction(
|
||||||
|
workspaceId,
|
||||||
|
queryRunner,
|
||||||
|
);
|
||||||
|
if (inputPayload.labelIdentifierFieldMetadataId) {
|
||||||
|
const labelIdentifierFieldMetadata =
|
||||||
|
existingObjectMetadata.fieldsById[
|
||||||
|
inputPayload.labelIdentifierFieldMetadataId
|
||||||
|
];
|
||||||
|
|
||||||
|
if (isSearchableFieldType(labelIdentifierFieldMetadata.type)) {
|
||||||
|
await this.searchVectorService.updateSearchVector(
|
||||||
|
inputId,
|
||||||
|
[
|
||||||
|
{
|
||||||
|
name: labelIdentifierFieldMetadata.name,
|
||||||
|
type: labelIdentifierFieldMetadata.type,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
workspaceId,
|
||||||
|
queryRunner,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
await this.workspaceMigrationRunnerService.executeMigrationFromPendingMigrationsWithinTransaction(
|
||||||
|
workspaceId,
|
||||||
|
queryRunner,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
await queryRunner.commitTransaction();
|
||||||
|
|
||||||
|
// After commit, do non-transactional work
|
||||||
|
await this.workspacePermissionsCacheService.recomputeRolesPermissionsCache(
|
||||||
|
{
|
||||||
|
workspaceId,
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
if (didUpdateLabelOrIcon) {
|
||||||
|
await this.objectMetadataRelatedRecordsService.updateObjectViews(
|
||||||
|
updatedObject,
|
||||||
workspaceId,
|
workspaceId,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
await this.workspaceMigrationRunnerService.executeMigrationFromPendingMigrations(
|
await this.workspaceMetadataVersionService.incrementMetadataVersion(
|
||||||
workspaceId,
|
workspaceId,
|
||||||
);
|
);
|
||||||
|
|
||||||
|
const formattedUpdatedObject = {
|
||||||
|
...updatedObject,
|
||||||
|
createdAt: new Date(updatedObject.createdAt),
|
||||||
|
};
|
||||||
|
|
||||||
|
return formattedUpdatedObject;
|
||||||
|
} catch (error) {
|
||||||
|
if (queryRunner.isTransactionActive) {
|
||||||
|
await queryRunner.rollbackTransaction();
|
||||||
|
}
|
||||||
|
throw error;
|
||||||
|
} finally {
|
||||||
|
await queryRunner.release();
|
||||||
}
|
}
|
||||||
|
|
||||||
await this.workspaceMetadataVersionService.incrementMetadataVersion(
|
|
||||||
workspaceId,
|
|
||||||
);
|
|
||||||
|
|
||||||
return updatedObject;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public async deleteOneObject(
|
public async deleteOneObject(
|
||||||
input: DeleteOneObjectInput,
|
input: DeleteOneObjectInput,
|
||||||
workspaceId: string,
|
workspaceId: string,
|
||||||
): Promise<ObjectMetadataEntity> {
|
): Promise<Partial<ObjectMetadataEntity>> {
|
||||||
const objectMetadata = await this.objectMetadataRepository.findOne({
|
const mainDataSource =
|
||||||
relations: [
|
await this.workspaceDataSourceService.connectToMainDataSource();
|
||||||
'fields',
|
const queryRunner = mainDataSource.createQueryRunner();
|
||||||
'fields.object',
|
|
||||||
'fields.relationTargetFieldMetadata',
|
await queryRunner.connect();
|
||||||
'fields.relationTargetFieldMetadata.object',
|
await queryRunner.startTransaction();
|
||||||
],
|
|
||||||
where: {
|
try {
|
||||||
id: input.id,
|
const objectMetadataRepository =
|
||||||
|
queryRunner.manager.getRepository(ObjectMetadataEntity);
|
||||||
|
const fieldMetadataRepository =
|
||||||
|
queryRunner.manager.getRepository(FieldMetadataEntity);
|
||||||
|
|
||||||
|
const objectMetadata = await objectMetadataRepository.findOne({
|
||||||
|
relations: [
|
||||||
|
'fields',
|
||||||
|
'fields.object',
|
||||||
|
'fields.relationTargetFieldMetadata',
|
||||||
|
'fields.relationTargetFieldMetadata.object',
|
||||||
|
],
|
||||||
|
where: {
|
||||||
|
id: input.id,
|
||||||
|
workspaceId,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!objectMetadata) {
|
||||||
|
throw new ObjectMetadataException(
|
||||||
|
'Object does not exist',
|
||||||
|
ObjectMetadataExceptionCode.OBJECT_METADATA_NOT_FOUND,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (objectMetadata.isRemote) {
|
||||||
|
throw new ObjectMetadataException(
|
||||||
|
'Remote objects are not supported yet',
|
||||||
|
ObjectMetadataExceptionCode.INVALID_OBJECT_INPUT,
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
await this.objectMetadataMigrationService.deleteAllRelationsAndDropTable(
|
||||||
|
objectMetadata,
|
||||||
|
workspaceId,
|
||||||
|
queryRunner,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
await this.workspaceMigrationRunnerService.executeMigrationFromPendingMigrationsWithinTransaction(
|
||||||
workspaceId,
|
workspaceId,
|
||||||
},
|
queryRunner,
|
||||||
});
|
|
||||||
|
|
||||||
if (!objectMetadata) {
|
|
||||||
throw new ObjectMetadataException(
|
|
||||||
'Object does not exist',
|
|
||||||
ObjectMetadataExceptionCode.OBJECT_METADATA_NOT_FOUND,
|
|
||||||
);
|
);
|
||||||
}
|
|
||||||
|
|
||||||
if (objectMetadata.isRemote) {
|
const fieldMetadataIds = objectMetadata.fields.map((field) => field.id);
|
||||||
await this.remoteTableRelationsService.deleteForeignKeysMetadataAndCreateMigrations(
|
const relationMetadataIds = objectMetadata.fields
|
||||||
objectMetadata.workspaceId,
|
.map((field) => field.relationTargetFieldMetadata?.id)
|
||||||
objectMetadata,
|
.filter(isDefined);
|
||||||
|
|
||||||
|
await fieldMetadataRepository.delete({
|
||||||
|
id: In(fieldMetadataIds.concat(relationMetadataIds)),
|
||||||
|
});
|
||||||
|
|
||||||
|
await objectMetadataRepository.delete(objectMetadata.id);
|
||||||
|
|
||||||
|
await queryRunner.commitTransaction();
|
||||||
|
|
||||||
|
// After commit, do non-transactional work
|
||||||
|
await this.workspaceMetadataVersionService.incrementMetadataVersion(
|
||||||
|
workspaceId,
|
||||||
);
|
);
|
||||||
} else {
|
|
||||||
await this.objectMetadataMigrationService.deleteAllRelationsAndDropTable(
|
await this.workspacePermissionsCacheService.recomputeRolesPermissionsCache(
|
||||||
|
{
|
||||||
|
workspaceId,
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
await this.objectMetadataRelatedRecordsService.deleteObjectViews(
|
||||||
objectMetadata,
|
objectMetadata,
|
||||||
workspaceId,
|
workspaceId,
|
||||||
);
|
);
|
||||||
|
|
||||||
|
return objectMetadata;
|
||||||
|
} catch (error) {
|
||||||
|
if (queryRunner.isTransactionActive) {
|
||||||
|
await queryRunner.rollbackTransaction();
|
||||||
|
}
|
||||||
|
throw error;
|
||||||
|
} finally {
|
||||||
|
await queryRunner.release();
|
||||||
}
|
}
|
||||||
|
|
||||||
await this.objectMetadataRelatedRecordsService.deleteObjectViews(
|
|
||||||
objectMetadata,
|
|
||||||
workspaceId,
|
|
||||||
);
|
|
||||||
|
|
||||||
await this.workspaceMigrationRunnerService.executeMigrationFromPendingMigrations(
|
|
||||||
workspaceId,
|
|
||||||
);
|
|
||||||
|
|
||||||
const fieldMetadataIds = objectMetadata.fields.map((field) => field.id);
|
|
||||||
const relationMetadataIds = objectMetadata.fields
|
|
||||||
.map((field) => field.relationTargetFieldMetadata?.id)
|
|
||||||
.filter(isDefined);
|
|
||||||
|
|
||||||
await this.fieldMetadataRepository.delete({
|
|
||||||
id: In(fieldMetadataIds.concat(relationMetadataIds)),
|
|
||||||
});
|
|
||||||
|
|
||||||
await this.objectMetadataRepository.delete(objectMetadata.id);
|
|
||||||
|
|
||||||
await this.workspaceMetadataVersionService.incrementMetadataVersion(
|
|
||||||
workspaceId,
|
|
||||||
);
|
|
||||||
|
|
||||||
await this.workspacePermissionsCacheService.recomputeRolesPermissionsCache({
|
|
||||||
workspaceId,
|
|
||||||
});
|
|
||||||
|
|
||||||
return objectMetadata;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public async findOneWithinWorkspace(
|
public async findOneWithinWorkspace(
|
||||||
@ -476,7 +569,8 @@ export class ObjectMetadataService extends TypeOrmQueryService<ObjectMetadataEnt
|
|||||||
| 'fieldsById'
|
| 'fieldsById'
|
||||||
>,
|
>,
|
||||||
inputPayload: UpdateObjectPayload,
|
inputPayload: UpdateObjectPayload,
|
||||||
) {
|
queryRunner: QueryRunner,
|
||||||
|
): Promise<{ didUpdateLabelOrIcon: boolean }> {
|
||||||
const newTargetTableName = computeObjectTargetTable(
|
const newTargetTableName = computeObjectTargetTable(
|
||||||
objectMetadataForUpdate,
|
objectMetadataForUpdate,
|
||||||
);
|
);
|
||||||
@ -489,12 +583,14 @@ export class ObjectMetadataService extends TypeOrmQueryService<ObjectMetadataEnt
|
|||||||
existingObjectMetadata,
|
existingObjectMetadata,
|
||||||
objectMetadataForUpdate,
|
objectMetadataForUpdate,
|
||||||
objectMetadataForUpdate.workspaceId,
|
objectMetadataForUpdate.workspaceId,
|
||||||
|
queryRunner,
|
||||||
);
|
);
|
||||||
|
|
||||||
const relationMetadataCollection =
|
const relationMetadataCollection =
|
||||||
await this.objectMetadataFieldRelationService.updateRelationsAndForeignKeysMetadata(
|
await this.objectMetadataFieldRelationService.updateRelationsAndForeignKeysMetadata(
|
||||||
objectMetadataForUpdate.workspaceId,
|
objectMetadataForUpdate.workspaceId,
|
||||||
objectMetadataForUpdate,
|
objectMetadataForUpdate,
|
||||||
|
queryRunner,
|
||||||
);
|
);
|
||||||
|
|
||||||
await this.objectMetadataMigrationService.updateRelationMigrations(
|
await this.objectMetadataMigrationService.updateRelationMigrations(
|
||||||
@ -502,23 +598,27 @@ export class ObjectMetadataService extends TypeOrmQueryService<ObjectMetadataEnt
|
|||||||
objectMetadataForUpdate,
|
objectMetadataForUpdate,
|
||||||
relationMetadataCollection,
|
relationMetadataCollection,
|
||||||
objectMetadataForUpdate.workspaceId,
|
objectMetadataForUpdate.workspaceId,
|
||||||
|
queryRunner,
|
||||||
);
|
);
|
||||||
|
|
||||||
await this.objectMetadataMigrationService.recomputeEnumNames(
|
await this.objectMetadataMigrationService.recomputeEnumNames(
|
||||||
objectMetadataForUpdate,
|
objectMetadataForUpdate,
|
||||||
objectMetadataForUpdate.workspaceId,
|
objectMetadataForUpdate.workspaceId,
|
||||||
|
queryRunner,
|
||||||
);
|
);
|
||||||
|
|
||||||
const recomputedIndexes =
|
const recomputedIndexes =
|
||||||
await this.indexMetadataService.recomputeIndexMetadataForObject(
|
await this.indexMetadataService.recomputeIndexMetadataForObject(
|
||||||
objectMetadataForUpdate.workspaceId,
|
objectMetadataForUpdate.workspaceId,
|
||||||
objectMetadataForUpdate,
|
objectMetadataForUpdate,
|
||||||
|
queryRunner,
|
||||||
);
|
);
|
||||||
|
|
||||||
await this.indexMetadataService.createIndexRecomputeMigrations(
|
await this.indexMetadataService.createIndexRecomputeMigrations(
|
||||||
objectMetadataForUpdate.workspaceId,
|
objectMetadataForUpdate.workspaceId,
|
||||||
objectMetadataForUpdate,
|
objectMetadataForUpdate,
|
||||||
recomputedIndexes,
|
recomputedIndexes,
|
||||||
|
queryRunner,
|
||||||
);
|
);
|
||||||
|
|
||||||
if (
|
if (
|
||||||
@ -526,12 +626,15 @@ export class ObjectMetadataService extends TypeOrmQueryService<ObjectMetadataEnt
|
|||||||
(inputPayload.labelPlural !== existingObjectMetadata.labelPlural ||
|
(inputPayload.labelPlural !== existingObjectMetadata.labelPlural ||
|
||||||
inputPayload.icon !== existingObjectMetadata.icon)
|
inputPayload.icon !== existingObjectMetadata.icon)
|
||||||
) {
|
) {
|
||||||
await this.objectMetadataRelatedRecordsService.updateObjectViews(
|
return {
|
||||||
objectMetadataForUpdate,
|
didUpdateLabelOrIcon: true,
|
||||||
objectMetadataForUpdate.workspaceId,
|
};
|
||||||
);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
didUpdateLabelOrIcon: false,
|
||||||
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
async resolveOverridableString(
|
async resolveOverridableString(
|
||||||
|
|||||||
@ -3,7 +3,7 @@ import { InjectRepository } from '@nestjs/typeorm';
|
|||||||
|
|
||||||
import { FieldMetadataType } from 'twenty-shared/types';
|
import { FieldMetadataType } from 'twenty-shared/types';
|
||||||
import { capitalize } from 'twenty-shared/utils';
|
import { capitalize } from 'twenty-shared/utils';
|
||||||
import { Repository } from 'typeorm';
|
import { QueryRunner, Repository } from 'typeorm';
|
||||||
import { v4 as uuidV4 } from 'uuid';
|
import { v4 as uuidV4 } from 'uuid';
|
||||||
|
|
||||||
import { FieldMetadataDefaultSettings } from 'src/engine/metadata-modules/field-metadata/interfaces/field-metadata-settings.interface';
|
import { FieldMetadataDefaultSettings } from 'src/engine/metadata-modules/field-metadata/interfaces/field-metadata-settings.interface';
|
||||||
@ -48,6 +48,7 @@ export class ObjectMetadataFieldRelationService {
|
|||||||
'id' | 'nameSingular' | 'labelSingular'
|
'id' | 'nameSingular' | 'labelSingular'
|
||||||
>,
|
>,
|
||||||
objectMetadataMaps: ObjectMetadataMaps,
|
objectMetadataMaps: ObjectMetadataMaps,
|
||||||
|
queryRunner?: QueryRunner,
|
||||||
) {
|
) {
|
||||||
const relatedObjectMetadataCollection = await Promise.all(
|
const relatedObjectMetadataCollection = await Promise.all(
|
||||||
DEFAULT_RELATIONS_OBJECTS_STANDARD_IDS.map(
|
DEFAULT_RELATIONS_OBJECTS_STANDARD_IDS.map(
|
||||||
@ -57,6 +58,7 @@ export class ObjectMetadataFieldRelationService {
|
|||||||
sourceObjectMetadata,
|
sourceObjectMetadata,
|
||||||
relationObjectMetadataStandardId,
|
relationObjectMetadataStandardId,
|
||||||
objectMetadataMaps,
|
objectMetadataMaps,
|
||||||
|
queryRunner,
|
||||||
}),
|
}),
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
@ -69,6 +71,7 @@ export class ObjectMetadataFieldRelationService {
|
|||||||
sourceObjectMetadata,
|
sourceObjectMetadata,
|
||||||
relationObjectMetadataStandardId,
|
relationObjectMetadataStandardId,
|
||||||
objectMetadataMaps,
|
objectMetadataMaps,
|
||||||
|
queryRunner,
|
||||||
}: {
|
}: {
|
||||||
workspaceId: string;
|
workspaceId: string;
|
||||||
sourceObjectMetadata: Pick<
|
sourceObjectMetadata: Pick<
|
||||||
@ -77,6 +80,7 @@ export class ObjectMetadataFieldRelationService {
|
|||||||
>;
|
>;
|
||||||
objectMetadataMaps: ObjectMetadataMaps;
|
objectMetadataMaps: ObjectMetadataMaps;
|
||||||
relationObjectMetadataStandardId: string;
|
relationObjectMetadataStandardId: string;
|
||||||
|
queryRunner?: QueryRunner;
|
||||||
}) {
|
}) {
|
||||||
const targetObjectMetadata = Object.values(objectMetadataMaps.byId).find(
|
const targetObjectMetadata = Object.values(objectMetadataMaps.byId).find(
|
||||||
(objectMetadata) =>
|
(objectMetadata) =>
|
||||||
@ -93,6 +97,7 @@ export class ObjectMetadataFieldRelationService {
|
|||||||
workspaceId,
|
workspaceId,
|
||||||
sourceObjectMetadata,
|
sourceObjectMetadata,
|
||||||
targetObjectMetadata,
|
targetObjectMetadata,
|
||||||
|
queryRunner,
|
||||||
);
|
);
|
||||||
|
|
||||||
return targetObjectMetadata;
|
return targetObjectMetadata;
|
||||||
@ -105,6 +110,7 @@ export class ObjectMetadataFieldRelationService {
|
|||||||
'id' | 'nameSingular' | 'labelSingular'
|
'id' | 'nameSingular' | 'labelSingular'
|
||||||
>,
|
>,
|
||||||
targetObjectMetadata: ObjectMetadataItemWithFieldMaps,
|
targetObjectMetadata: ObjectMetadataItemWithFieldMaps,
|
||||||
|
queryRunner?: QueryRunner,
|
||||||
): Promise<FieldMetadataEntity<FieldMetadataType.RELATION>[]> {
|
): Promise<FieldMetadataEntity<FieldMetadataType.RELATION>[]> {
|
||||||
const sourceFieldMetadata = this.createSourceFieldMetadata(
|
const sourceFieldMetadata = this.createSourceFieldMetadata(
|
||||||
workspaceId,
|
workspaceId,
|
||||||
@ -118,7 +124,11 @@ export class ObjectMetadataFieldRelationService {
|
|||||||
targetObjectMetadata,
|
targetObjectMetadata,
|
||||||
);
|
);
|
||||||
|
|
||||||
return this.fieldMetadataRepository.save([
|
const fieldMetadataRepository = queryRunner
|
||||||
|
? queryRunner.manager.getRepository(FieldMetadataEntity)
|
||||||
|
: this.fieldMetadataRepository;
|
||||||
|
|
||||||
|
return fieldMetadataRepository.save([
|
||||||
{
|
{
|
||||||
...sourceFieldMetadata,
|
...sourceFieldMetadata,
|
||||||
settings: {
|
settings: {
|
||||||
@ -146,6 +156,7 @@ export class ObjectMetadataFieldRelationService {
|
|||||||
ObjectMetadataEntity,
|
ObjectMetadataEntity,
|
||||||
'nameSingular' | 'isCustom' | 'id' | 'labelSingular'
|
'nameSingular' | 'isCustom' | 'id' | 'labelSingular'
|
||||||
>,
|
>,
|
||||||
|
queryRunner?: QueryRunner,
|
||||||
): Promise<
|
): Promise<
|
||||||
{
|
{
|
||||||
targetObjectMetadata: ObjectMetadataEntity;
|
targetObjectMetadata: ObjectMetadataEntity;
|
||||||
@ -160,6 +171,7 @@ export class ObjectMetadataFieldRelationService {
|
|||||||
workspaceId,
|
workspaceId,
|
||||||
updatedObjectMetadata,
|
updatedObjectMetadata,
|
||||||
relationObjectMetadataStandardId,
|
relationObjectMetadataStandardId,
|
||||||
|
queryRunner,
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
@ -172,20 +184,29 @@ export class ObjectMetadataFieldRelationService {
|
|||||||
'nameSingular' | 'id' | 'isCustom' | 'labelSingular'
|
'nameSingular' | 'id' | 'isCustom' | 'labelSingular'
|
||||||
>,
|
>,
|
||||||
targetObjectMetadataStandardId: string,
|
targetObjectMetadataStandardId: string,
|
||||||
|
queryRunner?: QueryRunner,
|
||||||
) {
|
) {
|
||||||
const targetObjectMetadata =
|
const objectMetadataRepository = queryRunner
|
||||||
await this.objectMetadataRepository.findOneByOrFail({
|
? queryRunner.manager.getRepository(ObjectMetadataEntity)
|
||||||
|
: this.objectMetadataRepository;
|
||||||
|
const fieldMetadataRepository = queryRunner
|
||||||
|
? queryRunner.manager.getRepository(FieldMetadataEntity)
|
||||||
|
: this.fieldMetadataRepository;
|
||||||
|
|
||||||
|
const targetObjectMetadata = await objectMetadataRepository.findOneByOrFail(
|
||||||
|
{
|
||||||
standardId: targetObjectMetadataStandardId,
|
standardId: targetObjectMetadataStandardId,
|
||||||
workspaceId: workspaceId,
|
workspaceId: workspaceId,
|
||||||
isCustom: false,
|
isCustom: false,
|
||||||
});
|
},
|
||||||
|
);
|
||||||
|
|
||||||
const targetFieldMetadataUpdateData = this.updateTargetFieldMetadata(
|
const targetFieldMetadataUpdateData = this.updateTargetFieldMetadata(
|
||||||
sourceObjectMetadata,
|
sourceObjectMetadata,
|
||||||
targetObjectMetadata,
|
targetObjectMetadata,
|
||||||
);
|
);
|
||||||
const targetFieldMetadataToUpdate =
|
const targetFieldMetadataToUpdate =
|
||||||
await this.fieldMetadataRepository.findOneByOrFail({
|
await fieldMetadataRepository.findOneByOrFail({
|
||||||
standardId: createRelationDeterministicUuid({
|
standardId: createRelationDeterministicUuid({
|
||||||
objectId: sourceObjectMetadata.id,
|
objectId: sourceObjectMetadata.id,
|
||||||
standardId:
|
standardId:
|
||||||
@ -201,7 +222,7 @@ export class ObjectMetadataFieldRelationService {
|
|||||||
targetFieldMetadataToUpdate as FieldMetadataEntity<FieldMetadataType.RELATION>
|
targetFieldMetadataToUpdate as FieldMetadataEntity<FieldMetadataType.RELATION>
|
||||||
).settings?.relationType === RelationType.MANY_TO_ONE;
|
).settings?.relationType === RelationType.MANY_TO_ONE;
|
||||||
|
|
||||||
const targetFieldMetadata = await this.fieldMetadataRepository.save({
|
const targetFieldMetadata = await fieldMetadataRepository.save({
|
||||||
id: targetFieldMetadataToUpdate.id,
|
id: targetFieldMetadataToUpdate.id,
|
||||||
...targetFieldMetadataUpdateData,
|
...targetFieldMetadataUpdateData,
|
||||||
settings: {
|
settings: {
|
||||||
@ -220,7 +241,7 @@ export class ObjectMetadataFieldRelationService {
|
|||||||
);
|
);
|
||||||
|
|
||||||
const sourceFieldMetadataToUpdate =
|
const sourceFieldMetadataToUpdate =
|
||||||
await this.fieldMetadataRepository.findOneByOrFail({
|
await fieldMetadataRepository.findOneByOrFail({
|
||||||
standardId:
|
standardId:
|
||||||
// @ts-expect-error legacy noImplicitAny
|
// @ts-expect-error legacy noImplicitAny
|
||||||
CUSTOM_OBJECT_STANDARD_FIELD_IDS[targetObjectMetadata.namePlural],
|
CUSTOM_OBJECT_STANDARD_FIELD_IDS[targetObjectMetadata.namePlural],
|
||||||
@ -233,7 +254,7 @@ export class ObjectMetadataFieldRelationService {
|
|||||||
sourceFieldMetadataToUpdate as FieldMetadataEntity<FieldMetadataType.RELATION>
|
sourceFieldMetadataToUpdate as FieldMetadataEntity<FieldMetadataType.RELATION>
|
||||||
).settings?.relationType === RelationType.MANY_TO_ONE;
|
).settings?.relationType === RelationType.MANY_TO_ONE;
|
||||||
|
|
||||||
const sourceFieldMetadata = await this.fieldMetadataRepository.save({
|
const sourceFieldMetadata = await fieldMetadataRepository.save({
|
||||||
id: sourceFieldMetadataToUpdate.id,
|
id: sourceFieldMetadataToUpdate.id,
|
||||||
...sourceFieldMetadataUpdateData,
|
...sourceFieldMetadataUpdateData,
|
||||||
settings: {
|
settings: {
|
||||||
|
|||||||
@ -2,7 +2,7 @@ import { Injectable } from '@nestjs/common';
|
|||||||
import { InjectRepository } from '@nestjs/typeorm';
|
import { InjectRepository } from '@nestjs/typeorm';
|
||||||
|
|
||||||
import { FieldMetadataType } from 'twenty-shared/types';
|
import { FieldMetadataType } from 'twenty-shared/types';
|
||||||
import { Repository } from 'typeorm';
|
import { QueryRunner, Repository } from 'typeorm';
|
||||||
|
|
||||||
import { RelationType } from 'src/engine/metadata-modules/field-metadata/interfaces/relation-type.interface';
|
import { RelationType } from 'src/engine/metadata-modules/field-metadata/interfaces/relation-type.interface';
|
||||||
|
|
||||||
@ -36,6 +36,7 @@ export class ObjectMetadataMigrationService {
|
|||||||
|
|
||||||
public async createTableMigration(
|
public async createTableMigration(
|
||||||
createdObjectMetadata: ObjectMetadataEntity,
|
createdObjectMetadata: ObjectMetadataEntity,
|
||||||
|
queryRunner?: QueryRunner,
|
||||||
) {
|
) {
|
||||||
await this.workspaceMigrationService.createCustomMigration(
|
await this.workspaceMigrationService.createCustomMigration(
|
||||||
generateMigrationName(`create-${createdObjectMetadata.nameSingular}`),
|
generateMigrationName(`create-${createdObjectMetadata.nameSingular}`),
|
||||||
@ -46,12 +47,14 @@ export class ObjectMetadataMigrationService {
|
|||||||
action: WorkspaceMigrationTableActionType.CREATE,
|
action: WorkspaceMigrationTableActionType.CREATE,
|
||||||
} satisfies WorkspaceMigrationTableAction,
|
} satisfies WorkspaceMigrationTableAction,
|
||||||
],
|
],
|
||||||
|
queryRunner,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
public async createColumnsMigrations(
|
public async createColumnsMigrations(
|
||||||
createdObjectMetadata: ObjectMetadataEntity,
|
createdObjectMetadata: ObjectMetadataEntity,
|
||||||
fieldMetadataCollection: FieldMetadataEntity[],
|
fieldMetadataCollection: FieldMetadataEntity[],
|
||||||
|
queryRunner?: QueryRunner,
|
||||||
) {
|
) {
|
||||||
await this.workspaceMigrationService.createCustomMigration(
|
await this.workspaceMigrationService.createCustomMigration(
|
||||||
generateMigrationName(
|
generateMigrationName(
|
||||||
@ -70,6 +73,7 @@ export class ObjectMetadataMigrationService {
|
|||||||
),
|
),
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
|
queryRunner,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -82,6 +86,7 @@ export class ObjectMetadataMigrationService {
|
|||||||
ObjectMetadataItemWithFieldMaps,
|
ObjectMetadataItemWithFieldMaps,
|
||||||
'nameSingular' | 'isCustom'
|
'nameSingular' | 'isCustom'
|
||||||
>[],
|
>[],
|
||||||
|
queryRunner?: QueryRunner,
|
||||||
) {
|
) {
|
||||||
await this.workspaceMigrationService.createCustomMigration(
|
await this.workspaceMigrationService.createCustomMigration(
|
||||||
generateMigrationName(
|
generateMigrationName(
|
||||||
@ -92,6 +97,7 @@ export class ObjectMetadataMigrationService {
|
|||||||
createdObjectMetadata,
|
createdObjectMetadata,
|
||||||
relatedObjectMetadataCollection,
|
relatedObjectMetadataCollection,
|
||||||
),
|
),
|
||||||
|
queryRunner,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -105,6 +111,7 @@ export class ObjectMetadataMigrationService {
|
|||||||
'nameSingular' | 'isCustom'
|
'nameSingular' | 'isCustom'
|
||||||
>,
|
>,
|
||||||
workspaceId: string,
|
workspaceId: string,
|
||||||
|
queryRunner?: QueryRunner,
|
||||||
) {
|
) {
|
||||||
const newTargetTableName = computeObjectTargetTable(
|
const newTargetTableName = computeObjectTargetTable(
|
||||||
objectMetadataForUpdate,
|
objectMetadataForUpdate,
|
||||||
@ -113,7 +120,7 @@ export class ObjectMetadataMigrationService {
|
|||||||
existingObjectMetadata,
|
existingObjectMetadata,
|
||||||
);
|
);
|
||||||
|
|
||||||
this.workspaceMigrationService.createCustomMigration(
|
await this.workspaceMigrationService.createCustomMigration(
|
||||||
generateMigrationName(`rename-${existingObjectMetadata.nameSingular}`),
|
generateMigrationName(`rename-${existingObjectMetadata.nameSingular}`),
|
||||||
workspaceId,
|
workspaceId,
|
||||||
[
|
[
|
||||||
@ -123,6 +130,7 @@ export class ObjectMetadataMigrationService {
|
|||||||
action: WorkspaceMigrationTableActionType.ALTER,
|
action: WorkspaceMigrationTableActionType.ALTER,
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
|
queryRunner,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -135,6 +143,7 @@ export class ObjectMetadataMigrationService {
|
|||||||
sourceFieldMetadata: FieldMetadataEntity;
|
sourceFieldMetadata: FieldMetadataEntity;
|
||||||
}[],
|
}[],
|
||||||
workspaceId: string,
|
workspaceId: string,
|
||||||
|
queryRunner?: QueryRunner,
|
||||||
) {
|
) {
|
||||||
for (const { targetObjectMetadata } of relationMetadataCollection) {
|
for (const { targetObjectMetadata } of relationMetadataCollection) {
|
||||||
const targetTableName = computeObjectTargetTable(targetObjectMetadata);
|
const targetTableName = computeObjectTargetTable(targetObjectMetadata);
|
||||||
@ -168,6 +177,7 @@ export class ObjectMetadataMigrationService {
|
|||||||
],
|
],
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
|
queryRunner,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -180,6 +190,7 @@ export class ObjectMetadataMigrationService {
|
|||||||
foreignKeyFieldMetadata: FieldMetadataEntity;
|
foreignKeyFieldMetadata: FieldMetadataEntity;
|
||||||
}[],
|
}[],
|
||||||
workspaceId: string,
|
workspaceId: string,
|
||||||
|
queryRunner?: QueryRunner,
|
||||||
) {
|
) {
|
||||||
for (const {
|
for (const {
|
||||||
relatedObjectMetadata,
|
relatedObjectMetadata,
|
||||||
@ -221,6 +232,7 @@ export class ObjectMetadataMigrationService {
|
|||||||
],
|
],
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
|
queryRunner,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -228,6 +240,7 @@ export class ObjectMetadataMigrationService {
|
|||||||
public async deleteAllRelationsAndDropTable(
|
public async deleteAllRelationsAndDropTable(
|
||||||
objectMetadata: ObjectMetadataEntity,
|
objectMetadata: ObjectMetadataEntity,
|
||||||
workspaceId: string,
|
workspaceId: string,
|
||||||
|
queryRunner?: QueryRunner,
|
||||||
) {
|
) {
|
||||||
const relationFields = objectMetadata.fields.filter((field) =>
|
const relationFields = objectMetadata.fields.filter((field) =>
|
||||||
isFieldMetadataInterfaceOfType(field, FieldMetadataType.RELATION),
|
isFieldMetadataInterfaceOfType(field, FieldMetadataType.RELATION),
|
||||||
@ -279,6 +292,7 @@ export class ObjectMetadataMigrationService {
|
|||||||
],
|
],
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
|
queryRunner,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -291,6 +305,7 @@ export class ObjectMetadataMigrationService {
|
|||||||
action: WorkspaceMigrationTableActionType.DROP,
|
action: WorkspaceMigrationTableActionType.DROP,
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
|
queryRunner,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -300,6 +315,7 @@ export class ObjectMetadataMigrationService {
|
|||||||
'nameSingular' | 'isCustom' | 'id' | 'fieldsById'
|
'nameSingular' | 'isCustom' | 'id' | 'fieldsById'
|
||||||
>,
|
>,
|
||||||
workspaceId: string,
|
workspaceId: string,
|
||||||
|
queryRunner?: QueryRunner,
|
||||||
) {
|
) {
|
||||||
const enumFieldMetadataTypes = [
|
const enumFieldMetadataTypes = [
|
||||||
FieldMetadataType.SELECT,
|
FieldMetadataType.SELECT,
|
||||||
@ -330,6 +346,7 @@ export class ObjectMetadataMigrationService {
|
|||||||
),
|
),
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
|
queryRunner,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -3,7 +3,7 @@ import { InjectRepository } from '@nestjs/typeorm';
|
|||||||
|
|
||||||
import { FieldMetadataType } from 'twenty-shared/types';
|
import { FieldMetadataType } from 'twenty-shared/types';
|
||||||
import { isDefined } from 'twenty-shared/utils';
|
import { isDefined } from 'twenty-shared/utils';
|
||||||
import { Repository } from 'typeorm';
|
import { QueryRunner, Repository } from 'typeorm';
|
||||||
|
|
||||||
import { FieldMetadataInterface } from 'src/engine/metadata-modules/field-metadata/interfaces/field-metadata.interface';
|
import { FieldMetadataInterface } from 'src/engine/metadata-modules/field-metadata/interfaces/field-metadata.interface';
|
||||||
|
|
||||||
@ -46,8 +46,13 @@ export class SearchVectorService {
|
|||||||
public async createSearchVectorFieldForObject(
|
public async createSearchVectorFieldForObject(
|
||||||
objectMetadataInput: CreateObjectInput,
|
objectMetadataInput: CreateObjectInput,
|
||||||
createdObjectMetadata: ObjectMetadataEntity,
|
createdObjectMetadata: ObjectMetadataEntity,
|
||||||
|
queryRunner?: QueryRunner,
|
||||||
) {
|
) {
|
||||||
const searchVectorFieldMetadata = await this.fieldMetadataRepository.save({
|
const repository = queryRunner
|
||||||
|
? queryRunner.manager.getRepository(FieldMetadataEntity)
|
||||||
|
: this.fieldMetadataRepository;
|
||||||
|
|
||||||
|
const searchVectorFieldMetadata = await repository.save({
|
||||||
standardId: CUSTOM_OBJECT_STANDARD_FIELD_IDS.searchVector,
|
standardId: CUSTOM_OBJECT_STANDARD_FIELD_IDS.searchVector,
|
||||||
objectMetadataId: createdObjectMetadata.id,
|
objectMetadataId: createdObjectMetadata.id,
|
||||||
workspaceId: objectMetadataInput.workspaceId,
|
workspaceId: objectMetadataInput.workspaceId,
|
||||||
@ -101,24 +106,31 @@ export class SearchVectorService {
|
|||||||
} as FieldMetadataInterface<FieldMetadataType.TS_VECTOR>),
|
} as FieldMetadataInterface<FieldMetadataType.TS_VECTOR>),
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
|
queryRunner,
|
||||||
);
|
);
|
||||||
|
|
||||||
await this.indexMetadataService.createIndexMetadata(
|
await this.indexMetadataService.createIndexMetadata({
|
||||||
objectMetadataInput.workspaceId,
|
workspaceId: objectMetadataInput.workspaceId,
|
||||||
createdObjectMetadata,
|
objectMetadata: createdObjectMetadata,
|
||||||
[searchVectorFieldMetadata],
|
fieldMetadataToIndex: [searchVectorFieldMetadata],
|
||||||
false,
|
isUnique: false,
|
||||||
false,
|
isCustom: false,
|
||||||
IndexType.GIN,
|
indexType: IndexType.GIN,
|
||||||
);
|
queryRunner,
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
public async updateSearchVector(
|
public async updateSearchVector(
|
||||||
objectMetadataId: string,
|
objectMetadataId: string,
|
||||||
fieldMetadataNameAndTypeForSearch: FieldTypeAndNameMetadata[],
|
fieldMetadataNameAndTypeForSearch: FieldTypeAndNameMetadata[],
|
||||||
workspaceId: string,
|
workspaceId: string,
|
||||||
|
queryRunner?: QueryRunner,
|
||||||
) {
|
) {
|
||||||
const objectMetadata = await this.objectMetadataRepository.findOneByOrFail({
|
const repository = queryRunner
|
||||||
|
? queryRunner.manager.getRepository(ObjectMetadataEntity)
|
||||||
|
: this.objectMetadataRepository;
|
||||||
|
|
||||||
|
const objectMetadata = await repository.findOneByOrFail({
|
||||||
id: objectMetadataId,
|
id: objectMetadataId,
|
||||||
});
|
});
|
||||||
|
|
||||||
@ -152,16 +164,17 @@ export class SearchVectorService {
|
|||||||
),
|
),
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
|
queryRunner,
|
||||||
);
|
);
|
||||||
|
|
||||||
// index needs to be recreated as typeorm deletes then recreates searchVector column at alter
|
// index needs to be recreated as typeorm deletes then recreates searchVector column at alter
|
||||||
await this.indexMetadataService.createIndexCreationMigration(
|
await this.indexMetadataService.createIndexCreationMigration({
|
||||||
workspaceId,
|
workspaceId,
|
||||||
objectMetadata,
|
objectMetadata,
|
||||||
[existingSearchVectorFieldMetadata],
|
fieldMetadataToIndex: [existingSearchVectorFieldMetadata],
|
||||||
false,
|
isUnique: false,
|
||||||
false,
|
indexType: IndexType.GIN,
|
||||||
IndexType.GIN,
|
queryRunner,
|
||||||
);
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -1,7 +1,7 @@
|
|||||||
import { Injectable } from '@nestjs/common';
|
import { Injectable } from '@nestjs/common';
|
||||||
import { InjectRepository } from '@nestjs/typeorm';
|
import { InjectRepository } from '@nestjs/typeorm';
|
||||||
|
|
||||||
import { IsNull, Repository } from 'typeorm';
|
import { IsNull, QueryRunner, Repository } from 'typeorm';
|
||||||
|
|
||||||
import {
|
import {
|
||||||
WorkspaceMigrationEntity,
|
WorkspaceMigrationEntity,
|
||||||
@ -23,8 +23,13 @@ export class WorkspaceMigrationService {
|
|||||||
*/
|
*/
|
||||||
public async getPendingMigrations(
|
public async getPendingMigrations(
|
||||||
workspaceId: string,
|
workspaceId: string,
|
||||||
|
queryRunner?: QueryRunner,
|
||||||
): Promise<WorkspaceMigrationEntity[]> {
|
): Promise<WorkspaceMigrationEntity[]> {
|
||||||
const pendingMigrations = await this.workspaceMigrationRepository.find({
|
const workspaceMigrationRepository = queryRunner
|
||||||
|
? queryRunner.manager.getRepository(WorkspaceMigrationEntity)
|
||||||
|
: this.workspaceMigrationRepository;
|
||||||
|
|
||||||
|
const pendingMigrations = await workspaceMigrationRepository.find({
|
||||||
order: { createdAt: 'ASC', name: 'ASC' },
|
order: { createdAt: 'ASC', name: 'ASC' },
|
||||||
where: {
|
where: {
|
||||||
appliedAt: IsNull(),
|
appliedAt: IsNull(),
|
||||||
@ -113,13 +118,21 @@ export class WorkspaceMigrationService {
|
|||||||
name: string,
|
name: string,
|
||||||
workspaceId: string,
|
workspaceId: string,
|
||||||
migrations: WorkspaceMigrationTableAction[],
|
migrations: WorkspaceMigrationTableAction[],
|
||||||
|
queryRunner?: QueryRunner,
|
||||||
) {
|
) {
|
||||||
return this.workspaceMigrationRepository.save({
|
const workspaceMigrationRepository = queryRunner
|
||||||
|
? queryRunner.manager.getRepository(WorkspaceMigrationEntity)
|
||||||
|
: this.workspaceMigrationRepository;
|
||||||
|
|
||||||
|
const migration = await workspaceMigrationRepository.save({
|
||||||
name,
|
name,
|
||||||
migrations,
|
migrations,
|
||||||
workspaceId,
|
workspaceId,
|
||||||
isCustom: true,
|
isCustom: true,
|
||||||
|
createdAt: new Date(),
|
||||||
});
|
});
|
||||||
|
|
||||||
|
return migration;
|
||||||
}
|
}
|
||||||
|
|
||||||
public async deleteAllWithinWorkspace(workspaceId: string) {
|
public async deleteAllWithinWorkspace(workspaceId: string) {
|
||||||
|
|||||||
@ -32,6 +32,54 @@ export class WorkspaceMigrationRunnerService {
|
|||||||
private readonly workspaceMigrationColumnService: WorkspaceMigrationColumnService,
|
private readonly workspaceMigrationColumnService: WorkspaceMigrationColumnService,
|
||||||
) {}
|
) {}
|
||||||
|
|
||||||
|
public async executeMigrationFromPendingMigrationsWithinTransaction(
|
||||||
|
workspaceId: string,
|
||||||
|
transactionQueryRunner: QueryRunner,
|
||||||
|
): Promise<WorkspaceMigrationTableAction[]> {
|
||||||
|
const pendingMigrations =
|
||||||
|
await this.workspaceMigrationService.getPendingMigrations(
|
||||||
|
workspaceId,
|
||||||
|
transactionQueryRunner,
|
||||||
|
);
|
||||||
|
|
||||||
|
if (pendingMigrations.length === 0) {
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
|
||||||
|
const migrationActionsWithParent = pendingMigrations.flatMap(
|
||||||
|
(pendingMigration) =>
|
||||||
|
(pendingMigration.migrations || []).map((tableAction) => ({
|
||||||
|
tableAction,
|
||||||
|
parentMigrationId: pendingMigration.id,
|
||||||
|
})),
|
||||||
|
);
|
||||||
|
|
||||||
|
const schemaName =
|
||||||
|
this.workspaceDataSourceService.getSchemaName(workspaceId);
|
||||||
|
|
||||||
|
await transactionQueryRunner.query(
|
||||||
|
`SET LOCAL search_path TO ${schemaName}`,
|
||||||
|
);
|
||||||
|
|
||||||
|
for (const {
|
||||||
|
tableAction,
|
||||||
|
parentMigrationId,
|
||||||
|
} of migrationActionsWithParent) {
|
||||||
|
await this.handleTableChanges(
|
||||||
|
transactionQueryRunner as PostgresQueryRunner,
|
||||||
|
schemaName,
|
||||||
|
tableAction,
|
||||||
|
);
|
||||||
|
|
||||||
|
await transactionQueryRunner.query(
|
||||||
|
`UPDATE "core"."workspaceMigration" SET "appliedAt" = NOW() WHERE "id" = $1 AND "workspaceId" = $2`,
|
||||||
|
[parentMigrationId, workspaceId],
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return migrationActionsWithParent.map((item) => item.tableAction);
|
||||||
|
}
|
||||||
|
|
||||||
public async executeMigrationFromPendingMigrations(
|
public async executeMigrationFromPendingMigrations(
|
||||||
workspaceId: string,
|
workspaceId: string,
|
||||||
): Promise<WorkspaceMigrationTableAction[]> {
|
): Promise<WorkspaceMigrationTableAction[]> {
|
||||||
@ -42,58 +90,32 @@ export class WorkspaceMigrationRunnerService {
|
|||||||
throw new Error('Main data source not found');
|
throw new Error('Main data source not found');
|
||||||
}
|
}
|
||||||
|
|
||||||
const pendingMigrations =
|
|
||||||
await this.workspaceMigrationService.getPendingMigrations(workspaceId);
|
|
||||||
|
|
||||||
if (pendingMigrations.length === 0) {
|
|
||||||
return [];
|
|
||||||
}
|
|
||||||
|
|
||||||
const flattenedPendingMigrations: WorkspaceMigrationTableAction[] =
|
|
||||||
pendingMigrations.reduce((acc, pendingMigration) => {
|
|
||||||
return [...acc, ...pendingMigration.migrations];
|
|
||||||
}, []);
|
|
||||||
|
|
||||||
const queryRunner =
|
const queryRunner =
|
||||||
mainDataSource.createQueryRunner() as PostgresQueryRunner;
|
mainDataSource.createQueryRunner() as PostgresQueryRunner;
|
||||||
|
|
||||||
await queryRunner.connect();
|
await queryRunner.connect();
|
||||||
await queryRunner.startTransaction();
|
await queryRunner.startTransaction();
|
||||||
|
|
||||||
const schemaName =
|
|
||||||
this.workspaceDataSourceService.getSchemaName(workspaceId);
|
|
||||||
|
|
||||||
await queryRunner.query(`SET LOCAL search_path TO ${schemaName}`);
|
|
||||||
|
|
||||||
try {
|
try {
|
||||||
// Loop over each migration and create or update the table
|
const result =
|
||||||
for (const migration of flattenedPendingMigrations) {
|
await this.executeMigrationFromPendingMigrationsWithinTransaction(
|
||||||
await this.handleTableChanges(queryRunner, schemaName, migration);
|
workspaceId,
|
||||||
}
|
queryRunner,
|
||||||
|
);
|
||||||
|
|
||||||
await queryRunner.commitTransaction();
|
await queryRunner.commitTransaction();
|
||||||
|
|
||||||
|
return result;
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
this.logger.error(
|
this.logger.error(
|
||||||
`Error executing migration: ${error.message}`,
|
`Error executing migration: ${error.message}`,
|
||||||
error.stack,
|
error.stack,
|
||||||
);
|
);
|
||||||
|
|
||||||
await queryRunner.rollbackTransaction();
|
await queryRunner.rollbackTransaction();
|
||||||
throw error;
|
throw error;
|
||||||
} finally {
|
} finally {
|
||||||
await queryRunner.release();
|
await queryRunner.release();
|
||||||
}
|
}
|
||||||
|
|
||||||
// Update appliedAt date for each migration
|
|
||||||
// TODO: Should be done after the migration is successful
|
|
||||||
for (const pendingMigration of pendingMigrations) {
|
|
||||||
await this.workspaceMigrationService.setAppliedAtForMigration(
|
|
||||||
workspaceId,
|
|
||||||
pendingMigration,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
return flattenedPendingMigrations;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private async handleTableChanges(
|
private async handleTableChanges(
|
||||||
|
|||||||
@ -191,15 +191,17 @@ export class WorkspaceSyncMetadataService {
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
await queryRunner.commitTransaction();
|
|
||||||
|
|
||||||
// Execute migrations
|
// Execute migrations
|
||||||
this.logger.log('Executing pending migrations');
|
this.logger.log('Executing pending migrations');
|
||||||
const executeMigrationsStart = performance.now();
|
const executeMigrationsStart = performance.now();
|
||||||
|
|
||||||
await this.workspaceMigrationRunnerService.executeMigrationFromPendingMigrations(
|
await this.workspaceMigrationRunnerService.executeMigrationFromPendingMigrationsWithinTransaction(
|
||||||
context.workspaceId,
|
context.workspaceId,
|
||||||
|
queryRunner,
|
||||||
);
|
);
|
||||||
|
|
||||||
|
await queryRunner.commitTransaction();
|
||||||
|
|
||||||
const executeMigrationsEnd = performance.now();
|
const executeMigrationsEnd = performance.now();
|
||||||
|
|
||||||
this.logger.log(
|
this.logger.log(
|
||||||
|
|||||||
@ -69,16 +69,12 @@ describe('deleteManyObjectRecordsPermissions', () => {
|
|||||||
expect(response.body.data).toBeDefined();
|
expect(response.body.data).toBeDefined();
|
||||||
expect(response.body.data.deletePeople).toBeDefined();
|
expect(response.body.data.deletePeople).toBeDefined();
|
||||||
expect(response.body.data.deletePeople).toHaveLength(2);
|
expect(response.body.data.deletePeople).toHaveLength(2);
|
||||||
expect(
|
expect(response.body.data.deletePeople).toEqual(
|
||||||
response.body.data.deletePeople.some(
|
expect.arrayContaining([
|
||||||
(person: { id: string }) => person.id === personId1,
|
expect.objectContaining({ id: personId1 }),
|
||||||
),
|
expect.objectContaining({ id: personId2 }),
|
||||||
).toBe(true);
|
]),
|
||||||
expect(
|
);
|
||||||
response.body.data.deletePeople.some(
|
|
||||||
(person: { id: string }) => person.id === personId2,
|
|
||||||
),
|
|
||||||
).toBe(true);
|
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should delete multiple object records when executed by api key', async () => {
|
it('should delete multiple object records when executed by api key', async () => {
|
||||||
@ -119,7 +115,11 @@ describe('deleteManyObjectRecordsPermissions', () => {
|
|||||||
expect(response.body.data).toBeDefined();
|
expect(response.body.data).toBeDefined();
|
||||||
expect(response.body.data.deletePeople).toBeDefined();
|
expect(response.body.data.deletePeople).toBeDefined();
|
||||||
expect(response.body.data.deletePeople).toHaveLength(2);
|
expect(response.body.data.deletePeople).toHaveLength(2);
|
||||||
expect(response.body.data.deletePeople[0].id).toBe(personId1);
|
expect(response.body.data.deletePeople).toEqual(
|
||||||
expect(response.body.data.deletePeople[1].id).toBe(personId2);
|
expect.arrayContaining([
|
||||||
|
expect.objectContaining({ id: personId1 }),
|
||||||
|
expect.objectContaining({ id: personId2 }),
|
||||||
|
]),
|
||||||
|
);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
@ -68,15 +68,11 @@ describe('destroyManyObjectRecordsPermissions', () => {
|
|||||||
expect(response.body.data).toBeDefined();
|
expect(response.body.data).toBeDefined();
|
||||||
expect(response.body.data.destroyPeople).toBeDefined();
|
expect(response.body.data.destroyPeople).toBeDefined();
|
||||||
expect(response.body.data.destroyPeople).toHaveLength(2);
|
expect(response.body.data.destroyPeople).toHaveLength(2);
|
||||||
expect(
|
expect(response.body.data.destroyPeople).toEqual(
|
||||||
response.body.data.destroyPeople.some(
|
expect.arrayContaining([
|
||||||
(person: { id: string }) => person.id === personId1,
|
expect.objectContaining({ id: personId1 }),
|
||||||
),
|
expect.objectContaining({ id: personId2 }),
|
||||||
).toBe(true);
|
]),
|
||||||
expect(
|
);
|
||||||
response.body.data.destroyPeople.some(
|
|
||||||
(person: { id: string }) => person.id === personId2,
|
|
||||||
),
|
|
||||||
).toBe(true);
|
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
@ -86,7 +86,11 @@ describe('restoreManyObjectRecordsPermissions', () => {
|
|||||||
expect(response.body.data).toBeDefined();
|
expect(response.body.data).toBeDefined();
|
||||||
expect(response.body.data.restorePeople).toBeDefined();
|
expect(response.body.data.restorePeople).toBeDefined();
|
||||||
expect(response.body.data.restorePeople).toHaveLength(2);
|
expect(response.body.data.restorePeople).toHaveLength(2);
|
||||||
expect(response.body.data.restorePeople[0].id).toBe(personId1);
|
expect(response.body.data.restorePeople).toEqual(
|
||||||
expect(response.body.data.restorePeople[1].id).toBe(personId2);
|
expect.arrayContaining([
|
||||||
|
expect.objectContaining({ id: personId1 }),
|
||||||
|
expect.objectContaining({ id: personId2 }),
|
||||||
|
]),
|
||||||
|
);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
Reference in New Issue
Block a user