Refactor upsertRecordsInStore to accept an object with partialRecords (#16707)
Fixes https://github.com/twentyhq/twenty/issues/16624 Original issue: - while persisting a field (calling useUpdateOne), the response from the backend is missing the taskTargets many to many (same for note). As we optimistically update the cache, we lose the "Relations" in the UI - I'm changing the behavior of useUpsertInRecordStore to accept recordGqlFields to only update the fields we want in the record store (this way, we are not losing the targets information in our case)
This commit is contained in:
+331
@@ -0,0 +1,331 @@
|
||||
import { filterRecordOnGqlFields } from '../filterRecordOnGqlFields';
|
||||
|
||||
describe('filterRecordOnGqlFields', () => {
|
||||
it('should filter fields based on recordGqlFields with true values', () => {
|
||||
const record = {
|
||||
id: '1',
|
||||
__typename: 'Person',
|
||||
name: 'John',
|
||||
email: 'john@example.com',
|
||||
phone: '123-456-7890',
|
||||
};
|
||||
|
||||
const recordGqlFields = {
|
||||
id: true,
|
||||
name: true,
|
||||
};
|
||||
|
||||
const result = filterRecordOnGqlFields({ record, recordGqlFields });
|
||||
|
||||
expect(result).toEqual({
|
||||
id: '1',
|
||||
name: 'John',
|
||||
});
|
||||
});
|
||||
|
||||
it('should exclude fields with false values', () => {
|
||||
const record = {
|
||||
id: '1',
|
||||
__typename: 'Person',
|
||||
name: 'John',
|
||||
email: 'john@example.com',
|
||||
};
|
||||
|
||||
const recordGqlFields = {
|
||||
id: true,
|
||||
name: false,
|
||||
email: true,
|
||||
};
|
||||
|
||||
const result = filterRecordOnGqlFields({ record, recordGqlFields });
|
||||
|
||||
expect(result).toEqual({
|
||||
id: '1',
|
||||
email: 'john@example.com',
|
||||
});
|
||||
});
|
||||
|
||||
it('should exclude fields with undefined values', () => {
|
||||
const record = {
|
||||
id: '1',
|
||||
__typename: 'Person',
|
||||
name: 'John',
|
||||
email: 'john@example.com',
|
||||
};
|
||||
|
||||
const recordGqlFields = {
|
||||
id: true,
|
||||
name: undefined,
|
||||
email: true,
|
||||
};
|
||||
|
||||
const result = filterRecordOnGqlFields({ record, recordGqlFields });
|
||||
|
||||
expect(result).toEqual({
|
||||
id: '1',
|
||||
email: 'john@example.com',
|
||||
});
|
||||
});
|
||||
|
||||
it('should handle composite fields with RecordGqlFields', () => {
|
||||
const record = {
|
||||
id: '1',
|
||||
__typename: 'Person',
|
||||
fullName: {
|
||||
firstName: 'John',
|
||||
lastName: 'Doe',
|
||||
},
|
||||
email: 'john@example.com',
|
||||
};
|
||||
|
||||
const recordGqlFields = {
|
||||
id: true,
|
||||
fullName: {
|
||||
firstName: true,
|
||||
lastName: true,
|
||||
},
|
||||
};
|
||||
|
||||
const result = filterRecordOnGqlFields({ record, recordGqlFields });
|
||||
|
||||
expect(result).toEqual({
|
||||
id: '1',
|
||||
fullName: {
|
||||
firstName: 'John',
|
||||
lastName: 'Doe',
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
it('should handle arrays with nested RecordGqlFields', () => {
|
||||
const record = {
|
||||
id: '1',
|
||||
__typename: 'Company',
|
||||
name: 'Acme Inc',
|
||||
employees: [
|
||||
{ id: '2', __typename: 'Person', name: 'John', email: 'john@test.com' },
|
||||
{ id: '3', __typename: 'Person', name: 'Jane', email: 'jane@test.com' },
|
||||
],
|
||||
};
|
||||
|
||||
const recordGqlFields = {
|
||||
id: true,
|
||||
name: true,
|
||||
employees: {
|
||||
id: true,
|
||||
name: true,
|
||||
},
|
||||
};
|
||||
|
||||
const result = filterRecordOnGqlFields({ record, recordGqlFields });
|
||||
|
||||
expect(result).toEqual({
|
||||
id: '1',
|
||||
name: 'Acme Inc',
|
||||
employees: [
|
||||
{ id: '2', name: 'John' },
|
||||
{ id: '3', name: 'Jane' },
|
||||
],
|
||||
});
|
||||
});
|
||||
|
||||
it('should handle null values in nested objects', () => {
|
||||
const record = {
|
||||
id: '1',
|
||||
__typename: 'Person',
|
||||
company: null,
|
||||
};
|
||||
|
||||
const recordGqlFields = {
|
||||
id: true,
|
||||
company: {
|
||||
id: true,
|
||||
name: true,
|
||||
},
|
||||
};
|
||||
|
||||
const result = filterRecordOnGqlFields({ record, recordGqlFields });
|
||||
|
||||
expect(result).toEqual({
|
||||
id: '1',
|
||||
company: null,
|
||||
});
|
||||
});
|
||||
|
||||
it('should handle undefined values in nested objects', () => {
|
||||
const record = {
|
||||
id: '1',
|
||||
__typename: 'Person',
|
||||
company: undefined,
|
||||
};
|
||||
|
||||
const recordGqlFields = {
|
||||
id: true,
|
||||
company: {
|
||||
id: true,
|
||||
name: true,
|
||||
},
|
||||
};
|
||||
|
||||
const result = filterRecordOnGqlFields({ record, recordGqlFields });
|
||||
|
||||
expect(result).toEqual({
|
||||
id: '1',
|
||||
company: undefined,
|
||||
});
|
||||
});
|
||||
|
||||
it('should handle deeply nested structures', () => {
|
||||
const record = {
|
||||
id: '1',
|
||||
__typename: 'Person',
|
||||
company: {
|
||||
id: '2',
|
||||
taskTargets: [
|
||||
{
|
||||
id: '3',
|
||||
task: {
|
||||
id: '4',
|
||||
title: 'Task 1',
|
||||
assignee: {
|
||||
id: '6',
|
||||
name: 'Assignee 1',
|
||||
},
|
||||
},
|
||||
targetCompany: {
|
||||
id: '5',
|
||||
name: 'Company 1',
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
};
|
||||
|
||||
const recordGqlFields = {
|
||||
id: true,
|
||||
company: {
|
||||
id: true,
|
||||
taskTargets: {
|
||||
id: true,
|
||||
task: {
|
||||
id: true,
|
||||
title: true,
|
||||
},
|
||||
targetCompany: {
|
||||
id: true,
|
||||
name: true,
|
||||
},
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
const result = filterRecordOnGqlFields({ record, recordGqlFields });
|
||||
|
||||
expect(result).toEqual({
|
||||
id: '1',
|
||||
company: {
|
||||
id: '2',
|
||||
taskTargets: [
|
||||
{
|
||||
id: '3',
|
||||
task: {
|
||||
id: '4',
|
||||
title: 'Task 1',
|
||||
},
|
||||
targetCompany: {
|
||||
id: '5',
|
||||
name: 'Company 1',
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
it('should handle arrays with primitive values when gqlField is true', () => {
|
||||
const record = {
|
||||
id: '1',
|
||||
__typename: 'Person',
|
||||
tags: ['tag1', 'tag2', 'tag3'],
|
||||
};
|
||||
|
||||
const recordGqlFields = {
|
||||
id: true,
|
||||
tags: true,
|
||||
};
|
||||
|
||||
const result = filterRecordOnGqlFields({ record, recordGqlFields });
|
||||
|
||||
expect(result).toEqual({
|
||||
id: '1',
|
||||
tags: ['tag1', 'tag2', 'tag3'],
|
||||
});
|
||||
});
|
||||
|
||||
it('should handle mixed arrays with null values', () => {
|
||||
const record = {
|
||||
id: '1',
|
||||
__typename: 'Company',
|
||||
employees: [
|
||||
{ id: '2', __typename: 'Person', name: 'John' },
|
||||
null,
|
||||
{ id: '3', __typename: 'Person', name: 'Jane' },
|
||||
],
|
||||
};
|
||||
|
||||
const recordGqlFields = {
|
||||
id: true,
|
||||
employees: {
|
||||
id: true,
|
||||
name: true,
|
||||
},
|
||||
};
|
||||
|
||||
const result = filterRecordOnGqlFields({ record, recordGqlFields });
|
||||
|
||||
expect(result).toEqual({
|
||||
id: '1',
|
||||
employees: [{ id: '2', name: 'John' }, null, { id: '3', name: 'Jane' }],
|
||||
});
|
||||
});
|
||||
|
||||
it('should return empty object when no fields match', () => {
|
||||
const record = {
|
||||
id: '1',
|
||||
__typename: 'Person',
|
||||
name: 'John',
|
||||
};
|
||||
|
||||
const recordGqlFields = {
|
||||
email: true,
|
||||
phone: true,
|
||||
};
|
||||
|
||||
const result = filterRecordOnGqlFields({ record, recordGqlFields });
|
||||
|
||||
expect(result).toEqual({});
|
||||
});
|
||||
|
||||
it('should not include fields that are not in recordGqlFields', () => {
|
||||
const record = {
|
||||
id: '1',
|
||||
__typename: 'Person',
|
||||
name: 'John',
|
||||
email: 'john@example.com',
|
||||
phone: '123-456-7890',
|
||||
address: 'NYC',
|
||||
};
|
||||
|
||||
const recordGqlFields = {
|
||||
id: true,
|
||||
email: true,
|
||||
};
|
||||
|
||||
const result = filterRecordOnGqlFields({ record, recordGqlFields });
|
||||
|
||||
expect(result).toEqual({
|
||||
id: '1',
|
||||
email: 'john@example.com',
|
||||
});
|
||||
});
|
||||
});
|
||||
+5
-1
@@ -22,7 +22,11 @@ export const deleteRecordFromCache = ({
|
||||
string,
|
||||
ObjectPermissions & { objectMetadataId: string }
|
||||
>;
|
||||
upsertRecordsInStore: (records: ObjectRecord[]) => void;
|
||||
upsertRecordsInStore: ({
|
||||
partialRecords,
|
||||
}: {
|
||||
partialRecords: ObjectRecord[];
|
||||
}) => void;
|
||||
}) => {
|
||||
triggerDestroyRecordsOptimisticEffect({
|
||||
cache,
|
||||
|
||||
+56
@@ -0,0 +1,56 @@
|
||||
import { type RecordGqlFields } from '@/object-record/graphql/record-gql-fields/types/RecordGqlFields';
|
||||
import { type ObjectRecord } from '@/object-record/types/ObjectRecord';
|
||||
import { isNonEmptyArray } from '@apollo/client/utilities';
|
||||
import { isObject } from '@sniptt/guards';
|
||||
import { isDefined } from 'twenty-shared/utils';
|
||||
export const filterRecordOnGqlFields = ({
|
||||
record,
|
||||
recordGqlFields,
|
||||
}: {
|
||||
record: ObjectRecord;
|
||||
recordGqlFields: RecordGqlFields;
|
||||
}): Partial<ObjectRecord> => {
|
||||
return Object.fromEntries(
|
||||
Object.entries(record)
|
||||
.map(([key, value]) => {
|
||||
const gqlFieldValue = recordGqlFields[key];
|
||||
|
||||
if (!isDefined(gqlFieldValue) || gqlFieldValue === false) {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
if (gqlFieldValue === true) {
|
||||
return [key, value];
|
||||
}
|
||||
|
||||
if (isNonEmptyArray(value)) {
|
||||
return [
|
||||
key,
|
||||
value.map((item) => {
|
||||
if (!isDefined(item) || typeof item !== 'object') {
|
||||
return item;
|
||||
}
|
||||
|
||||
return filterRecordOnGqlFields({
|
||||
record: item as ObjectRecord,
|
||||
recordGqlFields: gqlFieldValue,
|
||||
});
|
||||
}),
|
||||
];
|
||||
}
|
||||
|
||||
if (isObject(value)) {
|
||||
return [
|
||||
key,
|
||||
filterRecordOnGqlFields({
|
||||
record: value as ObjectRecord,
|
||||
recordGqlFields: gqlFieldValue,
|
||||
}),
|
||||
];
|
||||
}
|
||||
|
||||
return [key, value];
|
||||
})
|
||||
.filter(isDefined),
|
||||
) as Partial<ObjectRecord>;
|
||||
};
|
||||
Reference in New Issue
Block a user