-
Notifications
You must be signed in to change notification settings - Fork 5k
feat: new relation sync-metadata, twenty-orm, create/update #10217
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Conversation
45c4850 to
ed349c4
Compare
146ae15 to
6f38f6b
Compare
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
PR Summary
This PR introduces a significant architectural change in how relations are handled in the Twenty system, controlled by the isNewRelationEnabled feature flag.
Key changes:
- Drops creation of foreign key fields as
FieldMetadataand stops creatingRelationMetadataentities - Replaces
RelationMetadataTypewithRelationTypeacross all workspace entities - Introduces
WorkspaceSyncFieldMetadataRelationServicefor handling field metadata relations - Adds feature flag checks in query resolvers and formatters to support both old and new relation systems
- Implements new utilities like
determineSchemaRelationDetailsandconverRelationTypeToTypeORMRelationTypefor relation handling
The changes appear well-structured with proper feature flag controls for backward compatibility, though careful testing of database migrations and existing relation data is recommended.
123 file(s) reviewed, 28 comment(s)
Edit PR Review Bot Settings | Greptile
| // Temporary fix as we don't have relationId in the new relation | ||
| relationId: createDeterministicUuid([ | ||
| relation.sourceFieldMetadata.id, | ||
| relation.targetFieldMetadata.id, | ||
| ]), |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
style: Using field IDs for relation UUID could cause issues if fields are recreated with same configuration but different IDs. Consider using more stable identifiers.
| relation.sourceFieldMetadata.id, | ||
| relation.targetFieldMetadata.id, | ||
| ]), | ||
| direction: relation.type as unknown as RelationDefinitionType, |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
logic: Unsafe type casting from relationType to RelationDefinitionType. Should validate the values match before casting.
| const isNewRelationEnabled = | ||
| await this.featureFlagService.isFeatureEnabled( | ||
| FeatureFlagKey.IsNewRelationEnabled, | ||
| objectMetadataInput.workspaceId, | ||
| createdObjectMetadata, | ||
| { | ||
| primaryKeyFieldMetadataSettings: | ||
| objectMetadataInput.primaryKeyFieldMetadataSettings, | ||
| primaryKeyColumnType: objectMetadataInput.primaryKeyColumnType, | ||
| }, | ||
| ); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
logic: Feature flag check should be moved before table creation to ensure consistent state if creation fails
| if (input.update.isActive !== undefined) { | ||
| await this.objectMetadataRelationService.updateObjectRelationshipsActivationStatus( | ||
| input.id, | ||
| input.update.isActive, | ||
| ); | ||
| // For new relation system, the active status is stitched to the field metadata | ||
| if (!isNewRelationEnabled) { | ||
| await this.objectMetadataRelationService.updateObjectRelationshipsActivationStatus( | ||
| input.id, | ||
| input.update.isActive, | ||
| ); | ||
| } | ||
| } |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
style: Consider moving isActive handling to a separate method for better maintainability and reusability
| public async updateRelationMigrations( | ||
| currentObjectMetadata: ObjectMetadataEntity, | ||
| alteredObjectMetadata: ObjectMetadataEntity, | ||
| relationMetadataCollection: { | ||
| targetObjectMetadata: ObjectMetadataEntity; | ||
| targetFieldMetadata: FieldMetadataEntity; | ||
| sourceFieldMetadata: FieldMetadataEntity; | ||
| }[], | ||
| workspaceId: string, | ||
| ) { | ||
| for (const { targetObjectMetadata } of relationMetadataCollection) { | ||
| const targetTableName = computeObjectTargetTable(targetObjectMetadata); | ||
| const columnName = `${currentObjectMetadata.nameSingular}Id`; | ||
|
|
||
| await this.workspaceMigrationService.createCustomMigration( | ||
| generateMigrationName( | ||
| `rename-${currentObjectMetadata.nameSingular}-to-${alteredObjectMetadata.nameSingular}-in-${targetObjectMetadata.nameSingular}`, | ||
| ), | ||
| workspaceId, | ||
| [ | ||
| { | ||
| name: targetTableName, | ||
| action: WorkspaceMigrationTableActionType.ALTER, | ||
| columns: [ | ||
| { | ||
| action: WorkspaceMigrationColumnActionType.ALTER, | ||
| currentColumnDefinition: { | ||
| columnName, | ||
| columnType: 'uuid', | ||
| isNullable: true, | ||
| defaultValue: null, | ||
| }, | ||
| alteredColumnDefinition: { | ||
| columnName: `${alteredObjectMetadata.nameSingular}Id`, | ||
| columnType: 'uuid', | ||
| isNullable: true, | ||
| defaultValue: null, | ||
| }, | ||
| }, | ||
| ], | ||
| }, | ||
| ], | ||
| ); | ||
| } | ||
| } |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
style: Duplicates logic from createUpdateForeignKeysMigrations method - consider consolidating these methods to avoid maintenance issues
| Partial<ComputedPartialFieldMetadata<FieldMetadataType.RELATION>> & { | ||
| id: string; | ||
| } |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
logic: The id field is required in the create result but relations might not have an id at creation time. Consider making id optional for CREATE operations
| private readonly _fieldRelationMetadataCreateCollection: (Partial< | ||
| ComputedPartialFieldMetadata<FieldMetadataType.RELATION> | ||
| > & { | ||
| id: string; | ||
| })[] = []; | ||
| private readonly _fieldRelationMetadataUpdateCollection: (Partial< | ||
| ComputedPartialFieldMetadata<FieldMetadataType.RELATION> | ||
| > & { | ||
| id: string; | ||
| })[] = []; | ||
| private readonly _fieldRelationMetadataDeleteCollection: FieldMetadataEntity<FieldMetadataType.RELATION>[] = | ||
| []; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
logic: The field relation metadata collections require an id on creation, which differs from the regular field metadata collection that doesn't require an id. This inconsistency could lead to confusion and potential bugs.
| const isNewRelationEnabled = | ||
| context.featureFlags[FeatureFlagKey.IsNewRelationEnabled]; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
logic: Check that context.featureFlags exists before accessing IsNewRelationEnabled to prevent runtime errors
| const isNewRelationEnabled = | |
| context.featureFlags[FeatureFlagKey.IsNewRelationEnabled]; | |
| const isNewRelationEnabled = | |
| context.featureFlags?.[FeatureFlagKey.IsNewRelationEnabled] ?? false; |
| standardId: FAVORITE_STANDARD_FIELD_IDS.workflowVersion, | ||
| type: RelationMetadataType.MANY_TO_ONE, | ||
| type: RelationType.MANY_TO_ONE, | ||
| label: msg`Workflow`, | ||
| description: msg`Favorite workflow version`, |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
style: Label 'Workflow' is reused for three different relations (workflow, workflowVersion, workflowRun) which could cause confusion in the UI
| const isNewRelationEnabled = await this.featureFlagService.isFeatureEnabled( | ||
| FeatureFlagKey.IsNewRelationEnabled, | ||
| objectMetadataItemWithFieldsMaps.workspaceId, | ||
| ); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
style: Consider caching feature flag result at class level to avoid repeated DB calls for the same workspace
| relationType: RelationType; | ||
| onDelete?: RelationOnDeleteAction; | ||
| joinColumnName?: string; | ||
| joinColumnName?: string | null; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
can we avoid | null here and just rely on undefined?
Follow up from #10217 Adding onDelete action on Many_to_one sides
Follow up from #10217 Adding onDelete action on Many_to_one sides
Follow up from #10217 Adding onDelete action on Many_to_one sides
Fix twentyhq/core-team-issues#330 (comment) and twentyhq/core-team-issues#327 (comment)
What this PR does when
isNewRelationEnabledis set totrue:FieldMetadataRelationMetadataFieldMetadataof typeRELATIONduring the sync commandFieldMetadatarelations when we create a new objectdatabase:resetwith new relations