[FlexibleSchema] Add IndexMetadata decorator (#5981)
## Context
Our Flexible Schema engine dynamically generates entities/tables/APIs
for us but was not flexible enough to build indexes in the DB. With more
and more features involving heavy queries such as Messaging, we are now
adding a new WorkspaceIndex() decorator for our standard objects (will
come later for custom objects). This decorator will give enough
information to the workspace sync metadata manager to generate the
proper migrations that will create or drop indexes on demand.
To be aligned with the rest of the engine, we are adding 2 new tables:
IndexMetadata and IndexFieldMetadata, that will store the info of our
indexes.
## Implementation
```typescript
@WorkspaceEntity({
standardId: STANDARD_OBJECT_IDS.person,
namePlural: 'people',
labelSingular: 'Person',
labelPlural: 'People',
description: 'A person',
icon: 'IconUser',
})
export class PersonWorkspaceEntity extends BaseWorkspaceEntity {
@WorkspaceField({
standardId: PERSON_STANDARD_FIELD_IDS.email,
type: FieldMetadataType.EMAIL,
label: 'Email',
description: 'Contact’s Email',
icon: 'IconMail',
})
@WorkspaceIndex()
email: string;
```
By simply adding the WorkspaceIndex decorator, sync-metadata command
will create a new index for that column.
We can also add composite indexes, note that the order is important for
PSQL.
```typescript
@WorkspaceEntity({
standardId: STANDARD_OBJECT_IDS.person,
namePlural: 'people',
labelSingular: 'Person',
labelPlural: 'People',
description: 'A person',
icon: 'IconUser',
})
@WorkspaceIndex(['phone', 'email'])
export class PersonWorkspaceEntity extends BaseWorkspaceEntity {
```
Currently composite fields and relation fields are not handled by
@WorkspaceIndex() and you will need to use this notation instead
```typescript
@WorkspaceIndex(['companyId', 'nameFirstName'])
export class PersonWorkspaceEntity extends BaseWorkspaceEntity {
```
<img width="700" alt="Screenshot 2024-06-21 at 15 15 45"
src="https://github.com/twentyhq/twenty/assets/1834158/ac6da1d9-d315-40a4-9ba6-6ab9ae4709d4">
Next step: We might need to implement more complex index expressions,
this is why we have an expression column in IndexMetadata.
What I had in mind for the decorator, still open to discussion
```typescript
@WorkspaceIndex(['nameFirstName', 'nameLastName'], { expression: "$1 || ' ' || $2"})
export class PersonWorkspaceEntity extends BaseWorkspaceEntity {
```
---------
Co-authored-by: Charles Bochet <charles@twenty.com>
This commit is contained in:
@ -11,6 +11,7 @@ import { v4 as uuidV4 } from 'uuid';
|
||||
import { DeepPartial } from 'typeorm/common/DeepPartial';
|
||||
|
||||
import { PartialFieldMetadata } from 'src/engine/workspace-manager/workspace-sync-metadata/interfaces/partial-field-metadata.interface';
|
||||
import { PartialIndexMetadata } from 'src/engine/workspace-manager/workspace-sync-metadata/interfaces/partial-index-metadata.interface';
|
||||
|
||||
import { ObjectMetadataEntity } from 'src/engine/metadata-modules/object-metadata/object-metadata.entity';
|
||||
import {
|
||||
@ -22,6 +23,7 @@ import { FieldMetadataComplexOption } from 'src/engine/metadata-modules/field-me
|
||||
import { WorkspaceSyncStorage } from 'src/engine/workspace-manager/workspace-sync-metadata/storage/workspace-sync.storage';
|
||||
import { FieldMetadataUpdate } from 'src/engine/workspace-manager/workspace-migration-builder/factories/workspace-migration-field.factory';
|
||||
import { ObjectMetadataUpdate } from 'src/engine/workspace-manager/workspace-migration-builder/factories/workspace-migration-object.factory';
|
||||
import { IndexMetadataEntity } from 'src/engine/metadata-modules/index-metadata/index-metadata.entity';
|
||||
|
||||
@Injectable()
|
||||
export class WorkspaceMetadataUpdaterService {
|
||||
@ -230,6 +232,69 @@ export class WorkspaceMetadataUpdaterService {
|
||||
};
|
||||
}
|
||||
|
||||
async updateIndexMetadata(
|
||||
manager: EntityManager,
|
||||
storage: WorkspaceSyncStorage,
|
||||
originalObjectMetadataCollection: ObjectMetadataEntity[],
|
||||
): Promise<{
|
||||
createdIndexMetadataCollection: IndexMetadataEntity[];
|
||||
}> {
|
||||
const indexMetadataRepository = manager.getRepository(IndexMetadataEntity);
|
||||
|
||||
const convertIndexMetadataForSaving = (
|
||||
indexMetadata: PartialIndexMetadata,
|
||||
) => {
|
||||
const convertIndexFieldMetadataForSaving = (
|
||||
column: string,
|
||||
order: number,
|
||||
) => {
|
||||
const fieldMetadata = originalObjectMetadataCollection
|
||||
.find((object) => object.id === indexMetadata.objectMetadataId)
|
||||
?.fields.find((field) => column === field.name);
|
||||
|
||||
if (!fieldMetadata) {
|
||||
throw new Error(`
|
||||
Field metadata not found for column ${column} in object ${indexMetadata.objectMetadataId}
|
||||
`);
|
||||
}
|
||||
|
||||
return {
|
||||
fieldMetadataId: fieldMetadata.id,
|
||||
order,
|
||||
};
|
||||
};
|
||||
|
||||
return {
|
||||
...indexMetadata,
|
||||
indexFieldMetadatas: indexMetadata.columns.map((column, index) =>
|
||||
convertIndexFieldMetadataForSaving(column, index),
|
||||
),
|
||||
};
|
||||
};
|
||||
|
||||
/**
|
||||
* Create index metadata
|
||||
*/
|
||||
const createdIndexMetadataCollection = await indexMetadataRepository.save(
|
||||
storage.indexMetadataCreateCollection.map(convertIndexMetadataForSaving),
|
||||
);
|
||||
|
||||
/**
|
||||
* Delete index metadata
|
||||
*/
|
||||
if (storage.indexMetadataDeleteCollection.length > 0) {
|
||||
await indexMetadataRepository.delete(
|
||||
storage.indexMetadataDeleteCollection.map(
|
||||
(indexMetadata) => indexMetadata.id,
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
return {
|
||||
createdIndexMetadataCollection,
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Update entities in the database
|
||||
* @param manager EntityManager
|
||||
|
||||
@ -48,7 +48,7 @@ export class WorkspaceSyncFieldMetadataService {
|
||||
relations: ['dataSource', 'fields'],
|
||||
});
|
||||
|
||||
// Filter out custom objects
|
||||
// Filter out non-custom objects
|
||||
const customObjectMetadataCollection =
|
||||
originalObjectMetadataCollection.filter(
|
||||
(objectMetadata) => objectMetadata.isCustom,
|
||||
@ -61,7 +61,7 @@ export class WorkspaceSyncFieldMetadataService {
|
||||
workspaceFeatureFlagsMap,
|
||||
);
|
||||
|
||||
// Loop over all standard objects and compare them with the objects in DB
|
||||
// Loop over all custom objects from the DB and compare their fields with standard fields
|
||||
for (const customObjectMetadata of customObjectMetadataCollection) {
|
||||
// Also, maybe it's better to refactor a bit and move generation part into a separate module ?
|
||||
const standardObjectMetadata = computeStandardObject(
|
||||
|
||||
@ -0,0 +1,119 @@
|
||||
import { Injectable, Logger } from '@nestjs/common';
|
||||
|
||||
import { EntityManager } from 'typeorm';
|
||||
|
||||
import { FeatureFlagMap } from 'src/engine/core-modules/feature-flag/interfaces/feature-flag-map.interface';
|
||||
import { WorkspaceSyncContext } from 'src/engine/workspace-manager/workspace-sync-metadata/interfaces/workspace-sync-context.interface';
|
||||
import { ComparatorAction } from 'src/engine/workspace-manager/workspace-sync-metadata/interfaces/comparator.interface';
|
||||
import { WorkspaceMigrationBuilderAction } from 'src/engine/workspace-manager/workspace-migration-builder/interfaces/workspace-migration-builder-action.interface';
|
||||
|
||||
import { WorkspaceMigrationEntity } from 'src/engine/metadata-modules/workspace-migration/workspace-migration.entity';
|
||||
import { WorkspaceSyncStorage } from 'src/engine/workspace-manager/workspace-sync-metadata/storage/workspace-sync.storage';
|
||||
import { StandardIndexFactory } from 'src/engine/workspace-manager/workspace-sync-metadata/factories/standard-index.factory';
|
||||
import { ObjectMetadataEntity } from 'src/engine/metadata-modules/object-metadata/object-metadata.entity';
|
||||
import { mapObjectMetadataByUniqueIdentifier } from 'src/engine/workspace-manager/workspace-sync-metadata/utils/sync-metadata.util';
|
||||
import { IndexMetadataEntity } from 'src/engine/metadata-modules/index-metadata/index-metadata.entity';
|
||||
import { standardObjectMetadataDefinitions } from 'src/engine/workspace-manager/workspace-sync-metadata/standard-objects';
|
||||
import { WorkspaceIndexComparator } from 'src/engine/workspace-manager/workspace-sync-metadata/comparators/workspace-index.comparator';
|
||||
import { WorkspaceMetadataUpdaterService } from 'src/engine/workspace-manager/workspace-sync-metadata/services/workspace-metadata-updater.service';
|
||||
import { WorkspaceMigrationIndexFactory } from 'src/engine/workspace-manager/workspace-migration-builder/factories/workspace-migration-index.factory';
|
||||
|
||||
@Injectable()
|
||||
export class WorkspaceSyncIndexMetadataService {
|
||||
private readonly logger = new Logger(WorkspaceSyncIndexMetadataService.name);
|
||||
|
||||
constructor(
|
||||
private readonly standardIndexFactory: StandardIndexFactory,
|
||||
private readonly workspaceIndexComparator: WorkspaceIndexComparator,
|
||||
private readonly workspaceMetadataUpdaterService: WorkspaceMetadataUpdaterService,
|
||||
private readonly workspaceMigrationIndexFactory: WorkspaceMigrationIndexFactory,
|
||||
) {}
|
||||
|
||||
async synchronize(
|
||||
context: WorkspaceSyncContext,
|
||||
manager: EntityManager,
|
||||
storage: WorkspaceSyncStorage,
|
||||
workspaceFeatureFlagsMap: FeatureFlagMap,
|
||||
): Promise<Partial<WorkspaceMigrationEntity>[]> {
|
||||
this.logger.log('Syncing index metadata');
|
||||
|
||||
const objectMetadataRepository =
|
||||
manager.getRepository(ObjectMetadataEntity);
|
||||
|
||||
// Retrieve object metadata collection from DB
|
||||
const originalObjectMetadataCollection =
|
||||
await objectMetadataRepository.find({
|
||||
where: {
|
||||
workspaceId: context.workspaceId,
|
||||
// We're only interested in standard fields
|
||||
fields: { isCustom: false },
|
||||
isCustom: false,
|
||||
},
|
||||
relations: ['dataSource', 'fields', 'indexes'],
|
||||
});
|
||||
|
||||
// Create map of object metadata & field metadata by unique identifier
|
||||
const originalObjectMetadataMap = mapObjectMetadataByUniqueIdentifier(
|
||||
originalObjectMetadataCollection,
|
||||
// Relation are based on the singular name
|
||||
(objectMetadata) => objectMetadata.nameSingular,
|
||||
);
|
||||
|
||||
const indexMetadataRepository = manager.getRepository(IndexMetadataEntity);
|
||||
|
||||
const originalIndexMetadataCollection = await indexMetadataRepository.find({
|
||||
where: {
|
||||
workspaceId: context.workspaceId,
|
||||
},
|
||||
relations: ['indexFieldMetadatas.fieldMetadata'],
|
||||
});
|
||||
|
||||
// Generate index metadata from models
|
||||
const standardIndexMetadataCollection = this.standardIndexFactory.create(
|
||||
standardObjectMetadataDefinitions,
|
||||
context,
|
||||
originalObjectMetadataMap,
|
||||
workspaceFeatureFlagsMap,
|
||||
);
|
||||
|
||||
const indexComparatorResults = this.workspaceIndexComparator.compare(
|
||||
originalIndexMetadataCollection,
|
||||
standardIndexMetadataCollection,
|
||||
);
|
||||
|
||||
for (const indexComparatorResult of indexComparatorResults) {
|
||||
if (indexComparatorResult.action === ComparatorAction.CREATE) {
|
||||
storage.addCreateIndexMetadata(indexComparatorResult.object);
|
||||
} else if (indexComparatorResult.action === ComparatorAction.DELETE) {
|
||||
storage.addDeleteIndexMetadata(indexComparatorResult.object);
|
||||
}
|
||||
}
|
||||
|
||||
const metadataIndexUpdaterResult =
|
||||
await this.workspaceMetadataUpdaterService.updateIndexMetadata(
|
||||
manager,
|
||||
storage,
|
||||
originalObjectMetadataCollection,
|
||||
);
|
||||
|
||||
// Create migrations
|
||||
const createIndexWorkspaceMigrations =
|
||||
await this.workspaceMigrationIndexFactory.create(
|
||||
originalObjectMetadataCollection,
|
||||
metadataIndexUpdaterResult.createdIndexMetadataCollection,
|
||||
WorkspaceMigrationBuilderAction.CREATE,
|
||||
);
|
||||
|
||||
const deleteIndexWorkspaceMigrations =
|
||||
await this.workspaceMigrationIndexFactory.create(
|
||||
originalObjectMetadataCollection,
|
||||
storage.indexMetadataDeleteCollection,
|
||||
WorkspaceMigrationBuilderAction.DELETE,
|
||||
);
|
||||
|
||||
return [
|
||||
...createIndexWorkspaceMigrations,
|
||||
...deleteIndexWorkspaceMigrations,
|
||||
];
|
||||
}
|
||||
}
|
||||
@ -112,6 +112,10 @@ export class WorkspaceSyncObjectMetadataService {
|
||||
|
||||
/**
|
||||
* COMPARE FIELD METADATA
|
||||
* NOTE: This should be moved to WorkspaceSyncFieldMetadataService for more clarity since
|
||||
* this code only adds field metadata to the storage but it's actually used in the other service.
|
||||
* NOTE2: WorkspaceSyncFieldMetadataService has been added for custom fields sync, it should be refactored to handle
|
||||
* both custom and non-custom fields.
|
||||
*/
|
||||
const fieldComparatorResults = this.workspaceFieldComparator.compare(
|
||||
originalObjectMetadata,
|
||||
|
||||
Reference in New Issue
Block a user