Feat/put target object identifier on use activities (#4682)
When writing to the normalized cache (record), it's crucial to use _refs for relationships to avoid many problems. Essentially, we only deal with level 0 and generate all fields to be comfortable with their defaults. When writing in queries (which should be very rare, the only cases are prefetch and the case of activities due to the nested query; I've reduced this to a single file for activities usePrepareFindManyActivitiesQuery 🙂), it's important to use queryFields to avoid bugs. I've implemented them on the side of query generation and record generation. When doing an updateOne / createOne, etc., it's necessary to distinguish between optimistic writing (which we actually want to do with _refs) and the server response without refs. This allows for a clean write in the optimistic cache without worrying about nesting (as the first point). To simplify the whole activities part, write to the normalized cache first. Then, base queries on it in an idempotent manner. This way, there's no need to worry about the current page or action. The normalized cache is up-to-date, so I update the queries. Same idea as for optimisticEffects, actually. Finally, I've triggered optimisticEffects rather than the manual update of many queries. --------- Co-authored-by: Lucas Bordeau <bordeau.lucas@gmail.com>
This commit is contained in:
@ -1,64 +0,0 @@
|
||||
import { useApolloClient } from '@apollo/client';
|
||||
import gql from 'graphql-tag';
|
||||
import { useRecoilCallback, useRecoilValue } from 'recoil';
|
||||
|
||||
import { objectMetadataItemsState } from '@/object-metadata/states/objectMetadataItemsState';
|
||||
import { ObjectMetadataItem } from '@/object-metadata/types/ObjectMetadataItem';
|
||||
import { mapObjectMetadataToGraphQLQuery } from '@/object-metadata/utils/mapObjectMetadataToGraphQLQuery';
|
||||
import { useInjectIntoFindOneRecordQueryCache } from '@/object-record/cache/hooks/useInjectIntoFindOneRecordQueryCache';
|
||||
import { recordStoreFamilyState } from '@/object-record/record-store/states/recordStoreFamilyState';
|
||||
import { ObjectRecord } from '@/object-record/types/ObjectRecord';
|
||||
import { capitalize } from '~/utils/string/capitalize';
|
||||
|
||||
export const useAddRecordInCache = ({
|
||||
objectMetadataItem,
|
||||
}: {
|
||||
objectMetadataItem: ObjectMetadataItem;
|
||||
}) => {
|
||||
const objectMetadataItems = useRecoilValue(objectMetadataItemsState);
|
||||
const apolloClient = useApolloClient();
|
||||
|
||||
const { injectIntoFindOneRecordQueryCache } =
|
||||
useInjectIntoFindOneRecordQueryCache({
|
||||
objectMetadataItem,
|
||||
});
|
||||
|
||||
return useRecoilCallback(
|
||||
({ set }) =>
|
||||
(record: ObjectRecord) => {
|
||||
const fragment = gql`
|
||||
fragment Create${capitalize(
|
||||
objectMetadataItem.nameSingular,
|
||||
)}InCache on ${capitalize(
|
||||
objectMetadataItem.nameSingular,
|
||||
)} ${mapObjectMetadataToGraphQLQuery({
|
||||
objectMetadataItems,
|
||||
objectMetadataItem,
|
||||
})}
|
||||
`;
|
||||
|
||||
const cachedObjectRecord = {
|
||||
__typename: `${capitalize(objectMetadataItem.nameSingular)}`,
|
||||
...record,
|
||||
};
|
||||
|
||||
apolloClient.writeFragment({
|
||||
id: `${capitalize(objectMetadataItem.nameSingular)}:${record.id}`,
|
||||
fragment,
|
||||
data: cachedObjectRecord,
|
||||
});
|
||||
|
||||
// TODO: should we keep this here ? Or should the caller of createOneRecordInCache/createManyRecordsInCache be responsible for this ?
|
||||
injectIntoFindOneRecordQueryCache(cachedObjectRecord);
|
||||
|
||||
// TODO: remove this once we get rid of entityFieldsFamilyState
|
||||
set(recordStoreFamilyState(record.id), record);
|
||||
},
|
||||
[
|
||||
objectMetadataItem,
|
||||
objectMetadataItems,
|
||||
apolloClient,
|
||||
injectIntoFindOneRecordQueryCache,
|
||||
],
|
||||
);
|
||||
};
|
||||
@ -1,50 +0,0 @@
|
||||
import { ObjectMetadataItem } from '@/object-metadata/types/ObjectMetadataItem';
|
||||
import { useReadFindManyRecordsQueryInCache } from '@/object-record/cache/hooks/useReadFindManyRecordsQueryInCache';
|
||||
import { useUpsertFindManyRecordsQueryInCache } from '@/object-record/cache/hooks/useUpsertFindManyRecordsQueryInCache';
|
||||
import { ObjectRecord } from '@/object-record/types/ObjectRecord';
|
||||
import { ObjectRecordQueryVariables } from '@/object-record/types/ObjectRecordQueryVariables';
|
||||
|
||||
export const useAppendToFindManyRecordsQueryInCache = ({
|
||||
objectMetadataItem,
|
||||
}: {
|
||||
objectMetadataItem: ObjectMetadataItem;
|
||||
}) => {
|
||||
const { readFindManyRecordsQueryInCache } =
|
||||
useReadFindManyRecordsQueryInCache({
|
||||
objectMetadataItem,
|
||||
});
|
||||
|
||||
const {
|
||||
upsertFindManyRecordsQueryInCache: overwriteFindManyRecordsQueryInCache,
|
||||
} = useUpsertFindManyRecordsQueryInCache({
|
||||
objectMetadataItem,
|
||||
});
|
||||
|
||||
const appendToFindManyRecordsQueryInCache = <
|
||||
T extends ObjectRecord = ObjectRecord,
|
||||
>({
|
||||
queryVariables,
|
||||
objectRecordsToAppend,
|
||||
}: {
|
||||
queryVariables: ObjectRecordQueryVariables;
|
||||
objectRecordsToAppend: T[];
|
||||
}) => {
|
||||
const existingObjectRecords = readFindManyRecordsQueryInCache({
|
||||
queryVariables,
|
||||
});
|
||||
|
||||
const newObjectRecordList = [
|
||||
...existingObjectRecords,
|
||||
...objectRecordsToAppend,
|
||||
];
|
||||
|
||||
overwriteFindManyRecordsQueryInCache({
|
||||
objectRecordsToOverwrite: newObjectRecordList,
|
||||
queryVariables,
|
||||
});
|
||||
};
|
||||
|
||||
return {
|
||||
appendToFindManyRecordsQueryInCache,
|
||||
};
|
||||
};
|
||||
42
packages/twenty-front/src/modules/object-record/cache/hooks/useCreateManyRecordsInCache.ts
vendored
Normal file
42
packages/twenty-front/src/modules/object-record/cache/hooks/useCreateManyRecordsInCache.ts
vendored
Normal file
@ -0,0 +1,42 @@
|
||||
import { useObjectMetadataItem } from '@/object-metadata/hooks/useObjectMetadataItem';
|
||||
import { ObjectMetadataItemIdentifier } from '@/object-metadata/types/ObjectMetadataItemIdentifier';
|
||||
import { useCreateOneRecordInCache } from '@/object-record/cache/hooks/useCreateOneRecordInCache';
|
||||
import { ObjectRecord } from '@/object-record/types/ObjectRecord';
|
||||
import { prefillRecord } from '@/object-record/utils/prefillRecord';
|
||||
import { isDefined } from '~/utils/isDefined';
|
||||
|
||||
export const useCreateManyRecordsInCache = <T extends ObjectRecord>({
|
||||
objectNameSingular,
|
||||
}: ObjectMetadataItemIdentifier) => {
|
||||
const { objectMetadataItem } = useObjectMetadataItem({
|
||||
objectNameSingular,
|
||||
});
|
||||
|
||||
const createOneRecordInCache = useCreateOneRecordInCache({
|
||||
objectMetadataItem,
|
||||
});
|
||||
|
||||
const createManyRecordsInCache = (recordsToCreate: Partial<T>[]) => {
|
||||
const recordsWithId = recordsToCreate
|
||||
.map((record) => {
|
||||
return prefillRecord<T>({
|
||||
input: record,
|
||||
objectMetadataItem,
|
||||
});
|
||||
})
|
||||
.filter(isDefined);
|
||||
|
||||
const createdRecordsInCache = [] as T[];
|
||||
|
||||
for (const record of recordsWithId) {
|
||||
if (isDefined(record)) {
|
||||
createOneRecordInCache(record);
|
||||
createdRecordsInCache.push(record);
|
||||
}
|
||||
}
|
||||
|
||||
return createdRecordsInCache;
|
||||
};
|
||||
|
||||
return { createManyRecordsInCache };
|
||||
};
|
||||
62
packages/twenty-front/src/modules/object-record/cache/hooks/useCreateOneRecordInCache.ts
vendored
Normal file
62
packages/twenty-front/src/modules/object-record/cache/hooks/useCreateOneRecordInCache.ts
vendored
Normal file
@ -0,0 +1,62 @@
|
||||
import { useApolloClient } from '@apollo/client';
|
||||
import gql from 'graphql-tag';
|
||||
import { useRecoilValue } from 'recoil';
|
||||
|
||||
import { objectMetadataItemsState } from '@/object-metadata/states/objectMetadataItemsState';
|
||||
import { ObjectMetadataItem } from '@/object-metadata/types/ObjectMetadataItem';
|
||||
import { mapObjectMetadataToGraphQLQuery } from '@/object-metadata/utils/mapObjectMetadataToGraphQLQuery';
|
||||
import { useGetRecordFromCache } from '@/object-record/cache/hooks/useGetRecordFromCache';
|
||||
import { getRecordNodeFromRecord } from '@/object-record/cache/utils/getRecordNodeFromRecord';
|
||||
import { ObjectRecord } from '@/object-record/types/ObjectRecord';
|
||||
import { prefillRecord } from '@/object-record/utils/prefillRecord';
|
||||
import { capitalize } from '~/utils/string/capitalize';
|
||||
|
||||
export const useCreateOneRecordInCache = <T extends ObjectRecord>({
|
||||
objectMetadataItem,
|
||||
}: {
|
||||
objectMetadataItem: ObjectMetadataItem;
|
||||
}) => {
|
||||
const getRecordFromCache = useGetRecordFromCache({
|
||||
objectMetadataItem,
|
||||
});
|
||||
const objectMetadataItems = useRecoilValue(objectMetadataItemsState);
|
||||
const apolloClient = useApolloClient();
|
||||
|
||||
return (record: ObjectRecord) => {
|
||||
const fragment = gql`
|
||||
fragment Create${capitalize(
|
||||
objectMetadataItem.nameSingular,
|
||||
)}InCache on ${capitalize(
|
||||
objectMetadataItem.nameSingular,
|
||||
)} ${mapObjectMetadataToGraphQLQuery({
|
||||
objectMetadataItems,
|
||||
objectMetadataItem,
|
||||
computeReferences: true,
|
||||
})}
|
||||
`;
|
||||
|
||||
const prefilledRecord = prefillRecord({
|
||||
objectMetadataItem,
|
||||
input: record,
|
||||
depth: 1,
|
||||
});
|
||||
|
||||
const recordToCreateWithNestedConnections = getRecordNodeFromRecord({
|
||||
record: prefilledRecord,
|
||||
objectMetadataItem,
|
||||
objectMetadataItems,
|
||||
});
|
||||
|
||||
const cachedObjectRecord = {
|
||||
__typename: `${capitalize(objectMetadataItem.nameSingular)}`,
|
||||
...recordToCreateWithNestedConnections,
|
||||
};
|
||||
|
||||
apolloClient.writeFragment({
|
||||
id: `${capitalize(objectMetadataItem.nameSingular)}:${record.id}`,
|
||||
fragment,
|
||||
data: cachedObjectRecord,
|
||||
});
|
||||
return getRecordFromCache(record.id) as T;
|
||||
};
|
||||
};
|
||||
29
packages/twenty-front/src/modules/object-record/cache/hooks/useDeleteRecordFromCache.ts
vendored
Normal file
29
packages/twenty-front/src/modules/object-record/cache/hooks/useDeleteRecordFromCache.ts
vendored
Normal file
@ -0,0 +1,29 @@
|
||||
import { useApolloClient } from '@apollo/client';
|
||||
|
||||
import { useObjectMetadataItemOnly } from '@/object-metadata/hooks/useObjectMetadataItemOnly';
|
||||
import { useObjectMetadataItems } from '@/object-metadata/hooks/useObjectMetadataItems';
|
||||
import { deleteRecordFromCache } from '@/object-record/cache/utils/deleteRecordFromCache';
|
||||
import { ObjectRecord } from '@/object-record/types/ObjectRecord';
|
||||
|
||||
export const useDeleteRecordFromCache = ({
|
||||
objectNameSingular,
|
||||
}: {
|
||||
objectNameSingular: string;
|
||||
}) => {
|
||||
const apolloClient = useApolloClient();
|
||||
|
||||
const { objectMetadataItem } = useObjectMetadataItemOnly({
|
||||
objectNameSingular,
|
||||
});
|
||||
|
||||
const { objectMetadataItems } = useObjectMetadataItems();
|
||||
|
||||
return (recordToDelete: ObjectRecord) => {
|
||||
deleteRecordFromCache({
|
||||
objectMetadataItem,
|
||||
objectMetadataItems,
|
||||
recordToDelete,
|
||||
cache: apolloClient.cache,
|
||||
});
|
||||
};
|
||||
};
|
||||
@ -1,79 +0,0 @@
|
||||
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;
|
||||
|
||||
const relationRecord = input[fieldMetadataItem.name] as
|
||||
| ObjectRecord
|
||||
| undefined;
|
||||
|
||||
return {
|
||||
...result,
|
||||
[fieldMetadataItem.name]: relationRecordId
|
||||
? {
|
||||
__typename: relationRecordTypeName,
|
||||
id: relationRecordId,
|
||||
// TODO: there are too many bugs if we don't include the entire relation record
|
||||
// See if we can find a way to work only with the id and typename
|
||||
...relationRecord,
|
||||
}
|
||||
: null,
|
||||
};
|
||||
},
|
||||
input,
|
||||
);
|
||||
|
||||
return {
|
||||
__typename: capitalize(objectMetadataItem.nameSingular),
|
||||
...recordSchema.parse({
|
||||
id: v4(),
|
||||
createdAt: new Date().toISOString(),
|
||||
...inputWithRelationFields,
|
||||
}),
|
||||
} as GeneratedObjectRecord & { __typename: string };
|
||||
};
|
||||
|
||||
return {
|
||||
generateObjectRecordOptimisticResponse,
|
||||
};
|
||||
};
|
||||
@ -1,13 +1,11 @@
|
||||
import { useCallback } from 'react';
|
||||
import { gql, useApolloClient } from '@apollo/client';
|
||||
import { useApolloClient } from '@apollo/client';
|
||||
import { useRecoilValue } from 'recoil';
|
||||
|
||||
import { objectMetadataItemsState } from '@/object-metadata/states/objectMetadataItemsState';
|
||||
import { ObjectMetadataItem } from '@/object-metadata/types/ObjectMetadataItem';
|
||||
import { mapObjectMetadataToGraphQLQuery } from '@/object-metadata/utils/mapObjectMetadataToGraphQLQuery';
|
||||
import { getRecordFromCache } from '@/object-record/cache/utils/getRecordFromCache';
|
||||
import { ObjectRecord } from '@/object-record/types/ObjectRecord';
|
||||
import { isUndefinedOrNull } from '~/utils/isUndefinedOrNull';
|
||||
import { capitalize } from '~/utils/string/capitalize';
|
||||
|
||||
export const useGetRecordFromCache = ({
|
||||
objectMetadataItem,
|
||||
@ -23,29 +21,11 @@ export const useGetRecordFromCache = ({
|
||||
recordId: string,
|
||||
cache = apolloClient.cache,
|
||||
) => {
|
||||
if (isUndefinedOrNull(objectMetadataItem)) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const capitalizedObjectName = capitalize(objectMetadataItem.nameSingular);
|
||||
|
||||
const cacheReadFragment = gql`
|
||||
fragment ${capitalizedObjectName}Fragment on ${capitalizedObjectName} ${mapObjectMetadataToGraphQLQuery(
|
||||
{
|
||||
objectMetadataItems,
|
||||
objectMetadataItem,
|
||||
},
|
||||
)}
|
||||
`;
|
||||
|
||||
const cachedRecordId = cache.identify({
|
||||
__typename: capitalize(objectMetadataItem.nameSingular),
|
||||
id: recordId,
|
||||
});
|
||||
|
||||
return cache.readFragment<CachedObjectRecord & { __typename: string }>({
|
||||
id: cachedRecordId,
|
||||
fragment: cacheReadFragment,
|
||||
return getRecordFromCache<CachedObjectRecord>({
|
||||
cache,
|
||||
recordId,
|
||||
objectMetadataItems,
|
||||
objectMetadataItem,
|
||||
});
|
||||
},
|
||||
[objectMetadataItem, objectMetadataItems, apolloClient],
|
||||
|
||||
@ -1,44 +0,0 @@
|
||||
import { useApolloClient } from '@apollo/client';
|
||||
|
||||
import { ObjectMetadataItem } from '@/object-metadata/types/ObjectMetadataItem';
|
||||
import { useGenerateFindOneRecordQuery } from '@/object-record/hooks/useGenerateFindOneRecordQuery';
|
||||
import { ObjectRecord } from '@/object-record/types/ObjectRecord';
|
||||
import { capitalize } from '~/utils/string/capitalize';
|
||||
|
||||
export const useInjectIntoFindOneRecordQueryCache = ({
|
||||
objectMetadataItem,
|
||||
}: {
|
||||
objectMetadataItem: ObjectMetadataItem;
|
||||
}) => {
|
||||
const apolloClient = useApolloClient();
|
||||
|
||||
const generateFindOneRecordQuery = useGenerateFindOneRecordQuery();
|
||||
|
||||
const injectIntoFindOneRecordQueryCache = <
|
||||
T extends ObjectRecord = ObjectRecord,
|
||||
>(
|
||||
record: T,
|
||||
) => {
|
||||
const findOneRecordQueryForCacheInjection = generateFindOneRecordQuery({
|
||||
objectMetadataItem,
|
||||
depth: 1,
|
||||
});
|
||||
|
||||
apolloClient.writeQuery({
|
||||
query: findOneRecordQueryForCacheInjection,
|
||||
variables: {
|
||||
objectRecordId: record.id,
|
||||
},
|
||||
data: {
|
||||
[objectMetadataItem.nameSingular]: {
|
||||
__typename: `${capitalize(objectMetadataItem.nameSingular)}`,
|
||||
...record,
|
||||
},
|
||||
},
|
||||
});
|
||||
};
|
||||
|
||||
return {
|
||||
injectIntoFindOneRecordQueryCache,
|
||||
};
|
||||
};
|
||||
@ -1,32 +0,0 @@
|
||||
import { useApolloClient } from '@apollo/client';
|
||||
import { Modifiers } from '@apollo/client/cache';
|
||||
|
||||
import { ObjectMetadataItem } from '@/object-metadata/types/ObjectMetadataItem';
|
||||
import { ObjectRecord } from '@/object-record/types/ObjectRecord';
|
||||
import { isUndefinedOrNull } from '~/utils/isUndefinedOrNull';
|
||||
import { capitalize } from '~/utils/string/capitalize';
|
||||
|
||||
export const useModifyRecordFromCache = ({
|
||||
objectMetadataItem,
|
||||
}: {
|
||||
objectMetadataItem: ObjectMetadataItem;
|
||||
}) => {
|
||||
const { cache } = useApolloClient();
|
||||
|
||||
return <CachedObjectRecord extends ObjectRecord = ObjectRecord>(
|
||||
recordId: string,
|
||||
fieldModifiers: Modifiers<CachedObjectRecord>,
|
||||
) => {
|
||||
if (isUndefinedOrNull(objectMetadataItem)) return;
|
||||
|
||||
const cachedRecordId = cache.identify({
|
||||
__typename: capitalize(objectMetadataItem.nameSingular),
|
||||
id: recordId,
|
||||
});
|
||||
|
||||
cache.modify<CachedObjectRecord>({
|
||||
id: cachedRecordId,
|
||||
fields: fieldModifiers,
|
||||
});
|
||||
};
|
||||
};
|
||||
@ -21,11 +21,17 @@ export const useReadFindManyRecordsQueryInCache = ({
|
||||
T extends ObjectRecord = ObjectRecord,
|
||||
>({
|
||||
queryVariables,
|
||||
queryFields,
|
||||
depth,
|
||||
}: {
|
||||
queryVariables: ObjectRecordQueryVariables;
|
||||
queryFields?: Record<string, any>;
|
||||
depth?: number;
|
||||
}) => {
|
||||
const findManyRecordsQueryForCacheRead = generateFindManyRecordsQuery({
|
||||
objectMetadataItem,
|
||||
queryFields,
|
||||
depth,
|
||||
});
|
||||
|
||||
const existingRecordsQueryResult = apolloClient.readQuery<
|
||||
|
||||
@ -1,5 +1,7 @@
|
||||
import { useApolloClient } from '@apollo/client';
|
||||
import { useRecoilValue } from 'recoil';
|
||||
|
||||
import { objectMetadataItemsState } from '@/object-metadata/states/objectMetadataItemsState';
|
||||
import { ObjectMetadataItem } from '@/object-metadata/types/ObjectMetadataItem';
|
||||
import { MAX_QUERY_DEPTH_FOR_CACHE_INJECTION } from '@/object-record/cache/constants/MaxQueryDepthForCacheInjection';
|
||||
import { getRecordConnectionFromRecords } from '@/object-record/cache/utils/getRecordConnectionFromRecords';
|
||||
@ -18,6 +20,7 @@ export const useUpsertFindManyRecordsQueryInCache = ({
|
||||
const apolloClient = useApolloClient();
|
||||
|
||||
const generateFindManyRecordsQuery = useGenerateFindManyRecordsQuery();
|
||||
const objectMetadataItems = useRecoilValue(objectMetadataItemsState);
|
||||
|
||||
const upsertFindManyRecordsQueryInCache = <
|
||||
T extends ObjectRecord = ObjectRecord,
|
||||
@ -25,19 +28,28 @@ export const useUpsertFindManyRecordsQueryInCache = ({
|
||||
queryVariables,
|
||||
depth = MAX_QUERY_DEPTH_FOR_CACHE_INJECTION,
|
||||
objectRecordsToOverwrite,
|
||||
queryFields,
|
||||
computeReferences = false,
|
||||
}: {
|
||||
queryVariables: ObjectRecordQueryVariables;
|
||||
depth?: number;
|
||||
objectRecordsToOverwrite: T[];
|
||||
queryFields?: Record<string, any>;
|
||||
computeReferences?: boolean;
|
||||
}) => {
|
||||
const findManyRecordsQueryForCacheOverwrite = generateFindManyRecordsQuery({
|
||||
objectMetadataItem,
|
||||
depth, // TODO: fix this
|
||||
depth,
|
||||
queryFields,
|
||||
computeReferences,
|
||||
});
|
||||
|
||||
const newObjectRecordConnection = getRecordConnectionFromRecords({
|
||||
objectNameSingular: objectMetadataItem.nameSingular,
|
||||
objectMetadataItems: objectMetadataItems,
|
||||
objectMetadataItem: objectMetadataItem,
|
||||
records: objectRecordsToOverwrite,
|
||||
queryFields,
|
||||
computeReferences,
|
||||
});
|
||||
|
||||
apolloClient.writeQuery({
|
||||
|
||||
30
packages/twenty-front/src/modules/object-record/cache/utils/deleteRecordFromCache.ts
vendored
Normal file
30
packages/twenty-front/src/modules/object-record/cache/utils/deleteRecordFromCache.ts
vendored
Normal file
@ -0,0 +1,30 @@
|
||||
import { ApolloCache } from '@apollo/client';
|
||||
|
||||
import { triggerDeleteRecordsOptimisticEffect } from '@/apollo/optimistic-effect/utils/triggerDeleteRecordsOptimisticEffect';
|
||||
import { ObjectMetadataItem } from '@/object-metadata/types/ObjectMetadataItem';
|
||||
import { getObjectTypename } from '@/object-record/cache/utils/getObjectTypename';
|
||||
import { ObjectRecord } from '@/object-record/types/ObjectRecord';
|
||||
|
||||
export const deleteRecordFromCache = ({
|
||||
objectMetadataItem,
|
||||
objectMetadataItems,
|
||||
recordToDelete,
|
||||
cache,
|
||||
}: {
|
||||
objectMetadataItem: ObjectMetadataItem;
|
||||
objectMetadataItems: ObjectMetadataItem[];
|
||||
recordToDelete: ObjectRecord;
|
||||
cache: ApolloCache<object>;
|
||||
}) => {
|
||||
triggerDeleteRecordsOptimisticEffect({
|
||||
cache,
|
||||
objectMetadataItem,
|
||||
objectMetadataItems,
|
||||
recordsToDelete: [
|
||||
{
|
||||
...recordToDelete,
|
||||
__typename: getObjectTypename(objectMetadataItem.nameSingular),
|
||||
},
|
||||
],
|
||||
});
|
||||
};
|
||||
@ -1,31 +0,0 @@
|
||||
import { ApolloClient, makeReference, Reference } from '@apollo/client';
|
||||
|
||||
import { getCachedRecordFromRecord } from '@/object-record/cache/utils/getCachedRecordFromRecord';
|
||||
import { ObjectRecord } from '@/object-record/types/ObjectRecord';
|
||||
|
||||
export const getCacheReferenceFromRecord = <T extends ObjectRecord>({
|
||||
apolloClient,
|
||||
objectNameSingular,
|
||||
record,
|
||||
}: {
|
||||
apolloClient: ApolloClient<object>;
|
||||
objectNameSingular: string;
|
||||
record: T;
|
||||
}): Reference => {
|
||||
const cachedRecord = getCachedRecordFromRecord({
|
||||
objectNameSingular,
|
||||
record,
|
||||
});
|
||||
|
||||
const id = apolloClient.cache.identify(cachedRecord);
|
||||
|
||||
if (!id) {
|
||||
throw new Error(
|
||||
`Could not identify record "${objectNameSingular}", id : "${record.id}"`,
|
||||
);
|
||||
}
|
||||
|
||||
const recordReference = makeReference(id);
|
||||
|
||||
return recordReference;
|
||||
};
|
||||
@ -1,43 +0,0 @@
|
||||
import { ApolloClient, makeReference } from '@apollo/client';
|
||||
|
||||
import { CachedObjectRecordEdge } from '@/apollo/types/CachedObjectRecordEdge';
|
||||
import { getCachedRecordFromRecord } from '@/object-record/cache/utils/getCachedRecordFromRecord';
|
||||
import { getEdgeTypename } from '@/object-record/cache/utils/getEdgeTypename';
|
||||
import { ObjectRecord } from '@/object-record/types/ObjectRecord';
|
||||
|
||||
export const getCachedRecordEdgesFromRecords = <T extends ObjectRecord>({
|
||||
apolloClient,
|
||||
objectNameSingular,
|
||||
records,
|
||||
}: {
|
||||
apolloClient: ApolloClient<object>;
|
||||
objectNameSingular: string;
|
||||
records: T[];
|
||||
}): CachedObjectRecordEdge[] => {
|
||||
const cachedRecordEdges = records.map((record) => {
|
||||
const cachedRecord = getCachedRecordFromRecord({
|
||||
objectNameSingular,
|
||||
record,
|
||||
});
|
||||
|
||||
const id = apolloClient.cache.identify(cachedRecord);
|
||||
|
||||
if (!id) {
|
||||
throw new Error(
|
||||
`Could not identify record "${objectNameSingular}", id : "${record.id}"`,
|
||||
);
|
||||
}
|
||||
|
||||
const reference = makeReference(id);
|
||||
|
||||
const cachedObjectRecordEdge: CachedObjectRecordEdge = {
|
||||
cursor: '',
|
||||
node: reference,
|
||||
__typename: getEdgeTypename({ objectNameSingular }),
|
||||
};
|
||||
|
||||
return cachedObjectRecordEdge;
|
||||
});
|
||||
|
||||
return cachedRecordEdges;
|
||||
};
|
||||
@ -1,16 +0,0 @@
|
||||
import { CachedObjectRecord } from '@/apollo/types/CachedObjectRecord';
|
||||
import { getNodeTypename } from '@/object-record/cache/utils/getNodeTypename';
|
||||
import { ObjectRecord } from '@/object-record/types/ObjectRecord';
|
||||
|
||||
export const getCachedRecordFromRecord = <T extends ObjectRecord>({
|
||||
objectNameSingular,
|
||||
record,
|
||||
}: {
|
||||
objectNameSingular: string;
|
||||
record: T;
|
||||
}): CachedObjectRecord<T> => {
|
||||
return {
|
||||
__typename: getNodeTypename({ objectNameSingular }),
|
||||
...record,
|
||||
};
|
||||
};
|
||||
@ -1,9 +1,6 @@
|
||||
import { getObjectTypename } from '@/object-record/cache/utils/getObjectTypename';
|
||||
import { capitalize } from '~/utils/string/capitalize';
|
||||
|
||||
export const getConnectionTypename = ({
|
||||
objectNameSingular,
|
||||
}: {
|
||||
objectNameSingular: string;
|
||||
}) => {
|
||||
return `${capitalize(objectNameSingular)}Connection`;
|
||||
export const getConnectionTypename = (objectNameSingular: string) => {
|
||||
return `${capitalize(getObjectTypename(objectNameSingular))}Connection`;
|
||||
};
|
||||
|
||||
@ -1,9 +1,6 @@
|
||||
import { getObjectTypename } from '@/object-record/cache/utils/getObjectTypename';
|
||||
import { capitalize } from '~/utils/string/capitalize';
|
||||
|
||||
export const getEdgeTypename = ({
|
||||
objectNameSingular,
|
||||
}: {
|
||||
objectNameSingular: string;
|
||||
}) => {
|
||||
return `${capitalize(objectNameSingular)}Edge`;
|
||||
export const getEdgeTypename = (objectNameSingular: string) => {
|
||||
return `${capitalize(getObjectTypename(objectNameSingular))}Edge`;
|
||||
};
|
||||
|
||||
@ -1,9 +1,6 @@
|
||||
import { getObjectTypename } from '@/object-record/cache/utils/getObjectTypename';
|
||||
import { capitalize } from '~/utils/string/capitalize';
|
||||
|
||||
export const getNodeTypename = ({
|
||||
objectNameSingular,
|
||||
}: {
|
||||
objectNameSingular: string;
|
||||
}) => {
|
||||
return capitalize(objectNameSingular);
|
||||
export const getNodeTypename = (objectNameSingular: string) => {
|
||||
return capitalize(getObjectTypename(objectNameSingular));
|
||||
};
|
||||
|
||||
5
packages/twenty-front/src/modules/object-record/cache/utils/getObjectTypename.ts
vendored
Normal file
5
packages/twenty-front/src/modules/object-record/cache/utils/getObjectTypename.ts
vendored
Normal file
@ -0,0 +1,5 @@
|
||||
import { capitalize } from '~/utils/string/capitalize';
|
||||
|
||||
export const getObjectTypename = (objectNameSingular: string) => {
|
||||
return capitalize(objectNameSingular);
|
||||
};
|
||||
@ -1,19 +0,0 @@
|
||||
import { getConnectionTypename } from '@/object-record/cache/utils/getConnectionTypename';
|
||||
import { getEmptyPageInfo } from '@/object-record/cache/utils/getEmptyPageInfo';
|
||||
import { ObjectRecord } from '@/object-record/types/ObjectRecord';
|
||||
import { ObjectRecordConnection } from '@/object-record/types/ObjectRecordConnection';
|
||||
import { ObjectRecordEdge } from '@/object-record/types/ObjectRecordEdge';
|
||||
|
||||
export const getRecordConnectionFromEdges = <T extends ObjectRecord>({
|
||||
objectNameSingular,
|
||||
edges,
|
||||
}: {
|
||||
objectNameSingular: string;
|
||||
edges: ObjectRecordEdge<T>[];
|
||||
}) => {
|
||||
return {
|
||||
__typename: getConnectionTypename({ objectNameSingular }),
|
||||
edges: edges,
|
||||
pageInfo: getEmptyPageInfo(),
|
||||
} as ObjectRecordConnection<T>;
|
||||
};
|
||||
@ -1,3 +1,4 @@
|
||||
import { ObjectMetadataItem } from '@/object-metadata/types/ObjectMetadataItem';
|
||||
import { getConnectionTypename } from '@/object-record/cache/utils/getConnectionTypename';
|
||||
import { getEmptyPageInfo } from '@/object-record/cache/utils/getEmptyPageInfo';
|
||||
import { getRecordEdgeFromRecord } from '@/object-record/cache/utils/getRecordEdgeFromRecord';
|
||||
@ -5,21 +6,41 @@ import { ObjectRecord } from '@/object-record/types/ObjectRecord';
|
||||
import { ObjectRecordConnection } from '@/object-record/types/ObjectRecordConnection';
|
||||
|
||||
export const getRecordConnectionFromRecords = <T extends ObjectRecord>({
|
||||
objectNameSingular,
|
||||
objectMetadataItems,
|
||||
objectMetadataItem,
|
||||
records,
|
||||
queryFields,
|
||||
withPageInfo = true,
|
||||
computeReferences = false,
|
||||
isRootLevel = true,
|
||||
depth = 1,
|
||||
}: {
|
||||
objectNameSingular: string;
|
||||
objectMetadataItems: ObjectMetadataItem[];
|
||||
objectMetadataItem: Pick<
|
||||
ObjectMetadataItem,
|
||||
'fields' | 'namePlural' | 'nameSingular'
|
||||
>;
|
||||
records: T[];
|
||||
queryFields?: Record<string, any>;
|
||||
withPageInfo?: boolean;
|
||||
isRootLevel?: boolean;
|
||||
computeReferences?: boolean;
|
||||
depth?: number;
|
||||
}) => {
|
||||
return {
|
||||
__typename: getConnectionTypename({ objectNameSingular }),
|
||||
__typename: getConnectionTypename(objectMetadataItem.nameSingular),
|
||||
edges: records.map((record) => {
|
||||
return getRecordEdgeFromRecord({
|
||||
objectNameSingular,
|
||||
objectMetadataItems,
|
||||
objectMetadataItem,
|
||||
queryFields,
|
||||
record,
|
||||
isRootLevel,
|
||||
computeReferences,
|
||||
depth,
|
||||
});
|
||||
}),
|
||||
pageInfo: getEmptyPageInfo(),
|
||||
totalCount: records.length,
|
||||
...(withPageInfo && { pageInfo: getEmptyPageInfo() }),
|
||||
...(withPageInfo && { totalCount: records.length }),
|
||||
} as ObjectRecordConnection<T>;
|
||||
};
|
||||
|
||||
@ -1,37 +1,40 @@
|
||||
import { ObjectMetadataItem } from '@/object-metadata/types/ObjectMetadataItem';
|
||||
import { getEdgeTypename } from '@/object-record/cache/utils/getEdgeTypename';
|
||||
import { getNodeTypename } from '@/object-record/cache/utils/getNodeTypename';
|
||||
import { getRecordConnectionFromRecords } from '@/object-record/cache/utils/getRecordConnectionFromRecords';
|
||||
import { getRecordNodeFromRecord } from '@/object-record/cache/utils/getRecordNodeFromRecord';
|
||||
import { ObjectRecord } from '@/object-record/types/ObjectRecord';
|
||||
import { ObjectRecordEdge } from '@/object-record/types/ObjectRecordEdge';
|
||||
|
||||
export const getRecordEdgeFromRecord = <T extends ObjectRecord>({
|
||||
objectNameSingular,
|
||||
objectMetadataItems,
|
||||
objectMetadataItem,
|
||||
queryFields,
|
||||
record,
|
||||
computeReferences = false,
|
||||
isRootLevel = false,
|
||||
}: {
|
||||
objectNameSingular: string;
|
||||
objectMetadataItems: ObjectMetadataItem[];
|
||||
objectMetadataItem: Pick<
|
||||
ObjectMetadataItem,
|
||||
'fields' | 'namePlural' | 'nameSingular'
|
||||
>;
|
||||
queryFields?: Record<string, any>;
|
||||
computeReferences?: boolean;
|
||||
isRootLevel?: boolean;
|
||||
depth?: number;
|
||||
record: T;
|
||||
}) => {
|
||||
const nestedRecord = Object.fromEntries(
|
||||
Object.entries(record).map(([key, value]) => {
|
||||
if (Array.isArray(value)) {
|
||||
return [
|
||||
key,
|
||||
getRecordConnectionFromRecords({
|
||||
// Todo: this is a ugly and broken hack to get the singular, we need to infer this from metadata
|
||||
objectNameSingular: key.slice(0, -1),
|
||||
records: value as ObjectRecord[],
|
||||
}),
|
||||
];
|
||||
}
|
||||
return [key, value];
|
||||
}),
|
||||
) as T; // Todo fix typing once we have investigated apollo edges / nodes removal
|
||||
|
||||
return {
|
||||
__typename: getEdgeTypename({ objectNameSingular }),
|
||||
__typename: getEdgeTypename(objectMetadataItem.nameSingular),
|
||||
node: {
|
||||
__typename: getNodeTypename({ objectNameSingular }),
|
||||
...nestedRecord,
|
||||
...getRecordNodeFromRecord({
|
||||
objectMetadataItems,
|
||||
objectMetadataItem,
|
||||
queryFields,
|
||||
record,
|
||||
computeReferences,
|
||||
isRootLevel,
|
||||
depth: 1,
|
||||
}),
|
||||
},
|
||||
cursor: '',
|
||||
} as ObjectRecordEdge<T>;
|
||||
|
||||
55
packages/twenty-front/src/modules/object-record/cache/utils/getRecordFromCache.ts
vendored
Normal file
55
packages/twenty-front/src/modules/object-record/cache/utils/getRecordFromCache.ts
vendored
Normal file
@ -0,0 +1,55 @@
|
||||
import { ApolloCache, gql } from '@apollo/client';
|
||||
|
||||
import { CachedObjectRecord } from '@/apollo/types/CachedObjectRecord';
|
||||
import { ObjectMetadataItem } from '@/object-metadata/types/ObjectMetadataItem';
|
||||
import { mapObjectMetadataToGraphQLQuery } from '@/object-metadata/utils/mapObjectMetadataToGraphQLQuery';
|
||||
import { getRecordFromRecordNode } from '@/object-record/cache/utils/getRecordFromRecordNode';
|
||||
import { ObjectRecord } from '@/object-record/types/ObjectRecord';
|
||||
import { isUndefinedOrNull } from '~/utils/isUndefinedOrNull';
|
||||
import { capitalize } from '~/utils/string/capitalize';
|
||||
|
||||
export const getRecordFromCache = <T extends ObjectRecord = ObjectRecord>({
|
||||
objectMetadataItem,
|
||||
objectMetadataItems,
|
||||
cache,
|
||||
recordId,
|
||||
}: {
|
||||
cache: ApolloCache<object>;
|
||||
recordId: string;
|
||||
objectMetadataItems: ObjectMetadataItem[];
|
||||
objectMetadataItem: ObjectMetadataItem;
|
||||
}) => {
|
||||
if (isUndefinedOrNull(objectMetadataItem)) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const capitalizedObjectName = capitalize(objectMetadataItem.nameSingular);
|
||||
|
||||
const cacheReadFragment = gql`
|
||||
fragment ${capitalizedObjectName}Fragment on ${capitalizedObjectName} ${mapObjectMetadataToGraphQLQuery(
|
||||
{
|
||||
objectMetadataItems,
|
||||
objectMetadataItem,
|
||||
},
|
||||
)}
|
||||
`;
|
||||
|
||||
const cachedRecordId = cache.identify({
|
||||
__typename: capitalize(objectMetadataItem.nameSingular),
|
||||
id: recordId,
|
||||
});
|
||||
|
||||
const record = cache.readFragment<T & { __typename: string }>({
|
||||
id: cachedRecordId,
|
||||
fragment: cacheReadFragment,
|
||||
returnPartialData: true,
|
||||
});
|
||||
|
||||
if (isUndefinedOrNull(record)) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return getRecordFromRecordNode({
|
||||
recordNode: record,
|
||||
}) as CachedObjectRecord<T>;
|
||||
};
|
||||
34
packages/twenty-front/src/modules/object-record/cache/utils/getRecordFromRecordNode.ts
vendored
Normal file
34
packages/twenty-front/src/modules/object-record/cache/utils/getRecordFromRecordNode.ts
vendored
Normal file
@ -0,0 +1,34 @@
|
||||
import { getRecordsFromRecordConnection } from '@/object-record/cache/utils/getRecordsFromRecordConnection';
|
||||
import { ObjectRecord } from '@/object-record/types/ObjectRecord';
|
||||
import { isDefined } from '~/utils/isDefined';
|
||||
import { isUndefinedOrNull } from '~/utils/isUndefinedOrNull';
|
||||
|
||||
export const getRecordFromRecordNode = <T extends ObjectRecord>({
|
||||
recordNode,
|
||||
}: {
|
||||
recordNode: T;
|
||||
}): T => {
|
||||
return {
|
||||
...Object.fromEntries(
|
||||
Object.entries(recordNode).map(([fieldName, value]) => {
|
||||
if (isUndefinedOrNull(value)) {
|
||||
return [fieldName, value];
|
||||
}
|
||||
|
||||
if (typeof value === 'object' && isDefined(value.edges)) {
|
||||
return [
|
||||
fieldName,
|
||||
getRecordsFromRecordConnection({ recordConnection: value }),
|
||||
];
|
||||
}
|
||||
|
||||
if (typeof value === 'object' && !isDefined(value.edges)) {
|
||||
return [fieldName, getRecordFromRecordNode<T>({ recordNode: value })];
|
||||
}
|
||||
|
||||
return [fieldName, value];
|
||||
}),
|
||||
),
|
||||
id: recordNode.id,
|
||||
} as T;
|
||||
};
|
||||
143
packages/twenty-front/src/modules/object-record/cache/utils/getRecordNodeFromRecord.ts
vendored
Normal file
143
packages/twenty-front/src/modules/object-record/cache/utils/getRecordNodeFromRecord.ts
vendored
Normal file
@ -0,0 +1,143 @@
|
||||
import { isNull, isUndefined } from '@sniptt/guards';
|
||||
|
||||
import { CachedObjectRecord } from '@/apollo/types/CachedObjectRecord';
|
||||
import { ObjectMetadataItem } from '@/object-metadata/types/ObjectMetadataItem';
|
||||
import { getNodeTypename } from '@/object-record/cache/utils/getNodeTypename';
|
||||
import { getObjectTypename } from '@/object-record/cache/utils/getObjectTypename';
|
||||
import { getRecordConnectionFromRecords } from '@/object-record/cache/utils/getRecordConnectionFromRecords';
|
||||
import { ObjectRecord } from '@/object-record/types/ObjectRecord';
|
||||
import { FieldMetadataType } from '~/generated-metadata/graphql';
|
||||
import { isDefined } from '~/utils/isDefined';
|
||||
|
||||
export const getRecordNodeFromRecord = <T extends ObjectRecord>({
|
||||
objectMetadataItems,
|
||||
objectMetadataItem,
|
||||
queryFields,
|
||||
record,
|
||||
computeReferences = true,
|
||||
isRootLevel = true,
|
||||
depth = 1,
|
||||
}: {
|
||||
objectMetadataItems: ObjectMetadataItem[];
|
||||
objectMetadataItem: Pick<
|
||||
ObjectMetadataItem,
|
||||
'fields' | 'namePlural' | 'nameSingular'
|
||||
>;
|
||||
queryFields?: Record<string, any>;
|
||||
computeReferences?: boolean;
|
||||
isRootLevel?: boolean;
|
||||
record: T | null;
|
||||
depth?: number;
|
||||
}) => {
|
||||
if (isNull(record)) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const nodeTypeName = getNodeTypename(objectMetadataItem.nameSingular);
|
||||
|
||||
if (!isRootLevel && computeReferences) {
|
||||
return {
|
||||
__ref: `${nodeTypeName}:${record.id}`,
|
||||
} as unknown as CachedObjectRecord<T>; // Todo Fix typing
|
||||
}
|
||||
|
||||
const nestedRecord = Object.fromEntries(
|
||||
Object.entries(record)
|
||||
.map(([fieldName, value]) => {
|
||||
if (isDefined(queryFields) && !queryFields[fieldName]) {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
const field = objectMetadataItem.fields.find(
|
||||
(field) => field.name === fieldName,
|
||||
);
|
||||
|
||||
if (isUndefined(field)) {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
if (
|
||||
!isUndefined(depth) &&
|
||||
depth < 1 &&
|
||||
field.type === FieldMetadataType.Relation
|
||||
) {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
if (Array.isArray(value)) {
|
||||
const objectMetadataItem = objectMetadataItems.find(
|
||||
(objectMetadataItem) => objectMetadataItem.namePlural === fieldName,
|
||||
);
|
||||
|
||||
if (!objectMetadataItem) {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
return [
|
||||
fieldName,
|
||||
getRecordConnectionFromRecords({
|
||||
objectMetadataItems,
|
||||
objectMetadataItem: objectMetadataItem,
|
||||
records: value as ObjectRecord[],
|
||||
queryFields:
|
||||
queryFields?.[fieldName] === true ||
|
||||
isUndefined(queryFields?.[fieldName])
|
||||
? undefined
|
||||
: queryFields?.[fieldName],
|
||||
withPageInfo: false,
|
||||
isRootLevel: false,
|
||||
computeReferences,
|
||||
depth: depth - 1,
|
||||
}),
|
||||
];
|
||||
}
|
||||
|
||||
if (field.type === 'RELATION') {
|
||||
if (
|
||||
isUndefined(
|
||||
field.relationDefinition?.targetObjectMetadata.nameSingular,
|
||||
)
|
||||
) {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
if (isNull(value)) {
|
||||
return [fieldName, null];
|
||||
}
|
||||
|
||||
if (isUndefined(value?.id)) {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
const typeName = getObjectTypename(
|
||||
field.relationDefinition?.targetObjectMetadata.nameSingular,
|
||||
);
|
||||
|
||||
if (computeReferences) {
|
||||
return [
|
||||
fieldName,
|
||||
{
|
||||
__ref: `${typeName}:${value.id}`,
|
||||
},
|
||||
];
|
||||
}
|
||||
|
||||
return [
|
||||
fieldName,
|
||||
{
|
||||
__typename: typeName,
|
||||
...value,
|
||||
},
|
||||
];
|
||||
}
|
||||
|
||||
return [fieldName, value];
|
||||
})
|
||||
.filter(isDefined),
|
||||
) as T; // Todo fix typing once we have investigated apollo edges / nodes removal
|
||||
|
||||
return {
|
||||
__typename: getNodeTypename(objectMetadataItem.nameSingular),
|
||||
...nestedRecord,
|
||||
};
|
||||
};
|
||||
@ -1,3 +1,4 @@
|
||||
import { getRecordFromRecordNode } from '@/object-record/cache/utils/getRecordFromRecordNode';
|
||||
import { ObjectRecord } from '@/object-record/types/ObjectRecord';
|
||||
import { ObjectRecordConnection } from '@/object-record/types/ObjectRecordConnection';
|
||||
|
||||
@ -6,5 +7,7 @@ export const getRecordsFromRecordConnection = <T extends ObjectRecord>({
|
||||
}: {
|
||||
recordConnection: ObjectRecordConnection<T>;
|
||||
}): T[] => {
|
||||
return recordConnection.edges.map((edge) => edge.node);
|
||||
return recordConnection.edges.map((edge) =>
|
||||
getRecordFromRecordNode<T>({ recordNode: edge.node }),
|
||||
);
|
||||
};
|
||||
|
||||
30
packages/twenty-front/src/modules/object-record/cache/utils/isObjectRecordConnection.ts
vendored
Normal file
30
packages/twenty-front/src/modules/object-record/cache/utils/isObjectRecordConnection.ts
vendored
Normal file
@ -0,0 +1,30 @@
|
||||
import { z } from 'zod';
|
||||
|
||||
import { ObjectRecordConnection } from '@/object-record/types/ObjectRecordConnection';
|
||||
import { capitalize } from '~/utils/string/capitalize';
|
||||
|
||||
export const isObjectRecordConnection = (
|
||||
objectNameSingular: string,
|
||||
value: unknown,
|
||||
): value is ObjectRecordConnection => {
|
||||
const objectConnectionTypeName = `${capitalize(
|
||||
objectNameSingular,
|
||||
)}Connection`;
|
||||
const objectEdgeTypeName = `${capitalize(objectNameSingular)}Edge`;
|
||||
|
||||
const objectConnectionSchema = z.object({
|
||||
__typename: z.literal(objectConnectionTypeName).optional(),
|
||||
edges: z.array(
|
||||
z.object({
|
||||
__typename: z.literal(objectEdgeTypeName).optional(),
|
||||
node: z.object({
|
||||
id: z.string().uuid(),
|
||||
}),
|
||||
}),
|
||||
),
|
||||
});
|
||||
|
||||
const connectionValidation = objectConnectionSchema.safeParse(value);
|
||||
|
||||
return connectionValidation.success;
|
||||
};
|
||||
30
packages/twenty-front/src/modules/object-record/cache/utils/isObjectRecordConnectionWithRefs.ts
vendored
Normal file
30
packages/twenty-front/src/modules/object-record/cache/utils/isObjectRecordConnectionWithRefs.ts
vendored
Normal file
@ -0,0 +1,30 @@
|
||||
import { StoreValue } from '@apollo/client';
|
||||
import { z } from 'zod';
|
||||
|
||||
import { CachedObjectRecordConnection } from '@/apollo/types/CachedObjectRecordConnection';
|
||||
import { capitalize } from '~/utils/string/capitalize';
|
||||
|
||||
export const isObjectRecordConnectionWithRefs = (
|
||||
objectNameSingular: string,
|
||||
storeValue: StoreValue,
|
||||
): storeValue is CachedObjectRecordConnection => {
|
||||
const objectConnectionTypeName = `${capitalize(
|
||||
objectNameSingular,
|
||||
)}Connection`;
|
||||
const objectEdgeTypeName = `${capitalize(objectNameSingular)}Edge`;
|
||||
const cachedObjectConnectionSchema = z.object({
|
||||
__typename: z.literal(objectConnectionTypeName),
|
||||
edges: z.array(
|
||||
z.object({
|
||||
__typename: z.literal(objectEdgeTypeName),
|
||||
node: z.object({
|
||||
__ref: z.string().startsWith(`${capitalize(objectNameSingular)}:`),
|
||||
}),
|
||||
}),
|
||||
),
|
||||
});
|
||||
const cachedConnectionValidation =
|
||||
cachedObjectConnectionSchema.safeParse(storeValue);
|
||||
|
||||
return cachedConnectionValidation.success;
|
||||
};
|
||||
33
packages/twenty-front/src/modules/object-record/cache/utils/modifyRecordFromCache.ts
vendored
Normal file
33
packages/twenty-front/src/modules/object-record/cache/utils/modifyRecordFromCache.ts
vendored
Normal file
@ -0,0 +1,33 @@
|
||||
import { ApolloCache, Modifiers } from '@apollo/client/cache';
|
||||
|
||||
import { ObjectMetadataItem } from '@/object-metadata/types/ObjectMetadataItem';
|
||||
import { ObjectRecord } from '@/object-record/types/ObjectRecord';
|
||||
import { isUndefinedOrNull } from '~/utils/isUndefinedOrNull';
|
||||
import { capitalize } from '~/utils/string/capitalize';
|
||||
|
||||
export const modifyRecordFromCache = <
|
||||
CachedObjectRecord extends ObjectRecord = ObjectRecord,
|
||||
>({
|
||||
objectMetadataItem,
|
||||
cache,
|
||||
fieldModifiers,
|
||||
recordId,
|
||||
}: {
|
||||
objectMetadataItem: ObjectMetadataItem;
|
||||
cache: ApolloCache<object>;
|
||||
fieldModifiers: Modifiers<CachedObjectRecord>;
|
||||
recordId: string;
|
||||
}) => {
|
||||
if (isUndefinedOrNull(objectMetadataItem)) return;
|
||||
|
||||
const cachedRecordId = cache.identify({
|
||||
__typename: capitalize(objectMetadataItem.nameSingular),
|
||||
id: recordId,
|
||||
});
|
||||
|
||||
cache.modify<CachedObjectRecord>({
|
||||
id: cachedRecordId,
|
||||
fields: fieldModifiers,
|
||||
optimistic: true,
|
||||
});
|
||||
};
|
||||
59
packages/twenty-front/src/modules/object-record/cache/utils/updateRecordFromCache.ts
vendored
Normal file
59
packages/twenty-front/src/modules/object-record/cache/utils/updateRecordFromCache.ts
vendored
Normal file
@ -0,0 +1,59 @@
|
||||
import { ApolloCache } from '@apollo/client/cache';
|
||||
import gql from 'graphql-tag';
|
||||
|
||||
import { ObjectMetadataItem } from '@/object-metadata/types/ObjectMetadataItem';
|
||||
import { mapObjectMetadataToGraphQLQuery } from '@/object-metadata/utils/mapObjectMetadataToGraphQLQuery';
|
||||
import { getRecordNodeFromRecord } from '@/object-record/cache/utils/getRecordNodeFromRecord';
|
||||
import { ObjectRecord } from '@/object-record/types/ObjectRecord';
|
||||
import { isUndefinedOrNull } from '~/utils/isUndefinedOrNull';
|
||||
import { capitalize } from '~/utils/string/capitalize';
|
||||
|
||||
export const updateRecordFromCache = <T extends ObjectRecord>({
|
||||
objectMetadataItems,
|
||||
objectMetadataItem,
|
||||
cache,
|
||||
record,
|
||||
}: {
|
||||
objectMetadataItems: ObjectMetadataItem[];
|
||||
objectMetadataItem: ObjectMetadataItem;
|
||||
cache: ApolloCache<object>;
|
||||
record: T;
|
||||
}) => {
|
||||
if (isUndefinedOrNull(objectMetadataItem)) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const capitalizedObjectName = capitalize(objectMetadataItem.nameSingular);
|
||||
|
||||
const cacheWriteFragment = gql`
|
||||
fragment ${capitalizedObjectName}Fragment on ${capitalizedObjectName} ${mapObjectMetadataToGraphQLQuery(
|
||||
{
|
||||
objectMetadataItems,
|
||||
objectMetadataItem,
|
||||
computeReferences: true,
|
||||
},
|
||||
)}
|
||||
`;
|
||||
|
||||
const cachedRecordId = cache.identify({
|
||||
__typename: capitalize(objectMetadataItem.nameSingular),
|
||||
id: record.id,
|
||||
});
|
||||
|
||||
const recordWithConnection = getRecordNodeFromRecord<T>({
|
||||
objectMetadataItems,
|
||||
objectMetadataItem,
|
||||
record,
|
||||
depth: 1,
|
||||
});
|
||||
|
||||
if (isUndefinedOrNull(recordWithConnection)) {
|
||||
return;
|
||||
}
|
||||
|
||||
cache.writeFragment<T & { __typename: string }>({
|
||||
id: cachedRecordId,
|
||||
fragment: cacheWriteFragment,
|
||||
data: recordWithConnection,
|
||||
});
|
||||
};
|
||||
Reference in New Issue
Block a user