[BUG] Fix record relation optimistic mutation (#9881)
# Introduction It seems like optimistic caching isn't working as expected for any record relation mutation, CREATE UPDATE DELETE. It should not have an impact on the destroy We included a new `computeOptimisticRecordInput` that will calculate if a relation is added or detach. Updated the `triggerCreateRecordsOptimisticEffect` signature we should have a look to each of its call to determine if it should be checking cache or not Related to #9580 --------- Co-authored-by: Charles Bochet <charles@twenty.com>
This commit is contained in:
@ -6,6 +6,9 @@ process.env.TZ = 'GMT';
|
|||||||
const jestConfig: JestConfigWithTsJest = {
|
const jestConfig: JestConfigWithTsJest = {
|
||||||
// to enable logs, comment out the following line
|
// to enable logs, comment out the following line
|
||||||
silent: true,
|
silent: true,
|
||||||
|
// For more information please have a look to official docs https://jestjs.io/docs/configuration/#prettierpath-string
|
||||||
|
// Prettier v3 will should be supported in jest v30 https://github.com/jestjs/jest/releases/tag/v30.0.0-alpha.1
|
||||||
|
prettierPath: null,
|
||||||
displayName: 'twenty-front',
|
displayName: 'twenty-front',
|
||||||
preset: '../../jest.preset.js',
|
preset: '../../jest.preset.js',
|
||||||
setupFilesAfterEnv: ['./setupTests.ts'],
|
setupFilesAfterEnv: ['./setupTests.ts'],
|
||||||
|
|||||||
@ -80,7 +80,11 @@ export const useOpenCreateActivityDrawer = ({
|
|||||||
setViewableRecordNameSingular(activityObjectNameSingular);
|
setViewableRecordNameSingular(activityObjectNameSingular);
|
||||||
|
|
||||||
const activity = await createOneActivity({
|
const activity = await createOneActivity({
|
||||||
assigneeId: customAssignee?.id,
|
...(activityObjectNameSingular === CoreObjectNameSingular.Task
|
||||||
|
? {
|
||||||
|
assigneeId: customAssignee?.id,
|
||||||
|
}
|
||||||
|
: {}),
|
||||||
position: 'last',
|
position: 'last',
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
@ -11,6 +11,8 @@ import { isRecordMatchingFilter } from '@/object-record/record-filter/utils/isRe
|
|||||||
|
|
||||||
import { CachedObjectRecordQueryVariables } from '@/apollo/types/CachedObjectRecordQueryVariables';
|
import { CachedObjectRecordQueryVariables } from '@/apollo/types/CachedObjectRecordQueryVariables';
|
||||||
import { encodeCursor } from '@/apollo/utils/encodeCursor';
|
import { encodeCursor } from '@/apollo/utils/encodeCursor';
|
||||||
|
import { getRecordFromCache } from '@/object-record/cache/utils/getRecordFromCache';
|
||||||
|
import { getRecordNodeFromRecord } from '@/object-record/cache/utils/getRecordNodeFromRecord';
|
||||||
import { isDefined } from '~/utils/isDefined';
|
import { isDefined } from '~/utils/isDefined';
|
||||||
import { parseApolloStoreFieldName } from '~/utils/parseApolloStoreFieldName';
|
import { parseApolloStoreFieldName } from '~/utils/parseApolloStoreFieldName';
|
||||||
|
|
||||||
@ -19,28 +21,49 @@ import { parseApolloStoreFieldName } from '~/utils/parseApolloStoreFieldName';
|
|||||||
We need to refactor how the record creation works in the RecordTable so the created record row is temporarily displayed with a local state,
|
We need to refactor how the record creation works in the RecordTable so the created record row is temporarily displayed with a local state,
|
||||||
then we'll be able to uncomment the code below so the cached lists are updated coherently with the variables.
|
then we'll be able to uncomment the code below so the cached lists are updated coherently with the variables.
|
||||||
*/
|
*/
|
||||||
|
type TriggerCreateRecordsOptimisticEffectArgs = {
|
||||||
|
cache: ApolloCache<object>;
|
||||||
|
objectMetadataItem: ObjectMetadataItem;
|
||||||
|
recordsToCreate: RecordGqlNode[];
|
||||||
|
objectMetadataItems: ObjectMetadataItem[];
|
||||||
|
shouldMatchRootQueryFilter?: boolean;
|
||||||
|
checkForRecordInCache?: boolean;
|
||||||
|
};
|
||||||
export const triggerCreateRecordsOptimisticEffect = ({
|
export const triggerCreateRecordsOptimisticEffect = ({
|
||||||
cache,
|
cache,
|
||||||
objectMetadataItem,
|
objectMetadataItem,
|
||||||
recordsToCreate,
|
recordsToCreate,
|
||||||
objectMetadataItems,
|
objectMetadataItems,
|
||||||
shouldMatchRootQueryFilter,
|
shouldMatchRootQueryFilter,
|
||||||
}: {
|
checkForRecordInCache = false,
|
||||||
cache: ApolloCache<unknown>;
|
}: TriggerCreateRecordsOptimisticEffectArgs) => {
|
||||||
objectMetadataItem: ObjectMetadataItem;
|
const getRecordNodeFromCache = (recordId: string): RecordGqlNode | null => {
|
||||||
recordsToCreate: RecordGqlNode[];
|
const cachedRecord = getRecordFromCache({
|
||||||
objectMetadataItems: ObjectMetadataItem[];
|
cache,
|
||||||
shouldMatchRootQueryFilter?: boolean;
|
objectMetadataItem,
|
||||||
}) => {
|
objectMetadataItems,
|
||||||
recordsToCreate.forEach((record) =>
|
recordId,
|
||||||
|
});
|
||||||
|
return getRecordNodeFromRecord({
|
||||||
|
objectMetadataItem,
|
||||||
|
objectMetadataItems,
|
||||||
|
record: cachedRecord,
|
||||||
|
computeReferences: false,
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
recordsToCreate.forEach((record) => {
|
||||||
|
const currentSourceRecord = checkForRecordInCache
|
||||||
|
? getRecordNodeFromCache(record.id)
|
||||||
|
: null;
|
||||||
triggerUpdateRelationsOptimisticEffect({
|
triggerUpdateRelationsOptimisticEffect({
|
||||||
cache,
|
cache,
|
||||||
sourceObjectMetadataItem: objectMetadataItem,
|
sourceObjectMetadataItem: objectMetadataItem,
|
||||||
currentSourceRecord: null,
|
currentSourceRecord,
|
||||||
updatedSourceRecord: record,
|
updatedSourceRecord: record,
|
||||||
objectMetadataItems,
|
objectMetadataItems,
|
||||||
}),
|
});
|
||||||
);
|
});
|
||||||
|
|
||||||
cache.modify<StoreObject>({
|
cache.modify<StoreObject>({
|
||||||
fields: {
|
fields: {
|
||||||
|
|||||||
@ -7,7 +7,6 @@ import { ObjectMetadataItem } from '@/object-metadata/types/ObjectMetadataItem';
|
|||||||
import { isObjectRecordConnection } from '@/object-record/cache/utils/isObjectRecordConnection';
|
import { isObjectRecordConnection } from '@/object-record/cache/utils/isObjectRecordConnection';
|
||||||
import { RecordGqlConnection } from '@/object-record/graphql/types/RecordGqlConnection';
|
import { RecordGqlConnection } from '@/object-record/graphql/types/RecordGqlConnection';
|
||||||
import { RecordGqlNode } from '@/object-record/graphql/types/RecordGqlNode';
|
import { RecordGqlNode } from '@/object-record/graphql/types/RecordGqlNode';
|
||||||
import { ObjectRecord } from '@/object-record/types/ObjectRecord';
|
|
||||||
import { ApolloCache } from '@apollo/client';
|
import { ApolloCache } from '@apollo/client';
|
||||||
import { isArray } from '@sniptt/guards';
|
import { isArray } from '@sniptt/guards';
|
||||||
import { FieldMetadataType } from '~/generated-metadata/graphql';
|
import { FieldMetadataType } from '~/generated-metadata/graphql';
|
||||||
@ -17,8 +16,8 @@ import { isDefined } from '~/utils/isDefined';
|
|||||||
type triggerUpdateRelationsOptimisticEffectArgs = {
|
type triggerUpdateRelationsOptimisticEffectArgs = {
|
||||||
cache: ApolloCache<unknown>;
|
cache: ApolloCache<unknown>;
|
||||||
sourceObjectMetadataItem: ObjectMetadataItem;
|
sourceObjectMetadataItem: ObjectMetadataItem;
|
||||||
currentSourceRecord: ObjectRecord | null;
|
currentSourceRecord: RecordGqlNode | null;
|
||||||
updatedSourceRecord: ObjectRecord | null;
|
updatedSourceRecord: RecordGqlNode | null;
|
||||||
objectMetadataItems: ObjectMetadataItem[];
|
objectMetadataItems: ObjectMetadataItem[];
|
||||||
};
|
};
|
||||||
export const triggerUpdateRelationsOptimisticEffect = ({
|
export const triggerUpdateRelationsOptimisticEffect = ({
|
||||||
|
|||||||
@ -26,7 +26,6 @@ export const useCreateFavorite = () => {
|
|||||||
);
|
);
|
||||||
|
|
||||||
createOneFavorite({
|
createOneFavorite({
|
||||||
[targetObjectNameSingular]: targetRecord,
|
|
||||||
[`${targetObjectNameSingular}Id`]: targetRecord.id,
|
[`${targetObjectNameSingular}Id`]: targetRecord.id,
|
||||||
position: maxPosition + 1,
|
position: maxPosition + 1,
|
||||||
workspaceMemberId: currentWorkspaceMemberId,
|
workspaceMemberId: currentWorkspaceMemberId,
|
||||||
|
|||||||
@ -9,19 +9,20 @@ import { ObjectRecord } from '@/object-record/types/ObjectRecord';
|
|||||||
import { capitalize } from 'twenty-shared';
|
import { capitalize } from 'twenty-shared';
|
||||||
import { isUndefinedOrNull } from '~/utils/isUndefinedOrNull';
|
import { isUndefinedOrNull } from '~/utils/isUndefinedOrNull';
|
||||||
|
|
||||||
|
export type GetRecordFromCacheArgs = {
|
||||||
|
cache: ApolloCache<object>;
|
||||||
|
recordId: string;
|
||||||
|
objectMetadataItems: ObjectMetadataItem[];
|
||||||
|
objectMetadataItem: ObjectMetadataItem;
|
||||||
|
recordGqlFields?: RecordGqlFields;
|
||||||
|
};
|
||||||
export const getRecordFromCache = <T extends ObjectRecord = ObjectRecord>({
|
export const getRecordFromCache = <T extends ObjectRecord = ObjectRecord>({
|
||||||
objectMetadataItem,
|
objectMetadataItem,
|
||||||
objectMetadataItems,
|
objectMetadataItems,
|
||||||
cache,
|
cache,
|
||||||
recordId,
|
recordId,
|
||||||
recordGqlFields,
|
recordGqlFields,
|
||||||
}: {
|
}: GetRecordFromCacheArgs) => {
|
||||||
cache: ApolloCache<object>;
|
|
||||||
recordId: string;
|
|
||||||
objectMetadataItems: ObjectMetadataItem[];
|
|
||||||
objectMetadataItem: ObjectMetadataItem;
|
|
||||||
recordGqlFields?: RecordGqlFields;
|
|
||||||
}) => {
|
|
||||||
if (isUndefinedOrNull(objectMetadataItem)) {
|
if (isUndefinedOrNull(objectMetadataItem)) {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|||||||
@ -57,7 +57,6 @@ const connectedObjects = {
|
|||||||
export const variables = {
|
export const variables = {
|
||||||
idToUpdate: '36abbb63-34ed-4a16-89f5-f549ac55d0f9',
|
idToUpdate: '36abbb63-34ed-4a16-89f5-f549ac55d0f9',
|
||||||
input: {
|
input: {
|
||||||
...basePerson,
|
|
||||||
name: { firstName: 'John', lastName: 'Doe' },
|
name: { firstName: 'John', lastName: 'Doe' },
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|||||||
@ -11,7 +11,7 @@ import { expect } from '@storybook/test';
|
|||||||
import { getJestMetadataAndApolloMocksWrapper } from '~/testing/jest/getJestMetadataAndApolloMocksWrapper';
|
import { getJestMetadataAndApolloMocksWrapper } from '~/testing/jest/getJestMetadataAndApolloMocksWrapper';
|
||||||
|
|
||||||
const person = { id: '36abbb63-34ed-4a16-89f5-f549ac55d0f9' };
|
const person = { id: '36abbb63-34ed-4a16-89f5-f549ac55d0f9' };
|
||||||
const update = {
|
const updateInput = {
|
||||||
name: {
|
name: {
|
||||||
firstName: 'John',
|
firstName: 'John',
|
||||||
lastName: 'Doe',
|
lastName: 'Doe',
|
||||||
@ -20,7 +20,7 @@ const update = {
|
|||||||
const updatePerson = {
|
const updatePerson = {
|
||||||
...person,
|
...person,
|
||||||
...responseData,
|
...responseData,
|
||||||
...update,
|
...updateInput,
|
||||||
};
|
};
|
||||||
|
|
||||||
const mocks = [
|
const mocks = [
|
||||||
@ -64,11 +64,11 @@ describe('useUpdateOneRecord', () => {
|
|||||||
await act(async () => {
|
await act(async () => {
|
||||||
const res = await result.current.updateOneRecord({
|
const res = await result.current.updateOneRecord({
|
||||||
idToUpdate,
|
idToUpdate,
|
||||||
updateOneRecordInput: updatePerson,
|
updateOneRecordInput: updateInput,
|
||||||
});
|
});
|
||||||
expect(res).toBeDefined();
|
expect(res).toBeDefined();
|
||||||
expect(res).toHaveProperty('id', person.id);
|
expect(res).toHaveProperty('id', person.id);
|
||||||
expect(res).toHaveProperty('name', update.name);
|
expect(res).toHaveProperty('name', updateInput.name);
|
||||||
});
|
});
|
||||||
|
|
||||||
expect(mocks[0].result).toHaveBeenCalled();
|
expect(mocks[0].result).toHaveBeenCalled();
|
||||||
|
|||||||
@ -8,15 +8,21 @@ import { useObjectMetadataItems } from '@/object-metadata/hooks/useObjectMetadat
|
|||||||
import { useCreateOneRecordInCache } from '@/object-record/cache/hooks/useCreateOneRecordInCache';
|
import { useCreateOneRecordInCache } from '@/object-record/cache/hooks/useCreateOneRecordInCache';
|
||||||
import { deleteRecordFromCache } from '@/object-record/cache/utils/deleteRecordFromCache';
|
import { deleteRecordFromCache } from '@/object-record/cache/utils/deleteRecordFromCache';
|
||||||
import { getObjectTypename } from '@/object-record/cache/utils/getObjectTypename';
|
import { getObjectTypename } from '@/object-record/cache/utils/getObjectTypename';
|
||||||
|
import { getRecordNodeFromRecord } from '@/object-record/cache/utils/getRecordNodeFromRecord';
|
||||||
import { RecordGqlOperationGqlRecordFields } from '@/object-record/graphql/types/RecordGqlOperationGqlRecordFields';
|
import { RecordGqlOperationGqlRecordFields } from '@/object-record/graphql/types/RecordGqlOperationGqlRecordFields';
|
||||||
import { generateDepthOneRecordGqlFields } from '@/object-record/graphql/utils/generateDepthOneRecordGqlFields';
|
import { generateDepthOneRecordGqlFields } from '@/object-record/graphql/utils/generateDepthOneRecordGqlFields';
|
||||||
import { useCreateManyRecordsMutation } from '@/object-record/hooks/useCreateManyRecordsMutation';
|
import { useCreateManyRecordsMutation } from '@/object-record/hooks/useCreateManyRecordsMutation';
|
||||||
import { useRefetchAggregateQueries } from '@/object-record/hooks/useRefetchAggregateQueries';
|
import { useRefetchAggregateQueries } from '@/object-record/hooks/useRefetchAggregateQueries';
|
||||||
import { ObjectRecord } from '@/object-record/types/ObjectRecord';
|
import { ObjectRecord } from '@/object-record/types/ObjectRecord';
|
||||||
|
import { computeOptimisticRecordFromInput } from '@/object-record/utils/computeOptimisticRecordFromInput';
|
||||||
import { getCreateManyRecordsMutationResponseField } from '@/object-record/utils/getCreateManyRecordsMutationResponseField';
|
import { getCreateManyRecordsMutationResponseField } from '@/object-record/utils/getCreateManyRecordsMutationResponseField';
|
||||||
import { sanitizeRecordInput } from '@/object-record/utils/sanitizeRecordInput';
|
import { sanitizeRecordInput } from '@/object-record/utils/sanitizeRecordInput';
|
||||||
import { isDefined } from '~/utils/isDefined';
|
import { isDefined } from '~/utils/isDefined';
|
||||||
|
|
||||||
|
type PartialObjectRecordWithId = Partial<ObjectRecord> & {
|
||||||
|
id: string;
|
||||||
|
};
|
||||||
|
|
||||||
type useCreateManyRecordsProps = {
|
type useCreateManyRecordsProps = {
|
||||||
objectNameSingular: string;
|
objectNameSingular: string;
|
||||||
recordGqlFields?: RecordGqlOperationGqlRecordFields;
|
recordGqlFields?: RecordGqlOperationGqlRecordFields;
|
||||||
@ -60,42 +66,56 @@ export const useCreateManyRecords = <
|
|||||||
recordsToCreate: Partial<CreatedObjectRecord>[],
|
recordsToCreate: Partial<CreatedObjectRecord>[],
|
||||||
upsert?: boolean,
|
upsert?: boolean,
|
||||||
) => {
|
) => {
|
||||||
const sanitizedCreateManyRecordsInput = recordsToCreate.map(
|
const sanitizedCreateManyRecordsInput: PartialObjectRecordWithId[] = [];
|
||||||
(recordToCreate) => {
|
const recordOptimisticRecordsInput: PartialObjectRecordWithId[] = [];
|
||||||
const idForCreation = recordToCreate?.id ?? (upsert ? undefined : v4());
|
recordsToCreate.forEach((recordToCreate) => {
|
||||||
|
const idForCreation = recordToCreate?.id ?? v4();
|
||||||
|
const sanitizedRecord = {
|
||||||
|
...sanitizeRecordInput({
|
||||||
|
objectMetadataItem,
|
||||||
|
recordInput: recordToCreate,
|
||||||
|
}),
|
||||||
|
id: idForCreation,
|
||||||
|
};
|
||||||
|
const optimisticRecordInput = {
|
||||||
|
...computeOptimisticRecordFromInput({
|
||||||
|
cache: apolloClient.cache,
|
||||||
|
objectMetadataItem,
|
||||||
|
objectMetadataItems,
|
||||||
|
recordInput: recordToCreate,
|
||||||
|
}),
|
||||||
|
id: idForCreation,
|
||||||
|
};
|
||||||
|
|
||||||
return {
|
sanitizedCreateManyRecordsInput.push(sanitizedRecord);
|
||||||
...sanitizeRecordInput({
|
recordOptimisticRecordsInput.push(optimisticRecordInput);
|
||||||
objectMetadataItem,
|
});
|
||||||
recordInput: recordToCreate,
|
|
||||||
}),
|
|
||||||
id: idForCreation,
|
|
||||||
};
|
|
||||||
},
|
|
||||||
);
|
|
||||||
|
|
||||||
const recordsCreatedInCache: ObjectRecord[] = [];
|
const recordsCreatedInCache = recordOptimisticRecordsInput
|
||||||
|
.map((recordToCreate) =>
|
||||||
for (const recordToCreate of sanitizedCreateManyRecordsInput) {
|
createOneRecordInCache({
|
||||||
if (recordToCreate.id === null) {
|
...recordToCreate,
|
||||||
continue;
|
__typename: getObjectTypename(objectMetadataItem.nameSingular),
|
||||||
}
|
}),
|
||||||
|
)
|
||||||
const recordCreatedInCache = createOneRecordInCache({
|
.filter(isDefined);
|
||||||
...(recordToCreate as { id: string }),
|
|
||||||
__typename: getObjectTypename(objectMetadataItem.nameSingular),
|
|
||||||
});
|
|
||||||
|
|
||||||
if (isDefined(recordCreatedInCache)) {
|
|
||||||
recordsCreatedInCache.push(recordCreatedInCache);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (recordsCreatedInCache.length > 0) {
|
if (recordsCreatedInCache.length > 0) {
|
||||||
|
const recordNodeCreatedInCache = recordsCreatedInCache
|
||||||
|
.map((record) =>
|
||||||
|
getRecordNodeFromRecord({
|
||||||
|
objectMetadataItem,
|
||||||
|
objectMetadataItems,
|
||||||
|
record: record,
|
||||||
|
computeReferences: false,
|
||||||
|
}),
|
||||||
|
)
|
||||||
|
.filter(isDefined);
|
||||||
|
|
||||||
triggerCreateRecordsOptimisticEffect({
|
triggerCreateRecordsOptimisticEffect({
|
||||||
cache: apolloClient.cache,
|
cache: apolloClient.cache,
|
||||||
objectMetadataItem,
|
objectMetadataItem,
|
||||||
recordsToCreate: recordsCreatedInCache,
|
recordsToCreate: recordNodeCreatedInCache,
|
||||||
objectMetadataItems,
|
objectMetadataItems,
|
||||||
shouldMatchRootQueryFilter,
|
shouldMatchRootQueryFilter,
|
||||||
});
|
});
|
||||||
@ -123,6 +143,7 @@ export const useCreateManyRecords = <
|
|||||||
recordsToCreate: records,
|
recordsToCreate: records,
|
||||||
objectMetadataItems,
|
objectMetadataItems,
|
||||||
shouldMatchRootQueryFilter,
|
shouldMatchRootQueryFilter,
|
||||||
|
checkForRecordInCache: true,
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
})
|
})
|
||||||
|
|||||||
@ -9,11 +9,13 @@ import { useObjectMetadataItems } from '@/object-metadata/hooks/useObjectMetadat
|
|||||||
import { useCreateOneRecordInCache } from '@/object-record/cache/hooks/useCreateOneRecordInCache';
|
import { useCreateOneRecordInCache } from '@/object-record/cache/hooks/useCreateOneRecordInCache';
|
||||||
import { deleteRecordFromCache } from '@/object-record/cache/utils/deleteRecordFromCache';
|
import { deleteRecordFromCache } from '@/object-record/cache/utils/deleteRecordFromCache';
|
||||||
import { getObjectTypename } from '@/object-record/cache/utils/getObjectTypename';
|
import { getObjectTypename } from '@/object-record/cache/utils/getObjectTypename';
|
||||||
|
import { getRecordNodeFromRecord } from '@/object-record/cache/utils/getRecordNodeFromRecord';
|
||||||
import { RecordGqlOperationGqlRecordFields } from '@/object-record/graphql/types/RecordGqlOperationGqlRecordFields';
|
import { RecordGqlOperationGqlRecordFields } from '@/object-record/graphql/types/RecordGqlOperationGqlRecordFields';
|
||||||
import { generateDepthOneRecordGqlFields } from '@/object-record/graphql/utils/generateDepthOneRecordGqlFields';
|
import { generateDepthOneRecordGqlFields } from '@/object-record/graphql/utils/generateDepthOneRecordGqlFields';
|
||||||
import { useCreateOneRecordMutation } from '@/object-record/hooks/useCreateOneRecordMutation';
|
import { useCreateOneRecordMutation } from '@/object-record/hooks/useCreateOneRecordMutation';
|
||||||
import { useRefetchAggregateQueries } from '@/object-record/hooks/useRefetchAggregateQueries';
|
import { useRefetchAggregateQueries } from '@/object-record/hooks/useRefetchAggregateQueries';
|
||||||
import { ObjectRecord } from '@/object-record/types/ObjectRecord';
|
import { ObjectRecord } from '@/object-record/types/ObjectRecord';
|
||||||
|
import { computeOptimisticRecordFromInput } from '@/object-record/utils/computeOptimisticRecordFromInput';
|
||||||
import { getCreateOneRecordMutationResponseField } from '@/object-record/utils/getCreateOneRecordMutationResponseField';
|
import { getCreateOneRecordMutationResponseField } from '@/object-record/utils/getCreateOneRecordMutationResponseField';
|
||||||
import { sanitizeRecordInput } from '@/object-record/utils/sanitizeRecordInput';
|
import { sanitizeRecordInput } from '@/object-record/utils/sanitizeRecordInput';
|
||||||
import { isDefined } from '~/utils/isDefined';
|
import { isDefined } from '~/utils/isDefined';
|
||||||
@ -60,33 +62,48 @@ export const useCreateOneRecord = <
|
|||||||
objectMetadataNamePlural: objectMetadataItem.namePlural,
|
objectMetadataNamePlural: objectMetadataItem.namePlural,
|
||||||
});
|
});
|
||||||
|
|
||||||
const createOneRecord = async (input: Partial<CreatedObjectRecord>) => {
|
const createOneRecord = async (recordInput: Partial<CreatedObjectRecord>) => {
|
||||||
setLoading(true);
|
setLoading(true);
|
||||||
|
|
||||||
const idForCreation = input.id ?? v4();
|
const idForCreation = recordInput.id ?? v4();
|
||||||
|
|
||||||
const sanitizedInput = {
|
const sanitizedInput = {
|
||||||
...sanitizeRecordInput({
|
...sanitizeRecordInput({
|
||||||
objectMetadataItem,
|
objectMetadataItem,
|
||||||
recordInput: input,
|
recordInput,
|
||||||
}),
|
}),
|
||||||
id: idForCreation,
|
id: idForCreation,
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const optimisticRecordInput = computeOptimisticRecordFromInput({
|
||||||
|
cache: apolloClient.cache,
|
||||||
|
objectMetadataItem,
|
||||||
|
objectMetadataItems,
|
||||||
|
recordInput: { ...recordInput, id: idForCreation },
|
||||||
|
});
|
||||||
const recordCreatedInCache = createOneRecordInCache({
|
const recordCreatedInCache = createOneRecordInCache({
|
||||||
...input,
|
...optimisticRecordInput,
|
||||||
id: idForCreation,
|
id: idForCreation,
|
||||||
__typename: getObjectTypename(objectMetadataItem.nameSingular),
|
__typename: getObjectTypename(objectMetadataItem.nameSingular),
|
||||||
});
|
});
|
||||||
|
|
||||||
if (isDefined(recordCreatedInCache)) {
|
if (isDefined(recordCreatedInCache)) {
|
||||||
triggerCreateRecordsOptimisticEffect({
|
const optimisticRecordNode = getRecordNodeFromRecord({
|
||||||
cache: apolloClient.cache,
|
|
||||||
objectMetadataItem,
|
objectMetadataItem,
|
||||||
recordsToCreate: [recordCreatedInCache],
|
|
||||||
objectMetadataItems,
|
objectMetadataItems,
|
||||||
shouldMatchRootQueryFilter,
|
record: recordCreatedInCache,
|
||||||
|
computeReferences: false,
|
||||||
});
|
});
|
||||||
|
|
||||||
|
if (optimisticRecordNode !== null) {
|
||||||
|
triggerCreateRecordsOptimisticEffect({
|
||||||
|
cache: apolloClient.cache,
|
||||||
|
objectMetadataItem,
|
||||||
|
recordsToCreate: [optimisticRecordNode],
|
||||||
|
objectMetadataItems,
|
||||||
|
shouldMatchRootQueryFilter,
|
||||||
|
});
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const mutationResponseField =
|
const mutationResponseField =
|
||||||
@ -100,16 +117,16 @@ export const useCreateOneRecord = <
|
|||||||
},
|
},
|
||||||
update: (cache, { data }) => {
|
update: (cache, { data }) => {
|
||||||
const record = data?.[mutationResponseField];
|
const record = data?.[mutationResponseField];
|
||||||
|
if (skipPostOptmisticEffect === false && isDefined(record)) {
|
||||||
if (!record || skipPostOptmisticEffect) return;
|
triggerCreateRecordsOptimisticEffect({
|
||||||
|
cache,
|
||||||
triggerCreateRecordsOptimisticEffect({
|
objectMetadataItem,
|
||||||
cache,
|
recordsToCreate: [record],
|
||||||
objectMetadataItem,
|
objectMetadataItems,
|
||||||
recordsToCreate: [record],
|
shouldMatchRootQueryFilter,
|
||||||
objectMetadataItems,
|
checkForRecordInCache: true,
|
||||||
shouldMatchRootQueryFilter,
|
});
|
||||||
});
|
}
|
||||||
|
|
||||||
setLoading(false);
|
setLoading(false);
|
||||||
},
|
},
|
||||||
|
|||||||
@ -10,6 +10,7 @@ import { generateDepthOneRecordGqlFields } from '@/object-record/graphql/utils/g
|
|||||||
import { useRefetchAggregateQueries } from '@/object-record/hooks/useRefetchAggregateQueries';
|
import { useRefetchAggregateQueries } from '@/object-record/hooks/useRefetchAggregateQueries';
|
||||||
import { useUpdateOneRecordMutation } from '@/object-record/hooks/useUpdateOneRecordMutation';
|
import { useUpdateOneRecordMutation } from '@/object-record/hooks/useUpdateOneRecordMutation';
|
||||||
import { ObjectRecord } from '@/object-record/types/ObjectRecord';
|
import { ObjectRecord } from '@/object-record/types/ObjectRecord';
|
||||||
|
import { computeOptimisticRecordFromInput } from '@/object-record/utils/computeOptimisticRecordFromInput';
|
||||||
import { getUpdateOneRecordMutationResponseField } from '@/object-record/utils/getUpdateOneRecordMutationResponseField';
|
import { getUpdateOneRecordMutationResponseField } from '@/object-record/utils/getUpdateOneRecordMutationResponseField';
|
||||||
import { sanitizeRecordInput } from '@/object-record/utils/sanitizeRecordInput';
|
import { sanitizeRecordInput } from '@/object-record/utils/sanitizeRecordInput';
|
||||||
import { capitalize } from 'twenty-shared';
|
import { capitalize } from 'twenty-shared';
|
||||||
@ -59,12 +60,12 @@ export const useUpdateOneRecord = <
|
|||||||
updateOneRecordInput: Partial<Omit<UpdatedObjectRecord, 'id'>>;
|
updateOneRecordInput: Partial<Omit<UpdatedObjectRecord, 'id'>>;
|
||||||
optimisticRecord?: Partial<ObjectRecord>;
|
optimisticRecord?: Partial<ObjectRecord>;
|
||||||
}) => {
|
}) => {
|
||||||
const sanitizedInput = {
|
const optimisticRecordInput = computeOptimisticRecordFromInput({
|
||||||
...sanitizeRecordInput({
|
objectMetadataItem,
|
||||||
objectMetadataItem,
|
recordInput: updateOneRecordInput,
|
||||||
recordInput: updateOneRecordInput,
|
cache: apolloClient.cache,
|
||||||
}),
|
objectMetadataItems,
|
||||||
};
|
});
|
||||||
|
|
||||||
const cachedRecord = getRecordFromCache<ObjectRecord>(idToUpdate);
|
const cachedRecord = getRecordFromCache<ObjectRecord>(idToUpdate);
|
||||||
|
|
||||||
@ -73,12 +74,12 @@ export const useUpdateOneRecord = <
|
|||||||
objectMetadataItem,
|
objectMetadataItem,
|
||||||
objectMetadataItems,
|
objectMetadataItems,
|
||||||
recordGqlFields: computedRecordGqlFields,
|
recordGqlFields: computedRecordGqlFields,
|
||||||
computeReferences: true,
|
computeReferences: false,
|
||||||
});
|
});
|
||||||
|
|
||||||
const computedOptimisticRecord = {
|
const computedOptimisticRecord = {
|
||||||
...cachedRecord,
|
...cachedRecord,
|
||||||
...(optimisticRecord ?? sanitizedInput),
|
...(optimisticRecord ?? optimisticRecordInput),
|
||||||
...{ id: idToUpdate },
|
...{ id: idToUpdate },
|
||||||
...{ __typename: capitalize(objectMetadataItem.nameSingular) },
|
...{ __typename: capitalize(objectMetadataItem.nameSingular) },
|
||||||
};
|
};
|
||||||
@ -89,9 +90,8 @@ export const useUpdateOneRecord = <
|
|||||||
objectMetadataItem,
|
objectMetadataItem,
|
||||||
objectMetadataItems,
|
objectMetadataItems,
|
||||||
recordGqlFields: computedRecordGqlFields,
|
recordGqlFields: computedRecordGqlFields,
|
||||||
computeReferences: true,
|
computeReferences: false,
|
||||||
});
|
});
|
||||||
|
|
||||||
if (!optimisticRecordWithConnection || !cachedRecordWithConnection) {
|
if (!optimisticRecordWithConnection || !cachedRecordWithConnection) {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
@ -114,6 +114,12 @@ export const useUpdateOneRecord = <
|
|||||||
const mutationResponseField =
|
const mutationResponseField =
|
||||||
getUpdateOneRecordMutationResponseField(objectNameSingular);
|
getUpdateOneRecordMutationResponseField(objectNameSingular);
|
||||||
|
|
||||||
|
const sanitizedInput = {
|
||||||
|
...sanitizeRecordInput({
|
||||||
|
objectMetadataItem,
|
||||||
|
recordInput: updateOneRecordInput,
|
||||||
|
}),
|
||||||
|
};
|
||||||
const updatedRecord = await apolloClient
|
const updatedRecord = await apolloClient
|
||||||
.mutate({
|
.mutate({
|
||||||
mutation: updateOneRecordMutation,
|
mutation: updateOneRecordMutation,
|
||||||
|
|||||||
@ -30,6 +30,7 @@ import { isFieldArray } from '@/object-record/record-field/types/guards/isFieldA
|
|||||||
import { isFieldArrayValue } from '@/object-record/record-field/types/guards/isFieldArrayValue';
|
import { isFieldArrayValue } from '@/object-record/record-field/types/guards/isFieldArrayValue';
|
||||||
import { isFieldRichText } from '@/object-record/record-field/types/guards/isFieldRichText';
|
import { isFieldRichText } from '@/object-record/record-field/types/guards/isFieldRichText';
|
||||||
import { isFieldRichTextValue } from '@/object-record/record-field/types/guards/isFieldRichTextValue';
|
import { isFieldRichTextValue } from '@/object-record/record-field/types/guards/isFieldRichTextValue';
|
||||||
|
import { getForeignKeyNameFromRelationFieldName } from '@/object-record/utils/getForeignKeyNameFromRelationFieldName';
|
||||||
import { FieldContext } from '../contexts/FieldContext';
|
import { FieldContext } from '../contexts/FieldContext';
|
||||||
import { isFieldBoolean } from '../types/guards/isFieldBoolean';
|
import { isFieldBoolean } from '../types/guards/isFieldBoolean';
|
||||||
import { isFieldBooleanValue } from '../types/guards/isFieldBooleanValue';
|
import { isFieldBooleanValue } from '../types/guards/isFieldBooleanValue';
|
||||||
@ -153,8 +154,8 @@ export const usePersistField = () => {
|
|||||||
variables: {
|
variables: {
|
||||||
where: { id: recordId },
|
where: { id: recordId },
|
||||||
updateOneRecordInput: {
|
updateOneRecordInput: {
|
||||||
[fieldName]: value,
|
[getForeignKeyNameFromRelationFieldName(fieldName)]:
|
||||||
[`${fieldName}Id`]: value?.id ?? null,
|
value?.id ?? null,
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|||||||
@ -35,6 +35,7 @@ import { InlineCellHotkeyScope } from '@/object-record/record-inline-cell/types/
|
|||||||
import { RecordDetailRecordsListItem } from '@/object-record/record-show/record-detail-section/components/RecordDetailRecordsListItem';
|
import { RecordDetailRecordsListItem } from '@/object-record/record-show/record-detail-section/components/RecordDetailRecordsListItem';
|
||||||
import { RecordValueSetterEffect } from '@/object-record/record-store/components/RecordValueSetterEffect';
|
import { RecordValueSetterEffect } from '@/object-record/record-store/components/RecordValueSetterEffect';
|
||||||
import { ObjectRecord } from '@/object-record/types/ObjectRecord';
|
import { ObjectRecord } from '@/object-record/types/ObjectRecord';
|
||||||
|
import { getForeignKeyNameFromRelationFieldName } from '@/object-record/utils/getForeignKeyNameFromRelationFieldName';
|
||||||
import { isFieldCellSupported } from '@/object-record/utils/isFieldCellSupported';
|
import { isFieldCellSupported } from '@/object-record/utils/isFieldCellSupported';
|
||||||
import { Dropdown } from '@/ui/layout/dropdown/components/Dropdown';
|
import { Dropdown } from '@/ui/layout/dropdown/components/Dropdown';
|
||||||
import { DropdownMenuItemsContainer } from '@/ui/layout/dropdown/components/DropdownMenuItemsContainer';
|
import { DropdownMenuItemsContainer } from '@/ui/layout/dropdown/components/DropdownMenuItemsContainer';
|
||||||
@ -162,7 +163,9 @@ export const RecordDetailRelationRecordsListItem = ({
|
|||||||
updateOneRelationRecord({
|
updateOneRelationRecord({
|
||||||
idToUpdate: relationRecord.id,
|
idToUpdate: relationRecord.id,
|
||||||
updateOneRecordInput: {
|
updateOneRecordInput: {
|
||||||
[relationFieldMetadataItem.name]: null,
|
[getForeignKeyNameFromRelationFieldName(
|
||||||
|
relationFieldMetadataItem.name,
|
||||||
|
)]: null,
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|||||||
@ -0,0 +1,182 @@
|
|||||||
|
import { updateRecordFromCache } from '@/object-record/cache/utils/updateRecordFromCache';
|
||||||
|
import { computeOptimisticRecordFromInput } from '@/object-record/utils/computeOptimisticRecordFromInput';
|
||||||
|
import { InMemoryCache } from '@apollo/client';
|
||||||
|
import { generatedMockObjectMetadataItems } from '~/testing/mock-data/generatedMockObjectMetadataItems';
|
||||||
|
|
||||||
|
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 result = computeOptimisticRecordFromInput({
|
||||||
|
objectMetadataItems: generatedMockObjectMetadataItems,
|
||||||
|
objectMetadataItem: personObjectMetadataItem,
|
||||||
|
recordInput: {
|
||||||
|
city: 'Paris',
|
||||||
|
},
|
||||||
|
cache,
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(result).toEqual({
|
||||||
|
city: 'Paris',
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
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 result = computeOptimisticRecordFromInput({
|
||||||
|
objectMetadataItems: generatedMockObjectMetadataItems,
|
||||||
|
objectMetadataItem: personObjectMetadataItem,
|
||||||
|
recordInput: {
|
||||||
|
companyId: '123',
|
||||||
|
},
|
||||||
|
cache,
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(result).toEqual({
|
||||||
|
companyId: '123',
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
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 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',
|
||||||
|
},
|
||||||
|
cache,
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(result).toEqual({
|
||||||
|
companyId: '123',
|
||||||
|
company: companyRecord,
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
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 result = computeOptimisticRecordFromInput({
|
||||||
|
objectMetadataItems: generatedMockObjectMetadataItems,
|
||||||
|
objectMetadataItem: personObjectMetadataItem,
|
||||||
|
recordInput: {
|
||||||
|
companyId: null,
|
||||||
|
},
|
||||||
|
cache,
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(result).toEqual({
|
||||||
|
companyId: null,
|
||||||
|
company: null,
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should throw an error if recordInput contains fiels 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');
|
||||||
|
}
|
||||||
|
|
||||||
|
expect(() =>
|
||||||
|
computeOptimisticRecordFromInput({
|
||||||
|
objectMetadataItems: generatedMockObjectMetadataItems,
|
||||||
|
objectMetadataItem: personObjectMetadataItem,
|
||||||
|
recordInput: {
|
||||||
|
unknwon: 'unknown',
|
||||||
|
foo: 'foo',
|
||||||
|
bar: 'bar',
|
||||||
|
city: 'Paris',
|
||||||
|
},
|
||||||
|
cache,
|
||||||
|
}),
|
||||||
|
).toThrowErrorMatchingInlineSnapshot(
|
||||||
|
`"Should never occur, encountered unknown fields unknwon, foo, bar in objectMetadaItem person"`,
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
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');
|
||||||
|
}
|
||||||
|
|
||||||
|
expect(() =>
|
||||||
|
computeOptimisticRecordFromInput({
|
||||||
|
objectMetadataItems: generatedMockObjectMetadataItems,
|
||||||
|
objectMetadataItem: personObjectMetadataItem,
|
||||||
|
recordInput: {
|
||||||
|
companyId: '123',
|
||||||
|
company: {},
|
||||||
|
},
|
||||||
|
cache,
|
||||||
|
}),
|
||||||
|
).toThrowErrorMatchingInlineSnapshot(
|
||||||
|
`"Should never provide relation mutation through anything else than the fieldId e.g companyId"`,
|
||||||
|
);
|
||||||
|
});
|
||||||
|
});
|
||||||
@ -0,0 +1,153 @@
|
|||||||
|
import { isNull, isUndefined } from '@sniptt/guards';
|
||||||
|
|
||||||
|
import { ObjectMetadataItem } from '@/object-metadata/types/ObjectMetadataItem';
|
||||||
|
import {
|
||||||
|
getRecordFromCache,
|
||||||
|
GetRecordFromCacheArgs,
|
||||||
|
} from '@/object-record/cache/utils/getRecordFromCache';
|
||||||
|
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 { getForeignKeyNameFromRelationFieldName } from '@/object-record/utils/getForeignKeyNameFromRelationFieldName';
|
||||||
|
import { RelationDefinitionType } from '~/generated-metadata/graphql';
|
||||||
|
import { FieldMetadataType } from '~/generated/graphql';
|
||||||
|
import { isDefined } from '~/utils/isDefined';
|
||||||
|
|
||||||
|
type ComputeOptimisticCacheRecordInputArgs = {
|
||||||
|
objectMetadataItem: ObjectMetadataItem;
|
||||||
|
recordInput: Partial<ObjectRecord>;
|
||||||
|
} & Pick<GetRecordFromCacheArgs, 'cache' | 'objectMetadataItems'>;
|
||||||
|
export const computeOptimisticRecordFromInput = ({
|
||||||
|
objectMetadataItem,
|
||||||
|
recordInput,
|
||||||
|
cache,
|
||||||
|
objectMetadataItems,
|
||||||
|
}: ComputeOptimisticCacheRecordInputArgs) => {
|
||||||
|
const unknownRecordInputFields = Object.keys(recordInput).filter(
|
||||||
|
(fieldName) =>
|
||||||
|
objectMetadataItem.fields.find(({ name }) => name === fieldName) ===
|
||||||
|
undefined,
|
||||||
|
);
|
||||||
|
if (unknownRecordInputFields.length > 0) {
|
||||||
|
throw new Error(
|
||||||
|
`Should never occur, encountered unknown fields ${unknownRecordInputFields.join(', ')} in objectMetadaItem ${objectMetadataItem.nameSingular}`,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
const optimisticRecord: Partial<ObjectRecord> = {};
|
||||||
|
for (const fieldMetadataItem of objectMetadataItem.fields) {
|
||||||
|
if (isFieldUuid(fieldMetadataItem)) {
|
||||||
|
const isRelationFieldId = objectMetadataItem.fields.some(
|
||||||
|
({ type, relationDefinition }) => {
|
||||||
|
if (type !== FieldMetadataType.RELATION) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!isDefined(relationDefinition)) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
const sourceFieldName = relationDefinition.sourceFieldMetadata.name;
|
||||||
|
return (
|
||||||
|
getForeignKeyNameFromRelationFieldName(sourceFieldName) ===
|
||||||
|
fieldMetadataItem.name
|
||||||
|
);
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
if (isRelationFieldId) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const isRelationField = isFieldRelation(fieldMetadataItem);
|
||||||
|
|
||||||
|
const recordInputFieldValue: unknown = recordInput[fieldMetadataItem.name];
|
||||||
|
|
||||||
|
if (!isRelationField) {
|
||||||
|
if (!isDefined(recordInputFieldValue)) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!fieldMetadataItem.isNullable && recordInputFieldValue == null) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
optimisticRecord[fieldMetadataItem.name] = recordInputFieldValue;
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (
|
||||||
|
fieldMetadataItem.relationDefinition?.direction ===
|
||||||
|
RelationDefinitionType.ONE_TO_MANY
|
||||||
|
) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
const isManyToOneRelation =
|
||||||
|
fieldMetadataItem.relationDefinition?.direction ===
|
||||||
|
RelationDefinitionType.MANY_TO_ONE;
|
||||||
|
if (!isManyToOneRelation) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (isDefined(recordInputFieldValue)) {
|
||||||
|
throw new Error(
|
||||||
|
'Should never provide relation mutation through anything else than the fieldId e.g companyId',
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
const relationFieldIdName = getForeignKeyNameFromRelationFieldName(
|
||||||
|
fieldMetadataItem.name,
|
||||||
|
);
|
||||||
|
const recordInputFieldIdValue: string | null | undefined =
|
||||||
|
recordInput[relationFieldIdName];
|
||||||
|
|
||||||
|
if (isUndefined(recordInputFieldIdValue)) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
const relationIdFieldMetadataItem = objectMetadataItem.fields.find(
|
||||||
|
(field) => field.name === relationFieldIdName,
|
||||||
|
);
|
||||||
|
if (!isDefined(relationIdFieldMetadataItem)) {
|
||||||
|
throw new Error(
|
||||||
|
'Should never occur, encountered unknown relationId within relations definitions',
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (isNull(recordInputFieldIdValue)) {
|
||||||
|
optimisticRecord[relationFieldIdName] = null;
|
||||||
|
optimisticRecord[fieldMetadataItem.name] = null;
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
const targetNameSingular =
|
||||||
|
fieldMetadataItem.relationDefinition?.targetObjectMetadata.nameSingular;
|
||||||
|
const targetObjectMetataDataItem = objectMetadataItems.find(
|
||||||
|
({ nameSingular }) => nameSingular === targetNameSingular,
|
||||||
|
);
|
||||||
|
if (!isDefined(targetObjectMetataDataItem)) {
|
||||||
|
throw new Error(
|
||||||
|
'Should never occur, encountered invalid relation definition',
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
const cachedRecord = getRecordFromCache({
|
||||||
|
cache,
|
||||||
|
objectMetadataItem: targetObjectMetataDataItem,
|
||||||
|
objectMetadataItems,
|
||||||
|
recordId: recordInputFieldIdValue as string,
|
||||||
|
});
|
||||||
|
|
||||||
|
optimisticRecord[relationFieldIdName] = recordInputFieldIdValue;
|
||||||
|
|
||||||
|
if (!isDefined(cachedRecord) || Object.keys(cachedRecord).length <= 0) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
optimisticRecord[fieldMetadataItem.name] = cachedRecord;
|
||||||
|
}
|
||||||
|
|
||||||
|
return optimisticRecord;
|
||||||
|
};
|
||||||
@ -0,0 +1,2 @@
|
|||||||
|
export const getForeignKeyNameFromRelationFieldName = (nameSingular: string) =>
|
||||||
|
`${nameSingular}Id`;
|
||||||
@ -1,11 +1,8 @@
|
|||||||
import { isString } from '@sniptt/guards';
|
|
||||||
|
|
||||||
import { ObjectMetadataItem } from '@/object-metadata/types/ObjectMetadataItem';
|
import { ObjectMetadataItem } from '@/object-metadata/types/ObjectMetadataItem';
|
||||||
import { ObjectRecord } from '@/object-record/types/ObjectRecord';
|
import { ObjectRecord } from '@/object-record/types/ObjectRecord';
|
||||||
import { RelationDefinitionType } from '~/generated-metadata/graphql';
|
import { RelationDefinitionType } from '~/generated-metadata/graphql';
|
||||||
import { FieldMetadataType } from '~/generated/graphql';
|
import { FieldMetadataType } from '~/generated/graphql';
|
||||||
import { isDefined } from '~/utils/isDefined';
|
import { isDefined } from '~/utils/isDefined';
|
||||||
import { getUrlHostName } from '~/utils/url/getUrlHostName';
|
|
||||||
|
|
||||||
export const sanitizeRecordInput = ({
|
export const sanitizeRecordInput = ({
|
||||||
objectMetadataItem,
|
objectMetadataItem,
|
||||||
@ -56,15 +53,5 @@ export const sanitizeRecordInput = ({
|
|||||||
})
|
})
|
||||||
.filter(isDefined),
|
.filter(isDefined),
|
||||||
);
|
);
|
||||||
if (
|
return filteredResultRecord;
|
||||||
!(
|
|
||||||
isDefined(filteredResultRecord.domainName) &&
|
|
||||||
isString(filteredResultRecord.domainName)
|
|
||||||
)
|
|
||||||
)
|
|
||||||
return filteredResultRecord;
|
|
||||||
return {
|
|
||||||
...filteredResultRecord,
|
|
||||||
domainName: getUrlHostName(filteredResultRecord.domainName as string),
|
|
||||||
};
|
|
||||||
};
|
};
|
||||||
|
|||||||
Reference in New Issue
Block a user