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,
|
||||
});
|
||||
};
|
||||
@ -1,783 +0,0 @@
|
||||
import { Company } from '@/companies/types/Company';
|
||||
import { Favorite } from '@/favorites/types/Favorite';
|
||||
import { ObjectRecordConnection } from '@/object-record/types/ObjectRecordConnection';
|
||||
import { Person } from '@/people/types/Person';
|
||||
|
||||
export const emptyConnectionMock: ObjectRecordConnection = {
|
||||
edges: [],
|
||||
pageInfo: {
|
||||
hasNextPage: false,
|
||||
hasPreviousPage: false,
|
||||
startCursor: '',
|
||||
endCursor: '',
|
||||
},
|
||||
totalCount: 0,
|
||||
__typename: 'ObjectRecordConnection',
|
||||
};
|
||||
|
||||
export const companiesConnectionWithPeopleConnectionWithFavoritesConnectionMock: ObjectRecordConnection<
|
||||
Partial<Company> &
|
||||
Pick<Company, 'id'> & {
|
||||
people: ObjectRecordConnection<
|
||||
Pick<Person, 'id' | 'name'> & {
|
||||
favorites: ObjectRecordConnection<
|
||||
Pick<Favorite, 'id' | 'personId' | 'companyId' | 'position'>
|
||||
>;
|
||||
}
|
||||
>;
|
||||
}
|
||||
> = {
|
||||
pageInfo: {
|
||||
endCursor: 'WyJmZTI1NmIzOS0zZWMzLTRmZTMtODk5Ny1iNzZhYTBiZmE0MDgiXQ==',
|
||||
hasNextPage: true,
|
||||
hasPreviousPage: false,
|
||||
startCursor: 'WyIwNGIyZTlmNS0wNzEzLTQwYTUtODIxNi04MjgwMjQwMWQzM2UiXQ==',
|
||||
},
|
||||
edges: [
|
||||
{
|
||||
cursor: 'WyIwNGIyZTlmNS0wNzEzLTQwYTUtODIxNi04MjgwMjQwMWQzM2UiXQ==',
|
||||
node: {
|
||||
id: '04b2e9f5-0713-40a5-8216-82802401d33e',
|
||||
name: 'Qonto',
|
||||
people: {
|
||||
edges: [],
|
||||
pageInfo: {
|
||||
endCursor: null,
|
||||
hasNextPage: false,
|
||||
hasPreviousPage: false,
|
||||
startCursor: null,
|
||||
},
|
||||
totalCount: 0,
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
cursor: 'WyIwZDk0MDk5Ny1jMjFlLTRlYzItODczYi1kZTQyNjRkODkwMjUiXQ==',
|
||||
node: {
|
||||
id: '0d940997-c21e-4ec2-873b-de4264d89025',
|
||||
name: 'Google',
|
||||
people: {
|
||||
edges: [
|
||||
{
|
||||
cursor:
|
||||
'WyIyNDBkYTJlYy0yZDQwLTRlNDktOGRmNC05YzZhMDQ5MTkwZGYiXQ==',
|
||||
node: {
|
||||
id: '240da2ec-2d40-4e49-8df4-9c6a049190df',
|
||||
name: {
|
||||
firstName: 'Bertrand',
|
||||
lastName: 'Voulzy',
|
||||
},
|
||||
favorites: {
|
||||
edges: [
|
||||
{
|
||||
cursor:
|
||||
'WyJjODVhODY3Yy01YThmLTQ4NjEtOGVkMi05NmMzOTAyNDg0MjMiXQ==',
|
||||
node: {
|
||||
id: 'c85a867c-5a8f-4861-8ed2-96c390248423',
|
||||
personId: '240da2ec-2d40-4e49-8df4-9c6a049190df',
|
||||
companyId: null,
|
||||
position: 2,
|
||||
},
|
||||
},
|
||||
],
|
||||
pageInfo: {
|
||||
endCursor:
|
||||
'WyJjODVhODY3Yy01YThmLTQ4NjEtOGVkMi05NmMzOTAyNDg0MjMiXQ==',
|
||||
hasNextPage: false,
|
||||
hasPreviousPage: false,
|
||||
startCursor:
|
||||
'WyJjODVhODY3Yy01YThmLTQ4NjEtOGVkMi05NmMzOTAyNDg0MjMiXQ==',
|
||||
},
|
||||
totalCount: 1,
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
cursor:
|
||||
'WyIyNDBkYTJlYy0yZDQwLTRlNDktOGRmNC05YzZhMDQ5MTkwZWYiXQ==',
|
||||
node: {
|
||||
id: '240da2ec-2d40-4e49-8df4-9c6a049190ef',
|
||||
name: {
|
||||
firstName: 'Madison',
|
||||
lastName: 'Perez',
|
||||
},
|
||||
favorites: {
|
||||
edges: [],
|
||||
pageInfo: {
|
||||
endCursor: null,
|
||||
hasNextPage: false,
|
||||
hasPreviousPage: false,
|
||||
startCursor: null,
|
||||
},
|
||||
totalCount: 0,
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
cursor:
|
||||
'WyI1Njk1NTQyMi01ZDU0LTQxYjctYmEzNi1mMGQyMGUxNDE3YWUiXQ==',
|
||||
node: {
|
||||
id: '56955422-5d54-41b7-ba36-f0d20e1417ae',
|
||||
name: {
|
||||
firstName: 'Avery',
|
||||
lastName: 'Carter',
|
||||
},
|
||||
favorites: {
|
||||
edges: [],
|
||||
pageInfo: {
|
||||
endCursor: null,
|
||||
hasNextPage: false,
|
||||
hasPreviousPage: false,
|
||||
startCursor: null,
|
||||
},
|
||||
totalCount: 0,
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
cursor:
|
||||
'WyI3NTUwMzVkYi02MjNkLTQxZmUtOTJlNy1kZDQ1YjdjNTY4ZTEiXQ==',
|
||||
node: {
|
||||
id: '755035db-623d-41fe-92e7-dd45b7c568e1',
|
||||
name: {
|
||||
firstName: 'Ethan',
|
||||
lastName: 'Mitchell',
|
||||
},
|
||||
favorites: {
|
||||
edges: [],
|
||||
pageInfo: {
|
||||
endCursor: null,
|
||||
hasNextPage: false,
|
||||
hasPreviousPage: false,
|
||||
startCursor: null,
|
||||
},
|
||||
totalCount: 0,
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
cursor:
|
||||
'WyJhMmU3OGE1Zi0zMzhiLTQ2ZGYtODgxMS1mYTA4YzdkMTlkMzUiXQ==',
|
||||
node: {
|
||||
id: 'a2e78a5f-338b-46df-8811-fa08c7d19d35',
|
||||
name: {
|
||||
firstName: 'Elizabeth',
|
||||
lastName: 'Baker',
|
||||
},
|
||||
favorites: {
|
||||
edges: [],
|
||||
pageInfo: {
|
||||
endCursor: null,
|
||||
hasNextPage: false,
|
||||
hasPreviousPage: false,
|
||||
startCursor: null,
|
||||
},
|
||||
totalCount: 0,
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
cursor:
|
||||
'WyJjYTFmNWJmMy02NGFkLTRiMGUtYmJmZC1lOWZkNzk1YjcwMTYiXQ==',
|
||||
node: {
|
||||
id: 'ca1f5bf3-64ad-4b0e-bbfd-e9fd795b7016',
|
||||
name: {
|
||||
firstName: 'Christopher',
|
||||
lastName: 'Nelson',
|
||||
},
|
||||
favorites: {
|
||||
edges: [],
|
||||
pageInfo: {
|
||||
endCursor: null,
|
||||
hasNextPage: false,
|
||||
hasPreviousPage: false,
|
||||
startCursor: null,
|
||||
},
|
||||
totalCount: 0,
|
||||
},
|
||||
},
|
||||
},
|
||||
],
|
||||
pageInfo: {
|
||||
endCursor:
|
||||
'WyJjYTFmNWJmMy02NGFkLTRiMGUtYmJmZC1lOWZkNzk1YjcwMTYiXQ==',
|
||||
hasNextPage: false,
|
||||
hasPreviousPage: false,
|
||||
startCursor:
|
||||
'WyIyNDBkYTJlYy0yZDQwLTRlNDktOGRmNC05YzZhMDQ5MTkwZGYiXQ==',
|
||||
},
|
||||
totalCount: 6,
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
cursor: 'WyIxMTg5OTVmMy01ZDgxLTQ2ZDYtYmY4My1mN2ZkMzNlYTYxMDIiXQ==',
|
||||
node: {
|
||||
id: '118995f3-5d81-46d6-bf83-f7fd33ea6102',
|
||||
name: 'Facebook',
|
||||
people: {
|
||||
edges: [
|
||||
{
|
||||
cursor:
|
||||
'WyI5M2M3MmQyZS1mNTE3LTQyZmQtODBhZS0xNDE3M2IzYjcwYWUiXQ==',
|
||||
node: {
|
||||
id: '93c72d2e-f517-42fd-80ae-14173b3b70ae',
|
||||
name: {
|
||||
firstName: 'Christopher',
|
||||
lastName: 'Gonzalez',
|
||||
},
|
||||
favorites: {
|
||||
edges: [],
|
||||
pageInfo: {
|
||||
endCursor: null,
|
||||
hasNextPage: false,
|
||||
hasPreviousPage: false,
|
||||
startCursor: null,
|
||||
},
|
||||
totalCount: 0,
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
cursor:
|
||||
'WyJlZWVhY2FjZi1lZWUxLTQ2OTAtYWQyYy04NjE5ZTViNTZhMmUiXQ==',
|
||||
node: {
|
||||
id: 'eeeacacf-eee1-4690-ad2c-8619e5b56a2e',
|
||||
name: {
|
||||
firstName: 'Ashley',
|
||||
lastName: 'Parker',
|
||||
},
|
||||
favorites: {
|
||||
edges: [],
|
||||
pageInfo: {
|
||||
endCursor: null,
|
||||
hasNextPage: false,
|
||||
hasPreviousPage: false,
|
||||
startCursor: null,
|
||||
},
|
||||
totalCount: 0,
|
||||
},
|
||||
},
|
||||
},
|
||||
],
|
||||
pageInfo: {
|
||||
endCursor:
|
||||
'WyJlZWVhY2FjZi1lZWUxLTQ2OTAtYWQyYy04NjE5ZTViNTZhMmUiXQ==',
|
||||
hasNextPage: false,
|
||||
hasPreviousPage: false,
|
||||
startCursor:
|
||||
'WyI5M2M3MmQyZS1mNTE3LTQyZmQtODBhZS0xNDE3M2IzYjcwYWUiXQ==',
|
||||
},
|
||||
totalCount: 2,
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
cursor: 'WyIxZDNhMWM2ZS03MDdlLTQ0ZGMtYTFkMi0zMDAzMGJmMWE5NDQiXQ==',
|
||||
node: {
|
||||
id: '1d3a1c6e-707e-44dc-a1d2-30030bf1a944',
|
||||
name: 'Netflix',
|
||||
people: {
|
||||
edges: [],
|
||||
pageInfo: {
|
||||
endCursor: null,
|
||||
hasNextPage: false,
|
||||
hasPreviousPage: false,
|
||||
startCursor: null,
|
||||
},
|
||||
totalCount: 0,
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
cursor: 'WyI0NjBiNmZiMS1lZDg5LTQxM2EtYjMxYS05NjI5ODZlNjdiYjQiXQ==',
|
||||
node: {
|
||||
id: '460b6fb1-ed89-413a-b31a-962986e67bb4',
|
||||
name: 'Microsoft',
|
||||
people: {
|
||||
edges: [
|
||||
{
|
||||
cursor:
|
||||
'WyIxZDE1MTg1Mi00OTBmLTQ0NjYtODM5MS03MzNjZmQ2NmEwYzgiXQ==',
|
||||
node: {
|
||||
id: '1d151852-490f-4466-8391-733cfd66a0c8',
|
||||
name: {
|
||||
firstName: 'Isabella',
|
||||
lastName: 'Scott',
|
||||
},
|
||||
favorites: {
|
||||
edges: [],
|
||||
pageInfo: {
|
||||
endCursor: null,
|
||||
hasNextPage: false,
|
||||
hasPreviousPage: false,
|
||||
startCursor: null,
|
||||
},
|
||||
totalCount: 0,
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
cursor:
|
||||
'WyI5ODQwNmUyNi04MGYxLTRkZmYtYjU3MC1hNzQ5NDI1MjhkZTMiXQ==',
|
||||
node: {
|
||||
id: '98406e26-80f1-4dff-b570-a74942528de3',
|
||||
name: {
|
||||
firstName: 'Matthew',
|
||||
lastName: 'Green',
|
||||
},
|
||||
favorites: {
|
||||
edges: [],
|
||||
pageInfo: {
|
||||
endCursor: null,
|
||||
hasNextPage: false,
|
||||
hasPreviousPage: false,
|
||||
startCursor: null,
|
||||
},
|
||||
totalCount: 0,
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
cursor:
|
||||
'WyI5YjMyNGE4OC02Nzg0LTQ0NDktYWZkZi1kYzYyY2I4NzAyZjIiXQ==',
|
||||
node: {
|
||||
id: '9b324a88-6784-4449-afdf-dc62cb8702f2',
|
||||
name: {
|
||||
firstName: 'Nicholas',
|
||||
lastName: 'Wright',
|
||||
},
|
||||
favorites: {
|
||||
edges: [],
|
||||
pageInfo: {
|
||||
endCursor: null,
|
||||
hasNextPage: false,
|
||||
hasPreviousPage: false,
|
||||
startCursor: null,
|
||||
},
|
||||
totalCount: 0,
|
||||
},
|
||||
},
|
||||
},
|
||||
],
|
||||
pageInfo: {
|
||||
endCursor:
|
||||
'WyI5YjMyNGE4OC02Nzg0LTQ0NDktYWZkZi1kYzYyY2I4NzAyZjIiXQ==',
|
||||
hasNextPage: false,
|
||||
hasPreviousPage: false,
|
||||
startCursor:
|
||||
'WyIxZDE1MTg1Mi00OTBmLTQ0NjYtODM5MS03MzNjZmQ2NmEwYzgiXQ==',
|
||||
},
|
||||
totalCount: 3,
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
cursor: 'WyI3YTkzZDFlNS0zZjc0LTQ5MmQtYTEwMS0yYTcwZjUwYTE2NDUiXQ==',
|
||||
node: {
|
||||
id: '7a93d1e5-3f74-492d-a101-2a70f50a1645',
|
||||
name: 'Libeo',
|
||||
people: {
|
||||
edges: [],
|
||||
pageInfo: {
|
||||
endCursor: null,
|
||||
hasNextPage: false,
|
||||
hasPreviousPage: false,
|
||||
startCursor: null,
|
||||
},
|
||||
totalCount: 0,
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
cursor: 'WyI4OWJiODI1Yy0xNzFlLTRiY2MtOWNmNy00MzQ0OGQ2ZmIyNzgiXQ==',
|
||||
node: {
|
||||
id: '89bb825c-171e-4bcc-9cf7-43448d6fb278',
|
||||
name: 'Airbnb',
|
||||
people: {
|
||||
edges: [],
|
||||
pageInfo: {
|
||||
endCursor: null,
|
||||
hasNextPage: false,
|
||||
hasPreviousPage: false,
|
||||
startCursor: null,
|
||||
},
|
||||
totalCount: 0,
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
cursor: 'WyI5ZDE2MmRlNi1jZmJmLTQxNTYtYTc5MC1lMzk4NTRkY2Q0ZWIiXQ==',
|
||||
node: {
|
||||
id: '9d162de6-cfbf-4156-a790-e39854dcd4eb',
|
||||
name: 'Claap',
|
||||
people: {
|
||||
edges: [],
|
||||
pageInfo: {
|
||||
endCursor: null,
|
||||
hasNextPage: false,
|
||||
hasPreviousPage: false,
|
||||
startCursor: null,
|
||||
},
|
||||
totalCount: 0,
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
cursor: 'WyJhNjc0ZmE2Yy0xNDU1LTRjNTctYWZhZi1kZDVkYzA4NjM2MWQiXQ==',
|
||||
node: {
|
||||
id: 'a674fa6c-1455-4c57-afaf-dd5dc086361d',
|
||||
name: 'Algolia',
|
||||
people: {
|
||||
edges: [
|
||||
{
|
||||
cursor:
|
||||
'WyIyNDBkYTJlYy0yZDQwLTRlNDktOGRmNC05YzZhMDQ5MTkxZGYiXQ==',
|
||||
node: {
|
||||
id: '240da2ec-2d40-4e49-8df4-9c6a049191df',
|
||||
name: {
|
||||
firstName: 'Lorie',
|
||||
lastName: 'Vladim',
|
||||
},
|
||||
favorites: {
|
||||
edges: [],
|
||||
pageInfo: {
|
||||
endCursor: null,
|
||||
hasNextPage: false,
|
||||
hasPreviousPage: false,
|
||||
startCursor: null,
|
||||
},
|
||||
totalCount: 0,
|
||||
},
|
||||
},
|
||||
},
|
||||
],
|
||||
pageInfo: {
|
||||
endCursor:
|
||||
'WyIyNDBkYTJlYy0yZDQwLTRlNDktOGRmNC05YzZhMDQ5MTkxZGYiXQ==',
|
||||
hasNextPage: false,
|
||||
hasPreviousPage: false,
|
||||
startCursor:
|
||||
'WyIyNDBkYTJlYy0yZDQwLTRlNDktOGRmNC05YzZhMDQ5MTkxZGYiXQ==',
|
||||
},
|
||||
totalCount: 1,
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
cursor: 'WyJhN2JjNjhkNS1mNzllLTQwZGQtYmQwNi1jMzZlNmFiYjQ2NzgiXQ==',
|
||||
node: {
|
||||
id: 'a7bc68d5-f79e-40dd-bd06-c36e6abb4678',
|
||||
name: 'Samsung',
|
||||
people: {
|
||||
edges: [
|
||||
{
|
||||
cursor:
|
||||
'WyIyNDBkYTJlYy0yZDQwLTRlNDktOGRmNC05YzZhMDQ5MTkxZGUiXQ==',
|
||||
node: {
|
||||
id: '240da2ec-2d40-4e49-8df4-9c6a049191de',
|
||||
name: {
|
||||
firstName: 'Louis',
|
||||
lastName: 'Duss',
|
||||
},
|
||||
favorites: {
|
||||
edges: [],
|
||||
pageInfo: {
|
||||
endCursor: null,
|
||||
hasNextPage: false,
|
||||
hasPreviousPage: false,
|
||||
startCursor: null,
|
||||
},
|
||||
totalCount: 0,
|
||||
},
|
||||
},
|
||||
},
|
||||
],
|
||||
pageInfo: {
|
||||
endCursor:
|
||||
'WyIyNDBkYTJlYy0yZDQwLTRlNDktOGRmNC05YzZhMDQ5MTkxZGUiXQ==',
|
||||
hasNextPage: false,
|
||||
hasPreviousPage: false,
|
||||
startCursor:
|
||||
'WyIyNDBkYTJlYy0yZDQwLTRlNDktOGRmNC05YzZhMDQ5MTkxZGUiXQ==',
|
||||
},
|
||||
totalCount: 1,
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
cursor: 'WyJhYWZmY2ZiZC1mODZiLTQxOWYtYjc5NC0wMjMxOWFiZTg2MzciXQ==',
|
||||
node: {
|
||||
id: 'aaffcfbd-f86b-419f-b794-02319abe8637',
|
||||
name: 'Hasura',
|
||||
people: {
|
||||
edges: [],
|
||||
pageInfo: {
|
||||
endCursor: null,
|
||||
hasNextPage: false,
|
||||
hasPreviousPage: false,
|
||||
startCursor: null,
|
||||
},
|
||||
totalCount: 0,
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
cursor: 'WyJmMzNkYzI0Mi01NTE4LTQ1NTMtOTQzMy00MmQ4ZWI4MjgzNGIiXQ==',
|
||||
node: {
|
||||
id: 'f33dc242-5518-4553-9433-42d8eb82834b',
|
||||
name: 'Wework',
|
||||
people: {
|
||||
edges: [],
|
||||
pageInfo: {
|
||||
endCursor: null,
|
||||
hasNextPage: false,
|
||||
hasPreviousPage: false,
|
||||
startCursor: null,
|
||||
},
|
||||
totalCount: 0,
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
cursor: 'WyJmZTI1NmIzOS0zZWMzLTRmZTMtODk5Ny1iNzZhYTBiZmE0MDgiXQ==',
|
||||
node: {
|
||||
id: 'fe256b39-3ec3-4fe3-8997-b76aa0bfa408',
|
||||
name: 'Linkedin',
|
||||
people: {
|
||||
edges: [
|
||||
{
|
||||
cursor:
|
||||
'WyIwYWEwMGJlYi1hYzczLTQ3OTctODI0ZS04N2ExZjVhZWE5ZTAiXQ==',
|
||||
node: {
|
||||
id: '0aa00beb-ac73-4797-824e-87a1f5aea9e0',
|
||||
name: {
|
||||
firstName: 'Sylvie',
|
||||
lastName: 'Palmer',
|
||||
},
|
||||
favorites: {
|
||||
edges: [
|
||||
{
|
||||
cursor:
|
||||
'WyIzN2I5NzE0MC0yNmI5LTQ5OGMtODM3Yi00ZjNkZTQ5OWFkODMiXQ==',
|
||||
node: {
|
||||
id: '37b97140-26b9-498c-837b-4f3de499ad83',
|
||||
personId: '0aa00beb-ac73-4797-824e-87a1f5aea9e0',
|
||||
companyId: null,
|
||||
position: 1,
|
||||
},
|
||||
},
|
||||
],
|
||||
pageInfo: {
|
||||
endCursor:
|
||||
'WyIzN2I5NzE0MC0yNmI5LTQ5OGMtODM3Yi00ZjNkZTQ5OWFkODMiXQ==',
|
||||
hasNextPage: false,
|
||||
hasPreviousPage: false,
|
||||
startCursor:
|
||||
'WyIzN2I5NzE0MC0yNmI5LTQ5OGMtODM3Yi00ZjNkZTQ5OWFkODMiXQ==',
|
||||
},
|
||||
totalCount: 1,
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
cursor:
|
||||
'WyI4NjA4MzE0MS0xYzBlLTQ5NGMtYTFiNi04NWIxYzZmZWZhYTUiXQ==',
|
||||
node: {
|
||||
id: '86083141-1c0e-494c-a1b6-85b1c6fefaa5',
|
||||
name: {
|
||||
firstName: 'Christoph',
|
||||
lastName: 'Callisto',
|
||||
},
|
||||
favorites: {
|
||||
edges: [],
|
||||
pageInfo: {
|
||||
endCursor: null,
|
||||
hasNextPage: false,
|
||||
hasPreviousPage: false,
|
||||
startCursor: null,
|
||||
},
|
||||
totalCount: 0,
|
||||
},
|
||||
},
|
||||
},
|
||||
],
|
||||
pageInfo: {
|
||||
endCursor:
|
||||
'WyI4NjA4MzE0MS0xYzBlLTQ5NGMtYTFiNi04NWIxYzZmZWZhYTUiXQ==',
|
||||
hasNextPage: false,
|
||||
hasPreviousPage: false,
|
||||
startCursor:
|
||||
'WyIwYWEwMGJlYi1hYzczLTQ3OTctODI0ZS04N2ExZjVhZWE5ZTAiXQ==',
|
||||
},
|
||||
totalCount: 2,
|
||||
},
|
||||
},
|
||||
},
|
||||
],
|
||||
totalCount: 13,
|
||||
};
|
||||
|
||||
export const peopleWithTheirUniqueCompanies: ObjectRecordConnection<
|
||||
Pick<Person, 'id'> & { company: Pick<Company, 'id' | 'name'> }
|
||||
> = {
|
||||
pageInfo: {
|
||||
endCursor: 'WyJlZWVhY2FjZi1lZWUxLTQ2OTAtYWQyYy04NjE5ZTViNTZhMmUiXQ==',
|
||||
hasNextPage: false,
|
||||
hasPreviousPage: false,
|
||||
startCursor: 'WyIwYWEwMGJlYi1hYzczLTQ3OTctODI0ZS04N2ExZjVhZWE5ZTAiXQ==',
|
||||
},
|
||||
totalCount: 15,
|
||||
edges: [
|
||||
{
|
||||
cursor: 'WyIwYWEwMGJlYi1hYzczLTQ3OTctODI0ZS04N2ExZjVhZWE5ZTAiXQ==',
|
||||
node: {
|
||||
id: '0aa00beb-ac73-4797-824e-87a1f5aea9e0',
|
||||
company: {
|
||||
id: 'fe256b39-3ec3-4fe3-8997-b76aa0bfa408',
|
||||
name: 'Linkedin',
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
cursor: 'WyIxZDE1MTg1Mi00OTBmLTQ0NjYtODM5MS03MzNjZmQ2NmEwYzgiXQ==',
|
||||
node: {
|
||||
id: '1d151852-490f-4466-8391-733cfd66a0c8',
|
||||
company: {
|
||||
id: '460b6fb1-ed89-413a-b31a-962986e67bb4',
|
||||
name: 'Microsoft',
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
cursor: 'WyIyNDBkYTJlYy0yZDQwLTRlNDktOGRmNC05YzZhMDQ5MTkwZGYiXQ==',
|
||||
node: {
|
||||
id: '240da2ec-2d40-4e49-8df4-9c6a049190df',
|
||||
company: {
|
||||
id: '0d940997-c21e-4ec2-873b-de4264d89025',
|
||||
name: 'Google',
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
cursor: 'WyIyNDBkYTJlYy0yZDQwLTRlNDktOGRmNC05YzZhMDQ5MTkwZWYiXQ==',
|
||||
node: {
|
||||
id: '240da2ec-2d40-4e49-8df4-9c6a049190ef',
|
||||
company: {
|
||||
id: '0d940997-c21e-4ec2-873b-de4264d89025',
|
||||
name: 'Google',
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
cursor: 'WyIyNDBkYTJlYy0yZDQwLTRlNDktOGRmNC05YzZhMDQ5MTkxZGUiXQ==',
|
||||
node: {
|
||||
id: '240da2ec-2d40-4e49-8df4-9c6a049191de',
|
||||
company: {
|
||||
id: 'a7bc68d5-f79e-40dd-bd06-c36e6abb4678',
|
||||
name: 'Samsung',
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
cursor: 'WyIyNDBkYTJlYy0yZDQwLTRlNDktOGRmNC05YzZhMDQ5MTkxZGYiXQ==',
|
||||
node: {
|
||||
id: '240da2ec-2d40-4e49-8df4-9c6a049191df',
|
||||
company: {
|
||||
id: 'a674fa6c-1455-4c57-afaf-dd5dc086361d',
|
||||
name: 'Algolia',
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
cursor: 'WyI1Njk1NTQyMi01ZDU0LTQxYjctYmEzNi1mMGQyMGUxNDE3YWUiXQ==',
|
||||
node: {
|
||||
id: '56955422-5d54-41b7-ba36-f0d20e1417ae',
|
||||
company: {
|
||||
id: '0d940997-c21e-4ec2-873b-de4264d89025',
|
||||
name: 'Google',
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
cursor: 'WyI3NTUwMzVkYi02MjNkLTQxZmUtOTJlNy1kZDQ1YjdjNTY4ZTEiXQ==',
|
||||
node: {
|
||||
id: '755035db-623d-41fe-92e7-dd45b7c568e1',
|
||||
company: {
|
||||
id: '0d940997-c21e-4ec2-873b-de4264d89025',
|
||||
name: 'Google',
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
cursor: 'WyI4NjA4MzE0MS0xYzBlLTQ5NGMtYTFiNi04NWIxYzZmZWZhYTUiXQ==',
|
||||
node: {
|
||||
id: '86083141-1c0e-494c-a1b6-85b1c6fefaa5',
|
||||
company: {
|
||||
id: 'fe256b39-3ec3-4fe3-8997-b76aa0bfa408',
|
||||
name: 'Linkedin',
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
cursor: 'WyI5M2M3MmQyZS1mNTE3LTQyZmQtODBhZS0xNDE3M2IzYjcwYWUiXQ==',
|
||||
node: {
|
||||
id: '93c72d2e-f517-42fd-80ae-14173b3b70ae',
|
||||
company: {
|
||||
id: '118995f3-5d81-46d6-bf83-f7fd33ea6102',
|
||||
name: 'Facebook',
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
cursor: 'WyI5ODQwNmUyNi04MGYxLTRkZmYtYjU3MC1hNzQ5NDI1MjhkZTMiXQ==',
|
||||
node: {
|
||||
id: '98406e26-80f1-4dff-b570-a74942528de3',
|
||||
company: {
|
||||
id: '460b6fb1-ed89-413a-b31a-962986e67bb4',
|
||||
name: 'Microsoft',
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
cursor: 'WyI5YjMyNGE4OC02Nzg0LTQ0NDktYWZkZi1kYzYyY2I4NzAyZjIiXQ==',
|
||||
node: {
|
||||
id: '9b324a88-6784-4449-afdf-dc62cb8702f2',
|
||||
company: {
|
||||
id: '460b6fb1-ed89-413a-b31a-962986e67bb4',
|
||||
name: 'Microsoft',
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
cursor: 'WyJhMmU3OGE1Zi0zMzhiLTQ2ZGYtODgxMS1mYTA4YzdkMTlkMzUiXQ==',
|
||||
node: {
|
||||
id: 'a2e78a5f-338b-46df-8811-fa08c7d19d35',
|
||||
company: {
|
||||
id: '0d940997-c21e-4ec2-873b-de4264d89025',
|
||||
name: 'Google',
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
cursor: 'WyJjYTFmNWJmMy02NGFkLTRiMGUtYmJmZC1lOWZkNzk1YjcwMTYiXQ==',
|
||||
node: {
|
||||
id: 'ca1f5bf3-64ad-4b0e-bbfd-e9fd795b7016',
|
||||
company: {
|
||||
id: '0d940997-c21e-4ec2-873b-de4264d89025',
|
||||
name: 'Google',
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
cursor: 'WyJlZWVhY2FjZi1lZWUxLTQ2OTAtYWQyYy04NjE5ZTViNTZhMmUiXQ==',
|
||||
node: {
|
||||
id: 'eeeacacf-eee1-4690-ad2c-8619e5b56a2e',
|
||||
company: {
|
||||
id: '118995f3-5d81-46d6-bf83-f7fd33ea6102',
|
||||
name: 'Facebook',
|
||||
},
|
||||
},
|
||||
},
|
||||
],
|
||||
};
|
||||
@ -53,7 +53,6 @@ describe('useCreateOneRecord', () => {
|
||||
|
||||
await act(async () => {
|
||||
const res = await result.current.createOneRecord(input);
|
||||
console.log('res', res);
|
||||
expect(res).toBeDefined();
|
||||
expect(res).toHaveProperty('id', personId);
|
||||
});
|
||||
|
||||
@ -84,14 +84,5 @@ describe('useFindManyRecords', () => {
|
||||
expect(result.current.loading).toBe(true);
|
||||
expect(result.current.error).toBeUndefined();
|
||||
expect(result.current.records.length).toBe(0);
|
||||
|
||||
// FATAL ERROR: Ineffective mark-compacts near heap limit Allocation failed - JavaScript heap out of memory
|
||||
// await waitFor(() => {
|
||||
// expect(result.current.loading).toBe(false);
|
||||
// expect(result.current.records).toBeDefined();
|
||||
|
||||
// console.log({ res: result.current.records });
|
||||
// expect(result.current.records.length > 0).toBe(true);
|
||||
// });
|
||||
});
|
||||
});
|
||||
|
||||
@ -3,7 +3,7 @@ import { renderHook } from '@testing-library/react';
|
||||
import { RecoilRoot } from 'recoil';
|
||||
|
||||
import { getObjectMetadataItemsMock } from '@/object-metadata/utils/getObjectMetadataItemsMock';
|
||||
import { useGenerateFindManyRecordsForMultipleMetadataItemsQuery } from '@/object-record/hooks/useGenerateFindManyRecordsForMultipleMetadataItemsQuery';
|
||||
import { useGenerateFindManyRecordsForMultipleMetadataItemsQuery } from '@/object-record/multiple-objects/hooks/useGenerateFindManyRecordsForMultipleMetadataItemsQuery';
|
||||
|
||||
const Wrapper = ({ children }: { children: ReactNode }) => (
|
||||
<RecoilRoot>{children}</RecoilRoot>
|
||||
|
||||
@ -1,190 +0,0 @@
|
||||
import { isNonEmptyArray } from '@sniptt/guards';
|
||||
import { renderHook } from '@testing-library/react';
|
||||
|
||||
import { Company } from '@/companies/types/Company';
|
||||
import { objectMetadataItemsState } from '@/object-metadata/states/objectMetadataItemsState';
|
||||
import { CoreObjectNameSingular } from '@/object-metadata/types/CoreObjectNameSingular';
|
||||
import { getObjectMetadataItemsMock } from '@/object-metadata/utils/getObjectMetadataItemsMock';
|
||||
import {
|
||||
companiesConnectionWithPeopleConnectionWithFavoritesConnectionMock,
|
||||
emptyConnectionMock,
|
||||
peopleWithTheirUniqueCompanies,
|
||||
} from '@/object-record/hooks/__mocks__/useMapConnectionToRecords';
|
||||
import { useMapConnectionToRecords } from '@/object-record/hooks/useMapConnectionToRecords';
|
||||
import { Person } from '@/people/types/Person';
|
||||
import { getJestHookWrapper } from '~/testing/jest/getJestHookWrapper';
|
||||
import { isDefined } from '~/utils/isDefined';
|
||||
|
||||
const Wrapper = getJestHookWrapper({
|
||||
apolloMocks: [],
|
||||
onInitializeRecoilSnapshot: (snapshot) => {
|
||||
snapshot.set(objectMetadataItemsState, getObjectMetadataItemsMock());
|
||||
},
|
||||
});
|
||||
|
||||
describe('useMapConnectionToRecords', () => {
|
||||
it('Empty edges - should return an empty array if no edge', async () => {
|
||||
const { result } = renderHook(
|
||||
() => {
|
||||
const mapConnectionToRecords = useMapConnectionToRecords();
|
||||
|
||||
const records = mapConnectionToRecords({
|
||||
objectNameSingular: CoreObjectNameSingular.Company,
|
||||
objectRecordConnection: emptyConnectionMock,
|
||||
depth: 5,
|
||||
});
|
||||
|
||||
return records;
|
||||
},
|
||||
{
|
||||
wrapper: Wrapper,
|
||||
},
|
||||
);
|
||||
|
||||
expect(Array.isArray(result.current)).toBe(true);
|
||||
});
|
||||
|
||||
it('No relation fields - should return an array of company records', async () => {
|
||||
const { result } = renderHook(
|
||||
() => {
|
||||
const mapConnectionToRecords = useMapConnectionToRecords();
|
||||
|
||||
const records = mapConnectionToRecords({
|
||||
objectNameSingular: CoreObjectNameSingular.Company,
|
||||
objectRecordConnection:
|
||||
companiesConnectionWithPeopleConnectionWithFavoritesConnectionMock,
|
||||
depth: 5,
|
||||
});
|
||||
|
||||
return records;
|
||||
},
|
||||
{
|
||||
wrapper: Wrapper,
|
||||
},
|
||||
);
|
||||
|
||||
expect(Array.isArray(result.current)).toBe(true);
|
||||
});
|
||||
|
||||
it('n+1 relation fields - should return an array of company records with their people records', async () => {
|
||||
const { result } = renderHook(
|
||||
() => {
|
||||
const mapConnectionToRecords = useMapConnectionToRecords();
|
||||
|
||||
const records = mapConnectionToRecords({
|
||||
objectNameSingular: CoreObjectNameSingular.Company,
|
||||
objectRecordConnection:
|
||||
companiesConnectionWithPeopleConnectionWithFavoritesConnectionMock,
|
||||
depth: 5,
|
||||
});
|
||||
|
||||
return records;
|
||||
},
|
||||
{
|
||||
wrapper: Wrapper,
|
||||
},
|
||||
);
|
||||
|
||||
const secondCompanyMock =
|
||||
companiesConnectionWithPeopleConnectionWithFavoritesConnectionMock
|
||||
.edges[1];
|
||||
|
||||
const secondCompanyPeopleMock = secondCompanyMock.node.people.edges.map(
|
||||
(edge) => edge.node,
|
||||
);
|
||||
|
||||
const companiesResult = result.current;
|
||||
const secondCompanyResult = result.current[1];
|
||||
const secondCompanyPeopleResult = secondCompanyResult.people;
|
||||
|
||||
expect(isNonEmptyArray(companiesResult)).toBe(true);
|
||||
expect(secondCompanyResult.id).toBe(secondCompanyMock.node.id);
|
||||
expect(isNonEmptyArray(secondCompanyPeopleResult)).toBe(true);
|
||||
expect(secondCompanyPeopleResult[0].id).toEqual(
|
||||
secondCompanyPeopleMock[0].id,
|
||||
);
|
||||
});
|
||||
|
||||
it('n+2 relation fields - should return an array of company records with their people records with their favorites records', async () => {
|
||||
const { result } = renderHook(
|
||||
() => {
|
||||
const mapConnectionToRecords = useMapConnectionToRecords();
|
||||
|
||||
const records = mapConnectionToRecords({
|
||||
objectNameSingular: CoreObjectNameSingular.Company,
|
||||
objectRecordConnection:
|
||||
companiesConnectionWithPeopleConnectionWithFavoritesConnectionMock,
|
||||
depth: 5,
|
||||
});
|
||||
|
||||
return records;
|
||||
},
|
||||
{
|
||||
wrapper: Wrapper,
|
||||
},
|
||||
);
|
||||
|
||||
const secondCompanyMock =
|
||||
companiesConnectionWithPeopleConnectionWithFavoritesConnectionMock
|
||||
.edges[1];
|
||||
|
||||
const secondCompanyPeopleMock = secondCompanyMock.node.people;
|
||||
|
||||
const secondCompanyFirstPersonMock = secondCompanyPeopleMock.edges[0].node;
|
||||
|
||||
const secondCompanyFirstPersonFavoritesMock =
|
||||
secondCompanyFirstPersonMock.favorites;
|
||||
|
||||
const companiesResult = result.current;
|
||||
const secondCompanyResult = companiesResult[1];
|
||||
const secondCompanyPeopleResult = secondCompanyResult.people;
|
||||
const secondCompanyFirstPersonResult = secondCompanyPeopleResult[0];
|
||||
const secondCompanyFirstPersonFavoritesResult =
|
||||
secondCompanyFirstPersonResult.favorites;
|
||||
|
||||
expect(isNonEmptyArray(companiesResult)).toBe(true);
|
||||
expect(secondCompanyResult.id).toBe(secondCompanyMock.node.id);
|
||||
expect(isNonEmptyArray(secondCompanyPeopleResult)).toBe(true);
|
||||
expect(secondCompanyFirstPersonResult.id).toEqual(
|
||||
secondCompanyFirstPersonMock.id,
|
||||
);
|
||||
expect(isNonEmptyArray(secondCompanyFirstPersonFavoritesResult)).toBe(true);
|
||||
expect(secondCompanyFirstPersonFavoritesResult[0].id).toEqual(
|
||||
secondCompanyFirstPersonFavoritesMock.edges[0].node.id,
|
||||
);
|
||||
});
|
||||
|
||||
it("n+1 relation field TO_ONE_OBJECT - should return an array of people records with their company, mapConnectionToRecords shouldn't try to parse TO_ONE_OBJECT", async () => {
|
||||
const { result } = renderHook(
|
||||
() => {
|
||||
const mapConnectionToRecords = useMapConnectionToRecords();
|
||||
|
||||
const records = mapConnectionToRecords({
|
||||
objectNameSingular: CoreObjectNameSingular.Person,
|
||||
objectRecordConnection: peopleWithTheirUniqueCompanies,
|
||||
depth: 5,
|
||||
});
|
||||
|
||||
return records as (Person & { company: Company })[];
|
||||
},
|
||||
{
|
||||
wrapper: Wrapper,
|
||||
},
|
||||
);
|
||||
|
||||
const firstPersonMock = peopleWithTheirUniqueCompanies.edges[0].node;
|
||||
|
||||
const firstPersonsCompanyMock = firstPersonMock.company;
|
||||
|
||||
const peopleResult = result.current;
|
||||
|
||||
const firstPersonResult = result.current[0];
|
||||
const firstPersonsCompanyresult = firstPersonResult.company;
|
||||
|
||||
expect(isNonEmptyArray(peopleResult)).toBe(true);
|
||||
expect(firstPersonResult.id).toBe(firstPersonMock.id);
|
||||
|
||||
expect(isDefined(firstPersonsCompanyresult)).toBe(true);
|
||||
expect(firstPersonsCompanyresult.id).toEqual(firstPersonsCompanyMock.id);
|
||||
});
|
||||
});
|
||||
@ -1,52 +0,0 @@
|
||||
import { ReactNode } from 'react';
|
||||
import { useApolloClient } from '@apollo/client';
|
||||
import { MockedProvider } from '@apollo/client/testing';
|
||||
import { act, renderHook } from '@testing-library/react';
|
||||
import { RecoilRoot } from 'recoil';
|
||||
|
||||
import { getObjectMetadataItemsMock } from '@/object-metadata/utils/getObjectMetadataItemsMock';
|
||||
import { useModifyRecordFromCache } from '@/object-record/cache/hooks/useModifyRecordFromCache';
|
||||
|
||||
const Wrapper = ({ children }: { children: ReactNode }) => (
|
||||
<MockedProvider addTypename={false}>
|
||||
<RecoilRoot>{children}</RecoilRoot>
|
||||
</MockedProvider>
|
||||
);
|
||||
|
||||
const recordId = '91408718-a29f-4678-b573-c791e8664c2a';
|
||||
|
||||
describe('useModifyRecordFromCache', () => {
|
||||
it('should work as expected', async () => {
|
||||
const { result } = renderHook(
|
||||
() => {
|
||||
const apolloClient = useApolloClient();
|
||||
const mockObjectMetadataItems = getObjectMetadataItemsMock();
|
||||
|
||||
const personMetadataItem = mockObjectMetadataItems.find(
|
||||
(item) => item.nameSingular === 'person',
|
||||
)!;
|
||||
|
||||
return {
|
||||
modifyRecordFromCache: useModifyRecordFromCache({
|
||||
objectMetadataItem: personMetadataItem,
|
||||
}),
|
||||
cache: apolloClient.cache,
|
||||
};
|
||||
},
|
||||
{
|
||||
wrapper: Wrapper,
|
||||
},
|
||||
);
|
||||
|
||||
const spy = jest.spyOn(result.current.cache, 'modify');
|
||||
|
||||
act(() => {
|
||||
result.current.modifyRecordFromCache(recordId, {});
|
||||
});
|
||||
|
||||
expect(spy).toHaveBeenCalledWith({
|
||||
id: `Person:${recordId}`,
|
||||
fields: {},
|
||||
});
|
||||
});
|
||||
});
|
||||
@ -2,57 +2,87 @@ import { useApolloClient } from '@apollo/client';
|
||||
import { v4 } from 'uuid';
|
||||
|
||||
import { triggerCreateRecordsOptimisticEffect } from '@/apollo/optimistic-effect/utils/triggerCreateRecordsOptimisticEffect';
|
||||
import { CachedObjectRecord } from '@/apollo/types/CachedObjectRecord';
|
||||
import { useObjectMetadataItem } from '@/object-metadata/hooks/useObjectMetadataItem';
|
||||
import { useObjectMetadataItems } from '@/object-metadata/hooks/useObjectMetadataItems';
|
||||
import { ObjectMetadataItemIdentifier } from '@/object-metadata/types/ObjectMetadataItemIdentifier';
|
||||
import { useGenerateObjectRecordOptimisticResponse } from '@/object-record/cache/hooks/useGenerateObjectRecordOptimisticResponse';
|
||||
import { getCreateManyRecordsMutationResponseField } from '@/object-record/hooks/useGenerateCreateManyRecordMutation';
|
||||
import { useCreateOneRecordInCache } from '@/object-record/cache/hooks/useCreateOneRecordInCache';
|
||||
import {
|
||||
getCreateManyRecordsMutationResponseField,
|
||||
useGenerateCreateManyRecordMutation,
|
||||
} from '@/object-record/hooks/useGenerateCreateManyRecordMutation';
|
||||
import { ObjectRecord } from '@/object-record/types/ObjectRecord';
|
||||
import { sanitizeRecordInput } from '@/object-record/utils/sanitizeRecordInput';
|
||||
import { isDefined } from '~/utils/isDefined';
|
||||
|
||||
type CreateManyRecordsOptions = {
|
||||
skipOptimisticEffect?: boolean;
|
||||
type useCreateManyRecordsProps = {
|
||||
objectNameSingular: string;
|
||||
queryFields?: Record<string, any>;
|
||||
depth?: number;
|
||||
skipPostOptmisticEffect?: boolean;
|
||||
};
|
||||
|
||||
export const useCreateManyRecords = <
|
||||
CreatedObjectRecord extends ObjectRecord = ObjectRecord,
|
||||
>({
|
||||
objectNameSingular,
|
||||
}: ObjectMetadataItemIdentifier) => {
|
||||
queryFields,
|
||||
depth = 1,
|
||||
skipPostOptmisticEffect = false,
|
||||
}: useCreateManyRecordsProps) => {
|
||||
const apolloClient = useApolloClient();
|
||||
|
||||
const { objectMetadataItem, createManyRecordsMutation } =
|
||||
useObjectMetadataItem({
|
||||
objectNameSingular,
|
||||
});
|
||||
const { objectMetadataItem } = useObjectMetadataItem({
|
||||
objectNameSingular,
|
||||
});
|
||||
|
||||
const { generateObjectRecordOptimisticResponse } =
|
||||
useGenerateObjectRecordOptimisticResponse({
|
||||
objectMetadataItem,
|
||||
});
|
||||
const createManyRecordsMutation = useGenerateCreateManyRecordMutation({
|
||||
objectMetadataItem,
|
||||
queryFields,
|
||||
depth,
|
||||
});
|
||||
|
||||
const createOneRecordInCache = useCreateOneRecordInCache<CachedObjectRecord>({
|
||||
objectMetadataItem,
|
||||
});
|
||||
|
||||
const { objectMetadataItems } = useObjectMetadataItems();
|
||||
|
||||
const createManyRecords = async (
|
||||
data: Partial<CreatedObjectRecord>[],
|
||||
options?: CreateManyRecordsOptions,
|
||||
recordsToCreate: Partial<CreatedObjectRecord>[],
|
||||
) => {
|
||||
const sanitizedCreateManyRecordsInput = data.map((input) => {
|
||||
const idForCreation = input.id ?? v4();
|
||||
const sanitizedCreateManyRecordsInput = recordsToCreate.map(
|
||||
(recordToCreate) => {
|
||||
const idForCreation = recordToCreate?.id ?? v4();
|
||||
|
||||
const sanitizedRecordInput = sanitizeRecordInput({
|
||||
objectMetadataItem,
|
||||
recordInput: { ...input, id: idForCreation },
|
||||
});
|
||||
|
||||
return sanitizedRecordInput;
|
||||
});
|
||||
|
||||
const optimisticallyCreatedRecords = sanitizedCreateManyRecordsInput.map(
|
||||
(record) =>
|
||||
generateObjectRecordOptimisticResponse<CreatedObjectRecord>(record),
|
||||
return {
|
||||
...sanitizeRecordInput({
|
||||
objectMetadataItem,
|
||||
recordInput: recordToCreate,
|
||||
}),
|
||||
id: idForCreation,
|
||||
};
|
||||
},
|
||||
);
|
||||
|
||||
const recordsCreatedInCache = [];
|
||||
|
||||
for (const recordToCreate of sanitizedCreateManyRecordsInput) {
|
||||
const recordCreatedInCache = createOneRecordInCache(recordToCreate);
|
||||
|
||||
if (isDefined(recordCreatedInCache)) {
|
||||
recordsCreatedInCache.push(recordCreatedInCache);
|
||||
}
|
||||
}
|
||||
|
||||
if (recordsCreatedInCache.length > 0) {
|
||||
triggerCreateRecordsOptimisticEffect({
|
||||
cache: apolloClient.cache,
|
||||
objectMetadataItem,
|
||||
recordsToCreate: recordsCreatedInCache,
|
||||
objectMetadataItems,
|
||||
});
|
||||
}
|
||||
|
||||
const mutationResponseField = getCreateManyRecordsMutationResponseField(
|
||||
objectMetadataItem.namePlural,
|
||||
);
|
||||
@ -62,25 +92,18 @@ export const useCreateManyRecords = <
|
||||
variables: {
|
||||
data: sanitizedCreateManyRecordsInput,
|
||||
},
|
||||
optimisticResponse: options?.skipOptimisticEffect
|
||||
? undefined
|
||||
: {
|
||||
[mutationResponseField]: optimisticallyCreatedRecords,
|
||||
},
|
||||
update: options?.skipOptimisticEffect
|
||||
? undefined
|
||||
: (cache, { data }) => {
|
||||
const records = data?.[mutationResponseField];
|
||||
update: (cache, { data }) => {
|
||||
const records = data?.[mutationResponseField];
|
||||
|
||||
if (!records?.length) return;
|
||||
if (!records?.length || skipPostOptmisticEffect) return;
|
||||
|
||||
triggerCreateRecordsOptimisticEffect({
|
||||
cache,
|
||||
objectMetadataItem,
|
||||
recordsToCreate: records,
|
||||
objectMetadataItems,
|
||||
});
|
||||
},
|
||||
triggerCreateRecordsOptimisticEffect({
|
||||
cache,
|
||||
objectMetadataItem,
|
||||
recordsToCreate: records,
|
||||
objectMetadataItems,
|
||||
});
|
||||
},
|
||||
});
|
||||
|
||||
return createdObjects.data?.[mutationResponseField] ?? [];
|
||||
|
||||
@ -1,49 +0,0 @@
|
||||
import { v4 } from 'uuid';
|
||||
|
||||
import { useObjectMetadataItem } from '@/object-metadata/hooks/useObjectMetadataItem';
|
||||
import { ObjectMetadataItemIdentifier } from '@/object-metadata/types/ObjectMetadataItemIdentifier';
|
||||
import { useAddRecordInCache } from '@/object-record/cache/hooks/useAddRecordInCache';
|
||||
import { useGenerateObjectRecordOptimisticResponse } from '@/object-record/cache/hooks/useGenerateObjectRecordOptimisticResponse';
|
||||
import { ObjectRecord } from '@/object-record/types/ObjectRecord';
|
||||
import { isDefined } from '~/utils/isDefined';
|
||||
|
||||
export const useCreateManyRecordsInCache = <T extends ObjectRecord>({
|
||||
objectNameSingular,
|
||||
}: ObjectMetadataItemIdentifier) => {
|
||||
const { objectMetadataItem } = useObjectMetadataItem({
|
||||
objectNameSingular,
|
||||
});
|
||||
|
||||
const { generateObjectRecordOptimisticResponse } =
|
||||
useGenerateObjectRecordOptimisticResponse({
|
||||
objectMetadataItem,
|
||||
});
|
||||
|
||||
const addRecordInCache = useAddRecordInCache({
|
||||
objectMetadataItem,
|
||||
});
|
||||
|
||||
const createManyRecordsInCache = (data: Partial<T>[]) => {
|
||||
const recordsWithId = data.map((record) => ({
|
||||
...record,
|
||||
id: (record.id as string) ?? v4(),
|
||||
}));
|
||||
|
||||
const createdRecordsInCache = [] as T[];
|
||||
|
||||
for (const record of recordsWithId) {
|
||||
const generatedCachedObjectRecord =
|
||||
generateObjectRecordOptimisticResponse<T>(record);
|
||||
|
||||
if (isDefined(generatedCachedObjectRecord)) {
|
||||
addRecordInCache(generatedCachedObjectRecord);
|
||||
|
||||
createdRecordsInCache.push(generatedCachedObjectRecord);
|
||||
}
|
||||
}
|
||||
|
||||
return createdRecordsInCache;
|
||||
};
|
||||
|
||||
return { createManyRecordsInCache };
|
||||
};
|
||||
@ -2,55 +2,73 @@ import { useApolloClient } from '@apollo/client';
|
||||
import { v4 } from 'uuid';
|
||||
|
||||
import { triggerCreateRecordsOptimisticEffect } from '@/apollo/optimistic-effect/utils/triggerCreateRecordsOptimisticEffect';
|
||||
import { CachedObjectRecord } from '@/apollo/types/CachedObjectRecord';
|
||||
import { useObjectMetadataItem } from '@/object-metadata/hooks/useObjectMetadataItem';
|
||||
import { useObjectMetadataItems } from '@/object-metadata/hooks/useObjectMetadataItems';
|
||||
import { useGenerateObjectRecordOptimisticResponse } from '@/object-record/cache/hooks/useGenerateObjectRecordOptimisticResponse';
|
||||
import { getCreateOneRecordMutationResponseField } from '@/object-record/hooks/useGenerateCreateOneRecordMutation';
|
||||
import { useCreateOneRecordInCache } from '@/object-record/cache/hooks/useCreateOneRecordInCache';
|
||||
import {
|
||||
getCreateOneRecordMutationResponseField,
|
||||
useGenerateCreateOneRecordMutation,
|
||||
} from '@/object-record/hooks/useGenerateCreateOneRecordMutation';
|
||||
import { ObjectRecord } from '@/object-record/types/ObjectRecord';
|
||||
import { sanitizeRecordInput } from '@/object-record/utils/sanitizeRecordInput';
|
||||
import { isDefined } from '~/utils/isDefined';
|
||||
|
||||
type useCreateOneRecordProps = {
|
||||
objectNameSingular: string;
|
||||
};
|
||||
|
||||
type CreateOneRecordOptions = {
|
||||
skipOptimisticEffect?: boolean;
|
||||
queryFields?: Record<string, any>;
|
||||
depth?: number;
|
||||
skipPostOptmisticEffect?: boolean;
|
||||
};
|
||||
|
||||
export const useCreateOneRecord = <
|
||||
CreatedObjectRecord extends ObjectRecord = ObjectRecord,
|
||||
>({
|
||||
objectNameSingular,
|
||||
queryFields,
|
||||
depth = 1,
|
||||
skipPostOptmisticEffect = false,
|
||||
}: useCreateOneRecordProps) => {
|
||||
const apolloClient = useApolloClient();
|
||||
|
||||
const { objectMetadataItem, createOneRecordMutation } = useObjectMetadataItem(
|
||||
{ objectNameSingular },
|
||||
);
|
||||
const { objectMetadataItem } = useObjectMetadataItem({ objectNameSingular });
|
||||
|
||||
const { generateObjectRecordOptimisticResponse } =
|
||||
useGenerateObjectRecordOptimisticResponse({
|
||||
objectMetadataItem,
|
||||
});
|
||||
const createOneRecordMutation = useGenerateCreateOneRecordMutation({
|
||||
objectMetadataItem,
|
||||
queryFields,
|
||||
depth,
|
||||
});
|
||||
|
||||
const createOneRecordInCache = useCreateOneRecordInCache<CachedObjectRecord>({
|
||||
objectMetadataItem,
|
||||
});
|
||||
|
||||
const { objectMetadataItems } = useObjectMetadataItems();
|
||||
|
||||
const createOneRecord = async (
|
||||
input: Partial<CreatedObjectRecord>,
|
||||
options?: CreateOneRecordOptions,
|
||||
) => {
|
||||
const createOneRecord = async (input: Partial<CreatedObjectRecord>) => {
|
||||
const idForCreation = input.id ?? v4();
|
||||
|
||||
const sanitizedCreateOneRecordInput = sanitizeRecordInput({
|
||||
objectMetadataItem,
|
||||
recordInput: { ...input, id: idForCreation },
|
||||
const sanitizedInput = {
|
||||
...sanitizeRecordInput({
|
||||
objectMetadataItem,
|
||||
recordInput: input,
|
||||
}),
|
||||
id: idForCreation,
|
||||
};
|
||||
|
||||
const recordCreatedInCache = createOneRecordInCache({
|
||||
...input,
|
||||
id: idForCreation,
|
||||
});
|
||||
|
||||
const optimisticallyCreatedRecord =
|
||||
generateObjectRecordOptimisticResponse<CreatedObjectRecord>({
|
||||
...input,
|
||||
...sanitizedCreateOneRecordInput,
|
||||
if (isDefined(recordCreatedInCache)) {
|
||||
triggerCreateRecordsOptimisticEffect({
|
||||
cache: apolloClient.cache,
|
||||
objectMetadataItem,
|
||||
recordsToCreate: [recordCreatedInCache],
|
||||
objectMetadataItems,
|
||||
});
|
||||
}
|
||||
|
||||
const mutationResponseField =
|
||||
getCreateOneRecordMutationResponseField(objectNameSingular);
|
||||
@ -58,27 +76,20 @@ export const useCreateOneRecord = <
|
||||
const createdObject = await apolloClient.mutate({
|
||||
mutation: createOneRecordMutation,
|
||||
variables: {
|
||||
input: sanitizedCreateOneRecordInput,
|
||||
input: sanitizedInput,
|
||||
},
|
||||
optimisticResponse: options?.skipOptimisticEffect
|
||||
? undefined
|
||||
: {
|
||||
[mutationResponseField]: optimisticallyCreatedRecord,
|
||||
},
|
||||
update: options?.skipOptimisticEffect
|
||||
? undefined
|
||||
: (cache, { data }) => {
|
||||
const record = data?.[mutationResponseField];
|
||||
update: (cache, { data }) => {
|
||||
const record = data?.[mutationResponseField];
|
||||
|
||||
if (!record) return;
|
||||
if (!record || skipPostOptmisticEffect) return;
|
||||
|
||||
triggerCreateRecordsOptimisticEffect({
|
||||
cache,
|
||||
objectMetadataItem,
|
||||
recordsToCreate: [record],
|
||||
objectMetadataItems,
|
||||
});
|
||||
},
|
||||
triggerCreateRecordsOptimisticEffect({
|
||||
cache,
|
||||
objectMetadataItem,
|
||||
recordsToCreate: [record],
|
||||
objectMetadataItems,
|
||||
});
|
||||
},
|
||||
});
|
||||
|
||||
return createdObject.data?.[mutationResponseField] ?? null;
|
||||
|
||||
@ -1,38 +0,0 @@
|
||||
import { useObjectMetadataItem } from '@/object-metadata/hooks/useObjectMetadataItem';
|
||||
import { useAddRecordInCache } from '@/object-record/cache/hooks/useAddRecordInCache';
|
||||
import { useGenerateObjectRecordOptimisticResponse } from '@/object-record/cache/hooks/useGenerateObjectRecordOptimisticResponse';
|
||||
import { ObjectRecord } from '@/object-record/types/ObjectRecord';
|
||||
|
||||
type useCreateOneRecordInCacheProps = {
|
||||
objectNameSingular: string;
|
||||
};
|
||||
|
||||
export const useCreateOneRecordInCache = <T>({
|
||||
objectNameSingular,
|
||||
}: useCreateOneRecordInCacheProps) => {
|
||||
const { objectMetadataItem } = useObjectMetadataItem({
|
||||
objectNameSingular,
|
||||
});
|
||||
|
||||
const { generateObjectRecordOptimisticResponse } =
|
||||
useGenerateObjectRecordOptimisticResponse({
|
||||
objectMetadataItem,
|
||||
});
|
||||
|
||||
const addRecordInCache = useAddRecordInCache({
|
||||
objectMetadataItem,
|
||||
});
|
||||
|
||||
const createOneRecordInCache = (input: ObjectRecord) => {
|
||||
const generatedCachedObjectRecord =
|
||||
generateObjectRecordOptimisticResponse(input);
|
||||
|
||||
addRecordInCache(generatedCachedObjectRecord);
|
||||
|
||||
return generatedCachedObjectRecord as T;
|
||||
};
|
||||
|
||||
return {
|
||||
createOneRecordInCache,
|
||||
};
|
||||
};
|
||||
@ -3,8 +3,8 @@ import { useQuery } from '@apollo/client';
|
||||
|
||||
import { useObjectMetadataItem } from '@/object-metadata/hooks/useObjectMetadataItem';
|
||||
import { ObjectMetadataItemIdentifier } from '@/object-metadata/types/ObjectMetadataItemIdentifier';
|
||||
import { getRecordsFromRecordConnection } from '@/object-record/cache/utils/getRecordsFromRecordConnection';
|
||||
import { getFindDuplicateRecordsQueryResponseField } from '@/object-record/hooks/useGenerateFindDuplicateRecordsQuery';
|
||||
import { useMapConnectionToRecords } from '@/object-record/hooks/useMapConnectionToRecords';
|
||||
import { ObjectRecord } from '@/object-record/types/ObjectRecord';
|
||||
import { ObjectRecordConnection } from '@/object-record/types/ObjectRecordConnection';
|
||||
import { useSnackBar } from '@/ui/feedback/snack-bar-manager/hooks/useSnackBar';
|
||||
@ -60,16 +60,14 @@ export const useFindDuplicateRecords = <T extends ObjectRecord = ObjectRecord>({
|
||||
|
||||
const objectRecordConnection = data?.[queryResponseField];
|
||||
|
||||
const mapConnectionToRecords = useMapConnectionToRecords();
|
||||
|
||||
const records = useMemo(
|
||||
() =>
|
||||
mapConnectionToRecords({
|
||||
objectRecordConnection,
|
||||
objectNameSingular,
|
||||
depth: 5,
|
||||
}) as T[],
|
||||
[mapConnectionToRecords, objectRecordConnection, objectNameSingular],
|
||||
objectRecordConnection
|
||||
? (getRecordsFromRecordConnection({
|
||||
recordConnection: objectRecordConnection,
|
||||
}) as T[])
|
||||
: [],
|
||||
[objectRecordConnection],
|
||||
);
|
||||
|
||||
return {
|
||||
|
||||
@ -7,7 +7,7 @@ import { useRecoilState, useRecoilValue, useSetRecoilState } from 'recoil';
|
||||
import { currentWorkspaceMemberState } from '@/auth/states/currentWorkspaceMemberState';
|
||||
import { useObjectMetadataItem } from '@/object-metadata/hooks/useObjectMetadataItem';
|
||||
import { ObjectMetadataItemIdentifier } from '@/object-metadata/types/ObjectMetadataItemIdentifier';
|
||||
import { useMapConnectionToRecords } from '@/object-record/hooks/useMapConnectionToRecords';
|
||||
import { getRecordsFromRecordConnection } from '@/object-record/cache/utils/getRecordsFromRecordConnection';
|
||||
import { ObjectRecord } from '@/object-record/types/ObjectRecord';
|
||||
import { ObjectRecordConnection } from '@/object-record/types/ObjectRecordConnection';
|
||||
import { ObjectRecordEdge } from '@/object-record/types/ObjectRecordEdge';
|
||||
@ -22,7 +22,6 @@ import { cursorFamilyState } from '../states/cursorFamilyState';
|
||||
import { hasNextPageFamilyState } from '../states/hasNextPageFamilyState';
|
||||
import { isFetchingMoreRecordsFamilyState } from '../states/isFetchingMoreRecordsFamilyState';
|
||||
import { ObjectRecordQueryResult } from '../types/ObjectRecordQueryResult';
|
||||
import { mapPaginatedRecordsToRecords } from '../utils/mapPaginatedRecordsToRecords';
|
||||
|
||||
export const useFindManyRecords = <T extends ObjectRecord = ObjectRecord>({
|
||||
objectNameSingular,
|
||||
@ -31,17 +30,20 @@ export const useFindManyRecords = <T extends ObjectRecord = ObjectRecord>({
|
||||
limit,
|
||||
onCompleted,
|
||||
skip,
|
||||
useRecordsWithoutConnection = false,
|
||||
depth,
|
||||
depth = 1,
|
||||
queryFields,
|
||||
}: ObjectMetadataItemIdentifier &
|
||||
ObjectRecordQueryVariables & {
|
||||
onCompleted?: (
|
||||
data: ObjectRecordConnection<T>,
|
||||
pageInfo: ObjectRecordConnection<T>['pageInfo'],
|
||||
records: T[],
|
||||
options?: {
|
||||
pageInfo?: ObjectRecordConnection['pageInfo'];
|
||||
totalCount?: number;
|
||||
},
|
||||
) => void;
|
||||
skip?: boolean;
|
||||
useRecordsWithoutConnection?: boolean;
|
||||
depth?: number;
|
||||
queryFields?: Record<string, any>;
|
||||
}) => {
|
||||
const findManyQueryStateIdentifier =
|
||||
objectNameSingular +
|
||||
@ -66,6 +68,7 @@ export const useFindManyRecords = <T extends ObjectRecord = ObjectRecord>({
|
||||
objectNameSingular,
|
||||
},
|
||||
depth,
|
||||
queryFields,
|
||||
);
|
||||
|
||||
const { enqueueSnackBar } = useSnackBar();
|
||||
@ -81,9 +84,20 @@ export const useFindManyRecords = <T extends ObjectRecord = ObjectRecord>({
|
||||
orderBy,
|
||||
},
|
||||
onCompleted: (data) => {
|
||||
if (!isDefined(data)) {
|
||||
onCompleted?.([]);
|
||||
}
|
||||
|
||||
const pageInfo = data?.[objectMetadataItem.namePlural]?.pageInfo;
|
||||
|
||||
onCompleted?.(data[objectMetadataItem.namePlural], pageInfo);
|
||||
const records = getRecordsFromRecordConnection({
|
||||
recordConnection: data?.[objectMetadataItem.namePlural],
|
||||
}) as T[];
|
||||
|
||||
onCompleted?.(records, {
|
||||
pageInfo,
|
||||
totalCount: data?.[objectMetadataItem.namePlural]?.totalCount,
|
||||
});
|
||||
|
||||
if (isDefined(data?.[objectMetadataItem.namePlural])) {
|
||||
setLastCursor(pageInfo.endCursor ?? '');
|
||||
@ -132,24 +146,24 @@ export const useFindManyRecords = <T extends ObjectRecord = ObjectRecord>({
|
||||
|
||||
const pageInfo =
|
||||
fetchMoreResult?.[objectMetadataItem.namePlural]?.pageInfo;
|
||||
|
||||
if (isDefined(data?.[objectMetadataItem.namePlural])) {
|
||||
setLastCursor(pageInfo.endCursor ?? '');
|
||||
setHasNextPage(pageInfo.hasNextPage ?? false);
|
||||
}
|
||||
|
||||
onCompleted?.(
|
||||
{
|
||||
__typename: `${capitalize(
|
||||
objectMetadataItem.nameSingular,
|
||||
)}Connection`,
|
||||
const records = getRecordsFromRecordConnection({
|
||||
recordConnection: {
|
||||
edges: newEdges,
|
||||
pageInfo:
|
||||
fetchMoreResult?.[objectMetadataItem.namePlural].pageInfo,
|
||||
totalCount:
|
||||
fetchMoreResult?.[objectMetadataItem.namePlural].totalCount,
|
||||
pageInfo,
|
||||
},
|
||||
}) as T[];
|
||||
|
||||
onCompleted?.(records, {
|
||||
pageInfo,
|
||||
);
|
||||
totalCount:
|
||||
fetchMoreResult?.[objectMetadataItem.namePlural]?.totalCount,
|
||||
});
|
||||
|
||||
return Object.assign({}, prev, {
|
||||
[objectMetadataItem.namePlural]: {
|
||||
@ -196,40 +210,23 @@ export const useFindManyRecords = <T extends ObjectRecord = ObjectRecord>({
|
||||
enqueueSnackBar,
|
||||
]);
|
||||
|
||||
// TODO: remove this and use only mapConnectionToRecords when we've finished the refactor
|
||||
const totalCount = data?.[objectMetadataItem.namePlural].totalCount ?? 0;
|
||||
|
||||
const records = useMemo(
|
||||
() =>
|
||||
mapPaginatedRecordsToRecords({
|
||||
pagedRecords: data,
|
||||
objectNamePlural: objectMetadataItem.namePlural,
|
||||
}) as T[],
|
||||
[data, objectMetadataItem],
|
||||
);
|
||||
data?.[objectMetadataItem.namePlural]
|
||||
? getRecordsFromRecordConnection({
|
||||
recordConnection: data?.[objectMetadataItem.namePlural],
|
||||
})
|
||||
: ([] as T[]),
|
||||
|
||||
const mapConnectionToRecords = useMapConnectionToRecords();
|
||||
|
||||
const recordsWithoutConnection = useMemo(
|
||||
() =>
|
||||
useRecordsWithoutConnection
|
||||
? (mapConnectionToRecords({
|
||||
objectRecordConnection: data?.[objectMetadataItem.namePlural],
|
||||
objectNameSingular,
|
||||
depth: 5,
|
||||
}) as T[])
|
||||
: [],
|
||||
[
|
||||
data,
|
||||
objectNameSingular,
|
||||
objectMetadataItem.namePlural,
|
||||
mapConnectionToRecords,
|
||||
useRecordsWithoutConnection,
|
||||
],
|
||||
[data, objectMetadataItem.namePlural],
|
||||
);
|
||||
|
||||
return {
|
||||
objectMetadataItem,
|
||||
records: useRecordsWithoutConnection ? recordsWithoutConnection : records,
|
||||
totalCount: data?.[objectMetadataItem.namePlural].totalCount || 0,
|
||||
records,
|
||||
totalCount,
|
||||
loading,
|
||||
error,
|
||||
fetchMoreRecords,
|
||||
|
||||
@ -1,8 +1,11 @@
|
||||
import { useMemo } from 'react';
|
||||
import { useQuery } from '@apollo/client';
|
||||
|
||||
import { useObjectMetadataItem } from '@/object-metadata/hooks/useObjectMetadataItem';
|
||||
import { ObjectMetadataItemIdentifier } from '@/object-metadata/types/ObjectMetadataItemIdentifier';
|
||||
import { getRecordFromRecordNode } from '@/object-record/cache/utils/getRecordFromRecordNode';
|
||||
import { ObjectRecord } from '@/object-record/types/ObjectRecord';
|
||||
import { isDefined } from '~/utils/isDefined';
|
||||
|
||||
// TODO: fix connection in relation => automatically change to an array
|
||||
export const useFindOneRecord = <T extends ObjectRecord = ObjectRecord>({
|
||||
@ -28,11 +31,29 @@ export const useFindOneRecord = <T extends ObjectRecord = ObjectRecord>({
|
||||
>(findOneRecordQuery, {
|
||||
skip: !objectMetadataItem || !objectRecordId || skip,
|
||||
variables: { objectRecordId },
|
||||
onCompleted: (data) => onCompleted?.(data[objectNameSingular]),
|
||||
onCompleted: (data) => {
|
||||
const recordWithoutConnection = getRecordFromRecordNode({
|
||||
recordNode: { ...data[objectNameSingular] },
|
||||
});
|
||||
|
||||
if (isDefined(recordWithoutConnection)) {
|
||||
onCompleted?.(recordWithoutConnection);
|
||||
}
|
||||
},
|
||||
});
|
||||
|
||||
const recordWithoutConnection = useMemo(
|
||||
() =>
|
||||
data?.[objectNameSingular]
|
||||
? getRecordFromRecordNode({
|
||||
recordNode: data?.[objectNameSingular],
|
||||
})
|
||||
: undefined,
|
||||
[data, objectNameSingular],
|
||||
);
|
||||
|
||||
return {
|
||||
record: data?.[objectNameSingular] || undefined,
|
||||
record: recordWithoutConnection,
|
||||
loading,
|
||||
error,
|
||||
};
|
||||
|
||||
@ -14,8 +14,12 @@ export const getCreateManyRecordsMutationResponseField = (
|
||||
|
||||
export const useGenerateCreateManyRecordMutation = ({
|
||||
objectMetadataItem,
|
||||
queryFields,
|
||||
depth = 1,
|
||||
}: {
|
||||
objectMetadataItem: ObjectMetadataItem;
|
||||
queryFields?: Record<string, any>;
|
||||
depth?: number;
|
||||
}) => {
|
||||
const objectMetadataItems = useRecoilValue(objectMetadataItemsState);
|
||||
|
||||
@ -34,6 +38,8 @@ export const useGenerateCreateManyRecordMutation = ({
|
||||
${mutationResponseField}(data: $data) ${mapObjectMetadataToGraphQLQuery({
|
||||
objectMetadataItems,
|
||||
objectMetadataItem,
|
||||
queryFields,
|
||||
depth,
|
||||
})}
|
||||
}`;
|
||||
};
|
||||
|
||||
@ -14,8 +14,12 @@ export const getCreateOneRecordMutationResponseField = (
|
||||
|
||||
export const useGenerateCreateOneRecordMutation = ({
|
||||
objectMetadataItem,
|
||||
queryFields,
|
||||
depth = 1,
|
||||
}: {
|
||||
objectMetadataItem: ObjectMetadataItem;
|
||||
queryFields?: Record<string, any>;
|
||||
depth?: number;
|
||||
}) => {
|
||||
const objectMetadataItems = useRecoilValue(objectMetadataItemsState);
|
||||
|
||||
@ -34,6 +38,8 @@ export const useGenerateCreateOneRecordMutation = ({
|
||||
${mutationResponseField}(data: $input) ${mapObjectMetadataToGraphQLQuery({
|
||||
objectMetadataItems,
|
||||
objectMetadataItem,
|
||||
queryFields,
|
||||
depth,
|
||||
})}
|
||||
}
|
||||
`;
|
||||
|
||||
@ -12,14 +12,16 @@ export const useGenerateFindManyRecordsQuery = () => {
|
||||
return ({
|
||||
objectMetadataItem,
|
||||
depth,
|
||||
eagerLoadedRelations,
|
||||
queryFields,
|
||||
computeReferences = false,
|
||||
}: {
|
||||
objectMetadataItem: Pick<
|
||||
ObjectMetadataItem,
|
||||
'fields' | 'nameSingular' | 'namePlural'
|
||||
>;
|
||||
depth?: number;
|
||||
eagerLoadedRelations?: Record<string, any>;
|
||||
queryFields?: Record<string, any>;
|
||||
computeReferences?: boolean;
|
||||
}) => gql`
|
||||
query FindMany${capitalize(
|
||||
objectMetadataItem.namePlural,
|
||||
@ -36,7 +38,8 @@ export const useGenerateFindManyRecordsQuery = () => {
|
||||
objectMetadataItems,
|
||||
objectMetadataItem,
|
||||
depth,
|
||||
eagerLoadedRelations,
|
||||
queryFields,
|
||||
computeReferences,
|
||||
})}
|
||||
cursor
|
||||
}
|
||||
|
||||
@ -14,8 +14,12 @@ export const getUpdateOneRecordMutationResponseField = (
|
||||
|
||||
export const useGenerateUpdateOneRecordMutation = ({
|
||||
objectMetadataItem,
|
||||
depth = 1,
|
||||
computeReferences = false,
|
||||
}: {
|
||||
objectMetadataItem: ObjectMetadataItem;
|
||||
depth?: number;
|
||||
computeReferences?: boolean;
|
||||
}) => {
|
||||
const objectMetadataItems = useRecoilValue(objectMetadataItemsState);
|
||||
|
||||
@ -35,6 +39,8 @@ export const useGenerateUpdateOneRecordMutation = ({
|
||||
{
|
||||
objectMetadataItems,
|
||||
objectMetadataItem,
|
||||
depth,
|
||||
computeReferences,
|
||||
},
|
||||
)}
|
||||
}
|
||||
|
||||
@ -1,113 +0,0 @@
|
||||
import { useCallback } from 'react';
|
||||
import { isNonEmptyArray } from '@sniptt/guards';
|
||||
import { produce } from 'immer';
|
||||
import { useRecoilValue } from 'recoil';
|
||||
|
||||
import { objectMetadataItemsState } from '@/object-metadata/states/objectMetadataItemsState';
|
||||
import { parseFieldRelationType } from '@/object-metadata/utils/parseFieldRelationType';
|
||||
import { ObjectRecord } from '@/object-record/types/ObjectRecord';
|
||||
import { ObjectRecordConnection } from '@/object-record/types/ObjectRecordConnection';
|
||||
import { FieldMetadataType } from '~/generated/graphql';
|
||||
import { isDefined } from '~/utils/isDefined';
|
||||
|
||||
export const useMapConnectionToRecords = () => {
|
||||
const objectMetadataItems = useRecoilValue(objectMetadataItemsState);
|
||||
|
||||
const mapConnectionToRecords = useCallback(
|
||||
<T extends ObjectRecord>({
|
||||
objectRecordConnection,
|
||||
objectNameSingular,
|
||||
objectNamePlural,
|
||||
depth,
|
||||
}: {
|
||||
objectRecordConnection: ObjectRecordConnection<T> | undefined | null;
|
||||
objectNameSingular?: string;
|
||||
objectNamePlural?: string;
|
||||
depth: number;
|
||||
}): ObjectRecord[] => {
|
||||
if (
|
||||
!isDefined(objectRecordConnection) ||
|
||||
!isNonEmptyArray(objectMetadataItems)
|
||||
) {
|
||||
return [];
|
||||
}
|
||||
|
||||
const currentLevelObjectMetadataItem = objectMetadataItems.find(
|
||||
(objectMetadataItem) =>
|
||||
objectMetadataItem.nameSingular === objectNameSingular ||
|
||||
objectMetadataItem.namePlural === objectNamePlural,
|
||||
);
|
||||
|
||||
if (!currentLevelObjectMetadataItem) {
|
||||
throw new Error(
|
||||
`Could not find object metadata item for object name singular "${objectNameSingular}" in mapConnectionToRecords`,
|
||||
);
|
||||
}
|
||||
|
||||
const relationFields = currentLevelObjectMetadataItem.fields.filter(
|
||||
(field) => field.type === FieldMetadataType.Relation,
|
||||
);
|
||||
|
||||
const objectRecords = [
|
||||
...(objectRecordConnection.edges?.map((edge) => edge.node) ?? []),
|
||||
];
|
||||
|
||||
return produce(objectRecords, (objectRecordsDraft) => {
|
||||
for (const objectRecordDraft of objectRecordsDraft) {
|
||||
for (const relationField of relationFields) {
|
||||
const relationType = parseFieldRelationType(relationField);
|
||||
|
||||
if (
|
||||
relationType === 'TO_ONE_OBJECT' ||
|
||||
relationType === 'FROM_ONE_OBJECT'
|
||||
) {
|
||||
continue;
|
||||
}
|
||||
|
||||
const relatedObjectMetadataSingularName =
|
||||
relationField.toRelationMetadata?.fromObjectMetadata
|
||||
.nameSingular ??
|
||||
relationField.fromRelationMetadata?.toObjectMetadata
|
||||
.nameSingular ??
|
||||
null;
|
||||
|
||||
const relationFieldMetadataItem = objectMetadataItems.find(
|
||||
(objectMetadataItem) =>
|
||||
objectMetadataItem.nameSingular ===
|
||||
relatedObjectMetadataSingularName,
|
||||
);
|
||||
|
||||
if (
|
||||
!relationFieldMetadataItem ||
|
||||
!isDefined(relatedObjectMetadataSingularName)
|
||||
) {
|
||||
throw new Error(
|
||||
`Could not find relation object metadata item for object name plural ${relationField.name} in mapConnectionToRecords`,
|
||||
);
|
||||
}
|
||||
|
||||
const relationConnection = objectRecordDraft?.[
|
||||
relationField.name
|
||||
] as ObjectRecordConnection | undefined | null;
|
||||
|
||||
if (!isDefined(relationConnection)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
const relationConnectionMappedToRecords = mapConnectionToRecords({
|
||||
objectRecordConnection: relationConnection,
|
||||
objectNameSingular: relatedObjectMetadataSingularName,
|
||||
depth: depth - 1,
|
||||
});
|
||||
|
||||
(objectRecordDraft as any)[relationField.name] =
|
||||
relationConnectionMappedToRecords;
|
||||
}
|
||||
}
|
||||
}) as ObjectRecord[];
|
||||
},
|
||||
[objectMetadataItems],
|
||||
);
|
||||
|
||||
return mapConnectionToRecords;
|
||||
};
|
||||
@ -3,29 +3,29 @@ import { useApolloClient } from '@apollo/client';
|
||||
import { triggerUpdateRecordOptimisticEffect } from '@/apollo/optimistic-effect/utils/triggerUpdateRecordOptimisticEffect';
|
||||
import { useObjectMetadataItem } from '@/object-metadata/hooks/useObjectMetadataItem';
|
||||
import { useObjectMetadataItems } from '@/object-metadata/hooks/useObjectMetadataItems';
|
||||
import { useGenerateObjectRecordOptimisticResponse } from '@/object-record/cache/hooks/useGenerateObjectRecordOptimisticResponse';
|
||||
import { getRecordNodeFromRecord } from '@/object-record/cache/utils/getRecordNodeFromRecord';
|
||||
import { updateRecordFromCache } from '@/object-record/cache/utils/updateRecordFromCache';
|
||||
import { getUpdateOneRecordMutationResponseField } from '@/object-record/hooks/useGenerateUpdateOneRecordMutation';
|
||||
import { ObjectRecord } from '@/object-record/types/ObjectRecord';
|
||||
import { sanitizeRecordInput } from '@/object-record/utils/sanitizeRecordInput';
|
||||
|
||||
type useUpdateOneRecordProps = {
|
||||
objectNameSingular: string;
|
||||
queryFields?: Record<string, any>;
|
||||
depth?: number;
|
||||
};
|
||||
|
||||
export const useUpdateOneRecord = <
|
||||
UpdatedObjectRecord extends ObjectRecord = ObjectRecord,
|
||||
>({
|
||||
objectNameSingular,
|
||||
queryFields,
|
||||
depth = 1,
|
||||
}: useUpdateOneRecordProps) => {
|
||||
const apolloClient = useApolloClient();
|
||||
|
||||
const { objectMetadataItem, updateOneRecordMutation, getRecordFromCache } =
|
||||
useObjectMetadataItem({ objectNameSingular }, 1);
|
||||
|
||||
const { generateObjectRecordOptimisticResponse } =
|
||||
useGenerateObjectRecordOptimisticResponse({
|
||||
objectMetadataItem,
|
||||
});
|
||||
useObjectMetadataItem({ objectNameSingular }, depth, queryFields);
|
||||
|
||||
const { objectMetadataItems } = useObjectMetadataItems();
|
||||
|
||||
@ -36,17 +36,57 @@ export const useUpdateOneRecord = <
|
||||
idToUpdate: string;
|
||||
updateOneRecordInput: Partial<Omit<UpdatedObjectRecord, 'id'>>;
|
||||
}) => {
|
||||
const sanitizedInput = {
|
||||
...sanitizeRecordInput({
|
||||
objectMetadataItem,
|
||||
recordInput: updateOneRecordInput,
|
||||
}),
|
||||
};
|
||||
|
||||
const cachedRecord = getRecordFromCache<UpdatedObjectRecord>(idToUpdate);
|
||||
|
||||
const sanitizedUpdateOneRecordInput = sanitizeRecordInput({
|
||||
const cachedRecordWithConnection = getRecordNodeFromRecord<ObjectRecord>({
|
||||
record: cachedRecord,
|
||||
objectMetadataItem,
|
||||
recordInput: updateOneRecordInput,
|
||||
objectMetadataItems,
|
||||
depth,
|
||||
queryFields,
|
||||
computeReferences: true,
|
||||
});
|
||||
|
||||
const optimisticallyUpdatedRecord = generateObjectRecordOptimisticResponse({
|
||||
...(cachedRecord ?? {}),
|
||||
...sanitizedUpdateOneRecordInput,
|
||||
id: idToUpdate,
|
||||
const optimisticRecord = {
|
||||
...cachedRecord,
|
||||
...sanitizedInput,
|
||||
...{ id: idToUpdate },
|
||||
};
|
||||
|
||||
const optimisticRecordWithConnection =
|
||||
getRecordNodeFromRecord<ObjectRecord>({
|
||||
record: optimisticRecord,
|
||||
objectMetadataItem,
|
||||
objectMetadataItems,
|
||||
depth,
|
||||
queryFields,
|
||||
computeReferences: true,
|
||||
});
|
||||
|
||||
if (!optimisticRecordWithConnection || !cachedRecordWithConnection) {
|
||||
return null;
|
||||
}
|
||||
|
||||
updateRecordFromCache({
|
||||
objectMetadataItems,
|
||||
objectMetadataItem,
|
||||
cache: apolloClient.cache,
|
||||
record: optimisticRecord,
|
||||
});
|
||||
|
||||
triggerUpdateRecordOptimisticEffect({
|
||||
cache: apolloClient.cache,
|
||||
objectMetadataItem,
|
||||
currentRecord: cachedRecordWithConnection,
|
||||
updatedRecord: optimisticRecordWithConnection,
|
||||
objectMetadataItems,
|
||||
});
|
||||
|
||||
const mutationResponseField =
|
||||
@ -56,10 +96,7 @@ export const useUpdateOneRecord = <
|
||||
mutation: updateOneRecordMutation,
|
||||
variables: {
|
||||
idToUpdate,
|
||||
input: sanitizedUpdateOneRecordInput,
|
||||
},
|
||||
optimisticResponse: {
|
||||
[mutationResponseField]: optimisticallyUpdatedRecord,
|
||||
input: sanitizedInput,
|
||||
},
|
||||
update: (cache, { data }) => {
|
||||
const record = data?.[mutationResponseField];
|
||||
|
||||
@ -1,24 +0,0 @@
|
||||
import { useRecoilCallback } from 'recoil';
|
||||
|
||||
import { recordStoreFamilySelector } from '@/object-record/record-store/states/selectors/recordStoreFamilySelector';
|
||||
import { isDeeplyEqual } from '~/utils/isDeeplyEqual';
|
||||
|
||||
export const useUpsertRecordFieldFromState = () =>
|
||||
useRecoilCallback(
|
||||
({ set }) =>
|
||||
<T extends { id: string }, F extends keyof T>({
|
||||
record,
|
||||
fieldName,
|
||||
}: {
|
||||
record: T;
|
||||
fieldName: F extends string ? F : never;
|
||||
}) =>
|
||||
set(
|
||||
recordStoreFamilySelector({ recordId: record.id, fieldName }),
|
||||
(previousField) =>
|
||||
isDeeplyEqual(previousField, record[fieldName])
|
||||
? previousField
|
||||
: record[fieldName],
|
||||
),
|
||||
[],
|
||||
);
|
||||
@ -0,0 +1,44 @@
|
||||
import { useQuery } from '@apollo/client';
|
||||
|
||||
import { EMPTY_QUERY } from '@/object-metadata/hooks/useObjectMetadataItem';
|
||||
import { ObjectMetadataItem } from '@/object-metadata/types/ObjectMetadataItem';
|
||||
import { getRecordsFromRecordConnection } from '@/object-record/cache/utils/getRecordsFromRecordConnection';
|
||||
import { useGenerateFindManyRecordsForMultipleMetadataItemsQuery } from '@/object-record/multiple-objects/hooks/useGenerateFindManyRecordsForMultipleMetadataItemsQuery';
|
||||
import { MultiObjectRecordQueryResult } from '@/object-record/relation-picker/hooks/useMultiObjectRecordsQueryResultFormattedAsObjectRecordForSelectArray';
|
||||
|
||||
export const useFindManyRecordsForMultipleMetadataItems = ({
|
||||
objectMetadataItems,
|
||||
skip = false,
|
||||
depth = 2,
|
||||
}: {
|
||||
objectMetadataItems: ObjectMetadataItem[];
|
||||
skip: boolean;
|
||||
depth?: number;
|
||||
}) => {
|
||||
const findManyQuery = useGenerateFindManyRecordsForMultipleMetadataItemsQuery(
|
||||
{
|
||||
targetObjectMetadataItems: objectMetadataItems,
|
||||
depth,
|
||||
},
|
||||
);
|
||||
|
||||
const { data } = useQuery<MultiObjectRecordQueryResult>(
|
||||
findManyQuery ?? EMPTY_QUERY,
|
||||
{
|
||||
skip,
|
||||
},
|
||||
);
|
||||
|
||||
const resultWithoutConnection = Object.fromEntries(
|
||||
Object.entries(data ?? {}).map(([namePlural, objectRecordConnection]) => [
|
||||
namePlural,
|
||||
getRecordsFromRecordConnection({
|
||||
recordConnection: objectRecordConnection,
|
||||
}),
|
||||
]),
|
||||
);
|
||||
|
||||
return {
|
||||
result: resultWithoutConnection,
|
||||
};
|
||||
};
|
||||
@ -3,5 +3,7 @@ import { ObjectRecordQueryVariables } from '@/object-record/types/ObjectRecordQu
|
||||
export type QueryKey = {
|
||||
objectNameSingular: string;
|
||||
variables: ObjectRecordQueryVariables;
|
||||
depth: number;
|
||||
depth?: number;
|
||||
fields?: Record<string, any>; // Todo: Fields should be required
|
||||
fieldsFactory?: (fieldsFactoryParam: any) => Record<string, any>;
|
||||
};
|
||||
|
||||
@ -109,6 +109,19 @@ export const usePersistField = () => {
|
||||
valueToPersist,
|
||||
);
|
||||
|
||||
if (fieldIsRelation) {
|
||||
updateRecord?.({
|
||||
variables: {
|
||||
where: { id: entityId },
|
||||
updateOneRecordInput: {
|
||||
[fieldName]: valueToPersist,
|
||||
[`${fieldName}Id`]: valueToPersist?.id ?? null,
|
||||
},
|
||||
},
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
updateRecord?.({
|
||||
variables: {
|
||||
where: { id: entityId },
|
||||
|
||||
@ -68,7 +68,7 @@ const AddressInputWithContext = ({
|
||||
onEscape={onEscape}
|
||||
onClickOutside={onClickOutside}
|
||||
value={value}
|
||||
hotkeyScope=""
|
||||
hotkeyScope="hotkey-scope"
|
||||
onTab={onTab}
|
||||
onShiftTab={onShiftTab}
|
||||
/>
|
||||
@ -96,7 +96,7 @@ const clearMocksDecorator: Decorator = (Story, context) => {
|
||||
};
|
||||
|
||||
const meta: Meta = {
|
||||
title: 'UI/Data/Field/Input/AddressInput',
|
||||
title: 'UI/Data/Field/Input/AddressFieldInput',
|
||||
component: AddressInputWithContext,
|
||||
args: {
|
||||
value: 'text',
|
||||
|
||||
@ -8,6 +8,11 @@ export type FieldDefinitionRelationType =
|
||||
| 'TO_MANY_OBJECTS'
|
||||
| 'TO_ONE_OBJECT';
|
||||
|
||||
export type RelationDirections = {
|
||||
from: FieldDefinitionRelationType;
|
||||
to: FieldDefinitionRelationType;
|
||||
};
|
||||
|
||||
export type FieldDefinition<T extends FieldMetadata> = {
|
||||
fieldMetadataId: string;
|
||||
label: string;
|
||||
|
||||
@ -2,14 +2,13 @@ import { useRecoilValue, useSetRecoilState } from 'recoil';
|
||||
|
||||
import { currentWorkspaceState } from '@/auth/states/currentWorkspaceState.ts';
|
||||
import { useObjectMetadataItem } from '@/object-metadata/hooks/useObjectMetadataItem';
|
||||
import { useFindManyRecords } from '@/object-record/hooks/useFindManyRecords';
|
||||
import { turnSortsIntoOrderBy } from '@/object-record/object-sort-dropdown/utils/turnSortsIntoOrderBy';
|
||||
import { turnObjectDropdownFilterIntoQueryFilter } from '@/object-record/record-filter/utils/turnObjectDropdownFilterIntoQueryFilter';
|
||||
import { useRecordTableStates } from '@/object-record/record-table/hooks/internal/useRecordTableStates';
|
||||
import { useRecordTable } from '@/object-record/record-table/hooks/useRecordTable';
|
||||
import { SIGN_IN_BACKGROUND_MOCK_COMPANIES } from '@/sign-in-background-mock/constants/SignInBackgroundMockCompanies';
|
||||
|
||||
import { useFindManyRecords } from '../../hooks/useFindManyRecords';
|
||||
|
||||
export const useFindManyParams = (
|
||||
objectNameSingular: string,
|
||||
recordTableId?: string,
|
||||
|
||||
@ -141,8 +141,8 @@ export const useExportTableData = ({
|
||||
...usedFindManyParams,
|
||||
depth: 0,
|
||||
limit: pageSize,
|
||||
onCompleted: (_data, { hasNextPage }) => {
|
||||
setHasNextPage(hasNextPage ?? false);
|
||||
onCompleted: (_data, options) => {
|
||||
setHasNextPage(options?.pageInfo?.hasNextPage ?? false);
|
||||
},
|
||||
});
|
||||
|
||||
|
||||
@ -1,7 +1,6 @@
|
||||
import { useEffect } from 'react';
|
||||
import { useRecoilState, useSetRecoilState } from 'recoil';
|
||||
|
||||
import { useActivityConnectionUtils } from '@/activities/hooks/useActivityConnectionUtils';
|
||||
import { Activity } from '@/activities/types/Activity';
|
||||
import { useFindOneRecord } from '@/object-record/hooks/useFindOneRecord';
|
||||
import { recordLoadingFamilyState } from '@/object-record/record-store/states/recordLoadingFamilyState';
|
||||
@ -15,7 +14,7 @@ export const RecordShowContainer = ({
|
||||
objectRecordId: string;
|
||||
objectNameSingular: string;
|
||||
}) => {
|
||||
const { record, loading } = useFindOneRecord({
|
||||
const { record: activity, loading } = useFindOneRecord<Activity>({
|
||||
objectRecordId,
|
||||
objectNameSingular,
|
||||
depth: 3,
|
||||
@ -35,14 +34,9 @@ export const RecordShowContainer = ({
|
||||
}
|
||||
}, [loading, recordLoading, setRecordLoading]);
|
||||
|
||||
const { makeActivityWithoutConnection } = useActivityConnectionUtils();
|
||||
|
||||
useEffect(() => {
|
||||
if (!loading && isDefined(record)) {
|
||||
const { activity: activityWithoutConnection } =
|
||||
makeActivityWithoutConnection(record as any);
|
||||
|
||||
setRecordStore(activityWithoutConnection as Activity);
|
||||
if (!loading && isDefined(activity)) {
|
||||
setRecordStore(activity);
|
||||
}
|
||||
}, [loading, record, setRecordStore, makeActivityWithoutConnection]);
|
||||
}, [loading, setRecordStore, activity]);
|
||||
};
|
||||
|
||||
@ -51,16 +51,17 @@ export const RecordDetailRelationSection = () => {
|
||||
);
|
||||
|
||||
const fieldValue = useRecoilValue<
|
||||
({ id: string } & Record<string, any>) | null
|
||||
({ id: string } & Record<string, any>) | ObjectRecord[] | null
|
||||
>(recordStoreFamilySelector({ recordId: entityId, fieldName }));
|
||||
|
||||
// TODO: use new relation type
|
||||
const isToOneObject = relationType === 'TO_ONE_OBJECT';
|
||||
const isFromManyObjects = relationType === 'FROM_MANY_OBJECTS';
|
||||
|
||||
const relationRecords: ObjectRecord[] =
|
||||
fieldValue && isToOneObject
|
||||
? [fieldValue]
|
||||
: fieldValue?.edges.map(({ node }: { node: ObjectRecord }) => node) ?? [];
|
||||
? [fieldValue as ObjectRecord]
|
||||
: (fieldValue as ObjectRecord[]) ?? [];
|
||||
const relationRecordIds = relationRecords.map(({ id }) => id);
|
||||
|
||||
const dropdownId = `record-field-card-relation-picker-${fieldDefinition.label}`;
|
||||
|
||||
@ -81,7 +81,7 @@ const StyledTable = styled.table<{
|
||||
z-index: 6;
|
||||
}
|
||||
|
||||
thead th:nth-child(n + 3) {
|
||||
thead th:nth-of-type(n + 3) {
|
||||
top: 0;
|
||||
z-index: 5;
|
||||
position: sticky;
|
||||
|
||||
@ -4,7 +4,7 @@ import { useRecoilValue } from 'recoil';
|
||||
|
||||
import { EMPTY_QUERY } from '@/object-metadata/hooks/useObjectMetadataItem';
|
||||
import { objectMetadataItemsState } from '@/object-metadata/states/objectMetadataItemsState';
|
||||
import { useGenerateFindManyRecordsForMultipleMetadataItemsQuery } from '@/object-record/hooks/useGenerateFindManyRecordsForMultipleMetadataItemsQuery';
|
||||
import { useGenerateFindManyRecordsForMultipleMetadataItemsQuery } from '@/object-record/multiple-objects/hooks/useGenerateFindManyRecordsForMultipleMetadataItemsQuery';
|
||||
import { useLimitPerMetadataItem } from '@/object-record/relation-picker/hooks/useLimitPerMetadataItem';
|
||||
import {
|
||||
MultiObjectRecordQueryResult,
|
||||
|
||||
@ -4,7 +4,7 @@ import { useRecoilValue } from 'recoil';
|
||||
import { EMPTY_QUERY } from '@/object-metadata/hooks/useObjectMetadataItem';
|
||||
import { objectMetadataItemsState } from '@/object-metadata/states/objectMetadataItemsState';
|
||||
import { CoreObjectNameSingular } from '@/object-metadata/types/CoreObjectNameSingular';
|
||||
import { useGenerateFindManyRecordsForMultipleMetadataItemsQuery } from '@/object-record/hooks/useGenerateFindManyRecordsForMultipleMetadataItemsQuery';
|
||||
import { useGenerateFindManyRecordsForMultipleMetadataItemsQuery } from '@/object-record/multiple-objects/hooks/useGenerateFindManyRecordsForMultipleMetadataItemsQuery';
|
||||
import { useLimitPerMetadataItem } from '@/object-record/relation-picker/hooks/useLimitPerMetadataItem';
|
||||
import {
|
||||
MultiObjectRecordQueryResult,
|
||||
|
||||
@ -4,7 +4,7 @@ import { isNonEmptyArray } from '@sniptt/guards';
|
||||
import { useRecoilValue } from 'recoil';
|
||||
|
||||
import { objectMetadataItemsState } from '@/object-metadata/states/objectMetadataItemsState';
|
||||
import { useGenerateFindManyRecordsForMultipleMetadataItemsQuery } from '@/object-record/hooks/useGenerateFindManyRecordsForMultipleMetadataItemsQuery';
|
||||
import { useGenerateFindManyRecordsForMultipleMetadataItemsQuery } from '@/object-record/multiple-objects/hooks/useGenerateFindManyRecordsForMultipleMetadataItemsQuery';
|
||||
import { useLimitPerMetadataItem } from '@/object-record/relation-picker/hooks/useLimitPerMetadataItem';
|
||||
import {
|
||||
MultiObjectRecordQueryResult,
|
||||
|
||||
@ -10,6 +10,7 @@ export type ObjectRecordConnection<T extends ObjectRecord = ObjectRecord> = {
|
||||
hasPreviousPage?: boolean;
|
||||
startCursor?: Nullable<string>;
|
||||
endCursor?: Nullable<string>;
|
||||
totalCount?: number;
|
||||
};
|
||||
totalCount: number;
|
||||
totalCount?: number;
|
||||
};
|
||||
|
||||
@ -2,7 +2,6 @@ import { isNonEmptyString } from '@sniptt/guards';
|
||||
|
||||
import { FieldMetadataItem } from '@/object-metadata/types/FieldMetadataItem';
|
||||
import { FieldMetadataType } from '~/generated/graphql';
|
||||
import { capitalize } from '~/utils/string/capitalize';
|
||||
|
||||
export const generateEmptyFieldValue = (
|
||||
fieldMetadataItem: FieldMetadataItem,
|
||||
@ -53,8 +52,6 @@ export const generateEmptyFieldValue = (
|
||||
return true;
|
||||
}
|
||||
case FieldMetadataType.Relation: {
|
||||
// TODO: refactor with relationDefiniton once the PR is merged : https://github.com/twentyhq/twenty/pull/4378
|
||||
// so we can directly check the relation type from this field point of view.
|
||||
if (
|
||||
!isNonEmptyString(
|
||||
fieldMetadataItem.fromRelationMetadata?.toObjectMetadata
|
||||
@ -64,12 +61,7 @@ export const generateEmptyFieldValue = (
|
||||
return null;
|
||||
}
|
||||
|
||||
return {
|
||||
__typename: `${capitalize(
|
||||
fieldMetadataItem.fromRelationMetadata.toObjectMetadata.nameSingular,
|
||||
)}Connection`,
|
||||
edges: [],
|
||||
};
|
||||
return [];
|
||||
}
|
||||
case FieldMetadataType.Currency: {
|
||||
return {
|
||||
|
||||
@ -1,24 +0,0 @@
|
||||
export const mapPaginatedRecordsToRecords = <
|
||||
RecordType extends { id: string } & Record<string, any>,
|
||||
RecordTypeQuery extends {
|
||||
[objectNamePlural: string]: {
|
||||
edges: RecordEdge[];
|
||||
};
|
||||
},
|
||||
RecordEdge extends {
|
||||
node: RecordType;
|
||||
},
|
||||
>({
|
||||
pagedRecords,
|
||||
objectNamePlural,
|
||||
}: {
|
||||
pagedRecords: RecordTypeQuery | undefined;
|
||||
objectNamePlural: string;
|
||||
}) => {
|
||||
const formattedRecords: RecordType[] =
|
||||
pagedRecords?.[objectNamePlural]?.edges?.map((recordEdge: RecordEdge) => ({
|
||||
...recordEdge.node,
|
||||
})) ?? [];
|
||||
|
||||
return formattedRecords;
|
||||
};
|
||||
@ -0,0 +1,35 @@
|
||||
import { isUndefined } from '@sniptt/guards';
|
||||
|
||||
import { ObjectMetadataItem } from '@/object-metadata/types/ObjectMetadataItem';
|
||||
import { ObjectRecord } from '@/object-record/types/ObjectRecord';
|
||||
import { generateEmptyFieldValue } from '@/object-record/utils/generateEmptyFieldValue';
|
||||
import { isDefined } from '~/utils/isDefined';
|
||||
|
||||
export const prefillRecord = <T extends ObjectRecord>({
|
||||
objectMetadataItem,
|
||||
input,
|
||||
depth = 1,
|
||||
}: {
|
||||
objectMetadataItem: ObjectMetadataItem;
|
||||
input: Record<string, unknown>;
|
||||
depth?: number;
|
||||
}) => {
|
||||
return Object.fromEntries(
|
||||
objectMetadataItem.fields
|
||||
.filter(
|
||||
(fieldMetadataItem) =>
|
||||
depth > 0 || fieldMetadataItem.type !== 'RELATION',
|
||||
)
|
||||
.map((fieldMetadataItem) => {
|
||||
const inputValue = input[fieldMetadataItem.name];
|
||||
|
||||
return [
|
||||
fieldMetadataItem.name,
|
||||
isUndefined(inputValue)
|
||||
? generateEmptyFieldValue(fieldMetadataItem)
|
||||
: inputValue,
|
||||
];
|
||||
})
|
||||
.filter(isDefined),
|
||||
) as T;
|
||||
};
|
||||
@ -3,6 +3,7 @@ import { isString } from '@sniptt/guards';
|
||||
import { CoreObjectNameSingular } from '@/object-metadata/types/CoreObjectNameSingular';
|
||||
import { ObjectMetadataItem } from '@/object-metadata/types/ObjectMetadataItem';
|
||||
import { isFieldRelationValue } from '@/object-record/record-field/types/guards/isFieldRelationValue';
|
||||
import { ObjectRecord } from '@/object-record/types/ObjectRecord';
|
||||
import { sanitizeLink } from '@/object-record/utils/sanitizeLinkRecordInput';
|
||||
import { FieldMetadataType } from '~/generated/graphql';
|
||||
import { isDefined } from '~/utils/isDefined';
|
||||
@ -12,7 +13,7 @@ export const sanitizeRecordInput = ({
|
||||
recordInput,
|
||||
}: {
|
||||
objectMetadataItem: ObjectMetadataItem;
|
||||
recordInput: Record<string, unknown>;
|
||||
recordInput: Partial<ObjectRecord>;
|
||||
}) => {
|
||||
const filteredResultRecord = Object.fromEntries(
|
||||
Object.entries(recordInput)
|
||||
@ -23,6 +24,10 @@ export const sanitizeRecordInput = ({
|
||||
|
||||
if (!fieldMetadataItem) return undefined;
|
||||
|
||||
if (!fieldMetadataItem.isNullable && fieldValue == null) {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
if (
|
||||
fieldMetadataItem.type === FieldMetadataType.Relation &&
|
||||
isFieldRelationValue(fieldValue)
|
||||
|
||||
Reference in New Issue
Block a user