Activity injection into Apollo cache (#3665)

- Created addRecordInCache to inject a record in Apollo cache and inject single read query on this record
- Created createOneRecordInCache and createManyRecordsInCache that uses this addRecordInCache
- Created useOpenCreateActivityDrawerV2 hook to create an activity in cache and inject it into all other relevant requests in the app before opening activity drawer
- Refactored DEFAULT_SEARCH_REQUEST_LIMIT constant and hardcoded arbitrary request limits
- Added Apollo dev logs to see errors in the console when manipulating cache
This commit is contained in:
Lucas Bordeau
2024-01-29 16:12:52 +01:00
committed by GitHub
parent 64d0e15ada
commit 3b458d5207
57 changed files with 1160 additions and 190 deletions

View File

@ -0,0 +1,72 @@
import { useApolloClient } from '@apollo/client';
import gql from 'graphql-tag';
import { useRecoilCallback } from 'recoil';
import { useMapFieldMetadataToGraphQLQuery } from '@/object-metadata/hooks/useMapFieldMetadataToGraphQLQuery';
import { ObjectMetadataItem } from '@/object-metadata/types/ObjectMetadataItem';
import { useGenerateFindOneRecordQuery } from '@/object-record/hooks/useGenerateFindOneRecordQuery';
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 mapFieldMetadataToGraphQLQuery = useMapFieldMetadataToGraphQLQuery();
const apolloClient = useApolloClient();
const generateFindOneRecordQuery = useGenerateFindOneRecordQuery();
const findOneRecordQuery = generateFindOneRecordQuery({
objectMetadataItem,
});
return useRecoilCallback(
({ set }) =>
(record: ObjectRecord) => {
apolloClient.writeFragment({
id: `${capitalize(objectMetadataItem.nameSingular)}:${record.id}`,
fragment: gql`
fragment Create${capitalize(
objectMetadataItem.nameSingular,
)}InCache on ${capitalize(objectMetadataItem.nameSingular)} {
__typename
id
${objectMetadataItem.fields
.map((field) => mapFieldMetadataToGraphQLQuery(field))
.join('\n')}
}
`,
data: {
__typename: `${capitalize(objectMetadataItem.nameSingular)}`,
...record,
},
});
// TODO: Turn into injectIntoFindOneRecordQueryCache
apolloClient.writeQuery({
query: findOneRecordQuery,
variables: {
objectRecordId: record.id,
},
data: {
[objectMetadataItem.nameSingular]: {
__typename: `${capitalize(objectMetadataItem.nameSingular)}`,
...record,
},
},
});
// TODO: remove this once we get rid of entityFieldsFamilyState
set(recordStoreFamilyState(record.id), record);
},
[
objectMetadataItem,
apolloClient,
mapFieldMetadataToGraphQLQuery,
findOneRecordQuery,
],
);
};

View File

@ -0,0 +1,41 @@
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,45 @@
import { gql, useApolloClient } from '@apollo/client';
import { useMapFieldMetadataToGraphQLQuery } from '@/object-metadata/hooks/useMapFieldMetadataToGraphQLQuery';
import { ObjectMetadataItem } from '@/object-metadata/types/ObjectMetadataItem';
import { ObjectRecord } from '@/object-record/types/ObjectRecord';
import { capitalize } from '~/utils/string/capitalize';
export const useGetRecordFromCache = ({
objectMetadataItem,
}: {
objectMetadataItem: ObjectMetadataItem;
}) => {
const mapFieldMetadataToGraphQLQuery = useMapFieldMetadataToGraphQLQuery();
const apolloClient = useApolloClient();
return <CachedObjectRecord extends ObjectRecord = ObjectRecord>(
recordId: string,
) => {
if (!objectMetadataItem) {
return null;
}
const capitalizedObjectName = capitalize(objectMetadataItem.nameSingular);
const cacheReadFragment = gql`
fragment ${capitalizedObjectName}Fragment on ${capitalizedObjectName} {
id
${objectMetadataItem.fields
.map((field) => mapFieldMetadataToGraphQLQuery(field))
.join('\n')}
}
`;
const cache = apolloClient.cache;
const cachedRecordId = cache.identify({
__typename: capitalize(objectMetadataItem.nameSingular),
id: recordId,
});
return cache.readFragment<CachedObjectRecord & { __typename: string }>({
id: cachedRecordId,
fragment: cacheReadFragment,
});
};
};

View File

@ -0,0 +1,31 @@
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 { 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 (!objectMetadataItem) return;
const cachedRecordId = cache.identify({
__typename: capitalize(objectMetadataItem.nameSingular),
id: recordId,
});
cache.modify<CachedObjectRecord>({
id: cachedRecordId,
fields: fieldModifiers,
});
};
};

View File

@ -0,0 +1,31 @@
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;
};

View File

@ -0,0 +1,43 @@
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;
};

View File

@ -0,0 +1,16 @@
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,
};
};

View File

@ -0,0 +1,9 @@
import { capitalize } from '~/utils/string/capitalize';
export const getConnectionTypename = ({
objectNameSingular,
}: {
objectNameSingular: string;
}) => {
return `${capitalize(objectNameSingular)}Connection`;
};

View File

@ -0,0 +1,9 @@
import { capitalize } from '~/utils/string/capitalize';
export const getEdgeTypename = ({
objectNameSingular,
}: {
objectNameSingular: string;
}) => {
return `${capitalize(objectNameSingular)}Edge`;
};

View File

@ -0,0 +1,8 @@
export const getEmptyPageInfo = () => {
return {
hasNextPage: false,
hasPreviousPage: false,
startCursor: null,
endCursor: null,
};
};

View File

@ -0,0 +1,9 @@
import { capitalize } from '~/utils/string/capitalize';
export const getNodeTypename = ({
objectNameSingular,
}: {
objectNameSingular: string;
}) => {
return capitalize(objectNameSingular);
};

View File

@ -0,0 +1,19 @@
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>;
};

View File

@ -0,0 +1,24 @@
import { getConnectionTypename } from '@/object-record/cache/utils/getConnectionTypename';
import { getEmptyPageInfo } from '@/object-record/cache/utils/getEmptyPageInfo';
import { getRecordEdgeFromRecord } from '@/object-record/cache/utils/getRecordEdgeFromRecord';
import { ObjectRecord } from '@/object-record/types/ObjectRecord';
import { ObjectRecordConnection } from '@/object-record/types/ObjectRecordConnection';
export const getRecordConnectionFromRecords = <T extends ObjectRecord>({
objectNameSingular,
records,
}: {
objectNameSingular: string;
records: T[];
}) => {
return {
__typename: getConnectionTypename({ objectNameSingular }),
edges: records.map((record) => {
return getRecordEdgeFromRecord({
objectNameSingular,
record,
});
}),
pageInfo: getEmptyPageInfo(),
} as ObjectRecordConnection<T>;
};

View File

@ -0,0 +1,21 @@
import { getEdgeTypename } from '@/object-record/cache/utils/getEdgeTypename';
import { getNodeTypename } from '@/object-record/cache/utils/getNodeTypename';
import { ObjectRecord } from '@/object-record/types/ObjectRecord';
import { ObjectRecordEdge } from '@/object-record/types/ObjectRecordEdge';
export const getRecordEdgeFromRecord = <T extends ObjectRecord>({
objectNameSingular,
record,
}: {
objectNameSingular: string;
record: T;
}) => {
return {
__typename: getEdgeTypename({ objectNameSingular }),
node: {
__typename: getNodeTypename({ objectNameSingular }),
...record,
},
cursor: '',
} as ObjectRecordEdge<T>;
};

View File

@ -0,0 +1,10 @@
import { ObjectRecord } from '@/object-record/types/ObjectRecord';
import { ObjectRecordConnection } from '@/object-record/types/ObjectRecordConnection';
export const getRecordsFromRecordConnection = <T extends ObjectRecord>({
recordConnection,
}: {
recordConnection: ObjectRecordConnection<T>;
}): T[] => {
return recordConnection.edges.map((edge) => edge.node);
};