From 2028a8f9beed3fe7222bfb09b2b4c49f1998498c Mon Sep 17 00:00:00 2001 From: Etienne <45695613+etiennejouan@users.noreply.github.com> Date: Tue, 25 Nov 2025 14:07:32 +0100 Subject: [PATCH] Null equivalence - Fix (#16050) @charlesBochet In workflow codebase, NULL (instead of empty object) is expected on workflow version object step field, when a new workflow is created for example. (packages/twenty-front/src/modules/workflow/workflow-diagram/utils/generateWorkflowDiagram.ts - 42) This case is not isolated and it creates many issues. We decided to format NULL value to equivalent (empty string for text field, empty object for raw_json) but it seems it complicates the dev x. To unlock @Devessier I prefer revert the logic, the time we discuss how to solve this cases. --- .../src/engine/twenty-orm/utils/format-result.util.ts | 5 ----- .../standard-objects/workflow.workspace-entity.ts | 7 ++++--- .../workflow-trigger/jobs/workflow-trigger.job.ts | 2 +- ...ul-create-input-by-field-metadata-type.constant.ts | 11 +++-------- ...ul-filter-input-by-field-metadata-type.constant.ts | 4 ++-- 5 files changed, 10 insertions(+), 19 deletions(-) diff --git a/packages/twenty-server/src/engine/twenty-orm/utils/format-result.util.ts b/packages/twenty-server/src/engine/twenty-orm/utils/format-result.util.ts index 7aa7a4cfd2..70949f216e 100644 --- a/packages/twenty-server/src/engine/twenty-orm/utils/format-result.util.ts +++ b/packages/twenty-server/src/engine/twenty-orm/utils/format-result.util.ts @@ -10,7 +10,6 @@ import { isDefined } from 'twenty-shared/utils'; import { DEFAULT_ARRAY_FIELD_NULL_EQUIVALENT_VALUE, DEFAULT_COMPOSITE_FIELDS_NULL_EQUIVALENT_VALUE, - DEFAULT_RAW_JSON_FIELD_NULL_EQUIVALENT_VALUE, DEFAULT_TEXT_FIELD_NULL_EQUIVALENT_VALUE, } from 'src/engine/api/common/common-args-processors/data-arg-processor/constants/null-equivalent-values.constant'; import { type FieldMetadataEntity } from 'src/engine/metadata-modules/field-metadata/field-metadata.entity'; @@ -199,10 +198,6 @@ function formatFieldMetadataValue( return DEFAULT_ARRAY_FIELD_NULL_EQUIVALENT_VALUE; } - if (fieldMetadataType === FieldMetadataType.RAW_JSON) { - return DEFAULT_RAW_JSON_FIELD_NULL_EQUIVALENT_VALUE; - } - if (fieldMetadataType === FieldMetadataType.TEXT) { return DEFAULT_TEXT_FIELD_NULL_EQUIVALENT_VALUE; } diff --git a/packages/twenty-server/src/modules/workflow/common/standard-objects/workflow.workspace-entity.ts b/packages/twenty-server/src/modules/workflow/common/standard-objects/workflow.workspace-entity.ts index 14b1d1c47e..d0f2590fcc 100644 --- a/packages/twenty-server/src/modules/workflow/common/standard-objects/workflow.workspace-entity.ts +++ b/packages/twenty-server/src/modules/workflow/common/standard-objects/workflow.workspace-entity.ts @@ -1,10 +1,10 @@ import { msg } from '@lingui/core/macro'; +import { STANDARD_OBJECT_IDS } from 'twenty-shared/metadata'; import { + ActorMetadata, FieldMetadataType, RelationOnDeleteAction, - ActorMetadata, } from 'twenty-shared/types'; -import { STANDARD_OBJECT_IDS } from 'twenty-shared/metadata'; import { RelationType } from 'src/engine/metadata-modules/field-metadata/interfaces/relation-type.interface'; import { Relation } from 'src/engine/workspace-manager/workspace-sync-metadata/interfaces/relation.interface'; @@ -85,7 +85,8 @@ export class WorkflowWorkspaceEntity extends BaseWorkspaceEntity { description: msg`The workflow name`, icon: 'IconSettingsAutomation', }) - name: string; + @WorkspaceIsNullable() + name: string | null; @WorkspaceField({ standardId: WORKFLOW_STANDARD_FIELD_IDS.lastPublishedVersionId, diff --git a/packages/twenty-server/src/modules/workflow/workflow-trigger/jobs/workflow-trigger.job.ts b/packages/twenty-server/src/modules/workflow/workflow-trigger/jobs/workflow-trigger.job.ts index 87eea27f0a..54231583e5 100644 --- a/packages/twenty-server/src/modules/workflow/workflow-trigger/jobs/workflow-trigger.job.ts +++ b/packages/twenty-server/src/modules/workflow/workflow-trigger/jobs/workflow-trigger.job.ts @@ -1,8 +1,8 @@ import { Scope } from '@nestjs/common'; -import { isDefined } from 'class-validator'; import isEmpty from 'lodash.isempty'; import { FieldActorSource } from 'twenty-shared/types'; +import { isDefined } from 'twenty-shared/utils'; import { InjectMessageQueue } from 'src/engine/core-modules/message-queue/decorators/message-queue.decorator'; import { Process } from 'src/engine/core-modules/message-queue/decorators/process.decorator'; diff --git a/packages/twenty-server/test/integration/graphql/suites/inputs-validation/create-validation/constants/successful-create-input-by-field-metadata-type.constant.ts b/packages/twenty-server/test/integration/graphql/suites/inputs-validation/create-validation/constants/successful-create-input-by-field-metadata-type.constant.ts index 6175e831d4..d67fa7c8f3 100644 --- a/packages/twenty-server/test/integration/graphql/suites/inputs-validation/create-validation/constants/successful-create-input-by-field-metadata-type.constant.ts +++ b/packages/twenty-server/test/integration/graphql/suites/inputs-validation/create-validation/constants/successful-create-input-by-field-metadata-type.constant.ts @@ -1,6 +1,7 @@ import { type FieldMetadataTypesToTestForCreateInputValidation } from 'test/integration/graphql/suites/inputs-validation/types/field-metadata-type-to-test'; import { TEST_TARGET_OBJECT_RECORD_ID_FIELD_VALUE } from 'test/integration/graphql/suites/inputs-validation/utils/setup-test-objects-with-all-field-types.util'; import { FieldMetadataType } from 'twenty-shared/types'; +import { isEmptyObject } from 'twenty-shared/utils'; export const successfulCreateInputByFieldMetadataType: { [K in Exclude< @@ -134,10 +135,7 @@ export const successfulCreateInputByFieldMetadataType: { rawJsonField: {}, }, validateInput: (record: Record) => { - return ( - typeof record.rawJsonField === 'object' && - Object.keys(record.rawJsonField).length === 0 - ); + return isEmptyObject(record.rawJsonField); }, }, { @@ -145,10 +143,7 @@ export const successfulCreateInputByFieldMetadataType: { rawJsonField: null, }, validateInput: (record: Record) => { - return ( - typeof record.rawJsonField === 'object' && - Object.keys(record.rawJsonField).length === 0 - ); + return record.rawJsonField === null; }, }, { diff --git a/packages/twenty-server/test/integration/graphql/suites/inputs-validation/filter-validation/constants/successful-filter-input-by-field-metadata-type.constant.ts b/packages/twenty-server/test/integration/graphql/suites/inputs-validation/filter-validation/constants/successful-filter-input-by-field-metadata-type.constant.ts index 1dee44c6ca..bfc90aef18 100644 --- a/packages/twenty-server/test/integration/graphql/suites/inputs-validation/filter-validation/constants/successful-filter-input-by-field-metadata-type.constant.ts +++ b/packages/twenty-server/test/integration/graphql/suites/inputs-validation/filter-validation/constants/successful-filter-input-by-field-metadata-type.constant.ts @@ -4,7 +4,7 @@ import { TEST_UUID_FIELD_VALUE, } from 'test/integration/graphql/suites/inputs-validation/utils/setup-test-objects-with-all-field-types.util'; import { FieldMetadataType } from 'twenty-shared/types'; -import { isDefined, isEmptyObject } from 'twenty-shared/utils'; +import { isDefined } from 'twenty-shared/utils'; export const successfulFilterInputByFieldMetadataType: { [K in FieldMetadataTypesToTestForFilterInputValidation]: { @@ -895,7 +895,7 @@ export const successfulFilterInputByFieldMetadataType: { gqlFilterInput: { rawJsonField: { is: 'NULL' } }, restFilterInput: 'rawJsonField[is]:NULL', validateFilter: (record: Record) => { - return isEmptyObject(record.rawJsonField); + return record.rawJsonField === null; }, }, {