[REFACTOR][BUG] Dynamically compute field to write in cache CREATE (#10130)
# Introduction While importing records encountering missing expected fields when writting a fragment from apollo cache ## Updates ### 1/ `createdBy` Default value When inserting in cache in create single or many we will now make optimistic behavior on the createdBy value ### 2/ `createRecordInCache` dynamically create `recordGrqlFields` When creating an entry in cache, we will now dynamically generate fields to be written in the fragment instead of expecting all of them. As by nature record could be partial ### 3/ Strictly typed `RecordGqlFields` # Conclusion closes #9927
This commit is contained in:
@ -1,18 +1,23 @@
|
||||
import { ObjectMetadataItem } from '@/object-metadata/types/ObjectMetadataItem';
|
||||
import { updateRecordFromCache } from '@/object-record/cache/utils/updateRecordFromCache';
|
||||
import { generateDepthOneRecordGqlFields } from '@/object-record/graphql/utils/generateDepthOneRecordGqlFields';
|
||||
import { FieldActorForInputValue } from '@/object-record/record-field/types/FieldMetadata';
|
||||
import { computeOptimisticRecordFromInput } from '@/object-record/utils/computeOptimisticRecordFromInput';
|
||||
import { InMemoryCache } from '@apollo/client';
|
||||
import { getCompanyObjectMetadataItem } from '~/testing/mock-data/companies';
|
||||
import { generatedMockObjectMetadataItems } from '~/testing/mock-data/generatedMockObjectMetadataItems';
|
||||
import { getPersonObjectMetadataItem } from '~/testing/mock-data/people';
|
||||
import { mockCurrentWorkspaceMembers } from '~/testing/mock-data/workspace-members';
|
||||
|
||||
describe('computeOptimisticRecordFromInput', () => {
|
||||
const currentWorkspaceMember = mockCurrentWorkspaceMembers[0];
|
||||
const currentWorkspaceMemberFullname = `${currentWorkspaceMember.name.firstName} ${currentWorkspaceMember.name.lastName}`;
|
||||
it('should generate correct optimistic record if no relation field is present', () => {
|
||||
const cache = new InMemoryCache();
|
||||
const personObjectMetadataItem = getPersonObjectMetadataItem();
|
||||
|
||||
const result = computeOptimisticRecordFromInput({
|
||||
currentWorkspaceMember,
|
||||
objectMetadataItems: generatedMockObjectMetadataItems,
|
||||
objectMetadataItem: personObjectMetadataItem,
|
||||
recordInput: {
|
||||
@ -26,11 +31,69 @@ describe('computeOptimisticRecordFromInput', () => {
|
||||
});
|
||||
});
|
||||
|
||||
it('should generate correct optimistic record with actor field', () => {
|
||||
const cache = new InMemoryCache();
|
||||
const personObjectMetadataItem = getPersonObjectMetadataItem();
|
||||
const actorFieldValueForInput: FieldActorForInputValue = {
|
||||
context: {},
|
||||
source: 'API',
|
||||
};
|
||||
const result = computeOptimisticRecordFromInput({
|
||||
currentWorkspaceMember,
|
||||
objectMetadataItems: generatedMockObjectMetadataItems,
|
||||
objectMetadataItem: personObjectMetadataItem,
|
||||
recordInput: {
|
||||
city: 'Paris',
|
||||
createdBy: actorFieldValueForInput,
|
||||
},
|
||||
cache,
|
||||
});
|
||||
|
||||
expect(result).toEqual({
|
||||
city: 'Paris',
|
||||
createdBy: {
|
||||
context: {},
|
||||
name: currentWorkspaceMemberFullname,
|
||||
source: 'API',
|
||||
workspaceMemberId: currentWorkspaceMember.id,
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
it('should generate correct optimistic record createdBy when recordInput contains id', () => {
|
||||
const cache = new InMemoryCache();
|
||||
const personObjectMetadataItem = getPersonObjectMetadataItem();
|
||||
const result = computeOptimisticRecordFromInput({
|
||||
currentWorkspaceMember,
|
||||
objectMetadataItems: generatedMockObjectMetadataItems,
|
||||
objectMetadataItem: personObjectMetadataItem,
|
||||
recordInput: {
|
||||
id: '20202020-058c-4591-a7d7-50a75af6d1e6',
|
||||
createdBy: {
|
||||
source: 'SYSTEM',
|
||||
context: {},
|
||||
} satisfies FieldActorForInputValue,
|
||||
},
|
||||
cache,
|
||||
});
|
||||
|
||||
expect(result).toEqual({
|
||||
id: '20202020-058c-4591-a7d7-50a75af6d1e6',
|
||||
createdBy: {
|
||||
context: {},
|
||||
name: currentWorkspaceMemberFullname,
|
||||
source: 'SYSTEM',
|
||||
workspaceMemberId: currentWorkspaceMember.id,
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
it('should generate correct optimistic record if relation field is present but cache is empty', () => {
|
||||
const cache = new InMemoryCache();
|
||||
const personObjectMetadataItem = getPersonObjectMetadataItem();
|
||||
|
||||
const result = computeOptimisticRecordFromInput({
|
||||
currentWorkspaceMember,
|
||||
objectMetadataItems: generatedMockObjectMetadataItems,
|
||||
objectMetadataItem: personObjectMetadataItem,
|
||||
recordInput: {
|
||||
@ -73,6 +136,7 @@ describe('computeOptimisticRecordFromInput', () => {
|
||||
});
|
||||
|
||||
const result = computeOptimisticRecordFromInput({
|
||||
currentWorkspaceMember,
|
||||
objectMetadataItems: generatedMockObjectMetadataItems,
|
||||
objectMetadataItem: personObjectMetadataItem,
|
||||
recordInput: {
|
||||
@ -117,6 +181,7 @@ describe('computeOptimisticRecordFromInput', () => {
|
||||
});
|
||||
|
||||
const result = computeOptimisticRecordFromInput({
|
||||
currentWorkspaceMember,
|
||||
objectMetadataItems: generatedMockObjectMetadataItems,
|
||||
objectMetadataItem: personObjectMetadataItem,
|
||||
recordInput: {
|
||||
@ -136,6 +201,7 @@ describe('computeOptimisticRecordFromInput', () => {
|
||||
const personObjectMetadataItem = getPersonObjectMetadataItem();
|
||||
|
||||
const result = computeOptimisticRecordFromInput({
|
||||
currentWorkspaceMember,
|
||||
objectMetadataItems: generatedMockObjectMetadataItems,
|
||||
objectMetadataItem: personObjectMetadataItem,
|
||||
recordInput: {
|
||||
@ -156,6 +222,7 @@ describe('computeOptimisticRecordFromInput', () => {
|
||||
|
||||
expect(() =>
|
||||
computeOptimisticRecordFromInput({
|
||||
currentWorkspaceMember,
|
||||
objectMetadataItems: generatedMockObjectMetadataItems,
|
||||
objectMetadataItem: personObjectMetadataItem,
|
||||
recordInput: {
|
||||
@ -167,7 +234,7 @@ describe('computeOptimisticRecordFromInput', () => {
|
||||
cache,
|
||||
}),
|
||||
).toThrowErrorMatchingInlineSnapshot(
|
||||
`"Should never occur, encountered unknown fields unknwon, foo, bar in objectMetadaItem person"`,
|
||||
`"Should never occur, encountered unknown fields unknwon, foo, bar in objectMetadataItem person"`,
|
||||
);
|
||||
});
|
||||
|
||||
@ -177,6 +244,7 @@ describe('computeOptimisticRecordFromInput', () => {
|
||||
|
||||
expect(() =>
|
||||
computeOptimisticRecordFromInput({
|
||||
currentWorkspaceMember,
|
||||
objectMetadataItems: generatedMockObjectMetadataItems,
|
||||
objectMetadataItem: personObjectMetadataItem,
|
||||
recordInput: {
|
||||
@ -196,6 +264,7 @@ describe('computeOptimisticRecordFromInput', () => {
|
||||
|
||||
expect(() =>
|
||||
computeOptimisticRecordFromInput({
|
||||
currentWorkspaceMember,
|
||||
objectMetadataItems: generatedMockObjectMetadataItems,
|
||||
objectMetadataItem: personObjectMetadataItem,
|
||||
recordInput: {
|
||||
|
||||
@ -0,0 +1,29 @@
|
||||
import { CurrentWorkspaceMember } from '@/auth/states/currentWorkspaceMemberState';
|
||||
import { FieldActorValue } from '@/object-record/record-field/types/FieldMetadata';
|
||||
import { isDefined } from 'twenty-shared';
|
||||
|
||||
export const buildOptimisticActorFieldValueFromCurrentWorkspaceMember = (
|
||||
currentWorkspaceMember: CurrentWorkspaceMember | null,
|
||||
): FieldActorValue => {
|
||||
const defaultActorFieldValue: FieldActorValue = {
|
||||
context: {},
|
||||
name: '',
|
||||
source: 'MANUAL',
|
||||
workspaceMemberId: null,
|
||||
};
|
||||
|
||||
if (!isDefined(currentWorkspaceMember)) {
|
||||
return defaultActorFieldValue;
|
||||
}
|
||||
|
||||
const {
|
||||
id: workspaceMemberId,
|
||||
name: { firstName, lastName },
|
||||
} = currentWorkspaceMember;
|
||||
const name = `${firstName} ${lastName}`;
|
||||
return {
|
||||
...defaultActorFieldValue,
|
||||
name: name,
|
||||
workspaceMemberId,
|
||||
};
|
||||
};
|
||||
@ -1,14 +1,18 @@
|
||||
import { isNull, isUndefined } from '@sniptt/guards';
|
||||
|
||||
import { CurrentWorkspaceMember } from '@/auth/states/currentWorkspaceMemberState';
|
||||
import { ObjectMetadataItem } from '@/object-metadata/types/ObjectMetadataItem';
|
||||
import {
|
||||
getRecordFromCache,
|
||||
GetRecordFromCacheArgs,
|
||||
} from '@/object-record/cache/utils/getRecordFromCache';
|
||||
import { GRAPHQL_TYPENAME_KEY } from '@/object-record/constants/GraphqlTypenameKey';
|
||||
import { FieldActorValue } from '@/object-record/record-field/types/FieldMetadata';
|
||||
import { isFieldActor } from '@/object-record/record-field/types/guards/isFieldActor';
|
||||
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';
|
||||
import { buildOptimisticActorFieldValueFromCurrentWorkspaceMember } from '@/object-record/utils/buildOptimisticActorFieldValueFromCurrentWorkspaceMember';
|
||||
import { getForeignKeyNameFromRelationFieldName } from '@/object-record/utils/getForeignKeyNameFromRelationFieldName';
|
||||
import { isDefined } from 'twenty-shared';
|
||||
import { RelationDefinitionType } from '~/generated-metadata/graphql';
|
||||
@ -17,12 +21,14 @@ import { FieldMetadataType } from '~/generated/graphql';
|
||||
type ComputeOptimisticCacheRecordInputArgs = {
|
||||
objectMetadataItem: ObjectMetadataItem;
|
||||
recordInput: Partial<ObjectRecord>;
|
||||
currentWorkspaceMember: CurrentWorkspaceMember | null;
|
||||
} & Pick<GetRecordFromCacheArgs, 'cache' | 'objectMetadataItems'>;
|
||||
export const computeOptimisticRecordFromInput = ({
|
||||
objectMetadataItem,
|
||||
recordInput,
|
||||
cache,
|
||||
objectMetadataItems,
|
||||
currentWorkspaceMember,
|
||||
}: ComputeOptimisticCacheRecordInputArgs) => {
|
||||
const unknownRecordInputFields = Object.keys(recordInput).filter(
|
||||
(recordKey) => {
|
||||
@ -35,12 +41,14 @@ export const computeOptimisticRecordFromInput = ({
|
||||
);
|
||||
if (unknownRecordInputFields.length > 0) {
|
||||
throw new Error(
|
||||
`Should never occur, encountered unknown fields ${unknownRecordInputFields.join(', ')} in objectMetadaItem ${objectMetadataItem.nameSingular}`,
|
||||
`Should never occur, encountered unknown fields ${unknownRecordInputFields.join(', ')} in objectMetadataItem ${objectMetadataItem.nameSingular}`,
|
||||
);
|
||||
}
|
||||
|
||||
const optimisticRecord: Partial<ObjectRecord> = {};
|
||||
for (const fieldMetadataItem of objectMetadataItem.fields) {
|
||||
const recordInputFieldValue: unknown = recordInput[fieldMetadataItem.name];
|
||||
|
||||
if (isFieldUuid(fieldMetadataItem)) {
|
||||
const isRelationFieldId = objectMetadataItem.fields.some(
|
||||
({ type, relationDefinition }) => {
|
||||
@ -65,10 +73,19 @@ export const computeOptimisticRecordFromInput = ({
|
||||
}
|
||||
}
|
||||
|
||||
if (isFieldActor(fieldMetadataItem) && isDefined(recordInputFieldValue)) {
|
||||
const defaultActorFieldValue =
|
||||
buildOptimisticActorFieldValueFromCurrentWorkspaceMember(
|
||||
currentWorkspaceMember,
|
||||
);
|
||||
optimisticRecord[fieldMetadataItem.name] = {
|
||||
...defaultActorFieldValue,
|
||||
...(recordInputFieldValue as FieldActorValue),
|
||||
};
|
||||
continue;
|
||||
}
|
||||
|
||||
const isRelationField = isFieldRelation(fieldMetadataItem);
|
||||
|
||||
const recordInputFieldValue: unknown = recordInput[fieldMetadataItem.name];
|
||||
|
||||
if (!isRelationField) {
|
||||
if (!isDefined(recordInputFieldValue)) {
|
||||
continue;
|
||||
|
||||
@ -4,14 +4,19 @@ import { generateEmptyFieldValue } from '@/object-record/utils/generateEmptyFiel
|
||||
import { v4 } from 'uuid';
|
||||
import { stripSimpleQuotesFromString } from '~/utils/string/stripSimpleQuotesFromString';
|
||||
|
||||
export const generateDefaultFieldValue = (
|
||||
fieldMetadataItem: Pick<FieldMetadataItem, 'defaultValue' | 'type'>,
|
||||
) => {
|
||||
type GenerateEmptyFieldValueArgs = {
|
||||
fieldMetadataItem: Pick<FieldMetadataItem, 'defaultValue' | 'type'>;
|
||||
};
|
||||
export const generateDefaultFieldValue = ({
|
||||
fieldMetadataItem,
|
||||
}: GenerateEmptyFieldValueArgs) => {
|
||||
const defaultValue = isFieldValueEmpty({
|
||||
fieldValue: fieldMetadataItem.defaultValue,
|
||||
fieldDefinition: fieldMetadataItem,
|
||||
})
|
||||
? generateEmptyFieldValue(fieldMetadataItem)
|
||||
? generateEmptyFieldValue({
|
||||
fieldMetadataItem,
|
||||
})
|
||||
: stripSimpleQuotesFromString(fieldMetadataItem.defaultValue);
|
||||
|
||||
switch (defaultValue) {
|
||||
|
||||
@ -1,13 +1,18 @@
|
||||
import { FieldMetadataItem } from '@/object-metadata/types/FieldMetadataItem';
|
||||
import { FieldActorValue } from '@/object-record/record-field/types/FieldMetadata';
|
||||
import { assertUnreachable } from '@/workflow/utils/assertUnreachable';
|
||||
import {
|
||||
FieldMetadataType,
|
||||
RelationDefinitionType,
|
||||
} from '~/generated-metadata/graphql';
|
||||
|
||||
export const generateEmptyFieldValue = (
|
||||
fieldMetadataItem: Pick<FieldMetadataItem, 'type' | 'relationDefinition'>,
|
||||
) => {
|
||||
export type GenerateEmptyFieldValueArgs = {
|
||||
fieldMetadataItem: Pick<FieldMetadataItem, 'type' | 'relationDefinition'>;
|
||||
};
|
||||
// TODO strictly type each fieldValue following their FieldMetadataType
|
||||
export const generateEmptyFieldValue = ({
|
||||
fieldMetadataItem,
|
||||
}: GenerateEmptyFieldValueArgs) => {
|
||||
switch (fieldMetadataItem.type) {
|
||||
case FieldMetadataType.TEXT: {
|
||||
return '';
|
||||
@ -94,10 +99,10 @@ export const generateEmptyFieldValue = (
|
||||
case FieldMetadataType.ACTOR: {
|
||||
return {
|
||||
source: 'MANUAL',
|
||||
workspaceMemberId: null,
|
||||
name: '',
|
||||
context: {},
|
||||
};
|
||||
name: '',
|
||||
workspaceMemberId: null,
|
||||
} satisfies FieldActorValue;
|
||||
}
|
||||
case FieldMetadataType.PHONES: {
|
||||
return {
|
||||
|
||||
@ -7,13 +7,14 @@ import { generateDefaultFieldValue } from '@/object-record/utils/generateDefault
|
||||
import { isDefined } from 'twenty-shared';
|
||||
import { FieldMetadataType, RelationDefinitionType } from '~/generated/graphql';
|
||||
|
||||
type PrefillRecordArgs = {
|
||||
objectMetadataItem: ObjectMetadataItem;
|
||||
input: Record<string, unknown>;
|
||||
};
|
||||
export const prefillRecord = <T extends ObjectRecord>({
|
||||
objectMetadataItem,
|
||||
input,
|
||||
}: {
|
||||
objectMetadataItem: ObjectMetadataItem;
|
||||
input: Record<string, unknown>;
|
||||
}) => {
|
||||
}: PrefillRecordArgs) => {
|
||||
return Object.fromEntries(
|
||||
objectMetadataItem.fields
|
||||
.map((fieldMetadataItem) => {
|
||||
@ -26,12 +27,10 @@ export const prefillRecord = <T extends ObjectRecord>({
|
||||
throwIfInputRelationDataIsInconsistent(input, fieldMetadataItem);
|
||||
}
|
||||
|
||||
return [
|
||||
fieldMetadataItem.name,
|
||||
isUndefined(inputValue)
|
||||
? generateDefaultFieldValue(fieldMetadataItem)
|
||||
: inputValue,
|
||||
];
|
||||
const fieldValue = isUndefined(inputValue)
|
||||
? generateDefaultFieldValue({ fieldMetadataItem })
|
||||
: inputValue;
|
||||
return [fieldMetadataItem.name, fieldValue];
|
||||
})
|
||||
.filter(isDefined),
|
||||
) as T;
|
||||
|
||||
Reference in New Issue
Block a user