fix: detach relation records in cache on record deletion (#3707)
* fix: detach relation records in cache on record deletion * fix: fix useGetRelationMetadata tests
This commit is contained in:
@ -1,76 +0,0 @@
|
||||
import { useRecoilCallback } from 'recoil';
|
||||
|
||||
import { TriggerUpdateRelationFieldOptimisticEffectParams } from '@/apollo/optimistic-effect/utils/triggerUpdateRelationFieldOptimisticEffect';
|
||||
import { objectMetadataItemFamilySelector } from '@/object-metadata/states/objectMetadataItemFamilySelector';
|
||||
import { ObjectMetadataItem } from '@/object-metadata/types/ObjectMetadataItem';
|
||||
import { ObjectRecord } from '@/object-record/types/ObjectRecord';
|
||||
import { FieldMetadataType } from '~/generated-metadata/graphql';
|
||||
|
||||
export const useGetRelationFieldsToOptimisticallyUpdate = () =>
|
||||
useRecoilCallback(
|
||||
({ snapshot }) =>
|
||||
<UpdatedObjectRecord extends ObjectRecord = ObjectRecord>({
|
||||
cachedRecord,
|
||||
objectMetadataItem,
|
||||
updateRecordInput,
|
||||
}: {
|
||||
cachedRecord: UpdatedObjectRecord & { __typename: string };
|
||||
objectMetadataItem: ObjectMetadataItem;
|
||||
updateRecordInput: Partial<Omit<UpdatedObjectRecord, 'id'>>;
|
||||
}) =>
|
||||
Object.entries(updateRecordInput).reduce<
|
||||
Pick<
|
||||
TriggerUpdateRelationFieldOptimisticEffectParams,
|
||||
| 'relationObjectMetadataNameSingular'
|
||||
| 'relationFieldName'
|
||||
| 'previousRelationRecord'
|
||||
| 'nextRelationRecord'
|
||||
>[]
|
||||
>((result, [fieldName, nextRelationRecord]) => {
|
||||
const fieldDefinition = objectMetadataItem.fields.find(
|
||||
(fieldMetadataItem) => fieldMetadataItem.name === fieldName,
|
||||
);
|
||||
|
||||
if (fieldDefinition?.type !== FieldMetadataType.Relation)
|
||||
return result;
|
||||
|
||||
const relationObjectMetadataNameSingular = (
|
||||
fieldDefinition.toRelationMetadata?.fromObjectMetadata ||
|
||||
fieldDefinition.fromRelationMetadata?.toObjectMetadata
|
||||
)?.nameSingular;
|
||||
const relationFieldMetadataId =
|
||||
fieldDefinition.toRelationMetadata?.fromFieldMetadataId ||
|
||||
fieldDefinition.fromRelationMetadata?.toFieldMetadataId;
|
||||
|
||||
if (!relationObjectMetadataNameSingular || !relationFieldMetadataId)
|
||||
return result;
|
||||
|
||||
const relationObjectMetadataItem = snapshot
|
||||
.getLoadable(
|
||||
objectMetadataItemFamilySelector({
|
||||
objectName: relationObjectMetadataNameSingular,
|
||||
objectNameType: 'singular',
|
||||
}),
|
||||
)
|
||||
.valueOrThrow();
|
||||
|
||||
if (!relationObjectMetadataItem) return result;
|
||||
|
||||
const relationFieldName = relationObjectMetadataItem.fields.find(
|
||||
(fieldMetadataItem) =>
|
||||
fieldMetadataItem.id === relationFieldMetadataId,
|
||||
)?.name;
|
||||
|
||||
if (!relationFieldName) return result;
|
||||
|
||||
return [
|
||||
...result,
|
||||
{
|
||||
relationObjectMetadataNameSingular,
|
||||
relationFieldName,
|
||||
previousRelationRecord: cachedRecord[fieldName],
|
||||
nextRelationRecord,
|
||||
},
|
||||
];
|
||||
}, []),
|
||||
);
|
||||
@ -4,15 +4,24 @@ import { z } from 'zod';
|
||||
import { CachedObjectRecordConnection } from '@/apollo/types/CachedObjectRecordConnection';
|
||||
import { capitalize } from '~/utils/string/capitalize';
|
||||
|
||||
export const isCachedObjectConnection = (
|
||||
export const isCachedObjectRecordConnection = (
|
||||
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);
|
||||
@ -0,0 +1,28 @@
|
||||
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),
|
||||
edges: z.array(
|
||||
z.object({
|
||||
__typename: z.literal(objectEdgeTypeName),
|
||||
node: z.object({
|
||||
id: z.string().uuid(),
|
||||
}),
|
||||
}),
|
||||
),
|
||||
});
|
||||
const connectionValidation = objectConnectionSchema.safeParse(value);
|
||||
|
||||
return connectionValidation.success;
|
||||
};
|
||||
@ -0,0 +1,59 @@
|
||||
import { ApolloCache, StoreObject } from '@apollo/client';
|
||||
|
||||
import { isCachedObjectRecordConnection } from '@/apollo/optimistic-effect/utils/isCachedObjectRecordConnection';
|
||||
import { CachedObjectRecordEdge } from '@/apollo/types/CachedObjectRecordEdge';
|
||||
import { capitalize } from '~/utils/string/capitalize';
|
||||
|
||||
export const triggerAttachRelationOptimisticEffect = ({
|
||||
cache,
|
||||
objectNameSingular,
|
||||
recordId,
|
||||
relationObjectMetadataNameSingular,
|
||||
relationFieldName,
|
||||
relationRecordId,
|
||||
}: {
|
||||
cache: ApolloCache<unknown>;
|
||||
objectNameSingular: string;
|
||||
recordId: string;
|
||||
relationObjectMetadataNameSingular: string;
|
||||
relationFieldName: string;
|
||||
relationRecordId: string;
|
||||
}) => {
|
||||
const recordTypeName = capitalize(objectNameSingular);
|
||||
const relationRecordTypeName = capitalize(relationObjectMetadataNameSingular);
|
||||
|
||||
cache.modify<StoreObject>({
|
||||
id: cache.identify({
|
||||
id: relationRecordId,
|
||||
__typename: relationRecordTypeName,
|
||||
}),
|
||||
fields: {
|
||||
[relationFieldName]: (cachedFieldValue, { toReference }) => {
|
||||
const nodeReference = toReference({
|
||||
id: recordId,
|
||||
__typename: recordTypeName,
|
||||
});
|
||||
|
||||
if (!nodeReference) return cachedFieldValue;
|
||||
|
||||
if (
|
||||
isCachedObjectRecordConnection(objectNameSingular, cachedFieldValue)
|
||||
) {
|
||||
// To many objects => add record to next relation field list
|
||||
const nextEdges: CachedObjectRecordEdge[] = [
|
||||
...cachedFieldValue.edges,
|
||||
{
|
||||
__typename: `${recordTypeName}Edge`,
|
||||
node: nodeReference,
|
||||
cursor: '',
|
||||
},
|
||||
];
|
||||
return { ...cachedFieldValue, edges: nextEdges };
|
||||
}
|
||||
|
||||
// To one object => attach next relation record
|
||||
return nodeReference;
|
||||
},
|
||||
},
|
||||
});
|
||||
};
|
||||
@ -1,6 +1,6 @@
|
||||
import { ApolloCache, StoreObject } from '@apollo/client';
|
||||
|
||||
import { isCachedObjectConnection } from '@/apollo/optimistic-effect/utils/isCachedObjectConnection';
|
||||
import { isCachedObjectRecordConnection } from '@/apollo/optimistic-effect/utils/isCachedObjectRecordConnection';
|
||||
import { CachedObjectRecord } from '@/apollo/types/CachedObjectRecord';
|
||||
import { CachedObjectRecordEdge } from '@/apollo/types/CachedObjectRecordEdge';
|
||||
import { ObjectMetadataItem } from '@/object-metadata/types/ObjectMetadataItem';
|
||||
@ -36,7 +36,7 @@ export const triggerCreateRecordsOptimisticEffect = ({
|
||||
},
|
||||
) => {
|
||||
if (
|
||||
!isCachedObjectConnection(
|
||||
!isCachedObjectRecordConnection(
|
||||
objectMetadataItem.nameSingular,
|
||||
cachedConnection,
|
||||
)
|
||||
|
||||
@ -1,6 +1,6 @@
|
||||
import { ApolloCache, StoreObject } from '@apollo/client';
|
||||
|
||||
import { isCachedObjectConnection } from '@/apollo/optimistic-effect/utils/isCachedObjectConnection';
|
||||
import { isCachedObjectRecordConnection } from '@/apollo/optimistic-effect/utils/isCachedObjectRecordConnection';
|
||||
import { CachedObjectRecord } from '@/apollo/types/CachedObjectRecord';
|
||||
import { CachedObjectRecordEdge } from '@/apollo/types/CachedObjectRecordEdge';
|
||||
import { CachedObjectRecordQueryVariables } from '@/apollo/types/CachedObjectRecordQueryVariables';
|
||||
@ -24,7 +24,7 @@ export const triggerDeleteRecordsOptimisticEffect = ({
|
||||
{ DELETE, readField, storeFieldName },
|
||||
) => {
|
||||
if (
|
||||
!isCachedObjectConnection(
|
||||
!isCachedObjectRecordConnection(
|
||||
objectMetadataItem.nameSingular,
|
||||
cachedConnection,
|
||||
)
|
||||
|
||||
@ -0,0 +1,49 @@
|
||||
import { ApolloCache, StoreObject } from '@apollo/client';
|
||||
|
||||
import { isCachedObjectRecordConnection } from '@/apollo/optimistic-effect/utils/isCachedObjectRecordConnection';
|
||||
import { capitalize } from '~/utils/string/capitalize';
|
||||
|
||||
export const triggerDetachRelationOptimisticEffect = ({
|
||||
cache,
|
||||
objectNameSingular,
|
||||
recordId,
|
||||
relationObjectMetadataNameSingular,
|
||||
relationFieldName,
|
||||
relationRecordId,
|
||||
}: {
|
||||
cache: ApolloCache<unknown>;
|
||||
objectNameSingular: string;
|
||||
recordId: string;
|
||||
relationObjectMetadataNameSingular: string;
|
||||
relationFieldName: string;
|
||||
relationRecordId: string;
|
||||
}) => {
|
||||
const relationRecordTypeName = capitalize(relationObjectMetadataNameSingular);
|
||||
|
||||
cache.modify<StoreObject>({
|
||||
id: cache.identify({
|
||||
id: relationRecordId,
|
||||
__typename: relationRecordTypeName,
|
||||
}),
|
||||
fields: {
|
||||
[relationFieldName]: (cachedFieldValue, { isReference, readField }) => {
|
||||
// To many objects => remove record from previous relation field list
|
||||
if (
|
||||
isCachedObjectRecordConnection(objectNameSingular, cachedFieldValue)
|
||||
) {
|
||||
const nextEdges = cachedFieldValue.edges.filter(
|
||||
({ node }) => readField('id', node) !== recordId,
|
||||
);
|
||||
return { ...cachedFieldValue, edges: nextEdges };
|
||||
}
|
||||
|
||||
// To one object => detach previous relation record
|
||||
if (isReference(cachedFieldValue)) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return cachedFieldValue;
|
||||
},
|
||||
},
|
||||
});
|
||||
};
|
||||
@ -1,6 +1,6 @@
|
||||
import { ApolloCache, StoreObject } from '@apollo/client';
|
||||
|
||||
import { isCachedObjectConnection } from '@/apollo/optimistic-effect/utils/isCachedObjectConnection';
|
||||
import { isCachedObjectRecordConnection } from '@/apollo/optimistic-effect/utils/isCachedObjectRecordConnection';
|
||||
import { sortCachedObjectEdges } from '@/apollo/optimistic-effect/utils/sortCachedObjectEdges';
|
||||
import { CachedObjectRecord } from '@/apollo/types/CachedObjectRecord';
|
||||
import { CachedObjectRecordEdge } from '@/apollo/types/CachedObjectRecordEdge';
|
||||
@ -31,7 +31,7 @@ export const triggerUpdateRecordOptimisticEffect = ({
|
||||
{ DELETE, readField, storeFieldName, toReference },
|
||||
) => {
|
||||
if (
|
||||
!isCachedObjectConnection(
|
||||
!isCachedObjectRecordConnection(
|
||||
objectMetadataItem.nameSingular,
|
||||
cachedConnection,
|
||||
)
|
||||
|
||||
@ -1,86 +0,0 @@
|
||||
import { ApolloCache, StoreObject } from '@apollo/client';
|
||||
|
||||
import { isCachedObjectConnection } from '@/apollo/optimistic-effect/utils/isCachedObjectConnection';
|
||||
import { CachedObjectRecordEdge } from '@/apollo/types/CachedObjectRecordEdge';
|
||||
import { ObjectRecord } from '@/object-record/types/ObjectRecord';
|
||||
import { capitalize } from '~/utils/string/capitalize';
|
||||
|
||||
export type TriggerUpdateRelationFieldOptimisticEffectParams = {
|
||||
cache: ApolloCache<unknown>;
|
||||
objectNameSingular: string;
|
||||
record: ObjectRecord;
|
||||
relationObjectMetadataNameSingular: string;
|
||||
relationFieldName: string;
|
||||
previousRelationRecord: ObjectRecord | null;
|
||||
nextRelationRecord: ObjectRecord | null;
|
||||
};
|
||||
|
||||
export const triggerUpdateRelationFieldOptimisticEffect = ({
|
||||
cache,
|
||||
objectNameSingular,
|
||||
record,
|
||||
relationObjectMetadataNameSingular,
|
||||
relationFieldName,
|
||||
previousRelationRecord,
|
||||
nextRelationRecord,
|
||||
}: TriggerUpdateRelationFieldOptimisticEffectParams) => {
|
||||
const recordTypeName = capitalize(objectNameSingular);
|
||||
const relationRecordTypeName = capitalize(relationObjectMetadataNameSingular);
|
||||
|
||||
if (previousRelationRecord) {
|
||||
cache.modify<StoreObject>({
|
||||
id: cache.identify({
|
||||
...previousRelationRecord,
|
||||
__typename: relationRecordTypeName,
|
||||
}),
|
||||
fields: {
|
||||
[relationFieldName]: (cachedFieldValue, { isReference, readField }) => {
|
||||
// To many objects => remove record from previous relation field list
|
||||
if (isCachedObjectConnection(objectNameSingular, cachedFieldValue)) {
|
||||
const nextEdges = cachedFieldValue.edges.filter(
|
||||
({ node }) => readField('id', node) !== record.id,
|
||||
);
|
||||
return { ...cachedFieldValue, edges: nextEdges };
|
||||
}
|
||||
|
||||
// To one object => detach previous relation record
|
||||
if (isReference(cachedFieldValue)) {
|
||||
return null;
|
||||
}
|
||||
},
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
if (nextRelationRecord) {
|
||||
cache.modify<StoreObject>({
|
||||
id: cache.identify({
|
||||
...nextRelationRecord,
|
||||
__typename: relationRecordTypeName,
|
||||
}),
|
||||
fields: {
|
||||
[relationFieldName]: (cachedFieldValue, { toReference }) => {
|
||||
const nodeReference = toReference(record);
|
||||
|
||||
if (!nodeReference) return cachedFieldValue;
|
||||
|
||||
if (isCachedObjectConnection(objectNameSingular, cachedFieldValue)) {
|
||||
// To many objects => add record to next relation field list
|
||||
const nextEdges: CachedObjectRecordEdge[] = [
|
||||
...cachedFieldValue.edges,
|
||||
{
|
||||
__typename: `${recordTypeName}Edge`,
|
||||
node: nodeReference,
|
||||
cursor: '',
|
||||
},
|
||||
];
|
||||
return { ...cachedFieldValue, edges: nextEdges };
|
||||
}
|
||||
|
||||
// To one object => attach next relation record
|
||||
return nodeReference;
|
||||
},
|
||||
},
|
||||
});
|
||||
}
|
||||
};
|
||||
@ -0,0 +1,70 @@
|
||||
import { ReactNode, useEffect } from 'react';
|
||||
import { MockedProvider } from '@apollo/client/testing';
|
||||
import { renderHook } from '@testing-library/react';
|
||||
import { RecoilRoot, useSetRecoilState } from 'recoil';
|
||||
|
||||
import { useGetRelationMetadata } from '@/object-metadata/hooks/useGetRelationMetadata';
|
||||
import { objectMetadataItemsState } from '@/object-metadata/states/objectMetadataItemsState';
|
||||
import { getObjectMetadataItemsMock } from '@/object-metadata/utils/getObjectMetadataItemsMock';
|
||||
|
||||
import { TestApolloMetadataClientProvider } from '../__mocks__/ApolloMetadataClientProvider';
|
||||
|
||||
const Wrapper = ({ children }: { children: ReactNode }) => (
|
||||
<RecoilRoot>
|
||||
<MockedProvider addTypename={false}>
|
||||
<TestApolloMetadataClientProvider>
|
||||
{children}
|
||||
</TestApolloMetadataClientProvider>
|
||||
</MockedProvider>
|
||||
</RecoilRoot>
|
||||
);
|
||||
|
||||
describe('useGetRelationMetadata', () => {
|
||||
it('should return correct properties', async () => {
|
||||
const objectMetadataItems = getObjectMetadataItemsMock();
|
||||
const objectMetadata = objectMetadataItems.find(
|
||||
(item) => item.nameSingular === 'person',
|
||||
)!;
|
||||
const fieldMetadataItem = objectMetadata.fields.find(
|
||||
(field) => field.name === 'opportunities',
|
||||
)!;
|
||||
|
||||
const { result } = renderHook(
|
||||
() => {
|
||||
const setMetadataItems = useSetRecoilState(objectMetadataItemsState);
|
||||
|
||||
useEffect(() => {
|
||||
setMetadataItems(objectMetadataItems);
|
||||
}, [setMetadataItems]);
|
||||
|
||||
return useGetRelationMetadata();
|
||||
},
|
||||
{
|
||||
wrapper: Wrapper,
|
||||
initialProps: {},
|
||||
},
|
||||
);
|
||||
|
||||
const {
|
||||
relationFieldMetadataItem,
|
||||
relationObjectMetadataItem,
|
||||
relationType,
|
||||
} = result.current({ fieldMetadataItem }) ?? {};
|
||||
|
||||
const expectedRelationObjectMetadataItem = objectMetadataItems.find(
|
||||
(item) => item.nameSingular === 'opportunity',
|
||||
);
|
||||
const expectedRelationFieldMetadataItem =
|
||||
expectedRelationObjectMetadataItem?.fields.find(
|
||||
(field) => field.name === 'person',
|
||||
);
|
||||
|
||||
expect(relationObjectMetadataItem).toEqual(
|
||||
expectedRelationObjectMetadataItem,
|
||||
);
|
||||
expect(relationFieldMetadataItem).toEqual(
|
||||
expectedRelationFieldMetadataItem,
|
||||
);
|
||||
expect(relationType).toBe('ONE_TO_MANY');
|
||||
});
|
||||
});
|
||||
@ -1,55 +0,0 @@
|
||||
import { ReactNode } from 'react';
|
||||
import { MockedProvider } from '@apollo/client/testing';
|
||||
import { renderHook } from '@testing-library/react';
|
||||
import { RecoilRoot } from 'recoil';
|
||||
|
||||
import { useRelationMetadata } from '@/object-metadata/hooks/useRelationMetadata';
|
||||
import { FieldMetadataItem } from '@/object-metadata/types/FieldMetadataItem';
|
||||
import { getObjectMetadataItemsMock } from '@/object-metadata/utils/getObjectMetadataItemsMock';
|
||||
|
||||
import { TestApolloMetadataClientProvider } from '../__mocks__/ApolloMetadataClientProvider';
|
||||
|
||||
const Wrapper = ({ children }: { children: ReactNode }) => (
|
||||
<RecoilRoot>
|
||||
<MockedProvider addTypename={false}>
|
||||
<TestApolloMetadataClientProvider>
|
||||
{children}
|
||||
</TestApolloMetadataClientProvider>
|
||||
</MockedProvider>
|
||||
</RecoilRoot>
|
||||
);
|
||||
|
||||
describe('useRelationMetadata', () => {
|
||||
it('should return correct properties', async () => {
|
||||
const { result, rerender } = renderHook(
|
||||
({ fieldMetadataItem }: { fieldMetadataItem?: FieldMetadataItem }) =>
|
||||
useRelationMetadata({ fieldMetadataItem }),
|
||||
{
|
||||
wrapper: Wrapper,
|
||||
initialProps: {},
|
||||
},
|
||||
);
|
||||
|
||||
const {
|
||||
relationFieldMetadataItem,
|
||||
relationObjectMetadataItem,
|
||||
relationType,
|
||||
} = result.current;
|
||||
|
||||
expect(relationFieldMetadataItem).toBeUndefined();
|
||||
expect(relationObjectMetadataItem).toBeUndefined();
|
||||
expect(relationType).toBeUndefined();
|
||||
|
||||
const objectMetadataItems = getObjectMetadataItemsMock();
|
||||
const objectMetadata = objectMetadataItems.find(
|
||||
(item) => item.nameSingular === 'person',
|
||||
)!;
|
||||
const fieldMetadataItem = objectMetadata.fields.find(
|
||||
(field) => field.name === 'opportunities',
|
||||
)!;
|
||||
|
||||
rerender({ fieldMetadataItem });
|
||||
|
||||
expect(result.current.relationType).toBe('ONE_TO_MANY');
|
||||
});
|
||||
});
|
||||
@ -0,0 +1,67 @@
|
||||
import { useRecoilCallback } from 'recoil';
|
||||
|
||||
import { objectMetadataItemFamilySelector } from '@/object-metadata/states/objectMetadataItemFamilySelector';
|
||||
import { RelationType } from '@/settings/data-model/types/RelationType';
|
||||
import {
|
||||
FieldMetadataType,
|
||||
RelationMetadataType,
|
||||
} from '~/generated-metadata/graphql';
|
||||
|
||||
import { FieldMetadataItem } from '../types/FieldMetadataItem';
|
||||
|
||||
export const useGetRelationMetadata = () =>
|
||||
useRecoilCallback(
|
||||
({ snapshot }) =>
|
||||
({ fieldMetadataItem }: { fieldMetadataItem: FieldMetadataItem }) => {
|
||||
if (fieldMetadataItem.type !== FieldMetadataType.Relation) return null;
|
||||
|
||||
const relationMetadata =
|
||||
fieldMetadataItem.fromRelationMetadata ||
|
||||
fieldMetadataItem.toRelationMetadata;
|
||||
|
||||
if (!relationMetadata) return null;
|
||||
|
||||
const relationFieldMetadataId =
|
||||
'toFieldMetadataId' in relationMetadata
|
||||
? relationMetadata.toFieldMetadataId
|
||||
: relationMetadata.fromFieldMetadataId;
|
||||
|
||||
if (!relationFieldMetadataId) return null;
|
||||
|
||||
const relationType =
|
||||
relationMetadata.relationType === RelationMetadataType.OneToMany &&
|
||||
fieldMetadataItem.toRelationMetadata
|
||||
? 'MANY_TO_ONE'
|
||||
: (relationMetadata.relationType as RelationType);
|
||||
|
||||
const relationObjectMetadataNameSingular =
|
||||
'toObjectMetadata' in relationMetadata
|
||||
? relationMetadata.toObjectMetadata.nameSingular
|
||||
: relationMetadata.fromObjectMetadata.nameSingular;
|
||||
|
||||
const relationObjectMetadataItem = snapshot
|
||||
.getLoadable(
|
||||
objectMetadataItemFamilySelector({
|
||||
objectName: relationObjectMetadataNameSingular,
|
||||
objectNameType: 'singular',
|
||||
}),
|
||||
)
|
||||
.valueOrThrow();
|
||||
|
||||
if (!relationObjectMetadataItem) return null;
|
||||
|
||||
const relationFieldMetadataItem =
|
||||
relationObjectMetadataItem.fields.find(
|
||||
(field) => field.id === relationFieldMetadataId,
|
||||
);
|
||||
|
||||
if (!relationFieldMetadataItem) return null;
|
||||
|
||||
return {
|
||||
relationFieldMetadataItem,
|
||||
relationObjectMetadataItem,
|
||||
relationType,
|
||||
};
|
||||
},
|
||||
[],
|
||||
);
|
||||
@ -1,49 +0,0 @@
|
||||
import { RelationType } from '@/settings/data-model/types/RelationType';
|
||||
import { RelationMetadataType } from '~/generated-metadata/graphql';
|
||||
|
||||
import { useObjectMetadataItemForSettings } from '../hooks/useObjectMetadataItemForSettings';
|
||||
import { FieldMetadataItem } from '../types/FieldMetadataItem';
|
||||
|
||||
export const useRelationMetadata = ({
|
||||
fieldMetadataItem,
|
||||
}: {
|
||||
fieldMetadataItem?: FieldMetadataItem;
|
||||
}) => {
|
||||
const { findObjectMetadataItemById } = useObjectMetadataItemForSettings();
|
||||
|
||||
const relationMetadata =
|
||||
fieldMetadataItem?.fromRelationMetadata ||
|
||||
fieldMetadataItem?.toRelationMetadata;
|
||||
|
||||
const relationType =
|
||||
relationMetadata?.relationType === RelationMetadataType.OneToMany &&
|
||||
fieldMetadataItem?.toRelationMetadata
|
||||
? 'MANY_TO_ONE'
|
||||
: (relationMetadata?.relationType as RelationType | undefined);
|
||||
|
||||
const relationObjectMetadataId =
|
||||
relationMetadata && 'toObjectMetadata' in relationMetadata
|
||||
? relationMetadata.toObjectMetadata.id
|
||||
: relationMetadata?.fromObjectMetadata.id;
|
||||
|
||||
const relationObjectMetadataItem = relationObjectMetadataId
|
||||
? findObjectMetadataItemById(relationObjectMetadataId)
|
||||
: undefined;
|
||||
|
||||
const relationFieldMetadataId =
|
||||
relationMetadata && 'toFieldMetadataId' in relationMetadata
|
||||
? relationMetadata.toFieldMetadataId
|
||||
: relationMetadata?.fromFieldMetadataId;
|
||||
|
||||
const relationFieldMetadataItem = relationFieldMetadataId
|
||||
? relationObjectMetadataItem?.fields?.find(
|
||||
(field) => field.id === relationFieldMetadataId,
|
||||
)
|
||||
: undefined;
|
||||
|
||||
return {
|
||||
relationFieldMetadataItem,
|
||||
relationObjectMetadataItem,
|
||||
relationType,
|
||||
};
|
||||
};
|
||||
@ -15,6 +15,7 @@ export const useGetRecordFromCache = ({
|
||||
|
||||
return <CachedObjectRecord extends ObjectRecord = ObjectRecord>(
|
||||
recordId: string,
|
||||
cache = apolloClient.cache,
|
||||
) => {
|
||||
if (!objectMetadataItem) {
|
||||
return null;
|
||||
@ -31,7 +32,6 @@ export const useGetRecordFromCache = ({
|
||||
}
|
||||
`;
|
||||
|
||||
const cache = apolloClient.cache;
|
||||
const cachedRecordId = cache.identify({
|
||||
__typename: capitalize(objectMetadataItem.nameSingular),
|
||||
id: recordId,
|
||||
|
||||
@ -1,9 +1,14 @@
|
||||
import { useApolloClient } from '@apollo/client';
|
||||
|
||||
import { isObjectRecordConnection } from '@/apollo/optimistic-effect/utils/isObjectRecordConnection';
|
||||
import { triggerDeleteRecordsOptimisticEffect } from '@/apollo/optimistic-effect/utils/triggerDeleteRecordsOptimisticEffect';
|
||||
import { triggerDetachRelationOptimisticEffect } from '@/apollo/optimistic-effect/utils/triggerDetachRelationOptimisticEffect';
|
||||
import { useGetRelationMetadata } from '@/object-metadata/hooks/useGetRelationMetadata';
|
||||
import { useObjectMetadataItem } from '@/object-metadata/hooks/useObjectMetadataItem';
|
||||
import { getDeleteManyRecordsMutationResponseField } from '@/object-record/hooks/useGenerateDeleteManyRecordMutation';
|
||||
import { ObjectRecordQueryFilter } from '@/object-record/record-filter/types/ObjectRecordQueryFilter';
|
||||
import { ObjectRecord } from '@/object-record/types/ObjectRecord';
|
||||
import { ObjectRecordConnection } from '@/object-record/types/ObjectRecordConnection';
|
||||
import { isDefined } from '~/utils/isDefined';
|
||||
import { capitalize } from '~/utils/string/capitalize';
|
||||
|
||||
type useDeleteOneRecordProps = {
|
||||
@ -14,9 +19,11 @@ type useDeleteOneRecordProps = {
|
||||
export const useDeleteManyRecords = ({
|
||||
objectNameSingular,
|
||||
}: useDeleteOneRecordProps) => {
|
||||
const { objectMetadataItem, deleteManyRecordsMutation } =
|
||||
const { objectMetadataItem, deleteManyRecordsMutation, getRecordFromCache } =
|
||||
useObjectMetadataItem({ objectNameSingular });
|
||||
|
||||
const getRelationMetadata = useGetRelationMetadata();
|
||||
|
||||
const apolloClient = useApolloClient();
|
||||
|
||||
const mutationResponseField = getDeleteManyRecordsMutationResponseField(
|
||||
@ -24,16 +31,10 @@ export const useDeleteManyRecords = ({
|
||||
);
|
||||
|
||||
const deleteManyRecords = async (idsToDelete: string[]) => {
|
||||
const deleteRecordFilter: ObjectRecordQueryFilter = {
|
||||
id: {
|
||||
in: idsToDelete,
|
||||
},
|
||||
};
|
||||
const deletedRecords = await apolloClient.mutate({
|
||||
mutation: deleteManyRecordsMutation,
|
||||
variables: {
|
||||
filter: deleteRecordFilter,
|
||||
// atMost: idsToDelete.length,
|
||||
filter: { id: { in: idsToDelete } },
|
||||
},
|
||||
optimisticResponse: {
|
||||
[mutationResponseField]: idsToDelete.map((idToDelete) => ({
|
||||
@ -46,10 +47,49 @@ export const useDeleteManyRecords = ({
|
||||
|
||||
if (!records?.length) return;
|
||||
|
||||
objectMetadataItem.fields.forEach((fieldMetadataItem) => {
|
||||
const relationMetadata = getRelationMetadata({ fieldMetadataItem });
|
||||
|
||||
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({
|
||||
cache,
|
||||
objectMetadataItem,
|
||||
records,
|
||||
records: records,
|
||||
});
|
||||
},
|
||||
});
|
||||
|
||||
@ -1,9 +1,15 @@
|
||||
import { useCallback } from 'react';
|
||||
import { useApolloClient } from '@apollo/client';
|
||||
|
||||
import { isObjectRecordConnection } from '@/apollo/optimistic-effect/utils/isObjectRecordConnection';
|
||||
import { triggerDeleteRecordsOptimisticEffect } from '@/apollo/optimistic-effect/utils/triggerDeleteRecordsOptimisticEffect';
|
||||
import { triggerDetachRelationOptimisticEffect } from '@/apollo/optimistic-effect/utils/triggerDetachRelationOptimisticEffect';
|
||||
import { useGetRelationMetadata } from '@/object-metadata/hooks/useGetRelationMetadata';
|
||||
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 { isDefined } from '~/utils/isDefined';
|
||||
import { capitalize } from '~/utils/string/capitalize';
|
||||
|
||||
type useDeleteOneRecordProps = {
|
||||
@ -14,9 +20,10 @@ type useDeleteOneRecordProps = {
|
||||
export const useDeleteOneRecord = ({
|
||||
objectNameSingular,
|
||||
}: useDeleteOneRecordProps) => {
|
||||
const { objectMetadataItem, deleteOneRecordMutation } = useObjectMetadataItem(
|
||||
{ objectNameSingular },
|
||||
);
|
||||
const { objectMetadataItem, deleteOneRecordMutation, getRecordFromCache } =
|
||||
useObjectMetadataItem({ objectNameSingular });
|
||||
|
||||
const getRelationMetadata = useGetRelationMetadata();
|
||||
|
||||
const apolloClient = useApolloClient();
|
||||
|
||||
@ -39,6 +46,43 @@ export const useDeleteOneRecord = ({
|
||||
|
||||
if (!record) return;
|
||||
|
||||
objectMetadataItem.fields.forEach((fieldMetadataItem) => {
|
||||
const relationMetadata = getRelationMetadata({ fieldMetadataItem });
|
||||
|
||||
if (!relationMetadata) 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({
|
||||
cache,
|
||||
objectMetadataItem,
|
||||
@ -52,6 +96,8 @@ export const useDeleteOneRecord = ({
|
||||
[
|
||||
apolloClient,
|
||||
deleteOneRecordMutation,
|
||||
getRecordFromCache,
|
||||
getRelationMetadata,
|
||||
mutationResponseField,
|
||||
objectMetadataItem,
|
||||
objectNameSingular,
|
||||
|
||||
@ -1,12 +1,15 @@
|
||||
import { useApolloClient } from '@apollo/client';
|
||||
|
||||
import { useGetRelationFieldsToOptimisticallyUpdate } from '@/apollo/optimistic-effect/hooks/useGetRelationFieldsToOptimisticallyUpdate';
|
||||
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 { triggerUpdateRelationFieldOptimisticEffect } from '@/apollo/optimistic-effect/utils/triggerUpdateRelationFieldOptimisticEffect';
|
||||
import { useGetRelationMetadata } from '@/object-metadata/hooks/useGetRelationMetadata';
|
||||
import { useObjectMetadataItem } from '@/object-metadata/hooks/useObjectMetadataItem';
|
||||
import { getUpdateOneRecordMutationResponseField } from '@/object-record/hooks/useGenerateUpdateOneRecordMutation';
|
||||
import { ObjectRecord } from '@/object-record/types/ObjectRecord';
|
||||
import { sanitizeRecordInput } from '@/object-record/utils/sanitizeRecordInput';
|
||||
import { isDeeplyEqual } from '~/utils/isDeeplyEqual';
|
||||
import { capitalize } from '~/utils/string/capitalize';
|
||||
|
||||
type useUpdateOneRecordProps = {
|
||||
@ -21,8 +24,7 @@ export const useUpdateOneRecord = <
|
||||
const { objectMetadataItem, updateOneRecordMutation, getRecordFromCache } =
|
||||
useObjectMetadataItem({ objectNameSingular });
|
||||
|
||||
const getRelationFieldsToOptimisticallyUpdate =
|
||||
useGetRelationFieldsToOptimisticallyUpdate();
|
||||
const getRelationMetadata = useGetRelationMetadata();
|
||||
|
||||
const apolloClient = useApolloClient();
|
||||
|
||||
@ -48,14 +50,6 @@ export const useUpdateOneRecord = <
|
||||
id: idToUpdate,
|
||||
};
|
||||
|
||||
const updatedRelationFields = cachedRecord
|
||||
? getRelationFieldsToOptimisticallyUpdate({
|
||||
cachedRecord,
|
||||
objectMetadataItem,
|
||||
updateRecordInput: updateOneRecordInput,
|
||||
})
|
||||
: [];
|
||||
|
||||
const mutationResponseField =
|
||||
getUpdateOneRecordMutationResponseField(objectNameSingular);
|
||||
|
||||
@ -73,29 +67,59 @@ export const useUpdateOneRecord = <
|
||||
|
||||
if (!record) 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({
|
||||
cache,
|
||||
objectMetadataItem,
|
||||
record,
|
||||
});
|
||||
|
||||
updatedRelationFields.forEach(
|
||||
({
|
||||
relationObjectMetadataNameSingular,
|
||||
relationFieldName,
|
||||
previousRelationRecord,
|
||||
nextRelationRecord,
|
||||
}) =>
|
||||
triggerUpdateRelationFieldOptimisticEffect({
|
||||
cache,
|
||||
objectNameSingular,
|
||||
record,
|
||||
relationObjectMetadataNameSingular,
|
||||
relationFieldName,
|
||||
previousRelationRecord,
|
||||
nextRelationRecord,
|
||||
}),
|
||||
);
|
||||
},
|
||||
});
|
||||
|
||||
|
||||
@ -1,9 +1,9 @@
|
||||
import { ReactNode } from 'react';
|
||||
import { ReactNode, useMemo } from 'react';
|
||||
import { useNavigate } from 'react-router-dom';
|
||||
import { useTheme } from '@emotion/react';
|
||||
import styled from '@emotion/styled';
|
||||
|
||||
import { useRelationMetadata } from '@/object-metadata/hooks/useRelationMetadata';
|
||||
import { useGetRelationMetadata } from '@/object-metadata/hooks/useGetRelationMetadata';
|
||||
import { FieldMetadataItem } from '@/object-metadata/types/FieldMetadataItem';
|
||||
import { getObjectSlug } from '@/object-metadata/utils/getObjectSlug';
|
||||
import { FieldIdentifierType } from '@/settings/data-model/types/FieldIdentifierType';
|
||||
@ -53,9 +53,13 @@ export const SettingsObjectFieldItemTableRow = ({
|
||||
const fieldDataTypeIsSupported =
|
||||
fieldMetadataItem.type in settingsFieldMetadataTypes;
|
||||
|
||||
const { relationObjectMetadataItem, relationType } = useRelationMetadata({
|
||||
fieldMetadataItem,
|
||||
});
|
||||
const getRelationMetadata = useGetRelationMetadata();
|
||||
|
||||
const { relationObjectMetadataItem, relationType } =
|
||||
useMemo(
|
||||
() => getRelationMetadata({ fieldMetadataItem }),
|
||||
[fieldMetadataItem, getRelationMetadata],
|
||||
) ?? {};
|
||||
|
||||
if (!fieldDataTypeIsSupported) return null;
|
||||
|
||||
|
||||
@ -1,9 +1,9 @@
|
||||
import { useEffect } from 'react';
|
||||
import { useEffect, useMemo } from 'react';
|
||||
import { useNavigate, useParams } from 'react-router-dom';
|
||||
|
||||
import { useFieldMetadataItem } from '@/object-metadata/hooks/useFieldMetadataItem';
|
||||
import { useGetRelationMetadata } from '@/object-metadata/hooks/useGetRelationMetadata';
|
||||
import { useObjectMetadataItemForSettings } from '@/object-metadata/hooks/useObjectMetadataItemForSettings';
|
||||
import { useRelationMetadata } from '@/object-metadata/hooks/useRelationMetadata';
|
||||
import { getFieldSlug } from '@/object-metadata/utils/getFieldSlug';
|
||||
import { isLabelIdentifierField } from '@/object-metadata/utils/isLabelIdentifierField';
|
||||
import { SaveAndCancelButtons } from '@/settings/components/SaveAndCancelButtons/SaveAndCancelButtons';
|
||||
@ -43,11 +43,21 @@ export const SettingsObjectFieldEdit = () => {
|
||||
metadataField.isActive && getFieldSlug(metadataField) === fieldSlug,
|
||||
);
|
||||
|
||||
const getRelationMetadata = useGetRelationMetadata();
|
||||
const {
|
||||
relationFieldMetadataItem,
|
||||
relationObjectMetadataItem,
|
||||
relationType,
|
||||
} = useRelationMetadata({ fieldMetadataItem: activeMetadataField });
|
||||
} =
|
||||
useMemo(
|
||||
() =>
|
||||
activeMetadataField
|
||||
? getRelationMetadata({
|
||||
fieldMetadataItem: activeMetadataField,
|
||||
})
|
||||
: null,
|
||||
[activeMetadataField, getRelationMetadata],
|
||||
) ?? {};
|
||||
|
||||
const {
|
||||
formValues,
|
||||
|
||||
Reference in New Issue
Block a user