Import - Fix erase on update on Number, Boolean and Multi Select fields (#14686)

Before on an object with Number, Boolean, Multi-Select fields -->
createMany input from import feature with only one name firstName column
matched :

```
{
  "data": [
    {
      "booleanField": false,
      "createdBy": {
        "source": "IMPORT",
        "context": {}
      },
      "name": {
        "firstName": "Test"
      },
      "multiSelectField": [],
      "numberField": null
    },
    {
      "booleanField": false,
      "createdBy": {
        "source": "IMPORT",
        "context": {}
      },
      "name": {
        "firstName": "Test1"
      },
      "multiSelectField": [],
      "numberField": null
    }
  ],
  "upsert": true
}
```

After with same object : 


```
{
  "data": [
    {
      "createdBy": {
        "source": "IMPORT",
        "context": {}
      },
      "name": {
        "firstName": "Test"
      }
    },
    {
      "createdBy": {
        "source": "IMPORT",
        "context": {}
      },
      "name": {
        "firstName": "Test1"
      }
    }
  ],
  "upsert": true
}
```

Fixes https://github.com/twentyhq/twenty/issues/14672
This commit is contained in:
Etienne
2025-09-24 18:38:42 +02:00
committed by GitHub
parent 3cada58908
commit 950ce44341
2 changed files with 356 additions and 345 deletions
@@ -9,6 +9,302 @@ import { FieldMetadataType } from '~/generated-metadata/graphql';
import { RelationType } from '~/generated/graphql';
describe('buildRecordFromImportedStructuredRow', () => {
const fields: FieldMetadataItem[] = [
{
id: '3',
name: 'booleanField',
label: 'Boolean Field',
type: FieldMetadataType.BOOLEAN,
isNullable: true,
isActive: true,
isCustom: false,
isSystem: false,
createdAt: '2023-01-01',
updatedAt: '2023-01-01',
icon: 'IconCheck',
description: null,
},
{
id: '4',
name: 'numberField',
label: 'Number Field',
type: FieldMetadataType.NUMBER,
isNullable: true,
isActive: true,
isCustom: false,
isSystem: false,
createdAt: '2023-01-01',
updatedAt: '2023-01-01',
icon: 'IconNumber',
description: null,
},
{
id: '5',
name: 'multiSelectField',
label: 'Multi-Select Field',
type: FieldMetadataType.MULTI_SELECT,
isNullable: true,
isActive: true,
isCustom: false,
isSystem: false,
createdAt: '2023-01-01',
updatedAt: '2023-01-01',
icon: 'IconTag',
description: null,
options: [
{
id: '1',
value: 'tag1',
label: 'Tag 1',
color: 'blue',
position: 0,
},
{
id: '2',
value: 'tag2',
label: 'Tag 2',
color: 'red',
position: 1,
},
{
id: '3',
value: 'tag3',
label: 'Tag 3',
color: 'green',
position: 2,
},
],
},
{
id: '6',
name: 'relationField',
label: 'Relation Field',
type: FieldMetadataType.RELATION,
isNullable: true,
isActive: true,
isCustom: false,
isSystem: false,
createdAt: '2023-01-01',
updatedAt: '2023-01-01',
icon: 'IconBuilding',
description: null,
relation: {
type: RelationType.MANY_TO_ONE,
} as FieldMetadataItemRelation,
},
{
id: '7',
name: 'fullNameField',
label: 'Full Name Field',
type: FieldMetadataType.FULL_NAME,
isNullable: true,
isActive: true,
isCustom: false,
isSystem: false,
createdAt: '2023-01-01',
updatedAt: '2023-01-01',
icon: 'IconUser',
description: null,
},
{
id: '8',
name: 'currencyField',
label: 'Currency Field',
type: FieldMetadataType.CURRENCY,
isNullable: true,
isActive: true,
isCustom: false,
isSystem: false,
createdAt: '2023-01-01',
updatedAt: '2023-01-01',
icon: 'IconCurrencyDollar',
description: null,
},
{
id: '9',
name: 'addressField',
label: 'Address Field',
type: FieldMetadataType.ADDRESS,
isNullable: true,
isActive: true,
isCustom: false,
isSystem: false,
createdAt: '2023-01-01',
updatedAt: '2023-01-01',
icon: 'IconMap',
description: null,
},
{
id: '10',
name: 'selectField',
label: 'Select Field',
type: FieldMetadataType.SELECT,
isNullable: true,
isActive: true,
isCustom: false,
isSystem: false,
createdAt: '2023-01-01',
updatedAt: '2023-01-01',
icon: 'IconTag',
description: null,
options: [
{
id: '1',
value: 'option1',
label: 'Option 1',
color: 'blue',
position: 0,
},
{
id: '2',
value: 'option2',
label: 'Option 2',
color: 'red',
position: 1,
},
],
},
{
id: '11',
name: 'arrayField',
label: 'Array Field',
type: FieldMetadataType.ARRAY,
isNullable: true,
isActive: true,
isCustom: false,
isSystem: false,
createdAt: '2023-01-01',
updatedAt: '2023-01-01',
icon: 'IconBracketsContain',
description: null,
},
{
id: '12',
name: 'jsonField',
label: 'JSON Field',
type: FieldMetadataType.RAW_JSON,
isNullable: true,
isActive: true,
isCustom: false,
isSystem: false,
createdAt: '2023-01-01',
updatedAt: '2023-01-01',
icon: 'IconBraces',
description: null,
},
{
id: '13',
name: 'phoneField',
label: 'Phone Field',
type: FieldMetadataType.PHONES,
isNullable: true,
isActive: true,
isCustom: false,
isSystem: false,
createdAt: '2023-01-01',
updatedAt: '2023-01-01',
icon: 'IconPhone',
description: null,
},
{
id: '14',
name: 'linksField',
label: 'Links Field',
type: FieldMetadataType.LINKS,
isNullable: true,
isActive: true,
isCustom: false,
isSystem: false,
createdAt: '2023-01-01',
updatedAt: '2023-01-01',
icon: 'IconWorld',
description: null,
},
{
id: '15',
name: 'createdBy',
label: 'Created by',
type: FieldMetadataType.ACTOR,
isNullable: true,
isActive: true,
isCustom: false,
isSystem: false,
createdAt: '2023-01-01',
updatedAt: '2023-01-01',
icon: 'IconUsers',
description: null,
},
{
id: '16',
name: 'richTextField',
label: 'Rich Text Field',
type: FieldMetadataType.RICH_TEXT_V2,
isNullable: true,
isActive: true,
isCustom: false,
isSystem: false,
createdAt: '2023-01-01',
updatedAt: '2023-01-01',
icon: 'IconTextEditor',
description: null,
},
{
id: '17',
name: 'dateField',
label: 'Date Field',
type: FieldMetadataType.DATE,
isNullable: true,
isActive: true,
isCustom: false,
isSystem: false,
createdAt: '2023-01-01',
updatedAt: '2023-01-01',
icon: 'IconCalendarEvent',
description: null,
},
{
id: '18',
name: 'dateTimeField',
label: 'Date Time Field',
type: FieldMetadataType.DATE_TIME,
isNullable: true,
isActive: true,
isCustom: false,
isSystem: false,
createdAt: '2023-01-01',
updatedAt: '2023-01-01',
icon: 'IconCalendarClock',
description: null,
},
{
id: '19',
name: 'ratingField',
label: 'Rating Field',
type: FieldMetadataType.RATING,
isNullable: true,
isActive: true,
isCustom: false,
isSystem: false,
createdAt: '2023-01-01',
updatedAt: '2023-01-01',
icon: 'IconStar',
description: null,
},
{
id: '20',
name: 'emailField',
label: 'Email Field',
type: FieldMetadataType.EMAILS,
isNullable: true,
isActive: true,
isCustom: false,
isSystem: false,
createdAt: '2023-01-01',
updatedAt: '2023-01-01',
icon: 'IconMail',
description: null,
},
];
it('should successfully build a record from imported structured row', () => {
const importedStructuredRow: ImportedStructuredRow = {
booleanField: 'true',
@@ -48,303 +344,6 @@ describe('buildRecordFromImportedStructuredRow', () => {
'[{"url": "https://github.com/user", "label": "GitHub"}]',
};
const fields: FieldMetadataItem[] = [
{
id: '3',
name: 'booleanField',
label: 'Boolean Field',
type: FieldMetadataType.BOOLEAN,
isNullable: true,
isActive: true,
isCustom: false,
isSystem: false,
createdAt: '2023-01-01',
updatedAt: '2023-01-01',
icon: 'IconCheck',
description: null,
},
{
id: '4',
name: 'numberField',
label: 'Number Field',
type: FieldMetadataType.NUMBER,
isNullable: true,
isActive: true,
isCustom: false,
isSystem: false,
createdAt: '2023-01-01',
updatedAt: '2023-01-01',
icon: 'IconNumber',
description: null,
},
{
id: '5',
name: 'multiSelectField',
label: 'Multi-Select Field',
type: FieldMetadataType.MULTI_SELECT,
isNullable: true,
isActive: true,
isCustom: false,
isSystem: false,
createdAt: '2023-01-01',
updatedAt: '2023-01-01',
icon: 'IconTag',
description: null,
options: [
{
id: '1',
value: 'tag1',
label: 'Tag 1',
color: 'blue',
position: 0,
},
{
id: '2',
value: 'tag2',
label: 'Tag 2',
color: 'red',
position: 1,
},
{
id: '3',
value: 'tag3',
label: 'Tag 3',
color: 'green',
position: 2,
},
],
},
{
id: '6',
name: 'relationField',
label: 'Relation Field',
type: FieldMetadataType.RELATION,
isNullable: true,
isActive: true,
isCustom: false,
isSystem: false,
createdAt: '2023-01-01',
updatedAt: '2023-01-01',
icon: 'IconBuilding',
description: null,
relation: {
type: RelationType.MANY_TO_ONE,
} as FieldMetadataItemRelation,
},
{
id: '7',
name: 'fullNameField',
label: 'Full Name Field',
type: FieldMetadataType.FULL_NAME,
isNullable: true,
isActive: true,
isCustom: false,
isSystem: false,
createdAt: '2023-01-01',
updatedAt: '2023-01-01',
icon: 'IconUser',
description: null,
},
{
id: '8',
name: 'currencyField',
label: 'Currency Field',
type: FieldMetadataType.CURRENCY,
isNullable: true,
isActive: true,
isCustom: false,
isSystem: false,
createdAt: '2023-01-01',
updatedAt: '2023-01-01',
icon: 'IconCurrencyDollar',
description: null,
},
{
id: '9',
name: 'addressField',
label: 'Address Field',
type: FieldMetadataType.ADDRESS,
isNullable: true,
isActive: true,
isCustom: false,
isSystem: false,
createdAt: '2023-01-01',
updatedAt: '2023-01-01',
icon: 'IconMap',
description: null,
},
{
id: '10',
name: 'selectField',
label: 'Select Field',
type: FieldMetadataType.SELECT,
isNullable: true,
isActive: true,
isCustom: false,
isSystem: false,
createdAt: '2023-01-01',
updatedAt: '2023-01-01',
icon: 'IconTag',
description: null,
options: [
{
id: '1',
value: 'option1',
label: 'Option 1',
color: 'blue',
position: 0,
},
{
id: '2',
value: 'option2',
label: 'Option 2',
color: 'red',
position: 1,
},
],
},
{
id: '11',
name: 'arrayField',
label: 'Array Field',
type: FieldMetadataType.ARRAY,
isNullable: true,
isActive: true,
isCustom: false,
isSystem: false,
createdAt: '2023-01-01',
updatedAt: '2023-01-01',
icon: 'IconBracketsContain',
description: null,
},
{
id: '12',
name: 'jsonField',
label: 'JSON Field',
type: FieldMetadataType.RAW_JSON,
isNullable: true,
isActive: true,
isCustom: false,
isSystem: false,
createdAt: '2023-01-01',
updatedAt: '2023-01-01',
icon: 'IconBraces',
description: null,
},
{
id: '13',
name: 'phoneField',
label: 'Phone Field',
type: FieldMetadataType.PHONES,
isNullable: true,
isActive: true,
isCustom: false,
isSystem: false,
createdAt: '2023-01-01',
updatedAt: '2023-01-01',
icon: 'IconPhone',
description: null,
},
{
id: '14',
name: 'linksField',
label: 'Links Field',
type: FieldMetadataType.LINKS,
isNullable: true,
isActive: true,
isCustom: false,
isSystem: false,
createdAt: '2023-01-01',
updatedAt: '2023-01-01',
icon: 'IconWorld',
description: null,
},
{
id: '15',
name: 'createdBy',
label: 'Created by',
type: FieldMetadataType.ACTOR,
isNullable: true,
isActive: true,
isCustom: false,
isSystem: false,
createdAt: '2023-01-01',
updatedAt: '2023-01-01',
icon: 'IconUsers',
description: null,
},
{
id: '16',
name: 'richTextField',
label: 'Rich Text Field',
type: FieldMetadataType.RICH_TEXT_V2,
isNullable: true,
isActive: true,
isCustom: false,
isSystem: false,
createdAt: '2023-01-01',
updatedAt: '2023-01-01',
icon: 'IconTextEditor',
description: null,
},
{
id: '17',
name: 'dateField',
label: 'Date Field',
type: FieldMetadataType.DATE,
isNullable: true,
isActive: true,
isCustom: false,
isSystem: false,
createdAt: '2023-01-01',
updatedAt: '2023-01-01',
icon: 'IconCalendarEvent',
description: null,
},
{
id: '18',
name: 'dateTimeField',
label: 'Date Time Field',
type: FieldMetadataType.DATE_TIME,
isNullable: true,
isActive: true,
isCustom: false,
isSystem: false,
createdAt: '2023-01-01',
updatedAt: '2023-01-01',
icon: 'IconCalendarClock',
description: null,
},
{
id: '19',
name: 'ratingField',
label: 'Rating Field',
type: FieldMetadataType.RATING,
isNullable: true,
isActive: true,
isCustom: false,
isSystem: false,
createdAt: '2023-01-01',
updatedAt: '2023-01-01',
icon: 'IconStar',
description: null,
},
{
id: '20',
name: 'emailField',
label: 'Email Field',
type: FieldMetadataType.EMAILS,
isNullable: true,
isActive: true,
isCustom: false,
isSystem: false,
createdAt: '2023-01-01',
updatedAt: '2023-01-01',
icon: 'IconMail',
description: null,
},
];
const spreadsheetImportFields = [
{
fieldMetadataItemId: '6',
@@ -381,7 +380,6 @@ describe('buildRecordFromImportedStructuredRow', () => {
},
},
},
relationFieldId: undefined,
selectField: 'option1',
arrayField: ['item1', 'item2', 'item3'],
jsonField: { key: 'value', nested: { prop: 'data' } },
@@ -401,6 +399,10 @@ describe('buildRecordFromImportedStructuredRow', () => {
addressState: 'NY',
addressCountry: 'USA',
},
createdBy: {
source: 'IMPORT',
context: {},
},
phoneField: {
primaryPhoneNumber: '+1-555-0123',
primaryPhoneCountryCode: 'US',
@@ -423,10 +425,6 @@ describe('buildRecordFromImportedStructuredRow', () => {
},
],
},
createdBy: {
source: 'IMPORT',
context: {},
},
richTextField: {
blocknote: 'Rich content in blocknote format',
markdown: 'Content in markdown format',
@@ -442,23 +440,6 @@ describe('buildRecordFromImportedStructuredRow', () => {
'Primary Phone Number (phoneField)': '5550123',
};
const fields: FieldMetadataItem[] = [
{
id: '13',
name: 'phoneField',
label: 'Phone Field',
type: FieldMetadataType.PHONES,
isNullable: true,
isActive: true,
isCustom: false,
isSystem: false,
createdAt: '2023-01-01',
updatedAt: '2023-01-01',
icon: 'IconPhone',
description: null,
},
];
const result = buildRecordFromImportedStructuredRow({
importedStructuredRow,
fieldMetadataItems: fields,
@@ -470,6 +451,10 @@ describe('buildRecordFromImportedStructuredRow', () => {
primaryPhoneNumber: '5550123',
primaryPhoneCallingCode: '+1',
},
createdBy: {
source: 'IMPORT',
context: {},
},
});
});
@@ -478,26 +463,6 @@ describe('buildRecordFromImportedStructuredRow', () => {
'emailField (relationField)': 'john.doe@example.com',
};
const fields: FieldMetadataItem[] = [
{
id: '6',
name: 'relationField',
label: 'Relation Field',
type: FieldMetadataType.RELATION,
isNullable: true,
isActive: true,
isCustom: false,
isSystem: false,
createdAt: '2023-01-01',
updatedAt: '2023-01-01',
icon: 'IconBuilding',
description: null,
relation: {
type: RelationType.MANY_TO_ONE,
} as FieldMetadataItemRelation,
},
];
const spreadsheetImportFields = [
{
fieldMetadataItemId: '6',
@@ -530,6 +495,42 @@ describe('buildRecordFromImportedStructuredRow', () => {
},
},
},
createdBy: {
source: 'IMPORT',
context: {},
},
});
});
it('should return empty record for empty imported row', () => {
const importedStructuredRow: ImportedStructuredRow = {};
const spreadsheetImportFields = [
{
fieldMetadataItemId: '6',
isNestedField: false,
isRelationConnectField: true,
label: 'Relation Field / Name Field',
key: 'nameField (relationField)',
fieldMetadataType: FieldMetadataType.RELATION,
uniqueFieldMetadataItem: {
name: 'nameField',
type: FieldMetadataType.TEXT,
},
},
] as SpreadsheetImportField[];
const result = buildRecordFromImportedStructuredRow({
importedStructuredRow,
fieldMetadataItems: fields,
spreadsheetImportFields,
});
expect(result).toEqual({
createdBy: {
source: 'IMPORT',
context: {},
},
});
});
});
@@ -287,19 +287,27 @@ export const buildRecordFromImportedStructuredRow = ({
break;
}
case FieldMetadataType.BOOLEAN:
recordToBuild[field.name] =
importedFieldValue === 'true' || importedFieldValue === true;
if (isDefined(importedFieldValue)) {
recordToBuild[field.name] =
importedFieldValue === 'true' || importedFieldValue === true;
}
break;
case FieldMetadataType.NUMBER:
case FieldMetadataType.NUMERIC:
recordToBuild[field.name] = Number(importedFieldValue);
if (isDefined(importedFieldValue)) {
recordToBuild[field.name] = Number(importedFieldValue);
}
break;
case FieldMetadataType.RELATION: {
recordToBuild[field.name] = buildRelationConnectFieldRecord(
const relationConnectFieldValue = buildRelationConnectFieldRecord(
field,
importedStructuredRow,
spreadsheetImportFields,
);
if (isDefined(relationConnectFieldValue)) {
recordToBuild[field.name] = relationConnectFieldValue;
}
break;
}
case FieldMetadataType.ACTOR:
@@ -310,8 +318,10 @@ export const buildRecordFromImportedStructuredRow = ({
break;
case FieldMetadataType.ARRAY:
case FieldMetadataType.MULTI_SELECT: {
recordToBuild[field.name] =
stringArrayJSONSchema.parse(importedFieldValue);
if (isDefined(importedFieldValue)) {
recordToBuild[field.name] =
stringArrayJSONSchema.parse(importedFieldValue);
}
break;
}
case FieldMetadataType.RAW_JSON: {