feat: delete favorite in cache on related record deletion (#3751)
* feat: delete favorite in cache on related record deletion * fix: fix useCreateOneRecord tests * fix: fix usePipelineSteps tests * fix: fix useCreateManyRecords tests * fix: add null relation field values in useGenerateObjectRecordOptimisticResponse
This commit is contained in:
@ -1,8 +1,10 @@
|
|||||||
import { ApolloCache, StoreObject } from '@apollo/client';
|
import { ApolloCache, StoreObject } from '@apollo/client';
|
||||||
|
|
||||||
import { isCachedObjectRecordConnection } from '@/apollo/optimistic-effect/utils/isCachedObjectRecordConnection';
|
import { isCachedObjectRecordConnection } from '@/apollo/optimistic-effect/utils/isCachedObjectRecordConnection';
|
||||||
|
import { triggerUpdateRelationsOptimisticEffect } from '@/apollo/optimistic-effect/utils/triggerUpdateRelationsOptimisticEffect';
|
||||||
import { CachedObjectRecord } from '@/apollo/types/CachedObjectRecord';
|
import { CachedObjectRecord } from '@/apollo/types/CachedObjectRecord';
|
||||||
import { CachedObjectRecordEdge } from '@/apollo/types/CachedObjectRecordEdge';
|
import { CachedObjectRecordEdge } from '@/apollo/types/CachedObjectRecordEdge';
|
||||||
|
import { useGetRelationMetadata } from '@/object-metadata/hooks/useGetRelationMetadata';
|
||||||
import { ObjectMetadataItem } from '@/object-metadata/types/ObjectMetadataItem';
|
import { ObjectMetadataItem } from '@/object-metadata/types/ObjectMetadataItem';
|
||||||
import { capitalize } from '~/utils/string/capitalize';
|
import { capitalize } from '~/utils/string/capitalize';
|
||||||
|
|
||||||
@ -15,15 +17,27 @@ export const triggerCreateRecordsOptimisticEffect = ({
|
|||||||
cache,
|
cache,
|
||||||
objectMetadataItem,
|
objectMetadataItem,
|
||||||
records,
|
records,
|
||||||
|
getRelationMetadata,
|
||||||
}: {
|
}: {
|
||||||
cache: ApolloCache<unknown>;
|
cache: ApolloCache<unknown>;
|
||||||
objectMetadataItem: ObjectMetadataItem;
|
objectMetadataItem: ObjectMetadataItem;
|
||||||
records: CachedObjectRecord[];
|
records: CachedObjectRecord[];
|
||||||
|
getRelationMetadata: ReturnType<typeof useGetRelationMetadata>;
|
||||||
}) => {
|
}) => {
|
||||||
const objectEdgeTypeName = `${capitalize(
|
const objectEdgeTypeName = `${capitalize(
|
||||||
objectMetadataItem.nameSingular,
|
objectMetadataItem.nameSingular,
|
||||||
)}Edge`;
|
)}Edge`;
|
||||||
|
|
||||||
|
records.forEach((record) =>
|
||||||
|
triggerUpdateRelationsOptimisticEffect({
|
||||||
|
cache,
|
||||||
|
objectMetadataItem,
|
||||||
|
previousRecord: null,
|
||||||
|
nextRecord: record,
|
||||||
|
getRelationMetadata,
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
|
||||||
cache.modify<StoreObject>({
|
cache.modify<StoreObject>({
|
||||||
fields: {
|
fields: {
|
||||||
[objectMetadataItem.namePlural]: (
|
[objectMetadataItem.namePlural]: (
|
||||||
|
|||||||
@ -1,9 +1,11 @@
|
|||||||
import { ApolloCache, StoreObject } from '@apollo/client';
|
import { ApolloCache, StoreObject } from '@apollo/client';
|
||||||
|
|
||||||
import { isCachedObjectRecordConnection } from '@/apollo/optimistic-effect/utils/isCachedObjectRecordConnection';
|
import { isCachedObjectRecordConnection } from '@/apollo/optimistic-effect/utils/isCachedObjectRecordConnection';
|
||||||
|
import { triggerUpdateRelationsOptimisticEffect } from '@/apollo/optimistic-effect/utils/triggerUpdateRelationsOptimisticEffect';
|
||||||
import { CachedObjectRecord } from '@/apollo/types/CachedObjectRecord';
|
import { CachedObjectRecord } from '@/apollo/types/CachedObjectRecord';
|
||||||
import { CachedObjectRecordEdge } from '@/apollo/types/CachedObjectRecordEdge';
|
import { CachedObjectRecordEdge } from '@/apollo/types/CachedObjectRecordEdge';
|
||||||
import { CachedObjectRecordQueryVariables } from '@/apollo/types/CachedObjectRecordQueryVariables';
|
import { CachedObjectRecordQueryVariables } from '@/apollo/types/CachedObjectRecordQueryVariables';
|
||||||
|
import { useGetRelationMetadata } from '@/object-metadata/hooks/useGetRelationMetadata';
|
||||||
import { ObjectMetadataItem } from '@/object-metadata/types/ObjectMetadataItem';
|
import { ObjectMetadataItem } from '@/object-metadata/types/ObjectMetadataItem';
|
||||||
import { isDefined } from '~/utils/isDefined';
|
import { isDefined } from '~/utils/isDefined';
|
||||||
import { parseApolloStoreFieldName } from '~/utils/parseApolloStoreFieldName';
|
import { parseApolloStoreFieldName } from '~/utils/parseApolloStoreFieldName';
|
||||||
@ -12,10 +14,12 @@ export const triggerDeleteRecordsOptimisticEffect = ({
|
|||||||
cache,
|
cache,
|
||||||
objectMetadataItem,
|
objectMetadataItem,
|
||||||
records,
|
records,
|
||||||
|
getRelationMetadata,
|
||||||
}: {
|
}: {
|
||||||
cache: ApolloCache<unknown>;
|
cache: ApolloCache<unknown>;
|
||||||
objectMetadataItem: ObjectMetadataItem;
|
objectMetadataItem: ObjectMetadataItem;
|
||||||
records: Pick<CachedObjectRecord, 'id' | '__typename'>[];
|
records: CachedObjectRecord[];
|
||||||
|
getRelationMetadata: ReturnType<typeof useGetRelationMetadata>;
|
||||||
}) => {
|
}) => {
|
||||||
cache.modify<StoreObject>({
|
cache.modify<StoreObject>({
|
||||||
fields: {
|
fields: {
|
||||||
@ -64,5 +68,15 @@ export const triggerDeleteRecordsOptimisticEffect = ({
|
|||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
cache.gc();
|
records.forEach((record) => {
|
||||||
|
triggerUpdateRelationsOptimisticEffect({
|
||||||
|
cache,
|
||||||
|
objectMetadataItem,
|
||||||
|
previousRecord: record,
|
||||||
|
nextRecord: null,
|
||||||
|
getRelationMetadata,
|
||||||
|
});
|
||||||
|
|
||||||
|
cache.evict({ id: cache.identify(record) });
|
||||||
|
});
|
||||||
};
|
};
|
||||||
|
|||||||
@ -2,9 +2,11 @@ import { ApolloCache, StoreObject } from '@apollo/client';
|
|||||||
|
|
||||||
import { isCachedObjectRecordConnection } from '@/apollo/optimistic-effect/utils/isCachedObjectRecordConnection';
|
import { isCachedObjectRecordConnection } from '@/apollo/optimistic-effect/utils/isCachedObjectRecordConnection';
|
||||||
import { sortCachedObjectEdges } from '@/apollo/optimistic-effect/utils/sortCachedObjectEdges';
|
import { sortCachedObjectEdges } from '@/apollo/optimistic-effect/utils/sortCachedObjectEdges';
|
||||||
|
import { triggerUpdateRelationsOptimisticEffect } from '@/apollo/optimistic-effect/utils/triggerUpdateRelationsOptimisticEffect';
|
||||||
import { CachedObjectRecord } from '@/apollo/types/CachedObjectRecord';
|
import { CachedObjectRecord } from '@/apollo/types/CachedObjectRecord';
|
||||||
import { CachedObjectRecordEdge } from '@/apollo/types/CachedObjectRecordEdge';
|
import { CachedObjectRecordEdge } from '@/apollo/types/CachedObjectRecordEdge';
|
||||||
import { CachedObjectRecordQueryVariables } from '@/apollo/types/CachedObjectRecordQueryVariables';
|
import { CachedObjectRecordQueryVariables } from '@/apollo/types/CachedObjectRecordQueryVariables';
|
||||||
|
import { useGetRelationMetadata } from '@/object-metadata/hooks/useGetRelationMetadata';
|
||||||
import { ObjectMetadataItem } from '@/object-metadata/types/ObjectMetadataItem';
|
import { ObjectMetadataItem } from '@/object-metadata/types/ObjectMetadataItem';
|
||||||
import { isRecordMatchingFilter } from '@/object-record/record-filter/utils/isRecordMatchingFilter';
|
import { isRecordMatchingFilter } from '@/object-record/record-filter/utils/isRecordMatchingFilter';
|
||||||
import { isDefined } from '~/utils/isDefined';
|
import { isDefined } from '~/utils/isDefined';
|
||||||
@ -14,16 +16,29 @@ import { capitalize } from '~/utils/string/capitalize';
|
|||||||
export const triggerUpdateRecordOptimisticEffect = ({
|
export const triggerUpdateRecordOptimisticEffect = ({
|
||||||
cache,
|
cache,
|
||||||
objectMetadataItem,
|
objectMetadataItem,
|
||||||
record,
|
previousRecord,
|
||||||
|
nextRecord,
|
||||||
|
getRelationMetadata,
|
||||||
}: {
|
}: {
|
||||||
cache: ApolloCache<unknown>;
|
cache: ApolloCache<unknown>;
|
||||||
objectMetadataItem: ObjectMetadataItem;
|
objectMetadataItem: ObjectMetadataItem;
|
||||||
record: CachedObjectRecord;
|
previousRecord: CachedObjectRecord;
|
||||||
|
nextRecord: CachedObjectRecord;
|
||||||
|
getRelationMetadata: ReturnType<typeof useGetRelationMetadata>;
|
||||||
}) => {
|
}) => {
|
||||||
const objectEdgeTypeName = `${capitalize(
|
const objectEdgeTypeName = `${capitalize(
|
||||||
objectMetadataItem.nameSingular,
|
objectMetadataItem.nameSingular,
|
||||||
)}Edge`;
|
)}Edge`;
|
||||||
|
|
||||||
|
triggerUpdateRelationsOptimisticEffect({
|
||||||
|
cache,
|
||||||
|
objectMetadataItem,
|
||||||
|
previousRecord,
|
||||||
|
nextRecord,
|
||||||
|
getRelationMetadata,
|
||||||
|
});
|
||||||
|
|
||||||
|
// Optimistically update record lists
|
||||||
cache.modify<StoreObject>({
|
cache.modify<StoreObject>({
|
||||||
fields: {
|
fields: {
|
||||||
[objectMetadataItem.namePlural]: (
|
[objectMetadataItem.namePlural]: (
|
||||||
@ -49,18 +64,20 @@ export const triggerUpdateRecordOptimisticEffect = ({
|
|||||||
);
|
);
|
||||||
let nextCachedEdges = cachedEdges ? [...cachedEdges] : [];
|
let nextCachedEdges = cachedEdges ? [...cachedEdges] : [];
|
||||||
|
|
||||||
|
// Test if the record matches this list's filters
|
||||||
if (variables?.filter) {
|
if (variables?.filter) {
|
||||||
const matchesFilter = isRecordMatchingFilter({
|
const matchesFilter = isRecordMatchingFilter({
|
||||||
record,
|
record: nextRecord,
|
||||||
filter: variables.filter,
|
filter: variables.filter,
|
||||||
objectMetadataItem,
|
objectMetadataItem,
|
||||||
});
|
});
|
||||||
const recordIndex = nextCachedEdges.findIndex(
|
const recordIndex = nextCachedEdges.findIndex(
|
||||||
(cachedEdge) => readField('id', cachedEdge.node) === record.id,
|
(cachedEdge) => readField('id', cachedEdge.node) === nextRecord.id,
|
||||||
);
|
);
|
||||||
|
|
||||||
|
// If after update, the record matches this list's filters, then add it to the list
|
||||||
if (matchesFilter && recordIndex === -1) {
|
if (matchesFilter && recordIndex === -1) {
|
||||||
const nodeReference = toReference(record);
|
const nodeReference = toReference(nextRecord);
|
||||||
nodeReference &&
|
nodeReference &&
|
||||||
nextCachedEdges.push({
|
nextCachedEdges.push({
|
||||||
__typename: objectEdgeTypeName,
|
__typename: objectEdgeTypeName,
|
||||||
@ -69,11 +86,13 @@ export const triggerUpdateRecordOptimisticEffect = ({
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// If after update, the record does not match this list's filters anymore, then remove it from the list
|
||||||
if (!matchesFilter && recordIndex > -1) {
|
if (!matchesFilter && recordIndex > -1) {
|
||||||
nextCachedEdges.splice(recordIndex, 1);
|
nextCachedEdges.splice(recordIndex, 1);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Sort updated list
|
||||||
if (variables?.orderBy) {
|
if (variables?.orderBy) {
|
||||||
nextCachedEdges = sortCachedObjectEdges({
|
nextCachedEdges = sortCachedObjectEdges({
|
||||||
edges: nextCachedEdges,
|
edges: nextCachedEdges,
|
||||||
@ -82,6 +101,7 @@ export const triggerUpdateRecordOptimisticEffect = ({
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Limit the updated list to the required size
|
||||||
if (isDefined(variables?.first)) {
|
if (isDefined(variables?.first)) {
|
||||||
// If previous edges length was exactly at the required limit,
|
// If previous edges length was exactly at the required limit,
|
||||||
// but after update next edges length is under the limit,
|
// but after update next edges length is under the limit,
|
||||||
|
|||||||
@ -0,0 +1,112 @@
|
|||||||
|
import { ApolloCache } from '@apollo/client';
|
||||||
|
|
||||||
|
import { isObjectRecordConnection } from '@/apollo/optimistic-effect/utils/isObjectRecordConnection';
|
||||||
|
import { triggerAttachRelationOptimisticEffect } from '@/apollo/optimistic-effect/utils/triggerAttachRelationOptimisticEffect';
|
||||||
|
import { triggerDeleteRecordsOptimisticEffect } from '@/apollo/optimistic-effect/utils/triggerDeleteRecordsOptimisticEffect';
|
||||||
|
import { triggerDetachRelationOptimisticEffect } from '@/apollo/optimistic-effect/utils/triggerDetachRelationOptimisticEffect';
|
||||||
|
import { CachedObjectRecord } from '@/apollo/types/CachedObjectRecord';
|
||||||
|
import { coreObjectNamesToDeleteOnRelationDetach } from '@/apollo/types/coreObjectNamesToDeleteOnRelationDetach';
|
||||||
|
import { useGetRelationMetadata } from '@/object-metadata/hooks/useGetRelationMetadata';
|
||||||
|
import { CoreObjectNameSingular } from '@/object-metadata/types/CoreObjectNameSingular';
|
||||||
|
import { ObjectMetadataItem } from '@/object-metadata/types/ObjectMetadataItem';
|
||||||
|
import { ObjectRecordConnection } from '@/object-record/types/ObjectRecordConnection';
|
||||||
|
import { isDeeplyEqual } from '~/utils/isDeeplyEqual';
|
||||||
|
import { isDefined } from '~/utils/isDefined';
|
||||||
|
|
||||||
|
export const triggerUpdateRelationsOptimisticEffect = ({
|
||||||
|
cache,
|
||||||
|
objectMetadataItem,
|
||||||
|
previousRecord,
|
||||||
|
nextRecord,
|
||||||
|
getRelationMetadata,
|
||||||
|
}: {
|
||||||
|
cache: ApolloCache<unknown>;
|
||||||
|
objectMetadataItem: ObjectMetadataItem;
|
||||||
|
previousRecord: CachedObjectRecord | null;
|
||||||
|
nextRecord: CachedObjectRecord | null;
|
||||||
|
getRelationMetadata: ReturnType<typeof useGetRelationMetadata>;
|
||||||
|
}) =>
|
||||||
|
// Optimistically update relation records
|
||||||
|
objectMetadataItem.fields.forEach((fieldMetadataItem) => {
|
||||||
|
if (nextRecord && !(fieldMetadataItem.name in nextRecord)) return;
|
||||||
|
|
||||||
|
const relationMetadata = getRelationMetadata({
|
||||||
|
fieldMetadataItem,
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!relationMetadata) return;
|
||||||
|
|
||||||
|
const {
|
||||||
|
// Object metadata for the related record
|
||||||
|
relationObjectMetadataItem,
|
||||||
|
// Field on the related record
|
||||||
|
relationFieldMetadataItem,
|
||||||
|
} = relationMetadata;
|
||||||
|
|
||||||
|
const previousFieldValue:
|
||||||
|
| ObjectRecordConnection
|
||||||
|
| CachedObjectRecord
|
||||||
|
| null = previousRecord?.[fieldMetadataItem.name];
|
||||||
|
const nextFieldValue: ObjectRecordConnection | CachedObjectRecord | null =
|
||||||
|
nextRecord?.[fieldMetadataItem.name];
|
||||||
|
|
||||||
|
if (isDeeplyEqual(previousFieldValue, nextFieldValue)) return;
|
||||||
|
|
||||||
|
const isPreviousFieldValueRecordConnection = isObjectRecordConnection(
|
||||||
|
relationObjectMetadataItem.nameSingular,
|
||||||
|
previousFieldValue,
|
||||||
|
);
|
||||||
|
const relationRecordsToDetach = isPreviousFieldValueRecordConnection
|
||||||
|
? previousFieldValue.edges.map(({ node }) => node as CachedObjectRecord)
|
||||||
|
: [previousFieldValue].filter(isDefined);
|
||||||
|
|
||||||
|
const isNextFieldValueRecordConnection = isObjectRecordConnection(
|
||||||
|
relationObjectMetadataItem.nameSingular,
|
||||||
|
nextFieldValue,
|
||||||
|
);
|
||||||
|
const relationRecordsToAttach = isNextFieldValueRecordConnection
|
||||||
|
? nextFieldValue.edges.map(({ node }) => node as CachedObjectRecord)
|
||||||
|
: [nextFieldValue].filter(isDefined);
|
||||||
|
|
||||||
|
if (previousRecord && relationRecordsToDetach.length) {
|
||||||
|
const shouldDeleteRelationRecord =
|
||||||
|
coreObjectNamesToDeleteOnRelationDetach.includes(
|
||||||
|
relationObjectMetadataItem.nameSingular as CoreObjectNameSingular,
|
||||||
|
);
|
||||||
|
|
||||||
|
if (shouldDeleteRelationRecord) {
|
||||||
|
triggerDeleteRecordsOptimisticEffect({
|
||||||
|
cache,
|
||||||
|
objectMetadataItem: relationObjectMetadataItem,
|
||||||
|
records: relationRecordsToDetach,
|
||||||
|
getRelationMetadata,
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
relationRecordsToDetach.forEach((relationRecordToDetach) => {
|
||||||
|
triggerDetachRelationOptimisticEffect({
|
||||||
|
cache,
|
||||||
|
objectNameSingular: objectMetadataItem.nameSingular,
|
||||||
|
recordId: previousRecord.id,
|
||||||
|
relationFieldName: relationFieldMetadataItem.name,
|
||||||
|
relationObjectMetadataNameSingular:
|
||||||
|
relationObjectMetadataItem.nameSingular,
|
||||||
|
relationRecordId: relationRecordToDetach.id,
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (nextRecord && relationRecordsToAttach.length) {
|
||||||
|
relationRecordsToAttach.forEach((relationRecordToAttach) =>
|
||||||
|
triggerAttachRelationOptimisticEffect({
|
||||||
|
cache,
|
||||||
|
objectNameSingular: objectMetadataItem.nameSingular,
|
||||||
|
recordId: nextRecord.id,
|
||||||
|
relationFieldName: relationFieldMetadataItem.name,
|
||||||
|
relationObjectMetadataNameSingular:
|
||||||
|
relationObjectMetadataItem.nameSingular,
|
||||||
|
relationRecordId: relationRecordToAttach.id,
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
});
|
||||||
@ -0,0 +1,5 @@
|
|||||||
|
import { CoreObjectNameSingular } from '@/object-metadata/types/CoreObjectNameSingular';
|
||||||
|
|
||||||
|
export const coreObjectNamesToDeleteOnRelationDetach = [
|
||||||
|
CoreObjectNameSingular.Favorite,
|
||||||
|
];
|
||||||
@ -1,41 +0,0 @@
|
|||||||
import { v4 } from 'uuid';
|
|
||||||
import { z } from 'zod';
|
|
||||||
|
|
||||||
import { ObjectMetadataItem } from '@/object-metadata/types/ObjectMetadataItem';
|
|
||||||
import { ObjectRecord } from '@/object-record/types/ObjectRecord';
|
|
||||||
import { generateEmptyFieldValue } from '@/object-record/utils/generateEmptyFieldValue';
|
|
||||||
import { capitalize } from '~/utils/string/capitalize';
|
|
||||||
|
|
||||||
export const useGenerateCachedObjectRecord = ({
|
|
||||||
objectMetadataItem,
|
|
||||||
}: {
|
|
||||||
objectMetadataItem: ObjectMetadataItem;
|
|
||||||
}) => {
|
|
||||||
const generateCachedObjectRecord = <
|
|
||||||
GeneratedObjectRecord extends ObjectRecord,
|
|
||||||
>(
|
|
||||||
input: Record<string, unknown>,
|
|
||||||
) => {
|
|
||||||
const recordSchema = z.object(
|
|
||||||
Object.fromEntries(
|
|
||||||
objectMetadataItem.fields.map((fieldMetadataItem) => [
|
|
||||||
fieldMetadataItem.name,
|
|
||||||
z.unknown().default(generateEmptyFieldValue(fieldMetadataItem)),
|
|
||||||
]),
|
|
||||||
),
|
|
||||||
);
|
|
||||||
|
|
||||||
return {
|
|
||||||
__typename: capitalize(objectMetadataItem.nameSingular),
|
|
||||||
...recordSchema.parse({
|
|
||||||
id: v4(),
|
|
||||||
createdAt: new Date().toISOString(),
|
|
||||||
...input,
|
|
||||||
}),
|
|
||||||
} as GeneratedObjectRecord & { __typename: string };
|
|
||||||
};
|
|
||||||
|
|
||||||
return {
|
|
||||||
generateCachedObjectRecord,
|
|
||||||
};
|
|
||||||
};
|
|
||||||
@ -0,0 +1,72 @@
|
|||||||
|
import { v4 } from 'uuid';
|
||||||
|
import { z } from 'zod';
|
||||||
|
|
||||||
|
import { useGetRelationMetadata } from '@/object-metadata/hooks/useGetRelationMetadata';
|
||||||
|
import { ObjectMetadataItem } from '@/object-metadata/types/ObjectMetadataItem';
|
||||||
|
import { ObjectRecord } from '@/object-record/types/ObjectRecord';
|
||||||
|
import { generateEmptyFieldValue } from '@/object-record/utils/generateEmptyFieldValue';
|
||||||
|
import { capitalize } from '~/utils/string/capitalize';
|
||||||
|
|
||||||
|
export const useGenerateObjectRecordOptimisticResponse = ({
|
||||||
|
objectMetadataItem,
|
||||||
|
}: {
|
||||||
|
objectMetadataItem: ObjectMetadataItem;
|
||||||
|
}) => {
|
||||||
|
const getRelationMetadata = useGetRelationMetadata();
|
||||||
|
|
||||||
|
const generateObjectRecordOptimisticResponse = <
|
||||||
|
GeneratedObjectRecord extends ObjectRecord,
|
||||||
|
>(
|
||||||
|
input: Record<string, unknown>,
|
||||||
|
) => {
|
||||||
|
const recordSchema = z.object(
|
||||||
|
Object.fromEntries(
|
||||||
|
objectMetadataItem.fields.map((fieldMetadataItem) => [
|
||||||
|
fieldMetadataItem.name,
|
||||||
|
z.unknown().default(generateEmptyFieldValue(fieldMetadataItem)),
|
||||||
|
]),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
|
||||||
|
const inputWithRelationFields = objectMetadataItem.fields.reduce(
|
||||||
|
(result, fieldMetadataItem) => {
|
||||||
|
const relationIdFieldName = `${fieldMetadataItem.name}Id`;
|
||||||
|
|
||||||
|
if (!(relationIdFieldName in input)) return result;
|
||||||
|
|
||||||
|
const relationMetadata = getRelationMetadata({ fieldMetadataItem });
|
||||||
|
|
||||||
|
if (!relationMetadata) return result;
|
||||||
|
|
||||||
|
const relationRecordTypeName = capitalize(
|
||||||
|
relationMetadata.relationObjectMetadataItem.nameSingular,
|
||||||
|
);
|
||||||
|
const relationRecordId = result[relationIdFieldName] as string | null;
|
||||||
|
|
||||||
|
return {
|
||||||
|
...result,
|
||||||
|
[fieldMetadataItem.name]: relationRecordId
|
||||||
|
? {
|
||||||
|
__typename: relationRecordTypeName,
|
||||||
|
id: relationRecordId,
|
||||||
|
}
|
||||||
|
: null,
|
||||||
|
};
|
||||||
|
},
|
||||||
|
input,
|
||||||
|
);
|
||||||
|
|
||||||
|
return {
|
||||||
|
__typename: capitalize(objectMetadataItem.nameSingular),
|
||||||
|
...recordSchema.parse({
|
||||||
|
id: v4(),
|
||||||
|
createdAt: new Date().toISOString(),
|
||||||
|
...inputWithRelationFields,
|
||||||
|
}),
|
||||||
|
} as GeneratedObjectRecord & { __typename: string };
|
||||||
|
};
|
||||||
|
|
||||||
|
return {
|
||||||
|
generateObjectRecordOptimisticResponse,
|
||||||
|
};
|
||||||
|
};
|
||||||
@ -1,5 +1,7 @@
|
|||||||
import { gql } from '@apollo/client';
|
import { gql } from '@apollo/client';
|
||||||
|
|
||||||
|
import { Person } from '@/people/types/Person';
|
||||||
|
|
||||||
export const query = gql`
|
export const query = gql`
|
||||||
mutation CreatePeople($data: [PersonCreateInput!]!) {
|
mutation CreatePeople($data: [PersonCreateInput!]!) {
|
||||||
createPeople(data: $data) {
|
createPeople(data: $data) {
|
||||||
@ -67,12 +69,15 @@ export const query = gql`
|
|||||||
}
|
}
|
||||||
`;
|
`;
|
||||||
|
|
||||||
export const variables = {
|
const data = [
|
||||||
data: [
|
{
|
||||||
{ id: 'a7286b9a-c039-4a89-9567-2dfa7953cda9' },
|
id: 'a7286b9a-c039-4a89-9567-2dfa7953cda9',
|
||||||
{ id: '37faabcd-cb39-4a0a-8618-7e3fda9afca0' },
|
name: { firstName: 'John', lastName: 'Doe' },
|
||||||
],
|
},
|
||||||
};
|
{ id: '37faabcd-cb39-4a0a-8618-7e3fda9afca0', jobTitle: 'manager' },
|
||||||
|
] satisfies Partial<Person>[];
|
||||||
|
|
||||||
|
export const variables = { data };
|
||||||
|
|
||||||
export const responseData = {
|
export const responseData = {
|
||||||
opportunities: {
|
opportunities: {
|
||||||
@ -114,3 +119,8 @@ export const responseData = {
|
|||||||
avatarUrl: '',
|
avatarUrl: '',
|
||||||
companyId: '',
|
companyId: '',
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export const response = data.map((personData) => ({
|
||||||
|
...responseData,
|
||||||
|
...personData,
|
||||||
|
}));
|
||||||
|
|||||||
@ -67,10 +67,6 @@ export const query = gql`
|
|||||||
}
|
}
|
||||||
`;
|
`;
|
||||||
|
|
||||||
export const variables = {
|
|
||||||
input: { id: 'a7286b9a-c039-4a89-9567-2dfa7953cda9' },
|
|
||||||
};
|
|
||||||
|
|
||||||
export const responseData = {
|
export const responseData = {
|
||||||
opportunities: {
|
opportunities: {
|
||||||
edges: [],
|
edges: [],
|
||||||
|
|||||||
@ -1,19 +1,27 @@
|
|||||||
import { ReactNode } from 'react';
|
import { ReactNode } from 'react';
|
||||||
import { MockedProvider } from '@apollo/client/testing';
|
import { MockedProvider } from '@apollo/client/testing';
|
||||||
|
import { mocked } from '@storybook/test';
|
||||||
import { act, renderHook } from '@testing-library/react';
|
import { act, renderHook } from '@testing-library/react';
|
||||||
import { RecoilRoot } from 'recoil';
|
import { RecoilRoot } from 'recoil';
|
||||||
|
import { v4 } from 'uuid';
|
||||||
|
|
||||||
|
import { CoreObjectNameSingular } from '@/object-metadata/types/CoreObjectNameSingular';
|
||||||
import {
|
import {
|
||||||
query,
|
query,
|
||||||
responseData,
|
response,
|
||||||
variables,
|
variables,
|
||||||
} from '@/object-record/hooks/__mocks__/useCreateManyRecords';
|
} from '@/object-record/hooks/__mocks__/useCreateManyRecords';
|
||||||
import { useCreateManyRecords } from '@/object-record/hooks/useCreateManyRecords';
|
import { useCreateManyRecords } from '@/object-record/hooks/useCreateManyRecords';
|
||||||
|
|
||||||
const people = [
|
jest.mock('uuid', () => ({
|
||||||
{ id: 'a7286b9a-c039-4a89-9567-2dfa7953cda9' },
|
v4: jest.fn(),
|
||||||
{ id: '37faabcd-cb39-4a0a-8618-7e3fda9afca0' },
|
}));
|
||||||
];
|
|
||||||
|
mocked(v4)
|
||||||
|
.mockReturnValueOnce(variables.data[0].id)
|
||||||
|
.mockReturnValueOnce(variables.data[1].id);
|
||||||
|
|
||||||
|
const input = variables.data.map(({ id: _id, ...personInput }) => personInput);
|
||||||
|
|
||||||
const mocks = [
|
const mocks = [
|
||||||
{
|
{
|
||||||
@ -23,10 +31,7 @@ const mocks = [
|
|||||||
},
|
},
|
||||||
result: jest.fn(() => ({
|
result: jest.fn(() => ({
|
||||||
data: {
|
data: {
|
||||||
createPeople: people.map((person) => ({
|
createPeople: response,
|
||||||
id: person.id,
|
|
||||||
...responseData,
|
|
||||||
})),
|
|
||||||
},
|
},
|
||||||
})),
|
})),
|
||||||
},
|
},
|
||||||
@ -43,19 +48,18 @@ const Wrapper = ({ children }: { children: ReactNode }) => (
|
|||||||
describe('useCreateManyRecords', () => {
|
describe('useCreateManyRecords', () => {
|
||||||
it('works as expected', async () => {
|
it('works as expected', async () => {
|
||||||
const { result } = renderHook(
|
const { result } = renderHook(
|
||||||
() => useCreateManyRecords({ objectNameSingular: 'person' }),
|
() =>
|
||||||
|
useCreateManyRecords({
|
||||||
|
objectNameSingular: CoreObjectNameSingular.Person,
|
||||||
|
}),
|
||||||
{
|
{
|
||||||
wrapper: Wrapper,
|
wrapper: Wrapper,
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
|
|
||||||
await act(async () => {
|
await act(async () => {
|
||||||
const res = await result.current.createManyRecords(people);
|
const res = await result.current.createManyRecords(input);
|
||||||
expect(res).toBeDefined();
|
expect(res).toEqual(response);
|
||||||
expect(Array.isArray(res)).toBe(true);
|
|
||||||
expect(res?.length).toBe(2);
|
|
||||||
expect(res?.[0].id).toBe(people[0].id);
|
|
||||||
expect(res?.[1].id).toBe(people[1].id);
|
|
||||||
});
|
});
|
||||||
|
|
||||||
expect(mocks[0].result).toHaveBeenCalled();
|
expect(mocks[0].result).toHaveBeenCalled();
|
||||||
|
|||||||
@ -3,24 +3,29 @@ import { MockedProvider } from '@apollo/client/testing';
|
|||||||
import { act, renderHook } from '@testing-library/react';
|
import { act, renderHook } from '@testing-library/react';
|
||||||
import { RecoilRoot } from 'recoil';
|
import { RecoilRoot } from 'recoil';
|
||||||
|
|
||||||
|
import { CoreObjectNameSingular } from '@/object-metadata/types/CoreObjectNameSingular';
|
||||||
import {
|
import {
|
||||||
query,
|
query,
|
||||||
responseData,
|
responseData,
|
||||||
variables,
|
|
||||||
} from '@/object-record/hooks/__mocks__/useCreateOneRecord';
|
} from '@/object-record/hooks/__mocks__/useCreateOneRecord';
|
||||||
import { useCreateOneRecord } from '@/object-record/hooks/useCreateOneRecord';
|
import { useCreateOneRecord } from '@/object-record/hooks/useCreateOneRecord';
|
||||||
|
|
||||||
const person = { id: 'a7286b9a-c039-4a89-9567-2dfa7953cda9' };
|
const personId = 'a7286b9a-c039-4a89-9567-2dfa7953cda9';
|
||||||
|
const input = { name: { firstName: 'John', lastName: 'Doe' } };
|
||||||
|
|
||||||
|
jest.mock('uuid', () => ({
|
||||||
|
v4: jest.fn(() => personId),
|
||||||
|
}));
|
||||||
|
|
||||||
const mocks = [
|
const mocks = [
|
||||||
{
|
{
|
||||||
request: {
|
request: {
|
||||||
query,
|
query,
|
||||||
variables,
|
variables: { input: { ...input, id: personId } },
|
||||||
},
|
},
|
||||||
result: jest.fn(() => ({
|
result: jest.fn(() => ({
|
||||||
data: {
|
data: {
|
||||||
createPerson: { ...person, ...responseData },
|
createPerson: { ...responseData, ...input, id: personId },
|
||||||
},
|
},
|
||||||
})),
|
})),
|
||||||
},
|
},
|
||||||
@ -37,16 +42,20 @@ const Wrapper = ({ children }: { children: ReactNode }) => (
|
|||||||
describe('useCreateOneRecord', () => {
|
describe('useCreateOneRecord', () => {
|
||||||
it('works as expected', async () => {
|
it('works as expected', async () => {
|
||||||
const { result } = renderHook(
|
const { result } = renderHook(
|
||||||
() => useCreateOneRecord({ objectNameSingular: 'person' }),
|
() =>
|
||||||
|
useCreateOneRecord({
|
||||||
|
objectNameSingular: CoreObjectNameSingular.Person,
|
||||||
|
}),
|
||||||
{
|
{
|
||||||
wrapper: Wrapper,
|
wrapper: Wrapper,
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
|
|
||||||
await act(async () => {
|
await act(async () => {
|
||||||
const res = await result.current.createOneRecord(person);
|
const res = await result.current.createOneRecord(input);
|
||||||
|
console.log('res', res);
|
||||||
expect(res).toBeDefined();
|
expect(res).toBeDefined();
|
||||||
expect(res).toHaveProperty('id', person.id);
|
expect(res).toHaveProperty('id', personId);
|
||||||
});
|
});
|
||||||
|
|
||||||
expect(mocks[0].result).toHaveBeenCalled();
|
expect(mocks[0].result).toHaveBeenCalled();
|
||||||
|
|||||||
@ -1,9 +1,11 @@
|
|||||||
import { useApolloClient } from '@apollo/client';
|
import { useApolloClient } from '@apollo/client';
|
||||||
|
import { v4 } from 'uuid';
|
||||||
|
|
||||||
import { triggerCreateRecordsOptimisticEffect } from '@/apollo/optimistic-effect/utils/triggerCreateRecordsOptimisticEffect';
|
import { triggerCreateRecordsOptimisticEffect } from '@/apollo/optimistic-effect/utils/triggerCreateRecordsOptimisticEffect';
|
||||||
|
import { useGetRelationMetadata } from '@/object-metadata/hooks/useGetRelationMetadata';
|
||||||
import { useObjectMetadataItem } from '@/object-metadata/hooks/useObjectMetadataItem';
|
import { useObjectMetadataItem } from '@/object-metadata/hooks/useObjectMetadataItem';
|
||||||
import { ObjectMetadataItemIdentifier } from '@/object-metadata/types/ObjectMetadataItemIdentifier';
|
import { ObjectMetadataItemIdentifier } from '@/object-metadata/types/ObjectMetadataItemIdentifier';
|
||||||
import { useGenerateCachedObjectRecord } from '@/object-record/cache/hooks/useGenerateCachedObjectRecord';
|
import { useGenerateObjectRecordOptimisticResponse } from '@/object-record/cache/hooks/useGenerateObjectRecordOptimisticResponse';
|
||||||
import { getCreateManyRecordsMutationResponseField } from '@/object-record/hooks/useGenerateCreateManyRecordMutation';
|
import { getCreateManyRecordsMutationResponseField } from '@/object-record/hooks/useGenerateCreateManyRecordMutation';
|
||||||
import { ObjectRecord } from '@/object-record/types/ObjectRecord';
|
import { ObjectRecord } from '@/object-record/types/ObjectRecord';
|
||||||
import { sanitizeRecordInput } from '@/object-record/utils/sanitizeRecordInput';
|
import { sanitizeRecordInput } from '@/object-record/utils/sanitizeRecordInput';
|
||||||
@ -13,29 +15,33 @@ export const useCreateManyRecords = <
|
|||||||
>({
|
>({
|
||||||
objectNameSingular,
|
objectNameSingular,
|
||||||
}: ObjectMetadataItemIdentifier) => {
|
}: ObjectMetadataItemIdentifier) => {
|
||||||
|
const apolloClient = useApolloClient();
|
||||||
|
|
||||||
const { objectMetadataItem, createManyRecordsMutation } =
|
const { objectMetadataItem, createManyRecordsMutation } =
|
||||||
useObjectMetadataItem({
|
useObjectMetadataItem({
|
||||||
objectNameSingular,
|
objectNameSingular,
|
||||||
});
|
});
|
||||||
|
|
||||||
const { generateCachedObjectRecord } = useGenerateCachedObjectRecord({
|
const { generateObjectRecordOptimisticResponse } =
|
||||||
objectMetadataItem,
|
useGenerateObjectRecordOptimisticResponse({
|
||||||
});
|
objectMetadataItem,
|
||||||
|
});
|
||||||
|
|
||||||
const apolloClient = useApolloClient();
|
const getRelationMetadata = useGetRelationMetadata();
|
||||||
|
|
||||||
const createManyRecords = async (data: Partial<CreatedObjectRecord>[]) => {
|
const createManyRecords = async (data: Partial<CreatedObjectRecord>[]) => {
|
||||||
const optimisticallyCreatedRecords = data.map((record) =>
|
const sanitizedCreateManyRecordsInput = data.map((input) =>
|
||||||
generateCachedObjectRecord<CreatedObjectRecord>(record),
|
|
||||||
);
|
|
||||||
|
|
||||||
const sanitizedCreateManyRecordsInput = data.map((input, index) =>
|
|
||||||
sanitizeRecordInput({
|
sanitizeRecordInput({
|
||||||
objectMetadataItem,
|
objectMetadataItem,
|
||||||
recordInput: { ...input, id: optimisticallyCreatedRecords[index].id },
|
recordInput: { ...input, id: v4() },
|
||||||
}),
|
}),
|
||||||
);
|
);
|
||||||
|
|
||||||
|
const optimisticallyCreatedRecords = sanitizedCreateManyRecordsInput.map(
|
||||||
|
(record) =>
|
||||||
|
generateObjectRecordOptimisticResponse<CreatedObjectRecord>(record),
|
||||||
|
);
|
||||||
|
|
||||||
const mutationResponseField = getCreateManyRecordsMutationResponseField(
|
const mutationResponseField = getCreateManyRecordsMutationResponseField(
|
||||||
objectMetadataItem.namePlural,
|
objectMetadataItem.namePlural,
|
||||||
);
|
);
|
||||||
@ -57,6 +63,7 @@ export const useCreateManyRecords = <
|
|||||||
cache,
|
cache,
|
||||||
objectMetadataItem,
|
objectMetadataItem,
|
||||||
records,
|
records,
|
||||||
|
getRelationMetadata,
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|||||||
@ -3,7 +3,7 @@ import { v4 } from 'uuid';
|
|||||||
import { useObjectMetadataItem } from '@/object-metadata/hooks/useObjectMetadataItem';
|
import { useObjectMetadataItem } from '@/object-metadata/hooks/useObjectMetadataItem';
|
||||||
import { ObjectMetadataItemIdentifier } from '@/object-metadata/types/ObjectMetadataItemIdentifier';
|
import { ObjectMetadataItemIdentifier } from '@/object-metadata/types/ObjectMetadataItemIdentifier';
|
||||||
import { useAddRecordInCache } from '@/object-record/cache/hooks/useAddRecordInCache';
|
import { useAddRecordInCache } from '@/object-record/cache/hooks/useAddRecordInCache';
|
||||||
import { useGenerateCachedObjectRecord } from '@/object-record/cache/hooks/useGenerateCachedObjectRecord';
|
import { useGenerateObjectRecordOptimisticResponse } from '@/object-record/cache/hooks/useGenerateObjectRecordOptimisticResponse';
|
||||||
import { ObjectRecord } from '@/object-record/types/ObjectRecord';
|
import { ObjectRecord } from '@/object-record/types/ObjectRecord';
|
||||||
|
|
||||||
export const useCreateManyRecordsInCache = <T extends ObjectRecord>({
|
export const useCreateManyRecordsInCache = <T extends ObjectRecord>({
|
||||||
@ -13,9 +13,10 @@ export const useCreateManyRecordsInCache = <T extends ObjectRecord>({
|
|||||||
objectNameSingular,
|
objectNameSingular,
|
||||||
});
|
});
|
||||||
|
|
||||||
const { generateCachedObjectRecord } = useGenerateCachedObjectRecord({
|
const { generateObjectRecordOptimisticResponse } =
|
||||||
objectMetadataItem,
|
useGenerateObjectRecordOptimisticResponse({
|
||||||
});
|
objectMetadataItem,
|
||||||
|
});
|
||||||
|
|
||||||
const addRecordInCache = useAddRecordInCache({
|
const addRecordInCache = useAddRecordInCache({
|
||||||
objectMetadataItem,
|
objectMetadataItem,
|
||||||
@ -30,9 +31,8 @@ export const useCreateManyRecordsInCache = <T extends ObjectRecord>({
|
|||||||
const createdRecordsInCache = [] as T[];
|
const createdRecordsInCache = [] as T[];
|
||||||
|
|
||||||
for (const record of recordsWithId) {
|
for (const record of recordsWithId) {
|
||||||
const generatedCachedObjectRecord = generateCachedObjectRecord<T>({
|
const generatedCachedObjectRecord =
|
||||||
...record,
|
generateObjectRecordOptimisticResponse<T>(record);
|
||||||
});
|
|
||||||
|
|
||||||
if (generatedCachedObjectRecord) {
|
if (generatedCachedObjectRecord) {
|
||||||
addRecordInCache(generatedCachedObjectRecord);
|
addRecordInCache(generatedCachedObjectRecord);
|
||||||
|
|||||||
@ -1,8 +1,10 @@
|
|||||||
import { useApolloClient } from '@apollo/client';
|
import { useApolloClient } from '@apollo/client';
|
||||||
|
import { v4 } from 'uuid';
|
||||||
|
|
||||||
import { triggerCreateRecordsOptimisticEffect } from '@/apollo/optimistic-effect/utils/triggerCreateRecordsOptimisticEffect';
|
import { triggerCreateRecordsOptimisticEffect } from '@/apollo/optimistic-effect/utils/triggerCreateRecordsOptimisticEffect';
|
||||||
|
import { useGetRelationMetadata } from '@/object-metadata/hooks/useGetRelationMetadata';
|
||||||
import { useObjectMetadataItem } from '@/object-metadata/hooks/useObjectMetadataItem';
|
import { useObjectMetadataItem } from '@/object-metadata/hooks/useObjectMetadataItem';
|
||||||
import { useGenerateCachedObjectRecord } from '@/object-record/cache/hooks/useGenerateCachedObjectRecord';
|
import { useGenerateObjectRecordOptimisticResponse } from '@/object-record/cache/hooks/useGenerateObjectRecordOptimisticResponse';
|
||||||
import { getCreateOneRecordMutationResponseField } from '@/object-record/hooks/useGenerateCreateOneRecordMutation';
|
import { getCreateOneRecordMutationResponseField } from '@/object-record/hooks/useGenerateCreateOneRecordMutation';
|
||||||
import { ObjectRecord } from '@/object-record/types/ObjectRecord';
|
import { ObjectRecord } from '@/object-record/types/ObjectRecord';
|
||||||
import { sanitizeRecordInput } from '@/object-record/utils/sanitizeRecordInput';
|
import { sanitizeRecordInput } from '@/object-record/utils/sanitizeRecordInput';
|
||||||
@ -16,25 +18,27 @@ export const useCreateOneRecord = <
|
|||||||
>({
|
>({
|
||||||
objectNameSingular,
|
objectNameSingular,
|
||||||
}: useCreateOneRecordProps) => {
|
}: useCreateOneRecordProps) => {
|
||||||
|
const apolloClient = useApolloClient();
|
||||||
|
|
||||||
const { objectMetadataItem, createOneRecordMutation } = useObjectMetadataItem(
|
const { objectMetadataItem, createOneRecordMutation } = useObjectMetadataItem(
|
||||||
{ objectNameSingular },
|
{ objectNameSingular },
|
||||||
);
|
);
|
||||||
|
|
||||||
// TODO: type this with a minimal type at least with Record<string, any>
|
const { generateObjectRecordOptimisticResponse } =
|
||||||
const apolloClient = useApolloClient();
|
useGenerateObjectRecordOptimisticResponse({
|
||||||
|
objectMetadataItem,
|
||||||
|
});
|
||||||
|
|
||||||
const { generateCachedObjectRecord } = useGenerateCachedObjectRecord({
|
const getRelationMetadata = useGetRelationMetadata();
|
||||||
objectMetadataItem,
|
|
||||||
});
|
|
||||||
|
|
||||||
const createOneRecord = async (input: Partial<CreatedObjectRecord>) => {
|
const createOneRecord = async (input: Partial<CreatedObjectRecord>) => {
|
||||||
const sanitizedCreateOneRecordInput = sanitizeRecordInput({
|
const sanitizedCreateOneRecordInput = sanitizeRecordInput({
|
||||||
objectMetadataItem,
|
objectMetadataItem,
|
||||||
recordInput: input,
|
recordInput: { ...input, id: v4() },
|
||||||
});
|
});
|
||||||
|
|
||||||
const optimisticallyCreatedRecord =
|
const optimisticallyCreatedRecord =
|
||||||
generateCachedObjectRecord<CreatedObjectRecord>({
|
generateObjectRecordOptimisticResponse<CreatedObjectRecord>({
|
||||||
...input,
|
...input,
|
||||||
...sanitizedCreateOneRecordInput,
|
...sanitizedCreateOneRecordInput,
|
||||||
});
|
});
|
||||||
@ -45,10 +49,7 @@ export const useCreateOneRecord = <
|
|||||||
const createdObject = await apolloClient.mutate({
|
const createdObject = await apolloClient.mutate({
|
||||||
mutation: createOneRecordMutation,
|
mutation: createOneRecordMutation,
|
||||||
variables: {
|
variables: {
|
||||||
input: {
|
input: sanitizedCreateOneRecordInput,
|
||||||
...sanitizedCreateOneRecordInput,
|
|
||||||
id: optimisticallyCreatedRecord.id,
|
|
||||||
},
|
|
||||||
},
|
},
|
||||||
optimisticResponse: {
|
optimisticResponse: {
|
||||||
[mutationResponseField]: optimisticallyCreatedRecord,
|
[mutationResponseField]: optimisticallyCreatedRecord,
|
||||||
@ -62,6 +63,7 @@ export const useCreateOneRecord = <
|
|||||||
cache,
|
cache,
|
||||||
objectMetadataItem,
|
objectMetadataItem,
|
||||||
records: [record],
|
records: [record],
|
||||||
|
getRelationMetadata,
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|||||||
@ -1,6 +1,6 @@
|
|||||||
import { useObjectMetadataItem } from '@/object-metadata/hooks/useObjectMetadataItem';
|
import { useObjectMetadataItem } from '@/object-metadata/hooks/useObjectMetadataItem';
|
||||||
import { useAddRecordInCache } from '@/object-record/cache/hooks/useAddRecordInCache';
|
import { useAddRecordInCache } from '@/object-record/cache/hooks/useAddRecordInCache';
|
||||||
import { useGenerateCachedObjectRecord } from '@/object-record/cache/hooks/useGenerateCachedObjectRecord';
|
import { useGenerateObjectRecordOptimisticResponse } from '@/object-record/cache/hooks/useGenerateObjectRecordOptimisticResponse';
|
||||||
import { ObjectRecord } from '@/object-record/types/ObjectRecord';
|
import { ObjectRecord } from '@/object-record/types/ObjectRecord';
|
||||||
|
|
||||||
type useCreateOneRecordInCacheProps = {
|
type useCreateOneRecordInCacheProps = {
|
||||||
@ -14,19 +14,18 @@ export const useCreateOneRecordInCache = <T>({
|
|||||||
objectNameSingular,
|
objectNameSingular,
|
||||||
});
|
});
|
||||||
|
|
||||||
const { generateCachedObjectRecord } = useGenerateCachedObjectRecord({
|
const { generateObjectRecordOptimisticResponse } =
|
||||||
objectMetadataItem,
|
useGenerateObjectRecordOptimisticResponse({
|
||||||
});
|
objectMetadataItem,
|
||||||
|
});
|
||||||
|
|
||||||
const addRecordInCache = useAddRecordInCache({
|
const addRecordInCache = useAddRecordInCache({
|
||||||
objectMetadataItem,
|
objectMetadataItem,
|
||||||
});
|
});
|
||||||
|
|
||||||
const createOneRecordInCache = async (input: ObjectRecord) => {
|
const createOneRecordInCache = async (input: ObjectRecord) => {
|
||||||
const generatedCachedObjectRecord = generateCachedObjectRecord({
|
const generatedCachedObjectRecord =
|
||||||
createdAt: new Date().toISOString(),
|
generateObjectRecordOptimisticResponse(input);
|
||||||
...input,
|
|
||||||
});
|
|
||||||
|
|
||||||
addRecordInCache(generatedCachedObjectRecord);
|
addRecordInCache(generatedCachedObjectRecord);
|
||||||
|
|
||||||
|
|||||||
@ -1,13 +1,9 @@
|
|||||||
import { useApolloClient } from '@apollo/client';
|
import { useApolloClient } from '@apollo/client';
|
||||||
|
|
||||||
import { isObjectRecordConnection } from '@/apollo/optimistic-effect/utils/isObjectRecordConnection';
|
|
||||||
import { triggerDeleteRecordsOptimisticEffect } from '@/apollo/optimistic-effect/utils/triggerDeleteRecordsOptimisticEffect';
|
import { triggerDeleteRecordsOptimisticEffect } from '@/apollo/optimistic-effect/utils/triggerDeleteRecordsOptimisticEffect';
|
||||||
import { triggerDetachRelationOptimisticEffect } from '@/apollo/optimistic-effect/utils/triggerDetachRelationOptimisticEffect';
|
|
||||||
import { useGetRelationMetadata } from '@/object-metadata/hooks/useGetRelationMetadata';
|
import { useGetRelationMetadata } from '@/object-metadata/hooks/useGetRelationMetadata';
|
||||||
import { useObjectMetadataItem } from '@/object-metadata/hooks/useObjectMetadataItem';
|
import { useObjectMetadataItem } from '@/object-metadata/hooks/useObjectMetadataItem';
|
||||||
import { getDeleteManyRecordsMutationResponseField } from '@/object-record/hooks/useGenerateDeleteManyRecordMutation';
|
import { getDeleteManyRecordsMutationResponseField } from '@/object-record/hooks/useGenerateDeleteManyRecordMutation';
|
||||||
import { ObjectRecord } from '@/object-record/types/ObjectRecord';
|
|
||||||
import { ObjectRecordConnection } from '@/object-record/types/ObjectRecordConnection';
|
|
||||||
import { isDefined } from '~/utils/isDefined';
|
import { isDefined } from '~/utils/isDefined';
|
||||||
import { capitalize } from '~/utils/string/capitalize';
|
import { capitalize } from '~/utils/string/capitalize';
|
||||||
|
|
||||||
@ -19,13 +15,13 @@ type useDeleteOneRecordProps = {
|
|||||||
export const useDeleteManyRecords = ({
|
export const useDeleteManyRecords = ({
|
||||||
objectNameSingular,
|
objectNameSingular,
|
||||||
}: useDeleteOneRecordProps) => {
|
}: useDeleteOneRecordProps) => {
|
||||||
|
const apolloClient = useApolloClient();
|
||||||
|
|
||||||
const { objectMetadataItem, deleteManyRecordsMutation, getRecordFromCache } =
|
const { objectMetadataItem, deleteManyRecordsMutation, getRecordFromCache } =
|
||||||
useObjectMetadataItem({ objectNameSingular });
|
useObjectMetadataItem({ objectNameSingular });
|
||||||
|
|
||||||
const getRelationMetadata = useGetRelationMetadata();
|
const getRelationMetadata = useGetRelationMetadata();
|
||||||
|
|
||||||
const apolloClient = useApolloClient();
|
|
||||||
|
|
||||||
const mutationResponseField = getDeleteManyRecordsMutationResponseField(
|
const mutationResponseField = getDeleteManyRecordsMutationResponseField(
|
||||||
objectMetadataItem.namePlural,
|
objectMetadataItem.namePlural,
|
||||||
);
|
);
|
||||||
@ -47,49 +43,15 @@ export const useDeleteManyRecords = ({
|
|||||||
|
|
||||||
if (!records?.length) return;
|
if (!records?.length) return;
|
||||||
|
|
||||||
objectMetadataItem.fields.forEach((fieldMetadataItem) => {
|
const cachedRecords = records
|
||||||
const relationMetadata = getRelationMetadata({ fieldMetadataItem });
|
.map((record) => getRecordFromCache(record.id, cache))
|
||||||
|
.filter(isDefined);
|
||||||
if (!relationMetadata) return;
|
|
||||||
|
|
||||||
const { relationObjectMetadataItem, relationFieldMetadataItem } =
|
|
||||||
relationMetadata;
|
|
||||||
|
|
||||||
records.forEach((record) => {
|
|
||||||
const cachedRecord = getRecordFromCache(record.id, cache);
|
|
||||||
|
|
||||||
if (!cachedRecord) return;
|
|
||||||
|
|
||||||
const previousFieldValue:
|
|
||||||
| ObjectRecordConnection
|
|
||||||
| ObjectRecord
|
|
||||||
| null = cachedRecord[fieldMetadataItem.name];
|
|
||||||
|
|
||||||
const relationRecordIds = isObjectRecordConnection(
|
|
||||||
relationObjectMetadataItem.nameSingular,
|
|
||||||
previousFieldValue,
|
|
||||||
)
|
|
||||||
? previousFieldValue.edges.map(({ node }) => node.id)
|
|
||||||
: [previousFieldValue?.id].filter(isDefined);
|
|
||||||
|
|
||||||
relationRecordIds.forEach((relationRecordId) =>
|
|
||||||
triggerDetachRelationOptimisticEffect({
|
|
||||||
cache,
|
|
||||||
objectNameSingular,
|
|
||||||
recordId: record.id,
|
|
||||||
relationObjectMetadataNameSingular:
|
|
||||||
relationObjectMetadataItem.nameSingular,
|
|
||||||
relationFieldName: relationFieldMetadataItem.name,
|
|
||||||
relationRecordId,
|
|
||||||
}),
|
|
||||||
);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
triggerDeleteRecordsOptimisticEffect({
|
triggerDeleteRecordsOptimisticEffect({
|
||||||
cache,
|
cache,
|
||||||
objectMetadataItem,
|
objectMetadataItem,
|
||||||
records: records,
|
records: cachedRecords,
|
||||||
|
getRelationMetadata,
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|||||||
@ -1,15 +1,10 @@
|
|||||||
import { useCallback } from 'react';
|
import { useCallback } from 'react';
|
||||||
import { useApolloClient } from '@apollo/client';
|
import { useApolloClient } from '@apollo/client';
|
||||||
|
|
||||||
import { isObjectRecordConnection } from '@/apollo/optimistic-effect/utils/isObjectRecordConnection';
|
|
||||||
import { triggerDeleteRecordsOptimisticEffect } from '@/apollo/optimistic-effect/utils/triggerDeleteRecordsOptimisticEffect';
|
import { triggerDeleteRecordsOptimisticEffect } from '@/apollo/optimistic-effect/utils/triggerDeleteRecordsOptimisticEffect';
|
||||||
import { triggerDetachRelationOptimisticEffect } from '@/apollo/optimistic-effect/utils/triggerDetachRelationOptimisticEffect';
|
|
||||||
import { useGetRelationMetadata } from '@/object-metadata/hooks/useGetRelationMetadata';
|
import { useGetRelationMetadata } from '@/object-metadata/hooks/useGetRelationMetadata';
|
||||||
import { useObjectMetadataItem } from '@/object-metadata/hooks/useObjectMetadataItem';
|
import { useObjectMetadataItem } from '@/object-metadata/hooks/useObjectMetadataItem';
|
||||||
import { ObjectRecord } from '@/object-record/types/ObjectRecord';
|
|
||||||
import { ObjectRecordConnection } from '@/object-record/types/ObjectRecordConnection';
|
|
||||||
import { getDeleteOneRecordMutationResponseField } from '@/object-record/utils/generateDeleteOneRecordMutation';
|
import { getDeleteOneRecordMutationResponseField } from '@/object-record/utils/generateDeleteOneRecordMutation';
|
||||||
import { isDefined } from '~/utils/isDefined';
|
|
||||||
import { capitalize } from '~/utils/string/capitalize';
|
import { capitalize } from '~/utils/string/capitalize';
|
||||||
|
|
||||||
type useDeleteOneRecordProps = {
|
type useDeleteOneRecordProps = {
|
||||||
@ -20,13 +15,13 @@ type useDeleteOneRecordProps = {
|
|||||||
export const useDeleteOneRecord = ({
|
export const useDeleteOneRecord = ({
|
||||||
objectNameSingular,
|
objectNameSingular,
|
||||||
}: useDeleteOneRecordProps) => {
|
}: useDeleteOneRecordProps) => {
|
||||||
|
const apolloClient = useApolloClient();
|
||||||
|
|
||||||
const { objectMetadataItem, deleteOneRecordMutation, getRecordFromCache } =
|
const { objectMetadataItem, deleteOneRecordMutation, getRecordFromCache } =
|
||||||
useObjectMetadataItem({ objectNameSingular });
|
useObjectMetadataItem({ objectNameSingular });
|
||||||
|
|
||||||
const getRelationMetadata = useGetRelationMetadata();
|
const getRelationMetadata = useGetRelationMetadata();
|
||||||
|
|
||||||
const apolloClient = useApolloClient();
|
|
||||||
|
|
||||||
const mutationResponseField =
|
const mutationResponseField =
|
||||||
getDeleteOneRecordMutationResponseField(objectNameSingular);
|
getDeleteOneRecordMutationResponseField(objectNameSingular);
|
||||||
|
|
||||||
@ -46,47 +41,15 @@ export const useDeleteOneRecord = ({
|
|||||||
|
|
||||||
if (!record) return;
|
if (!record) return;
|
||||||
|
|
||||||
objectMetadataItem.fields.forEach((fieldMetadataItem) => {
|
const cachedRecord = getRecordFromCache(record.id, cache);
|
||||||
const relationMetadata = getRelationMetadata({ fieldMetadataItem });
|
|
||||||
|
|
||||||
if (!relationMetadata) return;
|
if (!cachedRecord) return;
|
||||||
|
|
||||||
const { relationObjectMetadataItem, relationFieldMetadataItem } =
|
|
||||||
relationMetadata;
|
|
||||||
|
|
||||||
const cachedRecord = getRecordFromCache(record.id, cache);
|
|
||||||
|
|
||||||
if (!cachedRecord) return;
|
|
||||||
|
|
||||||
const previousFieldValue:
|
|
||||||
| ObjectRecordConnection
|
|
||||||
| ObjectRecord
|
|
||||||
| null = cachedRecord[fieldMetadataItem.name];
|
|
||||||
|
|
||||||
const relationRecordIds = isObjectRecordConnection(
|
|
||||||
relationObjectMetadataItem.nameSingular,
|
|
||||||
previousFieldValue,
|
|
||||||
)
|
|
||||||
? previousFieldValue.edges.map(({ node }) => node.id)
|
|
||||||
: [previousFieldValue?.id].filter(isDefined);
|
|
||||||
|
|
||||||
relationRecordIds.forEach((relationRecordId) =>
|
|
||||||
triggerDetachRelationOptimisticEffect({
|
|
||||||
cache,
|
|
||||||
objectNameSingular,
|
|
||||||
recordId: record.id,
|
|
||||||
relationObjectMetadataNameSingular:
|
|
||||||
relationObjectMetadataItem.nameSingular,
|
|
||||||
relationFieldName: relationFieldMetadataItem.name,
|
|
||||||
relationRecordId,
|
|
||||||
}),
|
|
||||||
);
|
|
||||||
});
|
|
||||||
|
|
||||||
triggerDeleteRecordsOptimisticEffect({
|
triggerDeleteRecordsOptimisticEffect({
|
||||||
cache,
|
cache,
|
||||||
objectMetadataItem,
|
objectMetadataItem,
|
||||||
records: [record],
|
records: [cachedRecord],
|
||||||
|
getRelationMetadata,
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|||||||
@ -1,16 +1,12 @@
|
|||||||
import { useApolloClient } from '@apollo/client';
|
import { useApolloClient } from '@apollo/client';
|
||||||
|
|
||||||
import { isObjectRecordConnection } from '@/apollo/optimistic-effect/utils/isObjectRecordConnection';
|
|
||||||
import { triggerAttachRelationOptimisticEffect } from '@/apollo/optimistic-effect/utils/triggerAttachRelationOptimisticEffect';
|
|
||||||
import { triggerDetachRelationOptimisticEffect } from '@/apollo/optimistic-effect/utils/triggerDetachRelationOptimisticEffect';
|
|
||||||
import { triggerUpdateRecordOptimisticEffect } from '@/apollo/optimistic-effect/utils/triggerUpdateRecordOptimisticEffect';
|
import { triggerUpdateRecordOptimisticEffect } from '@/apollo/optimistic-effect/utils/triggerUpdateRecordOptimisticEffect';
|
||||||
import { useGetRelationMetadata } from '@/object-metadata/hooks/useGetRelationMetadata';
|
import { useGetRelationMetadata } from '@/object-metadata/hooks/useGetRelationMetadata';
|
||||||
import { useObjectMetadataItem } from '@/object-metadata/hooks/useObjectMetadataItem';
|
import { useObjectMetadataItem } from '@/object-metadata/hooks/useObjectMetadataItem';
|
||||||
|
import { useGenerateObjectRecordOptimisticResponse } from '@/object-record/cache/hooks/useGenerateObjectRecordOptimisticResponse';
|
||||||
import { getUpdateOneRecordMutationResponseField } from '@/object-record/hooks/useGenerateUpdateOneRecordMutation';
|
import { getUpdateOneRecordMutationResponseField } from '@/object-record/hooks/useGenerateUpdateOneRecordMutation';
|
||||||
import { ObjectRecord } from '@/object-record/types/ObjectRecord';
|
import { ObjectRecord } from '@/object-record/types/ObjectRecord';
|
||||||
import { sanitizeRecordInput } from '@/object-record/utils/sanitizeRecordInput';
|
import { sanitizeRecordInput } from '@/object-record/utils/sanitizeRecordInput';
|
||||||
import { isDeeplyEqual } from '~/utils/isDeeplyEqual';
|
|
||||||
import { capitalize } from '~/utils/string/capitalize';
|
|
||||||
|
|
||||||
type useUpdateOneRecordProps = {
|
type useUpdateOneRecordProps = {
|
||||||
objectNameSingular: string;
|
objectNameSingular: string;
|
||||||
@ -21,12 +17,17 @@ export const useUpdateOneRecord = <
|
|||||||
>({
|
>({
|
||||||
objectNameSingular,
|
objectNameSingular,
|
||||||
}: useUpdateOneRecordProps) => {
|
}: useUpdateOneRecordProps) => {
|
||||||
|
const apolloClient = useApolloClient();
|
||||||
|
|
||||||
const { objectMetadataItem, updateOneRecordMutation, getRecordFromCache } =
|
const { objectMetadataItem, updateOneRecordMutation, getRecordFromCache } =
|
||||||
useObjectMetadataItem({ objectNameSingular });
|
useObjectMetadataItem({ objectNameSingular });
|
||||||
|
|
||||||
const getRelationMetadata = useGetRelationMetadata();
|
const { generateObjectRecordOptimisticResponse } =
|
||||||
|
useGenerateObjectRecordOptimisticResponse({
|
||||||
|
objectMetadataItem,
|
||||||
|
});
|
||||||
|
|
||||||
const apolloClient = useApolloClient();
|
const getRelationMetadata = useGetRelationMetadata();
|
||||||
|
|
||||||
const updateOneRecord = async ({
|
const updateOneRecord = async ({
|
||||||
idToUpdate,
|
idToUpdate,
|
||||||
@ -42,13 +43,11 @@ export const useUpdateOneRecord = <
|
|||||||
recordInput: updateOneRecordInput,
|
recordInput: updateOneRecordInput,
|
||||||
});
|
});
|
||||||
|
|
||||||
const optimisticallyUpdatedRecord = {
|
const optimisticallyUpdatedRecord = generateObjectRecordOptimisticResponse({
|
||||||
...(cachedRecord ?? {}),
|
...(cachedRecord ?? {}),
|
||||||
...updateOneRecordInput,
|
|
||||||
...sanitizedUpdateOneRecordInput,
|
...sanitizedUpdateOneRecordInput,
|
||||||
__typename: capitalize(objectNameSingular),
|
|
||||||
id: idToUpdate,
|
id: idToUpdate,
|
||||||
};
|
});
|
||||||
|
|
||||||
const mutationResponseField =
|
const mutationResponseField =
|
||||||
getUpdateOneRecordMutationResponseField(objectNameSingular);
|
getUpdateOneRecordMutationResponseField(objectNameSingular);
|
||||||
@ -65,60 +64,14 @@ export const useUpdateOneRecord = <
|
|||||||
update: (cache, { data }) => {
|
update: (cache, { data }) => {
|
||||||
const record = data?.[mutationResponseField];
|
const record = data?.[mutationResponseField];
|
||||||
|
|
||||||
if (!record) return;
|
if (!record || !cachedRecord) return;
|
||||||
|
|
||||||
objectMetadataItem.fields.forEach((fieldMetadataItem) => {
|
|
||||||
const relationMetadata = getRelationMetadata({ fieldMetadataItem });
|
|
||||||
|
|
||||||
if (!relationMetadata) return;
|
|
||||||
|
|
||||||
const { relationObjectMetadataItem, relationFieldMetadataItem } =
|
|
||||||
relationMetadata;
|
|
||||||
|
|
||||||
const previousFieldValue = cachedRecord?.[fieldMetadataItem.name];
|
|
||||||
const nextFieldValue =
|
|
||||||
updateOneRecordInput[fieldMetadataItem.name] ?? null;
|
|
||||||
|
|
||||||
if (
|
|
||||||
!(fieldMetadataItem.name in updateOneRecordInput) ||
|
|
||||||
isObjectRecordConnection(
|
|
||||||
relationObjectMetadataItem.nameSingular,
|
|
||||||
previousFieldValue,
|
|
||||||
) ||
|
|
||||||
isDeeplyEqual(previousFieldValue, nextFieldValue)
|
|
||||||
) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (previousFieldValue) {
|
|
||||||
triggerDetachRelationOptimisticEffect({
|
|
||||||
cache,
|
|
||||||
objectNameSingular,
|
|
||||||
recordId: record.id,
|
|
||||||
relationObjectMetadataNameSingular:
|
|
||||||
relationObjectMetadataItem.nameSingular,
|
|
||||||
relationFieldName: relationFieldMetadataItem.name,
|
|
||||||
relationRecordId: previousFieldValue.id,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
if (nextFieldValue) {
|
|
||||||
triggerAttachRelationOptimisticEffect({
|
|
||||||
cache,
|
|
||||||
objectNameSingular,
|
|
||||||
recordId: record.id,
|
|
||||||
relationObjectMetadataNameSingular:
|
|
||||||
relationObjectMetadataItem.nameSingular,
|
|
||||||
relationFieldName: relationFieldMetadataItem.name,
|
|
||||||
relationRecordId: nextFieldValue.id,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
triggerUpdateRecordOptimisticEffect({
|
triggerUpdateRecordOptimisticEffect({
|
||||||
cache,
|
cache,
|
||||||
objectMetadataItem,
|
objectMetadataItem,
|
||||||
record,
|
previousRecord: cachedRecord,
|
||||||
|
nextRecord: record,
|
||||||
|
getRelationMetadata,
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|||||||
@ -13,22 +13,22 @@ export const sanitizeRecordInput = ({
|
|||||||
return Object.fromEntries(
|
return Object.fromEntries(
|
||||||
Object.entries(recordInput)
|
Object.entries(recordInput)
|
||||||
.map<[string, unknown] | undefined>(([fieldName, fieldValue]) => {
|
.map<[string, unknown] | undefined>(([fieldName, fieldValue]) => {
|
||||||
const fieldDefinition = objectMetadataItem.fields.find(
|
const fieldMetadataItem = objectMetadataItem.fields.find(
|
||||||
(field) => field.name === fieldName,
|
(field) => field.name === fieldName,
|
||||||
);
|
);
|
||||||
|
|
||||||
if (!fieldDefinition) return undefined;
|
if (!fieldMetadataItem) return undefined;
|
||||||
|
|
||||||
if (
|
if (
|
||||||
fieldDefinition.type === FieldMetadataType.Relation &&
|
fieldMetadataItem.type === FieldMetadataType.Relation &&
|
||||||
isFieldRelationValue(fieldValue)
|
isFieldRelationValue(fieldValue)
|
||||||
) {
|
) {
|
||||||
const relationIdFieldName = `${fieldDefinition.name}Id`;
|
const relationIdFieldName = `${fieldMetadataItem.name}Id`;
|
||||||
const relationIdFieldDefinition = objectMetadataItem.fields.find(
|
const relationIdFieldMetadataItem = objectMetadataItem.fields.find(
|
||||||
(field) => field.name === relationIdFieldName,
|
(field) => field.name === relationIdFieldName,
|
||||||
);
|
);
|
||||||
|
|
||||||
return relationIdFieldDefinition
|
return relationIdFieldMetadataItem
|
||||||
? [relationIdFieldName, fieldValue?.id ?? null]
|
? [relationIdFieldName, fieldValue?.id ?? null]
|
||||||
: undefined;
|
: undefined;
|
||||||
}
|
}
|
||||||
|
|||||||
@ -34,7 +34,7 @@ export const currentPipelineId = 'f088c8c9-05d2-4276-b065-b863cc7d0b33';
|
|||||||
|
|
||||||
const data = {
|
const data = {
|
||||||
color: 'yellow',
|
color: 'yellow',
|
||||||
id: 'columnId',
|
id: mockId,
|
||||||
position: 1,
|
position: 1,
|
||||||
name: 'Column Title',
|
name: 'Column Title',
|
||||||
};
|
};
|
||||||
|
|||||||
Reference in New Issue
Block a user