[BREAKING_CHANGE] Deprecate remaining entities standardId (#17639)

# Introduction
Following https://github.com/twentyhq/twenty/pull/17632 and
https://github.com/twentyhq/twenty/pull/17572
This PR deprecates the agent, skill, field metadata and role
`standardId` in favor of the `universalIdentifier` usage

## Note
- Removed previous standard ids declaration modules
- Twenty-sdk now re-exports the `STANDARD_OBJECTS` universalIdentifier
hashmap constant
- deleted some sync-metadata deadcode too ( mainly types )
This commit is contained in:
Paul Rastoin
2026-02-03 09:06:24 +01:00
committed by GitHub
parent cfdcc0065c
commit d35d5c0463
118 changed files with 534 additions and 2279 deletions
@@ -155,7 +155,6 @@ describe('WorkspaceEntityManager', () => {
isUnique: false,
options: null,
settings: null,
standardId: null,
standardOverrides: null,
workspaceId: 'test-workspace-id',
viewFieldIds: [],
@@ -1,11 +0,0 @@
import { type FactoryProvider, type ModuleMetadata } from '@nestjs/common';
export interface TwentyORMOptions {
[key: string]: unknown;
}
export type TwentyORMModuleAsyncOptions = {
// eslint-disable-next-line @typescript-eslint/no-explicit-any
useFactory: (...args: any[]) => TwentyORMOptions | Promise<TwentyORMOptions>;
} & Pick<ModuleMetadata, 'imports'> &
Pick<FactoryProvider, 'inject'>;
@@ -1,94 +0,0 @@
import { type ObjectType } from 'typeorm';
import { type RelationOnDeleteAction } from 'twenty-shared/types';
import { type RelationType } from 'src/engine/metadata-modules/field-metadata/interfaces/relation-type.interface';
import { type Gate } from 'src/engine/twenty-orm/interfaces/gate.interface';
import { type ObjectMetadataEntity } from 'src/engine/metadata-modules/object-metadata/object-metadata.entity';
export type WorkspaceDynamicRelationMetadataArgsFactory = (
oppositeObjectMetadata: ObjectMetadataEntity,
) => {
/**
* Standard id.
*/
readonly standardId: string;
/**
* Relation name.
*/
readonly name: string;
/**
* Relation label.
*/
readonly label: string;
/**
* Relation description.
*/
readonly description?: string;
/**
* Relation icon.
*/
readonly icon?: string;
/**
* Relation join column.
*/
readonly joinColumn?: string;
};
export interface WorkspaceDynamicRelationMetadataArgs {
/**
* Class to which relation is applied.
*/
readonly target: Function;
/**
* Factory function
*/
readonly argsFactory: WorkspaceDynamicRelationMetadataArgsFactory;
/**
* Relation type.
*/
readonly type: RelationType;
/**
* Relation inverse side target.
*/
readonly inverseSideTarget: () => ObjectType<object>;
/**
* Relation inverse side field key.
*/
readonly inverseSideFieldKey?: string;
/**
* Relation on delete action.
*/
readonly onDelete?: RelationOnDeleteAction;
/**
* Is primary field.
*/
readonly isPrimary: boolean;
/**
* Is system field.
*/
readonly isSystem: boolean;
/**
* Is nullable field.
*/
readonly isNullable: boolean;
/**
* Field gate.
*/
readonly gate?: Gate;
}
@@ -1,85 +0,0 @@
import { type Gate } from 'src/engine/twenty-orm/interfaces/gate.interface';
import { type WorkspaceEntityDuplicateCriteria } from 'src/engine/api/graphql/workspace-query-builder/types/workspace-entity-duplicate-criteria.type';
export interface WorkspaceEntityMetadataArgs {
/**
* Standard id.
*/
readonly standardId: string;
/**
* Class to which table is applied.
* Function target is a table defined in the class.
* String target is a table defined in a json schema.
*/
readonly target: Function;
/**
* Entity name.
*/
readonly nameSingular: string;
readonly namePlural: string;
/**
* Entity label.
*/
readonly labelSingular: string;
readonly labelPlural: string;
/**
* Entity description.
*/
readonly description?: string;
/**
* Entity icon.
*/
readonly icon?: string;
/**
* Entity shortcut.
*/
readonly shortcut?: string;
/**
* Is audit logged.
*/
readonly isAuditLogged: boolean;
/**
* Is system object.
*/
readonly isSystem: boolean;
/**
* Is UI read-only object.
*/
readonly isUIReadOnly: boolean;
/**
* Entity gate.
*/
readonly gate?: Gate;
/**
* Label identifier.
*/
readonly labelIdentifierStandardId: string | null;
/**
* Image identifier.
*/
readonly imageIdentifierStandardId: string | null;
/**
* Duplicate criteria.
*/
readonly duplicateCriteria?: WorkspaceEntityDuplicateCriteria[];
/**
* Is searchable object.
*/
readonly isSearchable: boolean;
}
@@ -1,16 +0,0 @@
import { type Gate } from 'src/engine/twenty-orm/interfaces/gate.interface';
export interface WorkspaceExtendedEntityMetadataArgs {
/**
* Class to which table is applied.
* Function target is a table defined in the class.
* String target is a table defined in a json schema.
*/
readonly target: Function;
/**
* Entity gate.
*/
readonly gate?: Gate;
}
@@ -1,117 +0,0 @@
import {
type FieldMetadataOptions,
type FieldMetadataSettings,
type FieldMetadataType,
type FieldMetadataDefaultValue,
} from 'twenty-shared/types';
import { type Gate } from 'src/engine/twenty-orm/interfaces/gate.interface';
import { type ObjectMetadataEntity } from 'src/engine/metadata-modules/object-metadata/object-metadata.entity';
export interface WorkspaceFieldMetadataArgs {
/**
* Standard id.
*/
readonly standardId: string;
/**
* Class to which field is applied.
*/
readonly target: Function;
/**
* Field name.
*/
readonly name: string;
/**
* Field label.
*/
readonly label: string | ((objectMetadata: ObjectMetadataEntity) => string);
/**
* Field type.
*/
readonly type: FieldMetadataType;
/**
* Field description.
*/
readonly description?:
| string
| ((objectMetadata: ObjectMetadataEntity) => string);
/**
* Field icon.
*/
readonly icon?: string;
/**
* Field default value.
*/
readonly defaultValue?: FieldMetadataDefaultValue;
/**
* Field options.
*/
readonly options?: FieldMetadataOptions;
/**
* Field settings.
*/
readonly settings?: FieldMetadataSettings;
/**
* Is primary field.
*/
readonly isPrimary: boolean;
/**
* Is system field.
*/
readonly isSystem: boolean;
/**
* Is UI read-only field.
*/
readonly isUIReadOnly: boolean;
/**
* Is nullable field.
*/
readonly isNullable: boolean;
/**
* Is unique field.
*/
readonly isUnique: boolean;
/**
* Field gate.
*/
readonly gate?: Gate;
/**
* Is deprecated field.
*/
readonly isDeprecated?: boolean;
/**
* Is active field.
*/
readonly isActive?: boolean;
/**
* Is active field.
*/
readonly generatedType?: 'STORED' | 'VIRTUAL';
/**
* Is active field.
*/
readonly asExpression?: string;
readonly isLabelSyncedWithName: boolean;
}
@@ -1,41 +0,0 @@
import { type Gate } from 'src/engine/twenty-orm/interfaces/gate.interface';
import { type IndexType } from 'src/engine/metadata-modules/index-metadata/types/indexType.types';
export interface WorkspaceIndexMetadataArgs {
/**
* Class to which index is applied.
*/
readonly target: Function;
/*
* Index name.
*/
name: string;
/*
* Index columns.
*/
columns: string[];
/**
* Is index unique.
*/
isUnique: boolean;
/*
* Index type. Defaults to Btree.
*/
type?: IndexType;
/**
* Index where clause.
*/
whereClause: string | null;
/**
* Field gate.
*/
readonly gate?: Gate;
}
@@ -1,17 +0,0 @@
export interface WorkspaceJoinColumnsMetadataArgs {
/**
* Class to which relation is applied.
*/
readonly target: Function;
/**
* Relation name.
*/
readonly relationName: string;
/**
* Relation label.
*/
readonly joinColumn: string;
}
@@ -1,104 +0,0 @@
import { type ObjectType } from 'typeorm';
import { type RelationOnDeleteAction } from 'twenty-shared/types';
import { type RelationType } from 'src/engine/metadata-modules/field-metadata/interfaces/relation-type.interface';
import { type Gate } from 'src/engine/twenty-orm/interfaces/gate.interface';
import { type ObjectMetadataEntity } from 'src/engine/metadata-modules/object-metadata/object-metadata.entity';
export interface WorkspaceRelationMetadataArgs {
/**
* Standard id.
*/
readonly standardId: string;
/**
* Class to which relation is applied.
*/
readonly target: Function;
/**
* Relation name.
*/
readonly name: string;
/**
* Relation label.
*/
readonly label: string | ((objectMetadata: ObjectMetadataEntity) => string);
/**
* Relation type.
*/
readonly type: RelationType;
/**
* Relation description.
*/
readonly description?:
| string
| ((objectMetadata: ObjectMetadataEntity) => string);
/**
* Relation icon.
*/
readonly icon?: string;
/**
* Relation inverse side target.
*/
readonly inverseSideTarget: () => ObjectType<object>;
/**
* Relation inverse side field key.
*/
readonly inverseSideFieldKey?: string;
/**
* Relation on delete action.
*/
readonly onDelete?: RelationOnDeleteAction;
/**
* Is primary field.
*/
readonly isPrimary: boolean;
/**
* Is system field.
*/
readonly isSystem: boolean;
/**
* Is UI read-only field.
*/
readonly isUIReadOnly: boolean;
/**
* Is nullable field.
*/
readonly isNullable: boolean;
/**
* Field gate.
*/
readonly gate?: Gate;
/**
* Is active field.
*/
readonly isActive?: boolean;
readonly isLabelSyncedWithName: boolean;
/**
* Is morph relation.
*/
readonly isMorphRelation: boolean;
/**
* Morph id.
*/
readonly morphId?: string;
}
@@ -87,7 +87,6 @@ describe('WorkspaceRepository', () => {
options: null,
settings: null,
morphId: null,
standardId: null,
standardOverrides: null,
applicationId: 'application-id',
relationTargetFieldMetadataId: null,
@@ -1,175 +0,0 @@
import { type WorkspaceDynamicRelationMetadataArgs } from 'src/engine/twenty-orm/interfaces/workspace-dynamic-relation-metadata-args.interface';
import { type WorkspaceEntityMetadataArgs } from 'src/engine/twenty-orm/interfaces/workspace-entity-metadata-args.interface';
import { type WorkspaceExtendedEntityMetadataArgs } from 'src/engine/twenty-orm/interfaces/workspace-extended-entity-metadata-args.interface';
import { type WorkspaceFieldMetadataArgs } from 'src/engine/twenty-orm/interfaces/workspace-field-metadata-args.interface';
import { type WorkspaceIndexMetadataArgs } from 'src/engine/twenty-orm/interfaces/workspace-index-metadata-args.interface';
import { type WorkspaceJoinColumnsMetadataArgs } from 'src/engine/twenty-orm/interfaces/workspace-join-columns-metadata-args.interface';
import { type WorkspaceRelationMetadataArgs } from 'src/engine/twenty-orm/interfaces/workspace-relation-metadata-args.interface';
export class MetadataArgsStorage {
private readonly entities: WorkspaceEntityMetadataArgs[] = [];
private readonly extendedEntities: WorkspaceExtendedEntityMetadataArgs[] = [];
private readonly fields: WorkspaceFieldMetadataArgs[] = [];
private readonly relations: WorkspaceRelationMetadataArgs[] = [];
private readonly dynamicRelations: WorkspaceDynamicRelationMetadataArgs[] =
[];
private readonly indexes: WorkspaceIndexMetadataArgs[] = [];
private readonly joinColumns: WorkspaceJoinColumnsMetadataArgs[] = [];
addEntities(...entities: WorkspaceEntityMetadataArgs[]): void {
this.entities.push(...entities);
}
addExtendedEntities(
...extendedEntities: WorkspaceExtendedEntityMetadataArgs[]
): void {
this.extendedEntities.push(...extendedEntities);
}
addFields(...fields: WorkspaceFieldMetadataArgs[]): void {
this.fields.push(...fields);
}
addRelations(...relations: WorkspaceRelationMetadataArgs[]): void {
this.relations.push(...relations);
}
addIndexes(...indexes: WorkspaceIndexMetadataArgs[]): void {
this.indexes.push(...indexes);
}
addDynamicRelations(
...dynamicRelations: WorkspaceDynamicRelationMetadataArgs[]
): void {
this.dynamicRelations.push(...dynamicRelations);
}
addJoinColumns(...joinColumns: WorkspaceJoinColumnsMetadataArgs[]): void {
this.joinColumns.push(...joinColumns);
}
filterEntities(
target: Function | string,
): WorkspaceEntityMetadataArgs | undefined;
filterEntities(target: (Function | string)[]): WorkspaceEntityMetadataArgs[];
filterEntities(
target: (Function | string) | (Function | string)[],
): WorkspaceEntityMetadataArgs | undefined | WorkspaceEntityMetadataArgs[] {
const objects = this.filterByTarget(this.entities, target);
return Array.isArray(objects) ? objects[0] : objects;
}
filterExtendedEntities(
target: Function | string,
): WorkspaceExtendedEntityMetadataArgs | undefined;
filterExtendedEntities(
target: (Function | string) | (Function | string)[],
):
| WorkspaceExtendedEntityMetadataArgs
| undefined
| WorkspaceExtendedEntityMetadataArgs[] {
const objects = this.filterByTarget(this.extendedEntities, target);
return Array.isArray(objects) ? objects[0] : objects;
}
filterFields(target: Function | string): WorkspaceFieldMetadataArgs[];
filterFields(
target: (Function | string) | (Function | string)[],
): WorkspaceFieldMetadataArgs[] {
return this.filterByTarget(this.fields, target);
}
filterRelations(target: Function | string): WorkspaceRelationMetadataArgs[];
filterRelations(
target: (Function | string)[],
): WorkspaceRelationMetadataArgs[];
filterRelations(
target: (Function | string) | (Function | string)[],
): WorkspaceRelationMetadataArgs[] {
return this.filterByTarget(this.relations, target);
}
filterIndexes(target: Function | string): WorkspaceIndexMetadataArgs[];
filterIndexes(
target: (Function | string) | (Function | string)[],
): WorkspaceIndexMetadataArgs[] {
return this.filterByTarget(this.indexes, target);
}
filterDynamicRelations(
target: Function | string,
): WorkspaceDynamicRelationMetadataArgs[];
filterDynamicRelations(
target: (Function | string) | (Function | string)[],
): WorkspaceDynamicRelationMetadataArgs[] {
return this.filterByTarget(this.dynamicRelations, target);
}
filterJoinColumns(
target: Function | string,
): WorkspaceJoinColumnsMetadataArgs[];
filterJoinColumns(
target: (Function | string) | (Function | string)[],
): WorkspaceJoinColumnsMetadataArgs[] {
return this.filterByTarget(this.joinColumns, target);
}
protected filterByTarget<T extends { target: Function | string }>(
array: T[],
target: (Function | string) | (Function | string)[],
): T[] {
if (Array.isArray(target)) {
return target.flatMap((targetItem) => {
if (typeof targetItem === 'function') {
return this.collectFromClass(array, targetItem);
}
return this.collectFromString(array, targetItem);
});
} else {
return typeof target === 'function'
? this.collectFromClass(array, target)
: this.collectFromString(array, target);
}
}
// Private helper to collect metadata from class prototypes
private collectFromClass<T extends { target: Function | string }>(
array: T[],
cls: Function,
): T[] {
const collectedMetadata: T[] = [];
let currentTarget = cls;
// Collect metadata from the current class and all its parent classes
while (currentTarget !== Function.prototype) {
collectedMetadata.push(
...array.filter((item) => item.target === currentTarget),
);
currentTarget = Object.getPrototypeOf(currentTarget);
}
return collectedMetadata;
}
// Private helper to collect metadata directly by string comparison
private collectFromString<T extends { target: Function | string }>(
array: T[],
targetString: string,
): T[] {
return array.filter((item) => item.target === targetString);
}
}
export const metadataArgsStorage = new MetadataArgsStorage();
@@ -233,7 +233,6 @@ describe('computeRelationConnectQueryConfigs', () => {
description: null,
standardOverrides: null,
isUIReadOnly: false,
standardId: null,
labelIdentifierFieldMetadataId: null,
imageIdentifierFieldMetadataId: null,
duplicateCriteria: null,
@@ -12,6 +12,7 @@ import {
isDefined,
isNonEmptyArray,
} from 'twenty-shared/utils';
import { STANDARD_OBJECTS } from 'twenty-shared/metadata';
import type { ObjectLiteral } from 'typeorm';
@@ -26,7 +27,6 @@ import {
TwentyORMExceptionCode,
} from 'src/engine/twenty-orm/exceptions/twenty-orm.exception';
import { type DatabaseBatchEventInput } from 'src/engine/workspace-event-emitter/workspace-event-emitter';
import { STANDARD_OBJECTS } from 'src/engine/workspace-manager/twenty-standard-application/constants/standard-object.constant';
export const formatTwentyOrmEventToDatabaseBatchEvent = <
T extends ObjectLiteral,
@@ -1,61 +0,0 @@
import { RelationType } from 'src/engine/metadata-modules/field-metadata/interfaces/relation-type.interface';
import { type WorkspaceJoinColumnsMetadataArgs } from 'src/engine/twenty-orm/interfaces/workspace-join-columns-metadata-args.interface';
import { type WorkspaceRelationMetadataArgs } from 'src/engine/twenty-orm/interfaces/workspace-relation-metadata-args.interface';
import {
RelationException,
RelationExceptionCode,
} from 'src/engine/twenty-orm/exceptions/relation.exception';
import { metadataArgsStorage } from 'src/engine/twenty-orm/storage/metadata-args.storage';
export const getJoinColumn = (
joinColumnsMetadataArgsCollection: WorkspaceJoinColumnsMetadataArgs[],
relationMetadataArgs: WorkspaceRelationMetadataArgs,
): string | null => {
if (relationMetadataArgs.type === RelationType.ONE_TO_MANY) {
return null;
}
const inverseSideTarget = relationMetadataArgs.inverseSideTarget();
const inverseSideJoinColumnsMetadataArgsCollection =
metadataArgsStorage.filterJoinColumns(inverseSideTarget);
const filteredJoinColumnsMetadataArgsCollection =
joinColumnsMetadataArgsCollection.filter(
(joinColumnsMetadataArgs) =>
joinColumnsMetadataArgs.relationName === relationMetadataArgs.name,
);
const oppositeFilteredJoinColumnsMetadataArgsCollection =
inverseSideJoinColumnsMetadataArgsCollection.filter(
(joinColumnsMetadataArgs) =>
joinColumnsMetadataArgs.relationName === relationMetadataArgs.name,
);
if (
filteredJoinColumnsMetadataArgsCollection.length > 0 &&
oppositeFilteredJoinColumnsMetadataArgsCollection.length > 0
) {
throw new RelationException(
`Join column for ${relationMetadataArgs.name} relation is present on both sides`,
RelationExceptionCode.RELATION_JOIN_COLUMN_ON_BOTH_SIDES,
);
}
// Check if there are multiple join columns for the relation
if (filteredJoinColumnsMetadataArgsCollection.length > 1) {
throw new RelationException(
`Multiple join columns found for relation ${relationMetadataArgs.name}`,
RelationExceptionCode.MULTIPLE_JOIN_COLUMNS_FOUND,
);
}
const joinColumnsMetadataArgs = filteredJoinColumnsMetadataArgsCollection[0];
if (!joinColumnsMetadataArgs) {
throw new RelationException(
`Join column is missing for relation ${relationMetadataArgs.name}`,
RelationExceptionCode.MISSING_RELATION_JOIN_COLUMN,
);
}
return joinColumnsMetadataArgs.joinColumn;
};