Feat/record optimistic effect (#3076)

* WIP

* WIP

* POC working on hard coded completedAt field

* Finished isRecordMatchingFilter, mock of pg_graphql filtering mechanism

* Fixed and cleaned

* Unregister unused optimistic effects

* Fix lint

* Fixes from review

---------

Co-authored-by: Charles Bochet <charles@twenty.com>
This commit is contained in:
Lucas Bordeau
2023-12-20 20:31:48 +01:00
committed by GitHub
parent a5f28b4395
commit 687c9131f4
37 changed files with 2309 additions and 233 deletions

View File

@ -1,21 +1,20 @@
import {
ApolloCache,
DocumentNode,
OperationVariables,
useApolloClient,
} from '@apollo/client';
import { useApolloClient } from '@apollo/client';
import { isNonEmptyArray } from '@sniptt/guards';
import { useRecoilCallback } from 'recoil';
import { computeOptimisticEffectKey } from '@/apollo/optimistic-effect/utils/computeOptimisticEffectKey';
import {
EMPTY_QUERY,
useObjectMetadataItem,
} from '@/object-metadata/hooks/useObjectMetadataItem';
import { ObjectMetadataItem } from '@/object-metadata/types/ObjectMetadataItem';
import { ObjectMetadataItemIdentifier } from '@/object-metadata/types/ObjectMetadataItemIdentifier';
import { ObjectRecordQueryVariables } from '@/object-record/types/ObjectRecordQueryVariables';
import { optimisticEffectState } from '../states/optimisticEffectState';
import { OptimisticEffect } from '../types/internal/OptimisticEffect';
import {
OptimisticEffect,
OptimisticEffectWriter,
} from '../types/internal/OptimisticEffect';
import { OptimisticEffectDefinition } from '../types/OptimisticEffectDefinition';
export const useOptimisticEffect = ({
@ -23,17 +22,41 @@ export const useOptimisticEffect = ({
}: ObjectMetadataItemIdentifier) => {
const apolloClient = useApolloClient();
const { findManyRecordsQuery } = useObjectMetadataItem({
const { findManyRecordsQuery, objectMetadataItem } = useObjectMetadataItem({
objectNameSingular,
});
const registerOptimisticEffect = useRecoilCallback(
const unregisterOptimisticEffect = useRecoilCallback(
({ snapshot, set }) =>
<T>({
({
variables,
definition,
}: {
variables: OperationVariables;
variables: ObjectRecordQueryVariables;
definition: OptimisticEffectDefinition;
}) => {
const optimisticEffects = snapshot
.getLoadable(optimisticEffectState)
.getValue();
const computedKey = computeOptimisticEffectKey({
variables,
definition,
});
const { [computedKey]: _, ...rest } = optimisticEffects;
set(optimisticEffectState, rest);
},
);
const registerOptimisticEffect = useRecoilCallback(
({ snapshot, set }) =>
({
variables,
definition,
}: {
variables: ObjectRecordQueryVariables;
definition: OptimisticEffectDefinition;
}) => {
if (findManyRecordsQuery === EMPTY_QUERY) {
@ -46,21 +69,14 @@ export const useOptimisticEffect = ({
.getLoadable(optimisticEffectState)
.getValue();
const optimisticEffectWriter = ({
const optimisticEffectWriter: OptimisticEffectWriter = ({
cache,
newData,
createdRecords,
updatedRecords,
deletedRecordIds,
query,
variables,
objectMetadataItem,
}: {
cache: ApolloCache<unknown>;
newData: unknown;
deletedRecordIds?: string[];
variables: OperationVariables;
query: DocumentNode;
isUsingFlexibleBackend?: boolean;
objectMetadataItem?: ObjectMetadataItem;
}) => {
if (objectMetadataItem) {
const existingData = cache.readQuery({
@ -77,10 +93,11 @@ export const useOptimisticEffect = ({
variables,
data: {
[objectMetadataItem.namePlural]: definition.resolver({
currentData: (existingData as any)?.[
currentCacheData: (existingData as any)?.[
objectMetadataItem.namePlural
],
newData,
updatedRecords,
createdRecords,
deletedRecordIds,
variables,
}),
@ -91,7 +108,7 @@ export const useOptimisticEffect = ({
}
const existingData = cache.readQuery({
query,
query: query ?? findManyRecordsQuery,
variables,
});
@ -100,26 +117,40 @@ export const useOptimisticEffect = ({
}
};
const computedKey = computeOptimisticEffectKey({
variables,
definition,
});
const optimisticEffect = {
key: definition.key,
variables,
typename: definition.typename,
query: definition.query,
writer: optimisticEffectWriter,
objectMetadataItem: definition.objectMetadataItem,
isUsingFlexibleBackend: definition.isUsingFlexibleBackend,
} satisfies OptimisticEffect<T>;
objectMetadataItem,
} satisfies OptimisticEffect;
set(optimisticEffectState, {
...optimisticEffects,
[definition.key]: optimisticEffect,
[computedKey]: optimisticEffect,
});
},
[findManyRecordsQuery, objectNameSingular, objectMetadataItem],
);
const triggerOptimisticEffects = useRecoilCallback(
({ snapshot }) =>
(typename: string, newData: unknown, deletedRecordIds?: string[]) => {
({
typename,
createdRecords,
updatedRecords,
deletedRecordIds,
}: {
typename: string;
createdRecords?: Record<string, unknown>[];
updatedRecords?: Record<string, unknown>[];
deletedRecordIds?: string[];
}) => {
const optimisticEffects = snapshot
.getLoadable(optimisticEffectState)
.getValue();
@ -127,20 +158,26 @@ export const useOptimisticEffect = ({
for (const optimisticEffect of Object.values(optimisticEffects)) {
// We need to update the typename when createObject type differs from listObject types
// It is the case for apiKey, where the creation route returns an ApiKeyToken type
const formattedNewData = isNonEmptyArray(newData)
? newData.map((data: any) => {
const formattedCreatedRecords = isNonEmptyArray(createdRecords)
? createdRecords.map((data: any) => {
return { ...data, __typename: typename };
})
: newData;
: [];
const formattedUpdatedRecords = isNonEmptyArray(updatedRecords)
? updatedRecords.map((data: any) => {
return { ...data, __typename: typename };
})
: [];
if (optimisticEffect.typename === typename) {
optimisticEffect.writer({
cache: apolloClient.cache,
query: optimisticEffect.query ?? ({} as DocumentNode),
newData: formattedNewData,
query: optimisticEffect.query,
createdRecords: formattedCreatedRecords,
updatedRecords: formattedUpdatedRecords,
deletedRecordIds,
variables: optimisticEffect.variables,
isUsingFlexibleBackend: optimisticEffect.isUsingFlexibleBackend,
objectMetadataItem: optimisticEffect.objectMetadataItem,
});
}
@ -152,5 +189,6 @@ export const useOptimisticEffect = ({
return {
registerOptimisticEffect,
triggerOptimisticEffects,
unregisterOptimisticEffect,
};
};

View File

@ -2,9 +2,7 @@ import { atom } from 'recoil';
import { OptimisticEffect } from '../types/internal/OptimisticEffect';
export const optimisticEffectState = atom<
Record<string, OptimisticEffect<unknown>>
>({
export const optimisticEffectState = atom<Record<string, OptimisticEffect>>({
key: 'optimisticEffectState',
default: {},
});

View File

@ -5,10 +5,8 @@ import { ObjectMetadataItem } from '@/object-metadata/types/ObjectMetadataItem';
import { OptimisticEffectResolver } from './OptimisticEffectResolver';
export type OptimisticEffectDefinition = {
key: string;
query?: DocumentNode;
typename: string;
resolver: OptimisticEffectResolver;
objectMetadataItem?: ObjectMetadataItem;
isUsingFlexibleBackend?: boolean;
};

View File

@ -1,13 +1,15 @@
import { OperationVariables } from '@apollo/client';
export type OptimisticEffectResolver = ({
currentData,
newData,
currentCacheData,
createdRecords,
updatedRecords,
deletedRecordIds,
variables,
}: {
currentData: any; //TODO: Change when decommissioning v1
newData: any; //TODO: Change when decommissioning v1
currentCacheData: any; //TODO: Change when decommissioning v1
createdRecords?: Record<string, unknown>[];
updatedRecords?: Record<string, unknown>[];
deletedRecordIds?: string[];
variables: OperationVariables;
}) => void;

View File

@ -1,28 +1,30 @@
import { ApolloCache, DocumentNode, OperationVariables } from '@apollo/client';
import { ApolloCache, DocumentNode } from '@apollo/client';
import { ObjectMetadataItem } from '@/object-metadata/types/ObjectMetadataItem';
import { ObjectRecordQueryVariables } from '@/object-record/types/ObjectRecordQueryVariables';
type OptimisticEffectWriter<T> = ({
export type OptimisticEffectWriter = ({
cache,
newData,
variables,
query,
createdRecords,
updatedRecords,
deletedRecordIds,
variables,
objectMetadataItem,
}: {
cache: ApolloCache<T>;
query: DocumentNode;
newData: T;
cache: ApolloCache<any>;
query?: DocumentNode;
createdRecords?: Record<string, unknown>[];
updatedRecords?: Record<string, unknown>[];
deletedRecordIds?: string[];
variables: OperationVariables;
objectMetadataItem?: ObjectMetadataItem;
isUsingFlexibleBackend?: boolean;
variables: ObjectRecordQueryVariables;
objectMetadataItem: ObjectMetadataItem;
}) => void;
export type OptimisticEffect<T> = {
key: string;
export type OptimisticEffect = {
query?: DocumentNode;
typename: string;
variables: OperationVariables;
writer: OptimisticEffectWriter<T>;
objectMetadataItem?: ObjectMetadataItem;
isUsingFlexibleBackend?: boolean;
variables: ObjectRecordQueryVariables;
writer: OptimisticEffectWriter;
objectMetadataItem: ObjectMetadataItem;
};

View File

@ -0,0 +1,17 @@
import { OptimisticEffectDefinition } from '@/apollo/optimistic-effect/types/OptimisticEffectDefinition';
import { ObjectRecordQueryVariables } from '@/object-record/types/ObjectRecordQueryVariables';
export const computeOptimisticEffectKey = ({
variables,
definition,
}: {
variables: ObjectRecordQueryVariables;
definition: OptimisticEffectDefinition;
}) => {
const computedKey =
(definition.objectMetadataItem?.namePlural ?? definition.typename) +
'-' +
JSON.stringify(variables);
return computedKey;
};