-
Notifications
You must be signed in to change notification settings - Fork 3.9k
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
Fix CSV import upsert #12048
Conversation
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 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 informatData.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 ( |
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.
may seem very custom but this still object-agnostic as we have it on all objects subject to user changes
@@ -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; |
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.
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
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.
if you use destructured parameters you won't need that 'hard coded' constant which seems rough at first sight
🚀 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. |
TODO: add a FE test if possible |
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.
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( |
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.
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; |
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.
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, |
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.
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)
if ( | ||
fieldMetadata?.name === 'createdBy' && | ||
fieldMetadata.isCustom === false | ||
) { | ||
if (shouldNotEraseCreatedByField) { | ||
continue; | ||
} | ||
} |
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.
see comment above, i would keep formatData
agnostic and remove the createdBy field from data
in a dedicated utils before calling formatData
...ine/api/graphql/graphql-query-runner/resolvers/graphql-query-create-many-resolver.service.ts
Show resolved
Hide resolved
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.
seen together, lgtm
* 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]>
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
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 updatedAlso noticed createdBy was being overriden even for records we are updating and not creating, which did not seem right, so fixed that too