[BUG][PROD] Fix ViewGroup creation optimistic cache (#10014)

# Introduction
When we create a new `view` from record table that has relation such as
opportunities.
Encountered invariant conditions:

## Unknown fiel `__typename`
`Should never occur, encountered unknown fields __typename in
objectMetadaItem viewGroup`,

### Fixed by ignoring unknown internal fields


## Provided both relation `view` and `viewId`
`Should never provide relation mutation through anything else than the
fieldId e.g companyId and not company, encountered: view`

### Fixed by sending only `viewId` to `createManyRecords` in
`usePersistViewGroupRecords.ts`
This commit is contained in:
Paul Rastoin
2025-02-05 12:22:45 +01:00
committed by GitHub
parent 736b845c98
commit 3e05c3743e
6 changed files with 95 additions and 72 deletions

View File

@ -0,0 +1,4 @@
import { BaseObjectRecord } from '@/object-record/types/BaseObjectRecord';
export const OBJECT_RECORD_TYPENAME_KEY =
'__typename' satisfies keyof BaseObjectRecord;

View File

@ -0,0 +1,4 @@
export type BaseObjectRecord = {
id: string;
__typename: string;
};

View File

@ -1,4 +1,3 @@
export type ObjectRecord = Record<string, any> & {
id: string;
__typename: string;
};
import { BaseObjectRecord } from '@/object-record/types/BaseObjectRecord';
export type ObjectRecord = Record<string, any> & BaseObjectRecord;

View File

@ -3,17 +3,34 @@ import { computeOptimisticRecordFromInput } from '@/object-record/utils/computeO
import { InMemoryCache } from '@apollo/client';
import { generatedMockObjectMetadataItems } from '~/testing/mock-data/generatedMockObjectMetadataItems';
const getPersonObjectMetadaItem = () => {
const personObjectMetadataItem = generatedMockObjectMetadataItems.find(
(item) => item.nameSingular === 'person',
);
if (!personObjectMetadataItem) {
throw new Error('Person object metadata item not found');
}
return personObjectMetadataItem;
};
const getCompanyObjectMetadataItem = () => {
const companyObjectMetadataItem = generatedMockObjectMetadataItems.find(
(item) => item.nameSingular === 'company',
);
if (!companyObjectMetadataItem) {
throw new Error('Company object metadata item not found');
}
return companyObjectMetadataItem;
};
describe('computeOptimisticRecordFromInput', () => {
it('should generate correct optimistic record if no relation field is present', () => {
const cache = new InMemoryCache();
const personObjectMetadataItem = generatedMockObjectMetadataItems.find(
(item) => item.nameSingular === 'person',
);
if (!personObjectMetadataItem) {
throw new Error('Person object metadata item not found');
}
const personObjectMetadataItem = getPersonObjectMetadaItem();
const result = computeOptimisticRecordFromInput({
objectMetadataItems: generatedMockObjectMetadataItems,
@ -31,14 +48,7 @@ describe('computeOptimisticRecordFromInput', () => {
it('should generate correct optimistic record if relation field is present but cache is empty', () => {
const cache = new InMemoryCache();
const personObjectMetadataItem = generatedMockObjectMetadataItems.find(
(item) => item.nameSingular === 'person',
);
if (!personObjectMetadataItem) {
throw new Error('Person object metadata item not found');
}
const personObjectMetadataItem = getPersonObjectMetadaItem();
const result = computeOptimisticRecordFromInput({
objectMetadataItems: generatedMockObjectMetadataItems,
@ -54,23 +64,48 @@ describe('computeOptimisticRecordFromInput', () => {
});
});
it('should generate correct optimistic record even if recordInput contains field __typename', () => {
const cache = new InMemoryCache();
const personObjectMetadataItem = getPersonObjectMetadaItem();
const companyObjectMetadataItem = getCompanyObjectMetadataItem();
const companyRecord = {
id: '123',
__typename: 'Company',
};
updateRecordFromCache({
objectMetadataItems: generatedMockObjectMetadataItems,
objectMetadataItem: {
...companyObjectMetadataItem,
fields: companyObjectMetadataItem.fields.filter(
(field) => field.name === 'id',
),
},
cache,
record: companyRecord,
});
const result = computeOptimisticRecordFromInput({
objectMetadataItems: generatedMockObjectMetadataItems,
objectMetadataItem: personObjectMetadataItem,
recordInput: {
companyId: '123',
__typename: 'test',
},
cache,
});
expect(result).toStrictEqual({
companyId: '123',
company: companyRecord,
});
});
it('should generate correct optimistic record if relation field is present and cache is not empty', () => {
const cache = new InMemoryCache();
const personObjectMetadataItem = generatedMockObjectMetadataItems.find(
(item) => item.nameSingular === 'person',
);
if (!personObjectMetadataItem) {
throw new Error('Person object metadata item not found');
}
const companyObjectMetadataItem = generatedMockObjectMetadataItems.find(
(item) => item.nameSingular === 'company',
);
if (!companyObjectMetadataItem) {
throw new Error('Company object metadata item not found');
}
const personObjectMetadataItem = getPersonObjectMetadaItem();
const companyObjectMetadataItem = getCompanyObjectMetadataItem();
const companyRecord = {
id: '123',
@ -106,14 +141,7 @@ describe('computeOptimisticRecordFromInput', () => {
it('should generate correct optimistic record if relation field is null and cache is empty', () => {
const cache = new InMemoryCache();
const personObjectMetadataItem = generatedMockObjectMetadataItems.find(
(item) => item.nameSingular === 'person',
);
if (!personObjectMetadataItem) {
throw new Error('Person object metadata item not found');
}
const personObjectMetadataItem = getPersonObjectMetadaItem();
const result = computeOptimisticRecordFromInput({
objectMetadataItems: generatedMockObjectMetadataItems,
@ -130,14 +158,9 @@ describe('computeOptimisticRecordFromInput', () => {
});
});
it('should throw an error if recordInput contains fiels unrelated to the current objectMetadata', () => {
it('should throw an error if recordInput contains fields unrelated to the current objectMetadata', () => {
const cache = new InMemoryCache();
const personObjectMetadataItem = generatedMockObjectMetadataItems.find(
(item) => item.nameSingular === 'person',
);
if (!personObjectMetadataItem) {
throw new Error('Person object metadata item not found');
}
const personObjectMetadataItem = getPersonObjectMetadaItem();
expect(() =>
computeOptimisticRecordFromInput({
@ -158,12 +181,7 @@ describe('computeOptimisticRecordFromInput', () => {
it('should throw an error if recordInput contains both the relationFieldId and relationField', () => {
const cache = new InMemoryCache();
const personObjectMetadataItem = generatedMockObjectMetadataItems.find(
(item) => item.nameSingular === 'person',
);
if (!personObjectMetadataItem) {
throw new Error('Person object metadata item not found');
}
const personObjectMetadataItem = getPersonObjectMetadaItem();
expect(() =>
computeOptimisticRecordFromInput({
@ -176,18 +194,13 @@ describe('computeOptimisticRecordFromInput', () => {
cache,
}),
).toThrowErrorMatchingInlineSnapshot(
`"Should never provide relation mutation through anything else than the fieldId e.g companyId"`,
`"Should never provide relation mutation through anything else than the fieldId e.g companyId and not company, encountered: company"`,
);
});
it('should throw an error if recordInput contains both the relationFieldId and relationField even if null', () => {
const cache = new InMemoryCache();
const personObjectMetadataItem = generatedMockObjectMetadataItems.find(
(item) => item.nameSingular === 'person',
);
if (!personObjectMetadataItem) {
throw new Error('Person object metadata item not found');
}
const personObjectMetadataItem = getPersonObjectMetadaItem();
expect(() =>
computeOptimisticRecordFromInput({
@ -200,7 +213,7 @@ describe('computeOptimisticRecordFromInput', () => {
cache,
}),
).toThrowErrorMatchingInlineSnapshot(
`"Should never provide relation mutation through anything else than the fieldId e.g companyId"`,
`"Should never provide relation mutation through anything else than the fieldId e.g companyId and not company, encountered: company"`,
);
});
});

View File

@ -5,6 +5,7 @@ import {
getRecordFromCache,
GetRecordFromCacheArgs,
} from '@/object-record/cache/utils/getRecordFromCache';
import { OBJECT_RECORD_TYPENAME_KEY } from '@/object-record/constants/ObjectRecordTypename';
import { isFieldRelation } from '@/object-record/record-field/types/guards/isFieldRelation';
import { isFieldUuid } from '@/object-record/record-field/types/guards/isFieldUuid';
import { ObjectRecord } from '@/object-record/types/ObjectRecord';
@ -24,9 +25,13 @@ export const computeOptimisticRecordFromInput = ({
objectMetadataItems,
}: ComputeOptimisticCacheRecordInputArgs) => {
const unknownRecordInputFields = Object.keys(recordInput).filter(
(fieldName) =>
objectMetadataItem.fields.find(({ name }) => name === fieldName) ===
undefined,
(fieldName) => {
const isUnknownMetadataItemField =
objectMetadataItem.fields.find(({ name }) => name === fieldName) ===
undefined;
const isTypenameField = fieldName === OBJECT_RECORD_TYPENAME_KEY;
return isUnknownMetadataItemField && !isTypenameField;
},
);
if (unknownRecordInputFields.length > 0) {
throw new Error(
@ -93,7 +98,7 @@ export const computeOptimisticRecordFromInput = ({
if (!isUndefined(recordInputFieldValue)) {
throw new Error(
'Should never provide relation mutation through anything else than the fieldId e.g companyId',
`Should never provide relation mutation through anything else than the fieldId e.g companyId and not company, encountered: ${fieldMetadataItem.name}`,
);
}

View File

@ -26,14 +26,12 @@ export const usePersistViewGroupRecords = () => {
const createViewGroupRecords = useCallback(
(viewGroupsToCreate: ViewGroup[], view: GraphQLView) => {
if (!viewGroupsToCreate.length) return;
if (viewGroupsToCreate.length === 0) return;
return createManyRecords(
viewGroupsToCreate.map((viewGroup) => ({
...viewGroup,
view: {
id: view.id,
},
viewId: view.id,
})),
);
},