Improve cleaning job (#17208)
# Introduction Refactored the workspace deletion to dynamically iterate over all known v2 syncable entities repos and delete all of them from child to parent Exception for field metadata that we chunk delete in order to avoid locking the core schema too long, it does not have an impact on perfs at all ( neither plus or less ) Chunking by constraint within a transaction is not necessary both does not cost more ## From 30s for a workspace complete deletion ```ts [Nest] 93244 - 01/16/2026, 10:24:52 PM LOG [WorkspaceService] workspace WS_ID cache flushed [Runner] Total execution: 26.290s // ( deleteAllObjectMetadatas v2 ) [Nest] 93244 - 01/16/2026, 10:25:22 PM LOG [WorkspaceService] workspace WS_ID hard deleted ``` ## To 3s ! ```ts [Nest] 65112 - 01/18/2026, 4:37:38 PM LOG [DatabaseConfigDriver] [INIT] Config variables loaded: 0 values found in DB, 69 falling to env vars/defaults [Nest] 65112 - 01/18/2026, 4:37:38 PM LOG [CleanSuspendedWorkspacesCommand] IGNORING GRACE PERIOD - Cleaning 1 suspended workspaces [Nest] 65112 - 01/18/2026, 4:37:38 PM LOG [CleanerWorkspaceService] batchWarnOrCleanSuspendedWorkspaces running... [Nest] 65112 - 01/18/2026, 4:37:38 PM LOG [CleanerWorkspaceService] Processing workspace - 1/1 [Nest] 65112 - 01/18/2026, 4:37:38 PM LOG [CleanerWorkspaceService] Destroying workspace Twenty Eng [Nest] 65112 - 01/18/2026, 4:37:38 PM LOG [WorkspaceService] workspace user workspaces deleted [Nest] 65112 - 01/18/2026, 4:37:38 PM LOG [WorkspaceService] workspace cache flushed [Nest] 65112 - 01/18/2026, 4:37:38 PM LOG [WorkspaceService] workspace: deleted 80 viewFilter record(s) [Nest] 65112 - 01/18/2026, 4:37:38 PM LOG [WorkspaceService] workspace: deleted 21 pageLayoutWidget record(s) [Nest] 65112 - 01/18/2026, 4:37:38 PM LOG [WorkspaceService] workspace: deleted 1515 viewField record(s) [Nest] 65112 - 01/18/2026, 4:37:38 PM LOG [WorkspaceService] workspace: deleted 91 index record(s) [Nest] 65112 - 01/18/2026, 4:37:38 PM LOG [WorkspaceService] workspace: deleted 66 roleTarget record(s) [Nest] 65112 - 01/18/2026, 4:37:38 PM LOG [WorkspaceService] workspace: deleted 174 viewGroup record(s) [Nest] 65112 - 01/18/2026, 4:37:38 PM LOG [WorkspaceService] workspace: deleted 1 agent record(s) [Nest] 65112 - 01/18/2026, 4:37:38 PM LOG [WorkspaceService] workspace: deleted 7 pageLayout record(s) [Nest] 65112 - 01/18/2026, 4:37:38 PM LOG [WorkspaceService] workspace: deleted 111 view record(s) [Nest] 65112 - 01/18/2026, 4:37:38 PM LOG [WorkspaceService] workspace: fieldMetadata chunk 1/15 - deleted 50 record(s) [Nest] 65112 - 01/18/2026, 4:37:38 PM LOG [WorkspaceService] workspace: fieldMetadata chunk 2/15 - deleted 50 record(s) [Nest] 65112 - 01/18/2026, 4:37:38 PM LOG [WorkspaceService] workspace: fieldMetadata chunk 3/15 - deleted 50 record(s) [Nest] 65112 - 01/18/2026, 4:37:38 PM LOG [WorkspaceService] workspace: fieldMetadata chunk 4/15 - deleted 50 record(s) [Nest] 65112 - 01/18/2026, 4:37:38 PM LOG [WorkspaceService] workspace: fieldMetadata chunk 5/15 - deleted 50 record(s) [Nest] 65112 - 01/18/2026, 4:37:38 PM LOG [WorkspaceService] workspace: fieldMetadata chunk 6/15 - deleted 50 record(s) [Nest] 65112 - 01/18/2026, 4:37:38 PM LOG [WorkspaceService] workspace: fieldMetadata chunk 7/15 - deleted 50 record(s) [Nest] 65112 - 01/18/2026, 4:37:38 PM LOG [WorkspaceService] workspace: fieldMetadata chunk 8/15 - deleted 50 record(s) [Nest] 65112 - 01/18/2026, 4:37:38 PM LOG [WorkspaceService] workspace: fieldMetadata chunk 9/15 - deleted 50 record(s) [Nest] 65112 - 01/18/2026, 4:37:38 PM LOG [WorkspaceService] workspace: fieldMetadata chunk 10/15 - deleted 50 record(s) [Nest] 65112 - 01/18/2026, 4:37:38 PM LOG [WorkspaceService] workspace: fieldMetadata chunk 11/15 - deleted 50 record(s) [Nest] 65112 - 01/18/2026, 4:37:38 PM LOG [WorkspaceService] workspace: fieldMetadata chunk 12/15 - deleted 51 record(s) [Nest] 65112 - 01/18/2026, 4:37:38 PM LOG [WorkspaceService] workspace: fieldMetadata chunk 13/15 - deleted 50 record(s) [Nest] 65112 - 01/18/2026, 4:37:38 PM LOG [WorkspaceService] workspace: fieldMetadata chunk 14/15 - deleted 50 record(s) [Nest] 65112 - 01/18/2026, 4:37:38 PM LOG [WorkspaceService] workspace: fieldMetadata chunk 15/15 - deleted 36 record(s) [Nest] 65112 - 01/18/2026, 4:37:38 PM LOG [WorkspaceService] workspace: deleted 737 fieldMetadata record(s) [Nest] 65112 - 01/18/2026, 4:37:38 PM LOG [WorkspaceService] workspace: deleted 6 role record(s) [Nest] 65112 - 01/18/2026, 4:37:38 PM LOG [WorkspaceService] workspace: deleted 78 serverlessFunction record(s) [Nest] 65112 - 01/18/2026, 4:37:39 PM LOG [WorkspaceService] workspace: deleted 43 objectMetadata record(s) [Nest] 65112 - 01/18/2026, 4:37:41 PM LOG [WorkspaceService] workspace hard deleted [Nest] 65112 - 01/18/2026, 4:37:41 PM LOG [CleanerWorkspaceService] Destroyed 1 workspaces on 5 limit durings this execution [Nest] 65112 - 01/18/2026, 4:37:41 PM LOG [CleanerWorkspaceService] batchWarnOrCleanSuspendedWorkspaces done! [Nest] 65112 - 01/18/2026, 4:37:41 PM LOG [CleanSuspendedWorkspacesCommand] Command completed! ``` ## Update Discussed with @charlesBochet ended debugging and analyzing sql query operations He discovered that we were not indexing foreignKey effectively We've ended up fixing all the FK indeces coverage leading to ## Cleaning Removed the ```sh npx nx run twenty-server:command workspace:clean-soft-deleted-suspended-workspaces --ignore-grace-period ``` In favor of ```sh npx nx run twenty-server:command workspace:clean --only-operation destroy --ignore-destroy-grace-period ``` ## Conclusion Not that crazy but still worth it and could demultiply in production
This commit is contained in:
+3
@@ -24,6 +24,9 @@ export enum CommandMenuItemAvailabilityType {
|
||||
'workflowVersionId',
|
||||
'workspaceId',
|
||||
])
|
||||
@Index('IDX_COMMAND_MENU_ITEM_AVAILABILITY_OBJECT_METADATA_ID', [
|
||||
'availabilityObjectMetadataId',
|
||||
])
|
||||
export class CommandMenuItemEntity
|
||||
extends SyncableEntityRequired
|
||||
implements Required<CommandMenuItemEntity>
|
||||
|
||||
+1
@@ -19,6 +19,7 @@ export type CronTriggerSettings = {
|
||||
|
||||
@Entity({ name: 'cronTrigger', schema: 'core' })
|
||||
@Index('IDX_CRON_TRIGGER_WORKSPACE_ID', ['workspaceId'])
|
||||
@Index('IDX_CRON_TRIGGER_SERVERLESS_FUNCTION_ID', ['serverlessFunctionId'])
|
||||
export class CronTriggerEntity
|
||||
extends SyncableEntity
|
||||
implements Required<CronTriggerEntity>
|
||||
|
||||
+3
@@ -19,6 +19,9 @@ export type DatabaseEventTriggerSettings = {
|
||||
|
||||
@Entity('databaseEventTrigger')
|
||||
@Index('IDX_DATABASE_EVENT_TRIGGER_WORKSPACE_ID', ['workspaceId'])
|
||||
@Index('IDX_DATABASE_EVENT_TRIGGER_SERVERLESS_FUNCTION_ID', [
|
||||
'serverlessFunctionId',
|
||||
])
|
||||
export class DatabaseEventTriggerEntity
|
||||
extends SyncableEntity
|
||||
implements Required<DatabaseEventTriggerEntity>
|
||||
|
||||
+52
@@ -0,0 +1,52 @@
|
||||
import { type AllMetadataName } from 'twenty-shared/metadata';
|
||||
import { type EntityTarget, type ObjectLiteral } from 'typeorm';
|
||||
|
||||
import { AgentEntity } from 'src/engine/metadata-modules/ai/ai-agent/entities/agent.entity';
|
||||
import { CommandMenuItemEntity } from 'src/engine/metadata-modules/command-menu-item/entities/command-menu-item.entity';
|
||||
import { CronTriggerEntity } from 'src/engine/metadata-modules/cron-trigger/entities/cron-trigger.entity';
|
||||
import { DatabaseEventTriggerEntity } from 'src/engine/metadata-modules/database-event-trigger/entities/database-event-trigger.entity';
|
||||
import { FieldMetadataEntity } from 'src/engine/metadata-modules/field-metadata/field-metadata.entity';
|
||||
import { FrontComponentEntity } from 'src/engine/metadata-modules/front-component/entities/front-component.entity';
|
||||
import { IndexMetadataEntity } from 'src/engine/metadata-modules/index-metadata/index-metadata.entity';
|
||||
import { ObjectMetadataEntity } from 'src/engine/metadata-modules/object-metadata/object-metadata.entity';
|
||||
import { PageLayoutTabEntity } from 'src/engine/metadata-modules/page-layout-tab/entities/page-layout-tab.entity';
|
||||
import { PageLayoutWidgetEntity } from 'src/engine/metadata-modules/page-layout-widget/entities/page-layout-widget.entity';
|
||||
import { PageLayoutEntity } from 'src/engine/metadata-modules/page-layout/entities/page-layout.entity';
|
||||
import { RoleTargetEntity } from 'src/engine/metadata-modules/role-target/role-target.entity';
|
||||
import { RoleEntity } from 'src/engine/metadata-modules/role/role.entity';
|
||||
import { RouteTriggerEntity } from 'src/engine/metadata-modules/route-trigger/route-trigger.entity';
|
||||
import { RowLevelPermissionPredicateGroupEntity } from 'src/engine/metadata-modules/row-level-permission-predicate/entities/row-level-permission-predicate-group.entity';
|
||||
import { RowLevelPermissionPredicateEntity } from 'src/engine/metadata-modules/row-level-permission-predicate/entities/row-level-permission-predicate.entity';
|
||||
import { ServerlessFunctionEntity } from 'src/engine/metadata-modules/serverless-function/serverless-function.entity';
|
||||
import { SkillEntity } from 'src/engine/metadata-modules/skill/entities/skill.entity';
|
||||
import { ViewFieldEntity } from 'src/engine/metadata-modules/view-field/entities/view-field.entity';
|
||||
import { ViewFilterGroupEntity } from 'src/engine/metadata-modules/view-filter-group/entities/view-filter-group.entity';
|
||||
import { ViewFilterEntity } from 'src/engine/metadata-modules/view-filter/entities/view-filter.entity';
|
||||
import { ViewGroupEntity } from 'src/engine/metadata-modules/view-group/entities/view-group.entity';
|
||||
import { ViewEntity } from 'src/engine/metadata-modules/view/entities/view.entity';
|
||||
|
||||
export const ALL_METADATA_ENTITY_BY_METADATA_NAME = {
|
||||
viewField: ViewFieldEntity,
|
||||
viewFilter: ViewFilterEntity,
|
||||
viewGroup: ViewGroupEntity,
|
||||
viewFilterGroup: ViewFilterGroupEntity,
|
||||
roleTarget: RoleTargetEntity,
|
||||
rowLevelPermissionPredicate: RowLevelPermissionPredicateEntity,
|
||||
pageLayoutWidget: PageLayoutWidgetEntity,
|
||||
rowLevelPermissionPredicateGroup: RowLevelPermissionPredicateGroupEntity,
|
||||
view: ViewEntity,
|
||||
index: IndexMetadataEntity,
|
||||
pageLayoutTab: PageLayoutTabEntity,
|
||||
routeTrigger: RouteTriggerEntity,
|
||||
cronTrigger: CronTriggerEntity,
|
||||
databaseEventTrigger: DatabaseEventTriggerEntity,
|
||||
frontComponent: FrontComponentEntity,
|
||||
fieldMetadata: FieldMetadataEntity,
|
||||
pageLayout: PageLayoutEntity,
|
||||
skill: SkillEntity,
|
||||
serverlessFunction: ServerlessFunctionEntity,
|
||||
objectMetadata: ObjectMetadataEntity,
|
||||
role: RoleEntity,
|
||||
agent: AgentEntity,
|
||||
commandMenuItem: CommandMenuItemEntity,
|
||||
} as const satisfies Record<AllMetadataName, EntityTarget<ObjectLiteral>>;
|
||||
+4
@@ -0,0 +1,4 @@
|
||||
import { sortMetadataNamesChildrenFirst } from 'src/engine/metadata-modules/flat-entity/utils/sort-metadata-names-children-first.util';
|
||||
|
||||
export const ALL_METADATA_NAMES_SORTED_ATOMICALLY =
|
||||
sortMetadataNamesChildrenFirst();
|
||||
+24
-46
@@ -1,12 +1,8 @@
|
||||
import { type AgentEntity } from 'src/engine/metadata-modules/ai/ai-agent/entities/agent.entity';
|
||||
import { type CommandMenuItemEntity } from 'src/engine/metadata-modules/command-menu-item/entities/command-menu-item.entity';
|
||||
import { type CronTriggerEntity } from 'src/engine/metadata-modules/cron-trigger/entities/cron-trigger.entity';
|
||||
import { type FlatCronTrigger } from 'src/engine/metadata-modules/cron-trigger/types/flat-cron-trigger.type';
|
||||
import { type DatabaseEventTriggerEntity } from 'src/engine/metadata-modules/database-event-trigger/entities/database-event-trigger.entity';
|
||||
import { type FlatDatabaseEventTrigger } from 'src/engine/metadata-modules/database-event-trigger/types/flat-database-event-trigger.type';
|
||||
import { type FieldMetadataEntity } from 'src/engine/metadata-modules/field-metadata/field-metadata.entity';
|
||||
import { type FlatAgent } from 'src/engine/metadata-modules/flat-agent/types/flat-agent.type';
|
||||
import { type FlatCommandMenuItem } from 'src/engine/metadata-modules/flat-command-menu-item/types/flat-command-menu-item.type';
|
||||
import { type MetadataEntity } from 'src/engine/metadata-modules/flat-entity/types/metadata-entity.type';
|
||||
import { type FlatFieldMetadata } from 'src/engine/metadata-modules/flat-field-metadata/types/flat-field-metadata.type';
|
||||
import { type FlatFrontComponent } from 'src/engine/metadata-modules/flat-front-component/types/flat-front-component.type';
|
||||
import { type FlatIndexMetadata } from 'src/engine/metadata-modules/flat-index-metadata/types/flat-index-metadata.type';
|
||||
@@ -22,28 +18,10 @@ import { type FlatViewFilterGroup } from 'src/engine/metadata-modules/flat-view-
|
||||
import { type FlatViewFilter } from 'src/engine/metadata-modules/flat-view-filter/types/flat-view-filter.type';
|
||||
import { type FlatViewGroup } from 'src/engine/metadata-modules/flat-view-group/types/flat-view-group.type';
|
||||
import { type FlatView } from 'src/engine/metadata-modules/flat-view/types/flat-view.type';
|
||||
import { type FrontComponentEntity } from 'src/engine/metadata-modules/front-component/entities/front-component.entity';
|
||||
import { type IndexMetadataEntity } from 'src/engine/metadata-modules/index-metadata/index-metadata.entity';
|
||||
import { type ObjectMetadataEntity } from 'src/engine/metadata-modules/object-metadata/object-metadata.entity';
|
||||
import { type PageLayoutTabEntity } from 'src/engine/metadata-modules/page-layout-tab/entities/page-layout-tab.entity';
|
||||
import { type PageLayoutWidgetEntity } from 'src/engine/metadata-modules/page-layout-widget/entities/page-layout-widget.entity';
|
||||
import { type PageLayoutEntity } from 'src/engine/metadata-modules/page-layout/entities/page-layout.entity';
|
||||
import { type RoleTargetEntity } from 'src/engine/metadata-modules/role-target/role-target.entity';
|
||||
import { type RoleEntity } from 'src/engine/metadata-modules/role/role.entity';
|
||||
import { type RouteTriggerEntity } from 'src/engine/metadata-modules/route-trigger/route-trigger.entity';
|
||||
import { type FlatRouteTrigger } from 'src/engine/metadata-modules/route-trigger/types/flat-route-trigger.type';
|
||||
import { type RowLevelPermissionPredicateGroupEntity } from 'src/engine/metadata-modules/row-level-permission-predicate/entities/row-level-permission-predicate-group.entity';
|
||||
import { type RowLevelPermissionPredicateEntity } from 'src/engine/metadata-modules/row-level-permission-predicate/entities/row-level-permission-predicate.entity';
|
||||
import { type FlatRowLevelPermissionPredicateGroup } from 'src/engine/metadata-modules/row-level-permission-predicate/types/flat-row-level-permission-predicate-group.type';
|
||||
import { type FlatRowLevelPermissionPredicate } from 'src/engine/metadata-modules/row-level-permission-predicate/types/flat-row-level-permission-predicate.type';
|
||||
import { type ServerlessFunctionEntity } from 'src/engine/metadata-modules/serverless-function/serverless-function.entity';
|
||||
import { type FlatServerlessFunction } from 'src/engine/metadata-modules/serverless-function/types/flat-serverless-function.type';
|
||||
import { type SkillEntity } from 'src/engine/metadata-modules/skill/entities/skill.entity';
|
||||
import { type ViewFieldEntity } from 'src/engine/metadata-modules/view-field/entities/view-field.entity';
|
||||
import { type ViewFilterGroupEntity } from 'src/engine/metadata-modules/view-filter-group/entities/view-filter-group.entity';
|
||||
import { type ViewFilterEntity } from 'src/engine/metadata-modules/view-filter/entities/view-filter.entity';
|
||||
import { type ViewGroupEntity } from 'src/engine/metadata-modules/view-group/entities/view-group.entity';
|
||||
import { type ViewEntity } from 'src/engine/metadata-modules/view/entities/view.entity';
|
||||
import {
|
||||
type CreateAgentAction,
|
||||
type DeleteAgentAction,
|
||||
@@ -168,7 +146,7 @@ export type AllFlatEntityTypesByMetadataName = {
|
||||
delete: DeleteFieldAction;
|
||||
};
|
||||
flatEntity: FlatFieldMetadata;
|
||||
entity: FieldMetadataEntity;
|
||||
entity: MetadataEntity<'fieldMetadata'>;
|
||||
};
|
||||
objectMetadata: {
|
||||
actions: {
|
||||
@@ -177,7 +155,7 @@ export type AllFlatEntityTypesByMetadataName = {
|
||||
delete: DeleteObjectAction;
|
||||
};
|
||||
flatEntity: FlatObjectMetadata;
|
||||
entity: ObjectMetadataEntity;
|
||||
entity: MetadataEntity<'objectMetadata'>;
|
||||
};
|
||||
view: {
|
||||
actions: {
|
||||
@@ -186,7 +164,7 @@ export type AllFlatEntityTypesByMetadataName = {
|
||||
delete: DeleteViewAction;
|
||||
};
|
||||
flatEntity: FlatView;
|
||||
entity: ViewEntity;
|
||||
entity: MetadataEntity<'view'>;
|
||||
};
|
||||
viewField: {
|
||||
actions: {
|
||||
@@ -195,7 +173,7 @@ export type AllFlatEntityTypesByMetadataName = {
|
||||
delete: DeleteViewFieldAction;
|
||||
};
|
||||
flatEntity: FlatViewField;
|
||||
entity: ViewFieldEntity;
|
||||
entity: MetadataEntity<'viewField'>;
|
||||
};
|
||||
viewGroup: {
|
||||
actions: {
|
||||
@@ -204,7 +182,7 @@ export type AllFlatEntityTypesByMetadataName = {
|
||||
delete: DeleteViewGroupAction;
|
||||
};
|
||||
flatEntity: FlatViewGroup;
|
||||
entity: ViewGroupEntity;
|
||||
entity: MetadataEntity<'viewGroup'>;
|
||||
};
|
||||
rowLevelPermissionPredicate: {
|
||||
actions: {
|
||||
@@ -213,7 +191,7 @@ export type AllFlatEntityTypesByMetadataName = {
|
||||
delete: DeleteRowLevelPermissionPredicateAction;
|
||||
};
|
||||
flatEntity: FlatRowLevelPermissionPredicate;
|
||||
entity: RowLevelPermissionPredicateEntity;
|
||||
entity: MetadataEntity<'rowLevelPermissionPredicate'>;
|
||||
};
|
||||
rowLevelPermissionPredicateGroup: {
|
||||
actions: {
|
||||
@@ -222,7 +200,7 @@ export type AllFlatEntityTypesByMetadataName = {
|
||||
delete: DeleteRowLevelPermissionPredicateGroupAction;
|
||||
};
|
||||
flatEntity: FlatRowLevelPermissionPredicateGroup;
|
||||
entity: RowLevelPermissionPredicateGroupEntity;
|
||||
entity: MetadataEntity<'rowLevelPermissionPredicateGroup'>;
|
||||
};
|
||||
viewFilterGroup: {
|
||||
actions: {
|
||||
@@ -231,7 +209,7 @@ export type AllFlatEntityTypesByMetadataName = {
|
||||
delete: DeleteViewFilterGroupAction;
|
||||
};
|
||||
flatEntity: FlatViewFilterGroup;
|
||||
entity: ViewFilterGroupEntity;
|
||||
entity: MetadataEntity<'viewFilterGroup'>;
|
||||
};
|
||||
index: {
|
||||
actions: {
|
||||
@@ -240,7 +218,7 @@ export type AllFlatEntityTypesByMetadataName = {
|
||||
delete: DeleteIndexAction;
|
||||
};
|
||||
flatEntity: FlatIndexMetadata;
|
||||
entity: IndexMetadataEntity;
|
||||
entity: MetadataEntity<'index'>;
|
||||
};
|
||||
serverlessFunction: {
|
||||
actions: {
|
||||
@@ -249,7 +227,7 @@ export type AllFlatEntityTypesByMetadataName = {
|
||||
delete: DeleteServerlessFunctionAction;
|
||||
};
|
||||
flatEntity: FlatServerlessFunction;
|
||||
entity: ServerlessFunctionEntity;
|
||||
entity: MetadataEntity<'serverlessFunction'>;
|
||||
};
|
||||
cronTrigger: {
|
||||
actions: {
|
||||
@@ -258,7 +236,7 @@ export type AllFlatEntityTypesByMetadataName = {
|
||||
delete: DeleteCronTriggerAction;
|
||||
};
|
||||
flatEntity: FlatCronTrigger;
|
||||
entity: CronTriggerEntity;
|
||||
entity: MetadataEntity<'cronTrigger'>;
|
||||
};
|
||||
databaseEventTrigger: {
|
||||
actions: {
|
||||
@@ -267,7 +245,7 @@ export type AllFlatEntityTypesByMetadataName = {
|
||||
delete: DeleteDatabaseEventTriggerAction;
|
||||
};
|
||||
flatEntity: FlatDatabaseEventTrigger;
|
||||
entity: DatabaseEventTriggerEntity;
|
||||
entity: MetadataEntity<'databaseEventTrigger'>;
|
||||
};
|
||||
routeTrigger: {
|
||||
actions: {
|
||||
@@ -276,7 +254,7 @@ export type AllFlatEntityTypesByMetadataName = {
|
||||
delete: DeleteRouteTriggerAction;
|
||||
};
|
||||
flatEntity: FlatRouteTrigger;
|
||||
entity: RouteTriggerEntity;
|
||||
entity: MetadataEntity<'routeTrigger'>;
|
||||
};
|
||||
viewFilter: {
|
||||
actions: {
|
||||
@@ -285,7 +263,7 @@ export type AllFlatEntityTypesByMetadataName = {
|
||||
delete: DeleteViewFilterAction;
|
||||
};
|
||||
flatEntity: FlatViewFilter;
|
||||
entity: ViewFilterEntity;
|
||||
entity: MetadataEntity<'viewFilter'>;
|
||||
};
|
||||
role: {
|
||||
actions: {
|
||||
@@ -294,7 +272,7 @@ export type AllFlatEntityTypesByMetadataName = {
|
||||
delete: DeleteRoleAction;
|
||||
};
|
||||
flatEntity: FlatRole;
|
||||
entity: RoleEntity;
|
||||
entity: MetadataEntity<'role'>;
|
||||
};
|
||||
roleTarget: {
|
||||
actions: {
|
||||
@@ -303,7 +281,7 @@ export type AllFlatEntityTypesByMetadataName = {
|
||||
delete: DeleteRoleTargetAction;
|
||||
};
|
||||
flatEntity: FlatRoleTarget;
|
||||
entity: RoleTargetEntity;
|
||||
entity: MetadataEntity<'roleTarget'>;
|
||||
};
|
||||
agent: {
|
||||
actions: {
|
||||
@@ -312,7 +290,7 @@ export type AllFlatEntityTypesByMetadataName = {
|
||||
delete: DeleteAgentAction;
|
||||
};
|
||||
flatEntity: FlatAgent;
|
||||
entity: AgentEntity;
|
||||
entity: MetadataEntity<'agent'>;
|
||||
};
|
||||
skill: {
|
||||
actions: {
|
||||
@@ -321,7 +299,7 @@ export type AllFlatEntityTypesByMetadataName = {
|
||||
delete: DeleteSkillAction;
|
||||
};
|
||||
flatEntity: FlatSkill;
|
||||
entity: SkillEntity;
|
||||
entity: MetadataEntity<'skill'>;
|
||||
};
|
||||
commandMenuItem: {
|
||||
actions: {
|
||||
@@ -330,7 +308,7 @@ export type AllFlatEntityTypesByMetadataName = {
|
||||
delete: DeleteCommandMenuItemAction;
|
||||
};
|
||||
flatEntity: FlatCommandMenuItem;
|
||||
entity: CommandMenuItemEntity;
|
||||
entity: MetadataEntity<'commandMenuItem'>;
|
||||
};
|
||||
pageLayout: {
|
||||
actions: {
|
||||
@@ -339,7 +317,7 @@ export type AllFlatEntityTypesByMetadataName = {
|
||||
delete: DeletePageLayoutAction;
|
||||
};
|
||||
flatEntity: FlatPageLayout;
|
||||
entity: PageLayoutEntity;
|
||||
entity: MetadataEntity<'pageLayout'>;
|
||||
};
|
||||
pageLayoutWidget: {
|
||||
actions: {
|
||||
@@ -348,7 +326,7 @@ export type AllFlatEntityTypesByMetadataName = {
|
||||
delete: DeletePageLayoutWidgetAction;
|
||||
};
|
||||
flatEntity: FlatPageLayoutWidget;
|
||||
entity: PageLayoutWidgetEntity;
|
||||
entity: MetadataEntity<'pageLayoutWidget'>;
|
||||
};
|
||||
pageLayoutTab: {
|
||||
actions: {
|
||||
@@ -357,7 +335,7 @@ export type AllFlatEntityTypesByMetadataName = {
|
||||
delete: DeletePageLayoutTabAction;
|
||||
};
|
||||
flatEntity: FlatPageLayoutTab;
|
||||
entity: PageLayoutTabEntity;
|
||||
entity: MetadataEntity<'pageLayoutTab'>;
|
||||
};
|
||||
frontComponent: {
|
||||
actions: {
|
||||
@@ -366,6 +344,6 @@ export type AllFlatEntityTypesByMetadataName = {
|
||||
delete: DeleteFrontComponentAction;
|
||||
};
|
||||
flatEntity: FlatFrontComponent;
|
||||
entity: FrontComponentEntity;
|
||||
entity: MetadataEntity<'frontComponent'>;
|
||||
};
|
||||
};
|
||||
|
||||
+4
-3
@@ -1,6 +1,7 @@
|
||||
import { type AllMetadataName } from 'twenty-shared/metadata';
|
||||
|
||||
import { type AllFlatEntityTypesByMetadataName } from 'src/engine/metadata-modules/flat-entity/types/all-flat-entity-types-by-metadata-name';
|
||||
import { type ALL_METADATA_ENTITY_BY_METADATA_NAME } from 'src/engine/metadata-modules/flat-entity/constant/all-metadata-entity-by-metadata-name.constant';
|
||||
|
||||
export type MetadataEntity<T extends AllMetadataName> =
|
||||
AllFlatEntityTypesByMetadataName[T]['entity'];
|
||||
export type MetadataEntity<K extends AllMetadataName> = InstanceType<
|
||||
(typeof ALL_METADATA_ENTITY_BY_METADATA_NAME)[K]
|
||||
>;
|
||||
|
||||
+29
@@ -0,0 +1,29 @@
|
||||
// Jest Snapshot v1, https://jestjs.io/docs/snapshot-testing
|
||||
|
||||
exports[`sortMetadataNamesChildrenFirst should return metadata names sorted with children first (most manyToOne relations first) 1`] = `
|
||||
[
|
||||
"rowLevelPermissionPredicate",
|
||||
"viewFilter",
|
||||
"pageLayoutWidget",
|
||||
"viewField",
|
||||
"commandMenuItem",
|
||||
"cronTrigger",
|
||||
"databaseEventTrigger",
|
||||
"index",
|
||||
"roleTarget",
|
||||
"routeTrigger",
|
||||
"rowLevelPermissionPredicateGroup",
|
||||
"viewGroup",
|
||||
"agent",
|
||||
"frontComponent",
|
||||
"pageLayout",
|
||||
"pageLayoutTab",
|
||||
"skill",
|
||||
"view",
|
||||
"viewFilterGroup",
|
||||
"fieldMetadata",
|
||||
"role",
|
||||
"serverlessFunction",
|
||||
"objectMetadata",
|
||||
]
|
||||
`;
|
||||
+9
@@ -0,0 +1,9 @@
|
||||
import { sortMetadataNamesChildrenFirst } from 'src/engine/metadata-modules/flat-entity/utils/sort-metadata-names-children-first.util';
|
||||
|
||||
describe('sortMetadataNamesChildrenFirst', () => {
|
||||
it('should return metadata names sorted with children first (most manyToOne relations first)', () => {
|
||||
const result = sortMetadataNamesChildrenFirst();
|
||||
|
||||
expect(result).toMatchSnapshot();
|
||||
});
|
||||
});
|
||||
+16
@@ -0,0 +1,16 @@
|
||||
import { type AllMetadataName } from 'twenty-shared/metadata';
|
||||
|
||||
import { ALL_METADATA_RELATIONS } from 'src/engine/metadata-modules/flat-entity/constant/all-metadata-relations.constant';
|
||||
import { type MetadataManyToOneRelatedMetadataNames } from 'src/engine/metadata-modules/flat-entity/types/metadata-many-to-one-related-metadata-names.type';
|
||||
|
||||
export const getMetadataManyToOneRelatedNames = <T extends AllMetadataName>(
|
||||
metadataName: T,
|
||||
): MetadataManyToOneRelatedMetadataNames<T>[] => {
|
||||
const relations = ALL_METADATA_RELATIONS[metadataName];
|
||||
|
||||
return Object.values(relations.manyToOne)
|
||||
.filter((relation) => relation !== null)
|
||||
.map(
|
||||
(relation) => relation.metadataName,
|
||||
) as MetadataManyToOneRelatedMetadataNames<T>[];
|
||||
};
|
||||
+16
@@ -0,0 +1,16 @@
|
||||
import { type AllMetadataName } from 'twenty-shared/metadata';
|
||||
|
||||
import { ALL_METADATA_RELATIONS } from 'src/engine/metadata-modules/flat-entity/constant/all-metadata-relations.constant';
|
||||
import { type MetadataOneToManyRelatedMetadataNames } from 'src/engine/metadata-modules/flat-entity/types/metadata-one-to-many-related-metadata-names.type';
|
||||
|
||||
export const getMetadataOneToManyRelatedNames = <T extends AllMetadataName>(
|
||||
metadataName: T,
|
||||
): MetadataOneToManyRelatedMetadataNames<T>[] => {
|
||||
const relations = ALL_METADATA_RELATIONS[metadataName];
|
||||
|
||||
return Object.values(relations.oneToMany)
|
||||
.filter((relation) => relation !== null)
|
||||
.map(
|
||||
(relation) => relation.metadataName,
|
||||
) as MetadataOneToManyRelatedMetadataNames<T>[];
|
||||
};
|
||||
+4
-10
@@ -1,21 +1,15 @@
|
||||
import { type AllMetadataName } from 'twenty-shared/metadata';
|
||||
|
||||
import { ALL_METADATA_RELATIONS } from 'src/engine/metadata-modules/flat-entity/constant/all-metadata-relations.constant';
|
||||
import { type MetadataManyToOneRelatedMetadataNames } from 'src/engine/metadata-modules/flat-entity/types/metadata-many-to-one-related-metadata-names.type';
|
||||
import { type MetadataOneToManyRelatedMetadataNames } from 'src/engine/metadata-modules/flat-entity/types/metadata-one-to-many-related-metadata-names.type';
|
||||
import { getMetadataManyToOneRelatedNames } from 'src/engine/metadata-modules/flat-entity/utils/get-metadata-many-to-one-related-names.util';
|
||||
import { getMetadataOneToManyRelatedNames } from 'src/engine/metadata-modules/flat-entity/utils/get-metadata-one-to-many-related-names.util';
|
||||
|
||||
export const getMetadataRelatedMetadataNames = <T extends AllMetadataName>(
|
||||
metadataName: T,
|
||||
) => {
|
||||
const relationProperties = ALL_METADATA_RELATIONS[metadataName];
|
||||
|
||||
const manyToOneMetadataNames = Object.values(relationProperties.manyToOne)
|
||||
.filter((relation) => relation !== null)
|
||||
.map((relation) => relation.metadataName);
|
||||
|
||||
const oneToManyMetadataNames = Object.values(relationProperties.oneToMany)
|
||||
.filter((relation) => relation !== null)
|
||||
.map((relation) => relation.metadataName);
|
||||
const manyToOneMetadataNames = getMetadataManyToOneRelatedNames(metadataName);
|
||||
const oneToManyMetadataNames = getMetadataOneToManyRelatedNames(metadataName);
|
||||
|
||||
return [
|
||||
...new Set([...manyToOneMetadataNames, ...oneToManyMetadataNames]),
|
||||
|
||||
+29
@@ -0,0 +1,29 @@
|
||||
import {
|
||||
ALL_METADATA_NAME,
|
||||
type AllMetadataName,
|
||||
} from 'twenty-shared/metadata';
|
||||
|
||||
import { getMetadataManyToOneRelatedNames } from 'src/engine/metadata-modules/flat-entity/utils/get-metadata-many-to-one-related-names.util';
|
||||
import { getMetadataOneToManyRelatedNames } from 'src/engine/metadata-modules/flat-entity/utils/get-metadata-one-to-many-related-names.util';
|
||||
|
||||
const computeDependencyScore = (metadataName: AllMetadataName): number => {
|
||||
const manyToOneCount = getMetadataManyToOneRelatedNames(metadataName).length;
|
||||
const oneToManyCount = getMetadataOneToManyRelatedNames(metadataName).length;
|
||||
|
||||
return manyToOneCount - oneToManyCount;
|
||||
};
|
||||
|
||||
export const sortMetadataNamesChildrenFirst = (): AllMetadataName[] => {
|
||||
const metadataNames = Object.keys(ALL_METADATA_NAME) as AllMetadataName[];
|
||||
|
||||
return metadataNames.sort((metadataNameA, metadataNameB) => {
|
||||
const scoreA = computeDependencyScore(metadataNameA);
|
||||
const scoreB = computeDependencyScore(metadataNameB);
|
||||
|
||||
if (scoreA !== scoreB) {
|
||||
return scoreB - scoreA;
|
||||
}
|
||||
|
||||
return metadataNameA.localeCompare(metadataNameB);
|
||||
});
|
||||
};
|
||||
+2
@@ -2,6 +2,7 @@ import {
|
||||
Column,
|
||||
CreateDateColumn,
|
||||
Entity,
|
||||
Index,
|
||||
ManyToOne,
|
||||
OneToMany,
|
||||
PrimaryGeneratedColumn,
|
||||
@@ -29,6 +30,7 @@ import { SyncableEntityRequired } from 'src/engine/workspace-manager/types/synca
|
||||
'namePlural',
|
||||
'workspaceId',
|
||||
])
|
||||
@Index('IDX_OBJECT_METADATA_DATA_SOURCE_ID', ['dataSourceId'])
|
||||
export class ObjectMetadataEntity
|
||||
extends SyncableEntityRequired
|
||||
implements Required<ObjectMetadataEntity>
|
||||
|
||||
+1
@@ -28,6 +28,7 @@ import { SyncableEntity } from 'src/engine/workspace-manager/types/syncable-enti
|
||||
['workspaceId', 'pageLayoutTabId'],
|
||||
{ where: '"deletedAt" IS NULL' },
|
||||
)
|
||||
@Index('IDX_PAGE_LAYOUT_WIDGET_OBJECT_METADATA_ID', ['objectMetadataId'])
|
||||
export class PageLayoutWidgetEntity<
|
||||
TWidgetConfigurationType extends
|
||||
WidgetConfigurationType = WidgetConfigurationType,
|
||||
|
||||
@@ -26,6 +26,7 @@ import { SyncableEntity } from 'src/engine/workspace-manager/types/syncable-enti
|
||||
@Index('IDX_ROLE_TARGET_WORKSPACE_ID', ['userWorkspaceId', 'workspaceId'])
|
||||
@Index('IDX_ROLE_TARGET_AGENT_ID', ['agentId'])
|
||||
@Index('IDX_ROLE_TARGET_API_KEY_ID', ['apiKeyId'])
|
||||
@Index('IDX_ROLE_TARGET_ROLE_ID', ['roleId'])
|
||||
@Check(
|
||||
'CHK_role_target_single_entity',
|
||||
'("agentId" IS NOT NULL AND "userWorkspaceId" IS NULL AND "apiKeyId" IS NULL) OR ("agentId" IS NULL AND "userWorkspaceId" IS NOT NULL AND "apiKeyId" IS NULL) OR ("agentId" IS NULL AND "userWorkspaceId" IS NULL AND "apiKeyId" IS NOT NULL)',
|
||||
|
||||
+2
@@ -3,6 +3,7 @@ import {
|
||||
Column,
|
||||
CreateDateColumn,
|
||||
Entity,
|
||||
Index,
|
||||
JoinColumn,
|
||||
ManyToOne,
|
||||
PrimaryGeneratedColumn,
|
||||
@@ -20,6 +21,7 @@ import { SyncableEntity } from 'src/engine/workspace-manager/types/syncable-enti
|
||||
'httpMethod',
|
||||
'workspaceId',
|
||||
])
|
||||
@Index('IDX_ROUTE_TRIGGER_SERVERLESS_FUNCTION_ID', ['serverlessFunctionId'])
|
||||
export class RouteTriggerEntity
|
||||
extends SyncableEntity
|
||||
implements Required<RouteTriggerEntity>
|
||||
|
||||
+3
@@ -29,6 +29,9 @@ import { SyncableEntity } from 'src/engine/workspace-manager/types/syncable-enti
|
||||
'roleId',
|
||||
'objectMetadataId',
|
||||
])
|
||||
@Index('IDX_RLPPG_PARENT_GROUP_ID', [
|
||||
'parentRowLevelPermissionPredicateGroupId',
|
||||
])
|
||||
export class RowLevelPermissionPredicateGroupEntity
|
||||
extends SyncableEntity
|
||||
implements
|
||||
|
||||
+3
@@ -32,6 +32,9 @@ import { SyncableEntity } from 'src/engine/workspace-manager/types/syncable-enti
|
||||
])
|
||||
@Index('IDX_RLPP_FIELD_METADATA_ID', ['fieldMetadataId'])
|
||||
@Index('IDX_RLPP_GROUP_ID', ['rowLevelPermissionPredicateGroupId'])
|
||||
@Index('IDX_RLPP_WORKSPACE_MEMBER_FIELD_METADATA_ID', [
|
||||
'workspaceMemberFieldMetadataId',
|
||||
])
|
||||
export class RowLevelPermissionPredicateEntity
|
||||
extends SyncableEntity
|
||||
implements
|
||||
|
||||
+1
@@ -31,6 +31,7 @@ export const DEFAULT_HANDLER_NAME = 'main';
|
||||
|
||||
@Entity('serverlessFunction')
|
||||
@Index('IDX_SERVERLESS_FUNCTION_ID_DELETED_AT', ['id', 'deletedAt'])
|
||||
@Index('IDX_SERVERLESS_FUNCTION_LAYER_ID', ['serverlessFunctionLayerId'])
|
||||
export class ServerlessFunctionEntity
|
||||
extends SyncableEntity
|
||||
implements Required<ServerlessFunctionEntity>
|
||||
|
||||
+2
-3
@@ -18,9 +18,8 @@ import { SyncableEntityRequired } from 'src/engine/workspace-manager/types/synca
|
||||
|
||||
@Entity({ name: 'viewField', schema: 'core' })
|
||||
@Index('IDX_VIEW_FIELD_WORKSPACE_ID_VIEW_ID', ['workspaceId', 'viewId'])
|
||||
@Index('IDX_VIEW_FIELD_VIEW_ID', ['viewId'], {
|
||||
where: '"deletedAt" IS NULL',
|
||||
})
|
||||
@Index('IDX_VIEW_FIELD_VIEW_ID', ['viewId'])
|
||||
@Index('IDX_VIEW_FIELD_FIELD_METADATA_ID', ['fieldMetadataId'])
|
||||
@Index(
|
||||
'IDX_VIEW_FIELD_FIELD_METADATA_ID_VIEW_ID_UNIQUE',
|
||||
['fieldMetadataId', 'viewId'],
|
||||
|
||||
+2
-3
@@ -19,9 +19,8 @@ import { SyncableEntity } from 'src/engine/workspace-manager/types/syncable-enti
|
||||
|
||||
@Entity({ name: 'viewFilterGroup', schema: 'core' })
|
||||
@Index('IDX_VIEW_FILTER_GROUP_WORKSPACE_ID_VIEW_ID', ['workspaceId', 'viewId'])
|
||||
@Index('IDX_VIEW_FILTER_GROUP_VIEW_ID', ['viewId'], {
|
||||
where: '"deletedAt" IS NULL',
|
||||
})
|
||||
@Index('IDX_VIEW_FILTER_GROUP_VIEW_ID', ['viewId'])
|
||||
@Index('IDX_VIEW_FILTER_GROUP_PARENT_ID', ['parentViewFilterGroupId'])
|
||||
export class ViewFilterGroupEntity extends SyncableEntity {
|
||||
@PrimaryGeneratedColumn('uuid')
|
||||
id: string;
|
||||
|
||||
+1
-3
@@ -20,9 +20,7 @@ import { SyncableEntityRequired } from 'src/engine/workspace-manager/types/synca
|
||||
|
||||
@Entity({ name: 'viewFilter', schema: 'core' })
|
||||
@Index('IDX_VIEW_FILTER_WORKSPACE_ID_VIEW_ID', ['workspaceId', 'viewId'])
|
||||
@Index('IDX_VIEW_FILTER_VIEW_ID', ['viewId'], {
|
||||
where: '"deletedAt" IS NULL',
|
||||
})
|
||||
@Index('IDX_VIEW_FILTER_VIEW_ID', ['viewId'])
|
||||
@Index('IDX_VIEW_FILTER_FIELD_METADATA_ID', ['fieldMetadataId'])
|
||||
export class ViewFilterEntity
|
||||
extends SyncableEntityRequired
|
||||
|
||||
+1
-3
@@ -16,9 +16,7 @@ import { SyncableEntity } from 'src/engine/workspace-manager/types/syncable-enti
|
||||
|
||||
@Entity({ name: 'viewGroup', schema: 'core' })
|
||||
@Index('IDX_VIEW_GROUP_WORKSPACE_ID_VIEW_ID', ['workspaceId', 'viewId'])
|
||||
@Index('IDX_VIEW_GROUP_VIEW_ID', ['viewId'], {
|
||||
where: '"deletedAt" IS NULL',
|
||||
})
|
||||
@Index('IDX_VIEW_GROUP_VIEW_ID', ['viewId'])
|
||||
export class ViewGroupEntity
|
||||
extends SyncableEntity
|
||||
implements Required<ViewGroupEntity>
|
||||
|
||||
+2
-3
@@ -18,9 +18,8 @@ import { SyncableEntity } from 'src/engine/workspace-manager/types/syncable-enti
|
||||
|
||||
@Entity({ name: 'viewSort', schema: 'core' })
|
||||
@Index('IDX_VIEW_SORT_WORKSPACE_ID_VIEW_ID', ['workspaceId', 'viewId'])
|
||||
@Index('IDX_VIEW_SORT_VIEW_ID', ['viewId'], {
|
||||
where: '"deletedAt" IS NULL',
|
||||
})
|
||||
@Index('IDX_VIEW_SORT_VIEW_ID', ['viewId'])
|
||||
@Index('IDX_VIEW_SORT_FIELD_METADATA_ID', ['fieldMetadataId'])
|
||||
@Index(
|
||||
'IDX_VIEW_SORT_FIELD_METADATA_ID_VIEW_ID_UNIQUE',
|
||||
['fieldMetadataId', 'viewId'],
|
||||
|
||||
@@ -36,6 +36,12 @@ import { SyncableEntityRequired } from 'src/engine/workspace-manager/types/synca
|
||||
'objectMetadataId',
|
||||
])
|
||||
@Index('IDX_VIEW_VISIBILITY', ['visibility'])
|
||||
@Index('IDX_VIEW_CALENDAR_FIELD_METADATA', ['calendarFieldMetadataId'])
|
||||
@Index('IDX_VIEW_KANBAN_FIELD_METADATA', [
|
||||
'kanbanAggregateOperationFieldMetadataId',
|
||||
])
|
||||
@Index('IDX_VIEW_MAIN_GROUP_BY_FIELD_METADATA', ['mainGroupByFieldMetadataId'])
|
||||
@Index('IDX_VIEW_CREATED_BY_USER_WORKSPACE', ['createdByUserWorkspaceId'])
|
||||
@Check(
|
||||
'CHK_VIEW_CALENDAR_INTEGRITY',
|
||||
`("type" != 'CALENDAR' OR ("calendarLayout" IS NOT NULL AND "calendarFieldMetadataId" IS NOT NULL))`,
|
||||
|
||||
Reference in New Issue
Block a user