Skip to content

Fix CSV import upsert #12048

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

Merged
merged 4 commits into from
May 15, 2025
Merged

Fix CSV import upsert #12048

merged 4 commits into from
May 15, 2025

Conversation

ijreilly
Copy link
Collaborator

@ijreilly ijreilly commented May 14, 2025

Fixes #11864 and twentyhq/core-team-issues#908

We should not send createManyXXX mutations with FE-forged ids in the payload if we want to do an upsert, because that 1) prevents records from being merged 2) triggers optimistic rendering while we can't know before-hand which records will actually be created and which records will only be updated

Also noticed createdBy was being overriden even for records we are updating and not creating, which did not seem right, so fixed that too

Copy link
Contributor

@greptile-apps greptile-apps bot left a 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 addresses CSV import functionality issues with a focus on preventing duplicate records and proper handling of the 'createdBy' field. Here's a summary of the key changes:

  • Modified useCreateManyRecords to skip frontend-generated IDs during upsert operations to enable proper record merging
  • Added conditional optimistic updates in useCreateManyRecords to only trigger when not performing upserts
  • Introduced shouldNotEraseCreatedByField parameter in formatData.util.ts to preserve createdBy field during updates
  • Enhanced graphql-query-create-many-resolver.service.ts to properly handle unique field constraints during upserts
  • Improved handling of email-based record matching when importing without explicit record IDs

The changes look well-structured and directly address the reported issues with CSV imports creating duplicates instead of updating existing records.

3 file(s) reviewed, no comment(s)
Edit PR Review Bot Settings | Greptile

@@ -47,6 +48,15 @@ export function formatData<T>(
objectMetadataItemWithFieldMaps.fieldsByName[key] ||
fieldMetadataByJoinColumnName.get(key);

if (
Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

may seem very custom but this still object-agnostic as we have it on all objects subject to user changes

@FelixMalfait FelixMalfait self-assigned this May 14, 2025
@@ -270,9 +270,12 @@ export class GraphqlQueryCreateManyResolverService extends GraphqlQueryBaseResol
// For insert, formating is done in the server
// While for update, formatting is done at the resolver level

const shouldNotEraseCreatedByField = true;
Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I did not add it to create-one as we don't have the "complex" merging logic that we have here for createMany, probably because this is useful for csv imports only which only uses createMany

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

if you use destructured parameters you won't need that 'hard coded' constant which seems rough at first sight

Copy link
Contributor

github-actions bot commented May 14, 2025

🚀 Preview Environment Ready!

Your preview environment is available at: http://bore.pub:9408

This environment will automatically shut down when the PR is closed or after 5 hours.

@ijreilly
Copy link
Collaborator Author

TODO: add a FE test if possible

@FelixMalfait FelixMalfait removed their assignment May 15, 2025
Copy link
Contributor

@martmull martmull left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hey, I have some comments about the code organization, but the logic looks good to me.

@@ -270,9 +270,12 @@ export class GraphqlQueryCreateManyResolverService extends GraphqlQueryBaseResol
// For insert, formating is done in the server
// While for update, formatting is done at the resolver level

const shouldNotEraseCreatedByField = true;

const formattedRecord = formatData(
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think we need destructured parameters for more than 3 params

@@ -270,9 +270,12 @@ export class GraphqlQueryCreateManyResolverService extends GraphqlQueryBaseResol
// For insert, formating is done in the server
// While for update, formatting is done at the resolver level

const shouldNotEraseCreatedByField = true;
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

if you use destructured parameters you won't need that 'hard coded' constant which seems rough at first sight

const formattedRecord = formatData(
record,
objectMetadataItemWithFieldMaps,
shouldNotEraseCreatedByField,
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

i don't feel that this parameter is very relevant for a formatData method. Maybe we should create a small utils removeNotErasableFields or something like that to do that before calling formatData (which in my opinion should remains agnostic of any specific field)

Comment on lines 51 to 58
if (
fieldMetadata?.name === 'createdBy' &&
fieldMetadata.isCustom === false
) {
if (shouldNotEraseCreatedByField) {
continue;
}
}
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

see comment above, i would keep formatData agnostic and remove the createdBy field from data in a dedicated utils before calling formatData

Copy link
Contributor

@martmull martmull left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

seen together, lgtm

@ijreilly ijreilly merged commit f782f4d into main May 15, 2025
52 checks passed
@ijreilly ijreilly deleted the fix-csv-import-duplicate branch May 15, 2025 12:44
jordan-chalupka added a commit to InsurOS/twenty that referenced this pull request May 15, 2025
* Messaging issues (twentyhq#12041)

messageImportException
for messaging and calendar
add more context to the Sentry Exceptions

Fixes twentyhq#11994

* [Rest Api] Fix find duplicates endpoint (twentyhq#12044)

- fix endpoint
- migrate to new rest api v2 service
- add integration test

* Correct default fallback logo path in Logo component (twentyhq#12053)

closes twentyhq#11849
The Logo component’s fallback URL was pointing to
`/icons/android/android-launchericon-192-192.png`, but the asset lives
under `/images/icons/....`. This updates defaultPrimaryLogoUrl to use
the correct `/images/icons/android/android-launchericon-192-192.png`
path, restoring the default logo display when no primaryLogo prop is
provided.

* Fix kanban loading bug (twentyhq#12042)

Fixes twentyhq/core-team-issues#956

This PR fixes a bug that appeared when switching between two kanban
views multiple times.

Steps to reproduce : 
- Go to kanban view A
- Go to kanban view B
- Go back to kanban view A

Video before : 


https://github.com/user-attachments/assets/4fa789ae-7187-498e-82b4-ee7896cd95d1

Video after : 


https://github.com/user-attachments/assets/2b323a2d-2f76-405d-9abd-38fe72ee2214

The problem was that we allowed a hook to take a nullable parameter that
can be nullable between page switch, and threw when it was undefined.

In order to be more cautious in the future, let's be sure that we don't
throw for undefined props of a hook, when it is expected that those
props can be in an undefined state for several React render loops, while
fetching new data.

Here I identified the parent effect : `<RecordIndexBoardDataLoader />`
that loads all states for a board when we switch between views, for each
column, by calling `<RecordIndexBoardColumnLoaderEffect />` in a
`.map()`.

Each `RecordIndexBoardColumnLoaderEffect` was calling
`useLoadRecordIndexBoardColumn` with a potentially undefined
`boardFieldMetadataId`.

So to fix this, I cut the render flow higher than the throw in
`useLoadRecordIndexBoardColumn`, and by doing that I was able to ensure
that `recordIndexKanbanFieldMetadataItem` in
`RecordIndexBoardDataLoader` was already defined when using it inside
`useLoadRecordIndexBoardColumn`.

`recordIndexKanbanFieldMetadataItem` was unnecessarily fetched two
times, one time in `RecordIndexBoardDataLoader` and another time in its
child `useLoadRecordIndexBoardColumn` hook.

By implementing this flow-cut higher up, I could then remove the `|
null` in TypeScript in children components, and expect a defined value
in all the children of `RecordIndexBoardDataLoader`, thus removing the
need to ask ourselves if we should throw or not in
`useLoadRecordIndexBoardColumn`.

* Validate existing fields on creation (twentyhq#12057)

Fixes twentyhq#12040

When fields are deleted but still used in workflows we do not update
create record action settings.
It breaks all following workflow execution and the user cannot update
the settings anymore.

This PR fixes the bug by filtering on existing fields.

Next step will be to clean settings on field deletion. Adding it to fast
follows.

Also lowering throttle limit because some infinite loops are not
catched.

* Exclude workflows from relation field object dropdown when inactive (twentyhq#12033)

closes twentyhq#11996
- Switched the “Object destination” select in
SettingsDataModelFieldRelationForm to use
activeObjectMetadataItems.filter(...) so workflows, system, and remote
objects are excluded
- Updated useRelationSettingsFormInitialValues to fall back on the same
filtered activeObjectMetadataItems list for its initial value

This ensures workflows (and any unwanted system/remote objects) no
longer show up in the dropdown or as the default.

* Fixed error with previous filters on ACTOR with new sub-field filtering (twentyhq#12050)

Fixes twentyhq/core-team-issues#969 that
appeared since the new ACTOR sub-field filtering that changed the
default sub-field filtering from name to source.

Now we apply any existing filter value on an ACTOR field, whether for a
sub-field or not, to the name sub-field by default.

If the user wants to create a sub-field filter on source, he has to
create a new advanced filter.

Fixes twentyhq/core-team-issues#967

---------

Co-authored-by: Félix Malfait <[email protected]>

* [reconnect account] Reseting calendar status and stage on reconnect (twentyhq#12061)

## Description
When a calendar channel fails, its status is not reset during the
reconnect step.

In some cases, resetting is necessary—especially when we’ve deployed a
fix and users try to reconnect their accounts after the patch.

## Why reseting to initial state
Our processes are idempotent, so we can safely set the status to null to
restart the flow. This approach covers all cases and avoids edge
conditions caused by inconsistent statuses.


Fixes : twentyhq#12026

* Make workflow custom fields editable (twentyhq#12063)

Fixes twentyhq#11989

<img width="1267" alt="Capture d’écran 2025-05-15 à 14 05 15"
src="https://github.com/user-attachments/assets/fbb22f52-2c76-424f-8b8c-fb030fef9fa8"
/>

`isCustom` was not properly set everywhere because it was not mandatory
in `isFieldValueReadOnly`. Removing the default value and adding it to
missing places

* Fix CSV import upsert (twentyhq#12048)

Fixes twentyhq#11864 and
twentyhq/core-team-issues#908

We should not send `createManyXXX` mutations with FE-forged ids in the
payload if we want to do an upsert, because that 1) prevents records
from being merged 2) triggers optimistic rendering while we can't know
before-hand which records will actually be created and which records
will only be updated

Also noticed createdBy was being overriden even for records we are
updating and not creating, which did not seem right, so fixed that too

* Gmail temporary error (twentyhq#12058)

# Handling Google error

This bug is hard to reproduce so the resolution is based only on the
Sentry logs. There is not definite error code
for this error message. in the documentation.
https://developers.google.com/workspace/gmail/api/guides

This is why i added more 5xx code types in order to be sure we catch
this. In order to refine this later, i added the error code to the
message.

Fixes twentyhq#12025

* [QRQC_2] No explicit any in `twenty-server` (twentyhq#12068)

# Introduction

Added a no-explicit-any rule to the twenty-server, not applicable to
tests and integration tests folder

Related to twentyhq/core-team-issues#975
Discussed with Charles

## In case of conflicts
Until this is approved I won't rebased and handle conflict, just need to
drop two latest commits and re run the scripts etc

## Legacy
We decided not to handle the existing lint error occurrences and
programmatically ignored them through a disable next line rule comment

## Open question
We might wanna activate the
[no-explicit-any](https://typescript-eslint.io/rules/no-explicit-any/)
`ignoreRestArgs` for our use case ?
```
    ignoreRestArgs?: boolean;
```

---------

Co-authored-by: etiennejouan <[email protected]>

* Fix missing exception catch (twentyhq#12069)

add a check about uuid in rest api findOne handler

---------

Co-authored-by: Guillim <[email protected]>
Co-authored-by: martmull <[email protected]>
Co-authored-by: nitin <[email protected]>
Co-authored-by: Lucas Bordeau <[email protected]>
Co-authored-by: Thomas Trompette <[email protected]>
Co-authored-by: Félix Malfait <[email protected]>
Co-authored-by: Marie <[email protected]>
Co-authored-by: Paul Rastoin <[email protected]>
Co-authored-by: etiennejouan <[email protected]>
jordan-chalupka pushed a commit to InsurOS/twenty that referenced this pull request May 15, 2025
Fixes twentyhq#11864 and
twentyhq/core-team-issues#908

We should not send `createManyXXX` mutations with FE-forged ids in the
payload if we want to do an upsert, because that 1) prevents records
from being merged 2) triggers optimistic rendering while we can't know
before-hand which records will actually be created and which records
will only be updated

Also noticed createdBy was being overriden even for records we are
updating and not creating, which did not seem right, so fixed that too
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

CSV import (without filling the record ID) creates duplicated records (with the same email) instead of updating the record
3 participants