Skip to content

Commit

Permalink
Add label identifier to object decorator (#6227)
Browse files Browse the repository at this point in the history
## Context
LabelIdentifier and ImageIdentifier are metadata info attached to
objectMetadata that are used to display a record in a more readable way.
Those columns point to existing fields that are part of the object.
For example, for a relation picker of a person, we will show a record
using the "name" labelIdentifier and the "avatarUrl" imageIdentifier.
<img width="215" alt="Screenshot 2024-07-11 at 18 45 51"
src="https://github.com/twentyhq/twenty/assets/1834158/488f8294-0d7c-4209-b763-2499716ef29d">

Currently, the FE has a specific logic for company and people objects
and we have a way to update this value via the API for custom objects,
but the code is not flexible enough to change other standard objects.

This PR updates the WorkspaceEntity API so we can now provide the
labelIdentifier and imageIdentifier in the WorkspaceEntity decorator.

Example:
```typescript
@WorkspaceEntity({
  standardId: STANDARD_OBJECT_IDS.activity,
  namePlural: 'activities',
  labelSingular: 'Activity',
  labelPlural: 'Activities',
  description: 'An activity',
  icon: 'IconCheckbox',
  labelIdentifierStandardId: ACTIVITY_STANDARD_FIELD_IDS.title,
})
@WorkspaceIsSystem()
export class ActivityWorkspaceEntity extends BaseWorkspaceEntity {
  @WorkspaceField({
    standardId: ACTIVITY_STANDARD_FIELD_IDS.title,
    type: FieldMetadataType.TEXT,
    label: 'Title',
    description: 'Activity title',
    icon: 'IconNotes',
  })
  title: string;
...
```
  • Loading branch information
Weiko authored Jul 19, 2024
1 parent 8a1af3a commit 67e2d5c
Show file tree
Hide file tree
Showing 29 changed files with 351 additions and 106 deletions.
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
import { MigrationInterface, QueryRunner } from 'typeorm';

export class FixIdentifierTypes1721057142509 implements MigrationInterface {
name = 'FixIdentifierTypes1721057142509';

public async up(queryRunner: QueryRunner): Promise<void> {
await queryRunner.query(
`ALTER TABLE "metadata"."objectMetadata" ALTER COLUMN "labelIdentifierFieldMetadataId" TYPE uuid USING "labelIdentifierFieldMetadataId"::uuid`,
);
await queryRunner.query(
`ALTER TABLE "metadata"."objectMetadata" ALTER COLUMN "imageIdentifierFieldMetadataId" TYPE uuid USING "imageIdentifierFieldMetadataId"::uuid`,
);
}

public async down(queryRunner: QueryRunner): Promise<void> {
await queryRunner.query(
`ALTER TABLE "metadata"."objectMetadata" ALTER COLUMN "labelIdentifierFieldMetadataId" TYPE text USING "labelIdentifierFieldMetadataId"::text`,
);
await queryRunner.query(
`ALTER TABLE "metadata"."objectMetadata" ALTER COLUMN "imageIdentifierFieldMetadataId" TYPE text USING "imageIdentifierFieldMetadataId"::text`,
);
}
}
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import { RelationMetadataInterface } from './relation-metadata.interface';
import { FieldMetadataInterface } from './field-metadata.interface';
import { RelationMetadataInterface } from './relation-metadata.interface';

export interface ObjectMetadataInterface {
id: string;
Expand All @@ -18,4 +18,6 @@ export interface ObjectMetadataInterface {
isActive: boolean;
isRemote: boolean;
isAuditLogged: boolean;
labelIdentifierFieldMetadataId?: string | null;
imageIdentifierFieldMetadataId?: string | null;
}
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { ObjectType, Field, HideField } from '@nestjs/graphql';
import { Field, HideField, ObjectType } from '@nestjs/graphql';

import {
Authorize,
Expand Down Expand Up @@ -72,9 +72,9 @@ export class ObjectMetadataDTO {
@Field()
updatedAt: Date;

@Field({ nullable: true })
labelIdentifierFieldMetadataId?: string;
@Field(() => String, { nullable: true })
labelIdentifierFieldMetadataId?: string | null;

@Field({ nullable: true })
imageIdentifierFieldMetadataId?: string;
@Field(() => String, { nullable: true })
imageIdentifierFieldMetadataId?: string | null;
}
Original file line number Diff line number Diff line change
@@ -1,21 +1,21 @@
import {
Entity,
Unique,
PrimaryGeneratedColumn,
Column,
OneToMany,
CreateDateColumn,
UpdateDateColumn,
Entity,
ManyToOne,
OneToMany,
PrimaryGeneratedColumn,
Relation,
Unique,
UpdateDateColumn,
} from 'typeorm';

import { ObjectMetadataInterface } from 'src/engine/metadata-modules/field-metadata/interfaces/object-metadata.interface';

import { FieldMetadataEntity } from 'src/engine/metadata-modules/field-metadata/field-metadata.entity';
import { RelationMetadataEntity } from 'src/engine/metadata-modules/relation-metadata/relation-metadata.entity';
import { DataSourceEntity } from 'src/engine/metadata-modules/data-source/data-source.entity';
import { FieldMetadataEntity } from 'src/engine/metadata-modules/field-metadata/field-metadata.entity';
import { IndexMetadataEntity } from 'src/engine/metadata-modules/index-metadata/index-metadata.entity';
import { RelationMetadataEntity } from 'src/engine/metadata-modules/relation-metadata/relation-metadata.entity';

@Entity('objectMetadata')
@Unique('IndexOnNameSingularAndWorkspaceIdUnique', [
Expand Down Expand Up @@ -69,11 +69,11 @@ export class ObjectMetadataEntity implements ObjectMetadataInterface {
@Column({ default: true })
isAuditLogged: boolean;

@Column({ nullable: true })
labelIdentifierFieldMetadataId?: string;
@Column({ nullable: true, type: 'uuid' })
labelIdentifierFieldMetadataId?: string | null;

@Column({ nullable: true })
imageIdentifierFieldMetadataId?: string;
@Column({ nullable: true, type: 'uuid' })
imageIdentifierFieldMetadataId?: string | null;

@Column({ nullable: false, type: 'uuid' })
workspaceId: string;
Expand Down
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import { metadataArgsStorage } from 'src/engine/twenty-orm/storage/metadata-args.storage';
import { BASE_OBJECT_STANDARD_FIELD_IDS } from 'src/engine/workspace-manager/workspace-sync-metadata/constants/standard-field-ids';
import { convertClassNameToObjectMetadataName } from 'src/engine/workspace-manager/workspace-sync-metadata/utils/convert-class-to-object-metadata-name.util';
import { TypedReflect } from 'src/utils/typed-reflect';

Expand All @@ -9,6 +10,8 @@ interface WorkspaceEntityOptions {
labelPlural: string;
description?: string;
icon?: string;
labelIdentifierStandardId?: string;
imageIdentifierStandardId?: string;
}

export function WorkspaceEntity(
Expand Down Expand Up @@ -37,6 +40,9 @@ export function WorkspaceEntity(
labelSingular: options.labelSingular,
labelPlural: options.labelPlural,
description: options.description,
labelIdentifierStandardId:
options.labelIdentifierStandardId ?? BASE_OBJECT_STANDARD_FIELD_IDS.id,
imageIdentifierStandardId: options.imageIdentifierStandardId ?? null,
icon: options.icon,
isAuditLogged,
isSystem,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -50,4 +50,16 @@ export interface WorkspaceEntityMetadataArgs {
* Entity gate.
*/
readonly gate?: Gate;

/**
* Label identifier.
*/

readonly labelIdentifierStandardId: string | null;

/**
* Image identifier.
*/

readonly imageIdentifierStandardId: string | null;
}
Original file line number Diff line number Diff line change
@@ -1,9 +1,9 @@
import { ObjectMetadataInterface } from 'src/engine/metadata-modules/field-metadata/interfaces/object-metadata.interface';
import {
ComputedPartialFieldMetadata,
PartialComputedFieldMetadata,
PartialFieldMetadata,
} from 'src/engine/workspace-manager/workspace-sync-metadata/interfaces/partial-field-metadata.interface';
import { ObjectMetadataInterface } from 'src/engine/metadata-modules/field-metadata/interfaces/object-metadata.interface';

export type PartialWorkspaceEntity = Omit<
ObjectMetadataInterface,
Expand All @@ -14,6 +14,8 @@ export type PartialWorkspaceEntity = Omit<
workspaceId: string;
dataSourceId: string;
fields: (PartialFieldMetadata | PartialComputedFieldMetadata)[];
labelIdentifierStandardId?: string | null;
imageIdentifierStandardId?: string | null;
};

export type ComputedPartialWorkspaceEntity = Omit<
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,169 @@
import { Injectable, Logger } from '@nestjs/common';

import { EntityManager, Repository } 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 {
FieldMetadataEntity,
FieldMetadataType,
} from 'src/engine/metadata-modules/field-metadata/field-metadata.entity';
import { ObjectMetadataEntity } from 'src/engine/metadata-modules/object-metadata/object-metadata.entity';
import { StandardObjectFactory } from 'src/engine/workspace-manager/workspace-sync-metadata/factories/standard-object.factory';
import { standardObjectMetadataDefinitions } from 'src/engine/workspace-manager/workspace-sync-metadata/standard-objects';
import { WorkspaceSyncStorage } from 'src/engine/workspace-manager/workspace-sync-metadata/storage/workspace-sync.storage';
import { mapObjectMetadataByUniqueIdentifier } from 'src/engine/workspace-manager/workspace-sync-metadata/utils/sync-metadata.util';

@Injectable()
export class WorkspaceSyncObjectMetadataIdentifiersService {
private readonly logger = new Logger(
WorkspaceSyncObjectMetadataIdentifiersService.name,
);

constructor(private readonly standardObjectFactory: StandardObjectFactory) {}

async synchronize(
context: WorkspaceSyncContext,
manager: EntityManager,
_storage: WorkspaceSyncStorage,
workspaceFeatureFlagsMap: FeatureFlagMap,
): Promise<void> {
const objectMetadataRepository =
manager.getRepository(ObjectMetadataEntity);

const originalObjectMetadataCollection =
await this.getOriginalObjectMetadataCollection(
context.workspaceId,
objectMetadataRepository,
);

const standardObjectMetadataMap = this.createStandardObjectMetadataMap(
context,
workspaceFeatureFlagsMap,
);

await this.processObjectMetadataCollection(
originalObjectMetadataCollection,
standardObjectMetadataMap,
objectMetadataRepository,
);
}

private async getOriginalObjectMetadataCollection(
workspaceId: string,
objectMetadataRepository: Repository<ObjectMetadataEntity>,
): Promise<ObjectMetadataEntity[]> {
return await objectMetadataRepository.find({
where: { workspaceId, isCustom: false },
relations: ['fields'],
});
}

private createStandardObjectMetadataMap(
context: WorkspaceSyncContext,
workspaceFeatureFlagsMap: FeatureFlagMap,
): Record<string, any> {
const standardObjectMetadataCollection = this.standardObjectFactory.create(
standardObjectMetadataDefinitions,
context,
workspaceFeatureFlagsMap,
);

return mapObjectMetadataByUniqueIdentifier(
standardObjectMetadataCollection,
);
}

private async processObjectMetadataCollection(
originalObjectMetadataCollection: ObjectMetadataEntity[],
standardObjectMetadataMap: Record<string, any>,
objectMetadataRepository: Repository<ObjectMetadataEntity>,
): Promise<void> {
for (const objectMetadata of originalObjectMetadataCollection) {
const objectStandardId = objectMetadata.standardId;

if (!objectStandardId) {
throw new Error(
`Object ${objectMetadata.nameSingular} is missing standardId`,
);
}

const labelIdentifierFieldMetadata = this.findIdentifierFieldMetadata(
objectMetadata,
objectStandardId,
standardObjectMetadataMap,
'labelIdentifierStandardId',
);

const imageIdentifierFieldMetadata = this.findIdentifierFieldMetadata(
objectMetadata,
objectStandardId,
standardObjectMetadataMap,
'imageIdentifierStandardId',
);

this.validateFieldMetadata(
objectMetadata,
labelIdentifierFieldMetadata,
imageIdentifierFieldMetadata,
);

// TODO: Add image identifier field metadata
await objectMetadataRepository.save({
...objectMetadata,
labelIdentifierFieldMetadataId:
labelIdentifierFieldMetadata?.id ?? null,
});
}
}

private findIdentifierFieldMetadata(
objectMetadata: ObjectMetadataEntity,
objectStandardId: string,
standardObjectMetadataMap: Record<string, any>,
standardIdFieldName: string,
): FieldMetadataEntity | undefined {
const identifierFieldMetadata = objectMetadata.fields.find(
(field) =>
field.standardId ===
standardObjectMetadataMap[objectStandardId][standardIdFieldName],
);

if (
!identifierFieldMetadata &&
standardObjectMetadataMap[objectStandardId][standardIdFieldName]
) {
throw new Error(
`Identifier field for object ${objectMetadata.nameSingular} does not exist`,
);
}

return identifierFieldMetadata;
}

private validateFieldMetadata(
objectMetadata: ObjectMetadataEntity,
labelIdentifierFieldMetadata: FieldMetadataEntity | undefined,
imageIdentifierFieldMetadata: FieldMetadataEntity | undefined,
): void {
if (
labelIdentifierFieldMetadata &&
![
FieldMetadataType.UUID,
FieldMetadataType.TEXT,
FieldMetadataType.FULL_NAME,
].includes(labelIdentifierFieldMetadata.type)
) {
throw new Error(
`Label identifier field for object ${objectMetadata.nameSingular} has invalid type ${labelIdentifierFieldMetadata.type}`,
);
}

if (imageIdentifierFieldMetadata) {
throw new Error(
`Image identifier field for object ${objectMetadata.nameSingular} are not supported yet.`,
);
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -2,20 +2,20 @@ import { Injectable, Logger } from '@nestjs/common';

import { EntityManager } from 'typeorm';

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 { FeatureFlagMap } from 'src/engine/core-modules/feature-flag/interfaces/feature-flag-map.interface';
import { WorkspaceMigrationBuilderAction } from 'src/engine/workspace-manager/workspace-migration-builder/interfaces/workspace-migration-builder-action.interface';
import { ComparatorAction } from 'src/engine/workspace-manager/workspace-sync-metadata/interfaces/comparator.interface';
import { WorkspaceSyncContext } from 'src/engine/workspace-manager/workspace-sync-metadata/interfaces/workspace-sync-context.interface';

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 { WorkspaceMigrationEntity } from 'src/engine/metadata-modules/workspace-migration/workspace-migration.entity';
import { StandardObjectFactory } from 'src/engine/workspace-manager/workspace-sync-metadata/factories/standard-object.factory';
import { WorkspaceMigrationObjectFactory } from 'src/engine/workspace-manager/workspace-migration-builder/factories/workspace-migration-object.factory';
import { WorkspaceObjectComparator } from 'src/engine/workspace-manager/workspace-sync-metadata/comparators/workspace-object.comparator';
import { StandardObjectFactory } from 'src/engine/workspace-manager/workspace-sync-metadata/factories/standard-object.factory';
import { WorkspaceMetadataUpdaterService } from 'src/engine/workspace-manager/workspace-sync-metadata/services/workspace-metadata-updater.service';
import { WorkspaceSyncStorage } from 'src/engine/workspace-manager/workspace-sync-metadata/storage/workspace-sync.storage';
import { WorkspaceMigrationObjectFactory } from 'src/engine/workspace-manager/workspace-migration-builder/factories/workspace-migration-object.factory';
import { standardObjectMetadataDefinitions } from 'src/engine/workspace-manager/workspace-sync-metadata/standard-objects';
import { WorkspaceSyncStorage } from 'src/engine/workspace-manager/workspace-sync-metadata/storage/workspace-sync.storage';
import { mapObjectMetadataByUniqueIdentifier } from 'src/engine/workspace-manager/workspace-sync-metadata/utils/sync-metadata.util';

@Injectable()
export class WorkspaceSyncObjectMetadataService {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ import { workspaceSyncMetadataFactories } from 'src/engine/workspace-manager/wor
import { WorkspaceMetadataUpdaterService } from 'src/engine/workspace-manager/workspace-sync-metadata/services/workspace-metadata-updater.service';
import { WorkspaceSyncFieldMetadataService } from 'src/engine/workspace-manager/workspace-sync-metadata/services/workspace-sync-field-metadata.service';
import { WorkspaceSyncIndexMetadataService } from 'src/engine/workspace-manager/workspace-sync-metadata/services/workspace-sync-index-metadata.service';
import { WorkspaceSyncObjectMetadataIdentifiersService } from 'src/engine/workspace-manager/workspace-sync-metadata/services/workspace-sync-object-metadata-identifiers.service';
import { WorkspaceSyncObjectMetadataService } from 'src/engine/workspace-manager/workspace-sync-metadata/services/workspace-sync-object-metadata.service';
import { WorkspaceSyncRelationMetadataService } from 'src/engine/workspace-manager/workspace-sync-metadata/services/workspace-sync-relation-metadata.service';
import { WorkspaceSyncMetadataService } from 'src/engine/workspace-manager/workspace-sync-metadata/workspace-sync-metadata.service';
Expand All @@ -39,6 +40,7 @@ import { WorkspaceSyncMetadataService } from 'src/engine/workspace-manager/works
...workspaceSyncMetadataComparators,
WorkspaceMetadataUpdaterService,
WorkspaceSyncObjectMetadataService,
WorkspaceSyncObjectMetadataIdentifiersService,
WorkspaceSyncRelationMetadataService,
WorkspaceSyncFieldMetadataService,
WorkspaceSyncMetadataService,
Expand Down
Loading

0 comments on commit 67e2d5c

Please sign in to comment.