6188c72f74
# Introduction This PR introduces a huge type refactor that will leverage dynamic intra entity optimistic flat maps update in the future and also a more granular cache invalidation enhancing performances close https://github.com/twentyhq/core-team-issues/issues/1717 close https://github.com/twentyhq/core-team-issues/issues/1716 close https://github.com/twentyhq/core-team-issues/issues/1643 ## What's done ### Comparators centralization Comparator is now done through global configuration as const for each metadata names Thanks to Note: Definition of standard is evolving, standard is now scoped to an app. Meaning that a manifest should be able to update its own standards objects but on other app standards ones ? Each synchronizable entities will have a standardOverrides ? ## Typing refactor ### `AllFlatEntityTypesByMetadataName` **Single source of truth for the complete type ecosystem**, mapping each metadata name to its entity types, flat entities, and migration actions: ```typescript export type AllFlatEntityTypesByMetadataName = { fieldMetadata: { actions: { created: CreateFieldAction; updated: UpdateFieldAction; deleted: DeleteFieldAction; }; flatEntity: FlatFieldMetadata; entity: FieldMetadataEntity; }; objectMetadata: { /* ... */ }; // ... all 10 metadata types }; ``` ### `ALL_METADATA_NAME_MANY_TO_ONE_RELATIONS` **Explicitly declares database relationships** between entities with compile-time validation: ```typescript export const ALL_METADATA_NAME_MANY_TO_ONE_RELATIONS = { viewField: { view: 'viewId', fieldMetadata: 'fieldMetadataId', }, cronTrigger: { serverlessFunction: 'serverlessFunctionId', }, // ... all relations } as const satisfies MetadataNameAndRelations; ``` ### `ALL_FLAT_ENTITY_CONFIGURATION` **Centralizes comparison and serialization logic** for each metadata type: ```typescript export const ALL_FLAT_ENTITY_CONFIGURATION = { fieldMetadata: { propertiesToCompare: ['name', 'type', 'label', 'defaultValue', /* ... */], propertiesToStringify: ['options', 'settings', 'defaultValue'], }, objectMetadata: { propertiesToCompare: ['nameSingular', 'namePlural', 'isActive', /* ... */], propertiesToStringify: [], }, // ... all metadata types } as const satisfies AllFlatEntityConfiguration; ``` ## Combined Impact These three configurations work together to create a **strongly-typed, centrally-managed metadata system**: 1. **`AllFlatEntityTypesByMetadataName`** defines *what exists* 2. **`ALL_METADATA_NAME_MANY_TO_ONE_RELATIONS`** defines *how they relate* 3. **`ALL_FLAT_ENTITY_CONFIGURATION`** defines *how to compare and serialize them* **Result:** Builders and validators become thin wrappers around type-safe, configuration-driven logic instead of containing scattered, error-prone manual implementations. ## What's next ### StandardOverrides standardization Every metadata entity can be a standard one for a workspace if it's an installed app, which means it might not expose the whole entity api to be editable through an import dynamically The standard overrides logic should not be applied to Fields and Objects but to every entities At the moment we have a logic of `EDITABLE_PROPERTIES` through the api, and also `STANDARD_OVERRIDEDABLE_PROPERTIES` This should be configuration centered like `propertiesToCompare` and `propertiesToStringify`. Scoping this PR to two last for the moment. As update dispatch to standardOverrides could be considered as a side effect prefer waiting to start the side effect refactor ### Granular Optimistic deprecation With this new grain at runtime we will be able to add a flat entity and dispatch its addition to related flat maps, so we don't have to describe an optimistic method for each flat entity operations See `addFlatEntityToFlatEntityAndRelatedEntityMapsOrThrow` Note: Still in wip and included in this PR but about to create a new one to integrate these utils and remove existing methods ### ValidateBuildAndRun dynamic args typed defintion We should restrain the devxp to send expected flat maps entity as at least from to or dependency as we now have the grain both a type lvl and runtime to do so It should not be possible in the devxp to forgot adding the views to the v2 builder when passing the view field anymore ( that would lead to permanent validation error in view field integrity checks ) ## Conclusion Thanks for reading and reviewing ! Any suggestions are more than welcomed ! ( same as for questions too ! )
793 lines
27 KiB
TypeScript
793 lines
27 KiB
TypeScript
import { Injectable } from '@nestjs/common';
|
|
import { InjectDataSource, InjectRepository } from '@nestjs/typeorm';
|
|
|
|
import { type Query, type QueryOptions } from '@ptc-org/nestjs-query-core';
|
|
import { TypeOrmQueryService } from '@ptc-org/nestjs-query-typeorm';
|
|
import { FieldMetadataType } from 'twenty-shared/types';
|
|
import { capitalize, isDefined } from 'twenty-shared/utils';
|
|
import {
|
|
DataSource,
|
|
In,
|
|
Repository,
|
|
type FindManyOptions,
|
|
type FindOneOptions,
|
|
type QueryRunner,
|
|
} from 'typeorm';
|
|
|
|
import { FeatureFlagKey } from 'src/engine/core-modules/feature-flag/enums/feature-flag-key.enum';
|
|
import { FeatureFlagService } from 'src/engine/core-modules/feature-flag/services/feature-flag.service';
|
|
import { DataSourceService } from 'src/engine/metadata-modules/data-source/data-source.service';
|
|
import { FieldMetadataEntity } from 'src/engine/metadata-modules/field-metadata/field-metadata.entity';
|
|
import { WorkspaceManyOrAllFlatEntityMapsCacheService } from 'src/engine/metadata-modules/flat-entity/services/workspace-many-or-all-flat-entity-maps-cache.service';
|
|
import { IndexMetadataService } from 'src/engine/metadata-modules/index-metadata/index-metadata.service';
|
|
import { type DeleteOneObjectInput } from 'src/engine/metadata-modules/object-metadata/dtos/delete-object.input';
|
|
import {
|
|
type UpdateObjectPayload,
|
|
type UpdateOneObjectInput,
|
|
} from 'src/engine/metadata-modules/object-metadata/dtos/update-object.input';
|
|
import { ObjectMetadataServiceV2 } from 'src/engine/metadata-modules/object-metadata/object-metadata-v2.service';
|
|
import {
|
|
ObjectMetadataException,
|
|
ObjectMetadataExceptionCode,
|
|
} from 'src/engine/metadata-modules/object-metadata/object-metadata.exception';
|
|
import { ObjectMetadataFieldRelationService } from 'src/engine/metadata-modules/object-metadata/services/object-metadata-field-relation.service';
|
|
import { ObjectMetadataMigrationService } from 'src/engine/metadata-modules/object-metadata/services/object-metadata-migration.service';
|
|
import { ObjectMetadataRelatedRecordsService } from 'src/engine/metadata-modules/object-metadata/services/object-metadata-related-records.service';
|
|
import { buildDefaultFieldsForCustomObject } from 'src/engine/metadata-modules/object-metadata/utils/build-default-fields-for-custom-object.util';
|
|
import {
|
|
validateLowerCasedAndTrimmedStringsAreDifferentOrThrow,
|
|
validateObjectMetadataInputLabelsOrThrow,
|
|
validateObjectMetadataInputNamesOrThrow,
|
|
} from 'src/engine/metadata-modules/object-metadata/utils/validate-object-metadata-input.util';
|
|
import { SearchVectorService } from 'src/engine/metadata-modules/search-vector/search-vector.service';
|
|
import { type ObjectMetadataItemWithFieldMaps } from 'src/engine/metadata-modules/types/object-metadata-item-with-field-maps';
|
|
import { ObjectMetadataMaps } from 'src/engine/metadata-modules/types/object-metadata-maps';
|
|
import { validateMetadataIdentifierFieldMetadataIds } from 'src/engine/metadata-modules/utils/validate-metadata-identifier-field-metadata-id.utils';
|
|
import { validateNameAndLabelAreSyncOrThrow } from 'src/engine/metadata-modules/utils/validate-name-and-label-are-sync-or-throw.util';
|
|
import { validatesNoOtherObjectWithSameNameExistsOrThrows } from 'src/engine/metadata-modules/utils/validate-no-other-object-with-same-name-exists-or-throw.util';
|
|
import { WorkspaceMetadataCacheService } from 'src/engine/metadata-modules/workspace-metadata-cache/services/workspace-metadata-cache.service';
|
|
import { WorkspaceMetadataVersionService } from 'src/engine/metadata-modules/workspace-metadata-version/services/workspace-metadata-version.service';
|
|
import { WorkspacePermissionsCacheService } from 'src/engine/metadata-modules/workspace-permissions-cache/workspace-permissions-cache.service';
|
|
import { computeObjectTargetTable } from 'src/engine/utils/compute-object-target-table.util';
|
|
import { isFieldMetadataEntityOfType } from 'src/engine/utils/is-field-metadata-of-type.util';
|
|
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 { isSearchableFieldType } from 'src/engine/workspace-manager/workspace-sync-metadata/utils/is-searchable-field.util';
|
|
|
|
import { ObjectMetadataEntity } from './object-metadata.entity';
|
|
|
|
import { type CreateObjectInput } from './dtos/create-object.input';
|
|
|
|
@Injectable()
|
|
export class ObjectMetadataService extends TypeOrmQueryService<ObjectMetadataEntity> {
|
|
constructor(
|
|
@InjectRepository(ObjectMetadataEntity)
|
|
private readonly objectMetadataRepository: Repository<ObjectMetadataEntity>,
|
|
@InjectRepository(FieldMetadataEntity)
|
|
private readonly fieldMetadataRepository: Repository<FieldMetadataEntity>,
|
|
|
|
private readonly dataSourceService: DataSourceService,
|
|
private readonly workspaceMetadataCacheService: WorkspaceMetadataCacheService,
|
|
private readonly workspaceMigrationRunnerService: WorkspaceMigrationRunnerService,
|
|
private readonly workspaceMetadataVersionService: WorkspaceMetadataVersionService,
|
|
private readonly searchVectorService: SearchVectorService,
|
|
private readonly objectMetadataFieldRelationService: ObjectMetadataFieldRelationService,
|
|
private readonly objectMetadataMigrationService: ObjectMetadataMigrationService,
|
|
private readonly objectMetadataRelatedRecordsService: ObjectMetadataRelatedRecordsService,
|
|
private readonly indexMetadataService: IndexMetadataService,
|
|
private readonly workspacePermissionsCacheService: WorkspacePermissionsCacheService,
|
|
private readonly flatEntityMapsCacheService: WorkspaceManyOrAllFlatEntityMapsCacheService,
|
|
@InjectDataSource()
|
|
private readonly coreDataSource: DataSource,
|
|
private readonly featureFlagService: FeatureFlagService,
|
|
private readonly objectMetadataServiceV2: ObjectMetadataServiceV2,
|
|
) {
|
|
super(objectMetadataRepository);
|
|
}
|
|
|
|
override async query(
|
|
query: Query<ObjectMetadataEntity>,
|
|
opts?: QueryOptions<ObjectMetadataEntity> | undefined,
|
|
): Promise<ObjectMetadataEntity[]> {
|
|
const start = performance.now();
|
|
|
|
const result = super.query(query, opts);
|
|
|
|
const end = performance.now();
|
|
|
|
// eslint-disable-next-line no-console
|
|
console.log(`metadata query time: ${end - start} ms`);
|
|
|
|
return result;
|
|
}
|
|
|
|
override async createOne(
|
|
createObjectInput: CreateObjectInput,
|
|
): Promise<ObjectMetadataEntity> {
|
|
const { workspaceId } = createObjectInput;
|
|
const isWorkspaceMigrationV2Enabled =
|
|
await this.featureFlagService.isFeatureEnabled(
|
|
FeatureFlagKey.IS_WORKSPACE_MIGRATION_V2_ENABLED,
|
|
workspaceId,
|
|
);
|
|
|
|
if (isWorkspaceMigrationV2Enabled) {
|
|
const flatObjectMetadata = await this.objectMetadataServiceV2.createOne({
|
|
createObjectInput,
|
|
workspaceId,
|
|
});
|
|
|
|
const createdObjectMetadata = await this.objectMetadataRepository.findOne(
|
|
{
|
|
where: {
|
|
id: flatObjectMetadata.id,
|
|
workspaceId,
|
|
},
|
|
},
|
|
);
|
|
|
|
if (!isDefined(createdObjectMetadata)) {
|
|
throw new ObjectMetadataException(
|
|
'Created object metadata not found',
|
|
ObjectMetadataExceptionCode.OBJECT_METADATA_NOT_FOUND,
|
|
);
|
|
}
|
|
|
|
return createdObjectMetadata;
|
|
}
|
|
|
|
const queryRunner = this.coreDataSource.createQueryRunner();
|
|
|
|
await queryRunner.connect();
|
|
await queryRunner.startTransaction();
|
|
|
|
try {
|
|
const objectMetadataRepository =
|
|
queryRunner.manager.getRepository(ObjectMetadataEntity);
|
|
|
|
const { objectMetadataMaps } =
|
|
await this.workspaceMetadataCacheService.getExistingOrRecomputeMetadataMaps(
|
|
{
|
|
workspaceId,
|
|
},
|
|
);
|
|
|
|
const lastDataSourceMetadata =
|
|
await this.dataSourceService.getLastDataSourceMetadataFromWorkspaceIdOrFail(
|
|
workspaceId,
|
|
);
|
|
|
|
createObjectInput.labelSingular = capitalize(
|
|
createObjectInput.labelSingular,
|
|
);
|
|
createObjectInput.labelPlural = capitalize(createObjectInput.labelPlural);
|
|
|
|
validateObjectMetadataInputNamesOrThrow(createObjectInput);
|
|
validateObjectMetadataInputLabelsOrThrow(createObjectInput);
|
|
|
|
validateLowerCasedAndTrimmedStringsAreDifferentOrThrow({
|
|
inputs: [createObjectInput.nameSingular, createObjectInput.namePlural],
|
|
message:
|
|
'The singular and plural names cannot be the same for an object',
|
|
});
|
|
validateLowerCasedAndTrimmedStringsAreDifferentOrThrow({
|
|
inputs: [
|
|
createObjectInput.labelPlural,
|
|
createObjectInput.labelSingular,
|
|
],
|
|
message:
|
|
'The singular and plural labels cannot be the same for an object',
|
|
});
|
|
|
|
if (createObjectInput.isLabelSyncedWithName === true) {
|
|
validateNameAndLabelAreSyncOrThrow({
|
|
label: createObjectInput.labelSingular,
|
|
name: createObjectInput.nameSingular,
|
|
});
|
|
validateNameAndLabelAreSyncOrThrow({
|
|
label: createObjectInput.labelPlural,
|
|
name: createObjectInput.namePlural,
|
|
});
|
|
}
|
|
|
|
validatesNoOtherObjectWithSameNameExistsOrThrows({
|
|
objectMetadataNamePlural: createObjectInput.namePlural,
|
|
objectMetadataNameSingular: createObjectInput.nameSingular,
|
|
objectMetadataMaps,
|
|
});
|
|
|
|
const baseCustomFields = buildDefaultFieldsForCustomObject(workspaceId);
|
|
|
|
const labelIdentifierFieldMetadataId = baseCustomFields.find(
|
|
(field) => field.standardId === CUSTOM_OBJECT_STANDARD_FIELD_IDS.name,
|
|
)?.id;
|
|
|
|
if (!isDefined(labelIdentifierFieldMetadataId)) {
|
|
throw new ObjectMetadataException(
|
|
'Label identifier field metadata not created properly',
|
|
ObjectMetadataExceptionCode.MISSING_CUSTOM_OBJECT_DEFAULT_LABEL_IDENTIFIER_FIELD,
|
|
);
|
|
}
|
|
|
|
const createdObjectMetadata = await objectMetadataRepository.save({
|
|
...createObjectInput,
|
|
dataSourceId: lastDataSourceMetadata.id,
|
|
targetTableName: 'DEPRECATED',
|
|
isActive: true,
|
|
isCustom: !createObjectInput.isRemote,
|
|
isSystem: false,
|
|
isRemote: createObjectInput.isRemote,
|
|
isSearchable: !createObjectInput.isRemote,
|
|
fields: createObjectInput.isRemote ? [] : baseCustomFields,
|
|
labelIdentifierFieldMetadataId,
|
|
});
|
|
|
|
if (createObjectInput.isRemote) {
|
|
throw new Error('Remote objects are not supported yet');
|
|
} else {
|
|
const fieldsById = createdObjectMetadata.fields.reduce(
|
|
(acc, field) => ({
|
|
...acc,
|
|
[field.id]: field,
|
|
}),
|
|
{},
|
|
);
|
|
|
|
const createdRelatedObjectMetadataCollection =
|
|
await this.objectMetadataFieldRelationService.createRelationsAndForeignKeysMetadata(
|
|
workspaceId,
|
|
{ ...createdObjectMetadata, fieldsById },
|
|
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(
|
|
createObjectInput,
|
|
createdObjectMetadata,
|
|
queryRunner,
|
|
);
|
|
}
|
|
|
|
await this.workspaceMigrationRunnerService.executeMigrationFromPendingMigrationsWithinTransaction(
|
|
createdObjectMetadata.workspaceId,
|
|
queryRunner,
|
|
);
|
|
|
|
await queryRunner.commitTransaction();
|
|
|
|
// After commit, do non-transactional work
|
|
await this.workspacePermissionsCacheService.recomputeRolesPermissionsCache(
|
|
{
|
|
workspaceId,
|
|
},
|
|
);
|
|
await this.objectMetadataRelatedRecordsService.createObjectRelatedRecords(
|
|
createdObjectMetadata,
|
|
);
|
|
|
|
await this.workspaceMetadataVersionService.incrementMetadataVersion(
|
|
workspaceId,
|
|
);
|
|
|
|
await this.flatEntityMapsCacheService.invalidateFlatEntityMaps({
|
|
workspaceId,
|
|
flatMapsKeys: ['flatFieldMetadataMaps', 'flatObjectMetadataMaps'],
|
|
});
|
|
|
|
return createdObjectMetadata;
|
|
} catch (error) {
|
|
if (queryRunner.isTransactionActive) {
|
|
try {
|
|
await queryRunner.rollbackTransaction();
|
|
} catch (error) {
|
|
// eslint-disable-next-line no-console
|
|
console.trace(`Failed to rollback transaction: ${error.message}`);
|
|
}
|
|
}
|
|
throw error;
|
|
} finally {
|
|
await queryRunner.release();
|
|
}
|
|
}
|
|
|
|
public async updateOneObject(
|
|
updateObjectInput: UpdateOneObjectInput,
|
|
workspaceId: string,
|
|
): Promise<ObjectMetadataEntity> {
|
|
const queryRunner = this.coreDataSource.createQueryRunner();
|
|
|
|
await queryRunner.connect();
|
|
await queryRunner.startTransaction();
|
|
|
|
try {
|
|
const objectMetadataRepository =
|
|
queryRunner.manager.getRepository(ObjectMetadataEntity);
|
|
|
|
const { objectMetadataMaps } =
|
|
await this.workspaceMetadataCacheService.getExistingOrRecomputeMetadataMaps(
|
|
{ workspaceId },
|
|
);
|
|
const inputId = updateObjectInput.id;
|
|
const inputPayload = {
|
|
...updateObjectInput.update,
|
|
...(isDefined(updateObjectInput.update.labelSingular)
|
|
? {
|
|
labelSingular: capitalize(updateObjectInput.update.labelSingular),
|
|
}
|
|
: {}),
|
|
...(isDefined(updateObjectInput.update.labelPlural)
|
|
? { labelPlural: capitalize(updateObjectInput.update.labelPlural) }
|
|
: {}),
|
|
};
|
|
|
|
validateObjectMetadataInputNamesOrThrow(inputPayload);
|
|
const existingObjectMetadata = objectMetadataMaps.byId[inputId];
|
|
|
|
if (!existingObjectMetadata) {
|
|
throw new ObjectMetadataException(
|
|
'Object does not exist',
|
|
ObjectMetadataExceptionCode.OBJECT_METADATA_NOT_FOUND,
|
|
);
|
|
}
|
|
const existingObjectMetadataCombinedWithUpdateInput = {
|
|
...existingObjectMetadata,
|
|
...inputPayload,
|
|
};
|
|
|
|
validatesNoOtherObjectWithSameNameExistsOrThrows({
|
|
objectMetadataNameSingular:
|
|
existingObjectMetadataCombinedWithUpdateInput.nameSingular,
|
|
objectMetadataNamePlural:
|
|
existingObjectMetadataCombinedWithUpdateInput.namePlural,
|
|
existingObjectMetadataId:
|
|
existingObjectMetadataCombinedWithUpdateInput.id,
|
|
objectMetadataMaps,
|
|
});
|
|
if (existingObjectMetadataCombinedWithUpdateInput.isLabelSyncedWithName) {
|
|
validateNameAndLabelAreSyncOrThrow({
|
|
label: existingObjectMetadataCombinedWithUpdateInput.labelSingular,
|
|
name: existingObjectMetadataCombinedWithUpdateInput.nameSingular,
|
|
});
|
|
validateNameAndLabelAreSyncOrThrow({
|
|
label: existingObjectMetadataCombinedWithUpdateInput.labelPlural,
|
|
name: existingObjectMetadataCombinedWithUpdateInput.namePlural,
|
|
});
|
|
}
|
|
if (
|
|
isDefined(inputPayload.nameSingular) ||
|
|
isDefined(inputPayload.namePlural)
|
|
) {
|
|
validateLowerCasedAndTrimmedStringsAreDifferentOrThrow({
|
|
inputs: [
|
|
existingObjectMetadataCombinedWithUpdateInput.nameSingular,
|
|
existingObjectMetadataCombinedWithUpdateInput.namePlural,
|
|
],
|
|
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,
|
|
objectMetadataForUpdate:
|
|
existingObjectMetadataCombinedWithUpdateInput,
|
|
inputPayload,
|
|
queryRunner,
|
|
objectMetadataMaps,
|
|
});
|
|
|
|
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,
|
|
);
|
|
}
|
|
|
|
if (
|
|
isDefined(inputPayload.labelIdentifierFieldMetadataId) &&
|
|
inputPayload.labelIdentifierFieldMetadataId !==
|
|
existingObjectMetadata.labelIdentifierFieldMetadataId
|
|
) {
|
|
const labelIdentifierFieldMetadata =
|
|
existingObjectMetadata.fieldsById[
|
|
inputPayload.labelIdentifierFieldMetadataId
|
|
];
|
|
|
|
await this.objectMetadataRelatedRecordsService.updateLabelMetadataIdentifierInObjectViews(
|
|
{
|
|
newLabelMetadataIdentifierFieldMetadata:
|
|
labelIdentifierFieldMetadata,
|
|
},
|
|
);
|
|
}
|
|
|
|
await this.workspaceMetadataVersionService.incrementMetadataVersion(
|
|
workspaceId,
|
|
);
|
|
|
|
await this.flatEntityMapsCacheService.invalidateFlatEntityMaps({
|
|
workspaceId,
|
|
flatMapsKeys: ['flatObjectMetadataMaps', 'flatFieldMetadataMaps'],
|
|
});
|
|
|
|
const formattedUpdatedObject = {
|
|
...updatedObject,
|
|
createdAt: new Date(updatedObject.createdAt),
|
|
updatedAt: new Date(updatedObject.updatedAt),
|
|
};
|
|
|
|
return formattedUpdatedObject;
|
|
} catch (error) {
|
|
if (queryRunner.isTransactionActive) {
|
|
try {
|
|
await queryRunner.rollbackTransaction();
|
|
} catch (error) {
|
|
// eslint-disable-next-line no-console
|
|
console.trace(`Failed to rollback transaction: ${error.message}`);
|
|
}
|
|
}
|
|
throw error;
|
|
} finally {
|
|
await queryRunner.release();
|
|
}
|
|
}
|
|
|
|
public async deleteOneObject(
|
|
deleteObjectInput: DeleteOneObjectInput,
|
|
workspaceId: string,
|
|
): Promise<Partial<ObjectMetadataEntity>> {
|
|
const isWorkspaceMigrationV2Enabled =
|
|
await this.featureFlagService.isFeatureEnabled(
|
|
FeatureFlagKey.IS_WORKSPACE_MIGRATION_V2_ENABLED,
|
|
workspaceId,
|
|
);
|
|
|
|
if (isWorkspaceMigrationV2Enabled) {
|
|
return await this.objectMetadataServiceV2.deleteOne({
|
|
deleteObjectInput,
|
|
workspaceId,
|
|
});
|
|
}
|
|
|
|
const queryRunner = this.coreDataSource.createQueryRunner();
|
|
|
|
await queryRunner.connect();
|
|
await queryRunner.startTransaction();
|
|
|
|
try {
|
|
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: deleteObjectInput.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,
|
|
queryRunner,
|
|
);
|
|
|
|
const fieldMetadataIds = objectMetadata.fields.map((field) => field.id);
|
|
const relationMetadataIds = objectMetadata.fields.flatMap((field) => {
|
|
if (
|
|
isFieldMetadataEntityOfType(field, FieldMetadataType.MORPH_RELATION)
|
|
) {
|
|
return field.relationTargetFieldMetadata.id;
|
|
}
|
|
|
|
return [];
|
|
});
|
|
|
|
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,
|
|
);
|
|
|
|
await this.flatEntityMapsCacheService.invalidateFlatEntityMaps({
|
|
workspaceId,
|
|
flatMapsKeys: ['flatObjectMetadataMaps', 'flatFieldMetadataMaps'],
|
|
});
|
|
|
|
await this.workspacePermissionsCacheService.recomputeRolesPermissionsCache(
|
|
{
|
|
workspaceId,
|
|
},
|
|
);
|
|
|
|
await this.objectMetadataRelatedRecordsService.deleteObjectViews(
|
|
objectMetadata,
|
|
workspaceId,
|
|
);
|
|
|
|
return objectMetadata;
|
|
} catch (error) {
|
|
if (queryRunner.isTransactionActive) {
|
|
try {
|
|
await queryRunner.rollbackTransaction();
|
|
} catch (error) {
|
|
// eslint-disable-next-line no-console
|
|
console.trace(`Failed to rollback transaction: ${error.message}`);
|
|
}
|
|
}
|
|
throw error;
|
|
} finally {
|
|
await queryRunner.release();
|
|
}
|
|
}
|
|
|
|
public async findOneWithinWorkspace(
|
|
workspaceId: string,
|
|
options: FindOneOptions<ObjectMetadataEntity>,
|
|
): Promise<ObjectMetadataEntity | null> {
|
|
return this.objectMetadataRepository.findOne({
|
|
relations: [
|
|
'fields',
|
|
'indexMetadatas',
|
|
'indexMetadatas.indexFieldMetadatas',
|
|
],
|
|
...options,
|
|
where: {
|
|
...options.where,
|
|
workspaceId,
|
|
},
|
|
});
|
|
}
|
|
|
|
public async findManyWithinWorkspace(
|
|
workspaceId: string,
|
|
options?: FindManyOptions<ObjectMetadataEntity>,
|
|
) {
|
|
return this.objectMetadataRepository.find({
|
|
relations: [
|
|
'fields.object',
|
|
'fields',
|
|
'fields.relationTargetObjectMetadata',
|
|
],
|
|
...options,
|
|
where: {
|
|
...options?.where,
|
|
workspaceId,
|
|
},
|
|
order: {
|
|
...options?.order,
|
|
},
|
|
});
|
|
}
|
|
|
|
public async deleteObjectsMetadata(workspaceId: string) {
|
|
const objectsMetadata = await this.objectMetadataRepository.find({
|
|
where: {
|
|
workspaceId,
|
|
},
|
|
});
|
|
|
|
await this.fieldMetadataRepository.delete({
|
|
workspaceId,
|
|
type: In([FieldMetadataType.MORPH_RELATION, FieldMetadataType.RELATION]),
|
|
});
|
|
|
|
for (const objectMetadata of objectsMetadata) {
|
|
await this.objectMetadataRepository.delete({
|
|
id: objectMetadata.id,
|
|
});
|
|
}
|
|
}
|
|
|
|
private async handleObjectNameAndLabelUpdates({
|
|
existingObjectMetadata,
|
|
objectMetadataForUpdate,
|
|
inputPayload,
|
|
queryRunner,
|
|
objectMetadataMaps,
|
|
}: {
|
|
existingObjectMetadata: Pick<
|
|
ObjectMetadataItemWithFieldMaps,
|
|
'nameSingular' | 'isCustom' | 'id' | 'labelPlural' | 'icon' | 'fieldsById'
|
|
>;
|
|
objectMetadataForUpdate: Pick<
|
|
ObjectMetadataItemWithFieldMaps,
|
|
| 'nameSingular'
|
|
| 'isCustom'
|
|
| 'workspaceId'
|
|
| 'id'
|
|
| 'labelSingular'
|
|
| 'labelPlural'
|
|
| 'icon'
|
|
| 'fieldsById'
|
|
>;
|
|
inputPayload: UpdateObjectPayload;
|
|
queryRunner: QueryRunner;
|
|
objectMetadataMaps: ObjectMetadataMaps;
|
|
}): Promise<{ didUpdateLabelOrIcon: boolean }> {
|
|
const newTargetTableName = computeObjectTargetTable(
|
|
objectMetadataForUpdate,
|
|
);
|
|
const existingTargetTableName = computeObjectTargetTable(
|
|
existingObjectMetadata,
|
|
);
|
|
|
|
if (newTargetTableName !== existingTargetTableName) {
|
|
await this.objectMetadataMigrationService.createRenameTableMigration(
|
|
existingObjectMetadata,
|
|
objectMetadataForUpdate,
|
|
objectMetadataForUpdate.workspaceId,
|
|
queryRunner,
|
|
);
|
|
|
|
const relationMetadataCollection =
|
|
await this.objectMetadataFieldRelationService.updateRelationsAndForeignKeysMetadata(
|
|
{
|
|
workspaceId: objectMetadataForUpdate.workspaceId,
|
|
updatedObjectMetadata: objectMetadataForUpdate,
|
|
queryRunner,
|
|
objectMetadataMaps,
|
|
},
|
|
);
|
|
|
|
await this.objectMetadataMigrationService.updateRelationMigrations(
|
|
existingObjectMetadata,
|
|
objectMetadataForUpdate,
|
|
relationMetadataCollection,
|
|
objectMetadataForUpdate.workspaceId,
|
|
queryRunner,
|
|
);
|
|
|
|
const morphRelationFieldMetadataToUpdate =
|
|
await this.objectMetadataFieldRelationService.updateMorphRelationsJoinColumnName(
|
|
{
|
|
existingObjectMetadata,
|
|
objectMetadataForUpdate,
|
|
queryRunner,
|
|
},
|
|
);
|
|
|
|
await this.objectMetadataMigrationService.updateMorphRelationMigrations({
|
|
workspaceId: objectMetadataForUpdate.workspaceId,
|
|
morphRelationFieldMetadataToUpdate: morphRelationFieldMetadataToUpdate,
|
|
queryRunner,
|
|
});
|
|
|
|
await this.objectMetadataMigrationService.recomputeEnumNames(
|
|
objectMetadataForUpdate,
|
|
objectMetadataForUpdate.workspaceId,
|
|
queryRunner,
|
|
);
|
|
|
|
const recomputedIndexes =
|
|
await this.indexMetadataService.recomputeIndexMetadataForObject(
|
|
objectMetadataForUpdate.workspaceId,
|
|
objectMetadataForUpdate,
|
|
queryRunner,
|
|
);
|
|
|
|
await this.indexMetadataService.createIndexRecomputeMigrations(
|
|
objectMetadataForUpdate.workspaceId,
|
|
objectMetadataForUpdate,
|
|
recomputedIndexes,
|
|
queryRunner,
|
|
);
|
|
|
|
if (
|
|
(inputPayload.labelPlural || inputPayload.icon) &&
|
|
(inputPayload.labelPlural !== existingObjectMetadata.labelPlural ||
|
|
inputPayload.icon !== existingObjectMetadata.icon)
|
|
) {
|
|
return {
|
|
didUpdateLabelOrIcon: true,
|
|
};
|
|
}
|
|
}
|
|
|
|
return {
|
|
didUpdateLabelOrIcon: false,
|
|
};
|
|
}
|
|
}
|