feat: delete favorite in cache on related record deletion (#3751)

* feat: delete favorite in cache on related record deletion

* fix: fix useCreateOneRecord tests

* fix: fix usePipelineSteps tests

* fix: fix useCreateManyRecords tests

* fix: add null relation field values in useGenerateObjectRecordOptimisticResponse
This commit is contained in:
Thaïs
2024-02-01 12:09:32 -03:00
committed by GitHub
parent 142affbeea
commit 7adb5cc00d
20 changed files with 376 additions and 275 deletions

View File

@ -1,8 +1,10 @@
import { ApolloCache, StoreObject } from '@apollo/client'; import { ApolloCache, StoreObject } from '@apollo/client';
import { isCachedObjectRecordConnection } from '@/apollo/optimistic-effect/utils/isCachedObjectRecordConnection'; import { isCachedObjectRecordConnection } from '@/apollo/optimistic-effect/utils/isCachedObjectRecordConnection';
import { triggerUpdateRelationsOptimisticEffect } from '@/apollo/optimistic-effect/utils/triggerUpdateRelationsOptimisticEffect';
import { CachedObjectRecord } from '@/apollo/types/CachedObjectRecord'; import { CachedObjectRecord } from '@/apollo/types/CachedObjectRecord';
import { CachedObjectRecordEdge } from '@/apollo/types/CachedObjectRecordEdge'; import { CachedObjectRecordEdge } from '@/apollo/types/CachedObjectRecordEdge';
import { useGetRelationMetadata } from '@/object-metadata/hooks/useGetRelationMetadata';
import { ObjectMetadataItem } from '@/object-metadata/types/ObjectMetadataItem'; import { ObjectMetadataItem } from '@/object-metadata/types/ObjectMetadataItem';
import { capitalize } from '~/utils/string/capitalize'; import { capitalize } from '~/utils/string/capitalize';
@ -15,15 +17,27 @@ export const triggerCreateRecordsOptimisticEffect = ({
cache, cache,
objectMetadataItem, objectMetadataItem,
records, records,
getRelationMetadata,
}: { }: {
cache: ApolloCache<unknown>; cache: ApolloCache<unknown>;
objectMetadataItem: ObjectMetadataItem; objectMetadataItem: ObjectMetadataItem;
records: CachedObjectRecord[]; records: CachedObjectRecord[];
getRelationMetadata: ReturnType<typeof useGetRelationMetadata>;
}) => { }) => {
const objectEdgeTypeName = `${capitalize( const objectEdgeTypeName = `${capitalize(
objectMetadataItem.nameSingular, objectMetadataItem.nameSingular,
)}Edge`; )}Edge`;
records.forEach((record) =>
triggerUpdateRelationsOptimisticEffect({
cache,
objectMetadataItem,
previousRecord: null,
nextRecord: record,
getRelationMetadata,
}),
);
cache.modify<StoreObject>({ cache.modify<StoreObject>({
fields: { fields: {
[objectMetadataItem.namePlural]: ( [objectMetadataItem.namePlural]: (

View File

@ -1,9 +1,11 @@
import { ApolloCache, StoreObject } from '@apollo/client'; import { ApolloCache, StoreObject } from '@apollo/client';
import { isCachedObjectRecordConnection } from '@/apollo/optimistic-effect/utils/isCachedObjectRecordConnection'; import { isCachedObjectRecordConnection } from '@/apollo/optimistic-effect/utils/isCachedObjectRecordConnection';
import { triggerUpdateRelationsOptimisticEffect } from '@/apollo/optimistic-effect/utils/triggerUpdateRelationsOptimisticEffect';
import { CachedObjectRecord } from '@/apollo/types/CachedObjectRecord'; import { CachedObjectRecord } from '@/apollo/types/CachedObjectRecord';
import { CachedObjectRecordEdge } from '@/apollo/types/CachedObjectRecordEdge'; import { CachedObjectRecordEdge } from '@/apollo/types/CachedObjectRecordEdge';
import { CachedObjectRecordQueryVariables } from '@/apollo/types/CachedObjectRecordQueryVariables'; import { CachedObjectRecordQueryVariables } from '@/apollo/types/CachedObjectRecordQueryVariables';
import { useGetRelationMetadata } from '@/object-metadata/hooks/useGetRelationMetadata';
import { ObjectMetadataItem } from '@/object-metadata/types/ObjectMetadataItem'; import { ObjectMetadataItem } from '@/object-metadata/types/ObjectMetadataItem';
import { isDefined } from '~/utils/isDefined'; import { isDefined } from '~/utils/isDefined';
import { parseApolloStoreFieldName } from '~/utils/parseApolloStoreFieldName'; import { parseApolloStoreFieldName } from '~/utils/parseApolloStoreFieldName';
@ -12,10 +14,12 @@ export const triggerDeleteRecordsOptimisticEffect = ({
cache, cache,
objectMetadataItem, objectMetadataItem,
records, records,
getRelationMetadata,
}: { }: {
cache: ApolloCache<unknown>; cache: ApolloCache<unknown>;
objectMetadataItem: ObjectMetadataItem; objectMetadataItem: ObjectMetadataItem;
records: Pick<CachedObjectRecord, 'id' | '__typename'>[]; records: CachedObjectRecord[];
getRelationMetadata: ReturnType<typeof useGetRelationMetadata>;
}) => { }) => {
cache.modify<StoreObject>({ cache.modify<StoreObject>({
fields: { fields: {
@ -64,5 +68,15 @@ export const triggerDeleteRecordsOptimisticEffect = ({
}, },
}); });
cache.gc(); records.forEach((record) => {
triggerUpdateRelationsOptimisticEffect({
cache,
objectMetadataItem,
previousRecord: record,
nextRecord: null,
getRelationMetadata,
});
cache.evict({ id: cache.identify(record) });
});
}; };

View File

@ -2,9 +2,11 @@ import { ApolloCache, StoreObject } from '@apollo/client';
import { isCachedObjectRecordConnection } from '@/apollo/optimistic-effect/utils/isCachedObjectRecordConnection'; import { isCachedObjectRecordConnection } from '@/apollo/optimistic-effect/utils/isCachedObjectRecordConnection';
import { sortCachedObjectEdges } from '@/apollo/optimistic-effect/utils/sortCachedObjectEdges'; import { sortCachedObjectEdges } from '@/apollo/optimistic-effect/utils/sortCachedObjectEdges';
import { triggerUpdateRelationsOptimisticEffect } from '@/apollo/optimistic-effect/utils/triggerUpdateRelationsOptimisticEffect';
import { CachedObjectRecord } from '@/apollo/types/CachedObjectRecord'; import { CachedObjectRecord } from '@/apollo/types/CachedObjectRecord';
import { CachedObjectRecordEdge } from '@/apollo/types/CachedObjectRecordEdge'; import { CachedObjectRecordEdge } from '@/apollo/types/CachedObjectRecordEdge';
import { CachedObjectRecordQueryVariables } from '@/apollo/types/CachedObjectRecordQueryVariables'; import { CachedObjectRecordQueryVariables } from '@/apollo/types/CachedObjectRecordQueryVariables';
import { useGetRelationMetadata } from '@/object-metadata/hooks/useGetRelationMetadata';
import { ObjectMetadataItem } from '@/object-metadata/types/ObjectMetadataItem'; import { ObjectMetadataItem } from '@/object-metadata/types/ObjectMetadataItem';
import { isRecordMatchingFilter } from '@/object-record/record-filter/utils/isRecordMatchingFilter'; import { isRecordMatchingFilter } from '@/object-record/record-filter/utils/isRecordMatchingFilter';
import { isDefined } from '~/utils/isDefined'; import { isDefined } from '~/utils/isDefined';
@ -14,16 +16,29 @@ import { capitalize } from '~/utils/string/capitalize';
export const triggerUpdateRecordOptimisticEffect = ({ export const triggerUpdateRecordOptimisticEffect = ({
cache, cache,
objectMetadataItem, objectMetadataItem,
record, previousRecord,
nextRecord,
getRelationMetadata,
}: { }: {
cache: ApolloCache<unknown>; cache: ApolloCache<unknown>;
objectMetadataItem: ObjectMetadataItem; objectMetadataItem: ObjectMetadataItem;
record: CachedObjectRecord; previousRecord: CachedObjectRecord;
nextRecord: CachedObjectRecord;
getRelationMetadata: ReturnType<typeof useGetRelationMetadata>;
}) => { }) => {
const objectEdgeTypeName = `${capitalize( const objectEdgeTypeName = `${capitalize(
objectMetadataItem.nameSingular, objectMetadataItem.nameSingular,
)}Edge`; )}Edge`;
triggerUpdateRelationsOptimisticEffect({
cache,
objectMetadataItem,
previousRecord,
nextRecord,
getRelationMetadata,
});
// Optimistically update record lists
cache.modify<StoreObject>({ cache.modify<StoreObject>({
fields: { fields: {
[objectMetadataItem.namePlural]: ( [objectMetadataItem.namePlural]: (
@ -49,18 +64,20 @@ export const triggerUpdateRecordOptimisticEffect = ({
); );
let nextCachedEdges = cachedEdges ? [...cachedEdges] : []; let nextCachedEdges = cachedEdges ? [...cachedEdges] : [];
// Test if the record matches this list's filters
if (variables?.filter) { if (variables?.filter) {
const matchesFilter = isRecordMatchingFilter({ const matchesFilter = isRecordMatchingFilter({
record, record: nextRecord,
filter: variables.filter, filter: variables.filter,
objectMetadataItem, objectMetadataItem,
}); });
const recordIndex = nextCachedEdges.findIndex( const recordIndex = nextCachedEdges.findIndex(
(cachedEdge) => readField('id', cachedEdge.node) === record.id, (cachedEdge) => readField('id', cachedEdge.node) === nextRecord.id,
); );
// If after update, the record matches this list's filters, then add it to the list
if (matchesFilter && recordIndex === -1) { if (matchesFilter && recordIndex === -1) {
const nodeReference = toReference(record); const nodeReference = toReference(nextRecord);
nodeReference && nodeReference &&
nextCachedEdges.push({ nextCachedEdges.push({
__typename: objectEdgeTypeName, __typename: objectEdgeTypeName,
@ -69,11 +86,13 @@ export const triggerUpdateRecordOptimisticEffect = ({
}); });
} }
// If after update, the record does not match this list's filters anymore, then remove it from the list
if (!matchesFilter && recordIndex > -1) { if (!matchesFilter && recordIndex > -1) {
nextCachedEdges.splice(recordIndex, 1); nextCachedEdges.splice(recordIndex, 1);
} }
} }
// Sort updated list
if (variables?.orderBy) { if (variables?.orderBy) {
nextCachedEdges = sortCachedObjectEdges({ nextCachedEdges = sortCachedObjectEdges({
edges: nextCachedEdges, edges: nextCachedEdges,
@ -82,6 +101,7 @@ export const triggerUpdateRecordOptimisticEffect = ({
}); });
} }
// Limit the updated list to the required size
if (isDefined(variables?.first)) { if (isDefined(variables?.first)) {
// If previous edges length was exactly at the required limit, // If previous edges length was exactly at the required limit,
// but after update next edges length is under the limit, // but after update next edges length is under the limit,

View File

@ -0,0 +1,112 @@
import { ApolloCache } from '@apollo/client';
import { isObjectRecordConnection } from '@/apollo/optimistic-effect/utils/isObjectRecordConnection';
import { triggerAttachRelationOptimisticEffect } from '@/apollo/optimistic-effect/utils/triggerAttachRelationOptimisticEffect';
import { triggerDeleteRecordsOptimisticEffect } from '@/apollo/optimistic-effect/utils/triggerDeleteRecordsOptimisticEffect';
import { triggerDetachRelationOptimisticEffect } from '@/apollo/optimistic-effect/utils/triggerDetachRelationOptimisticEffect';
import { CachedObjectRecord } from '@/apollo/types/CachedObjectRecord';
import { coreObjectNamesToDeleteOnRelationDetach } from '@/apollo/types/coreObjectNamesToDeleteOnRelationDetach';
import { useGetRelationMetadata } from '@/object-metadata/hooks/useGetRelationMetadata';
import { CoreObjectNameSingular } from '@/object-metadata/types/CoreObjectNameSingular';
import { ObjectMetadataItem } from '@/object-metadata/types/ObjectMetadataItem';
import { ObjectRecordConnection } from '@/object-record/types/ObjectRecordConnection';
import { isDeeplyEqual } from '~/utils/isDeeplyEqual';
import { isDefined } from '~/utils/isDefined';
export const triggerUpdateRelationsOptimisticEffect = ({
cache,
objectMetadataItem,
previousRecord,
nextRecord,
getRelationMetadata,
}: {
cache: ApolloCache<unknown>;
objectMetadataItem: ObjectMetadataItem;
previousRecord: CachedObjectRecord | null;
nextRecord: CachedObjectRecord | null;
getRelationMetadata: ReturnType<typeof useGetRelationMetadata>;
}) =>
// Optimistically update relation records
objectMetadataItem.fields.forEach((fieldMetadataItem) => {
if (nextRecord && !(fieldMetadataItem.name in nextRecord)) return;
const relationMetadata = getRelationMetadata({
fieldMetadataItem,
});
if (!relationMetadata) return;
const {
// Object metadata for the related record
relationObjectMetadataItem,
// Field on the related record
relationFieldMetadataItem,
} = relationMetadata;
const previousFieldValue:
| ObjectRecordConnection
| CachedObjectRecord
| null = previousRecord?.[fieldMetadataItem.name];
const nextFieldValue: ObjectRecordConnection | CachedObjectRecord | null =
nextRecord?.[fieldMetadataItem.name];
if (isDeeplyEqual(previousFieldValue, nextFieldValue)) return;
const isPreviousFieldValueRecordConnection = isObjectRecordConnection(
relationObjectMetadataItem.nameSingular,
previousFieldValue,
);
const relationRecordsToDetach = isPreviousFieldValueRecordConnection
? previousFieldValue.edges.map(({ node }) => node as CachedObjectRecord)
: [previousFieldValue].filter(isDefined);
const isNextFieldValueRecordConnection = isObjectRecordConnection(
relationObjectMetadataItem.nameSingular,
nextFieldValue,
);
const relationRecordsToAttach = isNextFieldValueRecordConnection
? nextFieldValue.edges.map(({ node }) => node as CachedObjectRecord)
: [nextFieldValue].filter(isDefined);
if (previousRecord && relationRecordsToDetach.length) {
const shouldDeleteRelationRecord =
coreObjectNamesToDeleteOnRelationDetach.includes(
relationObjectMetadataItem.nameSingular as CoreObjectNameSingular,
);
if (shouldDeleteRelationRecord) {
triggerDeleteRecordsOptimisticEffect({
cache,
objectMetadataItem: relationObjectMetadataItem,
records: relationRecordsToDetach,
getRelationMetadata,
});
} else {
relationRecordsToDetach.forEach((relationRecordToDetach) => {
triggerDetachRelationOptimisticEffect({
cache,
objectNameSingular: objectMetadataItem.nameSingular,
recordId: previousRecord.id,
relationFieldName: relationFieldMetadataItem.name,
relationObjectMetadataNameSingular:
relationObjectMetadataItem.nameSingular,
relationRecordId: relationRecordToDetach.id,
});
});
}
}
if (nextRecord && relationRecordsToAttach.length) {
relationRecordsToAttach.forEach((relationRecordToAttach) =>
triggerAttachRelationOptimisticEffect({
cache,
objectNameSingular: objectMetadataItem.nameSingular,
recordId: nextRecord.id,
relationFieldName: relationFieldMetadataItem.name,
relationObjectMetadataNameSingular:
relationObjectMetadataItem.nameSingular,
relationRecordId: relationRecordToAttach.id,
}),
);
}
});

View File

@ -0,0 +1,5 @@
import { CoreObjectNameSingular } from '@/object-metadata/types/CoreObjectNameSingular';
export const coreObjectNamesToDeleteOnRelationDetach = [
CoreObjectNameSingular.Favorite,
];

View File

@ -1,41 +0,0 @@
import { v4 } from 'uuid';
import { z } from 'zod';
import { ObjectMetadataItem } from '@/object-metadata/types/ObjectMetadataItem';
import { ObjectRecord } from '@/object-record/types/ObjectRecord';
import { generateEmptyFieldValue } from '@/object-record/utils/generateEmptyFieldValue';
import { capitalize } from '~/utils/string/capitalize';
export const useGenerateCachedObjectRecord = ({
objectMetadataItem,
}: {
objectMetadataItem: ObjectMetadataItem;
}) => {
const generateCachedObjectRecord = <
GeneratedObjectRecord extends ObjectRecord,
>(
input: Record<string, unknown>,
) => {
const recordSchema = z.object(
Object.fromEntries(
objectMetadataItem.fields.map((fieldMetadataItem) => [
fieldMetadataItem.name,
z.unknown().default(generateEmptyFieldValue(fieldMetadataItem)),
]),
),
);
return {
__typename: capitalize(objectMetadataItem.nameSingular),
...recordSchema.parse({
id: v4(),
createdAt: new Date().toISOString(),
...input,
}),
} as GeneratedObjectRecord & { __typename: string };
};
return {
generateCachedObjectRecord,
};
};

View File

@ -0,0 +1,72 @@
import { v4 } from 'uuid';
import { z } from 'zod';
import { useGetRelationMetadata } from '@/object-metadata/hooks/useGetRelationMetadata';
import { ObjectMetadataItem } from '@/object-metadata/types/ObjectMetadataItem';
import { ObjectRecord } from '@/object-record/types/ObjectRecord';
import { generateEmptyFieldValue } from '@/object-record/utils/generateEmptyFieldValue';
import { capitalize } from '~/utils/string/capitalize';
export const useGenerateObjectRecordOptimisticResponse = ({
objectMetadataItem,
}: {
objectMetadataItem: ObjectMetadataItem;
}) => {
const getRelationMetadata = useGetRelationMetadata();
const generateObjectRecordOptimisticResponse = <
GeneratedObjectRecord extends ObjectRecord,
>(
input: Record<string, unknown>,
) => {
const recordSchema = z.object(
Object.fromEntries(
objectMetadataItem.fields.map((fieldMetadataItem) => [
fieldMetadataItem.name,
z.unknown().default(generateEmptyFieldValue(fieldMetadataItem)),
]),
),
);
const inputWithRelationFields = objectMetadataItem.fields.reduce(
(result, fieldMetadataItem) => {
const relationIdFieldName = `${fieldMetadataItem.name}Id`;
if (!(relationIdFieldName in input)) return result;
const relationMetadata = getRelationMetadata({ fieldMetadataItem });
if (!relationMetadata) return result;
const relationRecordTypeName = capitalize(
relationMetadata.relationObjectMetadataItem.nameSingular,
);
const relationRecordId = result[relationIdFieldName] as string | null;
return {
...result,
[fieldMetadataItem.name]: relationRecordId
? {
__typename: relationRecordTypeName,
id: relationRecordId,
}
: null,
};
},
input,
);
return {
__typename: capitalize(objectMetadataItem.nameSingular),
...recordSchema.parse({
id: v4(),
createdAt: new Date().toISOString(),
...inputWithRelationFields,
}),
} as GeneratedObjectRecord & { __typename: string };
};
return {
generateObjectRecordOptimisticResponse,
};
};

View File

@ -1,5 +1,7 @@
import { gql } from '@apollo/client'; import { gql } from '@apollo/client';
import { Person } from '@/people/types/Person';
export const query = gql` export const query = gql`
mutation CreatePeople($data: [PersonCreateInput!]!) { mutation CreatePeople($data: [PersonCreateInput!]!) {
createPeople(data: $data) { createPeople(data: $data) {
@ -67,12 +69,15 @@ export const query = gql`
} }
`; `;
export const variables = { const data = [
data: [ {
{ id: 'a7286b9a-c039-4a89-9567-2dfa7953cda9' }, id: 'a7286b9a-c039-4a89-9567-2dfa7953cda9',
{ id: '37faabcd-cb39-4a0a-8618-7e3fda9afca0' }, name: { firstName: 'John', lastName: 'Doe' },
], },
}; { id: '37faabcd-cb39-4a0a-8618-7e3fda9afca0', jobTitle: 'manager' },
] satisfies Partial<Person>[];
export const variables = { data };
export const responseData = { export const responseData = {
opportunities: { opportunities: {
@ -114,3 +119,8 @@ export const responseData = {
avatarUrl: '', avatarUrl: '',
companyId: '', companyId: '',
}; };
export const response = data.map((personData) => ({
...responseData,
...personData,
}));

View File

@ -67,10 +67,6 @@ export const query = gql`
} }
`; `;
export const variables = {
input: { id: 'a7286b9a-c039-4a89-9567-2dfa7953cda9' },
};
export const responseData = { export const responseData = {
opportunities: { opportunities: {
edges: [], edges: [],

View File

@ -1,19 +1,27 @@
import { ReactNode } from 'react'; import { ReactNode } from 'react';
import { MockedProvider } from '@apollo/client/testing'; import { MockedProvider } from '@apollo/client/testing';
import { mocked } from '@storybook/test';
import { act, renderHook } from '@testing-library/react'; import { act, renderHook } from '@testing-library/react';
import { RecoilRoot } from 'recoil'; import { RecoilRoot } from 'recoil';
import { v4 } from 'uuid';
import { CoreObjectNameSingular } from '@/object-metadata/types/CoreObjectNameSingular';
import { import {
query, query,
responseData, response,
variables, variables,
} from '@/object-record/hooks/__mocks__/useCreateManyRecords'; } from '@/object-record/hooks/__mocks__/useCreateManyRecords';
import { useCreateManyRecords } from '@/object-record/hooks/useCreateManyRecords'; import { useCreateManyRecords } from '@/object-record/hooks/useCreateManyRecords';
const people = [ jest.mock('uuid', () => ({
{ id: 'a7286b9a-c039-4a89-9567-2dfa7953cda9' }, v4: jest.fn(),
{ id: '37faabcd-cb39-4a0a-8618-7e3fda9afca0' }, }));
];
mocked(v4)
.mockReturnValueOnce(variables.data[0].id)
.mockReturnValueOnce(variables.data[1].id);
const input = variables.data.map(({ id: _id, ...personInput }) => personInput);
const mocks = [ const mocks = [
{ {
@ -23,10 +31,7 @@ const mocks = [
}, },
result: jest.fn(() => ({ result: jest.fn(() => ({
data: { data: {
createPeople: people.map((person) => ({ createPeople: response,
id: person.id,
...responseData,
})),
}, },
})), })),
}, },
@ -43,19 +48,18 @@ const Wrapper = ({ children }: { children: ReactNode }) => (
describe('useCreateManyRecords', () => { describe('useCreateManyRecords', () => {
it('works as expected', async () => { it('works as expected', async () => {
const { result } = renderHook( const { result } = renderHook(
() => useCreateManyRecords({ objectNameSingular: 'person' }), () =>
useCreateManyRecords({
objectNameSingular: CoreObjectNameSingular.Person,
}),
{ {
wrapper: Wrapper, wrapper: Wrapper,
}, },
); );
await act(async () => { await act(async () => {
const res = await result.current.createManyRecords(people); const res = await result.current.createManyRecords(input);
expect(res).toBeDefined(); expect(res).toEqual(response);
expect(Array.isArray(res)).toBe(true);
expect(res?.length).toBe(2);
expect(res?.[0].id).toBe(people[0].id);
expect(res?.[1].id).toBe(people[1].id);
}); });
expect(mocks[0].result).toHaveBeenCalled(); expect(mocks[0].result).toHaveBeenCalled();

View File

@ -3,24 +3,29 @@ import { MockedProvider } from '@apollo/client/testing';
import { act, renderHook } from '@testing-library/react'; import { act, renderHook } from '@testing-library/react';
import { RecoilRoot } from 'recoil'; import { RecoilRoot } from 'recoil';
import { CoreObjectNameSingular } from '@/object-metadata/types/CoreObjectNameSingular';
import { import {
query, query,
responseData, responseData,
variables,
} from '@/object-record/hooks/__mocks__/useCreateOneRecord'; } from '@/object-record/hooks/__mocks__/useCreateOneRecord';
import { useCreateOneRecord } from '@/object-record/hooks/useCreateOneRecord'; import { useCreateOneRecord } from '@/object-record/hooks/useCreateOneRecord';
const person = { id: 'a7286b9a-c039-4a89-9567-2dfa7953cda9' }; const personId = 'a7286b9a-c039-4a89-9567-2dfa7953cda9';
const input = { name: { firstName: 'John', lastName: 'Doe' } };
jest.mock('uuid', () => ({
v4: jest.fn(() => personId),
}));
const mocks = [ const mocks = [
{ {
request: { request: {
query, query,
variables, variables: { input: { ...input, id: personId } },
}, },
result: jest.fn(() => ({ result: jest.fn(() => ({
data: { data: {
createPerson: { ...person, ...responseData }, createPerson: { ...responseData, ...input, id: personId },
}, },
})), })),
}, },
@ -37,16 +42,20 @@ const Wrapper = ({ children }: { children: ReactNode }) => (
describe('useCreateOneRecord', () => { describe('useCreateOneRecord', () => {
it('works as expected', async () => { it('works as expected', async () => {
const { result } = renderHook( const { result } = renderHook(
() => useCreateOneRecord({ objectNameSingular: 'person' }), () =>
useCreateOneRecord({
objectNameSingular: CoreObjectNameSingular.Person,
}),
{ {
wrapper: Wrapper, wrapper: Wrapper,
}, },
); );
await act(async () => { await act(async () => {
const res = await result.current.createOneRecord(person); const res = await result.current.createOneRecord(input);
console.log('res', res);
expect(res).toBeDefined(); expect(res).toBeDefined();
expect(res).toHaveProperty('id', person.id); expect(res).toHaveProperty('id', personId);
}); });
expect(mocks[0].result).toHaveBeenCalled(); expect(mocks[0].result).toHaveBeenCalled();

View File

@ -1,9 +1,11 @@
import { useApolloClient } from '@apollo/client'; import { useApolloClient } from '@apollo/client';
import { v4 } from 'uuid';
import { triggerCreateRecordsOptimisticEffect } from '@/apollo/optimistic-effect/utils/triggerCreateRecordsOptimisticEffect'; import { triggerCreateRecordsOptimisticEffect } from '@/apollo/optimistic-effect/utils/triggerCreateRecordsOptimisticEffect';
import { useGetRelationMetadata } from '@/object-metadata/hooks/useGetRelationMetadata';
import { useObjectMetadataItem } from '@/object-metadata/hooks/useObjectMetadataItem'; import { useObjectMetadataItem } from '@/object-metadata/hooks/useObjectMetadataItem';
import { ObjectMetadataItemIdentifier } from '@/object-metadata/types/ObjectMetadataItemIdentifier'; import { ObjectMetadataItemIdentifier } from '@/object-metadata/types/ObjectMetadataItemIdentifier';
import { useGenerateCachedObjectRecord } from '@/object-record/cache/hooks/useGenerateCachedObjectRecord'; import { useGenerateObjectRecordOptimisticResponse } from '@/object-record/cache/hooks/useGenerateObjectRecordOptimisticResponse';
import { getCreateManyRecordsMutationResponseField } from '@/object-record/hooks/useGenerateCreateManyRecordMutation'; import { getCreateManyRecordsMutationResponseField } from '@/object-record/hooks/useGenerateCreateManyRecordMutation';
import { ObjectRecord } from '@/object-record/types/ObjectRecord'; import { ObjectRecord } from '@/object-record/types/ObjectRecord';
import { sanitizeRecordInput } from '@/object-record/utils/sanitizeRecordInput'; import { sanitizeRecordInput } from '@/object-record/utils/sanitizeRecordInput';
@ -13,29 +15,33 @@ export const useCreateManyRecords = <
>({ >({
objectNameSingular, objectNameSingular,
}: ObjectMetadataItemIdentifier) => { }: ObjectMetadataItemIdentifier) => {
const apolloClient = useApolloClient();
const { objectMetadataItem, createManyRecordsMutation } = const { objectMetadataItem, createManyRecordsMutation } =
useObjectMetadataItem({ useObjectMetadataItem({
objectNameSingular, objectNameSingular,
}); });
const { generateCachedObjectRecord } = useGenerateCachedObjectRecord({ const { generateObjectRecordOptimisticResponse } =
objectMetadataItem, useGenerateObjectRecordOptimisticResponse({
}); objectMetadataItem,
});
const apolloClient = useApolloClient(); const getRelationMetadata = useGetRelationMetadata();
const createManyRecords = async (data: Partial<CreatedObjectRecord>[]) => { const createManyRecords = async (data: Partial<CreatedObjectRecord>[]) => {
const optimisticallyCreatedRecords = data.map((record) => const sanitizedCreateManyRecordsInput = data.map((input) =>
generateCachedObjectRecord<CreatedObjectRecord>(record),
);
const sanitizedCreateManyRecordsInput = data.map((input, index) =>
sanitizeRecordInput({ sanitizeRecordInput({
objectMetadataItem, objectMetadataItem,
recordInput: { ...input, id: optimisticallyCreatedRecords[index].id }, recordInput: { ...input, id: v4() },
}), }),
); );
const optimisticallyCreatedRecords = sanitizedCreateManyRecordsInput.map(
(record) =>
generateObjectRecordOptimisticResponse<CreatedObjectRecord>(record),
);
const mutationResponseField = getCreateManyRecordsMutationResponseField( const mutationResponseField = getCreateManyRecordsMutationResponseField(
objectMetadataItem.namePlural, objectMetadataItem.namePlural,
); );
@ -57,6 +63,7 @@ export const useCreateManyRecords = <
cache, cache,
objectMetadataItem, objectMetadataItem,
records, records,
getRelationMetadata,
}); });
}, },
}); });

View File

@ -3,7 +3,7 @@ import { v4 } from 'uuid';
import { useObjectMetadataItem } from '@/object-metadata/hooks/useObjectMetadataItem'; import { useObjectMetadataItem } from '@/object-metadata/hooks/useObjectMetadataItem';
import { ObjectMetadataItemIdentifier } from '@/object-metadata/types/ObjectMetadataItemIdentifier'; import { ObjectMetadataItemIdentifier } from '@/object-metadata/types/ObjectMetadataItemIdentifier';
import { useAddRecordInCache } from '@/object-record/cache/hooks/useAddRecordInCache'; import { useAddRecordInCache } from '@/object-record/cache/hooks/useAddRecordInCache';
import { useGenerateCachedObjectRecord } from '@/object-record/cache/hooks/useGenerateCachedObjectRecord'; import { useGenerateObjectRecordOptimisticResponse } from '@/object-record/cache/hooks/useGenerateObjectRecordOptimisticResponse';
import { ObjectRecord } from '@/object-record/types/ObjectRecord'; import { ObjectRecord } from '@/object-record/types/ObjectRecord';
export const useCreateManyRecordsInCache = <T extends ObjectRecord>({ export const useCreateManyRecordsInCache = <T extends ObjectRecord>({
@ -13,9 +13,10 @@ export const useCreateManyRecordsInCache = <T extends ObjectRecord>({
objectNameSingular, objectNameSingular,
}); });
const { generateCachedObjectRecord } = useGenerateCachedObjectRecord({ const { generateObjectRecordOptimisticResponse } =
objectMetadataItem, useGenerateObjectRecordOptimisticResponse({
}); objectMetadataItem,
});
const addRecordInCache = useAddRecordInCache({ const addRecordInCache = useAddRecordInCache({
objectMetadataItem, objectMetadataItem,
@ -30,9 +31,8 @@ export const useCreateManyRecordsInCache = <T extends ObjectRecord>({
const createdRecordsInCache = [] as T[]; const createdRecordsInCache = [] as T[];
for (const record of recordsWithId) { for (const record of recordsWithId) {
const generatedCachedObjectRecord = generateCachedObjectRecord<T>({ const generatedCachedObjectRecord =
...record, generateObjectRecordOptimisticResponse<T>(record);
});
if (generatedCachedObjectRecord) { if (generatedCachedObjectRecord) {
addRecordInCache(generatedCachedObjectRecord); addRecordInCache(generatedCachedObjectRecord);

View File

@ -1,8 +1,10 @@
import { useApolloClient } from '@apollo/client'; import { useApolloClient } from '@apollo/client';
import { v4 } from 'uuid';
import { triggerCreateRecordsOptimisticEffect } from '@/apollo/optimistic-effect/utils/triggerCreateRecordsOptimisticEffect'; import { triggerCreateRecordsOptimisticEffect } from '@/apollo/optimistic-effect/utils/triggerCreateRecordsOptimisticEffect';
import { useGetRelationMetadata } from '@/object-metadata/hooks/useGetRelationMetadata';
import { useObjectMetadataItem } from '@/object-metadata/hooks/useObjectMetadataItem'; import { useObjectMetadataItem } from '@/object-metadata/hooks/useObjectMetadataItem';
import { useGenerateCachedObjectRecord } from '@/object-record/cache/hooks/useGenerateCachedObjectRecord'; import { useGenerateObjectRecordOptimisticResponse } from '@/object-record/cache/hooks/useGenerateObjectRecordOptimisticResponse';
import { getCreateOneRecordMutationResponseField } from '@/object-record/hooks/useGenerateCreateOneRecordMutation'; import { getCreateOneRecordMutationResponseField } from '@/object-record/hooks/useGenerateCreateOneRecordMutation';
import { ObjectRecord } from '@/object-record/types/ObjectRecord'; import { ObjectRecord } from '@/object-record/types/ObjectRecord';
import { sanitizeRecordInput } from '@/object-record/utils/sanitizeRecordInput'; import { sanitizeRecordInput } from '@/object-record/utils/sanitizeRecordInput';
@ -16,25 +18,27 @@ export const useCreateOneRecord = <
>({ >({
objectNameSingular, objectNameSingular,
}: useCreateOneRecordProps) => { }: useCreateOneRecordProps) => {
const apolloClient = useApolloClient();
const { objectMetadataItem, createOneRecordMutation } = useObjectMetadataItem( const { objectMetadataItem, createOneRecordMutation } = useObjectMetadataItem(
{ objectNameSingular }, { objectNameSingular },
); );
// TODO: type this with a minimal type at least with Record<string, any> const { generateObjectRecordOptimisticResponse } =
const apolloClient = useApolloClient(); useGenerateObjectRecordOptimisticResponse({
objectMetadataItem,
});
const { generateCachedObjectRecord } = useGenerateCachedObjectRecord({ const getRelationMetadata = useGetRelationMetadata();
objectMetadataItem,
});
const createOneRecord = async (input: Partial<CreatedObjectRecord>) => { const createOneRecord = async (input: Partial<CreatedObjectRecord>) => {
const sanitizedCreateOneRecordInput = sanitizeRecordInput({ const sanitizedCreateOneRecordInput = sanitizeRecordInput({
objectMetadataItem, objectMetadataItem,
recordInput: input, recordInput: { ...input, id: v4() },
}); });
const optimisticallyCreatedRecord = const optimisticallyCreatedRecord =
generateCachedObjectRecord<CreatedObjectRecord>({ generateObjectRecordOptimisticResponse<CreatedObjectRecord>({
...input, ...input,
...sanitizedCreateOneRecordInput, ...sanitizedCreateOneRecordInput,
}); });
@ -45,10 +49,7 @@ export const useCreateOneRecord = <
const createdObject = await apolloClient.mutate({ const createdObject = await apolloClient.mutate({
mutation: createOneRecordMutation, mutation: createOneRecordMutation,
variables: { variables: {
input: { input: sanitizedCreateOneRecordInput,
...sanitizedCreateOneRecordInput,
id: optimisticallyCreatedRecord.id,
},
}, },
optimisticResponse: { optimisticResponse: {
[mutationResponseField]: optimisticallyCreatedRecord, [mutationResponseField]: optimisticallyCreatedRecord,
@ -62,6 +63,7 @@ export const useCreateOneRecord = <
cache, cache,
objectMetadataItem, objectMetadataItem,
records: [record], records: [record],
getRelationMetadata,
}); });
}, },
}); });

View File

@ -1,6 +1,6 @@
import { useObjectMetadataItem } from '@/object-metadata/hooks/useObjectMetadataItem'; import { useObjectMetadataItem } from '@/object-metadata/hooks/useObjectMetadataItem';
import { useAddRecordInCache } from '@/object-record/cache/hooks/useAddRecordInCache'; import { useAddRecordInCache } from '@/object-record/cache/hooks/useAddRecordInCache';
import { useGenerateCachedObjectRecord } from '@/object-record/cache/hooks/useGenerateCachedObjectRecord'; import { useGenerateObjectRecordOptimisticResponse } from '@/object-record/cache/hooks/useGenerateObjectRecordOptimisticResponse';
import { ObjectRecord } from '@/object-record/types/ObjectRecord'; import { ObjectRecord } from '@/object-record/types/ObjectRecord';
type useCreateOneRecordInCacheProps = { type useCreateOneRecordInCacheProps = {
@ -14,19 +14,18 @@ export const useCreateOneRecordInCache = <T>({
objectNameSingular, objectNameSingular,
}); });
const { generateCachedObjectRecord } = useGenerateCachedObjectRecord({ const { generateObjectRecordOptimisticResponse } =
objectMetadataItem, useGenerateObjectRecordOptimisticResponse({
}); objectMetadataItem,
});
const addRecordInCache = useAddRecordInCache({ const addRecordInCache = useAddRecordInCache({
objectMetadataItem, objectMetadataItem,
}); });
const createOneRecordInCache = async (input: ObjectRecord) => { const createOneRecordInCache = async (input: ObjectRecord) => {
const generatedCachedObjectRecord = generateCachedObjectRecord({ const generatedCachedObjectRecord =
createdAt: new Date().toISOString(), generateObjectRecordOptimisticResponse(input);
...input,
});
addRecordInCache(generatedCachedObjectRecord); addRecordInCache(generatedCachedObjectRecord);

View File

@ -1,13 +1,9 @@
import { useApolloClient } from '@apollo/client'; import { useApolloClient } from '@apollo/client';
import { isObjectRecordConnection } from '@/apollo/optimistic-effect/utils/isObjectRecordConnection';
import { triggerDeleteRecordsOptimisticEffect } from '@/apollo/optimistic-effect/utils/triggerDeleteRecordsOptimisticEffect'; import { triggerDeleteRecordsOptimisticEffect } from '@/apollo/optimistic-effect/utils/triggerDeleteRecordsOptimisticEffect';
import { triggerDetachRelationOptimisticEffect } from '@/apollo/optimistic-effect/utils/triggerDetachRelationOptimisticEffect';
import { useGetRelationMetadata } from '@/object-metadata/hooks/useGetRelationMetadata'; import { useGetRelationMetadata } from '@/object-metadata/hooks/useGetRelationMetadata';
import { useObjectMetadataItem } from '@/object-metadata/hooks/useObjectMetadataItem'; import { useObjectMetadataItem } from '@/object-metadata/hooks/useObjectMetadataItem';
import { getDeleteManyRecordsMutationResponseField } from '@/object-record/hooks/useGenerateDeleteManyRecordMutation'; import { getDeleteManyRecordsMutationResponseField } from '@/object-record/hooks/useGenerateDeleteManyRecordMutation';
import { ObjectRecord } from '@/object-record/types/ObjectRecord';
import { ObjectRecordConnection } from '@/object-record/types/ObjectRecordConnection';
import { isDefined } from '~/utils/isDefined'; import { isDefined } from '~/utils/isDefined';
import { capitalize } from '~/utils/string/capitalize'; import { capitalize } from '~/utils/string/capitalize';
@ -19,13 +15,13 @@ type useDeleteOneRecordProps = {
export const useDeleteManyRecords = ({ export const useDeleteManyRecords = ({
objectNameSingular, objectNameSingular,
}: useDeleteOneRecordProps) => { }: useDeleteOneRecordProps) => {
const apolloClient = useApolloClient();
const { objectMetadataItem, deleteManyRecordsMutation, getRecordFromCache } = const { objectMetadataItem, deleteManyRecordsMutation, getRecordFromCache } =
useObjectMetadataItem({ objectNameSingular }); useObjectMetadataItem({ objectNameSingular });
const getRelationMetadata = useGetRelationMetadata(); const getRelationMetadata = useGetRelationMetadata();
const apolloClient = useApolloClient();
const mutationResponseField = getDeleteManyRecordsMutationResponseField( const mutationResponseField = getDeleteManyRecordsMutationResponseField(
objectMetadataItem.namePlural, objectMetadataItem.namePlural,
); );
@ -47,49 +43,15 @@ export const useDeleteManyRecords = ({
if (!records?.length) return; if (!records?.length) return;
objectMetadataItem.fields.forEach((fieldMetadataItem) => { const cachedRecords = records
const relationMetadata = getRelationMetadata({ fieldMetadataItem }); .map((record) => getRecordFromCache(record.id, cache))
.filter(isDefined);
if (!relationMetadata) return;
const { relationObjectMetadataItem, relationFieldMetadataItem } =
relationMetadata;
records.forEach((record) => {
const cachedRecord = getRecordFromCache(record.id, cache);
if (!cachedRecord) return;
const previousFieldValue:
| ObjectRecordConnection
| ObjectRecord
| null = cachedRecord[fieldMetadataItem.name];
const relationRecordIds = isObjectRecordConnection(
relationObjectMetadataItem.nameSingular,
previousFieldValue,
)
? previousFieldValue.edges.map(({ node }) => node.id)
: [previousFieldValue?.id].filter(isDefined);
relationRecordIds.forEach((relationRecordId) =>
triggerDetachRelationOptimisticEffect({
cache,
objectNameSingular,
recordId: record.id,
relationObjectMetadataNameSingular:
relationObjectMetadataItem.nameSingular,
relationFieldName: relationFieldMetadataItem.name,
relationRecordId,
}),
);
});
});
triggerDeleteRecordsOptimisticEffect({ triggerDeleteRecordsOptimisticEffect({
cache, cache,
objectMetadataItem, objectMetadataItem,
records: records, records: cachedRecords,
getRelationMetadata,
}); });
}, },
}); });

View File

@ -1,15 +1,10 @@
import { useCallback } from 'react'; import { useCallback } from 'react';
import { useApolloClient } from '@apollo/client'; import { useApolloClient } from '@apollo/client';
import { isObjectRecordConnection } from '@/apollo/optimistic-effect/utils/isObjectRecordConnection';
import { triggerDeleteRecordsOptimisticEffect } from '@/apollo/optimistic-effect/utils/triggerDeleteRecordsOptimisticEffect'; import { triggerDeleteRecordsOptimisticEffect } from '@/apollo/optimistic-effect/utils/triggerDeleteRecordsOptimisticEffect';
import { triggerDetachRelationOptimisticEffect } from '@/apollo/optimistic-effect/utils/triggerDetachRelationOptimisticEffect';
import { useGetRelationMetadata } from '@/object-metadata/hooks/useGetRelationMetadata'; import { useGetRelationMetadata } from '@/object-metadata/hooks/useGetRelationMetadata';
import { useObjectMetadataItem } from '@/object-metadata/hooks/useObjectMetadataItem'; import { useObjectMetadataItem } from '@/object-metadata/hooks/useObjectMetadataItem';
import { ObjectRecord } from '@/object-record/types/ObjectRecord';
import { ObjectRecordConnection } from '@/object-record/types/ObjectRecordConnection';
import { getDeleteOneRecordMutationResponseField } from '@/object-record/utils/generateDeleteOneRecordMutation'; import { getDeleteOneRecordMutationResponseField } from '@/object-record/utils/generateDeleteOneRecordMutation';
import { isDefined } from '~/utils/isDefined';
import { capitalize } from '~/utils/string/capitalize'; import { capitalize } from '~/utils/string/capitalize';
type useDeleteOneRecordProps = { type useDeleteOneRecordProps = {
@ -20,13 +15,13 @@ type useDeleteOneRecordProps = {
export const useDeleteOneRecord = ({ export const useDeleteOneRecord = ({
objectNameSingular, objectNameSingular,
}: useDeleteOneRecordProps) => { }: useDeleteOneRecordProps) => {
const apolloClient = useApolloClient();
const { objectMetadataItem, deleteOneRecordMutation, getRecordFromCache } = const { objectMetadataItem, deleteOneRecordMutation, getRecordFromCache } =
useObjectMetadataItem({ objectNameSingular }); useObjectMetadataItem({ objectNameSingular });
const getRelationMetadata = useGetRelationMetadata(); const getRelationMetadata = useGetRelationMetadata();
const apolloClient = useApolloClient();
const mutationResponseField = const mutationResponseField =
getDeleteOneRecordMutationResponseField(objectNameSingular); getDeleteOneRecordMutationResponseField(objectNameSingular);
@ -46,47 +41,15 @@ export const useDeleteOneRecord = ({
if (!record) return; if (!record) return;
objectMetadataItem.fields.forEach((fieldMetadataItem) => { const cachedRecord = getRecordFromCache(record.id, cache);
const relationMetadata = getRelationMetadata({ fieldMetadataItem });
if (!relationMetadata) return; if (!cachedRecord) return;
const { relationObjectMetadataItem, relationFieldMetadataItem } =
relationMetadata;
const cachedRecord = getRecordFromCache(record.id, cache);
if (!cachedRecord) return;
const previousFieldValue:
| ObjectRecordConnection
| ObjectRecord
| null = cachedRecord[fieldMetadataItem.name];
const relationRecordIds = isObjectRecordConnection(
relationObjectMetadataItem.nameSingular,
previousFieldValue,
)
? previousFieldValue.edges.map(({ node }) => node.id)
: [previousFieldValue?.id].filter(isDefined);
relationRecordIds.forEach((relationRecordId) =>
triggerDetachRelationOptimisticEffect({
cache,
objectNameSingular,
recordId: record.id,
relationObjectMetadataNameSingular:
relationObjectMetadataItem.nameSingular,
relationFieldName: relationFieldMetadataItem.name,
relationRecordId,
}),
);
});
triggerDeleteRecordsOptimisticEffect({ triggerDeleteRecordsOptimisticEffect({
cache, cache,
objectMetadataItem, objectMetadataItem,
records: [record], records: [cachedRecord],
getRelationMetadata,
}); });
}, },
}); });

View File

@ -1,16 +1,12 @@
import { useApolloClient } from '@apollo/client'; import { useApolloClient } from '@apollo/client';
import { isObjectRecordConnection } from '@/apollo/optimistic-effect/utils/isObjectRecordConnection';
import { triggerAttachRelationOptimisticEffect } from '@/apollo/optimistic-effect/utils/triggerAttachRelationOptimisticEffect';
import { triggerDetachRelationOptimisticEffect } from '@/apollo/optimistic-effect/utils/triggerDetachRelationOptimisticEffect';
import { triggerUpdateRecordOptimisticEffect } from '@/apollo/optimistic-effect/utils/triggerUpdateRecordOptimisticEffect'; import { triggerUpdateRecordOptimisticEffect } from '@/apollo/optimistic-effect/utils/triggerUpdateRecordOptimisticEffect';
import { useGetRelationMetadata } from '@/object-metadata/hooks/useGetRelationMetadata'; import { useGetRelationMetadata } from '@/object-metadata/hooks/useGetRelationMetadata';
import { useObjectMetadataItem } from '@/object-metadata/hooks/useObjectMetadataItem'; import { useObjectMetadataItem } from '@/object-metadata/hooks/useObjectMetadataItem';
import { useGenerateObjectRecordOptimisticResponse } from '@/object-record/cache/hooks/useGenerateObjectRecordOptimisticResponse';
import { getUpdateOneRecordMutationResponseField } from '@/object-record/hooks/useGenerateUpdateOneRecordMutation'; import { getUpdateOneRecordMutationResponseField } from '@/object-record/hooks/useGenerateUpdateOneRecordMutation';
import { ObjectRecord } from '@/object-record/types/ObjectRecord'; import { ObjectRecord } from '@/object-record/types/ObjectRecord';
import { sanitizeRecordInput } from '@/object-record/utils/sanitizeRecordInput'; import { sanitizeRecordInput } from '@/object-record/utils/sanitizeRecordInput';
import { isDeeplyEqual } from '~/utils/isDeeplyEqual';
import { capitalize } from '~/utils/string/capitalize';
type useUpdateOneRecordProps = { type useUpdateOneRecordProps = {
objectNameSingular: string; objectNameSingular: string;
@ -21,12 +17,17 @@ export const useUpdateOneRecord = <
>({ >({
objectNameSingular, objectNameSingular,
}: useUpdateOneRecordProps) => { }: useUpdateOneRecordProps) => {
const apolloClient = useApolloClient();
const { objectMetadataItem, updateOneRecordMutation, getRecordFromCache } = const { objectMetadataItem, updateOneRecordMutation, getRecordFromCache } =
useObjectMetadataItem({ objectNameSingular }); useObjectMetadataItem({ objectNameSingular });
const getRelationMetadata = useGetRelationMetadata(); const { generateObjectRecordOptimisticResponse } =
useGenerateObjectRecordOptimisticResponse({
objectMetadataItem,
});
const apolloClient = useApolloClient(); const getRelationMetadata = useGetRelationMetadata();
const updateOneRecord = async ({ const updateOneRecord = async ({
idToUpdate, idToUpdate,
@ -42,13 +43,11 @@ export const useUpdateOneRecord = <
recordInput: updateOneRecordInput, recordInput: updateOneRecordInput,
}); });
const optimisticallyUpdatedRecord = { const optimisticallyUpdatedRecord = generateObjectRecordOptimisticResponse({
...(cachedRecord ?? {}), ...(cachedRecord ?? {}),
...updateOneRecordInput,
...sanitizedUpdateOneRecordInput, ...sanitizedUpdateOneRecordInput,
__typename: capitalize(objectNameSingular),
id: idToUpdate, id: idToUpdate,
}; });
const mutationResponseField = const mutationResponseField =
getUpdateOneRecordMutationResponseField(objectNameSingular); getUpdateOneRecordMutationResponseField(objectNameSingular);
@ -65,60 +64,14 @@ export const useUpdateOneRecord = <
update: (cache, { data }) => { update: (cache, { data }) => {
const record = data?.[mutationResponseField]; const record = data?.[mutationResponseField];
if (!record) return; if (!record || !cachedRecord) return;
objectMetadataItem.fields.forEach((fieldMetadataItem) => {
const relationMetadata = getRelationMetadata({ fieldMetadataItem });
if (!relationMetadata) return;
const { relationObjectMetadataItem, relationFieldMetadataItem } =
relationMetadata;
const previousFieldValue = cachedRecord?.[fieldMetadataItem.name];
const nextFieldValue =
updateOneRecordInput[fieldMetadataItem.name] ?? null;
if (
!(fieldMetadataItem.name in updateOneRecordInput) ||
isObjectRecordConnection(
relationObjectMetadataItem.nameSingular,
previousFieldValue,
) ||
isDeeplyEqual(previousFieldValue, nextFieldValue)
) {
return;
}
if (previousFieldValue) {
triggerDetachRelationOptimisticEffect({
cache,
objectNameSingular,
recordId: record.id,
relationObjectMetadataNameSingular:
relationObjectMetadataItem.nameSingular,
relationFieldName: relationFieldMetadataItem.name,
relationRecordId: previousFieldValue.id,
});
}
if (nextFieldValue) {
triggerAttachRelationOptimisticEffect({
cache,
objectNameSingular,
recordId: record.id,
relationObjectMetadataNameSingular:
relationObjectMetadataItem.nameSingular,
relationFieldName: relationFieldMetadataItem.name,
relationRecordId: nextFieldValue.id,
});
}
});
triggerUpdateRecordOptimisticEffect({ triggerUpdateRecordOptimisticEffect({
cache, cache,
objectMetadataItem, objectMetadataItem,
record, previousRecord: cachedRecord,
nextRecord: record,
getRelationMetadata,
}); });
}, },
}); });

View File

@ -13,22 +13,22 @@ export const sanitizeRecordInput = ({
return Object.fromEntries( return Object.fromEntries(
Object.entries(recordInput) Object.entries(recordInput)
.map<[string, unknown] | undefined>(([fieldName, fieldValue]) => { .map<[string, unknown] | undefined>(([fieldName, fieldValue]) => {
const fieldDefinition = objectMetadataItem.fields.find( const fieldMetadataItem = objectMetadataItem.fields.find(
(field) => field.name === fieldName, (field) => field.name === fieldName,
); );
if (!fieldDefinition) return undefined; if (!fieldMetadataItem) return undefined;
if ( if (
fieldDefinition.type === FieldMetadataType.Relation && fieldMetadataItem.type === FieldMetadataType.Relation &&
isFieldRelationValue(fieldValue) isFieldRelationValue(fieldValue)
) { ) {
const relationIdFieldName = `${fieldDefinition.name}Id`; const relationIdFieldName = `${fieldMetadataItem.name}Id`;
const relationIdFieldDefinition = objectMetadataItem.fields.find( const relationIdFieldMetadataItem = objectMetadataItem.fields.find(
(field) => field.name === relationIdFieldName, (field) => field.name === relationIdFieldName,
); );
return relationIdFieldDefinition return relationIdFieldMetadataItem
? [relationIdFieldName, fieldValue?.id ?? null] ? [relationIdFieldName, fieldValue?.id ?? null]
: undefined; : undefined;
} }

View File

@ -34,7 +34,7 @@ export const currentPipelineId = 'f088c8c9-05d2-4276-b065-b863cc7d0b33';
const data = { const data = {
color: 'yellow', color: 'yellow',
id: 'columnId', id: mockId,
position: 1, position: 1,
name: 'Column Title', name: 'Column Title',
}; };